Repository navigation
feat(actions-example): preview config changes on pull requests - #170
Merged
Merged
Conversation
Sample one state branch per configured prefix and run the existing process job on it when a pull request changes a policy config, without publishing anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Run previews as a separate caller job sharing the live job's inputs through YAML aliases, so they can cancel stale runs without the forbidden queue/cancel combination. Add an independent publication guard, derive the sample seed from the pull request ref, and spell out the tests and documentation edits the change requires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
A disposable probe repository confirmed that job-level queue: max serialises live runs, that job-level cancel-in-progress cancels stale previews, and that a read-only caller job fails at startup when the called workflow's skipped publish job requests write permissions, so the preview job keeps the caller's permissions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Seven TDD tasks: the discovery preview guard, per-prefix sampling, the publication guard, the reusable workflow preview mode, the caller preview jobs, documentation, and full validation with a separate test cleanup pass. The spec now keeps the live job's existing if and names the shared renderer's truncation noun. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
…discovery Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
…t branch A preview run on a pull request executes the PR's own workflows, so publication must not rely on the publish job's `if` alone: a lost or tampered gate there would still push update refs and change pull requests and issues from unmerged config. reconcile-state-branch.sh publish now independently refuses any caller ref other than refs/heads/<default branch>, dry run or not. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Add a `preview` input to the reusable workflow, wire it into discovery's `DISCOVERY_PREVIEW`, gate the publish job behind `!inputs.preview` so a preview never reaches publication, and have the processing report step append the candidate patch to the run summary whenever a preview produced one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Give each caller a preview job that runs a same-repository pull request into the default branch on the reusable workflow's new preview input, without publishing, so a config change can be seen before it merges. Move each caller's concurrency block to job level so the live job can keep queue: max while the preview job cancels its own older run; GitHub rejects both settings in one concurrency block. Anchors share the live job's with/secrets so the preview always runs with the pull request's edits. The preview job declares no permissions, because GitHub refuses to start a read-only caller when the called workflow's publish job requests write, even when that job is skipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Split the combined `grep && grep || fail` alias check in test_callers_preview_pull_requests into two independent checks, each with its own `|| fail`, naming which alias (with/secrets) is missing. shellcheck's SC2015 flags `A && B || C` because C also runs when A succeeds and B fails, masking which alias check actually failed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
A final review of the preview feature found three wording problems: the refresh paragraph claimed a re-run picks up a moved default branch, but GitHub re-runs reuse the original event's GITHUB_SHA, so only a moved state branch is picked up; the Environment paragraph conflated branch rules (which reject) with required reviewers (which hold for approval); and the fork-skip claim was never verified, since a fork or Dependabot pull request's read-only token may make GitHub fail the run at startup instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
The design and implementation plan served the branch; their reasoning stays in its history, and the README documents the behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
Fail preview discovery when sampling loses its branches: a failing sort inside a process substitution is invisible to set -e, and an empty matrix runs as zero jobs. Run the processing report step under shell: bash so pipefail catches a failed read in its summary pipelines. Correct the README: Dependabot pull requests do run the preview but receive Dependabot secrets rather than Actions secrets, and editing the scripts or reusable workflow alone does not start a preview. Make the reconcile usage text say publish refuses everything from another ref. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a pull-request preview to the GitHub Actions example. When a same-repository pull request into the default branch changes a policy's control configuration or its caller workflow, that caller's new
previewjob processes one sampled state branch per configured prefix exactly as a live run would — update,terraform init, optional formatting andterraform validate— and publishes nothing. Eachprocesssummary shows the updater's log and, for a changed candidate, the candidate patch.How it works
discover-state-branches.sh) gainsDISCOVERY_PREVIEW. In preview mode it accepts only arefs/pull/<n>/mergecaller ref, refuses a manual prefix, checks the 256-branch limit on the full matched set, then keeps one branch per prefix: a branch belongs to the first prefix it matches, and the pick is the first 15 hex digits ofsha256("<n>\t<prefix>")modulo that prefix's branch count, so every push to the same pull request previews the same branches. A prefix that owns no branch gets a warning annotation. Unset, discovery behaves exactly as before, so the version report is unchanged.previewinput.publishis skipped when it is set, and theReport processing resultstep appends the candidate patch through a sharedappend_fenced_filerenderer that the updater logs now use too.reconcile-state-branch.sh publishrefuses any caller ref other thanrefs/heads/<default branch>, dry run or not, before any Git or GitHub operation. A pull-request run executes the workflows it changes, so publication no longer rests on thepublishjob'sifalone.previewjob alongside the liveautomationjob. The two share theirwith:andsecrets:blocks through YAML anchors and aliases, so a pull request that edits a pin, prefix or root is previewed with its edits. Concurrency moves to the job level: the live job keepsqueue: max, while the preview's per-pull-request group usescancel-in-progress: true, which GitHub rejects in the same block asqueue: max.Verified GitHub Actions behaviour
The design depends on behaviour GitHub's documentation leaves open, so it was checked in a disposable private repository (since deleted) with sleep-only jobs mirroring the caller shape:
queue: maxon a job calling a reusable workflow serialises runs without cancelling any.cancel-in-progress: truecancels an in-progress preview when the pull request gets a new push.with:block reaches the called workflow intact (preview=true), with the live job andpublishskipped.permissions: contents: readfails at startup, because GitHub checks the called workflow'spublishpermissions even when that job would be skipped. The preview job therefore keeps the workflow's permissions and relies on the two publication guards.A Dependabot pull request that touches a caller does run the preview, because its branch lives in this repository; per GitHub's Dependabot documentation it receives Dependabot secrets rather than Actions secrets, so it has no
TF_API_TOKENunless one is also stored as a Dependabot secret. The README says so. Not verified: whether the skipped live job ever waits in its queue during a live run.Verification
go test -raceover the module (95.4% coverage),golangci-lint run(0 issues),make actionlint,make shellcheckandmake docs-checkall pass.examples/github-actions/test.sh, plusreconcile-test.shandreport-test.sh, passes locally (56/56). The Docker-backedtest_processing_*tests could not run locally, because the pinnedlinux/amd64Terraform image fails withexec format erroron this Mac's Docker, as it does onmain; CI runs them.process-state-branch.shis unchanged.publishgate (with a mutant that drops it), the patch summary's fencing and truncation, and each caller's triggers, concurrency and alias-shared inputs.Documentation
examples/github-actions/README.mdgains a "Preview on pull requests" section and corrects the passages the change made false (callers running only from the default branch, pull requests never processing state branches);docs/ADVANCED-USAGE.mdis updated to match.🤖 Generated with Claude Code
https://claude.ai/code/session_01AmjqpPp4yfPeg11EgGETuQ