- Python 85.1%
- Dockerfile 8.5%
- Shell 6.4%
| .forgejo/workflows | ||
| docs | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| Dockerfile.e2e | ||
| README.md | ||
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):
- Only when a rule applies:
fast_path_rules(one line per deploy branch,<deploy branch>: <source> ...), or its one-rule shorthandfast_path_branch=<fast_path_deploy_branch>: push:<fast_path_branch>(default empty = off). The ref must berefs/heads/<deploy branch>of a rule and the event a push. - Candidates: commits whose tree equals
HEAD^{tree}. For apush:<branch>source, among the lastfast_path_depth(30) first-parent commits of that branch; forpull_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). - A candidate proves the tree when the Forgejo task API
(
/repos/{o}/{r}/actions/tasks) shows every job infast_path_jobswith its latest attemptsuccessin that source's run of that commit (pushon the branch, orpull_request) offast_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. - With
fast_path_images, the proven commit must also have every<registry>/<image>:<sha>in the registry (docker manifest inspect, thefast_path_registry_tokensecret in a private docker config); the first proven commit that does wins.pull_requestsources require it: a fork PR's run can pass but never publishes. - 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 meansfalse. - Token: the runner's own
github.tokenfirst; if that is refused, the optional caller-passed secretfast_path_token(the consumers passCI_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.