Skip to content

Move the rollback workflow's steps into tested scripts and enforce the one-line run: rule - #1006

Merged
anth-volk merged 5 commits into
mainfrom
sim-rollback-workflow-scripts
Nov 5, 2026
Merged

anth-volk merged 5 commits into
mainfrom
sim-rollback-workflow-scripts

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1005

Same-Repository Draft PR Check

  • This PR is a draft.
  • This PR is from a branch in PolicyEngine/policyengine-sim-api, not a personal fork.

Changelog

  • This PR adds a changelog fragment, changelog.d/<name>.<type>.md, whose type is breaking, added, changed, fixed or removed.

Summary

Phase 1 step 1.7, last part. Stacked on #1004, which moves the deploy workflow's seven blocks and teaches the routing writer discovery to read the scripts a step calls.

The six multi-line run: blocks that #744 added to simulation-rollback.yml now live in tested scripts under .github/scripts/, each called from a single-command step:

Step Script
Restore Modal routing rollback-restore-routing.sh
Resolve the unfinished routing publication rollback-resolve-pending-publication.sh
Read the restored release identity rollback-read-release-identity.sh
Resolve the stable service rollback-stable-service-state.sh
Restore entrypoint traffic rollback-restore-entrypoint-traffic.sh
Verify the stable service serves the restored release rollback-verify-stable-service.sh

With the baseline then empty, sim.ci.multiline-run-is-script is enforced: rules/baselines/ci-multiline-run.txt is deleted, the rule loses its [rule.baseline], and [ci_scripts] lists no baseline for the check, so any multi-line run: block in a workflow fails the quality-guards job. The rules tool now lets [ci_scripts] leave out a check's baseline (scripts/rules/model.py, load.py, ci_scripts.py): every finding of that check then fails, and --write-baselines skips it and says so. python -m scripts.rules validate, which the rules-registry guard runs, also fails when [ci_scripts] lists a baseline with an entry for a check whose rule is enforced, so neither this rule's baseline nor the two empty ones listed for the other enforced checks can tolerate a finding again. docs/engineering/skills/ci-workflows.md describes the enforcement.

Behaviour is unchanged. A PyYAML comparison of the workflow on main and on this branch shows identical top-level keys, job settings and step keys other than run:; only these six steps' run: text differs. Four script bodies are the old inline shell, byte for byte. The traffic restore and the verification call their sibling scripts (set-cloud-run-simulation-entry-revision.sh, cloud-run-simulation-entry-smoke.sh) through the script's own directory instead of a path from the repository root. The scripts add set -u; every variable they read is set by the job or step env: or by the runner. The step IDs identity and service and the outputs later steps read are written by the same commands.

Kept as it was, for a follow-up: "Resolve the stable service" writes url=$(jq -er …) and revision=$(jq -er …) inside echo arguments, where a failing jq does not stop a bash -e shell. Without a URL it records url=null, and without exactly one revision at 100 percent it records an empty revision, and the step succeeds. The script keeps this, says so in a comment, and test_stable_service_records_what_jq_prints_when_a_value_is_missing pins it. Failing the step there would be a behaviour change, so it is not made here.

Tests.

  • test_simulation_rollback_scripts.py runs each new script with fake uv and gcloud first on PATH (jq is real), or as a copy beside a fake sibling script that records its arguments and environment, and checks the exact calls, $GITHUB_OUTPUT and exit status. It also checks that each moved step keeps every setting it had and calls its script.
  • The Gate releases on a numerical sentinel and make them reversible #744 tests of how the restore reads a deployment run's kept plans (test_simulation_rollback.py) now run rollback-restore-routing.sh instead of extracting the step's inline shell, and the step lookups read each step with the scripts it calls.
  • A routing discovery test pins the rollback workflow's two writing steps, which discovery now finds through the scripts.

Environment variables

Every variable the scripts read, and where it comes from. The job and step env: blocks are unchanged.

