Skip to content

Commit 5952c73

Browse files
authored
Merge pull request #11 from zelti/docs/local-ci-harness
Keep the local CI harness, and document it
2 parents 61d7610 + a22e803 commit 5952c73

7 files changed

Lines changed: 161 additions & 3 deletions

File tree

0 Bytes
Binary file not shown.
3.65 KB
Binary file not shown.

‎.github/scripts/run-step.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
"""Run one CI step's `run:` block locally, verbatim.
3+
4+
Verbatim is the point. Every CI failure this project has had came from checking
5+
a hand-typed variant of a step rather than the step itself: a flag passed
6+
locally but not in CI, a value assumed to be set by an earlier step, a pipeline
7+
that behaves differently under `pipefail`.
8+
9+
.github/scripts/run-step.py # list the steps
10+
.github/scripts/run-step.py "the forge command works"
11+
.github/scripts/run-step.py -x "<name>" # trace each command
12+
13+
Needs the stack running (`forge start`). Steps are not independent: some rely on
14+
fixtures an earlier step creates, so run those first when a step fails on
15+
missing files. See the Contributing section of the README.
16+
"""
17+
import subprocess
18+
import sys
19+
20+
try:
21+
import yaml
22+
except ImportError:
23+
raise SystemExit(
24+
"This needs PyYAML to read the workflow file.\n"
25+
" Debian/Ubuntu: sudo apt install python3-yaml\n"
26+
" Arch: sudo pacman -S python-yaml\n"
27+
" Any: pip install --user pyyaml"
28+
)
29+
30+
WORKFLOW = ".github/workflows/ci.yml"
31+
32+
33+
def steps():
34+
with open(WORKFLOW, encoding="utf-8") as fh:
35+
doc = yaml.safe_load(fh)
36+
for job_name, job in doc["jobs"].items():
37+
for step in job.get("steps", []):
38+
if step.get("name") and step.get("run"):
39+
yield job_name, step["name"], step["run"]
40+
41+
42+
def main():
43+
args = [a for a in sys.argv[1:]]
44+
trace = False
45+
if args and args[0] in ("-x", "--trace"):
46+
trace = True
47+
args = args[1:]
48+
49+
found = list(steps())
50+
if not args:
51+
print("Steps that can be run:\n")
52+
current = None
53+
for job, name, _ in found:
54+
if job != current:
55+
print(" [%s]" % job)
56+
current = job
57+
print(" %s" % name)
58+
print("\nRun one with: %s \"<name>\"" % sys.argv[0])
59+
return 0
60+
61+
want = args[0]
62+
for _, name, body in found:
63+
if name == want:
64+
print("--- running %r verbatim ---\n" % name, flush=True)
65+
# GitHub runs each step as `bash -e {0}`.
66+
flags = "-ex" if trace else "-e"
67+
result = subprocess.run(["bash", flags, "-c", body])
68+
print("\n--- exit %d ---" % result.returncode)
69+
return result.returncode
70+
71+
print("No step named %r. Run without arguments to list them." % want, file=sys.stderr)
72+
return 2
73+
74+
75+
if __name__ == "__main__":
76+
sys.exit(main())

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ jobs:
2525
docker-library/php/config_files/xdebug \
2626
docker-library/httpd/config_files/docker-entrypoint.sh
2727
28-
- name: the pty driver compiles
29-
run: python3 -m py_compile .github/scripts/drive-install.py
28+
- name: the helper scripts compile
29+
run: |
30+
python3 -m py_compile .github/scripts/drive-install.py
31+
python3 -m py_compile .github/scripts/run-step.py
3032
3133
- name: hadolint
3234
uses: hadolint/hadolint-action@v3.1.0

‎README.es.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,39 @@ comprueba que un `.php` nunca se sirve como código fuente, que los archivos cre
449449
en los contenedores son tuyos, y que Apache y nginx sirven lo mismo. Si está en
450450
verde, funciona en una máquina que no es la tuya.
451451

