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
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/augmentation_metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Example validation commands:

```bash
hatch run check-bundle-imports
hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump
hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump
# add --require-signature when validating main-branch policy
```

## Alternative Solutions
Expand Down
6 changes: 4 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ hatch run <command>
Example:

```bash
hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump
hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump
# For main-equivalent failures, also try:
# hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump
```

## Expected Behavior
Expand Down Expand Up @@ -55,7 +57,7 @@ Typical commands:
- `hatch run lint`
- `hatch run yaml-lint`
- `hatch run check-bundle-imports`
- `hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump`
- `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump` (and `--require-signature` if reproducing on **`main`**)

## Additional Context

Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/change_proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ High-level implementation summary. Include affected bundles and workflow/config
- [ ] Bundle changes are scoped and intentional (`packages/*`)
- [ ] `module-package.yaml` versions are bumped where contents changed
- [ ] Manifests are re-signed
- [ ] `hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump` passes
- [ ] `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump` passes for **`dev`**-targeting work; add `--require-signature` when the change must satisfy **`main`** policy
- [ ] Import boundaries respected (`hatch run check-bundle-imports`)
- [ ] Required quality gates pass in PR orchestrator

Expand Down
8 changes: 5 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ Paste command output snippets or link workflow runs.

### Signature + version integrity (required)

- [ ] `hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump`
- [ ] Changed bundle versions were bumped before signing
- [ ] Manifests re-signed after bundle content changes
- [ ] `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump` passes (matches PRs targeting **`dev`**)
- [ ] If this PR targets **`main`**, also confirmed: `hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump` (and/or approval triggered **`sign-modules-on-approval`** for same-repo PRs)
- [ ] Changed bundle versions were bumped when module payloads changed
- [ ] Manifests signed when required by your target branch (CI may sign on **approval** for `dev`/`main` same-repo PRs)

## CI and Branch Protection

- [ ] PR orchestrator jobs expected:
- `verify-module-signatures`
- `sign-modules-on-approval` (on approval, same-repo PRs to `dev`/`main` only)
- `quality (3.11)`
- `quality (3.12)`
- `quality (3.13)`
Expand Down
19 changes: 16 additions & 3 deletions .github/workflows/pr-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,31 @@ jobs:
python -m pip install pyyaml cryptography cffi
- name: Verify bundled module signatures and version bumps
run: |
set -euo pipefail
TARGET_BRANCH=""
if [ "${{ github.event_name }}" = "pull_request" ]; then
TARGET_BRANCH="${{ github.event.pull_request.base.ref }}"
else
TARGET_BRANCH="${GITHUB_REF#refs/heads/}"
fi

BASE_REF=""
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
fi

if [ -z "${SPECFACT_MODULE_PUBLIC_SIGN_KEY:-}" ] && [ -z "${SPECFACT_MODULE_SIGNING_PUBLIC_KEY_PEM:-}" ]; then
echo "warning: no public signing key secret set; verifier must resolve key from repo/default path"
fi

VERIFY_CMD=(python scripts/verify-modules-signature.py --payload-from-filesystem --enforce-version-bump)
if [ "$TARGET_BRANCH" = "main" ]; then
VERIFY_CMD+=(--require-signature)
fi
if [ -n "$BASE_REF" ]; then
python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump --version-check-base "$BASE_REF"
else
python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump
VERIFY_CMD+=(--version-check-base "$BASE_REF")
fi
"${VERIFY_CMD[@]}"

quality:
name: quality (${{ matrix.python-version }})
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/sign-modules-on-approval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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 }}
cancel-in-progress: true

permissions:
contents: write

jobs:
sign-modules:
if: >-
github.event.review.state == 'approved' &&
(github.event.pull_request.base.ref == 'dev' || github.event.pull_request.base.ref == 'main') &&
github.event.pull_request.head.repo.full_name == github.repository
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 }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
steps:
- name: Guard signing secrets
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
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

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

- name: Install signing dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml beartype icontract cryptography cffi

