ci: enforce PR title and commit subjects in GitHub Actions (EXT-32) #308
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: CI | |
| on: # yamllint disable-line rule:truthy | |
| workflow_dispatch: | |
| workflow_call: | |
| inputs: | |
| base_ref: | |
| description: >- | |
| Commit comparison base: an exact 40-character SHA (for push events | |
| and closed-PR validation), a branch name (for workflow_dispatch with | |
| an explicit base), or empty (falls back to origin/main). The Test job | |
| exports this as BASE_REF so ci/validate_commit_range.sh uses the | |
| correct range instead of re-resolving it from scratch. | |
| required: false | |
| type: string | |
| default: "" | |
| pull_request_title_required: | |
| description: >- | |
| Set to true only for pull_request events. ci/validate_pull_request_title.sh | |
| treats any other value as "skip" and exits 0. Pass "false" for push, | |
| closed-PR, and workflow_dispatch events where there is no PR title to enforce. | |
| required: false | |
| type: boolean | |
| default: false | |
| pull_request_title: | |
| description: >- | |
| The pull request title to validate. Only meaningful when | |
| pull_request_title_required is true. Passed as an environment variable | |
| so the title is never interpolated as shell code — quotes, backticks, | |
| dollar signs, and Unicode are all safe. | |
| required: false | |
| type: string | |
| default: "" | |
| pull_request: | |
| types: [opened, synchronize, reopened, edited] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: app | |
| steps: | |
| - | |
| uses: actions/checkout@v7 | |
| with: | |
| # mix precommit calls ci/validate_commit_range.sh, which validates | |
| # every commit since the merge base. A full-history checkout lets the | |
| # script resolve that base locally without a second network fetch. | |
| fetch-depth: 0 | |
| # pull_request otherwise checks out GitHub's synthetic test-merge | |
| # commit (subject "Merge <sha> into <sha>"). Validate the | |
| # contributor's actual branch tip instead so that commit never | |
| # appears in the validated range. Falls back to github.sha for | |
| # non-PR triggers (workflow_dispatch, workflow_call from push/closed). | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| otp-version: "29.0.3" | |
| elixir-version: "1.20.3" | |
| # Without this, setup-beam's problem matchers promote every | |
| # compiler warning from deps (e.g. postgrex/rewrite's deprecated | |
| # `xref: [exclude: ...]`, yamerl's deprecated `catch ...` syntax - | |
| # both already at their latest published Hex versions, so not | |
| # fixable from here) into noisy GH Actions annotations. See #9. | |
| disable_problem_matchers: true | |
| - | |
| name: Cache deps/build | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| app/deps | |
| app/_build | |
| key: ${{ runner.os }}-mix-${{ hashFiles('app/mix.lock') }} | |
| - | |
| # mix precommit runs the full 11-step gate: | |
| # 1. ci/validate_pull_request_title.sh — PR title (when required) | |
| # 2. ci/validate_commit_range.sh — all commit subjects in the range | |
| # 3-4. Root format + test (the repo-management tooling itself) | |
| # 5-11. App deps.get, hex.audit, deps.audit, format, credo, | |
| # usage_rules.sync, and test. | |
| # BASE_REF carries the exact comparison base SHA so the commit-range | |
| # validator never has to guess. For pull_request events it is the exact | |
| # base SHA; for push events it is github.event.before; for | |
| # workflow_dispatch and unverified closes it is empty, and the validator | |
| # falls back to origin/main. | |
| # PULL_REQUEST_TITLE is set via env (never ${{ }} in run:) so PR titles | |
| # containing quotes, backticks, dollar signs, or Unicode are treated as | |
| # inert data, not executable shell code. | |
| name: Run full quality gate | |
| env: | |
| BASE_REF: >- | |
| ${{ inputs.base_ref != '' && inputs.base_ref | |
| || github.event.pull_request.base.sha }} | |
| PULL_REQUEST_TITLE_REQUIRED: >- | |
| ${{ inputs.pull_request_title_required | |
| || github.event_name == 'pull_request' }} | |
| PULL_REQUEST_TITLE: >- | |
| ${{ inputs.pull_request_title != '' && inputs.pull_request_title | |
| || github.event.pull_request.title }} | |
| run: mix precommit | |
| working-directory: . | |
| burrito_changes: | |
| # Building Burrito is deliberately reserved for changes that affect its | |
| # dependency graph or packaging path. The release workflow still builds | |
| # every target before publishing. | |
| # Exclude 'edited' events — a title-only change never alters file content, | |
| # so there are no Burrito-impacting diffs to check. | |
| if: >- | |
| github.event_name == 'pull_request' | |
| && github.event.action != 'closed' | |
| && github.event.action != 'edited' | |
| name: Detect Burrito-impacting changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| burrito_changed: ${{ steps.changes.outputs.burrito_changed }} | |
| steps: | |
| - | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - | |
| id: changes | |
| name: Check whether Burrito-impacting files changed | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- \ | |
| app/mix.exs \ | |
| app/mix.lock \ | |
| app/release/ \ | |
| ci/prepare_musl_nifs.sh \ | |
| ci/test_burrito_shared_loader.sh \ | |
| .github/workflows/ci.yaml \ | |
| .github/workflows/main.yaml | |
| then | |
| printf 'burrito_changed=false\n' >> "$GITHUB_OUTPUT" | |
| else | |
| printf 'burrito_changed=true\n' >> "$GITHUB_OUTPUT" | |
| fi | |
| burrito_linux_regression: | |
| # ci.yaml is also called by main.yaml after pushes and closed release PRs. | |
| # Keep this expensive native build as a pre-merge dependency-change gate; | |
| # the release workflow builds every target after the release PR merges. | |
| needs: [burrito_changes] | |
| if: needs.burrito_changes.outputs.burrito_changed == 'true' | |
| name: Burrito Linux shared-loader regression | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: app | |
| shell: bash | |
| steps: | |
| - | |
| uses: actions/checkout@v7 | |
| - | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| otp-version: "29.0.3" | |
| elixir-version: "1.20.3" | |
| disable_problem_matchers: true | |
| - | |
| uses: mlugg/setup-zig@v2.2.1 | |
| with: | |
| version: "0.16.0" | |
| - | |
| run: mix deps.get | |
| - | |
| name: Compile makeup_syntect with its host NIF | |
| env: | |
| MIX_ENV: prod | |
| run: mix deps.compile castore rustler_precompiled makeup_syntect | |
| - | |
| name: Pre-compile mdex_native with its musl NIF | |
| env: | |
| MIX_ENV: prod | |
| TARGET_ABI: musl | |
| run: mix deps.compile mdex_native | |
| - | |
| name: Install and repair the musl NIFs | |
| run: ../ci/prepare_musl_nifs.sh | |
| - | |
| name: Build the native linux_x86_64 target | |
| run: MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release lc | |
| - | |
| name: Test the packaged binary across users | |
| timeout-minutes: 2 | |
| run: ../ci/test_burrito_shared_loader.sh ./burrito_out/lc_linux_x86_64 | |
| conventional_commits: | |
| # Temporary compatibility job retained while branch protection still lists | |
| # "Validate Commit Subjects" as a required check. The Test job above runs | |
| # the same ci/validate_commit_range.sh via mix precommit and is the | |
| # authoritative gate; this job will be removed in EXT-33 after the required- | |
| # check set is migrated to "Test" only. | |
| # Run for all direct triggers (pull_request, workflow_dispatch); skip only | |
| # when main.yaml calls this workflow for a non-PR event and explicitly sets | |
| # pull_request_title_required to false — those are post-merge runs where | |
| # there is no open PR to gate on Validate Commit Subjects. | |
| if: github.event_name != 'workflow_call' || inputs.pull_request_title_required | |
| name: Validate Commit Subjects | |
| runs-on: ubuntu-latest | |
| steps: | |
| - | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| # pull_request's default ref is the synthetic refs/pull/N/merge test-merge | |
| # commit (subject "Merge <sha> into <sha>"), not the PR branch tip - check | |
| # out the real head SHA instead so that commit is never in the validated | |
| # range. Falls back to github.sha for non-PR triggers (workflow_dispatch/ | |
| # workflow_call), where github.event.pull_request is unset. | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - | |
| env: | |
| BASE_REF: >- | |
| ${{ inputs.base_ref != '' && inputs.base_ref | |
| || github.event.pull_request.base.sha }} | |
| run: ./ci/validate_commit_range.sh |