Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ name: ci

on:
pull_request:
# `edited` included: the PR TITLE is the squash subject the pr-title job
# gates, so a title edit must re-evaluate CI (AND-1960) — the default
# types (opened/synchronize/reopened) never see it.
types: [opened, synchronize, reopened, edited]
push:
branches: [main]
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# PR-event runs get a PER-RUN group on purpose (no dedup, nothing to cancel):
# a run that ends "cancelled" on the head SHA can strand the required `ci`
# context (AND-1960), and both cancel-in-progress settings can produce one —
# true cancels in-flight runs, false still cancels queued ones. Push runs
# keep the shared group + cancellation; nothing gates on them.
group: ci-${{ github.ref }}-${{ github.event_name == 'push' && 'push' || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'push' }}

permissions:
contents: read
Expand Down Expand Up @@ -41,6 +50,38 @@ jobs:
with:
configFile: .commitlintrc.json

pr-title:
name: pr title (squash subject)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v7
# This repo squash-merges: the PR TITLE becomes the commit subject on
# main, so per-commit commitlint alone cannot protect main. Lint the
# title against the same config the commit lint uses.
# Title goes through env (never template-interpolated into the script).
# Isolated temp prefix (Codex P2 on anders-repo-template#14): npm install
# in the checkout would first install the PROJECT's own dependency tree —
# a private/heavy dep could fail the gate on a valid title. The prefix
# holds only the two exact-pinned lint packages (--ignore-scripts), and
# the config is copied beside them because commitlint resolves `extends`
# from the CONFIG FILE's directory.
- name: lint PR title against commitlint config
env:
TITLE: ${{ github.event.pull_request.title }}
PRNUM: ${{ github.event.pull_request.number }}
run: |
mkdir -p "$RUNNER_TEMP/title-lint"
cp .commitlintrc.json "$RUNNER_TEMP/title-lint/"
npm install --prefix "$RUNNER_TEMP/title-lint" --no-save --no-audit --no-fund \
--ignore-scripts @commitlint/cli@21.2.1 @commitlint/config-conventional@21.2.0
# Lint the REAL squash subject: GitHub appends " (#N)" on squash-merge
# (Codex P2 on anders-skills-lite#6), so a title at the length limit
# would land over it on main.
printf '%s (#%s)\n' "$TITLE" "$PRNUM" \
| npx --prefix "$RUNNER_TEMP/title-lint" --no-install commitlint \
--config "$RUNNER_TEMP/title-lint/.commitlintrc.json"

gitleaks:
name: secret scan
runs-on: ubuntu-latest
Expand Down Expand Up @@ -111,14 +152,14 @@ jobs:

ci:
name: ci
needs: [commitlint, gitleaks, shellcheck, python, node, swift]
needs: [commitlint, pr-title, gitleaks, shellcheck, python, node, swift]
if: always()
runs-on: ubuntu-latest
steps:
- name: aggregate
run: |
# Fail if any required upstream job failed (skipped is OK for stack-conditional jobs)
for r in "${{ needs.commitlint.result }}" "${{ needs.gitleaks.result }}" \
for r in "${{ needs.commitlint.result }}" "${{ needs.pr-title.result }}" "${{ needs.gitleaks.result }}" \
"${{ needs.shellcheck.result }}" \
"${{ needs.python.result }}" "${{ needs.node.result }}" "${{ needs.swift.result }}"; do
if [ "$r" = "failure" ] || [ "$r" = "cancelled" ]; then
Expand Down