452+
### Ejecutar un paso de la CI antes de subir
453+
454+
Una vuelta por GitHub son seis minutos, y la mayoría de los fallos no están en el
455+
código sino en el propio paso: una bandera que pasas en local y la CI no, un valor
456+
que debía dejar listo un paso anterior, una tubería que se comporta distinto bajo
457+
`pipefail`. Así que ejecuta el paso tal como está escrito, en vez de reescribirlo:
458+
459+
```bash
460+
forge start # los pasos necesitan la pila arriba
461+
462+
.github/scripts/run-step.py # lista todos los pasos
463+
.github/scripts/run-step.py "the forge command works"
464+
.github/scripts/run-step.py -x "<nombre>" # traza, para cazar el grep silencioso
465+
```
466+
467+
Lee `.github/workflows/ci.yml` y ejecuta el bloque `run:` del paso verbatim bajo
468+
`bash -e`, igual que GitHub. Necesita PyYAML; si no lo tienes, te dice cómo
469+
instalarlo.
470+
471+
**Los pasos no son independientes.** Algunos leen cosas que creó un paso anterior
472+
—el proyecto `my-app`, una base de datos encendida—, así que si uno falla por un
473+
archivo que no existe, ejecuta antes el que lo crea. `run-step.py` sin argumentos
474+
los lista en orden.
475+
476+
**Dos tipos de paso no pueden pasar en local**, y es lo esperado:
477+
478+
| Paso | Por qué | Para ejecutarlo igual |
479+
|---|---|---|
480+
| los de HTTPS | `curl` sin `-k`, a propósito: comprueba el almacén de confianza del sistema | `./install_cert.sh` (pide sudo, instala la CA) |
481+
| los de DNS | reescriben el resolutor del sistema | `./setup-local-dns.sh` (pide sudo) |
482+
483+
Todo lo demás corre contra tus propios contenedores.
484+
452485
## 📄 Licencia
453486

454487
MIT — ver [LICENSE](LICENSE).

‎README.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,38 @@ CI builds all three PHP versions, runs the installer, configures DNS, checks tha
451451
you, and that both Apache and nginx serve the same sites. If it is green, it works
452452
somewhere other than your laptop.
453453

454+
### Running a CI step before you push
455+
456+
A round trip through GitHub is six minutes, and most failures are not in the code
457+
but in the step itself: a flag you passed locally and CI does not, a value an
458+
earlier step was supposed to set, a pipeline that behaves differently under
459+
`pipefail`. So run the step as written instead of retyping it:
460+
461+
```bash
462+
forge start # the steps need the stack up
463+
464+
.github/scripts/run-step.py # list every step
465+
.github/scripts/run-step.py "the forge command works"
466+
.github/scripts/run-step.py -x "<name>" # trace, to find the silent grep
467+
```
468+
469+
It reads `.github/workflows/ci.yml` and runs the step's `run:` block verbatim
470+
under `bash -e`, exactly as GitHub does. Needs PyYAML; it tells you how to
471+
install it if you do not have it.
472+
473+
**Steps are not independent.** Some read fixtures an earlier step created — the
474+
`my-app` project, an enabled database — so when one fails on a missing file, run
475+
the earlier step first. `run-step.py` with no arguments lists them in order.
476+
477+
**Two kinds of step cannot pass locally**, and that is expected:
478+
479+
| Step | Why | To run it anyway |
480+
|---|---|---|
481+
| the HTTPS ones | `curl` without `-k`, on purpose: it checks the system trust store | `./install_cert.sh` (asks for sudo, installs the CA) |
482+
| the DNS ones | they rewrite the system resolver | `./setup-local-dns.sh` (asks for sudo) |
483+
484+
Everything else runs against your own containers.
485+
454486
## 📄 License
455487

456488
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

‎TASKS.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,22 @@ what the README explains first.
11291129
Make `forge` the documented interface, and say once — near the top — that it
11301130
wraps `docker compose`, so anyone who knows compose knows nothing is hidden and
11311131
the raw commands still work. Where no `forge` equivalent exists yet, that is a
1132-
gap to close, not a reason to document compose.
1132+
gap to close, not a reason to document compose. Task 38 closed the last one, so
1133+
there is no exception left to write around.
1134+
1135+
**While in there, realign the subsections.** The `##` sections match one to
1136+
one, which is what was checked when the Spanish README was written, but the
1137+
`###` inside them have drifted:
1138+
1139+
| English only | Spanish only |
1140+
|---|---|
1141+
| Starting and Stopping | Xdebug |
1142+
| Development Workflow | |
1143+
| Common Issues | |
1144+
1145+
Neither is wrong, but a reader switching languages gets a different shape, and
1146+
whoever edits one cannot tell what the other has. Counting `##` and calling
1147+
them aligned is exactly what hid this.
11331148
11341149
- [ ] **36. There is no uninstall**
11351150
`install.sh` writes `.env`, generates certificates, installs a CA into the

0 commit comments

Comments
 (0)