Skip to content

ci: run the deploy gates somewhere other than one laptop (C-341, C-342, #424) - #435

Merged
Polichinel merged 2 commits into
developmentfrom
ci/deploy-gates-off-one-laptop
Aug 11, 2026
Merged

ci: run the deploy gates somewhere other than one laptop (C-341, C-342, #424)#435
Polichinel merged 2 commits into
developmentfrom
ci/deploy-gates-off-one-laptop

Conversation

@Polichinel

Copy link
Copy Markdown
Contributor

Closes #424 (by hand — Closes #N is inert on PRs to a non-default branch). Part of #421, Story 3.

This story shipped smaller than its issue described, and that is the finding.

#424's own table was wrong

The issue said four gates would start running once the workflow got a step and a token. I simulated a runner checkout before writing the step:

Gate Issue claimed Measured
main ancestor of development would run skips, rc=128actions/checkout leaves one local branch, so bare main does not resolve even at fetch-depth: 0, which the workflow already had
local stale release branches would run passes trivially — nothing to inspect on a runner
remote stale release branches would run runs ✓
version-not-already-tagged would run xfail — cannot fail a suite anywhere

So: one redundant, one needing two git branch -f lines to unblock, one needing demotion to an honest skip, and one impossible until #425.

Had I implemented the issue as written, the step would have run, gone green, and asserted almost nothing — C-341 restored under the appearance of a fix.

The gain the issue never mentioned: test_merge_main_into_development_is_conflict_free now runs. Nothing was running it anywhere, and the bespoke shell check never did it.

C-341 — narrowed, not closed

TestF1VersionBumped::test_version_not_already_tagged is @pytest.mark.xfail. Scheduling it would add a green tick and no information. #425 owns that, and this entry stays open until it lands. Closing it now would claim a coverage the xfail denies.

C-342 — resolved

uv lock --check in the test job, before uv sync. Drilled:

clean tree                                   rc=0
dependency added without re-locking          rc=1  "The lockfile at `uv.lock` needs to be updated"
re-locked                                    rc=0

Why this one blocks a PR when the deploy gates deliberately do not. Those redden for reasons unrelated to the change under review — a diverged main is correct between a release and its back-merge, which is C-320. This reddens only when the pull request itself left the lock stale: its own fault, fixable inside it with one command. The distinction is in the workflow comment so nobody re-derives it.

Confirmed on its own change: this PR adds pyyaml to the dev group for the new guard, and uv lock --check agreed after re-locking.

Two near-misses, recorded rather than quietly fixed

A parse is not a verification. The lock-check step was first inserted between - name: Install dependencies and its run:, producing a step with a duplicate run: key. yaml.safe_load reported "YAML parses" — PyYAML silently keeps the last duplicate — so the check had been overwritten by uv sync and would never have run. Twenty minutes after registering C-345, the same defect in the same shape.

And the drill of the new guard was wrong first. Its pass criterion was rc != 0, which cannot distinguish "the guard caught it" from "the test file failed to import" — and it was failing to import, because pyyaml was not yet a dependency. All four mutations reported CAUGHT while nothing ran.

Re-drilled with rc == 1 = caught, rc >= 2 = error:

  PASSED (guard did not catch)             baseline, clean tree
  CAUGHT                                   lock check removed from the test job
  CAUGHT                                   lock check moved after uv sync
  CAUGHT                                   local-ref step gutted
  CAUGHT                                   GH_TOKEN dropped from the gates step
  CAUGHT                                   a step given both run and uses

New guard

tests/test_ci_gates.py asserts the two orderings that fail green when wrong — uv lock --check before uv sync, and the local-ref step before the gates — plus step well-formedness, which is what catches the duplicate-key class.

Also

Workflow display name widened to Release hygiene; the filename is kept so run history, the tracking issue's provenance links, and the citations in publishing_to_pypi.md and the register stay valid. The tracking issue is generalised to cover both checks, and the close step handles the old title so an issue opened by the narrower workflow still closes.

Verification

  • ruff check . → All checks passed
  • pytestPYTEST_EXIT=0, FAILED count: 0 (unpiped, two readers)
  • docs/validate_docs.sh → PASSED
  • Both workflows checked semantically, not just parsed: every step has exactly one of run/uses, all steps.* references resolve to existing ids
  • Register: 345 IDs, 303 resolved, 39 open (0 T1, 4 T2, 10 T3, 19 T4, 6 deferred), 119 struck, cluster eight open of eleven rows

🤖 Generated with Claude Code

Polichinel and others added 2 commits August 11, 2026 12:47
#424)

The story shipped smaller than its issue described, and measuring is what
showed why.

#424 claimed four gates would start running once the workflow got a step
and a token. Simulating a runner checkout first:

  - actions/checkout leaves exactly ONE local branch, so the gates' bare
    `git merge-base --is-ancestor main development` exits 128 and they
    skip themselves — even at fetch-depth: 0, which the workflow already
    had. Two `git branch -f` lines fix it. Without them the step would
    have run, passed, and asserted nothing.
  - The local-clone branch gate would have PASSED TRIVIALLY on a runner
    rather than skipped: a fresh runner has one local branch, so it
    reports coverage it does not have. It now skips with a reason.
  - TestF1VersionBumped is xfail. It cannot fail a suite anywhere, so
    scheduling it would add a green tick and no information.

So of the four named: one redundant, one unblocked, one demoted to an
honest skip, one impossible until #425. C-341 is NARROWED, not closed —
closing it would claim a coverage the xfail denies. The gain the issue
never mentioned: the conflict-free back-merge check, which nothing was
running anywhere, now runs.

C-342 resolved: `uv lock --check` in the test job, before `uv sync`.
Drilled clean/dirty/clean. It is the one merge-blocking check added and
it differs in kind from the deploy gates — those redden for reasons
unrelated to the change (a diverged main is CORRECT between a release and
its back-merge, which is C-320); this reddens only when the PR itself
left the lock stale. Stated in the workflow comment so nobody re-derives
it. Confirmed on its own change: this PR adds pyyaml for the new guard,
and the check agreed after re-locking.

Two near-misses, both recorded rather than quietly fixed:

A PARSE IS NOT A VERIFICATION. The lock-check step was first inserted
between `- name: Install dependencies` and its `run:`, producing a
duplicate `run:` key. yaml.safe_load reported "YAML parses" — PyYAML
silently keeps the last duplicate — so the check had been overwritten by
`uv sync` and would never have run. Twenty minutes after registering
C-345. tests/test_ci_gates.py now asserts step well-formedness.

AND THE DRILL OF THAT GUARD WAS WRONG FIRST. Its criterion was rc != 0,
which cannot tell "guard caught it" from "the file failed to import" —
and it was failing to import, because pyyaml was not a dependency. All
four mutations reported CAUGHT while nothing ran. Re-drilled with rc == 1
meaning caught and rc >= 2 meaning error: five mutations, five genuine
catches.

Workflow display name widened to "Release hygiene"; filename kept so run
history and existing citations stay valid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review medium found no defect in the workflows. It independently
verified the two things most likely to be silently wrong, and both were
already right: the gateresult step reads `steps.gates.outcome` and NOT
`conclusion` (with continue-on-error, conclusion is forced to 'success'
whatever pytest did — one field name from a gate that always reports
clean); and `set -o pipefail` inside a `run:` block does take effect,
which matters because Actions' default shell is `bash -e {0}` without
pipefail, so `pytest | tee` would otherwise report tee's status and mask
every failure. That is C-345's shape inside the step registering C-345.

It also confirmed adversarial content cannot escape the issue-body
heredocs — backticks and $(...) in pytest output are substituted
literally — and that the legacy issue title in the close step matches
byte-for-byte including the em-dash.

One hardening taken: the $GITHUB_OUTPUT delimiter was a fixed string.
`git log --oneline` cannot produce a bare "EOF" line, but pytest output
carries no such guarantee, so the gate log now uses a random delimiter.

/review-diff: C-341's residue table read as exhaustive while omitting
TestF7ProductPlanCurrency and TestDF2ReleaseGuideCompleteness. They are
absent deliberately — file-only, already in the PR suite — but omission
does not say so. Now stated.

Method note: my own check of the hardening asserted `"conclusion" not in
run` and failed on its own explanatory comment about why not to use
conclusion. Tightened to the expression `steps.gates.conclusion`. The
guard-narrower-than-the-property shape again, at miniature scale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Polichinel

Copy link
Copy Markdown
Contributor Author

Ritual complete — /code-review medium/register-risk/review-diff/register-risk

No defects in the workflows. The review verified rather than reasoned, and checked the two things most likely to be silently wrong here — both already correct:

outcome, not conclusion. With continue-on-error: true, conclusion is forced to success whatever pytest did. Reading it would have produced a gate that always reports clean. One field name away.

set -o pipefail inside a run: block. Actions' default shell is bash -e {0}without pipefail. Without the explicit set, pytest … | tee gates.log reports tee's status, which is always 0, and a failing suite passes silently. That is C-345's exact shape, inside the step that registers C-345.

Also confirmed by direct test rather than argument: adversarial content cannot escape the issue-body heredocs (backticks and $(rm -rf /) in SHAS/GATES_LOG are substituted literally, unexecuted), the YAML block-scalar dedent leaves the heredoc terminators valid, TOPOLOGY_SECTION="" does not trip set -u, and the legacy issue title in the close step matches the pre-PR one byte-for-byte including the em-dash.

One hardening taken

$GITHUB_OUTPUT used a fixed delimiter. git log --oneline cannot emit a bare EOF line — every line starts with a SHA — but pytest output has no such guarantee. The gate log now uses openssl rand. No realistic trigger exists under this workflow's trigger set (release/schedule/workflow_dispatch, never pull_request), so this is defence in depth rather than a fix.

/review-diff — one finding

C-341's residue table read as exhaustive while omitting TestF7ProductPlanCurrency and TestDF2ReleaseGuideCompleteness. Both are absent deliberately — they read only files and already run in the ordinary PR suite, so they never needed a scheduled runner — but omission cannot convey that. The table now says what it covers: gates requiring a deploy environment (git history, a token, or a real clone).

/register-risk — 0 new, 0 merged

The delimiter is an in-changeset hardening, applied. The method slip below is C-336's mechanism, which already carries two addenda from this week; a third for a self-corrected one-liner would be noise rather than signal.

Method note

My own verification of the hardening asserted "conclusion" not in gr["run"] — and failed on its own explanatory comment about why not to use conclusion. Tightened to the expression steps.gates.conclusion. The guard-narrower-than-the-property shape once more, at miniature scale, in a check written to confirm a fix for exactly that class.

Verification

  • ruff check . → All checks passed
  • pytestPYTEST_EXIT=0, FAILED count: 0 (unpiped, two readers)
  • docs/validate_docs.sh → PASSED
  • Workflows re-checked semantically: every step has exactly one of run/uses; gateresult reads steps.gates.outcome and not steps.gates.conclusion; delimiter randomised
  • tests/test_ci_gates.py still drills clean — five mutations, five genuine catches (rc == 1, not merely rc != 0)

@Polichinel
Polichinel enabled auto-merge (squash) August 11, 2026 12:53
@Polichinel
Polichinel merged commit 554a19f into development Aug 11, 2026
5 checks passed
@Polichinel
Polichinel deleted the ci/deploy-gates-off-one-laptop branch August 11, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant