--- type: types/instruction.md name: publish-and-ci description: How a stack change is published and how its CI run is waited for - the local checks first, tools/wikitool publish, reading the runs through the authenticated Gitea connection rather than curl, how long to wait, when to give up, and why a red run means the build phase is not over. --- # Publish a stack change and wait for its CI run The local checks cover what they cover on this machine; CI covers the same checks plus a full `setup-instance.md` replay against a fresh `dist export` (`.gitea/workflows/ci.yml`). A green run on the published commit - and, for a converter change, a green `Converters` run (step 3) - is therefore the end of the checked stretch of a work package, not a formality after it - which is why waiting for it belongs to the phase that wrote the code (`stack-build`), and a red run sends the work back there rather than into the closing phase. ## When to run - `stack-build`, every time it publishes - its last step. - `stack-close`, when its own pull-through of a stale document publishes something of its own (that skill's step 3 says when). ## Steps 1. **Run the local checks, explicitly rather than assumed:** ```bash tools/wikitool docs verify tools/wikitool instructions verify ``` plus the relevant `pytest` run in `tools/` ([testing-conventions.md](testing-conventions.md)). A red check here is fixed before anything is published. 2. **Publish with `tools/wikitool publish`.** The gates apply as everywhere (AGENTS.md § Gates); an exit 42 is shown to the user verbatim and waited on. When the changeset touches `tools/`, `types/`, `instructions/`, `AGENTS.md` or a `/CONTRACT.md`, `publish` prints a one-line note that CI is the last mechanical check still to come and that what follows it is covered by none. A push to `main` that moves `VERSION` additionally triggers a tagged release. **CI does the tagging** - a session never creates a tag, which is what keeps AGENTS.md invariant 5 intact. 3. **Wait for CI on the published commit, this way.** One runner serves the repository and it currently runs **no jobs in parallel**, so the jobs of a push run **one after another**: CI's `verify` job (about **8 minutes**) and `pwsh` job (about **3 minutes**) first, then the second run a push to `main` starts - `release.yml`, preceded by the `Converters` check - which waits `queued` until CI is done and then takes about **1 minute**. From the push to the last green run that is about **12 minutes** (measured 10.5 to 13 minutes, October 2026); a second run still `queued` while CI works is the runner's order, not a hang. The times below rest on that setup (`ENVIRONMENT.md` § CI records it); once the runner takes parallel jobs, measure them again and rewrite them here. - Read the runs for the published commit's SHA through the authenticated Gitea connection `ENVIRONMENT.md` lists (the Gitea MCP's `actions_run_read`, `list_runs`), right after `publish`, to get their ids. **Never anonymously via `curl`:** Gitea answers the Actions API with `401 token is required` even for this public repo. - Check again after about 11 minutes, then every 2 minutes. - **Give up after 25 minutes** - twice the usual time, so a slow day is not mistaken for a hang - and hand the open runs to the user by id and link, rather than waiting on. - **Count from when the runner is free, not from the push.** Runs of an earlier push that are still open when you publish run first, and the runner does not keep push order: it may take the next push's CI before the earlier push's release run. So list the open runs right after `publish`; when there are any, the first check and the give-up time start from the moment the last of them finishes. - Any shell loop that polls instead must **end on the first non-2xx status or missing field** and print the raw response. A loop that treats an error as "not finished yet" never ends; that happened in the #139 close-out. - Keep the polling out of the main context where the harness allows it - a background command or a fork - so a long wait does not fill the session with run listings. **Converter changes have a second run.** When the published commit touches the converter surface - `tools/chemenu/filters/`, `tools/chemenu/commands/raw/derive.py` or `accept.py`, `tools/chemenu/tests/test_raw_derive*.py`, `tools/chemenu/tests/binary_formats.py`, `tools/requirements.txt` or `.gitea/workflows/converters.yml` - the `Converters` workflow (`converters.yml`) starts by itself with CI and derives the real documents of the private test corpus. Deriving the corpus takes about **10 minutes** on top of CI, on the same runner and after it, so the last run finishes about **22 minutes** after the push; give up after **35 minutes** instead of 25, and count a red run like a red CI run (step 4). Start it by hand (`actions_run_write`, `dispatch_workflow` on `main`) when only the private repository changed or when the commit reaches the converters only indirectly. A release without suffix runs the same steps as the first job of `release.yml`, which the release waits for. 4. **A red run is not waited past.** Read the failing job's log, fix the cause, and go back to step 1: this is still build work, whichever skill published. The phase that published ends only on a green run, and the issue stays open until there is one.