From 8c491613605cb9a3fcf7e92b2f9263896349a28f Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 7 Mar 2026 12:18:10 -0800 Subject: [PATCH 1/2] Migrate to privilege-separated AI workflows and harden release permissions AI labeler: replace inline jobs with thin callers to centralized reusable workflows in basecamp/.github that separate inference from mutation. Release: narrow per-job permissions to principle of least privilege. Seed template: add environment gate to sync-skills and narrow permissions. Ref: HackerOne #3585928 --- .github/workflows/ai-labeler.yml | 226 +++---------------------------- 1 file changed, 20 insertions(+), 206 deletions(-) diff --git a/.github/workflows/ai-labeler.yml b/.github/workflows/ai-labeler.yml index 498f2e6..51dd78b 100644 --- a/.github/workflows/ai-labeler.yml +++ b/.github/workflows/ai-labeler.yml @@ -16,211 +16,25 @@ permissions: jobs: classify: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Build prompt - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR: ${{ github.event.pull_request.number }} - run: | - gh pr diff "$PR" > /tmp/pr.diff - gh pr view "$PR" --json title --jq .title > /tmp/pr-title.txt - gh pr view "$PR" --json body --jq '.body // ""' > /tmp/pr-body.txt - - # Compose user message - { - printf 'PR #%s: %s\n' "$PR" "$(cat /tmp/pr-title.txt)" - echo "" - cat /tmp/pr-body.txt - echo "" - echo "Diff (truncated):" - head -c 100000 /tmp/pr.diff - } > /tmp/user-message.txt - - # Build full prompt YAML: splice user message into the messages array - python3 -c " - with open('.github/prompts/classify-pr.prompt.yml') as f: - lines = f.readlines() - with open('/tmp/user-message.txt') as f: - user_msg = f.read() - - insert_at = len(lines) - for i, line in enumerate(lines): - if i == 0: - continue - if line.strip() and not line[0].isspace(): - insert_at = i - break - - entry = [' - role: user\n', ' content: |\n'] - for ln in user_msg.splitlines(): - entry.append(' ' + ln + '\n') - - lines[insert_at:insert_at] = entry - with open('/tmp/prompt.yml', 'w') as f: - f.writelines(lines) - - try: - import yaml - doc = yaml.safe_load(open('/tmp/prompt.yml')) - assert doc['messages'][-1]['role'] == 'user', 'prompt splice failed' - except ImportError: - pass - " - - - name: Classify - id: classify - uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7 - with: - prompt-file: /tmp/prompt.yml - - - name: Apply label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RESPONSE_FILE: ${{ steps.classify.outputs.response-file }} - PR: ${{ github.event.pull_request.number }} - run: | - LABEL=$(jq -r '.label // empty' "$RESPONSE_FILE" 2>/dev/null || cat "$RESPONSE_FILE") - LABEL=$(printf '%s' "$LABEL" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') - case "$LABEL" in - bug|enhancement|documentation) ;; - *) echo "Unexpected: $LABEL — skipping"; exit 0 ;; - esac - CURRENT=$(gh pr view "$PR" --json labels --jq '.labels[].name') - for L in bug enhancement documentation; do - if [ "$L" != "$LABEL" ] && echo "$CURRENT" | grep -qx "$L"; then - gh pr edit "$PR" --remove-label "$L" 2>/dev/null || true - fi - done - if ! echo "$CURRENT" | grep -qx "$LABEL"; then - gh pr edit "$PR" --add-label "$LABEL" 2>/dev/null || true - fi + uses: basecamp/.github/.github/workflows/ai-classify-pr.yml@fc560544cd2bb4e242530bde9b0d9deb7863ea45 + with: + prompt-file: .github/prompts/classify-pr.prompt.yml + labels: "bug,enhancement,documentation" + permissions: + contents: read + issues: write + models: read + pull-requests: write breaking: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Build prompt - id: api-diff - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR: ${{ github.event.pull_request.number }} - run: | - gh pr diff "$PR" > /tmp/full.diff - - # Filter diff to exported Go library files (not tests, seed, or non-package dirs) - python3 -c " - import sys, re - diff = open('/tmp/full.diff').read() - dir_exclude = ('seed/', 'internal/', 'actions/', 'prompts/', 'skills/') - sections = re.split(r'(?=^diff --git)', diff, flags=re.MULTILINE) - for s in sections: - m = re.match(r'diff --git a/(\S+)', s) - if m: - path = m.group(1) - if path.endswith('.go') and not path.endswith('_test.go') and not any(path.startswith(d) for d in dir_exclude): - sys.stdout.write(s) - " > /tmp/api.diff - - if [ ! -s /tmp/api.diff ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - else - TITLE=$(gh pr view "$PR" --json title --jq .title) - - { - printf 'PR #%s: %s\n' "$PR" "$TITLE" - echo "" - echo "Diff of exported Go library files:" - head -c 100000 /tmp/api.diff - } > /tmp/user-message.txt - - python3 -c " - with open('.github/prompts/detect-breaking.prompt.yml') as f: - lines = f.readlines() - with open('/tmp/user-message.txt') as f: - user_msg = f.read() - - insert_at = len(lines) - for i, line in enumerate(lines): - if i == 0: - continue - if line.strip() and not line[0].isspace(): - insert_at = i - break - - entry = [' - role: user\n', ' content: |\n'] - for ln in user_msg.splitlines(): - entry.append(' ' + ln + '\n') - - lines[insert_at:insert_at] = entry - with open('/tmp/prompt.yml', 'w') as f: - f.writelines(lines) - - try: - import yaml - doc = yaml.safe_load(open('/tmp/prompt.yml')) - assert doc['messages'][-1]['role'] == 'user', 'prompt splice failed' - except ImportError: - pass - " - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Detect breaking changes - if: steps.api-diff.outputs.skip != 'true' - id: detect - uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7 - with: - prompt-file: /tmp/prompt.yml - - - name: Apply breaking label - if: steps.api-diff.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RESPONSE_FILE: ${{ steps.detect.outputs.response-file }} - PR: ${{ github.event.pull_request.number }} - run: | - if [ -z "$RESPONSE_FILE" ] || [ ! -f "$RESPONSE_FILE" ]; then - echo "::warning::Model response file is missing; skipping breaking label." - exit 0 - fi - if ! jq empty "$RESPONSE_FILE" 2>/dev/null; then - echo "::warning::Model response is not valid JSON; skipping breaking label." - { - echo "## Breaking change detection failed" - echo "Model returned invalid JSON. Breaking label was **not** applied." - if [ -s "$RESPONSE_FILE" ]; then - echo '```' - cat "$RESPONSE_FILE" - echo '```' - fi - } >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - BREAKING=$(jq -r '.breaking' "$RESPONSE_FILE") - - if [ "$BREAKING" = "true" ]; then - ITEMS=$(jq -r '.items[]' "$RESPONSE_FILE" | sed 's/^/- /') - gh label create breaking --color "B60205" 2>/dev/null || true - gh pr edit "$PR" --add-label "breaking" - - { - echo "**Potential breaking changes detected:**" - echo "" - echo "$ITEMS" - echo "" - echo "_Review carefully before merging. Consider a major version bump._" - } > /tmp/breaking-comment.md - - EXISTING=$(gh pr view "$PR" --json comments --jq '.comments[] | select(.body | startswith("**Potential breaking")) | .id' | head -1) - if [ -n "$EXISTING" ]; then - gh api graphql -f query="mutation { updateIssueComment(input: {id: \"$EXISTING\", body: $(jq -Rs . /tmp/breaking-comment.md)}) { issueComment { id } } }" - else - gh pr comment "$PR" --body-file /tmp/breaking-comment.md - fi - else - gh pr edit "$PR" --remove-label "breaking" 2>/dev/null || true - fi + uses: basecamp/.github/.github/workflows/ai-breaking-change.yml@fc560544cd2bb4e242530bde9b0d9deb7863ea45 + with: + prompt-file: .github/prompts/detect-breaking.prompt.yml + file-patterns: | + internal/commands/*.go + cmd/*/main.go + permissions: + contents: read + issues: write + models: read + pull-requests: write From f865c171f87faf00e96beabbed9c40f855c72096 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 7 Mar 2026 13:03:35 -0800 Subject: [PATCH 2/2] Fix file-patterns to match actual library packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repo is a Go library, not a CLI — it has no internal/commands/ or cmd/ directories. Use the actual exported package paths for breaking change detection. --- .github/workflows/ai-labeler.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ai-labeler.yml b/.github/workflows/ai-labeler.yml index 51dd78b..011f75f 100644 --- a/.github/workflows/ai-labeler.yml +++ b/.github/workflows/ai-labeler.yml @@ -31,8 +31,13 @@ jobs: with: prompt-file: .github/prompts/detect-breaking.prompt.yml file-patterns: | - internal/commands/*.go - cmd/*/main.go + credstore/*.go + editor/*.go + oauthcallback/*.go + output/*.go + pkce/*.go + profile/*.go + surface/*.go permissions: contents: read issues: write