-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ci): marketplace-06 module signing on approval + branch-aware verify #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4914f12
81116d2
b386cb3
c047352
9b9ecf9
f513a39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # 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:-}" ] || [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE:-}" ]; then | ||
| echo "::error::SPECFACT_MODULE_PRIVATE_SIGN_KEY and SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE must be set" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.ref }} | ||
| 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/" | ||
|
|
||
|
djm81 marked this conversation as resolved.
|
||
| - 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" | ||
| git push origin "HEAD:${PR_HEAD_REF}" | ||
|
djm81 marked this conversation as resolved.
|
||
|
|
||
| - 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" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # TDD evidence: marketplace-06-ci-module-signing | ||
|
|
||
| Chain: delta specs (`ci-integration`, `ci-module-signing-on-approval`) → workflow tests → **Red** evidence → | ||
| implement `.github/workflows/sign-modules-on-approval.yml` + `pr-orchestrator.yml` → **Green** evidence. | ||
|
|
||
| ## Red (workflow tests before `sign-modules-on-approval.yml` existed) | ||
|
|
||
| Artifacts: `tests/unit/workflows/test_sign_modules_on_approval.py` (expects | ||
| `.github/workflows/sign-modules-on-approval.yml`), and companion tests for | ||
| `pr-orchestrator.yml` / pre-commit parity added in the same change. | ||
|
|
||
| Captured **2026-04-14** by temporarily moving the workflow file aside and running one failing test: | ||
|
|
||
| ```bash | ||
| python -m pytest \ | ||
| tests/unit/workflows/test_sign_modules_on_approval.py::test_sign_modules_on_approval_trigger_and_job_filter \ | ||
| -q | ||
| ``` | ||
|
|
||
| Excerpt (failure: missing `sign-modules-on-approval.yml`): | ||
|
|
||
| ```text | ||
| tests/unit/workflows/test_sign_modules_on_approval.py F [100%] | ||
|
|
||
| =================================== FAILURES =================================== | ||
| _____________ test_sign_modules_on_approval_trigger_and_job_filter _____________ | ||
|
|
||
| def test_sign_modules_on_approval_trigger_and_job_filter() -> None: | ||
| > workflow = _workflow_text() | ||
| ... | ||
| E FileNotFoundError: [Errno 2] No such file or directory: '.../.github/workflows/sign-modules-on-approval.yml' | ||
|
|
||
| =========================== short test summary info ============================ | ||
| FAILED tests/unit/workflows/test_sign_modules_on_approval.py::test_sign_modules_on_approval_trigger_and_job_filter | ||
| ============================== 1 failed in 0.22s =============================== | ||
| ``` | ||
|
|
||
| ## Green (verification) | ||
|
|
||
| Run from worktree `feature/marketplace-06-ci-module-signing` (repo root of this change): | ||
|
|
||
| ```bash | ||
| python -m pytest tests/unit/workflows/ tests/unit/test_pre_commit_quality_parity.py \ | ||
| tests/unit/test_pre_commit_verify_modules_signature_script.py -q | ||
| # workflow + pre-commit parity tests (2026-04-14) | ||
|
|
||
| hatch run contract-test | ||
| # 555 passed (2026-04-14, after sign-modules-on-approval hardening) | ||
|
|
||
| hatch run lint | ||
| # All checks passed | ||
|
|
||
| hatch run yaml-lint | ||
| # Validated manifests / registry | ||
|
|
||
| actionlint .github/workflows/pr-orchestrator.yml .github/workflows/sign-modules-on-approval.yml | ||
| # exit 0 | ||
| ``` | ||
|
|
||
| ### PR review follow-up (sign-modules-on-approval) | ||
|
|
||
| - Same-repo PRs only: job `if` adds `github.event.pull_request.head.repo.full_name == github.repository` so fork PRs are not pushed to the wrong remote. | ||
| - Checkout uses `head.ref` (branch tip) to align with `git push origin "HEAD:${PR_HEAD_REF}"`. | ||
| - Signing uses `MERGE_BASE="$(git merge-base HEAD "origin/${PR_BASE_REF}")"` after `git fetch origin "${PR_BASE_REF}" --no-tags` so `--changed-only` is PR-scoped vs the merge-base, not the moving base-branch tip. | ||
|
|
||
| SpecFact code review (use Hatch so `semgrep` resolves to the project venv; system `specfact` may | ||
| invoke a broken `semgrep` shim): | ||
|
|
||
| ```bash | ||
| hatch run specfact code review run --json --out .specfact/code-review.json --include-tests \ | ||
| tests/unit/workflows/test_pr_orchestrator_signing.py \ | ||
| tests/unit/workflows/test_sign_modules_on_approval.py | ||
| # exit 0 (2026-04-14) | ||
| ``` | ||
|
|
||
| ## Pre-commit note | ||
|
|
||
| `scripts/pre_commit_code_review.py` now skips `openspec/changes/**/*.md` (including `tasks.md`) so | ||
| the SpecFact review gate does not parse Markdown task lists as Python (false positives). | ||
|
|
||
| ## PR | ||
|
|
||
| - Opened: [specfact-cli-modules#188](https://github.com/nold-ai/specfact-cli-modules/pull/188) (target `dev`). | ||
|
|
||
| ## Cleanup (post-merge, human) | ||
|
|
||
| Tasks 6.3–6.4 (remove worktree / branch after merge, record cleanup): pending merge. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,3 +34,20 @@ for pull requests or pushes targeting `dev`; it SHALL enforce `--require-signatu | |
| - **WHEN** a commit is pushed to `main` (post-merge) | ||
| - **THEN** the `verify-module-signatures` job SHALL run with `--require-signature` | ||
| - **AND** fail if any `packages/*/module-package.yaml` lacks a valid signature | ||
|
|
||
| ### Requirement: pre-commit verify mirrors pr-orchestrator signature policy | ||
|
|
||
| The repository pre-commit hook that runs `verify-modules-signature.py` SHALL apply the same branch rule as the `verify-module-signatures` CI job: omit `--require-signature` unless the integration target is `main` (local branch `main`, or `GITHUB_BASE_REF=main` on pull request events in Actions). | ||
|
|
||
| #### Scenario: Commit on a feature branch without a signing key | ||
|
|
||
| - **WHEN** a developer commits on a branch other than `main` | ||
| - **AND** manifests satisfy checksum and version-bump policy but lack a valid signature | ||
| - **THEN** the pre-commit signature hook SHALL pass (no `--require-signature`) | ||
| - **AND** the developer SHALL remain unblocked until a `main`-targeting flow enforces signatures in CI | ||
|
|
||
| #### Scenario: Commit on main requires signatures | ||
|
|
||
| - **WHEN** a developer commits on branch `main` | ||
| - **AND** any `packages/*/module-package.yaml` lacks a valid signature under `--require-signature` | ||
| - **THEN** the pre-commit signature hook SHALL fail | ||
|
Comment on lines
+38
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Coordinate merge timing with the paired core-repo policy change. This requirement defines a cross-repo contract (“pre-commit/CI mirror policy”). Before rollout, confirm 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.