diff --git a/.github/workflows/agent-shield.yml b/.github/workflows/agent-shield.yml index 7304d3fc..8704981d 100644 --- a/.github/workflows/agent-shield.yml +++ b/.github/workflows/agent-shield.yml @@ -1,8 +1,22 @@ -# AgentShield — Agent configuration security validation -# Two-layer approach: -# 1. affaan-m/agentshield — deep security scan (102 rules) -# 2. Org-specific structural checks (required files, frontmatter) - +# ───────────────────────────────────────────────────────────────────────────── +# 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 +# +# 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: @@ -16,122 +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 - - - name: Validate module-help.csv consistency - if: hashFiles('src/module-help.csv') != '' - run: | - status=0 - - if [ -d "src/agents" ]; then - for dir in src/agents/*/; do - [ -d "$dir" ] || continue - skill_name=$(basename "$dir") - if ! awk -F',' -v name="$skill_name" \ - '{gsub(/^[ ]+|[ ]+$/, "", $2)} - $2 == name {found=1; exit} - END {exit !found}' src/module-help.csv; then - echo "::error::Agent $skill_name not in module-help.csv" - status=1 - fi - done - fi - - if [ -d "src/workflows" ]; then - for dir in src/workflows/*/; do - [ -d "$dir" ] || continue - skill_name=$(basename "$dir") - if ! awk -F',' -v name="$skill_name" \ - '{gsub(/^[ ]+|[ ]+$/, "", $2)} - $2 == name {found=1; exit} - END {exit !found}' src/module-help.csv; then - echo "::error::Workflow $skill_name not in module-help.csv" - status=1 - fi - done - fi - - if [ "$status" -eq 0 ]; then - echo "Module-help.csv consistency check passed." - fi - exit $status + uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@v1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 70bfde0f..004c6efa 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -1,5 +1,5 @@ -# Claude Code — thin caller that delegates to the org-level reusable workflow. -# All logic and prompts are maintained centrally in claude-code-reusable.yml. +# AI-assisted code review via Claude Code Action on PRs. +# Issue automation: implement, open PR, self-review, check CI, notify maintainer. # Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md#4-claude-code-claudeyml name: Claude Code @@ -17,9 +17,20 @@ on: permissions: {} jobs: - claude-code: - uses: petry-projects/.github/.github/workflows/claude-code-reusable.yml@main - secrets: inherit + # Interactive mode: PR reviews and @claude mentions + 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' && + github.event.pull_request.head.repo.full_name == github.repository && + 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 @@ -27,3 +38,62 @@ jobs: issues: write actions: read checks: read + steps: + - 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 + claude-issue: + if: >- + github.event_name == 'issues' && github.event.action == 'labeled' && + github.event.label.name == 'claude' + concurrency: + group: claude-issue-${{ github.event.issue.number }} + cancel-in-progress: true + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + id-token: write + pull-requests: write + issues: write + actions: read + checks: read + steps: + - 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 pr comment:*),Bash(gh issue comment:*),Bash(gh run view:*),Bash(gh run watch:*),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. If the PR has merge conflicts, leave a comment noting the conflicts and tag code owners for manual resolution. + 6. 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/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 946e9614..845c3295 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,3 +1,26 @@ +# ───────────────────────────────────────────────────────────────────────────── +# 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 +# +# 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, `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. +# ───────────────────────────────────────────────────────────────────────────── +# +# 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 name: Dependabot auto-merge on: @@ -8,60 +31,9 @@ on: permissions: {} jobs: - dependabot: - runs-on: ubuntu-latest + dependabot-automerge: 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 }}" - DEPENDENCY_TYPE="${{ steps.metadata.outputs.dependency-type }}" - ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}" - - # Auto-merge direct dependencies: minor and patch only - # Auto-merge indirect dependencies: patch only (minor/major require review) - if [[ "$DEPENDENCY_TYPE" == "indirect" ]]; then - if [[ "$UPDATE_TYPE" != "version-update:semver-patch" ]]; then - echo "eligible=false" >> "$GITHUB_OUTPUT" - echo "Skipping: indirect dependency updates only auto-merge for patch" - exit 0 - fi - else - if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \ - "$UPDATE_TYPE" != "version-update:semver-minor" ]]; then - echo "eligible=false" >> "$GITHUB_OUTPUT" - echo "Skipping: only patch/minor updates are auto-merge eligible" - exit 0 - fi - fi - - echo "eligible=true" >> "$GITHUB_OUTPUT" - echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE dependency=$DEPENDENCY_TYPE" - - - 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 }} + uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@v1 + secrets: inherit diff --git a/.github/workflows/dependabot-rebase.yml b/.github/workflows/dependabot-rebase.yml index 46c756f1..b58dbb15 100644 --- a/.github/workflows/dependabot-rebase.yml +++ b/.github/workflows/dependabot-rebase.yml @@ -1,35 +1,26 @@ -# 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, `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. +# ───────────────────────────────────────────────────────────────────────────── +# +# 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,91 +35,9 @@ concurrency: permissions: {} jobs: - update-and-merge: - runs-on: ubuntu-latest + dependabot-rebase: permissions: contents: read pull-requests: read - steps: - - 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" + uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@v1 + secrets: inherit diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 506010d3..f1173774 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -1,3 +1,22 @@ +# ───────────────────────────────────────────────────────────────────────────── +# 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 +# +# 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. +# ───────────────────────────────────────────────────────────────────────────── +# +# 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: @@ -10,41 +29,5 @@ permissions: contents: read jobs: - detect: - name: Detect ecosystems - runs-on: ubuntu-latest - outputs: - npm: ${{ steps.check.outputs.npm }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Detect package ecosystems - id: check - run: | - 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 - - 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: | - 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 + dependency-audit: + uses: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml@v1