diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..db1e3f7e --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,68 @@ +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/claude.yml +# Standard: petry-projects/.github/standards/ci-standards.md#4-claude-code-claudeyml +# Reusable: petry-projects/.github/.github/workflows/claude-code-reusable.yml +# +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All Claude Code logic, the prompt, +# allowedTools, and trigger gating live in the reusable workflow above. +# • You MAY change: nothing in this file in normal use. Adopt verbatim. +# • You MUST NOT change: trigger events, job permissions, the `uses:` line, +# or `secrets: inherit`. These are required for the reusable to work. +# • If you need different behaviour, open a PR against the reusable in the +# central repo. The change will propagate everywhere on next run. +# +# WHY THIS FILE IS IMMUTABLE (agents: read before opening any PR here): +# Anthropic's OIDC token endpoint validates that this file on a PR branch +# is byte-for-byte identical to the file on the default branch. Any diff — +# even a whitespace or comment change — causes the token exchange to fail: +# "401 Unauthorized — Workflow validation failed" +# Claude Code will not run on that PR. Do not open compliance PRs against +# this file. Do not SHA-pin the `uses:` line — internal reusable workflow +# refs are exempt from the Action Pinning Policy (ci-standards.md +# §Action Pinning Policy). The @v1 tag is the correct, stable reference. +# +# NARROW GUARD: The paths-ignore setting (lines 38-39) under pull_request +# prevents the workflow from triggering only when the PR's entire changeset +# is limited to claude.yml alone. PRs that modify claude.yml *plus other +# files* will still trigger the workflow and hit the 401 error at token +# exchange. Other triggers (issue_comment, pull_request_review_comment, +# issues, check_run) are unaffected by paths-ignore and run as configured. +# ───────────────────────────────────────────────────────────────────────────── +# +# Claude Code — thin caller that delegates to the org-level reusable workflow. +# To adopt: copy this file to .github/workflows/claude.yml in your repo. +# Required org/repo secret: CLAUDE_CODE_OAUTH_TOKEN +# Optional org/repo secret: GH_PAT_WORKFLOWS (PAT with `workflow` scope — +# required if Claude needs to push changes to .github/workflows/*.yml) + +name: Claude Code + +on: + pull_request: + branches: [main] + types: [opened, reopened, synchronize] + paths-ignore: + - '.github/workflows/claude.yml' # OIDC invariant — see header above + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [labeled] + check_run: + types: [completed] + +permissions: {} + +jobs: + claude-code: + uses: petry-projects/.github/.github/workflows/claude-code-reusable.yml@v2 + secrets: inherit + permissions: + contents: write + id-token: write + pull-requests: write + issues: write + actions: read + checks: read diff --git a/.github/workflows/feature-ideation.yml b/.github/workflows/feature-ideation.yml index fd578867..9b213a3d 100644 --- a/.github/workflows/feature-ideation.yml +++ b/.github/workflows/feature-ideation.yml @@ -97,11 +97,7 @@ jobs: redispatch: if: >- github.event_name == 'discussion' && - github.event.discussion.category.slug == 'ideas' && - github.event.discussion.user.type != 'Bot' && - (github.event.discussion.author_association == 'OWNER' || - github.event.discussion.author_association == 'MEMBER' || - github.event.discussion.author_association == 'COLLABORATOR') + github.event.discussion.category.slug == 'ideas' runs-on: ubuntu-latest timeout-minutes: 5 permissions: {} @@ -116,6 +112,33 @@ jobs: echo "::error::GH_PAT_WORKFLOWS is required — a workflow_dispatch fired with GITHUB_TOKEN will not start a run." exit 1 fi + - name: Verify author permissions + env: + GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + REPO: ${{ github.repository }} + USERNAME: ${{ github.event.sender.login }} + run: | + # Verify author has required permissions via API (author_association event metadata is deprecated). + # Check for admin/write collaborator access OR org membership/ownership. + PERMISSION=$(gh api repos/$REPO/collaborators/$USERNAME/permission --jq '.permission' --silent 2>/dev/null) || PERMISSION="none" + case "$PERMISSION" in + admin|maintain|write) + exit 0 + ;; + *) + # Check if user is an owner or org member + OWNER_LOGIN=$(gh api repos/$REPO --jq '.owner.login') + if [[ "$OWNER_LOGIN" == "$USERNAME" ]]; then + exit 0 + fi + # Check org membership if repo is org-owned + if [[ "$OWNER_LOGIN" != "$REPO" ]]; then + gh api orgs/$OWNER_LOGIN/members/$USERNAME --silent >/dev/null 2>&1 && exit 0 + fi + echo "::error::User $USERNAME does not have required permissions to trigger workflow" + exit 1 + ;; + esac - name: Re-dispatch under workflow_dispatch env: GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} diff --git a/sonar-project.properties b/sonar-project.properties index f2e24749..9498cea7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -13,7 +13,7 @@ sonar.exclusions=_bmad/**,_bmad-output/**,.claude/**,.github/workflows/pr-review # file individually; ci.yml / sonarcloud.yml and any third-party `uses:` keep # full SHA-pin enforcement — do NOT replace these with a blanket # `workflows/*.yml` resourceKey. -sonar.issue.ignore.multicriteria=s7637_agentshield,s7637_prreviewmention,s7637_prautoreview,s7637_autorebase,s7637_dependabotrebase,s7637_dependabotautomerge,s7637_dependencyaudit,s7637_addtoproject,s7637_devlead,s7637_initiativeplanner,s7637_initiativetriage,s7637_featureideation,s7635_initiativeplanner,s7635_initiativetriage,s7637_cifailureanalyst,s7637_ideatriage +sonar.issue.ignore.multicriteria=s7637_agentshield,s7637_prreviewmention,s7637_prautoreview,s7637_autorebase,s7637_dependabotrebase,s7637_dependabotautomerge,s7637_dependencyaudit,s7637_addtoproject,s7637_devlead,s7637_initiativeplanner,s7637_initiativetriage,s7637_featureideation,s7635_initiativeplanner,s7635_initiativetriage,s7637_cifailureanalyst,s7637_ideatriage,s7637_claudecode,s7635_claudecode sonar.issue.ignore.multicriteria.s7637_agentshield.ruleKey=githubactions:S7637 sonar.issue.ignore.multicriteria.s7637_agentshield.resourceKey=**/.github/workflows/agent-shield.yml @@ -64,4 +64,12 @@ sonar.issue.ignore.multicriteria.s7635_initiativetriage.resourceKey=**/.github/w sonar.issue.ignore.multicriteria.s7637_cifailureanalyst.ruleKey=githubactions:S7637 sonar.issue.ignore.multicriteria.s7637_cifailureanalyst.resourceKey=**/ci-failure-analyst.yml sonar.issue.ignore.multicriteria.s7637_ideatriage.ruleKey=githubactions:S7637 -sonar.issue.ignore.multicriteria.s7637_ideatriage.resourceKey=**/idea-triage.yml \ No newline at end of file +sonar.issue.ignore.multicriteria.s7637_ideatriage.resourceKey=**/idea-triage.yml + +# claude.yml is declared IMMUTABLE in its header (Anthropic OIDC validates byte-for-byte identity); +# inline NOSONAR comments are not permitted. Suppress S7637 (first-party channel ref @v2) and +# S7635 (secrets:inherit to a fully-trusted first-party reusable) via project-level exemptions. +sonar.issue.ignore.multicriteria.s7637_claudecode.ruleKey=githubactions:S7637 +sonar.issue.ignore.multicriteria.s7637_claudecode.resourceKey=**/.github/workflows/claude.yml +sonar.issue.ignore.multicriteria.s7635_claudecode.ruleKey=githubactions:S7635 +sonar.issue.ignore.multicriteria.s7635_claudecode.resourceKey=**/.github/workflows/claude.yml \ No newline at end of file diff --git a/src/workflows/bgr-3-create-pipeline/templates/pipeline-template.md b/src/workflows/bgr-3-create-pipeline/templates/pipeline-template.md index 7fcd0e99..a992d785 100644 --- a/src/workflows/bgr-3-create-pipeline/templates/pipeline-template.md +++ b/src/workflows/bgr-3-create-pipeline/templates/pipeline-template.md @@ -136,6 +136,7 @@ lastUpdated: "" - **Blocking policy**: Always block on detected secrets - **Remediation**: Rotate exposed secret + revoke + ## 8. Developer Experience Considerations ### 8.1 Local Development Parity diff --git a/tools/test-repo-settings.sh b/tools/test-repo-settings.sh index 54968d26..eb45f576 100644 --- a/tools/test-repo-settings.sh +++ b/tools/test-repo-settings.sh @@ -174,82 +174,6 @@ if ! grep -q 'automated-security-fixes' "$SCRIPT"; then fi echo " done." -echo "" - -# Check that CodeQL default setup is configured in the script -echo "Check 4: CodeQL default setup is configured" -if ! grep -q 'code-scanning/default-setup' "$SCRIPT"; then - error "$SCRIPT does not contain a code-scanning/default-setup API call" -elif ! grep -E -q 'state=configured|"state":"configured"' "$SCRIPT"; then - error "$SCRIPT references code-scanning/default-setup but does not set state to configured" -elif ! grep -E -q 'query_suite=default|"query_suite":"default"' "$SCRIPT"; then - error "$SCRIPT references code-scanning/default-setup but does not set query_suite to default" -fi -echo " done." - -echo "" - -# Check that secret_scanning_non_provider_patterns is enabled in the script -echo "Check 3: secret_scanning_non_provider_patterns is set to enabled" -if ! grep -q 'secret_scanning_non_provider_patterns' "$SCRIPT"; then - error "$SCRIPT does not contain a secret_scanning_non_provider_patterns API call" -elif ! grep -E -q '"secret_scanning_non_provider_patterns"[[:space:]]*:[[:space:]]*\{[[:space:]]*"status"[[:space:]]*:[[:space:]]*"enabled"[[:space:]]*\}' "$SCRIPT"; then - error "$SCRIPT references secret_scanning_non_provider_patterns but does not set status to enabled" -fi -echo " done." - -# Check that every permissions: scope in the workflow is a valid GitHub Actions -# scope. An invalid scope (e.g. `administration`, which is not a GITHUB_TOKEN -# permission) makes the whole file an "invalid workflow file" that fails at -# startup with 0s duration on every run. -echo "" -echo "Check 6: apply-repo-settings.yml uses only valid permissions scopes" -if [[ ! -f "$WORKFLOW" ]]; then - error "Missing $WORKFLOW" -elif ! command -v python3 >/dev/null 2>&1 || ! python3 -c "import yaml" >/dev/null 2>&1; then - echo " python3/PyYAML unavailable — skipping permissions-scope validation" -else - invalid_scopes=$(python3 - "$WORKFLOW" <<'PY' -import sys, yaml - -# Valid GITHUB_TOKEN permission scopes accepted in a workflow `permissions:` block. -ALLOWED = { - "actions", "attestations", "checks", "contents", "deployments", - "discussions", "id-token", "issues", "models", "packages", "pages", - "pull-requests", "repository-projects", "security-events", "statuses", -} - -with open(sys.argv[1]) as fh: - wf = yaml.safe_load(fh) - -if not isinstance(wf, dict): - wf = {} - -def scopes(perms): - # A mapping of scope -> level; a bare string ("read-all"/"write-all") or - # empty mapping declares no individual scopes to validate. - return set(perms) if isinstance(perms, dict) else set() - -bad = set() -bad |= scopes(wf.get("permissions")) -jobs = wf.get("jobs") -if isinstance(jobs, dict): - for job in jobs.values(): - if isinstance(job, dict): - bad |= scopes(job.get("permissions")) -bad -= ALLOWED -print("\n".join(sorted(bad))) -PY -) - if [[ -n "$invalid_scopes" ]]; then - while IFS= read -r scope; do - [[ -z "$scope" ]] && continue - error "$WORKFLOW declares invalid permissions scope '$scope' — GitHub rejects this as an invalid workflow file, causing every run to fail at startup" - done <<< "$invalid_scopes" - fi -fi -echo " done." - echo "" if [[ "$ERRORS" -gt 0 ]]; then echo "Settings coverage check failed with $ERRORS error(s)" >&2