- name: Discover module manifests
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
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
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 [skip ci]"
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:
COMMIT_CHANGED: ${{ steps.commit.outputs.changed }}
MANIFESTS_COUNT: ${{ steps.discover.outputs.manifests_count }}
run: |
{
echo "### Module signing (CI approval)"
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
} >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repos:
hooks:
- id: verify-module-signatures
name: Verify module signatures and version bumps
entry: hatch run ./scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump
entry: ./scripts/pre-commit-verify-modules-signature.sh
language: system
pass_filenames: false
always_run: true
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,25 @@ hatch run type-check
hatch run lint
hatch run yaml-lint
hatch run check-bundle-imports
hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump
hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump
hatch run contract-test
hatch run smart-test
hatch run test
hatch run specfact code review run --json --out .specfact/code-review.json
```

**Module signatures:** `pr-orchestrator` enforces `--require-signature` only for events targeting **`main`**; for **`dev`** (and feature branches) CI checks checksums and version bumps without requiring a cryptographic signature yet. Add `--require-signature` to the `verify-modules-signature` command when you want the same bar as **`main`** (for example before merging to `main`). Pre-commit runs `scripts/pre-commit-verify-modules-signature.sh`, which mirrors that policy (signatures required on branch `main`, or when `GITHUB_BASE_REF=main` in Actions).

**CI signing:** Approved PRs to `dev` or `main` from **this repository** (not forks) run `.github/workflows/sign-modules-on-approval.yml`, which can commit signed manifests using repository secrets. See [Module signing](/authoring/module-signing/).
Comment thread
djm81 marked this conversation as resolved.

To mirror CI locally with git hooks, enable pre-commit:

```bash
pre-commit install
pre-commit run --all-files
```

**Code review gate (matches specfact-cli core):** runs in **Block 2** after module signatures and Block 1 quality hooks (`pre-commit-quality-checks.sh block2`, which calls `scripts/pre_commit_code_review.py`). Staged paths under `packages/`, `registry/`, `scripts/`, `tools/`, `tests/`, and `openspec/changes/` (excluding `TDD_EVIDENCE.md`) are forwarded to the helper, which runs `specfact code review run --json --out .specfact/code-review.json` with that path list. The helper prints only a short findings summary and copy-paste prompts on stderr (not the nested CLI’s full tool output). Block 1 is split into separate pre-commit hooks so output appears between stages instead of buffering until the end. Requires a local **specfact-cli** install (`hatch run dev-deps` resolves sibling `../specfact-cli` or `SPECFACT_CLI_REPO`).
**Code review gate (matches specfact-cli core):** runs in **Block 2** after the module verify hook and Block 1 quality hooks (`pre-commit-quality-checks.sh block2`, which calls `scripts/pre_commit_code_review.py`). Staged paths under `packages/`, `registry/`, `scripts/`, `tools/`, `tests/`, and `openspec/changes/` are eligible; `openspec/changes/**/TDD_EVIDENCE.md` is excluded from the gate. OpenSpec Markdown other than evidence files is not passed to SpecFact (the review CLI treats paths as Python). The helper runs `specfact code review run --json --out .specfact/code-review.json` on the remaining paths and prints only a short findings summary and copy-paste prompts on stderr. Block 1 is split into separate pre-commit hooks so output appears between stages instead of buffering until the end. Requires a local **specfact-cli** install (`hatch run dev-deps` resolves sibling `../specfact-cli` or `SPECFACT_CLI_REPO`).

Scope notes:
- Pre-commit runs `hatch run lint` in the **Block 1 — lint** hook when any staged path matches `*.py` / `*.pyi`, matching the CI quality job (Ruff alone does not run pylint).
Expand Down
3 changes: 2 additions & 1 deletion docs/agent-rules/20-repository-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ hatch run format
hatch run type-check
hatch run lint
hatch run yaml-lint
hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump
hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump
# add --require-signature for main-equivalent checks (see agent-rules/50-quality-gates-and-review.md)
hatch run contract-test
hatch run smart-test
hatch run test
Expand Down
4 changes: 2 additions & 2 deletions docs/agent-rules/50-quality-gates-and-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ depends_on:
3. `hatch run lint`
4. `hatch run yaml-lint`
5. `hatch run check-bundle-imports`
6. `hatch run verify-modules-signature --require-signature --payload-from-filesystem --enforce-version-bump`
6. `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump` (add `--require-signature` when checking as for `main`; matches CI and `scripts/pre-commit-verify-modules-signature.sh`)
7. `hatch run contract-test`
8. `hatch run smart-test`
9. `hatch run test`
10. `hatch run specfact code review run --json --out .specfact/code-review.json` (full-repo scope when required: add `--scope full`; machine-readable evidence lives at `.specfact/code-review.json` and unresolved findings block merge unless an explicit exception is documented)

## Pre-commit order

1. Module signature verification (`.pre-commit-config.yaml`, `fail_fast: true` so a failing earlier hook never runs later stages).
1. Module signature verification via `scripts/pre-commit-verify-modules-signature.sh` (`.pre-commit-config.yaml`; `fail_fast: true` so a failing earlier hook never runs later stages). The hook adds `--require-signature` on branch `main`, or when `GITHUB_BASE_REF` is `main` (PR target in Actions).
2. **Block 1** — four separate hooks (each flushes pre-commit output when it exits, so you see progress between stages): `pre-commit-quality-checks.sh block1-format` (always), `block1-yaml` when staged `*.yaml` / `*.yml`, `block1-bundle` (always), `block1-lint` when staged `*.py` / `*.pyi`.
3. **Block 2** — `pre-commit-quality-checks.sh block2` (skipped for “safe-only” staged paths): `hatch run python scripts/pre_commit_code_review.py …` on **staged paths under `packages/`, `registry/`, `scripts/`, `tools/`, `tests/`, and `openspec/changes/`** (excluding `TDD_EVIDENCE.md`), then `contract-test-status` / `hatch run contract-test`.

Expand Down
26 changes: 18 additions & 8 deletions docs/authoring/module-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ hatch run python scripts/sign-modules.py \
--base-ref origin/dev \
--bump-version patch

# Verify after signing (must match sign payload mode)
# Verify after signing (must match sign payload mode). For a dev-targeting branch, CI omits
# --require-signature; add it when checking as for main:
hatch run python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump --version-check-base origin/dev
```
Comment thread
djm81 marked this conversation as resolved.

