Skip to content
Merged
Show file tree
Hide file tree
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
399 changes: 399 additions & 0 deletions .github/workflows/quality-baseline-update.yml

Large diffs are not rendered by default.

277 changes: 277 additions & 0 deletions .github/workflows/quality-ratchet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
name: Quality ratchet

# pull_request_target loads this workflow from protected main. The job never
# imports or executes PR code; it runs a protected-base judge over static files.
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]

permissions:
actions: read
contents: read
pull-requests: read
statuses: write

jobs:
quality-ratchet:
name: ruff + mypy (no new debt)
runs-on: ubuntu-latest
timeout-minutes: 20
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || 0 }}

steps:
- name: Check out the protected comparison graph
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install pinned quality tools
run: python -m pip install "ruff==0.15.10" "mypy==1.20.1"

- name: Resolve commits and protected judge
id: commits
shell: bash
run: |
set -euo pipefail
if [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid head SHA: $HEAD_SHA" >&2
exit 2
fi
if [[ "$BASE_SHA" =~ ^0{40}$ ]]; then
BASE_SHA=$(git rev-parse "${HEAD_SHA}^" 2>/dev/null || printf '%s' "$HEAD_SHA")
fi
if [[ ! "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid base SHA: $BASE_SHA" >&2
exit 2
fi
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$BASE_SHA"
fi
if ! git cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null; then
if [[ "$GITHUB_EVENT_NAME" == "pull_request_target" ]]; then
git fetch --no-tags origin \
"+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/quality-head"
else
git fetch --no-tags --depth=1 origin "$HEAD_SHA"
fi
fi
git cat-file -e "${BASE_SHA}^{commit}"
git cat-file -e "${HEAD_SHA}^{commit}"

JUDGE="$RUNNER_TEMP/protected-ci-quality-ratchet.py"
if git cat-file -e "${BASE_SHA}:scripts/ci_quality_ratchet.py" 2>/dev/null; then
git show "${BASE_SHA}:scripts/ci_quality_ratchet.py" > "$JUDGE"
JUDGE_SOURCE="protected-base"
elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
# One-time bootstrap for the merge that first introduces the gate.
git show "${HEAD_SHA}:scripts/ci_quality_ratchet.py" > "$JUDGE"
JUDGE_SOURCE="bootstrap-head"
else
echo "Protected base does not contain the quality judge" >&2
exit 2
fi
python -m py_compile "$JUDGE"
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "judge=$JUDGE" >> "$GITHUB_OUTPUT"
echo "judge_source=$JUDGE_SOURCE" >> "$GITHUB_OUTPUT"

- name: Bind the event to the current protected-main tip
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
MAIN_REF="$RUNNER_TEMP/current-main-ref.json"
gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" > "$MAIN_REF"
ARGS=(--verify-main-ref-response "$MAIN_REF")
if [[ "$GITHUB_EVENT_NAME" == "pull_request_target" ]]; then
PULL_REQUEST="$RUNNER_TEMP/current-pull-request.json"
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > "$PULL_REQUEST"
ARGS+=(
--verify-pull-request-response "$PULL_REQUEST"
--expected-current-main-sha "${{ steps.commits.outputs.base_sha }}"
--expected-current-head-sha "${{ steps.commits.outputs.head_sha }}"
)
else
ARGS+=(--expected-current-main-sha "${{ steps.commits.outputs.head_sha }}")
fi
python -I "${{ steps.commits.outputs.judge }}" "${ARGS[@]}"

- name: Verify protected policy authorization
id: authorization
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
echo "push_approved=true" >> "$GITHUB_OUTPUT"
echo "candidate=false" >> "$GITHUB_OUTPUT"
exit 0
fi

STATUS_JSON=$(gh api \
"repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/status")
STATUS_ENTRY=$(jq -c '
[.statuses[]
| select(.context == "quality-ratchet/policy-approved")
| select(.state == "success")
| select(.creator.login == "github-actions[bot]")]
| first // {}
' <<< "$STATUS_JSON")
TARGET_URL=$(jq -r '
.target_url // ""
' <<< "$STATUS_ENTRY")
if [[ ! "$TARGET_URL" =~ /actions/runs/([0-9]+)$ ]]; then
echo "push_approved=false" >> "$GITHUB_OUTPUT"
echo "candidate=false" >> "$GITHUB_OUTPUT"
exit 0
fi

RUN_ID="${BASH_REMATCH[1]}"
EXPECTED_DESCRIPTION="sha:${HEAD_SHA:0:12} run:${RUN_ID}"
[[ "$(jq -r '.description // ""' <<< "$STATUS_ENTRY")" == "$EXPECTED_DESCRIPTION" ]] || {
echo "push_approved=false" >> "$GITHUB_OUTPUT"
echo "candidate=false" >> "$GITHUB_OUTPUT"
exit 0
}
RUN_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}")
if ! jq -e --arg base_sha "$BASE_SHA" '
.event == "workflow_dispatch"
and .head_branch == "main"
and .head_sha == $base_sha
and .conclusion == "success"
and .path == ".github/workflows/quality-baseline-update.yml"
' <<< "$RUN_JSON" >/dev/null; then
echo "push_approved=false" >> "$GITHUB_OUTPUT"
echo "candidate=false" >> "$GITHUB_OUTPUT"
exit 0
fi

APPROVAL_NAME="quality-policy-approval-${HEAD_SHA}-${RUN_ID}"
ARTIFACTS=$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts")
MATCH_COUNT=$(jq --arg name "$APPROVAL_NAME" \
'[.artifacts[] | select(.name == $name and .expired == false)] | length' \
<<< "$ARTIFACTS")
[[ "$MATCH_COUNT" == "1" ]] || {
echo "push_approved=false" >> "$GITHUB_OUTPUT"
echo "candidate=false" >> "$GITHUB_OUTPUT"
exit 0
}
ARTIFACT_ID=$(jq -r --arg name "$APPROVAL_NAME" \
'.artifacts[] | select(.name == $name and .expired == false) | .id' \
<<< "$ARTIFACTS")
ARTIFACT_DIGEST=$(jq -r --arg name "$APPROVAL_NAME" \
'.artifacts[] | select(.name == $name and .expired == false) | .digest' \
<<< "$ARTIFACTS")
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]]
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
echo "push_approved=false" >> "$GITHUB_OUTPUT"
echo "candidate=true" >> "$GITHUB_OUTPUT"
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
echo "artifact_id=$ARTIFACT_ID" >> "$GITHUB_OUTPUT"

- name: Download exact approval evidence
if: steps.authorization.outputs.candidate == 'true'
uses: actions/download-artifact@v4
with:
artifact-ids: ${{ steps.authorization.outputs.artifact_id }}
path: ${{ runner.temp }}/approval-evidence
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ steps.authorization.outputs.run_id }}

