Skip to content

Declare plan in specfact-project manifest, update docs, and add regression test #87

Declare plan in specfact-project manifest, update docs, and add regression test

Declare plan in specfact-project manifest, update docs, and add regression test #87

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: sign-modules-on-approval
on:
pull_request_review:
types: [submitted]
concurrency:
group: sign-modules-on-approval-${{ github.event.pull_request.number || github.event.number }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: read
jobs:
sign-modules:
runs-on: ubuntu-latest
env:
SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }}
SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref || github.head_ref }}
steps:
- name: Eligibility gate (required status check)
id: gate
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
review_state="${{ github.event.review.state || '' }}"
author_association="${{ github.event.review.user.author_association || '' }}"
base_ref="${{ github.event.pull_request.base.ref }}"
if [ "$base_ref" != "dev" ] && [ "$base_ref" != "main" ]; then
echo "sign=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping module signing: base branch is not dev or main."
exit 0
fi
head_repo="${{ github.event.pull_request.head.repo.full_name }}"
this_repo="${{ github.repository }}"
if [ "$head_repo" != "$this_repo" ]; then
echo "sign=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping module signing: fork PR (head repo differs from target repo)."
exit 0
fi
if [ "$review_state" != "approved" ]; then
echo "sign=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping module signing: review state is not approved."
exit 0
fi
case "$author_association" in
COLLABORATOR|MEMBER|OWNER)
;;
*)
echo "sign=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping module signing: reviewer association '${author_association}' is not trusted for signing."
exit 0
;;
esac
echo "sign=true" >> "$GITHUB_OUTPUT"
echo "Eligible for module signing (same-repo PR to dev or main with trusted approval state)."
- name: Guard signing secrets
if: steps.gate.outputs.sign == 'true'
run: |
set -euo pipefail
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY:-}" ]; then
echo "::error::Missing secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY"
exit 1
fi
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE:-}" ]; then
echo "::error::Missing secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE"
exit 1
fi
- uses: actions/checkout@v4
if: steps.gate.outputs.sign == 'true'
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Set up Python 3.12
if: steps.gate.outputs.sign == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install signing dependencies
if: steps.gate.outputs.sign == 'true'
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml beartype icontract cryptography cffi
- name: Discover module manifests
if: steps.gate.outputs.sign == 'true'
id: discover
run: |
set -euo pipefail
mapfile -t MANIFESTS < <(find packages -name 'module-package.yaml' -type f | sort)
echo "manifests_count=${#MANIFESTS[@]}" >> "$GITHUB_OUTPUT"
echo "Discovered ${#MANIFESTS[@]} module-package.yaml file(s) under packages/"
- name: Sign changed module manifests
if: steps.gate.outputs.sign == 'true'
id: sign
run: |
set -euo pipefail
git fetch origin "${PR_BASE_REF}" --no-tags
MERGE_BASE="$(git merge-base HEAD "origin/${PR_BASE_REF}")"
python scripts/sign-modules.py \
--changed-only \
--base-ref "$MERGE_BASE" \
--bump-version patch \
--payload-from-filesystem
- name: Commit and push signed manifests
if: steps.gate.outputs.sign == 'true'
id: commit
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if [ -z "$(git status --porcelain -- packages/)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No manifest changes to commit."
exit 0
fi
git add -u -- packages/
git commit -m "chore(modules): ci sign changed modules"
echo "changed=true" >> "$GITHUB_OUTPUT"
if ! git push origin "HEAD:${PR_HEAD_REF}"; then
echo "::error::Push to ${PR_HEAD_REF} failed (branch may have advanced after the approved commit). Update the PR branch and re-approve if signing is still required."
exit 1
fi
- name: Write job summary
if: always()
env:
GATE_SIGN: ${{ steps.gate.outputs.sign }}
COMMIT_CHANGED: ${{ steps.commit.outputs.changed || '' }}
MANIFESTS_COUNT: ${{ steps.discover.outputs.manifests_count || '' }}
run: |
{
echo "### Module signing (CI approval)"
if [ "${GATE_SIGN}" != "true" ]; then
echo "Signing skipped (eligibility gate: no trusted approval, wrong base branch, or fork PR)."
else
echo "Manifests discovered under \`packages/\`: ${MANIFESTS_COUNT:-unknown}"
if [ "${COMMIT_CHANGED}" = "true" ]; then
echo "Committed signed manifest updates to ${PR_HEAD_REF}."
else
echo "No changes detected (manifests already signed or no module changes on this PR vs merge-base)."
fi
fi
} >> "$GITHUB_STEP_SUMMARY"