thermograph/.forgejo/workflows/pr-build.yml
Emi Griffith ea613e33ca
All checks were successful
PR build (required check) / changes (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 10s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 3s
flow: squash into dev, fast-forward the promotions
Forgejo cannot scope merge styles per branch — they live on the repo's
pull-request unit, and BranchProtection carries no merge-style or linear-history
field in 9.0.3. So the chain is enforced repo-wide by allowing exactly two
styles: squash and fast-forward-only. Merge commits and rebase merges are off.

Squash belongs on the short-lived hop. Squashing dev into main would put a
commit on main that does not have dev's commits as ancestors, freezing
merge-base(dev, main); every later promotion would then list all commits since
that stale point and re-apply changes main already has. Fast-forwarding the
promotions instead means a promotion creates no commit at all, so the three
branches keep identical history rather than merely identical trees, and
`release ⊆ main ⊆ dev` holds permanently.

Rebase-updates stay enabled. They are a different setting from rebase merges,
and BRANCHING.md is right that the merge queue 403s without them.

Also adds `release` to pr-build.yml's pull_request branches. This is step one of
the order BRANCHING.md prescribes for closing the missing check on release:
teach the workflow to fire there, confirm on a real PR that the context appears,
and only then require it. Requiring a context nothing produces is what makes a
production promotion unmergeable with a 405 that names no cause.
2026-08-01 17:14:45 -07:00

98 lines
3.3 KiB
YAML

name: PR build (required check)
# Monorepo port of the app repos' pr-build.yml: ONE workflow, ONE stable
# required-check name (`gate`), any number of domains. A `changes` job diffs
# the PR against its base; per-domain jobs run only for domains the PR
# touches; `gate` reduces their results into the single status branch
# protection requires.
#
# Deliberately NOT path-filtered at the workflow level: a path-filtered
# required check that never fires leaves the PR stuck "expected -- waiting for
# status" forever, so auto-merge never proceeds. An always-running gate whose
# domain jobs skip cleanly gives the same compute savings without the
# deadlock.
#
# One-time web UI setup (Settings -> Branches, rules for `dev` and `main`):
# -> enable "Enable Status Check", list this workflow's "gate" job
# -> merge style: squash
# Then on a ready PR: "Auto merge when checks succeed".
on:
pull_request:
branches: [dev, main, release]
concurrency:
group: pr-build-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
changes:
runs-on: docker
outputs:
backend: ${{ steps.diff.outputs.backend }}
frontend: ${{ steps.diff.outputs.frontend }}
observability: ${{ steps.diff.outputs.observability }}
steps:
- uses: actions/checkout@v4
with:
# The PR base commit isn't reachable from a depth-1 clone; full
# history keeps the diff computable however far the branch diverged.
fetch-depth: 0
- name: Detect changed domains
id: diff
run: |
set -euo pipefail
base="${{ github.event.pull_request.base.sha }}"
changed=$(git diff --name-only "$base" "$GITHUB_SHA")
echo "changed files:"; printf '%s\n' "$changed"
for d in backend frontend observability; do
if printf '%s\n' "$changed" | grep -q "^$d/"; then
echo "$d=true" >> "$GITHUB_OUTPUT"
else
echo "$d=false" >> "$GITHUB_OUTPUT"
fi
done
build-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
uses: ./.forgejo/workflows/build.yml
with:
domain: backend
build-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
uses: ./.forgejo/workflows/build.yml
with:
domain: frontend
validate-observability:
needs: changes
if: needs.changes.outputs.observability == 'true'
uses: ./.forgejo/workflows/observability-validate.yml
gate:
name: gate
# always(): skipped domain jobs (nothing changed there) must not skip the
# gate itself -- the required check has to report on EVERY PR.
if: always()
needs: [changes, build-backend, build-frontend, validate-observability]
runs-on: docker
steps:
- name: Reduce domain results
run: |
set -euo pipefail
ok=1
for pair in \
"backend=${{ needs.build-backend.result }}" \
"frontend=${{ needs.build-frontend.result }}" \
"observability=${{ needs.validate-observability.result }}"; do
echo "$pair"
case "${pair#*=}" in
success|skipped) ;;
*) ok=0 ;;
esac
done
[ "$ok" = 1 ] || { echo "a changed domain's check failed"; exit 1; }