- name: Verify immutable approval evidence binding
if: steps.authorization.outputs.candidate == 'true'
id: evidence
shell: bash
run: |
set -euo pipefail
python -I "${{ steps.commits.outputs.judge }}" \
--verify-approval-evidence \
"$RUNNER_TEMP/approval-evidence/quality-policy-approval.json" \
--expected-candidate-sha "$HEAD_SHA" \
--expected-base-sha "$BASE_SHA" \
--expected-run-id "${{ steps.authorization.outputs.run_id }}" \
--expected-repository "$GITHUB_REPOSITORY"
echo "approved=true" >> "$GITHUB_OUTPUT"

- name: Compare protected base and head findings
shell: bash
run: |
set -euo pipefail
ARGS=(
--repo .
--base-sha "${{ steps.commits.outputs.base_sha }}"
--head-sha "${{ steps.commits.outputs.head_sha }}"
--report "$RUNNER_TEMP/quality-ratchet.json"
)
if [[ "${{ steps.authorization.outputs.push_approved }}" == "true" \
|| "${{ steps.evidence.outputs.approved }}" == "true" ]]; then
ARGS+=(--authorization-approved)
fi
python -I "${{ steps.commits.outputs.judge }}" "${ARGS[@]}"

- name: Reconfirm current protected-main tip before publishing
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
MAIN_REF="$RUNNER_TEMP/final-main-ref.json"
gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" > "$MAIN_REF"
ARGS=(--verify-main-ref-response "$MAIN_REF")
if [[ "$GITHUB_EVENT_NAME" == "pull_request_target" ]]; then
PULL_REQUEST="$RUNNER_TEMP/final-pull-request.json"
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > "$PULL_REQUEST"
ARGS+=(
--verify-pull-request-response "$PULL_REQUEST"
--expected-current-main-sha "${{ steps.commits.outputs.base_sha }}"
--expected-current-head-sha "${{ steps.commits.outputs.head_sha }}"
)
else
ARGS+=(--expected-current-main-sha "${{ steps.commits.outputs.head_sha }}")
fi
python -I "${{ steps.commits.outputs.judge }}" "${ARGS[@]}"

