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
122 changes: 122 additions & 0 deletions .github/workflows/advance-staged-qpk-pin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Advance Staged QPK Pin

"on":
workflow_run:
workflows: ["CI"]
types: [completed]

permissions:
contents: read
pull-requests: write

jobs:
advance:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'auto/qpk-pin-update'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Resolve the completed staged pin PR
id: pr
env:
GH_TOKEN: ${{ github.token }}
COMPLETED_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
COMPLETED_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
set -euo pipefail
pr_payload=$(gh pr list \
--repo "$GITHUB_REPOSITORY" \
--state open \
--head "$COMPLETED_HEAD_BRANCH" \
--json number,headRefOid \
--jq '.[0] // {}')
pr_number=$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("number", ""))' <<<"$pr_payload")
pr_head_sha=$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("headRefOid", ""))' <<<"$pr_payload")

if [ -z "$pr_number" ]; then
echo "No open staged QPK pin PR found." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$pr_head_sha" != "$COMPLETED_HEAD_SHA" ]; then
echo "Skipping stale CI result for PR #$pr_number." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

echo "number=$pr_number" >> "$GITHUB_OUTPUT"
echo "head_sha=$pr_head_sha" >> "$GITHUB_OUTPUT"

- name: Enforce the staged-pin trust boundary
id: guard
if: steps.pr.outputs.number != ''
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
PR_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
set -euo pipefail
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
--json author,baseRefName,changedFiles,headRefName,isCrossRepository,isDraft,title,url > pr.json
python3 - <<'PY'
import json
import os
from pathlib import Path

pr = json.loads(Path("pr.json").read_text(encoding="utf-8"))
allowed = (
(pr.get("author") or {}).get("login") == "Pigbibi"
and pr.get("baseRefName") == "main"
and pr.get("headRefName") == "auto/qpk-pin-update"
and pr.get("title") == "chore: advance staged QPK pin"
and pr.get("changedFiles") == 1
and not pr.get("isCrossRepository")
and not pr.get("isDraft")
)
reason = "ready" if allowed else "unexpected_pr_metadata"
summary = [
"## Staged QPK Pin Gate",
f"- PR: {pr.get('url', '<unknown>')}",
f"- Author: `{(pr.get('author') or {}).get('login', '<unknown>')}`",
f"- Changed files: `{pr.get('changedFiles', '<unknown>')}`",
f"- Cross-repository: `{'yes' if pr.get('isCrossRepository') else 'no'}`",
f"- Decision: `{'advance' if allowed else 'skip'}`",
f"- Reason: `{reason}`",
]
Path("staged-pin-summary.md").write_text("\n".join(summary) + "\n", encoding="utf-8")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
print(f"allowed={'true' if allowed else 'false'}", file=output)
PY

files=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename')
if [ "$files" != "QPK_PIN" ]; then
echo "::error title=Staged QPK pin gate::unexpected_changed_files"
exit 1
fi

candidate=$(gh api "repos/$GITHUB_REPOSITORY/contents/QPK_PIN?ref=$PR_HEAD_SHA" --jq '.content' \
| base64 --decode | tr -d '\r\n')
if ! [[ "$candidate" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error title=Staged QPK pin gate::invalid_qpk_pin"
exit 1
fi

main_sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha')
comparison=$(gh api "repos/$GITHUB_REPOSITORY/compare/$candidate...$main_sha" --jq '.status')
if [ "$comparison" != "ahead" ] && [ "$comparison" != "identical" ]; then
echo "::error title=Staged QPK pin gate::candidate_is_not_on_main_history"
exit 1
fi

- name: Append gate summary
if: steps.pr.outputs.number != ''
run: cat staged-pin-summary.md >> "$GITHUB_STEP_SUMMARY"

- name: Queue validated staged pin for merge
if: steps.guard.outputs.allowed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr merge "${{ steps.pr.outputs.number }}" \
--repo "$GITHUB_REPOSITORY" \
--auto --rebase --delete-branch \
--match-head-commit "${{ steps.pr.outputs.head_sha }}"
2 changes: 1 addition & 1 deletion .github/workflows/open-downstream-qpk-pin-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:

- name: Create coherent aggregate bundle PR
if: steps.sync.outputs.missing_token != 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.8
with:
token: ${{ secrets.QSL_REPO_SYNC_TOKEN }}
commit-message: "chore(deps): reconcile coherent QSL pin bundle"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/update-qpk-pin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- "qsl-pins.txt"
- "constraints.txt"
- ".github/workflows/open-downstream-qpk-pin-prs.yml"
- ".github/workflows/advance-staged-qpk-pin.yml"
- ".github/workflows/update-qpk-pin.yml"
- "scripts/check_qpk_pin_consistency.py"
- "scripts/open_downstream_qpk_pin_prs.py"
Expand Down
23 changes: 23 additions & 0 deletions tests/test_update_qpk_pin_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
ROOT = Path(__file__).resolve().parents[1]
WORKFLOW_PATH = ROOT / ".github" / "workflows" / "update-qpk-pin.yml"
DOWNSTREAM_WORKFLOW_PATH = ROOT / ".github" / "workflows" / "open-downstream-qpk-pin-prs.yml"
STAGED_PIN_GUARD_WORKFLOW_PATH = (
ROOT / ".github" / "workflows" / "advance-staged-qpk-pin.yml"
)
OLD_QPK_SHA = "5d4bbd0e7ef9a1434010e8b6a69905d39ee55f1b"
STRATEGY_REFS = {
"us-equity-strategies": (
Expand Down Expand Up @@ -249,3 +252,23 @@ def test_downstream_rollout_is_scheduled_and_phase_gated() -> None:
assert "- consumers" in workflow
assert 'open_downstream_qpk_pin_prs.py --phase "$QSL_PIN_PHASE"' in workflow
assert "Create coherent aggregate bundle PR" in workflow
assert "peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676" in workflow
assert "peter-evans/create-pull-request@v7" not in workflow


def test_staged_pin_auto_advance_is_limited_to_verified_machine_prs() -> None:
workflow = STAGED_PIN_GUARD_WORKFLOW_PATH.read_text(encoding="utf-8")

assert 'workflows: ["CI"]' in workflow
assert "github.event.workflow_run.conclusion == 'success'" in workflow
assert "github.event.workflow_run.head_branch == 'auto/qpk-pin-update'" in workflow
assert '"author") or {}).get("login") == "Pigbibi"' in workflow
assert 'pr.get("baseRefName") == "main"' in workflow
assert 'pr.get("headRefName") == "auto/qpk-pin-update"' in workflow
assert 'pr.get("title") == "chore: advance staged QPK pin"' in workflow
assert "not pr.get(\"isCrossRepository\")" in workflow
assert 'if [ "$files" != "QPK_PIN" ]; then' in workflow
assert "candidate_is_not_on_main_history" in workflow
assert "actions/checkout" not in workflow
assert "--auto --rebase --delete-branch" in workflow
assert "--match-head-commit" in workflow