From a62d3ca6d5fbfc50d874613bee14cde6e550e03f Mon Sep 17 00:00:00 2001 From: DJ Date: Tue, 7 Apr 2026 19:46:03 -0700 Subject: [PATCH 1/3] feat(workflows): centralize standards via reusable workflows Build org-wide reusable workflows for the four standards that previously required full inline copies in every downstream repo, and migrate the matching standards/workflows/*.yml templates to thin caller stubs that delegate via `uses: petry-projects/.github/.github/workflows/*-reusable.yml@main`. This extends the pattern already proven by feature-ideation and the existing claude-code-reusable workflow to the rest of the standard set: - dependency-audit-reusable.yml (zero per-repo config) - dependabot-automerge-reusable.yml (uses secrets: inherit for APP_*) - dependabot-rebase-reusable.yml (uses secrets: inherit for APP_*) - agent-shield-reusable.yml (inputs for severity/required-files/org-ref) The standards/workflows/claude.yml template was also still the inline 115-line version even though claude-code-reusable.yml has existed for weeks; migrate it to a stub matching the central repo's own claude.yml. Each migrated stub now carries a uniform "SOURCE OF TRUTH" header block telling agents what they may and may not edit. Net effect: ~580 lines removed from standards/workflows, single point of maintenance for the five centralizable workflows. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/agent-shield-reusable.yml | 130 ++++++++++ .../dependabot-automerge-reusable.yml | 94 ++++++++ .../workflows/dependabot-rebase-reusable.yml | 144 +++++++++++ .../workflows/dependency-audit-reusable.yml | 215 +++++++++++++++++ standards/workflows/agent-shield.yml | 108 ++------- standards/workflows/claude.yml | 109 ++------- standards/workflows/dependabot-automerge.yml | 96 ++------ standards/workflows/dependabot-rebase.yml | 146 ++---------- standards/workflows/dependency-audit.yml | 223 ++---------------- 9 files changed, 682 insertions(+), 583 deletions(-) create mode 100644 .github/workflows/agent-shield-reusable.yml create mode 100644 .github/workflows/dependabot-automerge-reusable.yml create mode 100644 .github/workflows/dependabot-rebase-reusable.yml create mode 100644 .github/workflows/dependency-audit-reusable.yml diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml new file mode 100644 index 0000000..86c1c99 --- /dev/null +++ b/.github/workflows/agent-shield-reusable.yml @@ -0,0 +1,130 @@ +# Reusable AgentShield workflow — single source of truth for the org. +# Repo-level agent-shield.yml files call this to avoid duplicating +# the security scan and structural checks. +# Standard: https://github.com/petry-projects/.github/blob/main/standards/agent-standards.md +# +# Two-layer approach: +# 1. ecc-agentshield CLI — deep security scan (102 rules across secrets, +# permissions, hooks, MCP servers, and agent config) +# 2. Org-specific structural checks — required files, cross-references, +# SKILL.md frontmatter validation +# +# Inputs let callers tune severity, the org reference string, and the set +# of required files without forking the workflow. +name: AgentShield (Reusable) + +on: + workflow_call: + inputs: + min-severity: + description: "Minimum AgentShield severity to fail on (low/medium/high/critical)" + required: false + type: string + default: "high" + agentshield-version: + description: "Pinned ecc-agentshield npm version" + required: false + type: string + default: "1.4.0" + required-files: + description: "Newline-separated list of required agent config files" + required: false + type: string + default: | + CLAUDE.md + AGENTS.md + org-standards-ref: + description: "String AGENTS.md must reference (regex, basic grep -E)" + required: false + type: string + default: 'petry-projects/\.github' + +permissions: + contents: read + +jobs: + agent-shield: + name: AgentShield + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # --- Deep security scan via AgentShield CLI --- + # Uses ecc-agentshield (https://github.com/affaan-m/agentshield) + # 102 rules: secrets, permissions, hooks, MCP servers, agent config + - name: AgentShield Security Scan + env: + AS_VERSION: ${{ inputs.agentshield-version }} + AS_SEVERITY: ${{ inputs.min-severity }} + run: | + npx "ecc-agentshield@${AS_VERSION}" scan \ + --path . \ + --min-severity "${AS_SEVERITY}" \ + --format terminal + + # --- Org-specific structural checks --- + - name: Check required agent files exist + env: + REQUIRED_FILES: ${{ inputs.required-files }} + run: | + status=0 + while IFS= read -r f; do + [ -z "$f" ] && continue + if [ ! -f "$f" ]; then + echo "::error::Missing required agent file: $f" + status=1 + fi + done <<< "$REQUIRED_FILES" + exit $status + + - name: Validate cross-references + env: + ORG_REF: ${{ inputs.org-standards-ref }} + run: | + status=0 + + if [ -f "CLAUDE.md" ] && \ + ! grep -qi 'AGENTS.md' CLAUDE.md; then + echo "::error file=CLAUDE.md::Must reference AGENTS.md" + status=1 + fi + + if [ -f "AGENTS.md" ] && \ + ! grep -qiE "$ORG_REF" AGENTS.md; then + echo "::error file=AGENTS.md::Must reference org standards ($ORG_REF)" + status=1 + fi + + exit $status + + - name: Validate SKILL.md frontmatter + run: | + status=0 + + while IFS= read -r file; do + frontmatter=$(awk \ + '/^---$/{n++; next} n==1{print} n>=2{exit}' \ + "$file") + + if [ -z "$frontmatter" ]; then + echo "::error file=$file::Missing YAML frontmatter" + status=1 + continue + fi + + if ! echo "$frontmatter" | grep -q '^name:'; then + echo "::error file=$file::Missing 'name' field" + status=1 + fi + if ! echo "$frontmatter" | grep -q '^description:'; then + echo "::error file=$file::Missing 'description' field" + status=1 + fi + done < <(find . -name 'SKILL.md' \ + -not -path '*/node_modules/*' \ + -not -path '*/.git/*') + + if [ "$status" -eq 0 ]; then + echo "All SKILL.md frontmatter validated." + fi + exit $status diff --git a/.github/workflows/dependabot-automerge-reusable.yml b/.github/workflows/dependabot-automerge-reusable.yml new file mode 100644 index 0000000..cfdad43 --- /dev/null +++ b/.github/workflows/dependabot-automerge-reusable.yml @@ -0,0 +1,94 @@ +# Reusable Dependabot auto-merge workflow — single source of truth for the org. +# Repo-level dependabot-automerge.yml files call this to avoid duplicating +# eligibility logic and the GitHub App token dance. +# Standard: https://github.com/petry-projects/.github/blob/main/standards/dependabot-policy.md +# +# Auto-approves and enables auto-merge for Dependabot PRs that are: +# - GitHub Actions updates (patch or minor version bumps) +# - Security updates for any ecosystem (patch or minor) +# - Indirect (transitive) dependency updates +# Major version updates are always left for human review. +# Uses --auto so the merge waits for all required CI checks to pass. +# +# Safety model: application ecosystems use open-pull-requests-limit: 0 in +# dependabot.yml, so the only app-ecosystem PRs Dependabot can create are +# security updates. This workflow adds defense-in-depth by also checking +# the package ecosystem. +# +# Required org/repo secrets (passed via `secrets: inherit` from caller): +# APP_ID — GitHub App ID with contents:write and pull-requests:write +# APP_PRIVATE_KEY — GitHub App private key +name: Dependabot auto-merge (Reusable) + +on: + workflow_call: + secrets: + APP_ID: + description: "GitHub App ID with contents:write and pull-requests:write" + required: true + APP_PRIVATE_KEY: + description: "GitHub App private key" + required: true + +jobs: + dependabot: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + skip-commit-verification: true + + - name: Determine if auto-merge eligible + id: eligible + run: | + UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}" + DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}" + ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}" + + # Must be patch, minor, or indirect + if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \ + "$UPDATE_TYPE" != "version-update:semver-minor" && \ + "$DEP_TYPE" != "indirect" ]]; then + echo "eligible=false" >> "$GITHUB_OUTPUT" + echo "Skipping: major update requires human review" + exit 0 + fi + + # GitHub Actions version updates are always eligible + # App ecosystem PRs can only exist as security updates (limit: 0) + echo "eligible=true" >> "$GITHUB_OUTPUT" + echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE" + + - name: Check app secrets + if: steps.eligible.outputs.eligible == 'true' + env: + APP_ID: ${{ secrets.APP_ID }} + run: | + if [[ -z "$APP_ID" ]]; then + echo "::error::APP_ID secret is missing. Set APP_ID and APP_PRIVATE_KEY in org secrets. See standards/dependabot-policy.md" + exit 1 + fi + + - name: Generate app token + if: steps.eligible.outputs.eligible == 'true' + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Approve and enable auto-merge + if: steps.eligible.outputs.eligible == 'true' + run: | + gh pr review --approve "$PR_URL" + gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/dependabot-rebase-reusable.yml b/.github/workflows/dependabot-rebase-reusable.yml new file mode 100644 index 0000000..83eff82 --- /dev/null +++ b/.github/workflows/dependabot-rebase-reusable.yml @@ -0,0 +1,144 @@ +# Reusable Dependabot update-and-merge workflow — single source of truth for the org. +# Repo-level dependabot-rebase.yml files call this to avoid duplicating +# the rebase/merge serialization logic. +# Standard: https://github.com/petry-projects/.github/blob/main/standards/dependabot-policy.md +# +# Problem: when branch protection requires branches to be up-to-date +# (strict status checks), merging one Dependabot PR makes the others fall +# behind. Dependabot does not auto-rebase PRs that are merely behind — it +# only rebases on its next scheduled run or when there are merge conflicts. +# Additionally, GitHub auto-merge (--auto) may not trigger when rulesets +# cause mergeable_state to report "blocked" even though all requirements +# are actually met. +# +# Solution: after every push to main (typically a merged PR), this workflow: +# 1. Updates behind Dependabot PRs using the merge method (not rebase) +# 2. Merges any Dependabot PR that is up-to-date, approved, and passing CI +# +# Using the app token for merges ensures the resulting push to main triggers +# this workflow again, creating a self-sustaining chain that serializes +# Dependabot PR merges one at a time. +# +# Important: never use the API update-branch endpoint with rebase method on +# Dependabot PRs — it replaces Dependabot's commit signature with GitHub's, +# which breaks dependabot/fetch-metadata verification and causes Dependabot +# to refuse future rebases on that PR. The merge method preserves the +# original commits. +# +# Note: the merge commit is authored by GitHub, not Dependabot, so the +# dependabot-automerge workflow must use skip-commit-verification: true +# in the dependabot/fetch-metadata step. +# +# Required org/repo secrets (passed via `secrets: inherit` from caller): +# APP_ID — GitHub App ID with contents:write and pull-requests:write +# APP_PRIVATE_KEY — GitHub App private key +name: Dependabot update and merge (Reusable) + +on: + workflow_call: + secrets: + APP_ID: + description: "GitHub App ID with contents:write and pull-requests:write" + required: true + APP_PRIVATE_KEY: + description: "GitHub App private key" + required: true + +jobs: + update-and-merge: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Check app secrets + env: + APP_ID: ${{ secrets.APP_ID }} + run: | + if [[ -z "$APP_ID" ]]; then + echo "::error::APP_ID secret is missing. Set APP_ID and APP_PRIVATE_KEY in org secrets. See standards/dependabot-policy.md" + exit 1 + fi + + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Update and merge Dependabot PRs + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + run: | + # Find open Dependabot PRs + PRS=$(gh pr list --repo "$REPO" --author "app/dependabot" \ + --json number,headRefName \ + --jq '.[] | "\(.number) \(.headRefName)"') + + if [[ -z "$PRS" ]]; then + echo "No open Dependabot PRs" + exit 0 + fi + + MERGED=false + + while IFS=' ' read -r PR_NUMBER HEAD_REF; do + BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \ + --jq '.behind_by') + + if [[ "$BEHIND" -gt 0 ]]; then + echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — merging main into branch" + gh api "repos/$REPO/pulls/$PR_NUMBER/update-branch" \ + -X PUT -f update_method=merge \ + --silent || echo "Warning: failed to update PR #$PR_NUMBER" + continue + fi + + echo "PR #$PR_NUMBER ($HEAD_REF) is up to date — checking if merge-ready" + + # Skip if we already merged one (strict mode means others are now behind) + if [[ "$MERGED" == "true" ]]; then + echo " Skipping — already merged a PR this run" + continue + fi + + # Check if auto-merge is enabled (set by the automerge workflow) + AUTO_MERGE=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json autoMergeRequest --jq '.autoMergeRequest != null') + + if [[ "$AUTO_MERGE" != "true" ]]; then + echo " Skipping — auto-merge not enabled" + continue + fi + + # Check if all required checks pass (look at overall rollup) + CHECKS_PASS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json statusCheckRollup \ + --jq '[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or . == "NEUTRAL" or . == "SKIPPED")') + + CHECKS_PENDING=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json statusCheckRollup \ + --jq '[.statusCheckRollup[]? | select(.name != null and .status != "COMPLETED")] | length') + + if [[ "$CHECKS_PENDING" -gt 0 ]]; then + echo " Skipping — $CHECKS_PENDING check(s) still pending" + continue + fi + + if [[ "$CHECKS_PASS" != "true" ]]; then + echo " Skipping — some checks failed" + continue + fi + + echo " All checks pass — merging PR #$PR_NUMBER" + if gh api "repos/$REPO/pulls/$PR_NUMBER/merge" \ + -X PUT -f merge_method=squash \ + --silent; then + echo " Merged PR #$PR_NUMBER" + MERGED=true + else + echo " Warning: failed to merge PR #$PR_NUMBER" + fi + done <<< "$PRS" diff --git a/.github/workflows/dependency-audit-reusable.yml b/.github/workflows/dependency-audit-reusable.yml new file mode 100644 index 0000000..51a0613 --- /dev/null +++ b/.github/workflows/dependency-audit-reusable.yml @@ -0,0 +1,215 @@ +# Reusable dependency vulnerability audit — single source of truth for the org. +# Repo-level dependency-audit.yml files call this to avoid duplicating the +# multi-ecosystem detection and audit logic. +# Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md#5-dependency-audit-dependency-auditym +# +# Auto-detects ecosystems present in the repository and runs the appropriate +# audit tool. Fails the build if any dependency has a known security advisory. +# +# Pinned tool versions (update deliberately): +# govulncheck v1.1.4 | cargo-audit 0.22.1 | pip-audit 2.9.0 +name: Dependency audit (Reusable) + +on: + workflow_call: + +permissions: + contents: read + +jobs: + detect: + name: Detect ecosystems + runs-on: ubuntu-latest + outputs: + npm: ${{ steps.check.outputs.npm }} + pnpm: ${{ steps.check.outputs.pnpm }} + gomod: ${{ steps.check.outputs.gomod }} + cargo: ${{ steps.check.outputs.cargo }} + pip: ${{ steps.check.outputs.pip }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Detect package ecosystems + id: check + run: | + # npm — look for package-lock.json anywhere (excluding node_modules) + if find . -name 'package-lock.json' -not -path '*/node_modules/*' | grep -q .; then + echo "npm=true" >> "$GITHUB_OUTPUT" + else + echo "npm=false" >> "$GITHUB_OUTPUT" + fi + + # pnpm — look for pnpm-lock.yaml anywhere + if find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' | grep -q .; then + echo "pnpm=true" >> "$GITHUB_OUTPUT" + else + echo "pnpm=false" >> "$GITHUB_OUTPUT" + fi + + # Go modules — detect via go.mod (not go.sum, which may not exist) + if find . -name 'go.mod' -not -path '*/vendor/*' | grep -q .; then + echo "gomod=true" >> "$GITHUB_OUTPUT" + else + echo "gomod=false" >> "$GITHUB_OUTPUT" + fi + + # Cargo — detect via Cargo.toml anywhere (lockfile may not exist for libraries) + if find . -name 'Cargo.toml' -not -path '*/target/*' | grep -q .; then + echo "cargo=true" >> "$GITHUB_OUTPUT" + else + echo "cargo=false" >> "$GITHUB_OUTPUT" + fi + + # Python — detect pyproject.toml or requirements.txt anywhere + if find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q . || \ + find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q .; then + echo "pip=true" >> "$GITHUB_OUTPUT" + else + echo "pip=false" >> "$GITHUB_OUTPUT" + fi + + audit-npm: + name: npm audit + needs: detect + if: needs.detect.outputs.npm == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: "lts/*" + + - name: Audit npm dependencies + run: | + # Audit each package-lock.json found in the repo + status=0 + while IFS= read -r dir; do + echo "::group::npm audit $dir" + if ! (cd "$dir" && npm audit --audit-level=low); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'package-lock.json' -not -path '*/node_modules/*' -exec dirname {} \;) + exit $status + + audit-pnpm: + name: pnpm audit + needs: detect + if: needs.detect.outputs.pnpm == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: "lts/*" + + - name: Audit pnpm dependencies + run: | + # Audit each pnpm-lock.yaml found in the repo + status=0 + while IFS= read -r dir; do + echo "::group::pnpm audit $dir" + if ! (cd "$dir" && pnpm audit --audit-level low); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' -exec dirname {} \;) + exit $status + + audit-go: + name: govulncheck + needs: detect + if: needs.detect.outputs.gomod == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5 + with: + go-version: "stable" + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 + + - name: Audit Go dependencies + run: | + status=0 + while IFS= read -r dir; do + echo "::group::govulncheck $dir" + if ! (cd "$dir" && govulncheck ./...); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'go.mod' -not -path '*/vendor/*' -exec dirname {} \;) + exit $status + + audit-cargo: + name: cargo audit + needs: detect + if: needs.detect.outputs.cargo == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install Rust stable toolchain + run: rustup toolchain install stable --profile minimal + + - name: Install cargo-audit + run: cargo install cargo-audit@0.22.1 --locked + + - name: Audit Cargo dependencies + run: | + # cargo audit operates on Cargo.lock at workspace root + # For workspaces, a single audit at root covers all crates + status=0 + while IFS= read -r dir; do + echo "::group::cargo audit $dir" + if ! (cd "$dir" && cargo generate-lockfile 2>/dev/null; cargo audit); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'Cargo.toml' -not -path '*/target/*' -exec dirname {} \; | sort -u) + exit $status + + audit-pip: + name: pip-audit + needs: detect + if: needs.detect.outputs.pip == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.x" + + - name: Install pip-audit + run: pip install pip-audit==2.9.0 + + - name: Audit Python dependencies + run: | + status=0 + # Audit each Python project found in the repo + while IFS= read -r dir; do + echo "::group::pip-audit $dir" + if [ -f "$dir/pyproject.toml" ]; then + if ! pip-audit "$dir"; then + status=1 + fi + elif [ -f "$dir/requirements.txt" ]; then + if ! pip-audit -r "$dir/requirements.txt"; then + status=1 + fi + fi + echo "::endgroup::" + done < <( + { + find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \; + find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \; + } | sort -u + ) + exit $status diff --git a/standards/workflows/agent-shield.yml b/standards/workflows/agent-shield.yml index 98be81f..b48ef5c 100644 --- a/standards/workflows/agent-shield.yml +++ b/standards/workflows/agent-shield.yml @@ -1,12 +1,22 @@ -# AgentShield — Agent configuration security validation -# See: standards/agent-standards.md +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/agent-shield.yml +# Standard: petry-projects/.github/standards/agent-standards.md +# Reusable: petry-projects/.github/.github/workflows/agent-shield-reusable.yml # -# Two-layer approach: -# 1. affaan-m/agentshield action — deep security scan (102 rules across -# secrets, permissions, hooks, MCP servers, and agent config) -# 2. Org-specific structural checks — required files, cross-references, -# SKILL.md frontmatter validation - +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. The AgentShield CLI scan and the +# org-specific structural checks live in the reusable workflow above. +# • You MAY change: the `with:` inputs (min-severity, agentshield-version, +# required-files, org-standards-ref) — only if your repo genuinely needs +# a different policy. +# • You MUST NOT change: trigger events, the `uses:` line, or the job name +# (used as a required status check). +# • If you need different behaviour beyond the inputs, open a PR against +# the reusable in the central repo. +# ───────────────────────────────────────────────────────────────────────────── +# +# AgentShield — thin caller for the org-level reusable. +# To adopt: copy this file to .github/workflows/agent-shield.yml in your repo. name: AgentShield on: @@ -20,84 +30,4 @@ permissions: jobs: agent-shield: - name: AgentShield - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - # --- Deep security scan via AgentShield CLI --- - # Uses ecc-agentshield (https://github.com/affaan-m/agentshield) - # 102 rules: secrets, permissions, hooks, MCP servers, agent config - - name: AgentShield Security Scan - run: | - npx ecc-agentshield@1.4.0 scan \ - --path . \ - --min-severity high \ - --format terminal - - # --- Org-specific structural checks --- - - name: Check required agent files exist - run: | - status=0 - - if [ ! -f "CLAUDE.md" ]; then - echo "::error::Missing CLAUDE.md" - status=1 - fi - - if [ ! -f "AGENTS.md" ]; then - echo "::error::Missing AGENTS.md" - status=1 - fi - - exit $status - - - name: Validate cross-references - run: | - status=0 - - if [ -f "CLAUDE.md" ] && \ - ! grep -qi 'AGENTS.md' CLAUDE.md; then - echo "::error file=CLAUDE.md::Must reference AGENTS.md" - status=1 - fi - - if [ -f "AGENTS.md" ] && \ - ! grep -qi 'petry-projects/\.github' AGENTS.md; then - echo "::error file=AGENTS.md::Must reference org standards" - status=1 - fi - - exit $status - - - name: Validate SKILL.md frontmatter - run: | - status=0 - - while IFS= read -r file; do - frontmatter=$(awk \ - '/^---$/{n++; next} n==1{print} n>=2{exit}' \ - "$file") - - if [ -z "$frontmatter" ]; then - echo "::error file=$file::Missing YAML frontmatter" - status=1 - continue - fi - - if ! echo "$frontmatter" | grep -q '^name:'; then - echo "::error file=$file::Missing 'name' field" - status=1 - fi - if ! echo "$frontmatter" | grep -q '^description:'; then - echo "::error file=$file::Missing 'description' field" - status=1 - fi - done < <(find . -name 'SKILL.md' \ - -not -path '*/node_modules/*' \ - -not -path '*/.git/*') - - if [ "$status" -eq 0 ]; then - echo "All SKILL.md frontmatter validated." - fi - exit $status + uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@main diff --git a/standards/workflows/claude.yml b/standards/workflows/claude.yml index 7efa6d6..b3ecd91 100644 --- a/standards/workflows/claude.yml +++ b/standards/workflows/claude.yml @@ -1,14 +1,23 @@ -# Claude Code workflow template +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/claude.yml +# Standard: petry-projects/.github/standards/ci-standards.md#4-claude-code-claudeyml +# Reusable: petry-projects/.github/.github/workflows/claude-code-reusable.yml # -# Both jobs MUST include the "Checkout repository" step. -# claude-code-action reads CLAUDE.md and AGENTS.md from the working tree; -# without checkout it errors on every PR/issue trigger. +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All Claude Code logic, the prompt, +# allowedTools, and trigger gating live in the reusable workflow above. +# • You MAY change: nothing in this file in normal use. Adopt verbatim. +# • You MUST NOT change: trigger events, job permissions, the `uses:` line, +# or `secrets: inherit`. These are required for the reusable to work. +# • If you need different behaviour, open a PR against the reusable in the +# central repo. The change will propagate everywhere on next run. +# ───────────────────────────────────────────────────────────────────────────── # -# Copy this file to .github/workflows/claude.yml in each repo. -# Adjust the `prompt` in the claude-issue job to reference the correct issue number -# expression (${{ github.event.issue.number }}) — no other customisation is needed. -# -# Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md#4-claude-code-claudeyml +# Claude Code — thin caller that delegates to the org-level reusable workflow. +# To adopt: copy this file to .github/workflows/claude.yml in your repo. +# Required org/repo secret: CLAUDE_CODE_OAUTH_TOKEN +# Optional org/repo secret: GH_PAT_WORKFLOWS (PAT with `workflow` scope — +# required if Claude needs to push changes to .github/workflows/*.yml) name: Claude Code @@ -26,50 +35,9 @@ on: permissions: {} jobs: - # Interactive mode: PR reviews and @claude mentions - # NOTE: This job also requires a checkout step (see below). - claude: - if: >- - (github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository) || - (github.event_name == 'issue_comment' && github.event.issue.pull_request && - contains(github.event.comment.body, '@claude') && - contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || - (github.event_name == 'pull_request_review_comment' && - contains(github.event.comment.body, '@claude') && - contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - contents: write - id-token: write - pull-requests: write - issues: write - actions: read - checks: read - steps: - # REQUIRED: checkout must be present so claude-code-action can read CLAUDE.md / AGENTS.md - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - - name: Run Claude Code - if: github.event_name != 'pull_request' || github.event.pull_request.user.login != 'dependabot[bot]' - uses: anthropics/claude-code-action@6e2bd52842c65e914eba5c8badd17560bd26b5de # v1.0.89 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - additional_permissions: | - actions: read - checks: read - - # Automation mode: issue-triggered work — implement, open PR, review, and notify - # NOTE: This job also requires a checkout step (see below). - claude-issue: - if: >- - github.event_name == 'issues' && github.event.action == 'labeled' && - github.event.label.name == 'claude' - runs-on: ubuntu-latest - timeout-minutes: 60 + claude-code: + uses: petry-projects/.github/.github/workflows/claude-code-reusable.yml@main + secrets: inherit permissions: contents: write id-token: write @@ -77,38 +45,3 @@ jobs: issues: write actions: read checks: read - steps: - # REQUIRED: checkout must be present so claude-code-action can read CLAUDE.md / AGENTS.md - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - - name: Run Claude Code - uses: anthropics/claude-code-action@6e2bd52842c65e914eba5c8badd17560bd26b5de # v1.0.89 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - label_trigger: "claude" - track_progress: "true" - additional_permissions: | - actions: read - checks: read - claude_args: | - --allowedTools "Bash(gh pr create:*),Bash(gh pr view:*),Bash(gh run view:*),Bash(gh run watch:*),Bash(cat:*),Edit,Write" - prompt: | - Implement a fix for issue #${{ github.event.issue.number }}. - - After implementing: - 1. Create a pull request with a clear title and description. - Include "Closes #${{ github.event.issue.number }}" in the PR body. - 2. Self-review your own PR — look for bugs, style issues, - missed edge cases, and test gaps. If you find problems, push fixes. - 3. Review all comments and review threads on the PR. For each one: - - If you can address the feedback, make the fix, push, and - mark the conversation as resolved. - - If the comment requires human judgment, leave a reply - explaining what you need. - 4. Check CI status. If CI fails, read the logs, fix the issues, - and push again. Repeat until CI passes. - 5. When CI is green, all actionable review comments are resolved, - and the PR is ready, read the CODEOWNERS file and leave a - comment tagging the relevant code owners to review and merge. diff --git a/standards/workflows/dependabot-automerge.yml b/standards/workflows/dependabot-automerge.yml index c9b70d4..c445511 100644 --- a/standards/workflows/dependabot-automerge.yml +++ b/standards/workflows/dependabot-automerge.yml @@ -1,21 +1,23 @@ -# Dependabot auto-merge workflow -# Copy to .github/workflows/dependabot-automerge.yml +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/dependabot-automerge.yml +# Standard: petry-projects/.github/standards/dependabot-policy.md +# Reusable: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml # -# Requires repository secrets: +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All eligibility logic and the GitHub +# App token dance live in the reusable workflow above. +# • You MAY change: nothing in this file in normal use. Adopt verbatim. +# • You MUST NOT change: trigger event (must be `pull_request_target`), +# the `uses:` line, or `secrets: inherit`. +# • If you need different behaviour, open a PR against the reusable in the +# central repo. +# ───────────────────────────────────────────────────────────────────────────── +# +# Dependabot auto-merge — thin caller for the org-level reusable. +# To adopt: copy this file to .github/workflows/dependabot-automerge.yml in your repo. +# Required org/repo secrets (inherited): # APP_ID — GitHub App ID with contents:write and pull-requests:write # APP_PRIVATE_KEY — GitHub App private key -# -# Auto-approves and enables auto-merge for Dependabot PRs that are: -# - GitHub Actions updates (patch or minor version bumps) -# - Security updates for any ecosystem (patch or minor) -# - Indirect (transitive) dependency updates -# Major version updates are always left for human review. -# Uses --auto so the merge waits for all required CI checks to pass. -# -# Safety model: application ecosystems use open-pull-requests-limit: 0 in -# dependabot.yml, so the only app-ecosystem PRs Dependabot can create are -# security updates. This workflow adds defense-in-depth by also checking -# the package ecosystem. name: Dependabot auto-merge on: @@ -26,64 +28,6 @@ on: permissions: {} jobs: - dependabot: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - if: github.event.pull_request.user.login == 'dependabot[bot]' - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - skip-commit-verification: true - - - name: Determine if auto-merge eligible - id: eligible - run: | - UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}" - DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}" - ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}" - - # Must be patch, minor, or indirect - if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \ - "$UPDATE_TYPE" != "version-update:semver-minor" && \ - "$DEP_TYPE" != "indirect" ]]; then - echo "eligible=false" >> "$GITHUB_OUTPUT" - echo "Skipping: major update requires human review" - exit 0 - fi - - # GitHub Actions version updates are always eligible - # App ecosystem PRs can only exist as security updates (limit: 0) - echo "eligible=true" >> "$GITHUB_OUTPUT" - echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE" - - - name: Check app secrets - if: steps.eligible.outputs.eligible == 'true' - env: - APP_ID: ${{ secrets.APP_ID }} - run: | - if [[ -z "$APP_ID" ]]; then - echo "::error::APP_ID secret is missing. Set APP_ID and APP_PRIVATE_KEY in org secrets. See standards/dependabot-policy.md" - exit 1 - fi - - - name: Generate app token - if: steps.eligible.outputs.eligible == 'true' - id: app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Approve and enable auto-merge - if: steps.eligible.outputs.eligible == 'true' - run: | - gh pr review --approve "$PR_URL" - gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} + dependabot-automerge: + uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@main + secrets: inherit diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml index ff324cb..8e80bb6 100644 --- a/standards/workflows/dependabot-rebase.yml +++ b/standards/workflows/dependabot-rebase.yml @@ -1,35 +1,23 @@ -# Dependabot update and merge workflow -# Copy to .github/workflows/dependabot-rebase.yml +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/dependabot-rebase.yml +# Standard: petry-projects/.github/standards/dependabot-policy.md +# Reusable: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml # -# Requires repository secrets: +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All rebase/merge serialization logic +# lives in the reusable workflow above. +# • You MAY change: nothing in this file in normal use. Adopt verbatim. +# • You MUST NOT change: trigger event, the concurrency group name, +# the `uses:` line, or `secrets: inherit`. +# • If you need different behaviour, open a PR against the reusable in the +# central repo. +# ───────────────────────────────────────────────────────────────────────────── +# +# Dependabot update-and-merge — thin caller for the org-level reusable. +# To adopt: copy this file to .github/workflows/dependabot-rebase.yml in your repo. +# Required org/repo secrets (inherited): # APP_ID — GitHub App ID with contents:write and pull-requests:write # APP_PRIVATE_KEY — GitHub App private key -# -# Problem: when branch protection requires branches to be up-to-date -# (strict status checks), merging one Dependabot PR makes the others fall -# behind. Dependabot does not auto-rebase PRs that are merely behind — it -# only rebases on its next scheduled run or when there are merge conflicts. -# Additionally, GitHub auto-merge (--auto) may not trigger when rulesets -# cause mergeable_state to report "blocked" even though all requirements -# are actually met. -# -# Solution: after every push to main (typically a merged PR), this workflow: -# 1. Updates behind Dependabot PRs using the merge method (not rebase) -# 2. Merges any Dependabot PR that is up-to-date, approved, and passing CI -# -# Using the app token for merges ensures the resulting push to main triggers -# this workflow again, creating a self-sustaining chain that serializes -# Dependabot PR merges one at a time. -# -# Important: never use the API update-branch endpoint with rebase method on -# Dependabot PRs — it replaces Dependabot's commit signature with GitHub's, -# which breaks dependabot/fetch-metadata verification and causes Dependabot -# to refuse future rebases on that PR. The merge method preserves the -# original commits. -# -# Note: the merge commit is authored by GitHub, not Dependabot, so the -# dependabot-automerge workflow must use skip-commit-verification: true -# in the dependabot/fetch-metadata step. name: Dependabot update and merge on: @@ -44,100 +32,6 @@ concurrency: permissions: {} jobs: - update-and-merge: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - name: Check app secrets - env: - APP_ID: ${{ secrets.APP_ID }} - run: | - if [[ -z "$APP_ID" ]]; then - echo "::error::APP_ID secret is missing. Set APP_ID and APP_PRIVATE_KEY in org secrets. See standards/dependabot-policy.md" - exit 1 - fi - - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Update and merge Dependabot PRs - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - REPO: ${{ github.repository }} - run: | - # Find open Dependabot PRs - PRS=$(gh pr list --repo "$REPO" --author "app/dependabot" \ - --json number,headRefName \ - --jq '.[] | "\(.number) \(.headRefName)"') - - if [[ -z "$PRS" ]]; then - echo "No open Dependabot PRs" - exit 0 - fi - - MERGED=false - - while IFS=' ' read -r PR_NUMBER HEAD_REF; do - BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \ - --jq '.behind_by') - - if [[ "$BEHIND" -gt 0 ]]; then - echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — merging main into branch" - gh api "repos/$REPO/pulls/$PR_NUMBER/update-branch" \ - -X PUT -f update_method=merge \ - --silent || echo "Warning: failed to update PR #$PR_NUMBER" - continue - fi - - echo "PR #$PR_NUMBER ($HEAD_REF) is up to date — checking if merge-ready" - - # Skip if we already merged one (strict mode means others are now behind) - if [[ "$MERGED" == "true" ]]; then - echo " Skipping — already merged a PR this run" - continue - fi - - # Check if auto-merge is enabled (set by the automerge workflow) - AUTO_MERGE=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ - --json autoMergeRequest --jq '.autoMergeRequest != null') - - if [[ "$AUTO_MERGE" != "true" ]]; then - echo " Skipping — auto-merge not enabled" - continue - fi - - # Check if all required checks pass (look at overall rollup) - CHECKS_PASS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ - --json statusCheckRollup \ - --jq '[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or . == "NEUTRAL" or . == "SKIPPED")') - - CHECKS_PENDING=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ - --json statusCheckRollup \ - --jq '[.statusCheckRollup[]? | select(.name != null and .status != "COMPLETED")] | length') - - if [[ "$CHECKS_PENDING" -gt 0 ]]; then - echo " Skipping — $CHECKS_PENDING check(s) still pending" - continue - fi - - if [[ "$CHECKS_PASS" != "true" ]]; then - echo " Skipping — some checks failed" - continue - fi - - echo " All checks pass — merging PR #$PR_NUMBER" - if gh api "repos/$REPO/pulls/$PR_NUMBER/merge" \ - -X PUT -f merge_method=squash \ - --silent; then - echo " Merged PR #$PR_NUMBER" - MERGED=true - else - echo " Warning: failed to merge PR #$PR_NUMBER" - fi - done <<< "$PRS" + dependabot-rebase: + uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@main + secrets: inherit diff --git a/standards/workflows/dependency-audit.yml b/standards/workflows/dependency-audit.yml index a413603..8024aa2 100644 --- a/standards/workflows/dependency-audit.yml +++ b/standards/workflows/dependency-audit.yml @@ -1,13 +1,22 @@ -# Dependency vulnerability audit -# Copy to .github/workflows/dependency-audit.yml +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/dependency-audit.yml +# Standard: petry-projects/.github/standards/ci-standards.md#5-dependency-audit-dependency-auditym +# Reusable: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml # -# Auto-detects ecosystems present in the repository and runs the appropriate -# audit tool. Fails the build if any dependency has a known security advisory. +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All ecosystem-detection and audit logic +# lives in the reusable workflow above. +# • You MAY change: nothing in this file in normal use. Adopt verbatim. +# • You MUST NOT change: trigger events, the `uses:` line, or job name +# (used as a required status check). +# • If you need different behaviour (new ecosystem, tool version bump), +# open a PR against the reusable in the central repo. +# ───────────────────────────────────────────────────────────────────────────── # -# Add "dependency-audit" as a required status check in branch protection. -# -# Pinned tool versions (update deliberately): -# govulncheck v1.1.4 | cargo-audit 0.22.1 | pip-audit 2.9.0 +# Dependency vulnerability audit — thin caller for the org-level reusable. +# To adopt: copy this file to .github/workflows/dependency-audit.yml in your repo. +# Add "dependency-audit / Detect ecosystems" as a required status check +# in branch protection. name: Dependency audit on: @@ -20,199 +29,5 @@ permissions: contents: read jobs: - detect: - name: Detect ecosystems - runs-on: ubuntu-latest - outputs: - npm: ${{ steps.check.outputs.npm }} - pnpm: ${{ steps.check.outputs.pnpm }} - gomod: ${{ steps.check.outputs.gomod }} - cargo: ${{ steps.check.outputs.cargo }} - pip: ${{ steps.check.outputs.pip }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Detect package ecosystems - id: check - run: | - # npm — look for package-lock.json anywhere (excluding node_modules) - if find . -name 'package-lock.json' -not -path '*/node_modules/*' | grep -q .; then - echo "npm=true" >> "$GITHUB_OUTPUT" - else - echo "npm=false" >> "$GITHUB_OUTPUT" - fi - - # pnpm — look for pnpm-lock.yaml anywhere - if find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' | grep -q .; then - echo "pnpm=true" >> "$GITHUB_OUTPUT" - else - echo "pnpm=false" >> "$GITHUB_OUTPUT" - fi - - # Go modules — detect via go.mod (not go.sum, which may not exist) - if find . -name 'go.mod' -not -path '*/vendor/*' | grep -q .; then - echo "gomod=true" >> "$GITHUB_OUTPUT" - else - echo "gomod=false" >> "$GITHUB_OUTPUT" - fi - - # Cargo — detect via Cargo.toml anywhere (lockfile may not exist for libraries) - if find . -name 'Cargo.toml' -not -path '*/target/*' | grep -q .; then - echo "cargo=true" >> "$GITHUB_OUTPUT" - else - echo "cargo=false" >> "$GITHUB_OUTPUT" - fi - - # Python — detect pyproject.toml or requirements.txt anywhere - if find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q . || \ - find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q .; then - echo "pip=true" >> "$GITHUB_OUTPUT" - else - echo "pip=false" >> "$GITHUB_OUTPUT" - fi - - audit-npm: - name: npm audit - needs: detect - if: needs.detect.outputs.npm == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "lts/*" - - - name: Audit npm dependencies - run: | - # Audit each package-lock.json found in the repo - status=0 - while IFS= read -r dir; do - echo "::group::npm audit $dir" - if ! (cd "$dir" && npm audit --audit-level=low); then - status=1 - fi - echo "::endgroup::" - done < <(find . -name 'package-lock.json' -not -path '*/node_modules/*' -exec dirname {} \;) - exit $status - - audit-pnpm: - name: pnpm audit - needs: detect - if: needs.detect.outputs.pnpm == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "lts/*" - - - name: Audit pnpm dependencies - run: | - # Audit each pnpm-lock.yaml found in the repo - status=0 - while IFS= read -r dir; do - echo "::group::pnpm audit $dir" - if ! (cd "$dir" && pnpm audit --audit-level low); then - status=1 - fi - echo "::endgroup::" - done < <(find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' -exec dirname {} \;) - exit $status - - audit-go: - name: govulncheck - needs: detect - if: needs.detect.outputs.gomod == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5 - with: - go-version: "stable" - - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 - - - name: Audit Go dependencies - run: | - status=0 - while IFS= read -r dir; do - echo "::group::govulncheck $dir" - if ! (cd "$dir" && govulncheck ./...); then - status=1 - fi - echo "::endgroup::" - done < <(find . -name 'go.mod' -not -path '*/vendor/*' -exec dirname {} \;) - exit $status - - audit-cargo: - name: cargo audit - needs: detect - if: needs.detect.outputs.cargo == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install Rust stable toolchain - run: rustup toolchain install stable --profile minimal - - - name: Install cargo-audit - run: cargo install cargo-audit@0.22.1 --locked - - - name: Audit Cargo dependencies - run: | - # cargo audit operates on Cargo.lock at workspace root - # For workspaces, a single audit at root covers all crates - status=0 - while IFS= read -r dir; do - echo "::group::cargo audit $dir" - if ! (cd "$dir" && cargo generate-lockfile 2>/dev/null; cargo audit); then - status=1 - fi - echo "::endgroup::" - done < <(find . -name 'Cargo.toml' -not -path '*/target/*' -exec dirname {} \; | sort -u) - exit $status - - audit-pip: - name: pip-audit - needs: detect - if: needs.detect.outputs.pip == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: - python-version: "3.x" - - - name: Install pip-audit - run: pip install pip-audit==2.9.0 - - - name: Audit Python dependencies - run: | - status=0 - # Audit each Python project found in the repo - while IFS= read -r dir; do - echo "::group::pip-audit $dir" - if [ -f "$dir/pyproject.toml" ]; then - if ! pip-audit "$dir"; then - status=1 - fi - elif [ -f "$dir/requirements.txt" ]; then - if ! pip-audit -r "$dir/requirements.txt"; then - status=1 - fi - fi - echo "::endgroup::" - done < <( - { - find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \; - find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \; - } | sort -u - ) - exit $status + dependency-audit: + uses: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml@main From bc1903327395e19a82853d7fa1be24d485934484 Mon Sep 17 00:00:00 2001 From: DJ Date: Tue, 7 Apr 2026 19:58:20 -0700 Subject: [PATCH 2/3] fix(workflows): grant read permissions to dependabot caller stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable workflows can be granted no more permissions than the calling workflow has. The dependabot-automerge and dependabot-rebase stubs had `permissions: {}` at workflow level with no job-level overrides, which intersected to zero — the reusable's `gh pr ...` calls would fail because GITHUB_TOKEN had no scopes. Fix: declare `contents: read` and `pull-requests: read` on the calling job, matching the scopes the reusable's job already declares. Caught by Copilot review on #87. Co-Authored-By: Claude Opus 4.6 (1M context) --- standards/workflows/dependabot-automerge.yml | 3 +++ standards/workflows/dependabot-rebase.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/standards/workflows/dependabot-automerge.yml b/standards/workflows/dependabot-automerge.yml index c445511..8bf1af6 100644 --- a/standards/workflows/dependabot-automerge.yml +++ b/standards/workflows/dependabot-automerge.yml @@ -29,5 +29,8 @@ permissions: {} jobs: dependabot-automerge: + permissions: + contents: read + pull-requests: read uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@main secrets: inherit diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml index 8e80bb6..bdf8872 100644 --- a/standards/workflows/dependabot-rebase.yml +++ b/standards/workflows/dependabot-rebase.yml @@ -33,5 +33,8 @@ permissions: {} jobs: dependabot-rebase: + permissions: + contents: read + pull-requests: read uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@main secrets: inherit From ff437f25a26da837ef968ef89af94718862d3212 Mon Sep 17 00:00:00 2001 From: DJ Date: Tue, 7 Apr 2026 20:05:50 -0700 Subject: [PATCH 3/3] docs(workflows): note permissions stanza in immutable-stub contract CodeRabbit follow-up on #87: now that the dependabot stubs declare a job-level permissions block (required for the reusable's gh API calls), add it to the "MUST NOT change" list so future adopters don't strip it. Co-Authored-By: Claude Opus 4.6 (1M context) --- standards/workflows/dependabot-automerge.yml | 5 ++++- standards/workflows/dependabot-rebase.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/standards/workflows/dependabot-automerge.yml b/standards/workflows/dependabot-automerge.yml index 8bf1af6..de974cd 100644 --- a/standards/workflows/dependabot-automerge.yml +++ b/standards/workflows/dependabot-automerge.yml @@ -8,7 +8,10 @@ # App token dance live in the reusable workflow above. # • You MAY change: nothing in this file in normal use. Adopt verbatim. # • You MUST NOT change: trigger event (must be `pull_request_target`), -# the `uses:` line, or `secrets: inherit`. +# the `uses:` line, `secrets: inherit`, or the job-level `permissions:` +# block — reusable workflows can be granted no more permissions than the +# calling job has, so removing the stanza breaks the reusable's gh API +# calls. # • If you need different behaviour, open a PR against the reusable in the # central repo. # ───────────────────────────────────────────────────────────────────────────── diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml index bdf8872..21ebe40 100644 --- a/standards/workflows/dependabot-rebase.yml +++ b/standards/workflows/dependabot-rebase.yml @@ -8,7 +8,10 @@ # lives in the reusable workflow above. # • You MAY change: nothing in this file in normal use. Adopt verbatim. # • You MUST NOT change: trigger event, the concurrency group name, -# the `uses:` line, or `secrets: inherit`. +# the `uses:` line, `secrets: inherit`, or the job-level `permissions:` +# block — reusable workflows can be granted no more permissions than the +# calling job has, so removing the stanza breaks the reusable's gh API +# calls. # • If you need different behaviour, open a PR against the reusable in the # central repo. # ─────────────────────────────────────────────────────────────────────────────