All checks were successful
secrets-guard / encrypted (push) Successful in 12s
shell-lint / shellcheck (push) Successful in 12s
Build + push images (Forgejo registry) / build-push (frontend) (push) Successful in 44s
Build + push images (Forgejo registry) / build-push (backend) (push) Successful in 2m27s
Deploy / deploy (backend) (push) Successful in 2m55s
Deploy / deploy (frontend) (push) Successful in 3m6s
Co-authored-by: admin_emi <admin_emi@noreply.dev.jinemi.com> Co-committed-by: admin_emi <admin_emi@noreply.dev.jinemi.com>
129 lines
4.8 KiB
YAML
129 lines
4.8 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:
|
|
# `release` included: it is the branch that deploys to PROD, and it used to
|
|
# be the least-checked branch in the repo. A PR into release ran neither a
|
|
# build nor a test — only the two standalone lint workflows — so the intended
|
|
# promotion path was the one with no gate on it.
|
|
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
|
|
|
|
# NOT domain-gated and NOT `needs: changes`: both are repo-wide invariants that
|
|
# must hold on every PR regardless of which directories it touches. A plaintext
|
|
# secret or a broken script can arrive in a commit that changes no app domain at
|
|
# all — precisely the case where every domain job skips and `gate` used to pass
|
|
# vacuously.
|
|
lint-shell:
|
|
uses: ./.forgejo/workflows/shell-lint.yml
|
|
|
|
guard-secrets:
|
|
uses: ./.forgejo/workflows/secrets-guard.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,
|
|
lint-shell, guard-secrets]
|
|
runs-on: docker
|
|
steps:
|
|
- name: Reduce domain results
|
|
run: |
|
|
set -euo pipefail
|
|
ok=1
|
|
# Domain jobs: `skipped` is a pass — it means the PR did not touch that
|
|
# domain, which is the whole point of the path filtering.
|
|
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
|
|
# Repo-wide invariants: `skipped` is a FAILURE. These have no `if:` and
|
|
# no `needs:`, so they run on every PR; a skip means the job did not
|
|
# execute and the invariant is therefore unverified, which must not be
|
|
# reported as a pass.
|
|
for pair in \
|
|
"shellcheck=${{ needs.lint-shell.result }}" \
|
|
"secrets-encrypted=${{ needs.guard-secrets.result }}"; do
|
|
echo "$pair"
|
|
case "${pair#*=}" in
|
|
success) ;;
|
|
*) ok=0 ;;
|
|
esac
|
|
done
|
|
[ "$ok" = 1 ] || { echo "a required check failed"; exit 1; }
|