Repository navigation
A build-input pull request builds the production image once, not twice - #1420
Merged
Merged
Conversation
A pull request that changed a build input — Gemfile.lock among them, so every weekly Dependabot group — built the production image twice: in docker_build, with the GitHub Actions layer cache, and in image_scan, from scratch, before scanning it. Both build the same image: the same context, the same Dockerfile, no target, linux/amd64. image_scan's cold build already proves the image builds, so docker_build's second build proved nothing new and billed for it. On a private fork the second build was also the expensive one. On the fork's last full run, docker_build took 794 seconds, 742 of them in its build step; image_scan's build of the same image on the same pull request, plus its Trivy scan, took 370. The cache cost more than it saved there — most likely the mode=max export on a small runner, though that was not measured on its own. docker_build now runs on every push to main, unfiltered, which remains the merged tree's proof, and on a pull request only when its own workflow changes, so a change to it is still tested before it lands. image_scan's pull_request paths widen to the full build-input list (Gemfile, .tool-versions and bin/docker-entrypoint join Dockerfile, .dockerignore and Gemfile.lock), so it is each build-input pull request's single image build. The pre-push hook still builds locally on the same inputs. The template invariants change first: docker_build's pull request paths are now exactly its own workflow, image_scan's name every build input, and a new example pins that the two workflows build the same image — the fact the change rests on. The two trigger examples were red against the old workflows; the same-image example was already green. Touching image_scan.yml was the trigger #1404 waited for: its 18-line header and the 27-line cache-scope comment shrink to two-line pointers. The rationale already lived in /docs/developer/testing under #536; the one point it lacked — why keying the cache on the base image's digest would not help — moves there. Closes #1404
dschmura
added a commit
that referenced
this pull request
Oct 17, 2026
…1421) A private repository is billed per job, each rounded up to the minute. CI ran its static checks as four jobs — scan_ruby (Brakeman and bundler-audit), scan_js (the importmap audit), lint (RuboCop and the comment-block gate) and lint_docs (markdown and ERB) — that each finished well inside a minute: 25, 28, 36 and 58 seconds on a private fork's last full run. Four billed minutes for about two and a half of work, and a minute of that was the same setup repeated: Ruby set up four times (59 seconds between them) and libvips installed twice (29). They are now one `static` job: one checkout, one Ruby, one RuboCop cache, one libvips, then every check as its own step. Each check step carries `if: ${{ !cancelled() }}`, so it still runs after an earlier one fails and a push reports every failing check, as four separate jobs did; the setup steps carry no such guard, so a failed setup still stops the job. A new template invariant pins both halves — the four jobs gone, and every check guarded — and a mutation that dropped the guard from the ERB step turned it red. This changes required status-check names. This repository's main ruleset requires scan_js, scan_ruby, lint and lint_docs by name; this pull request reports `static` instead, so it waits on those four until the ruleset requires `static` in their place. The forks were checked: MClassrooms requires nothing, modelrails_base_wads requires only `CI/test` (kept), and the private fork's plan has no rulesets. No fork workflow `needs:` the old jobs. The names were also written in prose: js_advisory.yml's header (which said scan_js "remains a required ruleset context"), the Gemfile's list of jobs that boot Rails, three lefthook.yml comments, getting-started's Gate 2 table and its js_advisory row (which named scan_js as the required check), and the scan_js workflow spec's positive control, which now expects `ci.yml / static`. The comments touched shrink to two-line gists. The playbook's deployment and markdown standards name the old jobs too; they follow in a playbook pull request. ## Before merging The main ruleset must require `static` in place of scan_js, scan_ruby, lint and lint_docs. Until then this pull request waits on four checks no workflow reports any more. ## Verification - Full suite (`bin/parallel-rspec`): 6057 examples, 0 failures, example-count parity, coverage floor met, exit 0; pre-push hooks green. - Template invariants: the new example fails when the `!cancelled()` guard is removed from the ERB step (mutation checked). - `lefthook validate`: All good. - Merge with #1420 conflicts in one place: two adjacent rows of getting-started's scheduled-workflows table. Whichever merges second takes a one-line rebase.
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.
A pull request that changed a build input — Gemfile.lock among them, so
every weekly Dependabot group — built the production image twice: in
docker_build, with the GitHub Actions layer cache, and in image_scan,
from scratch, before scanning it. Both build the same image: the same
context, the same Dockerfile, no target, linux/amd64. image_scan's cold
build already proves the image builds, so docker_build's second build
proved nothing new and billed for it.
On a private fork the second build was also the expensive one. On the
fork's last full run, docker_build took 794 seconds, 742 of them in its
build step; image_scan's build of the same image on the same pull
request, plus its Trivy scan, took 370. The cache cost more than it
saved there — most likely the mode=max export on a small runner, though
that was not measured on its own.
docker_build now runs on every push to main, unfiltered, which remains
the merged tree's proof, and on a pull request only when its own
workflow changes, so a change to it is still tested before it lands.
image_scan's pull_request paths widen to the full build-input list
(Gemfile, .tool-versions and bin/docker-entrypoint join Dockerfile,
.dockerignore and Gemfile.lock), so it is each build-input pull
request's single image build. The pre-push hook still builds locally on
the same inputs.
The template invariants change first: docker_build's pull request paths
are now exactly its own workflow, image_scan's name every build input,
and a new example pins that the two workflows build the same image —
the fact the change rests on. The two trigger examples were red against
the old workflows; the same-image example was already green.
Touching image_scan.yml was the trigger #1404 waited for: its 18-line
header and the 27-line cache-scope comment shrink to two-line pointers.
The rationale already lived in /docs/developer/testing under #536; the
one point it lacked — why keying the cache on the base image's digest
would not help — moves there.
Closes #1404
Verification
docker_buildbecause its own file changed,image_scanbecause its file changed — so CI exercises the new trigger shapes, andworkflow_audit(zizmor) audits the edited workflows.