From c9bd61255ff2043559a8692ecc2faf8f3b37a54d Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 7 Mar 2026 07:54:44 -0800 Subject: [PATCH 1/3] Fix script injection in AI labeler workflow Use response-file instead of inline model output interpolation, pass PR numbers through environment variables, and switch to structured JSON output for the classifier prompt. Applied to both repo workflow and seed template. --- .github/prompts/classify-pr.prompt.yml | 19 ++++++++++++++++++- .github/workflows/ai-labeler.yml | 13 +++++++------ seed/.github/prompts/classify-pr.prompt.yml | 19 ++++++++++++++++++- seed/.github/workflows/ai-labeler.yml | 14 +++++++------- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/.github/prompts/classify-pr.prompt.yml b/.github/prompts/classify-pr.prompt.yml index 0d87499..e980961 100644 --- a/.github/prompts/classify-pr.prompt.yml +++ b/.github/prompts/classify-pr.prompt.yml @@ -2,7 +2,7 @@ messages: - role: system content: | You classify pull requests for release note categorization. - Respond with exactly one word: bug, enhancement, or documentation. + Respond with JSON: {"label": "bug"}, {"label": "enhancement"}, or {"label": "documentation"}. - bug: corrects wrong behavior, broken defaults, incorrect error codes, retry/backoff defects, auth handling bugs, compatibility regressions. @@ -19,6 +19,23 @@ messages: When a PR mixes categories: bug > enhancement > documentation. Prefer diff evidence over the PR title. model: openai/gpt-4o-mini +responseFormat: + type: json_schema + json_schema: + name: classification + strict: true + schema: + type: object + properties: + label: + type: string + enum: + - bug + - enhancement + - documentation + required: + - label + additionalProperties: false modelParameters: maxCompletionTokens: 10 temperature: 0 diff --git a/.github/workflows/ai-labeler.yml b/.github/workflows/ai-labeler.yml index de1c201..e1d6c52 100644 --- a/.github/workflows/ai-labeler.yml +++ b/.github/workflows/ai-labeler.yml @@ -23,8 +23,8 @@ jobs: - name: Build prompt env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} run: | - PR=${{ github.event.pull_request.number }} 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 @@ -79,13 +79,14 @@ jobs: - name: Apply label env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RESPONSE_FILE: ${{ steps.classify.outputs.response-file }} + PR: ${{ github.event.pull_request.number }} run: | - LABEL=$(echo "${{ steps.classify.outputs.response }}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') + LABEL=$(jq -r '.label // empty' "$RESPONSE_FILE" 2>/dev/null || tr -d '[:space:]' < "$RESPONSE_FILE" | tr '[:upper:]' '[:lower:]') case "$LABEL" in bug|enhancement|documentation) ;; *) echo "Unexpected: $LABEL — skipping"; exit 0 ;; esac - PR=${{ github.event.pull_request.number }} 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 @@ -105,8 +106,8 @@ jobs: id: api-diff env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} run: | - PR=${{ github.event.pull_request.number }} gh pr diff "$PR" > /tmp/full.diff # Filter diff to exported Go library files (not tests, seed, or non-package dirs) @@ -178,8 +179,9 @@ jobs: 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: | - RESPONSE_FILE="${{ steps.detect.outputs.response-file }}" if [ -z "$RESPONSE_FILE" ] || [ ! -f "$RESPONSE_FILE" ]; then echo "::warning::Model response file is missing; skipping breaking label." exit 0 @@ -198,7 +200,6 @@ jobs: exit 0 fi BREAKING=$(jq -r '.breaking' "$RESPONSE_FILE") - PR=${{ github.event.pull_request.number }} if [ "$BREAKING" = "true" ]; then ITEMS=$(jq -r '.items[]' "$RESPONSE_FILE" | sed 's/^/- /') diff --git a/seed/.github/prompts/classify-pr.prompt.yml b/seed/.github/prompts/classify-pr.prompt.yml index 0d87499..e980961 100644 --- a/seed/.github/prompts/classify-pr.prompt.yml +++ b/seed/.github/prompts/classify-pr.prompt.yml @@ -2,7 +2,7 @@ messages: - role: system content: | You classify pull requests for release note categorization. - Respond with exactly one word: bug, enhancement, or documentation. + Respond with JSON: {"label": "bug"}, {"label": "enhancement"}, or {"label": "documentation"}. - bug: corrects wrong behavior, broken defaults, incorrect error codes, retry/backoff defects, auth handling bugs, compatibility regressions. @@ -19,6 +19,23 @@ messages: When a PR mixes categories: bug > enhancement > documentation. Prefer diff evidence over the PR title. model: openai/gpt-4o-mini +responseFormat: + type: json_schema + json_schema: + name: classification + strict: true + schema: + type: object + properties: + label: + type: string + enum: + - bug + - enhancement + - documentation + required: + - label + additionalProperties: false modelParameters: maxCompletionTokens: 10 temperature: 0 diff --git a/seed/.github/workflows/ai-labeler.yml b/seed/.github/workflows/ai-labeler.yml index 3d1048a..9653e8f 100644 --- a/seed/.github/workflows/ai-labeler.yml +++ b/seed/.github/workflows/ai-labeler.yml @@ -23,8 +23,8 @@ jobs: - name: Build prompt env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} run: | - PR=${{ github.event.pull_request.number }} 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 @@ -79,13 +79,14 @@ jobs: - name: Apply label env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RESPONSE_FILE: ${{ steps.classify.outputs.response-file }} + PR: ${{ github.event.pull_request.number }} run: | - LABEL=$(echo "${{ steps.classify.outputs.response }}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') + LABEL=$(jq -r '.label // empty' "$RESPONSE_FILE" 2>/dev/null || tr -d '[:space:]' < "$RESPONSE_FILE" | tr '[:upper:]' '[:lower:]') case "$LABEL" in bug|enhancement|documentation) ;; *) echo "Unexpected: $LABEL — skipping"; exit 0 ;; esac - PR=${{ github.event.pull_request.number }} 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 @@ -105,9 +106,8 @@ jobs: id: cmd-diff env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} run: | - PR=${{ github.event.pull_request.number }} - # CLI command surface files PATTERNS=( "internal/commands/*.go" @@ -184,8 +184,9 @@ jobs: if: steps.cmd-diff.outputs.skip != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RESPONSE_FILE: ${{ steps.detect.outputs.response-file }} + PR: ${{ github.event.pull_request.number }} run: | - RESPONSE_FILE="${{ steps.detect.outputs.response-file }}" if [ -z "$RESPONSE_FILE" ] || [ ! -f "$RESPONSE_FILE" ]; then echo "::warning::Model response file is missing; skipping breaking label." exit 0 @@ -204,7 +205,6 @@ jobs: exit 0 fi BREAKING=$(jq -r '.breaking' "$RESPONSE_FILE") - PR=${{ github.event.pull_request.number }} if [ "$BREAKING" = "true" ]; then ITEMS=$(jq -r '.items[]' "$RESPONSE_FILE" | sed 's/^/- /') From 22ce17c7dc682727c982b958d0e837d7bd3dceac Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 7 Mar 2026 07:54:53 -0800 Subject: [PATCH 2/3] Fix script injection and harden release workflows Replace $GITHUB_ENV injection vector in release changelog with file-based approach. Remove secrets:inherit in favor of explicit secret declarations. Move ref_name and commit SHA out of shell interpolation into environment variables. Add environment gate on sync-skills job. Narrow permissions from workflow-level to per-job. Apply all fixes to seed templates. --- .github/workflows/release.yml | 51 ++++++++++------------- seed/.github/workflows/release.yml | 64 +++++++++++++++++------------ seed/.github/workflows/security.yml | 11 ++++- seed/.github/workflows/test.yml | 28 +++++++++---- 4 files changed, 89 insertions(+), 65 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1c2465..fdb3275 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,17 +5,10 @@ on: tags: - 'v*' -permissions: - contents: write - security-events: write - pull-requests: read - models: read - jobs: security: name: Security uses: ./.github/workflows/security.yml - secrets: inherit test: name: Test gate @@ -63,9 +56,11 @@ jobs: govulncheck ./... - name: Verify tag is on main + env: + COMMIT_SHA: ${{ github.sha }} run: | git fetch origin main - if ! git merge-base --is-ancestor "${{ github.sha }}" origin/main; then + if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/main; then echo "Error: tag is not on the main branch" exit 1 fi @@ -83,9 +78,11 @@ jobs: fetch-depth: 0 - name: Verify tag is on main + env: + COMMIT_SHA: ${{ github.sha }} run: | git fetch origin main - if ! git merge-base --is-ancestor "${{ github.sha }}" origin/main; then + if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/main; then echo "Error: tag is not on the main branch" exit 1 fi @@ -142,35 +139,23 @@ jobs: with: prompt-file: /tmp/prompt.yml - - name: Set changelog env - run: | - RESPONSE_FILE="${{ steps.ai-changelog.outputs.response-file }}" - if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then - # Strip markdown code fences that AI models wrap around responses - sed -i '/^```\(markdown\)\?$/d' "$RESPONSE_FILE" - DELIM="CHANGELOG_DELIM_$(openssl rand -hex 8)" - { - echo "RELEASE_CHANGELOG<<${DELIM}" - cat "$RESPONSE_FILE" - echo "" - echo "${DELIM}" - } >> $GITHUB_ENV - else - echo "RELEASE_CHANGELOG=" >> $GITHUB_ENV - fi - - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHANGELOG_FILE: ${{ steps.ai-changelog.outputs.response-file }} + TAG: ${{ github.ref_name }} run: | - TAG="${{ github.ref_name }}" NOTES="" - if [ -n "$RELEASE_CHANGELOG" ]; then - NOTES="${RELEASE_CHANGELOG} + if [ -n "$CHANGELOG_FILE" ] && [ -f "$CHANGELOG_FILE" ]; then + sed -i '/^```\(markdown\)\?$/d' "$CHANGELOG_FILE" + CHANGELOG=$(cat "$CHANGELOG_FILE") + if [ -n "$CHANGELOG" ]; then + NOTES="${CHANGELOG} --- " + fi fi NOTES="${NOTES}### Install @@ -197,6 +182,7 @@ jobs: if: vars.SKILLS_APP_ID != '' continue-on-error: true timeout-minutes: 5 + environment: release concurrency: group: sync-skills cancel-in-progress: false @@ -232,4 +218,9 @@ jobs: - name: Sync skills if: steps.check.outputs.ready == 'true' - run: CLI_NAME=cli SKILLS_TOKEN=${{ steps.skills-token.outputs.token }} RELEASE_TAG=${{ github.ref_name }} SOURCE_SHA=${{ github.sha }} scripts/sync-skills.sh + env: + CLI_NAME: cli + SKILLS_TOKEN: ${{ steps.skills-token.outputs.token }} + RELEASE_TAG: ${{ github.ref_name }} + SOURCE_SHA: ${{ github.sha }} + run: scripts/sync-skills.sh diff --git a/seed/.github/workflows/release.yml b/seed/.github/workflows/release.yml index cd94b9f..e9e3c5b 100644 --- a/seed/.github/workflows/release.yml +++ b/seed/.github/workflows/release.yml @@ -5,22 +5,18 @@ on: tags: - 'v*' -permissions: - contents: write - id-token: write # keyless cosign signing via OIDC - security-events: write # SARIF upload in security scan - pull-requests: read - models: read # AI changelog (when enabled) - jobs: security: name: Security scan uses: ./.github/workflows/security.yml - secrets: inherit + secrets: + RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} test: name: Test before release runs-on: ubuntu-latest + permissions: + contents: read env: APPNAME_NO_KEYRING: "1" # Uncomment for private module access: GOPRIVATE: github.com/basecamp/ @@ -47,7 +43,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Install golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9 @@ -115,6 +113,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 environment: release + permissions: + contents: write + id-token: write + models: read env: HAS_MACOS_SIGNING: ${{ secrets.MACOS_SIGN_P12 && 'true' || '' }} HAS_AUR_KEY: ${{ secrets.AUR_SSH_KEY && 'true' || '' }} @@ -142,7 +144,9 @@ jobs: - name: Configure git for private modules if: steps.sdk-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.sdk-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.sdk-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Verify tag is on main run: | @@ -237,22 +241,15 @@ jobs: with: prompt-file: /tmp/prompt.yml - - name: Set changelog env + - name: Set changelog path + id: changelog if: vars.ENABLE_AI_CHANGELOG == 'true' + env: + RESPONSE_FILE: ${{ steps.ai-changelog.outputs.response-file }} run: | - RESPONSE_FILE="${{ steps.ai-changelog.outputs.response-file }}" if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then - # Strip markdown code fences that AI models wrap around responses sed -i '/^```\(markdown\)\?$/d' "$RESPONSE_FILE" - DELIM="CHANGELOG_DELIM_$(openssl rand -hex 8)" - { - echo "RELEASE_CHANGELOG<<${DELIM}" - cat "$RESPONSE_FILE" - echo "" - echo "${DELIM}" - } >> "$GITHUB_ENV" - else - echo "RELEASE_CHANGELOG=" >> "$GITHUB_ENV" + echo "file=$RESPONSE_FILE" >> "$GITHUB_OUTPUT" fi # Configure secrets.MACOS_SIGN_P12 (plus MACOS_SIGN_PASSWORD, MACOS_NOTARY_KEY, @@ -278,13 +275,16 @@ jobs: MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - - name: Run GoReleaser + - name: Install GoReleaser uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7 with: distribution: goreleaser version: '~> v2' - args: release --clean + install-only: true + + - name: Run GoReleaser env: + CHANGELOG_FILE: ${{ steps.changelog.outputs.file }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_TOKEN: ${{ steps.sdk-token.outputs.token }} MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} @@ -292,6 +292,12 @@ jobs: MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + run: | + if [ -n "$CHANGELOG_FILE" ] && [ -f "$CHANGELOG_FILE" ]; then + RELEASE_CHANGELOG=$(cat "$CHANGELOG_FILE") + export RELEASE_CHANGELOG + fi + goreleaser release --clean # Configure secrets.AUR_SSH_KEY to enable Arch Linux AUR publishing - name: Publish to AUR @@ -321,6 +327,9 @@ jobs: cancel-in-progress: false runs-on: ubuntu-latest timeout-minutes: 10 + environment: release + permissions: + contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -357,9 +366,12 @@ jobs: if: failure() && steps.check-skills.outputs.found == 'true' env: GH_TOKEN: ${{ steps.skills-token.outputs.token }} + REF_NAME: ${{ github.ref_name }} + REPO: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} run: | TITLE="Skills sync failure" - BODY="The automatic skills sync from [${{ github.ref_name }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) failed. Check the workflow run for details." + BODY="The automatic skills sync from [${REF_NAME}](https://github.com/${REPO}/actions/runs/${RUN_ID}) failed. Check the workflow run for details." # Check for existing open issue before creating a new one existing=$(gh issue list --repo basecamp/skills --state open --search "in:title $TITLE" --json number,title --jq '[.[] | select(.title == "'"$TITLE"'")][0].number // empty' 2>/dev/null || true) @@ -370,4 +382,4 @@ jobs: fi # Always emit annotation so the failure is visible in the workflow summary - echo "::error::Skills sync to basecamp/skills failed for ${{ github.ref_name }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + echo "::error::Skills sync to basecamp/skills failed for ${REF_NAME}. See https://github.com/${REPO}/actions/runs/${RUN_ID}" diff --git a/seed/.github/workflows/security.yml b/seed/.github/workflows/security.yml index a7dde00..a5dc882 100644 --- a/seed/.github/workflows/security.yml +++ b/seed/.github/workflows/security.yml @@ -9,6 +9,9 @@ on: # Weekly scan on Monday at 6am UTC - cron: '0 6 * * 1' workflow_call: # Allow release.yml to invoke the full security suite + secrets: + RELEASE_APP_PRIVATE_KEY: + required: false workflow_dispatch: permissions: @@ -83,7 +86,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Run gosec run: | @@ -131,7 +136,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Initialize CodeQL uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4 diff --git a/seed/.github/workflows/test.yml b/seed/.github/workflows/test.yml index 4d70709..3c88e8b 100644 --- a/seed/.github/workflows/test.yml +++ b/seed/.github/workflows/test.yml @@ -38,7 +38,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Check module tidiness run: make tidy-check @@ -78,7 +80,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Run golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9 @@ -107,7 +111,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Run govulncheck # @latest intentional — pinning delays scanning improvements and @@ -141,7 +147,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Run tests with race detector run: go test -race -v ./... @@ -171,7 +179,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Cache BATS id: cache-bats @@ -216,7 +226,9 @@ jobs: - name: Configure git for private modules if: steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Build and snapshot PR surface run: | @@ -280,7 +292,9 @@ jobs: - name: Configure git for private modules if: steps.filter.outputs.bench == 'true' && steps.app-token.outputs.token != '' - run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + env: + TOKEN: ${{ steps.app-token.outputs.token }} + run: git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - name: Run benchmarks if: steps.filter.outputs.bench == 'true' From 5088d7fc7a5b0532e5d56a9a4538ca4ec6a7346a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 7 Mar 2026 08:57:56 -0800 Subject: [PATCH 3/3] Address review feedback: fix jq normalization, response format, permissions, and changelog default - Split label extraction into two lines so tr normalization always runs - Standardize responseFormat to match detect-breaking.prompt.yml shape - Bump maxCompletionTokens from 10 to 25 for JSON output headroom - Restore top-level permissions so reusable security workflow gets security-events:write - Default RELEASE_CHANGELOG to empty string so GoReleaser template doesn't fail on missing key --- .github/prompts/classify-pr.prompt.yml | 36 ++++++++++----------- .github/workflows/ai-labeler.yml | 3 +- .github/workflows/release.yml | 6 ++++ seed/.github/prompts/classify-pr.prompt.yml | 36 ++++++++++----------- seed/.github/workflows/ai-labeler.yml | 3 +- seed/.github/workflows/release.yml | 10 +++++- 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/.github/prompts/classify-pr.prompt.yml b/.github/prompts/classify-pr.prompt.yml index e980961..4ae0c75 100644 --- a/.github/prompts/classify-pr.prompt.yml +++ b/.github/prompts/classify-pr.prompt.yml @@ -19,23 +19,23 @@ messages: When a PR mixes categories: bug > enhancement > documentation. Prefer diff evidence over the PR title. model: openai/gpt-4o-mini -responseFormat: - type: json_schema - json_schema: - name: classification - strict: true - schema: - type: object - properties: - label: - type: string - enum: - - bug - - enhancement - - documentation - required: - - label - additionalProperties: false +responseFormat: json_schema +jsonSchema: |- + { + "name": "classification", + "strict": true, + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "enum": ["bug", "enhancement", "documentation"] + } + }, + "required": ["label"], + "additionalProperties": false + } + } modelParameters: - maxCompletionTokens: 10 + maxCompletionTokens: 25 temperature: 0 diff --git a/.github/workflows/ai-labeler.yml b/.github/workflows/ai-labeler.yml index e1d6c52..498f2e6 100644 --- a/.github/workflows/ai-labeler.yml +++ b/.github/workflows/ai-labeler.yml @@ -82,7 +82,8 @@ jobs: RESPONSE_FILE: ${{ steps.classify.outputs.response-file }} PR: ${{ github.event.pull_request.number }} run: | - LABEL=$(jq -r '.label // empty' "$RESPONSE_FILE" 2>/dev/null || tr -d '[:space:]' < "$RESPONSE_FILE" | tr '[:upper:]' '[:lower:]') + 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 ;; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fdb3275..8066dd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,12 @@ on: tags: - 'v*' +permissions: + contents: write + security-events: write + pull-requests: read + models: read + jobs: security: name: Security diff --git a/seed/.github/prompts/classify-pr.prompt.yml b/seed/.github/prompts/classify-pr.prompt.yml index e980961..4ae0c75 100644 --- a/seed/.github/prompts/classify-pr.prompt.yml +++ b/seed/.github/prompts/classify-pr.prompt.yml @@ -19,23 +19,23 @@ messages: When a PR mixes categories: bug > enhancement > documentation. Prefer diff evidence over the PR title. model: openai/gpt-4o-mini -responseFormat: - type: json_schema - json_schema: - name: classification - strict: true - schema: - type: object - properties: - label: - type: string - enum: - - bug - - enhancement - - documentation - required: - - label - additionalProperties: false +responseFormat: json_schema +jsonSchema: |- + { + "name": "classification", + "strict": true, + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "enum": ["bug", "enhancement", "documentation"] + } + }, + "required": ["label"], + "additionalProperties": false + } + } modelParameters: - maxCompletionTokens: 10 + maxCompletionTokens: 25 temperature: 0 diff --git a/seed/.github/workflows/ai-labeler.yml b/seed/.github/workflows/ai-labeler.yml index 9653e8f..c4813f4 100644 --- a/seed/.github/workflows/ai-labeler.yml +++ b/seed/.github/workflows/ai-labeler.yml @@ -82,7 +82,8 @@ jobs: RESPONSE_FILE: ${{ steps.classify.outputs.response-file }} PR: ${{ github.event.pull_request.number }} run: | - LABEL=$(jq -r '.label // empty' "$RESPONSE_FILE" 2>/dev/null || tr -d '[:space:]' < "$RESPONSE_FILE" | tr '[:upper:]' '[:lower:]') + 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 ;; diff --git a/seed/.github/workflows/release.yml b/seed/.github/workflows/release.yml index e9e3c5b..cc9acaa 100644 --- a/seed/.github/workflows/release.yml +++ b/seed/.github/workflows/release.yml @@ -5,6 +5,13 @@ on: tags: - 'v*' +permissions: + contents: write + id-token: write + security-events: write + pull-requests: read + models: read + jobs: security: name: Security scan @@ -293,10 +300,11 @@ jobs: MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} run: | + RELEASE_CHANGELOG="" if [ -n "$CHANGELOG_FILE" ] && [ -f "$CHANGELOG_FILE" ]; then RELEASE_CHANGELOG=$(cat "$CHANGELOG_FILE") - export RELEASE_CHANGELOG fi + export RELEASE_CHANGELOG goreleaser release --clean # Configure secrets.AUR_SSH_KEY to enable Arch Linux AUR publishing