Variable Script Provider and wiring
MODAL_TOKEN_ID, MODAL_TOKEN_SECRET routing scripts (read by the Modal client) Repository GitHub Actions secrets, set in each step's env:.
GH_TOKEN rollback-restore-routing.sh (read by gh) ${{ github.token }} in the step's env:; the job grants actions: read for the run and job lookups.
TARGET, DEPLOY_RUN_ID, POLICYENGINE_VERSION, EXPECTED_LATEST, MODAL_ENVIRONMENT, SERVICE routing, traffic and verification scripts Job env:, from the workflow_dispatch inputs.
PROJECT_ID, REGION rollback-stable-service-state.sh; also passed to the traffic script as SIMULATION_ENTRYPOINT_GCP_* Job env:, from the beta and prod environment variables SIMULATION_ENTRYPOINT_GCP_PROJECT_ID and SIMULATION_ENTRYPOINT_GCP_REGION (region defaults to us-central1). The job sets environment: ${{ inputs.target }}, and target is beta or prod.
SIMULATION_ENTRYPOINT_* rollback-restore-entrypoint-traffic.sh Step env:, from the job env:, the entrypoint_revision input and step service's revision output.
EXPECTED_*, STABLE_URL, EXPECTED_REVISION, PREVIOUS_REVISION rollback-verify-stable-service.sh Step env:, from steps identity and service and the entrypoint_revision input.
Google credentials for gcloud stable service and traffic scripts google-github-actions/auth with the beta and prod environment secrets SIMULATION_ENTRYPOINT_GCP_WIF_PROVIDER and SIMULATION_ENTRYPOINT_GCP_DEPLOY_SERVICE_ACCOUNT.
GITHUB_REPOSITORY, RUNNER_TEMP, GITHUB_OUTPUT all Set by the GitHub Actions runner.

Testing

  • The first commit adds the tests. Against the old workflow, 34 entry tests and the two rules-tool tests fail; three rewritten structure tests pass, since they read each step with its scripts. With the second commit, all of them pass. From review, a third commit adds a failing validation test for an enforced check's baseline with an entry and the new --write-baselines message, and the fourth makes them pass:
    • projects/policyengine-simulation-entry: the tests in test_simulation_rollback_scripts.py, the rewritten tests in test_simulation_rollback.py, the rollback writers test and the routing discovery tests in test_routing_workflow_invariants.py, and test_deployment_assets_routing.py::test_a_failing_routing_command_fails_its_step_and_job and ::test_routing_jobs_never_allow_command_failures (157 passed).
    • scripts/rules/tests/test_ci_scripts.py: CompareTest.test_a_check_without_a_baseline_fails_every_finding, CompareTest.test_writing_baselines_skips_a_check_without_one and the three EnforcedCheckBaselineTest tests; scripts/rules/tests/test_cli.py: CiScriptsCommandTest.test_write_baselines_records_the_findings.
  • python -m scripts.rules ci-scripts passes with no baseline file; python scripts/run_quality_guards.py, python -m scripts.rules validate and python -m scripts.rules guard --base-ref origin/main pass.
  • shellcheck and actionlint pass on the new scripts and the workflow.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #1007 November 4, 2026 12:50
@anth-volk
anth-volk force-pushed the sim-rollback-workflow-scripts branch from 0f45cd0 to 063fe6d Compare November 4, 2026 15:11
anth-volk added a commit that referenced this pull request Nov 4, 2026
Review of #1006 found that [ci_scripts] could list a baseline file again
for a check whose rule is enforced, and the entries in it would be
tolerated: validate checks only that a listed file exists, and guard
compares only [rule.baseline] counts. The same holds for the two other
enforced checks, whose listed baselines are empty.

- A new validation test fails: an enforced check's listed baseline holds
  an entry, and validate reports nothing. Tests that an enforced check may
  list an empty baseline, and a ratcheted one may list entries, pass.
- The write-baselines command test now expects the message to say that
  a check without a baseline in [ci_scripts] is not recorded, and fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 4, 2026
