feat(ci): PR-run sources for the tested-tree fast path; dev reuses and retags the PR image #14

Merged
john merged 1 commit from feat/pr-image-dev-fast-path into main 2026-09-29 10:51:11 +00:00
Owner

What

Extends the tested-tree fast path (#13) so the dev push after a PR merge also skips tests and the Docker build, reusing the image the PR run built. John approved this on 2026-09-29. All new inputs are optional. With the old inputs, behaviour is unchanged except for one relaxation, listed under Behaviour changes.

tested-tree.yml and detect-changes.yml get the same new inputs. The step stays byte-identical in both, and the test still enforces that.

  • fast_path_rules: one line per deploy branch, <deploy branch>: <source> .... A source is either push:<branch> (that branch's push run) or pull_request (a PR run of a same-tree commit). The consumers use:
    main: push:dev pull_request
    dev: pull_request
    
    fast_path_branch / fast_path_deploy_branch remain as the one-rule shorthand <deploy>: push:<branch>.
  • Candidates:
    • For push:<branch>: same-tree commits among that branch's last 30 first-parent commits (as before).
    • For pull_request: same-tree commits among HEAD's last 30 ancestors. A merge's PR head is its second parent; a fast-forward's is HEAD itself. The step fetches by sha to deepen the shallow checkout, as actions/checkout does.
    • PR tasks are matched on event == pull_request. The task API reports their head_branch as #<n>, so branch is not matched.
  • Per (commit, source) proof: every fast_path_jobs job must have its latest attempt at success in that source's run of that commit. A pair with a skipped, failed, running or missing job proves nothing, and the step moves on to the next pair.
    • This matters on main after a fast-pathed dev. dev's own run has Frontend and Docker Build skipped, so that pair is passed over and a same-tree PR run proves the tree instead.
  • fast_path_images plus the secret fast_path_registry_token (and fast_path_registry_prefix / _username):
    • verified_sha becomes the first proven commit whose image(s) exist. This is a docker manifest inspect check, like the one in dokku-image-deploy.yml.
    • Credentials go into a private DOCKER_CONFIG, are never put on a command line or printed, and are deleted on exit.
    • pull_request sources require fast_path_images, because a fork PR can pass its tests but never publishes.
  • fast_path_retag: dev: on that branch, a verified commit other than HEAD is copied registry-side to :<HEAD sha> with docker buildx imagetools create (no pull, no build) and then read back. If the copy fails, the step reports tested_tree=false and the caller builds as today. Result: every dev sha still has its own tag, and nothing downstream changes.

README.md documents the rules, the per-run flow and the expected timings.

Behaviour changes (legacy inputs)

  • One relaxation: under #13, the first same-tree dev commit whose jobs were all seen decided the result. A failure there meant false, even if an older same-tree commit had passed. Now each same-tree commit stands alone, and any one that passed every job proves the tree. That is the same tree, so the failure was flakiness, the same as a retried job, which was already accepted. It is also what lets main pass over a fast-pathed dev run whose jobs are skipped.
  • Finding, not changed here: inside the reusable workflow, a caller's workflow_dispatch reads as workflow_call. Forward-deploy-web run #110 showed this: the dispatch passed the event check and was stopped only by the ref check. So the step cannot refuse a manual dispatch on main or dev.
    • Today, a dispatch on main would skip Frontend but not deploy, because Deploy Gate checks push in a normal job.
    • The consumer PRs now honour tested_tree only when their own github.event_name == 'push', and the README tells callers to do the same.

Validation

  • uv run --with pyyaml==6.0.3 python -m unittest discover -s tests: 44 pass. That is 11 new, and the 33 existing ones pass unmodified. They also pass under -o pipefail.
    • The new tests use real git repos (a PR branch, a --no-ff merge into dev, a promotion to main, shallow checkouts), with curl, docker and sleep mocked.
    • dev reuses the PR image and retags it. The test checks the exact imagetools create arguments and the read-back. It also checks that the auth in the private config decodes to publisher:token, that the token is never in the output, and that the config file is gone after the step.
    • A fast-forward needs no retag, and there is no retag unless the branch is listed.
    • These cases each give false: a failed retag, a missing PR image (the fork case), and a merge onto a moved dev (tree differs).
    • These PR-run cases also give false: a skipped job, a failed job, and a push run standing in for a PR run.
    • Rule validation: a PR source without images, retag without images, an unknown source, a malformed rule, an empty rule, source equal to the deploy branch, a non-rule ref, a refs/pull ref, and dispatch or pull_request events.
    • main after a fast-pathed dev falls through to the PR run, with no retag on main. main prefers dev's run, and if dev's image is pruned it uses the PR image.
    • Legacy inputs never call docker.
  • actionlint 1.7.12 with shellcheck: no new findings. The finding set is identical to origin/main's (14, all pre-existing: the ci label and SC2086 in the change filter). The two consumer ci.yml files are likewise unchanged in findings.
  • ruff check and ruff format pass on tests/.
  • Nothing was deployed or run live.

Expected timings

Run Today After
PR ≈ 2 min ≈ 2 min. Unchanged, except the publish adds one registry push of the image just built.
dev push (clean merge) ≈ 1.5–2 min (tests + build + publish) ≈ 30–40 s: detect-changes ∥ Tested Tree (task API + manifest check + retag), everything else skipped
main promotion fast path ≈ 4.6 min (runs #116, #80) ≈ unchanged. The Dokku git:from-image phase (~3.3 min) is the floor.

Spec Drift Callouts

  • Retag lives in tested-tree.yml, not in a consumer job. The brief suggested the dev run retag with imagetools create. Doing it inside the Fast Path step:
    • puts it under the forgejo-ci tests;
    • avoids a new job-level if: (the runner mis-evaluates those on workflow_call jobs);
    • makes a failed retag fall back to the slow path automatically, instead of reddening dev.
  • main also accepts PR runs (main: push:dev pull_request). This is not optional. After a fast-pathed dev, dev's own run has the jobs skipped, so without a PR source every promotion would take the slow path.
    • On forward-deploy-web, the dev→main PR run usually proves the tree, with image :<dev sha> from the retag.
    • haskos-academy runs no PR CI for PRs into main, so there the feature PR run proves it and main deploys :<PR head sha>. That is the same bytes the dev retag points at.
  • fast_path_retag is a branch list, not a boolean, so one Fast Path job can serve both dev (retag) and main (no retag).

Rollout order

  1. This PR first. It is safe alone: nothing changes for any consumer until one passes the new inputs.
  2. Then forward-deploy-web and haskos-academy (feat/ci-pr-image → dev), in either order. Both pass fast_path_rules, fast_path_images and fast_path_retag, which only exist once this is on main. After this merges, push an empty re-trigger commit to each PR branch (as last time), so their PR runs use the new tested-tree.yml.
  3. The first PR merged into dev after a consumer PR lands is the first live test. Watch its Tested Tree log for retagged ... as ... and fast path: YES. Then promote to main and check the Deploy Gate says fast path:.

Not verifiable without a live run

  • Secrets in same-repo pull_request runs: Forgejo documents that same-repo PR runs get repo secrets and fork PR runs do not. I could not observe it. If REGISTRY_PUBLISH_TOKEN is absent, the consumer publish step warns and skips, and dev takes the slow path.
  • github.event.pull_request.head.repo.full_name and .head.sha: not confirmed as populated in Forgejo's payload. If empty, the step does not publish, which is again the slow path.
  • PR-run GITHUB_SHA: assumed to be the PR head. The task API's head_sha for PR runs is the head (e.g. forward-deploy-web #28, 522110a, the merge's second parent). The publish step refuses to tag if the checkout's HEAD is not pull_request.head.sha.
  • docker buildx imagetools create against registry-direct with publisher credentials. For a single-platform source it writes an index that wraps the same manifest, so the tag's digest differs but the image bytes are the same. Dokku pulls it like any tag. The deploy's alternate-tags check is by tag, so it is unaffected.
  • Fetch-by-sha deepening against Forgejo: actions/checkout relies on the same thing, so I expect it to work.

Caveat

PR runs now publish one tag per PR head push. That is more tags in haskytech/<repo>/<image>, each sharing layers with the rest. A registry cleanup rule (keep N / older than X days, excluding deployed shas) would be worth adding. It is not part of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CS99mH2YQv9t6iPuAbr21S

## What Extends the tested-tree fast path (#13) so the **`dev` push after a PR merge** also skips tests and the Docker build, reusing the image the PR run built. John approved this on 2026-09-29. All new inputs are optional. With the old inputs, behaviour is unchanged except for one relaxation, listed under Behaviour changes. `tested-tree.yml` and `detect-changes.yml` get the same new inputs. The step stays byte-identical in both, and the test still enforces that. - **`fast_path_rules`**: one line per deploy branch, `<deploy branch>: <source> ...`. A source is either `push:<branch>` (that branch's push run) or `pull_request` (a PR run of a same-tree commit). The consumers use: ``` main: push:dev pull_request dev: pull_request ``` `fast_path_branch` / `fast_path_deploy_branch` remain as the one-rule shorthand `<deploy>: push:<branch>`. - **Candidates**: - For `push:<branch>`: same-tree commits among that branch's last 30 first-parent commits (as before). - For `pull_request`: same-tree commits among HEAD's last 30 ancestors. A merge's PR head is its second parent; a fast-forward's is HEAD itself. The step fetches by sha to deepen the shallow checkout, as `actions/checkout` does. - PR tasks are matched on `event == pull_request`. The task API reports their `head_branch` as `#<n>`, so branch is not matched. - **Per (commit, source) proof**: every `fast_path_jobs` job must have its latest attempt at `success` in that source's run of that commit. A pair with a skipped, failed, running or missing job proves nothing, and the step moves on to the next pair. - This matters on `main` after a fast-pathed `dev`. dev's own run has Frontend and Docker Build *skipped*, so that pair is passed over and a same-tree PR run proves the tree instead. - **`fast_path_images`** plus the secret `fast_path_registry_token` (and `fast_path_registry_prefix` / `_username`): - `verified_sha` becomes the first proven commit whose image(s) exist. This is a `docker manifest inspect` check, like the one in `dokku-image-deploy.yml`. - Credentials go into a private `DOCKER_CONFIG`, are never put on a command line or printed, and are deleted on exit. - `pull_request` sources **require** `fast_path_images`, because a fork PR can pass its tests but never publishes. - **`fast_path_retag: dev`**: on that branch, a verified commit other than HEAD is copied registry-side to `:<HEAD sha>` with `docker buildx imagetools create` (no pull, no build) and then read back. If the copy fails, the step reports `tested_tree=false` and the caller builds as today. Result: every dev sha still has its own tag, and nothing downstream changes. `README.md` documents the rules, the per-run flow and the expected timings. ## Behaviour changes (legacy inputs) - **One relaxation:** under #13, the first same-tree dev commit whose jobs were all seen decided the result. A failure there meant `false`, even if an older same-tree commit had passed. Now each same-tree commit stands alone, and any one that passed every job proves the tree. That is the same tree, so the failure was flakiness, the same as a retried job, which was already accepted. It is also what lets `main` pass over a fast-pathed dev run whose jobs are skipped. - **Finding, not changed here:** inside the reusable workflow, a caller's `workflow_dispatch` reads as `workflow_call`. Forward-deploy-web run #110 showed this: the dispatch passed the event check and was stopped only by the ref check. So the step cannot refuse a manual dispatch on `main` or `dev`. - Today, a dispatch on `main` would skip Frontend but not deploy, because Deploy Gate checks `push` in a normal job. - The consumer PRs now honour `tested_tree` only when their own `github.event_name == 'push'`, and the README tells callers to do the same. ## Validation - `uv run --with pyyaml==6.0.3 python -m unittest discover -s tests`: **44 pass**. That is 11 new, and the 33 existing ones pass unmodified. They also pass under `-o pipefail`. - The new tests use real git repos (a PR branch, a `--no-ff` merge into dev, a promotion to main, shallow checkouts), with `curl`, `docker` and `sleep` mocked. - dev reuses the PR image and retags it. The test checks the exact `imagetools create` arguments and the read-back. It also checks that the auth in the private config decodes to publisher:token, that the token is never in the output, and that the config file is gone after the step. - A fast-forward needs no retag, and there is no retag unless the branch is listed. - These cases each give `false`: a failed retag, a missing PR image (the fork case), and a merge onto a moved dev (tree differs). - These PR-run cases also give `false`: a skipped job, a failed job, and a push run standing in for a PR run. - Rule validation: a PR source without images, retag without images, an unknown source, a malformed rule, an empty rule, source equal to the deploy branch, a non-rule ref, a `refs/pull` ref, and dispatch or `pull_request` events. - main after a fast-pathed dev falls through to the PR run, with no retag on main. main prefers dev's run, and if dev's image is pruned it uses the PR image. - Legacy inputs never call docker. - `actionlint` 1.7.12 with shellcheck: no new findings. The finding set is identical to `origin/main`'s (14, all pre-existing: the `ci` label and SC2086 in the change filter). The two consumer `ci.yml` files are likewise unchanged in findings. - `ruff check` and `ruff format` pass on `tests/`. - **Nothing was deployed or run live.** ## Expected timings | Run | Today | After | |---|---|---| | PR | ≈ 2 min | ≈ 2 min. Unchanged, except the publish adds one registry push of the image just built. | | `dev` push (clean merge) | ≈ 1.5–2 min (tests + build + publish) | **≈ 30–40 s**: detect-changes ∥ Tested Tree (task API + manifest check + retag), everything else skipped | | `main` promotion | fast path ≈ 4.6 min (runs #116, #80) | ≈ unchanged. The Dokku `git:from-image` phase (~3.3 min) is the floor. | ## Spec Drift Callouts - **Retag lives in `tested-tree.yml`, not in a consumer job.** The brief suggested the dev run retag with `imagetools create`. Doing it inside the Fast Path step: - puts it under the forgejo-ci tests; - avoids a new job-level `if:` (the runner mis-evaluates those on `workflow_call` jobs); - makes a failed retag fall back to the slow path automatically, instead of reddening dev. - **`main` also accepts PR runs** (`main: push:dev pull_request`). This is not optional. After a fast-pathed dev, dev's own run has the jobs *skipped*, so without a PR source every promotion would take the slow path. - On forward-deploy-web, the dev→main PR run usually proves the tree, with image `:<dev sha>` from the retag. - haskos-academy runs no PR CI for PRs into `main`, so there the feature PR run proves it and main deploys `:<PR head sha>`. That is the same bytes the dev retag points at. - **`fast_path_retag` is a branch list**, not a boolean, so one Fast Path job can serve both `dev` (retag) and `main` (no retag). ## Rollout order 1. **This PR first.** It is safe alone: nothing changes for any consumer until one passes the new inputs. 2. Then forward-deploy-web and haskos-academy (`feat/ci-pr-image` → `dev`), in either order. Both pass `fast_path_rules`, `fast_path_images` and `fast_path_retag`, which only exist once this is on `main`. **After this merges, push an empty re-trigger commit to each PR branch** (as last time), so their PR runs use the new `tested-tree.yml`. 3. The first PR merged into dev *after* a consumer PR lands is the first live test. Watch its Tested Tree log for `retagged ... as ...` and `fast path: YES`. Then promote to main and check the Deploy Gate says `fast path:`. ## Not verifiable without a live run - **Secrets in same-repo `pull_request` runs:** Forgejo documents that same-repo PR runs get repo secrets and fork PR runs do not. I could not observe it. If `REGISTRY_PUBLISH_TOKEN` is absent, the consumer publish step warns and skips, and dev takes the slow path. - **`github.event.pull_request.head.repo.full_name` and `.head.sha`:** not confirmed as populated in Forgejo's payload. If empty, the step does not publish, which is again the slow path. - **PR-run `GITHUB_SHA`:** assumed to be the PR head. The task API's `head_sha` for PR runs is the head (e.g. forward-deploy-web #28, `522110a`, the merge's second parent). The publish step refuses to tag if the checkout's HEAD is not `pull_request.head.sha`. - **`docker buildx imagetools create` against `registry-direct` with publisher credentials.** For a single-platform source it writes an index that wraps the same manifest, so the tag's digest differs but the image bytes are the same. Dokku pulls it like any tag. The deploy's `alternate-tags` check is by tag, so it is unaffected. - **Fetch-by-sha deepening against Forgejo:** `actions/checkout` relies on the same thing, so I expect it to work. ## Caveat PR runs now publish one tag per PR head push. That is more tags in `haskytech/<repo>/<image>`, each sharing layers with the rest. A registry cleanup rule (keep N / older than X days, excluding deployed shas) would be worth adding. It is not part of this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01CS99mH2YQv9t6iPuAbr21S
feat(ci): tested-tree rules with PR-run sources, registry check and dev retag
All checks were successful
Workflow Tests / shell-regression (pull_request) Successful in 13s
release-checkpoint Human-approved exact production head
55f97f265a
tested-tree.yml / detect-changes.yml (byte-identical step, new optional inputs):
- fast_path_rules: per deploy branch, a list of proof sources, push:<branch>
  or pull_request. fast_path_branch stays as the one-rule shorthand.
- Each (commit, source) pair must pass every fast_path_jobs job on its own
  (latest attempt success in the task API); a skipped pair proves nothing.
- fast_path_images + fast_path_registry_token: verified_sha must have its
  image(s) in the registry (docker manifest inspect, private docker config).
  Required for pull_request sources (a fork PR passes but never publishes).
- fast_path_retag: on the listed branches, copy the verified image to
  :<HEAD sha> registry-side (buildx imagetools create); failure = false.

11 new mocked tests (44 total).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CS99mH2YQv9t6iPuAbr21S
john merged commit d3a61593ac into main 2026-09-29 10:51:11 +00:00
john deleted branch feat/pr-image-dev-fast-path 2026-09-29 10:51:12 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
haskytech/forgejo-ci!14
No description provided.