Expand All @@ -110,26 +111,35 @@ python scripts/sign-modules.py --allow-unsigned --payload-from-filesystem packag

## Verify Signatures Locally

Strict verification (checksum + signature required):
Checksum + version enforcement (matches **`dev`** / feature CI and pre-commit when not on `main`):

```bash
python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem
python scripts/verify-modules-signature.py --payload-from-filesystem --enforce-version-bump
```

Strict verification (checksum + **signature** required, matches **`main`** CI):

```bash
python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump
```

With explicit public key file:

```bash
python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --public-key-file resources/keys/module-signing-public.pem
python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump --public-key-file resources/keys/module-signing-public.pem
```

## CI Enforcement

`pr-orchestrator.yml` contains a strict gate:
`pr-orchestrator.yml` job **`verify-module-signatures`** always runs with `--payload-from-filesystem --enforce-version-bump`. It adds **`--require-signature` only when the pull request or push targets `main`**. For **`dev`** and feature work, the job still enforces checksums and version bumps so unsigned manifests can land on `dev`; signatures are expected by the time changes reach **`main`**.

### Signing on approval (same-repo PRs)

Workflow **`sign-modules-on-approval.yml`** runs when a review is **submitted** and **approved** on a PR whose base is **`dev`** or **`main`**, and only when the PR head is in **this** repository (`head.repo` equals the base repo). It checks out **`github.event.pull_request.head.sha`** (the commit that was approved, not the moving branch tip), uses `SPECFACT_MODULE_PRIVATE_SIGN_KEY` and `SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE` (each validated with a named error if missing), discovers changes against the **merge-base** with the base branch (not the moving base tip alone), runs `scripts/sign-modules.py --changed-only --bump-version patch --payload-from-filesystem`, and commits results with `[skip ci]`. If `git push` is rejected because the PR branch advanced after approval, the job fails with guidance to update the branch and re-approve. **Fork PRs** are skipped (the default `GITHUB_TOKEN` cannot push to a contributor fork).

- Job: `verify-module-signatures`
- Command: `python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem --enforce-version-bump`
### Pre-commit

This runs on PR/push for `dev` and `main` and fails the pipeline if module signatures/checksums are missing or stale.
The first pre-commit hook runs **`scripts/pre-commit-verify-modules-signature.sh`**, which mirrors CI: **`--require-signature` on branch `main`**, or when **`GITHUB_BASE_REF=main`** in Actions pull-request contexts; otherwise checksum + version enforcement only.

## Rotation Procedure

Expand Down
2 changes: 1 addition & 1 deletion docs/authoring/publishing-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Repository workflow `.github/workflows/publish-modules.yml`:

- Bump module `version` in `module-package.yaml` whenever payload or manifest content changes; keep versions immutable for published artifacts.
- Use `namespace/name` for any module you publish to a registry.
- Run `scripts/verify-modules-signature.py --require-signature --payload-from-filesystem` (or your registry’s policy) before releasing.
- Before releasing from **`main`**, run `scripts/verify-modules-signature.py --require-signature --payload-from-filesystem` (or your registry’s policy). On **`dev`**, CI may accept checksum-only manifests until promotion; align with your registry’s requirements.
Comment thread
djm81 marked this conversation as resolved.
- Prefer `--download-base-url` and `--index-fragment` when integrating with a custom registry index.

## See also
Expand Down
Loading
Loading