Review of #1006:

- python -m scripts.rules validate, which the rules-registry guard runs,
  now fails when [ci_scripts] lists a baseline with an entry for a check
  whose rule is enforced. Before, an entry added there was tolerated,
  because validate checked only that the file exists and guard compares
  only [rule.baseline] counts. An enforced check may list no baseline or
  an empty one; a ratcheted check may list entries.
- ci-scripts --write-baselines says that a check without a baseline in
  [ci_scripts] is not recorded.
- docs/engineering/skills/ci-workflows.md says the two listed empty
  baselines stay empty because validate refuses entries in them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 4, 2026
Review round 2 of #1006 found no defect and noted that, with the
scripts' set -u, a variable the rollback job stopped setting would now
fail the scripts, while every test passed because the tests set the
variables themselves. A test now pins the job's env:. The CiScripts
docstring says what the rules tool requires: a check may leave its
baseline out, and an enforced check lists none or an empty one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from sim-deploy-workflow-scripts to main November 4, 2026 16:51
@anth-volk
anth-volk force-pushed the sim-rollback-workflow-scripts branch from 802ff23 to bb9061d Compare November 4, 2026 16:51
anth-volk added a commit that referenced this pull request Nov 4, 2026
Review of #1006 found that [ci_scripts] could list a baseline file again
for a check whose rule is enforced, and the entries in it would be
tolerated: validate checks only that a listed file exists, and guard
compares only [rule.baseline] counts. The same holds for the two other
enforced checks, whose listed baselines are empty.

- A new validation test fails: an enforced check's listed baseline holds
  an entry, and validate reports nothing. Tests that an enforced check may
  list an empty baseline, and a ratcheted one may list entries, pass.
- The write-baselines command test now expects the message to say that
  a check without a baseline in [ci_scripts] is not recorded, and fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 4, 2026
Review of #1006:

- python -m scripts.rules validate, which the rules-registry guard runs,
  now fails when [ci_scripts] lists a baseline with an entry for a check
  whose rule is enforced. Before, an entry added there was tolerated,
  because validate checked only that the file exists and guard compares
  only [rule.baseline] counts. An enforced check may list no baseline or
  an empty one; a ratcheted check may list entries.
- ci-scripts --write-baselines says that a check without a baseline in
  [ci_scripts] is not recorded.
- docs/engineering/skills/ci-workflows.md says the two listed empty
  baselines stay empty because validate refuses entries in them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 4, 2026
Review round 2 of #1006 found no defect and noted that, with the
scripts' set -u, a variable the rollback job stopped setting would now
fail the scripts, while every test passed because the tests set the
variables themselves. A test now pins the job's env:. The CiScripts
docstring says what the rules tool requires: a check may leave its
baseline out, and an enforced check lists none or an empty one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 5 commits November 4, 2026 20:55
…failing)

Tests for moving the six multi-line run: blocks of
simulation-rollback.yml into scripts under .github/scripts/, and for
enforcing sim.ci.multiline-run-is-script without a baseline file.

- test_simulation_rollback_scripts.py runs each new script with fake uv
  and gcloud first on PATH, or beside a fake copy of the sibling script it
  calls, and checks its exact calls, $GITHUB_OUTPUT and exit status. It
  also checks that each moved step keeps every setting it had (if, id,
  working-directory and env) and calls its script.
- The #744 tests of how the routing restore reads a deployment run's
  kept plans now run rollback-restore-routing.sh instead of extracting
  the step's inline shell, and the step lookups read each step with the
  scripts it calls.
- Two rules-tool tests check that [ci_scripts] may leave out a check's
  baseline: every finding of that check then fails, and writing
  baselines skips it.
- A routing discovery test pins the rollback workflow's two writing
  steps; it passes before and after the move.

Against the current code, 34 entry tests and both rules-tool tests fail.
Three rewritten rollback structure tests pass, since they read each step
together with its scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The six multi-line run: blocks that #744 added to
simulation-rollback.yml now live in tested scripts under
.github/scripts/, each called from a single-command step:

