Skip to content

culture(release): promote heritage engine + wow anchor to main #908

culture(release): promote heritage engine + wow anchor to main

culture(release): promote heritage engine + wow anchor to main #908

Workflow file for this run

name: pr-gate
# Server-side mirror of the local pre-commit hook's enforcement.
# Closes Web UI bypass paths for branch-scope and base-branch rules.
#
# Triggers on every pull_request event (no branch filter) so a PR with
# the wrong base — e.g. culture/germany -> main bypassing culture/release —
# is also caught here. The base-branch-check job decides if the head/base
# pair is even allowed.
#
# Adding a check: define a new job below and run a tests/validate_pr_*.py
# helper. Keep CI logic in Python (testable) and yaml as orchestration only.
#
# The report job turns a rejection into instruction: it posts the
# validators' prescriptive remedy as a PR comment, writes it to the job
# summary, and fails its own check when routing is broken. The remedy
# then reaches an agent that opened the PR via the GitHub API whether it
# reads the check conclusion, the run log, the job summary, or the PR
# comment -- four channels, because the failure has to be self-fixable.
#
# Note on the validation stamp: this workflow used to include a third job
# (stamp-tree-hash-check) that verified .validation-stamp equalled the
# tree hash of HEAD. That gate was dropped because API-driven PR pushes
# (via the GitHub MCP / web UI) cannot run the local pre-commit hook to
# refresh the stamp, which created persistent friction. The remaining
# protection is the cultures - Validation stamp gate in validate.yml
# (file existence check, regions-only) plus every actual validator job in
# validate.yml. Branch protection still requires those checks before merge.
on:
pull_request:
permissions:
contents: read
jobs:
base-branch-check:
name: pr-gate - Allowed base for head
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Verify PR base matches head's allowed target
run: |
python3 tests/validate_pr_base.py \
"${{ github.head_ref }}" \
"${{ github.base_ref }}"
branch-scope-check:
name: pr-gate - Diff within branch scope
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Need history back to the merge base so `base...HEAD` works.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Verify diff stays inside head branch's allowed paths
run: |
python3 tests/validate_pr_scope.py \
"${{ github.head_ref }}" \
"origin/${{ github.base_ref }}"
brownfield-check:
name: pr-gate - Brownfield migration ("if you touch, you migrate")
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Need history back to the merge base so `base...HEAD` works,
# same reason as branch-scope-check.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pyyaml
- name: Verify touched files are on the khai-lexicon schema
run: |
python3 tests/validate_pr_brownfield.py \
"origin/${{ github.base_ref }}"
report:
name: pr-gate - Routing remedy
needs: [base-branch-check, branch-scope-check]
# Always runs for same-repo PRs: on failure it posts the prescriptive fix
# as a PR comment (so an agent that opened the PR via the API still sees
# it); on success it marks any earlier remedy comment resolved. Forked PRs
# get a read-only token, so they are skipped -- they still get the check
# failures and the step summary.
if: always() && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build routing remedy
id: build
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
run: |
# Capture both validators WITHOUT aborting. GitHub runs `run:`
# blocks under `set -e`, so a failing command substitution
# (var=$(... exit 1)) kills the step -- exactly the
# routing-rejected case this job exists to report. Drop `set -e`
# around the captures; keep the recorded exit codes.
set +e
base_out=$(python3 tests/validate_pr_base.py "$HEAD_REF" "$BASE_REF" 2>&1)
base_rc=$?
scope_out=$(python3 tests/validate_pr_scope.py "$HEAD_REF" "origin/$BASE_REF" 2>&1)
scope_rc=$?
set -e
if [ "$base_rc" -eq 0 ] && [ "$scope_rc" -eq 0 ]; then
echo "status=pass" >> "$GITHUB_OUTPUT"
else
echo "status=fail" >> "$GITHUB_OUTPUT"
fi
{
echo "## pr-gate routing"
echo
echo "Branch routing is decided by the *operation*, not the file types a"
echo "change happens to touch. A failure here is fixed by re-routing the"
echo "PR (branch kind / base), never by editing validators or workflows."
echo
echo "### Allowed base for head"
echo '```'
echo "$base_out"
echo '```'
echo "### Diff within branch scope"
echo '```'
echo "$scope_out"
echo '```'
echo
echo "Pre-flight next time, before opening the PR:"
echo '```'
echo "python tests/branch_scope.py check-pr --head <head> --base <base>"
echo '```'
echo "(or \`advise --op <operation>\` before \`git checkout -b\`)."
echo "A failure says whether it is fixable in place -- a wrong base"
echo "(retarget the PR) or a wrong head (close and re-open: a PR's"
echo "head branch cannot be changed)."
} > remedy.md
# On a routing FAILURE, surface the remedy in the run log and
# the job summary too -- actionable from the Actions UI and the
# API, not only from the PR comment.
if [ "$base_rc" -ne 0 ] || [ "$scope_rc" -ne 0 ]; then
cat remedy.md
cat remedy.md >> "$GITHUB_STEP_SUMMARY"
fi
- name: Post or update PR comment
uses: actions/github-script@v7
env:
STATUS: ${{ steps.build.outputs.status }}
with:
script: |
const fs = require('fs');
const marker = '<!-- pr-gate-remedy -->';
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (process.env.STATUS === 'pass') {
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id,
body: marker + '\n## pr-gate routing\n\n✅ Routing checks pass.',
});
}
return;
}
const body = marker + '\n' + fs.readFileSync('remedy.md', 'utf8');
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id, body,
});
} else {
await github.rest.issues.createComment({
owner, repo, issue_number, body,
});
}
# The remedy is in the run log, the job summary, and the PR comment
# by now -- this step makes "pr-gate - Routing remedy" itself go red
# so a routing failure cannot be missed by a glance at the check list.
- name: Fail when routing is broken
if: steps.build.outputs.status == 'fail'
run: |
echo "::error title=pr-gate routing rejected this PR::Branch/base routing is invalid. The prescriptive fix is in this job summary, the run log above, and a PR comment. Re-route the PR (branch kind / base) -- do not edit validators or workflows."
exit 1