diff --git a/.github/workflows/actions-fleet-monitor.yml b/.github/workflows/actions-fleet-monitor.yml index f7f44b83e..81f309570 100644 --- a/.github/workflows/actions-fleet-monitor.yml +++ b/.github/workflows/actions-fleet-monitor.yml @@ -80,11 +80,11 @@ jobs: with: repository: petry-projects/.github-private ref: main # Always checkout .github-private@main; workflow_call callers target @main per docs - token: ${{ secrets.GH_PAT_WORKFLOWS }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} - name: Run fleet monitor env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: bash scripts/fleet_monitor.sh - name: Summarize token usage by workflow @@ -94,7 +94,7 @@ jobs: # tracking + the fleet-tracker auto-close). Let it fail soft. continue-on-error: true env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REPO: petry-projects/.github-private LOOKBACK_DAYS: ${{ env.LOOKBACK_DAYS }} run: | @@ -226,7 +226,7 @@ jobs: - name: Migrate misaligned fleet-tracker issues uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} with: github-token: ${{ env.GH_PAT_WORKFLOWS }} script: | @@ -286,7 +286,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ORG: ${{ env.ORG }} - GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} with: # Use GH_PAT_WORKFLOWS for cross-repo issues:write on target repos github-token: ${{ env.GH_PAT_WORKFLOWS }} @@ -409,7 +409,7 @@ jobs: env: ORG: ${{ env.ORG }} STALE_DAYS: '3' - GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} with: # Use GH_PAT_WORKFLOWS for cross-repo issues:write on target repos github-token: ${{ env.GH_PAT_WORKFLOWS }} @@ -467,7 +467,7 @@ jobs: - name: Dispatch dev-lead for critical items if: env.HIGH_FAILURE_COUNT != '' && env.HIGH_FAILURE_COUNT != '0' env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: | # github.token events do not trigger issues:labeled workflows; use the # PAT to fire repository_dispatch so Dev-Lead Agent is explicitly woken. @@ -556,7 +556,7 @@ jobs: - name: Dispatch dev-lead for stub drift if: env.HAS_STUB_DRIFT == 'true' env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: | # github.token events do not trigger issues:labeled workflows; use the # PAT to fire repository_dispatch so Dev-Lead Agent is explicitly woken. @@ -585,7 +585,7 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} REMEDIATE_MODE: ${{ inputs.remediate || 'dry-run' }} - REMEDIATE_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + REMEDIATE_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REMEDIATE_PILOT_REPO: ${{ vars.FLEET_REMEDIATE_PILOT_REPO }} REMEDIATE_MAX_REPOS: ${{ vars.FLEET_REMEDIATE_MAX_REPOS || '1' }} run: | @@ -617,7 +617,7 @@ jobs: # continue-on-error keeps this informational step non-blocking. continue-on-error: true env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} ORG: ${{ env.ORG }} LOOKBACK_DAYS: ${{ env.LOOKBACK_DAYS }} run: bash scripts/token_report.sh >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/auto-rebase-retry.yml b/.github/workflows/auto-rebase-retry.yml index 1e19737f2..2d7c56137 100644 --- a/.github/workflows/auto-rebase-retry.yml +++ b/.github/workflows/auto-rebase-retry.yml @@ -46,7 +46,7 @@ jobs: # workflow_run events (GitHub's recursion guard), so only GH_PAT_WORKFLOWS # lets attempts past the first chain back into this handler toward # MAX_ATTEMPTS. With github.token alone, a single retry still fires. - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} REPO: ${{ github.repository }} CONCLUSION: ${{ github.event.workflow_run.conclusion }} RUN_ID: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/dependency-advisory.yml b/.github/workflows/dependency-advisory.yml index e7a30d507..2ace05a9d 100644 --- a/.github/workflows/dependency-advisory.yml +++ b/.github/workflows/dependency-advisory.yml @@ -45,7 +45,8 @@ jobs: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - token: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -72,5 +73,5 @@ jobs: - name: Run dependency advisory env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} run: bash scripts/aw-dependency-advisory.sh diff --git a/.github/workflows/dev-lead-retry.yml b/.github/workflows/dev-lead-retry.yml index e6439648e..34126cd45 100644 --- a/.github/workflows/dev-lead-retry.yml +++ b/.github/workflows/dev-lead-retry.yml @@ -35,7 +35,7 @@ concurrency: cancel-in-progress: false env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} TARGET_ORG: ${{ vars.TARGET_ORG || 'petry-projects' }} DRY_RUN: ${{ inputs.dry_run || 'false' }} DISPATCH_DELAY_SEC: ${{ inputs.dispatch_delay_sec || '30' }} @@ -48,7 +48,8 @@ jobs: - name: Checkout agent repo uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - token: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Scan and retry rate-limited PRs run: bash scripts/dev-lead-retry.sh diff --git a/.github/workflows/dev-lead-reusable.yml b/.github/workflows/dev-lead-reusable.yml index db57e330a..5cde5faba 100644 --- a/.github/workflows/dev-lead-reusable.yml +++ b/.github/workflows/dev-lead-reusable.yml @@ -20,14 +20,27 @@ on: secrets: CLAUDE_CODE_OAUTH_TOKEN: required: true + GH_PAT_DON_PETRY: + # don-petry's write/workflows PAT (the account dev-lead commits/pushes as). + # Canonical name under the GH_PAT_ schema; supersedes GH_PAT_WORKFLOWS, + # which is kept below as a transition fallback. + required: false GH_PAT_WORKFLOWS: + # DEPRECATED alias of GH_PAT_DON_PETRY — kept during the rename migration so + # callers that haven't set the new secret yet still authenticate. Remove once + # GH_PAT_DON_PETRY is confirmed everywhere. # Required for cross-repo callers: the default github.token cannot read # petry-projects/.github-private from another repo. Callers must pass a # PAT that has read access to petry-projects/.github-private. required: false GOOGLE_API_KEY: required: false + GH_PAT_DON_PETRY_COPILOT: + # don-petry's Copilot-subscription PAT (Copilot review engine). Canonical + # name; supersedes GH_PAT, kept below as a transition fallback. + required: false GH_PAT: + # DEPRECATED alias of GH_PAT_DON_PETRY_COPILOT — kept during migration. required: false permissions: {} @@ -98,14 +111,21 @@ jobs: checks: read statuses: read env: - BOT_USER: ${{ vars.BOT_USER || 'donpetry-bot' }} + # dev-lead's acting identity is DECLARED in personas/dev-lead/persona.yml + # (runtime.identity.account) and resolved into BOT_USER by the "Resolve + # persona identity" step below. This job-level value is the hardcoded + # fail-safe default (don-petry) used only if that step cannot read the + # manifest. It does NOT consult vars.BOT_USER — that shared variable is + # what caused the donpetry-bot regression (#1316); the manifest is now the + # sole source of truth. See scripts/lib/resolve-persona-identity.sh. + BOT_USER: 'don-petry' TRUSTED_BOTS: ${{ vars.TRUSTED_BOTS || 'copilot-pull-request-reviewer[bot],gemini-code-assist[bot],sonarqubecloud[bot],coderabbitai[bot],chatgpt-codex-connector[bot]' }} TRIGGER_PHRASES: ${{ vars.TRIGGER_PHRASES || '@dev-lead' }} DEV_LEAD_ENGINE: ${{ vars.DEV_LEAD_ENGINE || 'claude' }} DEV_LEAD_DRY_RUN: ${{ vars.DEV_LEAD_DRY_RUN || 'false' }} DEEP_TIMEOUT_SEC: ${{ vars.DEEP_TIMEOUT_SEC || '' }} ACTION_TIMEOUT_SEC: ${{ vars.ACTION_TIMEOUT_SEC || '' }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} GEMINI_CLI_TRUST_WORKSPACE: "true" DEV_LEAD_SCRIPTS: .dev-lead/scripts DEV_LEAD_PROMPTS: .dev-lead/prompts/dev-lead @@ -116,7 +136,7 @@ jobs: - name: Checkout caller repo uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - token: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} - name: Checkout dev-lead scripts uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -128,10 +148,11 @@ jobs: path: .dev-lead # Cross-repo callers: github.token cannot read a private org repo — ensure # GH_PAT_WORKFLOWS is set as an org or repo secret in the calling repo. - token: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} sparse-checkout: | scripts prompts/dev-lead + personas/dev-lead - name: Protect dev-lead scripts from accidental git staging run: echo ".dev-lead/" >> .gitignore @@ -141,6 +162,20 @@ jobs: git checkout .gitignore || true echo ".dev-lead/" >> .git/info/exclude + - name: Resolve persona identity + # Source of truth for who dev-lead acts as: personas/dev-lead/persona.yml + # (runtime.identity.account). Exporting BOT_USER here overrides the job-env + # fallback for every later step — intent classification, git-identity, and + # the commit/push. On resolve failure we keep the job default (don-petry) + # and warn, rather than silently reverting to the shared machine user. + run: | + if account="$(bash .dev-lead/scripts/lib/resolve-persona-identity.sh dev-lead .dev-lead/personas account)"; then + echo "BOT_USER=$account" >> "$GITHUB_ENV" + echo "resolved dev-lead identity: BOT_USER=$account" + else + echo "::warning::could not resolve dev-lead identity from manifest; using fallback BOT_USER=$BOT_USER" + fi + - name: Classify event intent id: intent env: @@ -148,7 +183,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} run: bash .dev-lead/scripts/dev-lead-intent.sh - name: Log intent @@ -185,10 +220,10 @@ jobs: if: env.INTENT_TYPE != 'skip' && env.INTENT_TYPE != 'enable-auto-merge' env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GH_PAT: ${{ secrets.GH_PAT }} + GH_PAT: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} run: bash .dev-lead/scripts/dev-lead-preflight.sh # ── Install engine CLIs ─────────────────────────────────────────────── @@ -244,7 +279,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-ci.sh || rc=$? @@ -263,7 +298,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? @@ -282,7 +317,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? @@ -301,7 +336,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? @@ -320,7 +355,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? @@ -337,7 +372,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-issue.sh || rc=$? @@ -354,7 +389,7 @@ jobs: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} PROMPTS_DIR: .dev-lead/prompts/dev-lead run: | bash .dev-lead/scripts/dev-lead-fix-reviews.sh || rc=$? @@ -400,7 +435,7 @@ jobs: - name: Check for associated non-fork PR id: check-pr env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} CHECK_RUN_PRS: ${{ toJson(github.event.check_run.pull_requests) }} CHECK_RUN_HEAD_SHA: ${{ github.event.check_run.head_sha }} REPO_FULL_NAME: ${{ github.repository }} @@ -446,7 +481,7 @@ jobs: - name: Emit repository_dispatch dev-lead-ci-failure if: steps.check-pr.outputs.should_relay == 'true' env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} RELAY_PR_NUMBER: ${{ steps.check-pr.outputs.pr_number }} RELAY_HEAD_SHA: ${{ github.event.check_run.head_sha }} RELAY_REPO: ${{ github.repository }} diff --git a/.github/workflows/feature-ideation.yml b/.github/workflows/feature-ideation.yml index 9263fd1f1..b68d48af4 100644 --- a/.github/workflows/feature-ideation.yml +++ b/.github/workflows/feature-ideation.yml @@ -125,7 +125,7 @@ jobs: # NEVER starts a run (loop prevention) — the enhancement would silently # no-op. Same constraint as scripts/initiative-planner/redispatch.sh. env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: | if [ -z "${GH_TOKEN}" ]; then echo "::error::GH_PAT_WORKFLOWS is required — a workflow_dispatch fired with GITHUB_TOKEN will not start a run." @@ -134,7 +134,7 @@ jobs: - name: Re-dispatch under workflow_dispatch env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} # Discussion numbers are always positive integers, so this is safe to # forward directly to the dispatch input. diff --git a/.github/workflows/gh-aw-cross-org.yml b/.github/workflows/gh-aw-cross-org.yml index e01799733..a052db53a 100644 --- a/.github/workflows/gh-aw-cross-org.yml +++ b/.github/workflows/gh-aw-cross-org.yml @@ -34,7 +34,7 @@ env: BOT_USER: ${{ vars.BOT_USER || 'donpetry-bot' }} DEV_LEAD_ENGINE: ${{ vars.DEV_LEAD_ENGINE || 'claude' }} DEV_LEAD_DRY_RUN: ${{ vars.DEV_LEAD_DRY_RUN || 'false' }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} jobs: dispatch: @@ -45,7 +45,7 @@ jobs: GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_CLI_TRUST_WORKSPACE: "true" - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} steps: - name: Validate source repo @@ -62,7 +62,7 @@ jobs: - name: Checkout .github-private (scripts and prompts) uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - token: ${{ secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || secrets.GITHUB_TOKEN }} path: dotgithub-private # Checkout the target (source) repo so the engine can read and modify its code. @@ -71,7 +71,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: ${{ github.event.client_payload.source_repo }} - token: ${{ secrets.GH_PAT_WORKFLOWS }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} path: target-repo - name: Configure git identity for target repo @@ -89,7 +89,7 @@ jobs: env: CLAUDE_CODE_VERSION: ${{ vars.CLAUDE_CODE_VERSION || 'latest' }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT || secrets.GITHUB_TOKEN }} run: | set -euo pipefail mkdir -p "$HOME/.npm-global" @@ -127,7 +127,7 @@ jobs: REPO: ${{ github.event.client_payload.source_repo }} PROMPTS_DIR: ${{ github.workspace }}/dotgithub-private/prompts/dev-lead REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} working-directory: target-repo run: bash ${{ github.workspace }}/dotgithub-private/scripts/dev-lead-fix-issue.sh @@ -144,6 +144,6 @@ jobs: REPO: ${{ github.event.client_payload.source_repo }} PROMPTS_DIR: ${{ github.workspace }}/dotgithub-private/prompts/dev-lead REVIEW_ENGINE: ${{ env.DEV_LEAD_ENGINE }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} working-directory: target-repo run: bash ${{ github.workspace }}/dotgithub-private/scripts/dev-lead-fix-ci.sh diff --git a/.github/workflows/idea-triage.yml b/.github/workflows/idea-triage.yml index cc62853c6..ca3fe7b26 100644 --- a/.github/workflows/idea-triage.yml +++ b/.github/workflows/idea-triage.yml @@ -65,7 +65,7 @@ jobs: # the self path only; cross-repo runs use GH_PAT_WORKFLOWS (same pattern as # initiative-planner.yml). S1 (#818) will replace this with a minted GitHub # App token for tighter scoping. - TARGET_GH_TOKEN: ${{ (github.event.inputs.target_repo == '' || github.event.inputs.target_repo == github.repository) && secrets.GITHUB_TOKEN || secrets.GH_PAT_WORKFLOWS }} + TARGET_GH_TOKEN: ${{ (github.event.inputs.target_repo == '' || github.event.inputs.target_repo == github.repository) && secrets.GITHUB_TOKEN || secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} DRY_RUN: ${{ inputs.dry_run && '1' || '0' }} steps: - name: Checkout agent repo diff --git a/.github/workflows/initiative-driver-canary.yml b/.github/workflows/initiative-driver-canary.yml index 60f07f914..bf04b57a2 100644 --- a/.github/workflows/initiative-driver-canary.yml +++ b/.github/workflows/initiative-driver-canary.yml @@ -81,7 +81,7 @@ jobs: if: steps.guard.outputs.configured == 'true' env: # PAT required — a workflow_dispatch fired with GITHUB_TOKEN never starts a run. - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} CANARY_OUT: initiative_driver_canary.md run: bash scripts/initiative_driver_canary.sh diff --git a/.github/workflows/initiative-driver.yml b/.github/workflows/initiative-driver.yml index b9d85f06a..0b1641263 100644 --- a/.github/workflows/initiative-driver.yml +++ b/.github/workflows/initiative-driver.yml @@ -88,7 +88,7 @@ jobs: - name: Guard — PAT present # PAT required: a label applied with GITHUB_TOKEN does NOT trigger dev-lead.yml. env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: | if [ -z "${GH_TOKEN}" ]; then echo "::error::GH_PAT_WORKFLOWS is required — a label applied with GITHUB_TOKEN will not trigger dev-lead." @@ -97,5 +97,5 @@ jobs: - name: Drive initiative env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: bash scripts/initiative-driver.sh diff --git a/.github/workflows/initiative-planner-canary.yml b/.github/workflows/initiative-planner-canary.yml index c80e0049d..eb1191a01 100644 --- a/.github/workflows/initiative-planner-canary.yml +++ b/.github/workflows/initiative-planner-canary.yml @@ -67,7 +67,7 @@ jobs: if: steps.guard.outputs.configured == 'true' env: # PAT required — a workflow_dispatch fired with GITHUB_TOKEN never starts a run. - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} CANARY_OUT: initiative_canary.md run: bash scripts/initiative_canary.sh diff --git a/.github/workflows/initiative-planner.yml b/.github/workflows/initiative-planner.yml index 918fcba66..36fb0a1e9 100644 --- a/.github/workflows/initiative-planner.yml +++ b/.github/workflows/initiative-planner.yml @@ -99,7 +99,7 @@ jobs: # PAT required: a workflow_dispatch fired with GITHUB_TOKEN is accepted but # NEVER starts a run (loop prevention) — the planner would silently no-op. env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: | if [ -z "${GH_TOKEN}" ]; then echo "::error::GH_PAT_WORKFLOWS is required — a workflow_dispatch fired with GITHUB_TOKEN will not start a run." @@ -108,7 +108,7 @@ jobs: - name: Re-dispatch under workflow_dispatch env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} # Dogfood path: this repo plans its own discussions, so the target is # self. Fleet callers (#820) live in their own repos and set their host. @@ -144,7 +144,7 @@ jobs: # S1 (#818) will replace this with a minted GitHub App token for tighter scoping. # The workflow_dispatch re-invoke keeps GH_PAT_WORKFLOWS (redispatch job) — that # role is loop-prevention, not content writes, and is unaffected. - TARGET_GH_TOKEN: ${{ (github.event.inputs.target_repo == '' || github.event.inputs.target_repo == github.repository) && secrets.GITHUB_TOKEN || secrets.GH_PAT_WORKFLOWS }} + TARGET_GH_TOKEN: ${{ (github.event.inputs.target_repo == '' || github.event.inputs.target_repo == github.repository) && secrets.GITHUB_TOKEN || secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} DISCUSSION_NUMBER: ${{ github.event.inputs.discussion }} DRY_RUN: ${{ inputs.dry_run == true && '1' || '0' }} FORCE_REPLAN: ${{ inputs.force_replan == true && '1' || '0' }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9c1a5011a..fc8814a92 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -334,9 +334,28 @@ jobs: - name: Verify live persona addressing teams env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: bash scripts/verify-persona-teams.sh personas + verify-persona-identity: + # Drift check (.github-private#1316) that ties each persona's declared + # runtime.identity to the workflows that consume it: the credential follows the + # GH_PAT_ naming convention, the secret is actually referenced by a + # workflow, and every persona shipping a write workflow resolves BOT_USER from + # the manifest (not the shared vars.BOT_USER that let dev-lead regress to + # committing as donpetry-bot). No GitHub API calls / no gh — pure + # manifest↔workflow consistency (PyYAML installed via pip below). + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Install PyYAML + run: pip3 install --quiet pyyaml + + - name: Verify persona identity ↔ workflow wiring + run: bash scripts/verify-persona-identity.sh personas + gh-aw-compile: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/persona-runner-reusable.yml b/.github/workflows/persona-runner-reusable.yml index f948dfdeb..adb41e6c5 100644 --- a/.github/workflows/persona-runner-reusable.yml +++ b/.github/workflows/persona-runner-reusable.yml @@ -53,7 +53,12 @@ on: secrets: CLAUDE_CODE_OAUTH_TOKEN: required: true + GH_PAT_DON_PETRY: + # don-petry's PAT — the account the persona comment-poster authenticates as. + # Canonical name (GH_PAT_ schema); supersedes GH_PAT_WORKFLOWS below. + required: false GH_PAT_WORKFLOWS: + # DEPRECATED alias of GH_PAT_DON_PETRY — kept during the rename migration. required: true permissions: {} @@ -179,7 +184,7 @@ jobs: if: steps.resolve.outputs.skip == 'false' && steps.advise.outputs.skip_post == 'false' env: # The PAT lives ONLY here. The agent step above never had it. - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} PERSONA: ${{ inputs.persona }} SOURCE_REPO: ${{ inputs.source_repo }} ITEM_NUMBER: ${{ inputs.item_number }} diff --git a/.github/workflows/pr-review-canary.yml b/.github/workflows/pr-review-canary.yml index 04f34fd62..5d2539cd0 100644 --- a/.github/workflows/pr-review-canary.yml +++ b/.github/workflows/pr-review-canary.yml @@ -59,7 +59,7 @@ jobs: - name: Run pr-review-trigger canary (dry-run) env: # PAT required — a workflow_dispatch fired with GITHUB_TOKEN never starts a run. - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} CANARY_OUT: pr_review_canary.md run: bash scripts/pr_review_canary.sh diff --git a/.github/workflows/pr-review-mention.yml b/.github/workflows/pr-review-mention.yml index dfab4345a..d07129aa3 100644 --- a/.github/workflows/pr-review-mention.yml +++ b/.github/workflows/pr-review-mention.yml @@ -39,5 +39,5 @@ jobs: pull-requests: write uses: petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml@pr-review-mention/v2-next secrets: - GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} DON_PETRY_BOT_GH_PAT: ${{ secrets.DON_PETRY_BOT_GH_PAT }} diff --git a/.github/workflows/pr-review-sweep.yml b/.github/workflows/pr-review-sweep.yml index 81b74b369..10d5d45bf 100644 --- a/.github/workflows/pr-review-sweep.yml +++ b/.github/workflows/pr-review-sweep.yml @@ -84,7 +84,7 @@ jobs: # the sweep "succeeds" while re-triggering nothing (the original #573 gap # regression). Fail loudly instead of silently no-op'ing. env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} run: | if [ -z "${GH_TOKEN}" ]; then echo "::error::GH_PAT_WORKFLOWS is required — GITHUB_TOKEN cannot start a dispatched run and the bot comment PAT lacks actions:write." @@ -96,7 +96,7 @@ jobs: # PAT with workflow/actions:write scope required: GITHUB_TOKEN cannot # start dispatched workflow runs, and the bot comment PAT lacks the # actions:write scope (HTTP 403 on the dispatch). - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} DRY_RUN: ${{ inputs.dry_run || 'false' }} MAX_DISPATCH: ${{ inputs.max_dispatch || '20' }} run: bash scripts/sweep-stuck-reviews.sh diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index a1b6f0158..be9aee571 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -46,7 +46,13 @@ on: # over DON_PETRY_BOT_GH_PAT when set; see docs/pr-review-agent/machine-user-setup.md. DON_PETRY_BOT_GH_PAT_CLASSIC: required: false + GH_PAT_DON_PETRY_COPILOT: + # don-petry's Copilot-subscription PAT (Copilot review engine). Canonical + # name under the GH_PAT_ schema; supersedes GH_PAT (below, kept as + # a transition fallback). The bot's own PATs keep their grandfathered names. + required: false GH_PAT: + # DEPRECATED alias of GH_PAT_DON_PETRY_COPILOT — kept during migration. required: false # ── Reusable-only (#497 self-host pinning) ─────────────────────────────── @@ -138,7 +144,7 @@ jobs: REVIEW_ENGINE: ${{ vars.REVIEW_ENGINE || 'claude' }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Copilot engine reuses GH_PAT (a separate user PAT with Copilot subscription). - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} # Gemini engine auth — GOOGLE_API_KEY is the secret name; newer Gemini CLI # versions expect GEMINI_API_KEY so we expose the same value under both names. GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} @@ -211,6 +217,20 @@ jobs: repository: petry-projects/.github-private ref: ${{ inputs.agent_ref || 'main' }} + - name: Resolve persona identity + # pr-review's acting account is DECLARED in personas/pr-review/persona.yml + # (runtime.identity.account = donpetry-bot, the review-only machine user). + # Resolving BOT_USER from the manifest — instead of the shared vars.BOT_USER + # — is what keeps this identity from colliding with dev-lead's (#1316). The + # job-env BOT_USER remains the fail-safe default if the manifest is unreadable. + run: | + if account="$(bash scripts/lib/resolve-persona-identity.sh pr-review personas account)"; then + echo "BOT_USER=$account" >> "$GITHUB_ENV" + echo "resolved pr-review identity: BOT_USER=$account" + else + echo "::warning::could not resolve pr-review identity from manifest; using fallback BOT_USER=$BOT_USER" + fi + - name: Cache claude-code CLI uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: diff --git a/.github/workflows/readme-refresh.yml b/.github/workflows/readme-refresh.yml index 17775c837..1595d3ba4 100644 --- a/.github/workflows/readme-refresh.yml +++ b/.github/workflows/readme-refresh.yml @@ -65,6 +65,6 @@ jobs: - name: Run README refresh env: - GH_TOKEN: ${{ secrets.GH_PAT }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} BOT_USER: don-petry # commit/push as don-petry (GH_PAT owner), not donpetry-bot run: bash scripts/aw-readme-refresh.sh diff --git a/.github/workflows/reviewer-report.yml b/.github/workflows/reviewer-report.yml index 4df8b48ac..687a7a869 100644 --- a/.github/workflows/reviewer-report.yml +++ b/.github/workflows/reviewer-report.yml @@ -59,11 +59,11 @@ jobs: with: repository: petry-projects/.github-private ref: main - token: ${{ secrets.GH_PAT_WORKFLOWS }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} - name: Generate org-wide reviewer scorecard env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} REVIEWER_REPORT_OUT: reviewer_report.md REVIEWER_SNAPSHOT_OUT: reviewer-report-state.json run: | diff --git a/.github/workflows/token-report.yml b/.github/workflows/token-report.yml index b2c6b4ee4..dc15c3ed1 100644 --- a/.github/workflows/token-report.yml +++ b/.github/workflows/token-report.yml @@ -56,11 +56,11 @@ jobs: with: repository: petry-projects/.github-private ref: main - token: ${{ secrets.GH_PAT_WORKFLOWS }} + token: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} - name: Generate org-wide token report env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} TOKEN_REPORT_OUT: token_report.md run: | bash scripts/token_report.sh >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index 43987a67c..b6bb85e56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -302,6 +302,27 @@ run to `remediate=live` for the configured pilot. - Scripts must be POSIX-compatible shell (`#!/usr/bin/env bash` with `set -euo pipefail`). - No hardcoded tokens or secrets — use `$GITHUB_TOKEN` from the environment. +### Agent identity & credential secrets + +Every persona/agent declares the GitHub account it **acts as** — the login it +commits, pushes, reviews, or comments as — in its manifest under +`runtime.identity` (`personas//persona.yml`): an `account` and the `credential` +secret holding that account's PAT. The runtime resolves `BOT_USER` from +`account` via `scripts/lib/resolve-persona-identity.sh`; **never** derive a +write persona's identity from a shared `vars.BOT_USER` — that coupling let +dev-lead regress to committing as the review-only machine user `donpetry-bot` +([#1316](https://github.com/petry-projects/.github-private/issues/1316)). + +- **dev-lead** acts as `don-petry` (the owner); **pr-review** acts as the + review-only machine user `donpetry-bot`. They are deliberately different accounts. +- PAT secrets follow **`GH_PAT_[_]`** (login upper-snake-cased), + e.g. `GH_PAT_DON_PETRY`, `GH_PAT_DON_PETRY_COPILOT`. Pre-existing account-named + secrets (`DON_PETRY_BOT_GH_PAT`) are grandfathered. +- `lint.yml`'s `verify-persona-identity` job enforces the manifest↔workflow + wiring; `validate-personas.py` makes `runtime.identity` mandatory. See + `standards/persona-standards.md` §5.1 and + `docs/pr-review-agent/machine-user-setup.md`. + ### Initiative Planner — blocking open-questions gate `scripts/initiative-planner/apply-plan.sh` will **not** materialize an epic + sub-issue diff --git a/docs/dev-lead/spec.md b/docs/dev-lead/spec.md index 8bb9935a3..6f4346882 100644 --- a/docs/dev-lead/spec.md +++ b/docs/dev-lead/spec.md @@ -290,9 +290,13 @@ env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }} - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} - BOT_USER: ${{ vars.BOT_USER || 'donpetry-bot' }} + COPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY_COPILOT || secrets.GH_PAT }} + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS || github.token }} + # Hardcoded fail-safe default only; the real value is resolved from + # personas/dev-lead/persona.yml (runtime.identity.account = don-petry) by the + # "Resolve persona identity" step. vars.BOT_USER is NOT consulted — that + # shared variable caused the donpetry-bot regression (#1316). See persona-standards.md §5.1. + BOT_USER: 'don-petry' TRUSTED_BOTS: ${{ vars.TRUSTED_BOTS || 'copilot-pull-request-reviewer[bot],gemini-code-assist[bot],coderabbitai[bot],sonarqubecloud[bot]' }} TRIGGER_PHRASES: ${{ vars.TRIGGER_PHRASES || '@claude,@dev-lead' }} ``` @@ -579,7 +583,7 @@ No external script. Runs entirely as a `run:` shell block within the job. | `CLAUDE_CODE_VERSION` | `latest` | Claude CLI version to install and cache | | `TRUSTED_BOTS` | See §5.2 | Comma-separated list of trusted bot logins | | `TRIGGER_PHRASES` | `@claude,@dev-lead` | Phrases that trigger human intent in comments | -| `BOT_USER` | `donpetry-bot` | Machine user login (excluded from self-triggering) | +| `BOT_USER` | `don-petry` (resolved from `personas/dev-lead/persona.yml` `runtime.identity.account`) | The account dev-lead acts as — commits/pushes and self-exclusion. Manifest-driven since #1316; the env value is only a fail-safe default. | | `MAX_CI_CYCLES` | `3` | Max fix-CI loops before giving up | | `MAX_REVIEW_CYCLES` | `3` | Max review-fix loops before giving up | | `DEV_LEAD_LIVE_MODE` | `false` | If `true`, run live on all events; if `false`, dry-run for scheduled (N/A for event-driven) | diff --git a/docs/pr-review-agent/machine-user-setup.md b/docs/pr-review-agent/machine-user-setup.md index 78e507e24..f3c4bd373 100644 --- a/docs/pr-review-agent/machine-user-setup.md +++ b/docs/pr-review-agent/machine-user-setup.md @@ -118,11 +118,26 @@ gh secret list --repo petry-projects/.github-private | grep DON_PETRY_BOT_GH_PAT ### Secret naming reference -| Secret | Purpose | -|--------|---------| -| `DON_PETRY_BOT_GH_PAT` | Machine user / bot reviewer PAT. Used by `pr-review.yml`, `repair-pr-approvals.yml`, `daily-pr-review-health.yml`, and `claude.yml` for GitHub API authentication as the bot. | -| `GH_PAT` | User PAT with a Copilot subscription. Exposed as `COPILOT_GITHUB_TOKEN` in `pr-review.yml` for Copilot CLI access. Do **not** store the bot reviewer PAT here. | -| `GOOGLE_API_KEY` | API key for Google AI (Gemini). Used by the `gemini` engine for triage, deep review, and audit tiers. | +PAT secrets follow the schema **`GH_PAT_[_]`**, where `` +is the GitHub login upper-snake-cased (`don-petry` → `GH_PAT_DON_PETRY`). This makes +the *owning account* obvious from the secret name, resolving the ambiguity that let dev-lead +regress to committing as the bot ([#1316](https://github.com/petry-projects/.github-private/issues/1316)). +Each persona declares which account/secret it acts as in `runtime.identity` +(`personas//persona.yml`); see `standards/persona-standards.md` §5.1. + +| Secret | Account | Purpose | +|--------|---------|---------| +| `GH_PAT_DON_PETRY` | don-petry | Write/workflows PAT — the account dev-lead and the persona runner commit/push/comment as. **Canonical name** for the PAT formerly stored as `GH_PAT_WORKFLOWS`. | +| `GH_PAT_DON_PETRY_COPILOT` | don-petry | User PAT with a Copilot subscription. Exposed as `COPILOT_GITHUB_TOKEN` for Copilot CLI access. **Canonical name** for the PAT formerly stored as `GH_PAT`. | +| `DON_PETRY_BOT_GH_PAT` | donpetry-bot | Machine user / bot reviewer PAT. Used by `pr-review.yml`, `repair-pr-approvals.yml`, `daily-pr-review-health.yml` for GitHub API auth as the bot. Grandfathered name (pre-dates the schema). | +| `DON_PETRY_BOT_GH_PAT_CLASSIC` | donpetry-bot | Classic PAT for the bot; required for `gh pr review --approve`. Preferred over the fine-grained secret when set. Grandfathered. | +| `GOOGLE_API_KEY` | — | API key for Google AI (Gemini). Used by the `gemini` engine for triage, deep review, and audit tiers. | + +> **Migration:** `GH_PAT_WORKFLOWS` → `GH_PAT_DON_PETRY` and `GH_PAT` → +> `GH_PAT_DON_PETRY_COPILOT`. Workflows read `${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }}` +> during the transition, so **create the new secrets as copies of the old ones**; +> once confirmed present, a follow-up drops the `|| secrets.GH_PAT_WORKFLOWS` +> fallbacks and deletes the old secrets. ## Step 5: Verify the setup diff --git a/personas/business-analyst/persona.yml b/personas/business-analyst/persona.yml index c7bee950d..90cbb5896 100644 --- a/personas/business-analyst/persona.yml +++ b/personas/business-analyst/persona.yml @@ -67,6 +67,15 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] +# --- Runtime identity (§5.1) ------------------------------------------------ +# Advisory-only (producers under prompts/incubation, consumed by path and the +# persona runner). On the comment path it posts as `don-petry` (the runner +# authenticates with that account's PAT). No dedicated reusable yet. +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY + evals: required_before: stable path: evals/business-analyst/ diff --git a/personas/dev-lead/persona.yml b/personas/dev-lead/persona.yml index e77dcffb1..ab95a1096 100644 --- a/personas/dev-lead/persona.yml +++ b/personas/dev-lead/persona.yml @@ -83,6 +83,19 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] +# --- Runtime identity (§5.1) ------------------------------------------------ +# The account dev-lead ACTS AS: it implements issues into PRs and commits/pushes +# fixes, so it must author commits as the human owner `don-petry`, NOT the +# review-only machine user `donpetry-bot`. dev-lead-reusable.yml resolves +# BOT_USER from identity.account (see scripts/lib/resolve-persona-identity.sh); +# git-identity.sh then attributes commits to it. Distinct from address.handle +# (petry-projects/dev-lead, the mention team). This decoupling is the fix for +# the donpetry-bot regression (.github-private#1316). +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY + evals: required_before: stable path: evals/dev-lead/ diff --git a/personas/devops-lead/persona.yml b/personas/devops-lead/persona.yml index 753b1e14b..09c15f879 100644 --- a/personas/devops-lead/persona.yml +++ b/personas/devops-lead/persona.yml @@ -72,9 +72,14 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] -# No `runtime:` block yet — devops-lead ships no dedicated reusable/stub in this -# pilot. It is consumed by path and is human-invocable. Add `runtime` + a -# canary-rings.json entry when a dedicated advisory workflow is wired. +# --- Runtime identity (§5.1) ------------------------------------------------ +# Advisory-only: devops-lead ships no dedicated reusable/stub in this pilot. +# It is consumed by path and is human-invocable. On the comment path it posts +# as `don-petry` (the persona runner authenticates with that account's PAT). +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY evals: required_before: stable diff --git a/personas/pr-review/persona.yml b/personas/pr-review/persona.yml index 553bb6c97..8146cf9fc 100644 --- a/personas/pr-review/persona.yml +++ b/personas/pr-review/persona.yml @@ -64,6 +64,18 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] +# --- Runtime identity (§5.1) ------------------------------------------------ +# pr-review ACTS AS the dedicated machine user `donpetry-bot`: a review-only +# account distinct from `don-petry` so its approvals satisfy CODEOWNERS and it +# never authors code. It posts NO commits (review-only) — the identity is used +# for its API auth and approvals. Credential is the bot's classic PAT (required +# for addPullRequestReview); the base secret name is grandfathered (pre-dates +# the GH_PAT_ convention). See docs/pr-review-agent/machine-user-setup.md. +runtime: + identity: + account: donpetry-bot + credential: DON_PETRY_BOT_GH_PAT + # pr-review wraps two skills (deep-review + triage); their cases are combined # under evals/pr-review/ to satisfy the schema's single-path constraint. evals: diff --git a/personas/qa-lead/persona.yml b/personas/qa-lead/persona.yml index 3051cfdff..5d3af0600 100644 --- a/personas/qa-lead/persona.yml +++ b/personas/qa-lead/persona.yml @@ -122,10 +122,16 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] -# No `runtime:` block yet — qa-lead ships no dedicated reusable/stub in this -# pilot. It is consumed by path (e.g. by the initiative-planner during -# planning) and is human-invocable. Add `runtime` + a canary-rings.json entry -# when a dedicated advisory workflow is wired. +# qa-lead ships no dedicated reusable/stub in this pilot — it is consumed by +# path (e.g. by the initiative-planner) and is human-invocable via the persona +# runner. It declares only `runtime.identity`: on the advisory/comment path it +# posts as `don-petry` (the persona runner authenticates with that account's +# PAT). Add the rest of `runtime` + a canary-rings.json entry when a dedicated +# advisory workflow is wired. +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY evals: required_before: stable diff --git a/personas/scrum-master/persona.yml b/personas/scrum-master/persona.yml index b836a5374..fd42899cc 100644 --- a/personas/scrum-master/persona.yml +++ b/personas/scrum-master/persona.yml @@ -69,6 +69,15 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] +# --- Runtime identity (§5.1) ------------------------------------------------ +# Advisory-only (consumed by the initiative-planner and the persona runner). On +# the comment path it posts as `don-petry` (the runner authenticates with that +# account's PAT). No dedicated reusable yet. +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY + evals: required_before: stable path: evals/scrum-master/ diff --git a/personas/security-lead/persona.yml b/personas/security-lead/persona.yml index 981df0822..b6666798a 100644 --- a/personas/security-lead/persona.yml +++ b/personas/security-lead/persona.yml @@ -86,9 +86,14 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] -# No `runtime:` block yet — security-lead ships no dedicated reusable/stub in -# this pilot. It is consumed by path and is human-invocable. Add `runtime` + a -# canary-rings.json entry when a dedicated advisory workflow is wired. +# --- Runtime identity (§5.1) ------------------------------------------------ +# Advisory-only: security-lead ships no dedicated reusable/stub in this pilot. +# It is consumed by path and is human-invocable. On the comment path it posts +# as `don-petry` (the persona runner authenticates with that account's PAT). +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY evals: required_before: stable diff --git a/personas/sre-lead/persona.yml b/personas/sre-lead/persona.yml index e160266ef..439cda78d 100644 --- a/personas/sre-lead/persona.yml +++ b/personas/sre-lead/persona.yml @@ -84,9 +84,14 @@ triggers: trust: author_association_floor: [OWNER, MEMBER, COLLABORATOR] -# No `runtime:` block yet — sre-lead ships no dedicated reusable/stub in this -# pilot. It is consumed by path and is human-invocable. Add `runtime` + a -# canary-rings.json entry when a dedicated advisory workflow is wired. +# --- Runtime identity (§5.1) ------------------------------------------------ +# Advisory-only: sre-lead ships no dedicated reusable/stub in this pilot. +# It is consumed by path and is human-invocable. On the comment path it posts +# as `don-petry` (the persona runner authenticates with that account's PAT). +runtime: + identity: + account: don-petry + credential: GH_PAT_DON_PETRY evals: required_before: stable diff --git a/personas/validate-personas.py b/personas/validate-personas.py index af21186a4..2f7b9ad5a 100644 --- a/personas/validate-personas.py +++ b/personas/validate-personas.py @@ -188,6 +188,39 @@ def check_evals(manifest: dict, manifest_path: Path, repo_root: Path) -> None: f"placeholder evals)") +# Credentials that pre-date the GH_PAT_ naming convention and are kept +# as-is (they are already account-named, just in the legacy suffix order). New +# personas MUST use the GH_PAT_[_] form. +GRANDFATHERED_CREDENTIALS = frozenset({ + "DON_PETRY_BOT_GH_PAT", + "DON_PETRY_BOT_GH_PAT_CLASSIC", +}) + + +def check_identity(manifest: dict, manifest_path: Path) -> None: + """Every persona MUST declare the account it acts as. The schema validates the + SHAPE of runtime.identity when present; this makes it MANDATORY and enforces + the credential naming convention — so no persona can silently fall back to a + shared machine user (the dev-lead->donpetry-bot regression, .github-private#1316).""" + runtime = manifest.get("runtime") + if not isinstance(runtime, dict) or "identity" not in runtime: + fail(f"{manifest_path}: missing runtime.identity — every persona must declare the " + f"account it acts as (account + credential). See persona-standards.md §5.1.") + identity = runtime["identity"] + if not isinstance(identity, dict) or "credential" not in identity or "account" not in identity: + fail(f"{manifest_path}: runtime.identity must be a mapping containing 'account' and " + f"'credential'. See persona-standards.md §5.1.") + credential = identity["credential"] + account = identity["account"] + if credential not in GRANDFATHERED_CREDENTIALS: + upper_account = account.upper().replace("-", "_") + prefix = f"GH_PAT_{upper_account}" + if credential != prefix and not credential.startswith(prefix + "_"): + fail(f"{manifest_path}: runtime.identity.credential '{credential}' must follow the " + f"GH_PAT_[_] convention for account '{account}' " + f"(expected '{prefix}[_]'). See persona-standards.md §5.1.") + + def check_invariants(manifest: dict, manifest_path: Path, repo_root: Path, registry_arg: str | None) -> None: persona_dir = manifest_path.parent @@ -226,6 +259,8 @@ def check_invariants(manifest: dict, manifest_path: Path, repo_root: Path, fail(f"{manifest_path}: vendor_pin '{fw['vendor_pin']}' not found in {vendor_md} " f"(manifest pin and vendored version disagree)") + check_identity(manifest, manifest_path) + check_evals(manifest, manifest_path, repo_root) if manifest["status"] != "draft": diff --git a/scripts/lib/resolve-persona-identity.sh b/scripts/lib/resolve-persona-identity.sh new file mode 100755 index 000000000..7dbabae8f --- /dev/null +++ b/scripts/lib/resolve-persona-identity.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# resolve-persona-identity.sh — print a persona's declared runtime identity. +# +# Usage: resolve-persona-identity.sh [field] +# field: account (default) | credential +# +# Reads //persona.yml and prints runtime.identity.. +# This is the SINGLE runtime source of a persona's acting GitHub identity: the +# dev-lead and pr-review workflows resolve BOT_USER from `account` here instead of +# the shared `vars.BOT_USER` that let dev-lead regress to committing as the +# review-only machine user donpetry-bot (.github-private#1316). +# +# Exits non-zero (message on stderr) if the manifest, the identity block, or the +# requested field is missing — it never emits a silent fallback identity. Callers +# decide whether to treat the non-zero exit as fatal or fall back to a safe default. +set -euo pipefail + +id="${1:?usage: resolve-persona-identity.sh [field]}" +root="${2:?usage: resolve-persona-identity.sh [field]}" +field="${3:-account}" + +case "$field" in + account | credential) ;; + *) + echo "resolve-persona-identity: field must be 'account' or 'credential', got '$field'" >&2 + exit 2 + ;; +esac + +manifest="${root%/}/${id}/persona.yml" +if [ ! -f "$manifest" ]; then + echo "resolve-persona-identity: no manifest at $manifest" >&2 + exit 1 +fi + +# Prefer PyYAML (same parser validate-personas.py uses); fall back to a simple +# standard-library-only parser if PyYAML is not installed to avoid pip install +# overhead and PEP 668 ("externally-managed-environment") failures. +python3 - "$manifest" "$field" <<'PY' +import sys + +manifest, field = sys.argv[1], sys.argv[2] +val = None + +try: + import yaml + with open(manifest, encoding="utf-8") as fh: + doc = yaml.safe_load(fh) or {} + identity = ((doc.get("runtime") or {}).get("identity")) or {} + val = identity.get(field) +except ImportError: + # Fallback to a simple line-by-line parser if PyYAML is not installed + try: + with open(manifest, encoding="utf-8") as fh: + state = "start" + runtime_indent = -1 + identity_indent = -1 + for line in fh: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + indent = len(line) - len(line.lstrip()) + if state == "start" and stripped == "runtime:": + state = "runtime" + runtime_indent = indent + elif state == "runtime": + if indent <= runtime_indent: + state = "start" + elif stripped == "identity:": + state = "identity" + identity_indent = indent + elif state == "identity": + if indent <= identity_indent: + if indent <= runtime_indent: + state = "start" + else: + state = "runtime" + else: + if ":" in stripped: + k, v = stripped.split(":", 1) + if k.strip() == field: + val = v.strip().strip("'\"") + break + except Exception as e: + sys.stderr.write(f"resolve-persona-identity: fallback parser failed: {e}\n") + sys.exit(5) + +if not val: + sys.stderr.write( + f"resolve-persona-identity: runtime.identity.{field} missing in {manifest}\n" + ) + sys.exit(4) +print(val) +PY diff --git a/scripts/verify-persona-identity.sh b/scripts/verify-persona-identity.sh new file mode 100755 index 000000000..a3057df59 --- /dev/null +++ b/scripts/verify-persona-identity.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# verify-persona-identity.sh — LIVE-ish drift check for persona runtime identity. +# +# The schema (persona.schema.json) validates the SHAPE of runtime.identity and +# validate-personas.py makes it MANDATORY. This script closes the loop between the +# manifest and the workflows that consume it, so the declared identity and the +# actual CI wiring can never silently diverge (the failure mode behind +# .github-private#1316). For each persona it asserts: +# +# 1. credential naming — a non-grandfathered credential MUST be +# GH_PAT_[_] for the declared account. +# 2. credential is real — the secret is referenced by at least one workflow +# (guards against declaring a phantom/typo'd secret). +# 3. runtime consumption — a persona that ships a write workflow MUST resolve +# its identity from the manifest (call resolve-persona-identity.sh), not from +# the shared vars.BOT_USER. +# +# Usage: verify-persona-identity.sh [personas-root] +# Exit 0 = all good; exit 1 = a drift/violation was found. +set -euo pipefail + +ROOT="${1:-personas}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RESOLVE="$SCRIPT_DIR/lib/resolve-persona-identity.sh" +WORKFLOWS_DIR=".github/workflows" + +# Credentials that pre-date the GH_PAT_ convention (kept as-is). Mirror +# GRANDFATHERED_CREDENTIALS in personas/validate-personas.py. +GRANDFATHERED=( + DON_PETRY_BOT_GH_PAT + DON_PETRY_BOT_GH_PAT_CLASSIC +) + +# Personas that ship a write workflow which MUST consume the manifest identity. +# id:workflow-path pairs. Advisory personas post via the shared persona runner +# and are covered by checks 1-2 only. +declare -a WRITE_RUNTIMES=( + "dev-lead:.github/workflows/dev-lead-reusable.yml" + "pr-review:.github/workflows/pr-review.yml" +) + +fail() { echo "::error::verify-persona-identity: $*" >&2; exit 1; } + +is_grandfathered() { + local c="$1" g + for g in "${GRANDFATHERED[@]}"; do [ "$c" = "$g" ] && return 0; done + return 1 +} + +# Upper-snake-case conversion for account names is done inline via pure Bash parameter expansion. + +runtime_for() { + local id="$1" pair + for pair in "${WRITE_RUNTIMES[@]}"; do + [ "${pair%%:*}" = "$id" ] && { printf '%s' "${pair#*:}"; return 0; } + done + return 1 +} + +count=0 +for manifest in "$ROOT"/*/persona.yml; do + [ -f "$manifest" ] || continue + manifest_dir="${manifest%/persona.yml}" + id="${manifest_dir##*/}" + account="$(bash "$RESOLVE" "$id" "$ROOT" account)" + credential="$(bash "$RESOLVE" "$id" "$ROOT" credential)" + + # 1. credential naming convention + if ! is_grandfathered "$credential"; then + upper_account="${account^^}" + expect="GH_PAT_${upper_account//-/_}" + case "$credential" in + "$expect" | "${expect}_"*) : ;; + *) fail "$id: credential '$credential' does not match '${expect}[_]' for account '$account'" ;; + esac + fi + + # 2. credential is referenced by at least one workflow + if ! grep -rqlE "secrets\.${credential}([^A-Z0-9_]|$)" "$WORKFLOWS_DIR" 2>/dev/null; then + fail "$id: credential secret '$credential' is not referenced by any workflow in $WORKFLOWS_DIR" + fi + + # 3. write personas must consume the manifest identity at runtime + if wf="$(runtime_for "$id")"; then + [ -f "$wf" ] || fail "$id: declared runtime workflow '$wf' does not exist" + if ! grep -qE '(bash|sh)[[:space:]]+.*resolve-persona-identity\.sh' "$wf"; then + fail "$id: runtime workflow '$wf' must resolve identity from the manifest (call resolve-persona-identity.sh), not vars.BOT_USER" + fi + fi + + echo " ok: $id acts as '$account' via secret '$credential'" + count=$((count + 1)) +done + +if [ "$count" -eq 0 ]; then + echo "no persona manifests under $ROOT — nothing to verify." +else + echo "verify-persona-identity: $count persona identity mapping(s) consistent." +fi diff --git a/tests/test_validate_personas.bats b/tests/test_validate_personas.bats index 0beb11e88..e7277901f 100644 --- a/tests/test_validate_personas.bats +++ b/tests/test_validate_personas.bats @@ -53,6 +53,10 @@ triggers: surfaces: [] trust: author_association_floor: [OWNER] +runtime: + identity: + account: demo + credential: GH_PAT_DEMO evals: required_before: stable path: evals/demo/ @@ -132,6 +136,10 @@ triggers: surfaces: [] trust: author_association_floor: [OWNER] +runtime: + identity: + account: demo + credential: GH_PAT_DEMO evals: required_before: stable path: evals/demo/ @@ -295,3 +303,37 @@ clone_persona() { [ "$status" -ne 0 ] # only 2 real < 3 [[ "$output" == *"2 held-out"* ]] } + +@test "validate-personas REQUIRES runtime.identity" { + # Strip the runtime.identity block -> every persona must declare who it acts as. + sed '/^runtime:$/,/^ credential:/d' "$TMP/personas/demo/persona.yml" > "$TMP/personas/demo/persona.yml.tmp" && mv "$TMP/personas/demo/persona.yml.tmp" "$TMP/personas/demo/persona.yml" + run python3 "$VALIDATOR" "$TMP/personas" --schema "$TMP/schema.json" + [ "$status" -ne 0 ] + [[ "$output" == *"missing runtime.identity"* ]] +} + +@test "validate-personas enforces the GH_PAT_ credential convention" { + # A non-grandfathered credential that isn't GH_PAT_* is rejected. + sed 's/^ credential: GH_PAT_DEMO/ credential: DEMO_TOKEN/' "$TMP/personas/demo/persona.yml" > "$TMP/personas/demo/persona.yml.tmp" && mv "$TMP/personas/demo/persona.yml.tmp" "$TMP/personas/demo/persona.yml" + run python3 "$VALIDATOR" "$TMP/personas" --schema "$TMP/schema.json" + [ "$status" -ne 0 ] + [[ "$output" == *"GH_PAT_"* ]] +} + +@test "validate-personas rejects identity missing the 'credential' key" { + # identity present but credential key omitted -> defensive check catches it cleanly. + sed '/^ credential:/d' "$TMP/personas/demo/persona.yml" > "$TMP/personas/demo/persona.yml.tmp" + mv "$TMP/personas/demo/persona.yml.tmp" "$TMP/personas/demo/persona.yml" + run python3 "$VALIDATOR" "$TMP/personas" --schema "$TMP/schema.json" + [ "$status" -ne 0 ] + [[ "$output" == *"runtime.identity must be a mapping"* ]] +} + +@test "validate-personas rejects credential that does not match the declared account name" { + # GH_PAT_OTHER starts with GH_PAT_ but account is 'demo' — expected GH_PAT_DEMO. + sed 's/^ credential: GH_PAT_DEMO/ credential: GH_PAT_OTHER/' "$TMP/personas/demo/persona.yml" > "$TMP/personas/demo/persona.yml.tmp" + mv "$TMP/personas/demo/persona.yml.tmp" "$TMP/personas/demo/persona.yml" + run python3 "$VALIDATOR" "$TMP/personas" --schema "$TMP/schema.json" + [ "$status" -ne 0 ] + [[ "$output" == *"GH_PAT_"* ]] +}