Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .github/scripts/__pycache__/drive-install.cpython-314.pyc
Binary file not shown.
Binary file not shown.
76 changes: 76 additions & 0 deletions .github/scripts/run-step.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Run one CI step's `run:` block locally, verbatim.

Verbatim is the point. Every CI failure this project has had came from checking
a hand-typed variant of a step rather than the step itself: a flag passed
locally but not in CI, a value assumed to be set by an earlier step, a pipeline
that behaves differently under `pipefail`.

.github/scripts/run-step.py # list the steps
.github/scripts/run-step.py "the forge command works"
.github/scripts/run-step.py -x "<name>" # trace each command

Needs the stack running (`forge start`). Steps are not independent: some rely on
fixtures an earlier step creates, so run those first when a step fails on
missing files. See the Contributing section of the README.
"""
import subprocess
import sys

try:
import yaml
except ImportError:
raise SystemExit(
"This needs PyYAML to read the workflow file.\n"
" Debian/Ubuntu: sudo apt install python3-yaml\n"
" Arch: sudo pacman -S python-yaml\n"
" Any: pip install --user pyyaml"
)

WORKFLOW = ".github/workflows/ci.yml"


def steps():
with open(WORKFLOW, encoding="utf-8") as fh:
doc = yaml.safe_load(fh)
for job_name, job in doc["jobs"].items():
for step in job.get("steps", []):
if step.get("name") and step.get("run"):
yield job_name, step["name"], step["run"]


def main():
args = [a for a in sys.argv[1:]]
trace = False
if args and args[0] in ("-x", "--trace"):
trace = True
args = args[1:]

found = list(steps())
if not args:
print("Steps that can be run:\n")
current = None
for job, name, _ in found:
if job != current:
print(" [%s]" % job)
current = job
print(" %s" % name)
print("\nRun one with: %s \"<name>\"" % sys.argv[0])
return 0

want = args[0]
for _, name, body in found:
if name == want:
print("--- running %r verbatim ---\n" % name, flush=True)
# GitHub runs each step as `bash -e {0}`.
flags = "-ex" if trace else "-e"
result = subprocess.run(["bash", flags, "-c", body])
print("\n--- exit %d ---" % result.returncode)
return result.returncode

print("No step named %r. Run without arguments to list them." % want, file=sys.stderr)
return 2


if __name__ == "__main__":
sys.exit(main())
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ jobs:
docker-library/php/config_files/xdebug \
docker-library/httpd/config_files/docker-entrypoint.sh

- name: the pty driver compiles
run: python3 -m py_compile .github/scripts/drive-install.py
- name: the helper scripts compile
run: |
python3 -m py_compile .github/scripts/drive-install.py
python3 -m py_compile .github/scripts/run-step.py

- name: hadolint
uses: hadolint/hadolint-action@v3.1.0
Expand Down
33 changes: 33 additions & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,39 @@ comprueba que un `.php` nunca se sirve como código fuente, que los archivos cre
en los contenedores son tuyos, y que Apache y nginx sirven lo mismo. Si está en
verde, funciona en una máquina que no es la tuya.

### Ejecutar un paso de la CI antes de subir

Una vuelta por GitHub son seis minutos, y la mayoría de los fallos no están en el
código sino en el propio paso: una bandera que pasas en local y la CI no, un valor
que debía dejar listo un paso anterior, una tubería que se comporta distinto bajo
`pipefail`. Así que ejecuta el paso tal como está escrito, en vez de reescribirlo:

```bash
forge start # los pasos necesitan la pila arriba

.github/scripts/run-step.py # lista todos los pasos
.github/scripts/run-step.py "the forge command works"
.github/scripts/run-step.py -x "<nombre>" # traza, para cazar el grep silencioso
```

Lee `.github/workflows/ci.yml` y ejecuta el bloque `run:` del paso verbatim bajo
`bash -e`, igual que GitHub. Necesita PyYAML; si no lo tienes, te dice cómo
instalarlo.

**Los pasos no son independientes.** Algunos leen cosas que creó un paso anterior
—el proyecto `my-app`, una base de datos encendida—, así que si uno falla por un
archivo que no existe, ejecuta antes el que lo crea. `run-step.py` sin argumentos
los lista en orden.

**Dos tipos de paso no pueden pasar en local**, y es lo esperado:

| Paso | Por qué | Para ejecutarlo igual |
|---|---|---|
| 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) |
| los de DNS | reescriben el resolutor del sistema | `./setup-local-dns.sh` (pide sudo) |

Todo lo demás corre contra tus propios contenedores.

## 📄 Licencia

MIT — ver [LICENSE](LICENSE).
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,38 @@ CI builds all three PHP versions, runs the installer, configures DNS, checks tha
you, and that both Apache and nginx serve the same sites. If it is green, it works
somewhere other than your laptop.

### Running a CI step before you push

A round trip through GitHub is six minutes, and most failures are not in the code
but in the step itself: a flag you passed locally and CI does not, a value an
earlier step was supposed to set, a pipeline that behaves differently under
`pipefail`. So run the step as written instead of retyping it:

```bash
forge start # the steps need the stack up

.github/scripts/run-step.py # list every step
.github/scripts/run-step.py "the forge command works"
.github/scripts/run-step.py -x "<name>" # trace, to find the silent grep
```

It reads `.github/workflows/ci.yml` and runs the step's `run:` block verbatim
under `bash -e`, exactly as GitHub does. Needs PyYAML; it tells you how to
install it if you do not have it.

**Steps are not independent.** Some read fixtures an earlier step created — the
`my-app` project, an enabled database — so when one fails on a missing file, run
the earlier step first. `run-step.py` with no arguments lists them in order.

**Two kinds of step cannot pass locally**, and that is expected:

| Step | Why | To run it anyway |
|---|---|---|
| the HTTPS ones | `curl` without `-k`, on purpose: it checks the system trust store | `./install_cert.sh` (asks for sudo, installs the CA) |
| the DNS ones | they rewrite the system resolver | `./setup-local-dns.sh` (asks for sudo) |

Everything else runs against your own containers.

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
17 changes: 16 additions & 1 deletion TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,22 @@ what the README explains first.
Make `forge` the documented interface, and say once — near the top — that it
wraps `docker compose`, so anyone who knows compose knows nothing is hidden and
the raw commands still work. Where no `forge` equivalent exists yet, that is a
gap to close, not a reason to document compose.
gap to close, not a reason to document compose. Task 38 closed the last one, so
there is no exception left to write around.

**While in there, realign the subsections.** The `##` sections match one to
one, which is what was checked when the Spanish README was written, but the
`###` inside them have drifted:

| English only | Spanish only |
|---|---|
| Starting and Stopping | Xdebug |
| Development Workflow | |
| Common Issues | |

Neither is wrong, but a reader switching languages gets a different shape, and
whoever edits one cannot tell what the other has. Counting `##` and calling
them aligned is exactly what hid this.

- [ ] **36. There is no uninstall**
`install.sh` writes `.env`, generates certificates, installs a CA into the
Expand Down