- name: Publish protected result on the exact head SHA
if: always()
env:
GH_TOKEN: ${{ github.token }}
JOB_STATUS: ${{ job.status }}
run: |
STATE=failure
DESCRIPTION='Protected base quality ratchet failed'
if [[ "$JOB_STATUS" == "success" ]]; then
STATE=success
DESCRIPTION='Protected base quality ratchet passed'
fi
gh api --method POST \
"repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \
-f state="$STATE" \
-f context='quality-ratchet/protected' \
-f description="$DESCRIPTION" \
-f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"

- name: Upload ratchet evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: quality-ratchet-${{ steps.commits.outputs.head_sha }}
path: ${{ runner.temp }}/quality-ratchet.json
if-no-files-found: warn
retention-days: 14
68 changes: 68 additions & 0 deletions docs/ci_quality_ratchet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# CI quality ratchet

The quality ratchet compares Ruff check, Ruff format, and mypy findings at the
protected base commit and the exact pull-request head. It recomputes both sides
under the protected base's tool policy; there is no checked-in debt snapshot.

## Repository settings

After the bootstrap merge, repository administrators must:

1. Require the `quality-ratchet/protected` commit status on `main`.
2. Create the `quality-ratchet-policy` environment.
3. Restrict that environment to protected branches and configure
`@rylinjames` as its sole reviewer.

The manual workflow requires the exact reviewer allowlist before it can
authorize a commit. Missing, duplicate, team, or additional reviewers and a
weaker branch policy all fail closed because every configured reviewer could
otherwise grant deployment approval.

The protected quality policy forbids `tool.mypy.plugins`. Mypy imports plugin
code during analysis; permitting a repository-local plugin would let a pull
request replace that code and execute it inside the ordinary status-writing
job. Both the current protected policy and proposed policy updates are rejected
before Ruff or mypy starts if a plugin setting is present.

## Updating the gate or policy

Ordinary pull requests cannot authorize changes to the ratchet script,
workflows, CODEOWNERS, mutable baseline-like files, or the effective Ruff/mypy
sections of `pyproject.toml`.

For an intentional update:

1. Open the pull request and complete code-owner review.
2. From protected `main`, run **Quality ratchet protected policy update** with
the pull-request number and reason.
3. Approve the `quality-ratchet-policy` environment deployment.
4. After the manual run succeeds, rerun the pull request's quality-ratchet job.

The manual workflow has three deliberately separate jobs. Its first job has no
write authority or Actions cache: it validates the exact current head SHA under
the old policy, parses the proposed Ruff/mypy configuration strictly as TOML
data without invoking either tool or importing configured plugins, statically
validates the proposed judge under protected policy, and uploads immutable
pre-execution evidence.

Its second job starts on a separate unprivileged runner only after that evidence
is sealed. Candidate tool configuration and tests can execute there, but the
job has no authority, produces no authorization outputs, and cannot mutate the
first runner or its immutable artifact.

Its third job starts on a fresh environment-protected runner. It checks out
only protected `main`, executes no candidate content, downloads only the exact
validation artifact, and re-queries both `refs/heads/main` and the current pull
request immediately before approval creation. Both must still identify the
sealed base and candidate. It then issues an immutable approval artifact plus a
status whose description binds the candidate SHA to the manual run ID, with a
second live-tip check immediately before that status write.

The ordinary protected workflow independently queries the current `main` tip
and pull request before comparison, rejecting stale event payloads and reruns
after `main` or the pull-request head advances. It repeats the live-tip check
after analysis and immediately before publishing the protected result. It also
queries the exact manual run and artifact through the GitHub API, downloads it
by artifact ID, and verifies its repository, base SHA, candidate SHA, workflow
path, and run ID with the protected judge. A status from an unrelated run, a
stale approval or event, or missing/expired evidence fails closed.
Loading
Loading