CI runner image for Forgejo pipelines
  • Python 85.1%
  • Dockerfile 8.5%
  • Shell 6.4%
Find a file
john d3a61593ac
All checks were successful
Workflow Tests / shell-regression (push) Successful in 17s
Build CI Image / Build and Push (push) Successful in 1m30s
Merge pull request 'feat(ci): PR-run sources for the tested-tree fast path; dev reuses and retags the PR image' (#14) from feat/pr-image-dev-fast-path into main
2026-09-29 10:51:10 +00:00
.forgejo/workflows feat(ci): tested-tree rules with PR-run sources, registry check and dev retag 2026-09-29 18:48:55 +08:00
docs fix(deploy): direct registry downloads and bounded pull retries [skip cd] 2026-09-22 20:09:05 +08:00
scripts ci: update build workflow for Contabo migration 2026-07-18 19:03:23 +08:00
tests feat(ci): tested-tree rules with PR-run sources, registry check and dev retag 2026-09-29 18:48:55 +08:00
.gitignore chore: drop committed bytecode, ignore __pycache__ 2026-09-24 18:29:06 +08:00
AGENTS.md chore: add AGENTS.md symlink for cross-harness invariance [skip cd] (#1) 2026-06-28 06:30:41 +00:00
CLAUDE.md feat(ci): tested-tree fast path, image_tag reuse, timestamped Dokku output 2026-09-29 17:13:22 +08:00
Dockerfile fix: arch-aware tool downloads via TARGETARCH — enables native arm64 runner image 2026-08-04 04:32:58 +08:00
Dockerfile.e2e fix: use local tag in Dockerfile.e2e (registry is HTTP-only) 2026-05-16 22:21:46 +08:00
README.md feat(ci): tested-tree rules with PR-run sources, registry check and dev retag 2026-09-29 18:48:55 +08:00

forgejo-ci

The forgejo-ci:latest runner image (see CLAUDE.md) and the reusable workflows every Haskytech repo calls at @main:

Workflow What it does
.forgejo/workflows/detect-changes.yml has_code change filter; optional tested-tree fast path
.forgejo/workflows/tested-tree.yml The tested-tree fast path alone, for repos with their own change filter
.forgejo/workflows/dokku-image-deploy.yml Build, push, deploy to Dokku via git:from-image; optional image_tag reuse
.forgejo/workflows/deploy-key-healthcheck.yml Deploy-key health check

Every consumer pins @main, so every change here must be backward compatible: new inputs are optional and default to the old behaviour. Mocked shell tests live in tests/ (uv run --with pyyaml==6.0.3 python -m unittest discover -s tests -v).

Tested-tree fast path

A dev -> main promotion is a merge commit whose git tree is identical to the dev head that CI already tested, built and published. The fast path lets that main push skip tests and image builds and deploy the image the dev run published.

How it decides (tested-tree.yml, or detect-changes.yml with the same inputs; the step is byte-identical in both, enforced by tests/test_tested_tree.py):

  1. Only when a rule applies: fast_path_rules (one line per deploy branch, <deploy branch>: <source> ...), or its one-rule shorthand fast_path_branch = <fast_path_deploy_branch>: push:<fast_path_branch> (default empty = off). The ref must be refs/heads/<deploy branch> of a rule and the event a push.
  2. Candidates: commits whose tree equals HEAD^{tree}. For a push:<branch> source, among the last fast_path_depth (30) first-parent commits of that branch; for pull_request, among the last 30 commits of HEAD's full ancestry (a merge's PR head is its second parent; a fast-forward's is HEAD itself).
  3. A candidate proves the tree when the Forgejo task API (/repos/{o}/{r}/actions/tasks) shows every job in fast_path_jobs with its latest attempt success in that source's run of that commit (push on the branch, or pull_request) of fast_path_workflow (ci.yml). Each (commit, source) pair stands alone; one whose jobs were skipped, failed or are still running proves nothing. The task API is used rather than commit statuses because Forgejo reports a skipped job's commit status as success.
  4. With fast_path_images, the proven commit must also have every <registry>/<image>:<sha> in the registry (docker manifest inspect, the fast_path_registry_token secret in a private docker config); the first proven commit that does wins. pull_request sources require it: a fork PR's run can pass but never publishes.
  5. With fast_path_retag: <deploy branch>, on that branch a winner other than HEAD is copied registry-side (docker buildx imagetools create, no pull) to :<HEAD sha> and read back; a failed copy means false.
  6. Token: the runner's own github.token first; if that is refused, the optional caller-passed secret fast_path_token (the consumers pass CI_FORGEJO_TOKEN).

Outputs: tested_tree ('true' or 'false') and verified_sha (the proving commit). Any doubt means false: API unreachable, no candidate with every job passed, image missing, retag failed, or the list not in newest-first order. The step also runs with continue-on-error, so it can never fail the caller's run. When it is unsure, the caller takes the normal path.

Manual dispatch: inside the reusable workflow github.event_name reads workflow_call for a caller's workflow_dispatch too (observed on forward-deploy-web run #110), so the step cannot tell them apart. Callers must honour tested_tree only when their own github.event_name == 'push' (see the wiring below).

What takes the normal path automatically: a direct push, a merge that had to resolve conflicts, landed on a moved dev or carries a main-only commit (tree differs), a merge made before the proving run finished, a docs-only head whose code jobs were skipped, or a fork PR (never publishes).

Consumer wiring (reference: forward-deploy-web, haskos-academy). Build once, in the PR run:

fast-path:
  name: Fast Path
  uses: haskytech/forgejo-ci/.forgejo/workflows/tested-tree.yml@main
  with:
    fast_path_rules: |
      main: push:dev pull_request
      dev: pull_request
    fast_path_jobs: |
      Frontend
      Docker Build          # must be the job that PUBLISHED the image
    fast_path_images: forward-deploy-web
    fast_path_retag: dev
  secrets:
    fast_path_token: ${{ secrets.CI_FORGEJO_TOKEN }}
    fast_path_registry_token: ${{ secrets.REGISTRY_PUBLISH_TOKEN }}

# test/build jobs (a manual dispatch must never skip them):
#   if: !cancelled() && ... &&
#       !(github.event_name == 'push' && needs.fast-path.outputs.tested_tree == 'true')
# deploy (dokku-image-deploy.yml):
#   image_tag: ${{ needs.fast-path.outputs.verified_sha || github.sha }}

Docker Build publishes <registry>/<image>:<sha> (the same registry-direct.haskytech.com/<owner>/<repo>/<image> path and publisher credentials dokku-image-deploy.yml uses) on a dev push that took the normal path and on a PR run whose head repo is this repo (github.event.pull_request.head.repo.full_name == github.repository), tagged with the PR head sha after checking the checkout is that commit, and skipping a tag that already exists. The flow per change:

Run What happens Expected time
PR tests + Docker Build + smoke, publishes :<PR head> ≈ 2 min (unchanged)
dev push (clean merge) Tested Tree finds the PR run, retags :<PR head> as :<dev sha>; tests and Docker Build skipped ≈ 30–40 s
main push (promotion) Tested Tree finds dev's run or a same-tree PR run; deploys that image ≈ unchanged (Dokku phase dominates)

The older single-branch wiring (fast_path_branch: dev, dev publishes) keeps working unchanged.

dokku-image-deploy.yml image_tag

Empty (default) means github.sha: build, push, deploy, as before. Any other value skips build+push, checks with docker manifest inspect that every image exists at that tag (three tries; fails with a clear error, before any SSH, if one is missing), deploys it, and asserts the running container's alternate-tags label carries that tag. Passing github.sha itself behaves like the default.

The "Deploy to Dokku" step streams Dokku's output live with a UTC HH:MM:SS prefix on every line (route resolution, config check, manifest check and ps:inspect are stamped too), so a slow pull, checks wait or retire wait shows up in the log. The pull-retry logic reads the raw, unstamped copy of the output from a temp file.