diff --git a/.github/workflows/agent-shield-reusable.yml b/.github/workflows/agent-shield-reusable.yml index 2d26ba57..2c1e1571 100644 --- a/.github/workflows/agent-shield-reusable.yml +++ b/.github/workflows/agent-shield-reusable.yml @@ -47,7 +47,7 @@ jobs: name: AgentShield runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # --- Deep security scan via AgentShield CLI --- # Uses ecc-agentshield (https://github.com/affaan-m/agentshield) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dec9252..7728e195 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Lint Markdown uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0 @@ -72,7 +72,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run ShellCheck run: | @@ -92,7 +92,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -109,7 +109,7 @@ jobs: contents: read steps: - name: Checkout (full history) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/compliance-audit-and-improvement.yml b/.github/workflows/compliance-audit-and-improvement.yml index 42e930d0..0899eaa8 100644 --- a/.github/workflows/compliance-audit-and-improvement.yml +++ b/.github/workflows/compliance-audit-and-improvement.yml @@ -4,7 +4,7 @@ # Job 3: Claude analyzes both datasets in six phases: # Phase 1-3: Load data, categorize findings, research root causes. # Phase 4: Evaluate against industry best practices & emerging capabilities. -# Phase 5: Create actionable issues per repo (dev-lead label for agent pickup). +# Phase 5: Create actionable issues per repo (claude label for agent pickup). # Phase 6: Summary report. # Standard: https://github.com/${{ github.repository_owner }}/.github/tree/main/standards name: Org Standards Compliance Audit @@ -52,11 +52,10 @@ jobs: repos_with_findings: ${{ steps.audit.outputs.repos_with_findings }} issues_added: ${{ steps.audit.outputs.issues_added }} issues_existing: ${{ steps.audit.outputs.issues_existing }} - issues_retriggered: ${{ steps.audit.outputs.issues_retriggered }} issues_removed: ${{ steps.audit.outputs.issues_removed }} steps: - name: Checkout .github repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run compliance audit id: audit @@ -84,14 +83,12 @@ jobs: if [ -f "$COUNTS_FILE" ]; then ISSUES_ADDED=$(jq '.added' "$COUNTS_FILE") ISSUES_EXISTING=$(jq '.existing' "$COUNTS_FILE") - ISSUES_RETRIGGERED=$(jq '.retriggered // 0' "$COUNTS_FILE") ISSUES_REMOVED=$(jq '.removed' "$COUNTS_FILE") else - ISSUES_ADDED=0; ISSUES_EXISTING=0; ISSUES_RETRIGGERED=0; ISSUES_REMOVED=0 + ISSUES_ADDED=0; ISSUES_EXISTING=0; ISSUES_REMOVED=0 fi echo "issues_added=$ISSUES_ADDED" >> "$GITHUB_OUTPUT" echo "issues_existing=$ISSUES_EXISTING" >> "$GITHUB_OUTPUT" - echo "issues_retriggered=$ISSUES_RETRIGGERED" >> "$GITHUB_OUTPUT" echo "issues_removed=$ISSUES_REMOVED" >> "$GITHUB_OUTPUT" - name: Write step summary @@ -276,7 +273,7 @@ jobs: # ----------------------------------------------------------------------- # Job 3: Combined analysis — Claude reviews both datasets - # Creates actionable issues in the appropriate repo with the dev-lead label. + # Creates actionable issues in the appropriate repo with the claude label. # ----------------------------------------------------------------------- analyze: name: Analyze & Create Issues (Claude) @@ -290,7 +287,7 @@ jobs: id-token: write steps: - name: Checkout .github repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download compliance audit report if: needs.audit.result == 'success' @@ -310,7 +307,7 @@ jobs: env: GH_TOKEN: ${{ secrets.ORG_SCORECARD_TOKEN }} DRY_RUN: ${{ inputs.dry_run || 'false' }} - uses: anthropics/claude-code-action@787c5a0ce96a9a6cfb050ea0c8f4c05f2447c251 # v1 + uses: anthropics/claude-code-action@51ea8ea73a139f2a74ff649e3092c25a904aed7e # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} prompt: | @@ -335,7 +332,6 @@ jobs: - Repos with findings: ${{ needs.audit.outputs.repos_with_findings }} - Issues added (new): ${{ needs.audit.outputs.issues_added }} - Issues existing (updated): ${{ needs.audit.outputs.issues_existing }} - - Issues re-triggered (dev-lead re-engaged on persistent findings): ${{ needs.audit.outputs.issues_retriggered }} - Issues removed (resolved): ${{ needs.audit.outputs.issues_removed }} ### Health Survey (runtime telemetry) @@ -449,7 +445,7 @@ jobs: exclusively **proposed new or improved standards** for the org. For each top opportunity (max 2-3), create a standards proposal issue in `${{ github.repository_owner }}/.github`: - Title: `Standards: ` - - Labels: `dev-lead,enhancement` + - Labels: `claude,enhancement` - Body must include: - **Proposed Standard** — the specific policy, workflow, or configuration to adopt - **Rationale** — why this matters, linked to the feasibility/impact/urgency assessment @@ -465,10 +461,10 @@ jobs: - Issues affecting multiple repos or org-wide concerns go in `${{ github.repository_owner }}/.github` - Standards improvement proposals go in `${{ github.repository_owner }}/.github` - **IMPORTANT: Every issue MUST have the `dev-lead` label** so it gets picked up for implementation. - Ensure the `dev-lead` label exists in the target repo before creating the issue: + **IMPORTANT: Every issue MUST have the `claude` label** so it gets picked up for implementation. + Ensure the `claude` label exists in the target repo before creating the issue: ```bash - gh label create dev-lead --repo ${{ github.repository_owner }}/ --color "8B5CF6" --description "For dev-lead agent pickup" 2>/dev/null || true + gh label create claude --repo ${{ github.repository_owner }}/ --color "8B5CF6" --description "For Claude agent pickup" 2>/dev/null || true ``` Additional labels by type: `bug`, `security`, `ci`, `automation`, `enhancement`, `documentation` @@ -481,7 +477,7 @@ jobs: ```bash gh issue create --repo ${{ github.repository_owner }}/ \ --title ": " \ - --label "dev-lead," \ + --label "claude," \ --body "" ``` @@ -517,9 +513,9 @@ jobs: per-repo issues; your job is to identify systemic patterns and create higher-level issues for them. - If a similar issue exists, add a comment with latest findings instead - - When commenting on existing issues, also ensure the `dev-lead` label is present: + - When commenting on existing issues, also ensure the `claude` label is present: ```bash - gh issue edit --repo ${{ github.repository_owner }}/ --add-label dev-lead + gh issue edit --repo ${{ github.repository_owner }}/ --add-label claude ``` **Before writing the Phase 6 summary**, gather linked PR data for all issues you @@ -560,7 +556,6 @@ jobs: |--------|-------| | Added (new) | ${{ needs.audit.outputs.issues_added }} | | Existing (updated) | ${{ needs.audit.outputs.issues_existing }} | - | Re-triggered (dev-lead re-engaged) | ${{ needs.audit.outputs.issues_retriggered }} | | Removed (resolved) | ${{ needs.audit.outputs.issues_removed }} | Group by compliance issue type — one subsection per distinct check/finding type, @@ -602,9 +597,9 @@ jobs: ## Rules - **Do not fix code or push changes.** Analysis and issue creation only. - - **Do not close or modify existing issues** beyond adding the `dev-lead` label. + - **Do not close or modify existing issues** beyond adding the `claude` label. - **Do not create PRs.** Only create issues with actionable recommendations. - - **Every issue gets the `dev-lead` label.** No exceptions. + - **Every issue gets the `claude` label.** No exceptions. - **Repo-specific issues go in that repo.** Org-wide issues go in `.github`. - **Be specific.** Include run IDs, URLs, and exact error messages. - **Deduplicate aggressively.** One well-written issue beats five vague ones. diff --git a/.github/workflows/compliance-retrigger.yml b/.github/workflows/compliance-retrigger.yml index 4874d2db..593afb3c 100644 --- a/.github/workflows/compliance-retrigger.yml +++ b/.github/workflows/compliance-retrigger.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout .github repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Re-trigger stale compliance issues run: bash scripts/compliance-retrigger.sh diff --git a/.github/workflows/daily-org-status.yml b/.github/workflows/daily-org-status.yml index 23a24264..840feea9 100644 --- a/.github/workflows/daily-org-status.yml +++ b/.github/workflows/daily-org-status.yml @@ -17,13 +17,25 @@ jobs: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '20' + + - name: Install Claude Code CLI + run: | + # --ignore-scripts prevents npm from auto-running lifecycle scripts (avoids SonarCloud S6476 hotspot). + # install.cjs is the postinstall step that downloads the claude native binary — required for the CLI. + npm install -g --ignore-scripts @anthropic-ai/claude-code@2.1.132 + node "$(npm root -g)/@anthropic-ai/claude-code/install.cjs" - name: Generate org status report env: GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} run: | - chmod +x scripts/org_status.sh scripts/org_report.sh + chmod +x scripts/org_status.sh bash scripts/org_status.sh > /tmp/report.md [ -s /tmp/report.md ] || { echo "Report is empty — aborting"; exit 1; } diff --git a/.github/workflows/dependabot-rebase-reusable.yml b/.github/workflows/dependabot-rebase-reusable.yml index f3ed8ee4..e808d63b 100644 --- a/.github/workflows/dependabot-rebase-reusable.yml +++ b/.github/workflows/dependabot-rebase-reusable.yml @@ -94,13 +94,8 @@ jobs: MERGED=false while IFS=' ' read -r PR_NUMBER HEAD_REF; do - # Branch may be transiently unavailable (race: PR list fetched before - # branch delete propagates). Skip gracefully instead of crashing. - if ! BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \ - --jq '.behind_by' 2>/dev/null); then - echo " Skipping PR #$PR_NUMBER — branch $HEAD_REF not found, skipping" - continue - fi + 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 — updating branch" diff --git a/.github/workflows/dependency-audit-reusable.yml b/.github/workflows/dependency-audit-reusable.yml index 46cc9511..71b6bfed 100644 --- a/.github/workflows/dependency-audit-reusable.yml +++ b/.github/workflows/dependency-audit-reusable.yml @@ -27,7 +27,7 @@ jobs: cargo: ${{ steps.check.outputs.cargo }} pip: ${{ steps.check.outputs.pip }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Detect package ecosystems id: check @@ -74,7 +74,7 @@ jobs: if: needs.detect.outputs.npm == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -99,7 +99,7 @@ jobs: if: needs.detect.outputs.pnpm == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 @@ -126,7 +126,7 @@ jobs: if: needs.detect.outputs.gomod == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5 with: @@ -153,7 +153,7 @@ jobs: if: needs.detect.outputs.cargo == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust stable toolchain run: rustup toolchain install stable --profile minimal @@ -222,7 +222,7 @@ jobs: if: needs.detect.outputs.pip == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index ca3e5226..3ec17807 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -31,7 +31,7 @@ jobs: cargo: ${{ steps.check.outputs.cargo }} pip: ${{ steps.check.outputs.pip }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - name: Detect package ecosystems id: check @@ -78,7 +78,7 @@ jobs: if: needs.detect.outputs.npm == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -103,7 +103,7 @@ jobs: if: needs.detect.outputs.pnpm == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v4 @@ -130,7 +130,7 @@ jobs: if: needs.detect.outputs.gomod == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v5 with: @@ -157,7 +157,7 @@ jobs: if: needs.detect.outputs.cargo == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable @@ -184,7 +184,7 @@ jobs: if: needs.detect.outputs.pip == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/dev-lead.yml b/.github/workflows/dev-lead.yml index e6e479ad..02f5ada0 100644 --- a/.github/workflows/dev-lead.yml +++ b/.github/workflows/dev-lead.yml @@ -37,9 +37,15 @@ on: permissions: {} -# Concurrency is centralised in the reusable workflow (dev-lead-reusable.yml) with -# per-issue / per-PR lanes, so issue pickups are never cancelled by PR follow-up -# traffic and the grouping can't drift per-repo. See petry-projects/.github#402. +concurrency: + # One active run per repo; ci-relay (check_run) keeps an ephemeral per-SHA slot + # so it can fire immediately without blocking or being blocked by the dispatch queue. + group: >- + ${{ + github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha) || + 'dev-lead' + }} + cancel-in-progress: false jobs: dev-lead: @@ -51,4 +57,3 @@ jobs: issues: write actions: read checks: read - statuses: read # required by dev-lead-reusable.yml since #435 diff --git a/.github/workflows/feature-ideation-reusable.yml b/.github/workflows/feature-ideation-reusable.yml index 9c9d1dde..e69c75a3 100644 --- a/.github/workflows/feature-ideation-reusable.yml +++ b/.github/workflows/feature-ideation-reusable.yml @@ -111,12 +111,12 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout calling repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: Checkout feature-ideation tooling - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: petry-projects/.github ref: ${{ inputs.tooling_ref }} @@ -162,12 +162,12 @@ jobs: id-token: write steps: - name: Checkout calling repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: Checkout feature-ideation tooling - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: petry-projects/.github ref: ${{ inputs.tooling_ref }} @@ -215,7 +215,7 @@ jobs: FOCUS_AREA: ${{ inputs.focus_area || '' }} SOURCES_FILE_PATH: ${{ inputs.sources_file }} RESEARCH_DEPTH: ${{ inputs.research_depth }} - uses: anthropics/claude-code-action@787c5a0ce96a9a6cfb050ea0c8f4c05f2447c251 # v1.0.133 + uses: anthropics/claude-code-action@51ea8ea73a139f2a74ff649e3092c25a904aed7e # v1.0.123 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # CRITICAL: pass the workflow's GITHUB_TOKEN explicitly so the diff --git a/.github/workflows/feature-ideation-tests.yml b/.github/workflows/feature-ideation-tests.yml index 557ee076..a11f0274 100644 --- a/.github/workflows/feature-ideation-tests.yml +++ b/.github/workflows/feature-ideation-tests.yml @@ -51,7 +51,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/feature-ideation.yml b/.github/workflows/feature-ideation.yml index 6d1b78c7..17ca4b6e 100644 --- a/.github/workflows/feature-ideation.yml +++ b/.github/workflows/feature-ideation.yml @@ -63,7 +63,7 @@ jobs: discussions: write actions: read id-token: write - uses: petry-projects/.github/.github/workflows/feature-ideation-reusable.yml@7bf5a75b92730dedb6db7eacf2881f4c578080ac # v1 + uses: petry-projects/.github/.github/workflows/feature-ideation-reusable.yml@c7104f49cb590c46ae219d9bd677fc68073692b7 # v1 with: project_context: | petry-projects/.github is the org-level standards and tooling repository diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e247cc03..5ce47c91 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -20,7 +20,7 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # First attempt. continue-on-error lets the retry step below recover from diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index a66b2684..ebb70571 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -1173,6 +1173,273 @@ check_centralized_check_names() { done <<< "$contexts" } +# --------------------------------------------------------------------------- +# Check: Tier 1 centralized workflows must be thin caller stubs pinned to @v1 +# +# For each workflow that the org has centralized into a reusable workflow, +# verify the downstream repo's copy is a stub that delegates via: +# uses: petry-projects/.github/.github/workflows/.yml@ +# +# This prevents drift: a repo that copies the inline pre-centralization +# version (or pins to @main, or pins to a non-canonical tag) is flagged so +# it can be re-synced from the standard. The central .github repo itself is +# exempt because it owns the reusables and may legitimately reference +# its own workflows by @main during release prep. +# +# Array format: "workflow-filename:expected-reusable-basename:version-tag" +# --------------------------------------------------------------------------- +check_centralized_workflow_stubs() { + local repo="$1" + + # The .github repo is the source of truth and is allowed to reference its + # own reusables by @main; skip the stub check for it. + [ "$repo" = ".github" ] && return + + # workflow-filename:expected-reusable-basename:version-tag + local centralized=( + "auto-rebase.yml:auto-rebase-reusable:v1" + "dependency-audit.yml:dependency-audit-reusable:v1" + "dependabot-automerge.yml:dependabot-automerge-reusable:v1" + "dependabot-rebase.yml:dependabot-rebase-reusable:v1" + "agent-shield.yml:agent-shield-reusable:v1" + "feature-ideation.yml:feature-ideation-reusable:v1" + "pr-review-mention.yml:pr-review-mention-reusable:v2" + ) + + # List the repo's workflow directory once instead of probing each file. + # If the listing fails (no workflows dir), there's nothing to check. + local workflow_list + workflow_list=$(gh_api "repos/$ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null || echo "") + [ -z "$workflow_list" ] && return + + local entry wf reusable version + for entry in "${centralized[@]}"; do + IFS=':' read -r wf reusable version <<< "$entry" + [ -z "$version" ] && { echo "::error::centralized entry '$entry' missing version tag — expected format 'wf:reusable:version'" >&2; exit 1; } + + # Skip workflows that don't exist in this repo. Required workflows are + # checked separately by check_required_workflows; conditional ones + # (dependabot-rebase, feature-ideation) are intentionally optional. + if ! echo "$workflow_list" | grep -qxF "$wf"; then + continue + fi + + local content + content=$(gh_api "repos/$ORG/$repo/contents/.github/workflows/$wf" --jq '.content' 2>/dev/null || echo "") + [ -z "$content" ] && continue + + local decoded + decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") + [ -z "$decoded" ] && continue + + # Required pattern: a non-comment line whose `uses:` value is exactly + # petry-projects/.github/.github/workflows/.yml@ + # Anchor to start-of-line + optional indent so a `# uses: ...` comment + # cannot satisfy the check. + local esc_reusable esc_version + esc_reusable=$(escape_ere "$reusable") + esc_version=$(escape_ere "$version") + local expected="petry-projects/\\.github/\\.github/workflows/${esc_reusable}\\.yml@${esc_version}" + + if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*${expected}([[:space:]]|$)"; then + continue # stub is correctly pinned to the canonical version — compliant + fi + + # Determine why it's non-compliant for a more actionable message. + local why + if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*petry-projects/\\.github/\\.github/workflows/${esc_reusable}\\.yml@"; then + why="references the reusable but is not pinned to \`@${version}\` (org standard)" + elif echo "$decoded" | grep -qF "petry-projects/.github/.github/workflows/${reusable}"; then + why="references the reusable but the \`uses:\` line does not match the canonical stub" + else + why="is an inline copy instead of a thin caller stub — re-sync from \`standards/workflows/${wf}\`" + fi + + add_finding "$repo" "ci-workflows" "non-stub-$wf" "error" \ + "Centralized workflow \`$wf\` $why. Replace with the canonical stub from \`standards/workflows/${wf}\` which delegates to \`petry-projects/.github/.github/workflows/${reusable}.yml@${version}\`." \ + "standards/ci-standards.md#centralization-tiers" + done +} + +# --------------------------------------------------------------------------- +# Check: dev-lead.yml caller stub conforms to the centralized contract +# +# Unlike the other reusables, dev-lead lives in the PRIVATE repo and is pinned +# @main, and its concurrency + permissions are owned centrally (see +# standards/ci-standards.md#dev-lead-agent). A stub drifts — and breaks — in +# three ways this check catches (all root causes of petry-projects/.github#402): +# +# 1. Wrong pin: not petry-projects/.github-private/.../dev-lead-reusable.yml@dev-lead/stable. +# 2. Local concurrency block: per-stub concurrency drifts and cancels issue +# pickups; concurrency is owned by the reusable (per-issue/per-PR lanes). +# 3. Missing `statuses: read`: the reusable requests it since #435, so without +# it every run fails at startup (startup_failure) with no runtime error. +# --------------------------------------------------------------------------- +check_dev_lead_stub() { + local repo="$1" + + # .github holds the template (exercised by the reusable's own CI) and + # .github-private runs the workflow inline rather than as a caller stub. + [ "$repo" = ".github" ] && return + [ "$repo" = ".github-private" ] && return + + local content decoded + content=$(gh_api "repos/$ORG/$repo/contents/.github/workflows/dev-lead.yml" --jq '.content' 2>/dev/null || echo "") + [ -z "$content" ] && return # repo hasn't adopted dev-lead — nothing to check + decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") + [ -z "$decoded" ] && return + + # 1) Canonical pin (non-comment `uses:` line, exact ref) — the moving + # dev-lead/stable channel tag (self-host channel model). + if ! printf '%s\n' "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*petry-projects/\\.github-private/\\.github/workflows/dev-lead-reusable\\.yml@dev-lead/stable([[:space:]]|$)"; then + add_finding "$repo" "ci-workflows" "dev-lead-stub-pin" "error" \ + "The \`dev-lead.yml\` caller stub must pin \`petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable\`. Re-sync from \`standards/workflows/dev-lead.yml\`." \ + "standards/ci-standards.md#dev-lead-agent" + fi + + # 2) agent_ref must be threaded through to pin the same channel inside the + # reusable's own script/prompt checkout (prevents split-brain on promotion). + if ! printf '%s\n' "$decoded" | grep -qE "^[[:space:]]*agent_ref:[[:space:]]*dev-lead/stable([[:space:]]|$)"; then + add_finding "$repo" "ci-workflows" "dev-lead-stub-agent-ref" "error" \ + "The \`dev-lead.yml\` caller stub must pass \`with: agent_ref: dev-lead/stable\` so the reusable checks out its own scripts/prompts from the same channel. Re-sync from \`standards/workflows/dev-lead.yml\`." \ + "standards/ci-standards.md#dev-lead-agent" + fi + + # 3) No per-stub concurrency block — concurrency is owned by the reusable. + if echo "$decoded" | grep -qE "^concurrency:"; then + add_finding "$repo" "ci-workflows" "dev-lead-stub-concurrency" "warning" \ + "The \`dev-lead.yml\` stub defines its own \`concurrency:\` block. Concurrency is centralized in the reusable (per-issue/per-PR lanes); a per-stub block drifts and can cancel issue pickups. Remove it — see petry-projects/.github#402." \ + "standards/ci-standards.md#dev-lead-agent" + fi + + # 4) Caller permissions must grant `statuses: read`. + if ! echo "$decoded" | grep -qE "^[[:space:]]*statuses:[[:space:]]*read([[:space:]]|$)"; then + add_finding "$repo" "ci-workflows" "dev-lead-stub-statuses-perm" "error" \ + "The \`dev-lead.yml\` stub is missing \`statuses: read\` in \`jobs.dev-lead.permissions\`. The reusable requests it (since #435), so without it every run fails at startup (\`startup_failure\`). Add \`statuses: read\`." \ + "standards/ci-standards.md#dev-lead-agent" + fi +} + +# --------------------------------------------------------------------------- +# Check: required-status-check rulesets reference current names +# +# After centralizing workflows into reusables (#87, #88), GitHub composes +# check names as ` / `. Repos +# that updated their workflow files but didn't update their rulesets +# are silently broken — the merge gate references a name that no +# longer exists, so it can never be satisfied. +# +# Inspects both the new ruleset system and classic branch protection. +# Flags two distinct problems: +# 1. Stale pre-centralization name (e.g. `claude`, `AgentShield`) +# → emit "stale-required-check-" +# 2. `claude-code / claude` listed as required +# → emit "required-claude-code-check-broken" because that check +# is structurally incompatible with workflow-modifying PRs +# (claude-code-action's app-token validation refuses to mint +# a token whenever the PR diff includes any workflow file) +# --------------------------------------------------------------------------- +check_centralized_check_names() { + local repo="$1" + + # The .github repo owns the reusables; its own ruleset is allowed to + # reference whatever check names it likes. + [ "$repo" = ".github" ] && return + + # Map from stale name → current canonical name. Used for the rename + # remediation message. The remediation here is "rename in the + # ruleset" because both the old and new names refer to a check that + # CAN be required (it runs on PRs and reports a definitive result). + # + # NOTE: `claude` and `claude-issue` are deliberately NOT in this map. + # The post-centralization equivalents are `claude-code / claude` and + # `claude-code / claude-issue`, but those checks are themselves + # incompatible with workflow-modifying PRs (claude-code-action's app + # token validation refuses to mint a token for any PR whose diff + # includes a workflow file, so the check fails on every workflow PR + # and the merge gate becomes a deadlock). The remediation for the + # `claude*` cases is therefore "remove from required checks", not + # "rename" — handled below as a separate finding so the message + # never recommends a name that creates a new deadlock. + local renames=( + "AgentShield:agent-shield / AgentShield" + "Detect ecosystems:dependency-audit / Detect ecosystems" + ) + + # Patterns for required checks that are structurally broken and + # should be removed (not renamed). Matched as either: + # - the bare legacy name ("claude" / "claude-issue"), or + # - any reusable-workflow check whose suffix is "/ claude" or + # "/ claude-issue", regardless of caller-job-id prefix + # (so a custom caller named e.g. "Claude Code / claude" is + # also caught, not just the canonical "claude-code / claude"). + # + # The match is computed against each context line below. + + # Collect every required-status-check context from every source. + # Sources: (1) every active ruleset, (2) classic branch protection. + local contexts="" + + # Source 1: rulesets that apply to main + local ruleset_contexts + ruleset_contexts=$(gh_api "repos/$ORG/$repo/rules/branches/main" \ + --jq '.[] | select(.type == "required_status_checks") | .parameters.required_status_checks[].context' 2>/dev/null || echo "") + contexts+="$ruleset_contexts"$'\n' + + # Source 2: classic branch protection (may not exist) + local classic_contexts + classic_contexts=$(gh_api "repos/$ORG/$repo/branches/main/protection/required_status_checks" \ + --jq '.contexts[]' 2>/dev/null || echo "") + contexts+="$classic_contexts" + + [ -z "$(echo "$contexts" | tr -d '[:space:]')" ] && return + + # Check 1: stale pre-centralization names that have a safe rename + local entry old new + for entry in "${renames[@]}"; do + IFS=':' read -r old new <<< "$entry" + if echo "$contexts" | grep -qxF "$old"; then + add_finding "$repo" "rulesets" "stale-required-check-${old// /-}" "error" \ + "Required-status-check ruleset references the stale check name \`$old\`. After workflow centralization (petry-projects/.github#87) this check is published as \`$new\`. Update the ruleset (and any classic branch protection) to use the new name." \ + "standards/ci-standards.md#centralization-tiers" + fi + done + + # Check 2: claude-* checks (legacy or post-centralization) listed as + # required. These cannot be made compliant by renaming because the + # post-centralization name is itself broken — the only safe action + # is to remove the check from required-status-checks entirely. + # + # We classify each context line by suffix so any caller-job-id prefix + # is caught (e.g. "claude-code / claude", "Claude Code / claude", + # "review-claude / claude" all match). + local context match_type + while IFS= read -r context; do + [ -z "$context" ] && continue + match_type="" + case "$context" in + "claude") match_type="claude" ;; + "claude-issue") match_type="claude-issue" ;; + *"/ claude") match_type="claude" ;; + *"/ claude-issue") match_type="claude-issue" ;; + *) continue ;; + esac + + # Stable check id per match type so findings don't churn across + # audit runs from variations in caller-job-id prefixes. + local check_id + if [ "$match_type" = "claude-issue" ]; then + check_id="required-claude-issue-check-broken" + else + check_id="required-claude-check-broken" + fi + + add_finding "$repo" "rulesets" "$check_id" "error" \ + "Required-status-check ruleset includes \`$context\`, which is incompatible with workflow-modifying PRs. claude-code-action's GitHub App refuses to mint an OAuth token for any PR whose diff includes a workflow file, so the check fails on every workflow PR and the merge gate becomes a deadlock. **Remove \`$context\` from required status checks** — do NOT rename it. The Claude review check still runs on normal PRs and surfaces feedback without being a merge gate. See \`scripts/apply-rulesets.sh\` (post petry-projects/.github#94) for the canonical required-checks list." \ + "standards/ci-standards.md#centralization-tiers" + done <<< "$contexts" +} + # --------------------------------------------------------------------------- # Check: CLAUDE.md exists and references AGENTS.md # --------------------------------------------------------------------------- @@ -1443,6 +1710,53 @@ ensure_required_labels() { done } +# Create all required labels (idempotent — uses --force to update if present) +ensure_required_labels() { + local repo="$1" + # Format: "name|color|description" (pipe-delimited to avoid colon conflicts) + local label_configs=( + "security|d93f0b|Security-related PRs and issues" + "dependencies|0075ca|Dependency update PRs" + "scorecard|d93f0b|OpenSSF Scorecard findings" + "bug|d73a4a|Bug reports" + "enhancement|a2eeef|Feature requests" + "documentation|0075ca|Documentation changes" + "in-progress|fbca04|An agent is actively working this issue" + ) + + for config in "${label_configs[@]}"; do + IFS='|' read -r name color description <<< "$config" + gh label create "$name" \ + --repo "$ORG/$repo" \ + --description "$description" \ + --color "$color" \ + --force 2>/dev/null || true + done +} + +# Create all required labels (idempotent — uses --force to update if present) +ensure_required_labels() { + local repo="$1" + # Format: "name|color|description" (pipe-delimited to avoid colon conflicts) + local label_configs=( + "security|d93f0b|Security-related PRs and issues" + "dependencies|0075ca|Dependency update PRs" + "scorecard|d93f0b|OpenSSF Scorecard findings" + "bug|d73a4a|Bug reports" + "enhancement|a2eeef|Feature requests" + "documentation|0075ca|Documentation changes" + ) + + for config in "${label_configs[@]}"; do + IFS='|' read -r name color description <<< "$config" + gh label create "$name" \ + --repo "$ORG/$repo" \ + --description "$description" \ + --color "$color" \ + --force 2>/dev/null || true + done +} + create_issue_for_finding() { local repo="$1" category="$2" check="$3" severity="$4" detail="$5" standard_ref="$6" diff --git a/scripts/compliance-retrigger.sh b/scripts/compliance-retrigger.sh index e361d172..c864b876 100644 --- a/scripts/compliance-retrigger.sh +++ b/scripts/compliance-retrigger.sh @@ -18,6 +18,14 @@ set -euo pipefail # concurrent dev-lead runs in any single repo to one, avoiding the rebase # storms and token exhaustion a fleet-wide burst would cause. # +# Throttling: at most ONE issue per repo is engaged per run (shared across +# the primary and legacy-label sweeps), and a repo already active (open PR +# or in-progress issue) is skipped. Issues are processed oldest-first, so +# the most-stuck finding in each repo is the one re-engaged; the daily +# cadence drains the rest of each repo's backlog one at a time. This keeps +# concurrent dev-lead runs in any single repo to one, avoiding the rebase +# storms and token exhaustion a fleet-wide burst would cause. +# # Why re-trigger instead of relying on the original event? # GitHub only fires issues:labeled once per label application. If dev-lead # had a transient failure at that moment (template error, git-identity bug, @@ -30,22 +38,13 @@ set -euo pipefail # STALE_DAYS — issues older than this are considered stale (default: 2) # DRY_RUN — set to "true" to log actions without executing them # AUDIT_LABEL — label used to tag compliance findings (default: compliance-audit) -# TRIGGER_LABEL — label used to trigger dev-lead (default: dev-lead) +# TRIGGER_LABEL — label used to trigger dev-lead (default: claude) ORG="${ORG:-petry-projects}" STALE_DAYS="${STALE_DAYS:-2}" DRY_RUN="${DRY_RUN:-false}" AUDIT_LABEL="${AUDIT_LABEL:-compliance-audit}" -TRIGGER_LABEL="${TRIGGER_LABEL:-dev-lead}" -# Legacy label to also sweep during the transition window before the one-time -# migration script has run. Issues that still carry only the old label are -# found here and given the new label so dev-lead picks them up. -LEGACY_TRIGGER_LABEL="${LEGACY_TRIGGER_LABEL:-claude}" - -# Shared dev-lead retrigger helpers (dl_dev_lead_active, dl_cycle_trigger_label). -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib/dev-lead-retrigger.sh -. "$SCRIPT_DIR/lib/dev-lead-retrigger.sh" +TRIGGER_LABEL="${TRIGGER_LABEL:-claude}" ISSUES_RETRIGGERED=0 ISSUES_SKIPPED=0 @@ -59,6 +58,14 @@ ISSUES_DEFERRED=0 # every path that could engage dev-lead. declare -A REPO_ENGAGED=() +# Repos that already have an in-flight dev-lead engagement THIS run — either an +# issue we re-triggered or one we found already active. At most one engagement +# per repo per run keeps concurrent dev-lead runs in a single repo to one, +# avoiding rebase storms and token exhaustion from a fleet-wide burst. Shared +# across BOTH the primary and legacy-label sweeps so the per-repo budget covers +# every path that could engage dev-lead. +declare -A REPO_ENGAGED=() + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -67,10 +74,7 @@ info() { echo "[info] $*"; } warn() { echo "[warn] $*" >&2; } error() { echo "[error] $*" >&2; } -# has_open_pr / cycle_label live in lib/dev-lead-retrigger.sh as -# dl_dev_lead_active() and dl_cycle_trigger_label(), shared with the weekly -# compliance audit so both stay in sync with dev-lead's branch-naming -# convention. Sourced via SCRIPT_DIR resolved at the top of the file. +gh_api() { gh api "$@"; } # stale_cutoff — ISO timestamp N days ago stale_cutoff() { @@ -79,6 +83,30 @@ stale_cutoff() { print((datetime.now(timezone.utc)-timedelta(days=${STALE_DAYS})).strftime('%Y-%m-%dT%H:%M:%SZ'))" } +# has_open_pr +# Returns 0 (true) if there is an open PR with head ref dev-lead/issue-* +has_open_pr() { + local repo="$1" issue="$2" + local count + count=$(gh_api "repos/$ORG/$repo/pulls?state=open" \ + --jq "[.[] | select(.head.ref | startswith(\"dev-lead/issue-${issue}\"))] | length" \ + 2>/dev/null || echo "0") + [ "${count:-0}" -gt 0 ] +} + +# cycle_label +# Removes and re-adds TRIGGER_LABEL so issues:labeled fires again. +cycle_label() { + local repo="$1" issue="$2" + if [ "$DRY_RUN" = "true" ]; then + info "[dry-run] would cycle '$TRIGGER_LABEL' on $repo#$issue" + return 0 + fi + gh api -X DELETE "repos/$ORG/$repo/issues/$issue/labels/$TRIGGER_LABEL" 2>/dev/null || true + gh api -X POST "repos/$ORG/$repo/issues/$issue/labels" \ + --field "labels[]=$TRIGGER_LABEL" >/dev/null +} + # --------------------------------------------------------------------------- # Re-trigger stale compliance issues # --------------------------------------------------------------------------- @@ -128,10 +156,11 @@ retrigger_stale_issues() { info "search/issues returned ${total} matching issues" local issues - issues=$(echo "$raw" | jq -c '.items[] | {number: .number, repo: (.repository_url | split("/") | last), created_at: .created_at, title: .title}') + issues=$(echo "$raw" | jq -rs '[.[].items[]] | .[] | {number: .number, repo: (.repository_url | split("/") | last), created_at: .created_at, title: .title}') if [ -z "$issues" ]; then - info "No open compliance-audit issues found with '$TRIGGER_LABEL' label." + info "No open compliance-audit issues found." + return 0 fi while IFS= read -r issue_json; do @@ -160,7 +189,7 @@ retrigger_stale_issues() { # Skip if dev-lead is already working this issue (open PR or in-progress). # This consumes the repo's slot: a repo already churning on a fix must not # get a second concurrent engagement. - if dl_dev_lead_active "$ORG" "$repo" "$number"; then + if has_open_pr "$repo" "$number"; then info "Skipping $repo#$number — dev-lead already active (open PR or in-progress); $repo slot taken" REPO_ENGAGED[$repo]=1 ISSUES_SKIPPED=$((ISSUES_SKIPPED + 1)) @@ -172,21 +201,10 @@ retrigger_stale_issues() { # reintroduce burst behaviour. The next daily sweep retries this repo. REPO_ENGAGED[$repo]=1 info "Re-triggering $repo#$number: $title (created $created_at)" - if dl_cycle_trigger_label "$ORG" "$repo" "$number" "$TRIGGER_LABEL" "$DRY_RUN"; then - ISSUES_RETRIGGERED=$((ISSUES_RETRIGGERED + 1)) - else - warn "Failed to re-trigger dev-lead on issue #$number in $repo — attempting to restore label" - # The label may have been deleted but the re-add failed. Restore it so the - # issue remains visible to the next sweep's search query. - if [ "$DRY_RUN" != "true" ]; then - gh api -X POST "repos/$ORG/$repo/issues/$number/labels" \ - --field "labels[]=$TRIGGER_LABEL" >/dev/null 2>&1 \ - && info "Restored $TRIGGER_LABEL on $repo#$number" \ - || warn "Could not restore $TRIGGER_LABEL on $repo#$number — issue may drop out of next sweep" - fi - ISSUES_SKIPPED=$((ISSUES_SKIPPED + 1)) - fi - # dl_cycle_trigger_label already sleeps 1s internally; no additional pause needed. + cycle_label "$repo" "$number" + ISSUES_RETRIGGERED=$((ISSUES_RETRIGGERED + 1)) + # Brief pause to avoid flooding the API + sleep 1 done <<< "$issues" # Sweep pre-migration issues that still carry only the legacy trigger label. @@ -228,7 +246,7 @@ retrigger_stale_issues() { ISSUES_DEFERRED=$((ISSUES_DEFERRED + 1)) continue fi - if dl_dev_lead_active "$ORG" "$repo" "$number"; then + if has_open_pr "$repo" "$number"; then info "Skipping legacy $repo#$number — dev-lead already active; $repo slot taken" REPO_ENGAGED[$repo]=1 ISSUES_SKIPPED=$((ISSUES_SKIPPED + 1)) diff --git a/scripts/org_status.sh b/scripts/org_status.sh index bc3c36fe..dc0d7fcd 100644 --- a/scripts/org_status.sh +++ b/scripts/org_status.sh @@ -1,12 +1,7 @@ #!/usr/bin/env bash -# Daily org status — collects GitHub org data and generates a markdown report to stdout. -# Report generation is handled by org_report.sh (programmatic, no external AI dependency). +# Daily org status data collector — runs in GitHub Actions, outputs a markdown report to stdout. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=scripts/org_report.sh -source "${SCRIPT_DIR}/org_report.sh" - TODAY=$(date -u +%Y-%m-%d) if [[ "$(uname)" == "Darwin" ]]; then SINCE=$(date -u -v-7d +%Y-%m-%d) @@ -255,7 +250,9 @@ DISCUSSIONS=$(gh api graphql -f query=' ]') || DISCUSSIONS='[]' echo "::endgroup::" >&2 -# ── Generate Report ─────────────────────────────────────────────────────────── +# ── Build Prompt ────────────────────────────────────────────────────────────── +# Limit issues per repo to keep prompt size manageable and ensure Claude has +# enough output budget to generate all sections (especially the PR tables first). ISSUE_LIMIT=25 ISSUES_BY_REPO_TRIMMED=$(echo "$ISSUES_BY_REPO" | jq --argjson limit "$ISSUE_LIMIT" ' map({ @@ -265,6 +262,188 @@ ISSUES_BY_REPO_TRIMMED=$(echo "$ISSUES_BY_REPO" | jq --argjson limit "$ISSUE_LIM issues: .issues[:$limit] })') +cat > "$DATA_DIR/prompt.txt" << PROMPT +Generate a daily GitHub org status report for petry-projects on $TODAY. + +Use ONLY the data below. Output ONLY the markdown report — no preamble, no commentary. + +CRITICAL: You MUST output ALL sections listed in REPORT FORMAT, in order. Do NOT skip or abbreviate any section. + +--- + +## DATA + +### PR Counts by Repo (pre-classified) +$(echo "$PR_BY_REPO" | jq -c '.') + +### PRs Needing Human Review (full detail, includes url field) +$(echo "$NEEDS_REVIEW_PRS" | jq -c '.') + +### Issue → Linked PR Map (key: "owner/repo#issue_number", value: [{number, url}]) +$(echo "$ISSUE_PR_MAP" | jq -c '.') + +### Merge Activity — Daily Counts (last 8 days, $SINCE to $TODAY) +$(echo "$MERGE_DAILY" | jq -c '.') + +### Merge Activity — Per-Repo Per-Day (repo, total, by_date map keyed by YYYY-MM-DD) +$(echo "$MERGE_BY_REPO_DAY" | jq -c '.') + +### Open Issues by Repo (each issue has url field; truncated:true means more exist beyond the $ISSUE_LIMIT shown) +$(echo "$ISSUES_BY_REPO_TRIMMED" | jq -c '.') + +### Open Discussions (each discussion has url field) +$(echo "$DISCUSSIONS" | jq -c '.') + +--- + +## REPORT FORMAT + +Begin the report with this exact line (replace nothing): +@org-leads + +Then produce ALL of these sections in EXACTLY this order. Output each \`##\` section header before its content — do NOT skip any header, table header row, or section. + +--- + +### \`## Org Summary — $TODAY\` + +A single compact table with one row per metric: +| Metric | Value | +|---|---| +| Total open PRs | _sum all repos_ | +| PRs needing rebase | _sum needs_rebase across all repos_ | +| Total open issues | _sum all repos_ | +| PR merges (last 8 days) | _sum .org across all dates in Merge Activity — Daily Counts_ | +| Open discussions | _count all discussions_ | + +Then immediately after the table, a mermaid pie chart of open PRs by category (use the org-wide totals): +\`\`\`mermaid +pie title Open PRs by Status + "Awaiting Review" : + ... +\`\`\` +Replace each with the actual org-wide count. Omit zero-count categories. Sort slices from largest to smallest count. -echo "Generating report..." >&2 -generate_org_report +--- + +### \`## Open PRs — Why They're Unmerged (N total)\` +(Replace N with the actual total count.) + +First, an xychart-beta bar chart of org-wide PR counts by blocker category. Omit zero-count categories. Sort x-axis from highest to lowest count: +\`\`\`mermaid +xychart-beta + title "Open PRs by Blocker Category" + x-axis [] + y-axis "Count" + bar [] +\`\`\` + +Then, a grouped bar chart for per-repo breakdown using multiple bar series (one per key category). Omit repos with 0 total PRs. Sort repos by total PRs descending. Use short repo names (e.g. "broodly"). Include only the 4 most actionable categories as separate bar series: No CI/Policy, Awaiting Review, CI Failing, Approved. Note: xychart-beta does not support stacked bars — multiple bar lines render as grouped/overlapping series: +\`\`\`mermaid +xychart-beta + title "Open PRs per Repo by Category" + x-axis [] + y-axis "PRs" + bar [] + bar [] + bar [] + bar [] +\`\`\` + +--- + +### \`## PR Merge Activity — Last 8 Days\` +A mermaid bar chart of daily org merge counts (use Merge Activity — Daily Counts): +\`\`\`mermaid +xychart-beta + title "petry-projects Merges — Last 8 Days" + x-axis [] + y-axis "Merges" + bar [] +\`\`\` + +Per-repo-per-day table using Merge Activity — Per-Repo Per-Day data (omit repos with 0 total): +| Repo | Mon-DD | … | Total | +- One column per date in chronological order (all 8 dates) +- Date headers: short format Mon-DD (e.g. Apr-26) +- Repo as link: [owner/repo](https://github.com/owner/repo) +- Last column is Total (bold the number) +- Add a **TOTAL** row summing each date column and grand total +Grand total and trend sentence (immediately after the per-repo table). Trend: Increasing if avg(last 3 days) > avg(first 3 days), Decreasing if opposite, Flat otherwise. + +--- + +### \`## Open PRs — Needs Human Review\` +Full table for PRs with needsHumanReview == true, sorted by Opened ascending (oldest first): +| Repo | PR | Opened | CI | Approvals | +|---|---|---|---|---| +- PR cell: single markdown link combining number and title, e.g. \`[#42 — Fix the thing](url)\` +- CI: PASS (SUCCESS) / FAIL (FAILURE or ERROR) / PENDING / N/A (null) +If none: _none_ + +--- + +### \`## Open PRs — Automation (Dependency Bumps)\` +Counts only per repo (dep_bumps > 0): +| Repo | # Dep PRs | +|---|---| +- Repo as link: [owner/repo](https://github.com/owner/repo) +If none: _none_ + +--- + +### \`## Open Issues (N total)\` +Render as a per-repo subsection list (NOT a single flat table). For each repo with issues, in the order provided: + +\`### [owner/repo](https://github.com/owner/repo) (N issues)\` +- If truncated:true, replace the suffix with " (showing $ISSUE_LIMIT of N issues)". + +Then a table (Repo column omitted — it's in the heading): +| Issue | Opened | Labels | Linked PR | +|---|---|---|---| +- Issue cell: single markdown link combining number and title, e.g. \`[#123 — Compliance: foo](url)\` +- Opened = createdAt date only (YYYY-MM-DD) +- Linked PR: look up "owner/repo#N" in the Issue→Linked PR Map; if found render as [#M](pr_url); if multiple, comma-separate; if none render — + +--- + +### \`## Open Discussions\` +| Repo | Discussion | Opened | Replies | +|---|---|---|---| +- Discussion cell: single markdown link combining number and title, e.g. \`[#7 — Feature idea](url)\` +If none: _none_ + +--- + +OUTPUT CONTRACT +- Dates: YYYY-MM-DD +- Section headers include total counts: \`## Open Issues (47 total)\` +- Empty sections show _none_, never omit them +- Every item with a url must be rendered as a markdown hyperlink +- Output sections in EXACTLY the order listed above — do not reorder them +PROMPT + +# ── Generate Report ─────────────────────────────────────────────────────────── +# --disallowedTools: block all action tools so Claude cannot act on untrusted PR/issue content +# --output-format json: capture the full final result as JSON and extract the text with jq. +# In text mode, output preceding disallowed tool call attempts is silently dropped; json +# mode always includes the complete response in .result regardless of tool call filtering. +# Write to a temp file to avoid large bash variable assignments. +# Pipe prompt via stdin rather than a shell argument to avoid ARG_MAX (~1MB) with large orgs +REPORT_JSON="$DATA_DIR/report.json" +echo "Generating report with Claude..." >&2 +claude -p \ + --output-format json \ + --disallowedTools "Bash,Read,Write,Edit,Grep,Glob,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit" \ + < "$DATA_DIR/prompt.txt" > "$REPORT_JSON" +echo "JSON lines: $(wc -l < "$REPORT_JSON")" >&2 +echo "JSON first 600 chars:" >&2 +head -c 600 "$REPORT_JSON" >&2 +echo "" >&2 +if ! jq -e '(.result // "") | type == "string" and length > 0' "$REPORT_JSON" > /dev/null 2>&1; then + echo "ERROR: claude returned missing or empty .result field — raw output:" >&2 + cat "$REPORT_JSON" >&2 + exit 1 +fi +jq '{stop_reason,num_turns,total_cost_usd,result_len:((.result//"")|length),result_start:((.result//"")|.[0:120])}' "$REPORT_JSON" >&2 || true +jq -r '.result' "$REPORT_JSON" diff --git a/standards/workflows/dev-lead.yml b/standards/workflows/dev-lead.yml index 9c236b60..8deb01e8 100644 --- a/standards/workflows/dev-lead.yml +++ b/standards/workflows/dev-lead.yml @@ -37,9 +37,15 @@ on: permissions: {} -# Concurrency is centralised in the reusable workflow (dev-lead-reusable.yml) with -# per-issue / per-PR lanes, so issue pickups are never cancelled by PR follow-up -# traffic and the grouping can't drift per-repo. See petry-projects/.github#402. +concurrency: + # One active run per repo; ci-relay (check_run) keeps an ephemeral per-SHA slot + # so it can fire immediately without blocking or being blocked by the dispatch queue. + group: >- + ${{ + github.event_name == 'check_run' && format('dev-lead-ci-relay-{0}', github.event.check_run.head_sha) || + 'dev-lead' + }} + cancel-in-progress: false jobs: dev-lead: @@ -58,4 +64,3 @@ jobs: issues: write actions: read checks: read - statuses: read