- rollback-restore-routing.sh: Restore Modal routing
- rollback-resolve-pending-publication.sh: Resolve the unfinished
  routing publication
- rollback-read-release-identity.sh: Read the restored release identity
- rollback-stable-service-state.sh: Resolve the stable service
- rollback-restore-entrypoint-traffic.sh: Restore entrypoint traffic
- rollback-verify-stable-service.sh: Verify the stable service serves
  the restored release

Four script bodies are the old inline shell unchanged. The traffic
restore and the verification call their sibling scripts through the
script's own directory instead of a path from the repository root. The
scripts add set -u; every variable they read is set by the job or step
env: or by the runner. Every step keeps its if, id, working-directory
and env, so the identity and service outputs and exit statuses are
unchanged. As before, the stable service step records url=null or an
empty revision instead of failing when jq finds no URL or no single
revision at 100 percent; its script says so.

With the baseline empty, sim.ci.multiline-run-is-script is enforced:
rules/baselines/ci-multiline-run.txt is deleted, the rule loses its
[rule.baseline], and [ci_scripts] lists no baseline for the check. The
rules tool now lets [ci_scripts] leave out a check's baseline, and then
fails on every finding of it. docs/engineering/skills/ci-workflows.md
describes the enforcement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1006 found that [ci_scripts] could list a baseline file again
for a check whose rule is enforced, and the entries in it would be
tolerated: validate checks only that a listed file exists, and guard
compares only [rule.baseline] counts. The same holds for the two other
enforced checks, whose listed baselines are empty.

- A new validation test fails: an enforced check's listed baseline holds
  an entry, and validate reports nothing. Tests that an enforced check may
  list an empty baseline, and a ratcheted one may list entries, pass.
- The write-baselines command test now expects the message to say that
  a check without a baseline in [ci_scripts] is not recorded, and fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1006:

- python -m scripts.rules validate, which the rules-registry guard runs,
  now fails when [ci_scripts] lists a baseline with an entry for a check
  whose rule is enforced. Before, an entry added there was tolerated,
  because validate checked only that the file exists and guard compares
  only [rule.baseline] counts. An enforced check may list no baseline or
  an empty one; a ratcheted check may list entries.
- ci-scripts --write-baselines says that a check without a baseline in
  [ci_scripts] is not recorded.
- docs/engineering/skills/ci-workflows.md says the two listed empty
  baselines stay empty because validate refuses entries in them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 of #1006 found no defect and noted that, with the
scripts' set -u, a variable the rollback job stopped setting would now
fail the scripts, while every test passed because the tests set the
variables themselves. A test now pins the job's env:. The CiScripts
docstring says what the rules tool requires: a check may leave its
baseline out, and an enforced check lists none or an empty one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the sim-rollback-workflow-scripts branch from bb9061d to b64dca0 Compare November 4, 2026 16:56
@anth-volk
anth-volk marked this pull request as ready for review November 5, 2026 13:00
@anth-volk
anth-volk requested a review from a team as a code owner November 5, 2026 13:00
@anth-volk
anth-volk merged commit 682aa9d into main Nov 5, 2026
23 checks passed
@anth-volk
anth-volk deleted the sim-rollback-workflow-scripts branch November 5, 2026 13:00
anth-volk added a commit that referenced this pull request Nov 5, 2026
Brings in #1006, which moved the rollback workflow's steps into scripts
and enforced sim.ci.multiline-run-is-script, and the release commit
after it.

The only conflict was docs/generated/rules.md, in the list of ratcheted
rules: main removed the multiline rule's line, and this branch removed
the weight-stripping rule's line. The file is regenerated with
python -m scripts.rules render from the merged registry, in which both
rules are enforced with no baseline, so the list is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Move the rollback workflow's steps into tested scripts and enforce the one-line run: rule

1 participant