Skip to content

Make specfact upgrade install-method-aware (uv/uvx support, pipx/pip detection) #271

Make specfact upgrade install-method-aware (uv/uvx support, pipx/pip detection)

Make specfact upgrade install-method-aware (uv/uvx support, pipx/pip detection) #271

Workflow file for this run

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# Auto-sign changed bundled modules on push to dev/main, then strict-verify; manifest commits
# open an auto/sign-* PR (protected branches — no direct push). PRs / workflow_dispatch use the
# same relaxed verify bundle as pre-commit omit (see scripts/module-verify-policy.sh).
#
# Push runs for every actor (including github-actions[bot]) so merge commits that
# land manifest or payload changes still refresh integrity.checksum before strict
# verify; otherwise verify fails with checksum mismatch when signing is skipped.
name: Module Signature Hardening
on:
workflow_dispatch:
inputs:
base_branch:
description: Remote branch to compare for --changed-only (fetches origin/<branch>)
type: choice
options:
- dev
- main
default: dev
version_bump:
description: Auto-bump when module version is still unchanged from the base ref
type: choice
options:
- patch
- minor
- major
default: patch
resign_all_manifests:
description: Sign every bundled module-package.yaml (not only --changed-only vs base). Use when manifests match the base but lack signatures.
type: boolean
default: false
push:
branches: [dev, main]
paths:
- "src/specfact_cli/modules/**"
- "modules/**"
- "resources/keys/**"
- "scripts/sign-modules.py"
- "scripts/verify-modules-signature.py"
- "scripts/module-verify-policy.sh"
- ".github/workflows/sign-modules.yml"
- ".github/workflows/sign-modules-on-approval.yml"
pull_request:
branches: [dev, main]
paths:
- "src/specfact_cli/modules/**"
- "modules/**"
- "resources/keys/**"
- "scripts/sign-modules.py"
- "scripts/verify-modules-signature.py"
- "scripts/module-verify-policy.sh"
- ".github/workflows/sign-modules.yml"
- ".github/workflows/sign-modules-on-approval.yml"
jobs:
verify:
name: Verify Module Signatures
runs-on: ubuntu-latest
outputs:
signing_pr_created: ${{ steps.open_auto_sign_pr.outputs.created == 'true' && 'true' || 'false' }}
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch workflow_dispatch comparison base
if: github.event_name == 'workflow_dispatch'
run: git fetch --no-tags origin "${{ github.event.inputs.base_branch }}"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install signer dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml beartype icontract cryptography cffi
- name: Auto-sign changed bundled modules (push to dev/main)
if: >-
github.event_name == 'push' &&
(github.ref_name == 'dev' || github.ref_name == 'main')
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 }}
run: |
set -euo pipefail
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
echo "::error::Missing SPECFACT_MODULE_PRIVATE_SIGN_KEY. Configure the secret so pushes to ${GITHUB_REF_NAME} can auto-sign bundled modules."
exit 1
fi
BEFORE="${{ github.event.before }}"
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
BEFORE="$(git rev-parse HEAD~1 2>/dev/null || true)"
fi
if [ -z "$BEFORE" ]; then
echo "::error::Unable to resolve parent commit for --changed-only signing."
exit 1
fi
python scripts/sign-modules.py \
--changed-only \
--repair-stale-integrity \
--base-ref "$BEFORE" \
--bump-version patch \
--payload-from-filesystem
- name: Strict verify bundled modules (push to dev/main)
if: github.event_name == 'push' && (github.ref_name == 'dev' || github.ref_name == 'main')
run: |
set -euo pipefail
# shellcheck disable=SC1091
source scripts/module-verify-policy.sh
BEFORE="${{ github.event.before }}"
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
BEFORE="HEAD~1"
fi
python scripts/verify-modules-signature.py "${VERIFY_MODULES_STRICT[@]}" --version-check-base "$BEFORE"
- name: PR or dispatch verify (relaxed checksum; version bump vs base)
if: github.event_name != 'push'
run: |
set -euo pipefail
# shellcheck disable=SC1091
source scripts/module-verify-policy.sh
VERIFY_ARGS=("${VERIFY_MODULES_PR[@]}")
BASE_REF=""
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BASE_REF="origin/${{ github.event.inputs.base_branch }}"
fi
if [ -z "$BASE_REF" ]; then
echo "::error::Missing comparison base for module verification."
exit 1
fi
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.resign_all_manifests }}" = "true" ]; then
RESIGN_ARGS=()
skip_next=0
for arg in "${VERIFY_ARGS[@]}"; do
if [ "${skip_next}" -eq 1 ]; then
skip_next=0
continue
fi
if [ "${arg}" = "--enforce-version-bump" ]; then
continue
fi
if [ "${arg}" = "--version-check-base" ]; then
skip_next=1
continue
fi
RESIGN_ARGS+=("${arg}")
done
python scripts/verify-modules-signature.py "${RESIGN_ARGS[@]}"
else
python scripts/verify-modules-signature.py "${VERIFY_ARGS[@]}" --version-check-base "$BASE_REF"
fi
- id: open_auto_sign_pr
name: Open PR with auto-signed manifests (dev/main; no direct push)
if: >-
github.event_name == 'push' &&
(github.ref_name == 'dev' || github.ref_name == 'main')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
SIGNING_PR_CREATED=false
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -u -- src/specfact_cli/modules modules
if git diff --cached --quiet; then
echo "No manifest signing changes to commit."
echo "created=${SIGNING_PR_CREATED}" >> "${GITHUB_OUTPUT}"
exit 0
fi
BRANCH="auto/sign-${GITHUB_REF_NAME}-${{ github.run_id }}"
git checkout -b "${BRANCH}"
git commit -m "chore(modules): auto-sign bundled manifests [skip ci]"
git push -u origin "${BRANCH}"
gh pr create \
--repo "${{ github.repository }}" \
--base "${GITHUB_REF_NAME}" \
--head "${BRANCH}" \
--title "chore(modules): auto-sign bundled manifests (${GITHUB_REF_NAME})" \
--body "Automated integrity refresh after push to \`${GITHUB_REF_NAME}\`. Merge so strict verify and reproducibility run against the signed tip (protected branch — no direct push)."
SIGNING_PR_CREATED=true
echo "created=${SIGNING_PR_CREATED}" >> "${GITHUB_OUTPUT}"
reproducibility:
name: Assert signing reproducibility
if: >-
github.event_name == 'push' &&
github.ref_name == 'main' &&
needs.verify.outputs.signing_pr_created != 'true'
runs-on: ubuntu-latest
needs: [verify]
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sync to remote branch tip (after verify; skip when a signing PR was opened instead)
run: |
set -euo pipefail
git fetch origin "${GITHUB_REF_NAME}"
git reset --hard "origin/${GITHUB_REF_NAME}"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install signer dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml beartype icontract cryptography cffi
- name: Re-sign manifests and assert no diff
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 }}
run: |
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
echo "::notice::Skipping reproducibility check because SPECFACT_MODULE_PRIVATE_SIGN_KEY is not configured."
exit 0
fi
mapfile -t MANIFESTS < <(find src/specfact_cli/modules modules -name 'module-package.yaml' -type f 2>/dev/null | sort)
if [ "${#MANIFESTS[@]}" -eq 0 ]; then
echo "No module manifests found"
exit 0
fi
python scripts/sign-modules.py --payload-from-filesystem "${MANIFESTS[@]}"
if ! git diff --exit-code -- src/specfact_cli/modules modules; then
echo "::error::Module signatures are stale for the configured signing key. Re-sign and commit manifest updates."
git --no-pager diff --name-only -- src/specfact_cli/modules modules
exit 1
fi
sign-and-push:
name: Sign changed modules (manual dispatch)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
needs: [verify]
permissions:
contents: write
steps:
- name: Require module signing key secret
env:
SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }}
run: |
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
echo "::error::Missing or empty repository secret SPECFACT_MODULE_PRIVATE_SIGN_KEY."
exit 1
fi
- name: Checkout branch
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
persist-credentials: true
- name: Fetch comparison base
run: git fetch --no-tags origin "${{ github.event.inputs.base_branch }}"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install signer dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml beartype icontract cryptography cffi
- name: Sign module manifests
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 }}
run: |
set -euo pipefail
MERGE_BASE="$(git merge-base HEAD "origin/${{ github.event.inputs.base_branch }}")"
BUMP="${{ github.event.inputs.version_bump }}"
if [ "${{ github.event.inputs.resign_all_manifests }}" = "true" ]; then
mapfile -t MANIFESTS < <(find src/specfact_cli/modules modules -name 'module-package.yaml' -type f 2>/dev/null | sort)
if [ "${#MANIFESTS[@]}" -eq 0 ]; then
echo "No module manifests found"
exit 0
fi
python scripts/sign-modules.py --payload-from-filesystem "${MANIFESTS[@]}"
else
python scripts/sign-modules.py \
--changed-only \
--base-ref "$MERGE_BASE" \
--bump-version "${BUMP}" \
--payload-from-filesystem
fi
- name: Commit and push signed manifests
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No manifest changes to commit."
echo "## No signing changes" >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
git add -u -- src/specfact_cli/modules modules
if git diff --cached --quiet; then
echo "No staged module manifest updates."
exit 0
fi
git commit -m "chore(modules): manual workflow_dispatch sign changed modules [skip ci]"
git push origin "HEAD:${GITHUB_REF_NAME}"
echo "## Signed manifests pushed" >> "${GITHUB_STEP_SUMMARY}"
echo "Branch: \`${GITHUB_REF_NAME}\` (base: \`origin/${{ github.event.inputs.base_branch }}\`, bump: \`${{ github.event.inputs.version_bump }}\`, resign_all: \`${{ github.event.inputs.resign_all_manifests }}\`)." >> "${GITHUB_STEP_SUMMARY}"