Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/prompts/classify-pr.prompt.yml
Review prompt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ messages:
- role: system
Comment thread
jeremy marked this conversation as resolved.
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.
Expand All @@ -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: 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
15 changes: 8 additions & 7 deletions .github/workflows/ai-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,13 +79,15 @@ 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 || 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
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
Expand All @@ -105,9 +107,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"
Expand Down Expand Up @@ -184,8 +185,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
Expand All @@ -202,7 +204,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/^/- /')
Expand Down
62 changes: 41 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ on:

permissions:
contents: write
id-token: write # Required for keyless cosign signing via OIDC
security-events: write # Required for SARIF upload in security scan
id-token: write
security-events: write
pull-requests: read
models: read

jobs:
security:
name: Security scan
uses: ./.github/workflows/security.yml
Comment thread
jeremy marked this conversation as resolved.
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:
BASECAMP_NO_KEYRING: "1"
GOPRIVATE: github.com/basecamp/basecamp-sdk
Expand All @@ -44,7 +47,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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
Expand Down Expand Up @@ -110,6 +115,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 45
environment: release
permissions:
contents: write
id-token: write
models: read
env:
GOPRIVATE: github.com/basecamp/basecamp-sdk
steps:
Expand All @@ -132,7 +141,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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: |
Expand Down Expand Up @@ -224,21 +235,14 @@ jobs:
with:
prompt-file: /tmp/prompt.yml

- name: Set changelog env
- name: Set changelog path
id: changelog
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"
Comment thread
jeremy marked this conversation as resolved.
fi

- name: Verify macOS signing secrets
Expand All @@ -262,20 +266,30 @@ 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 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
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: |
RELEASE_CHANGELOG=""
if [ -n "$CHANGELOG_FILE" ] && [ -f "$CHANGELOG_FILE" ]; then
Comment thread
jeremy marked this conversation as resolved.
RELEASE_CHANGELOG=$(cat "$CHANGELOG_FILE")
fi
export RELEASE_CHANGELOG
goreleaser release --clean

- name: Publish to AUR
env:
Expand All @@ -297,6 +311,8 @@ jobs:
continue-on-error: true
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

Expand All @@ -318,6 +334,8 @@ jobs:
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

Expand All @@ -342,9 +360,11 @@ jobs:
if: failure()
env:
GH_TOKEN: ${{ steps.skills-token.outputs.token }}
REF_NAME: ${{ github.ref_name }}
RUN_ID: ${{ github.run_id }}
run: |
TITLE="Skills sync failure"
BODY="The automatic skills sync from [basecamp-cli ${{ github.ref_name }}](https://github.com/basecamp/basecamp-cli/actions/runs/${{ github.run_id }}) failed. Check the workflow run for details."
BODY="The automatic skills sync from [basecamp-cli ${REF_NAME}](https://github.com/basecamp/basecamp-cli/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)
Expand All @@ -355,4 +375,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/basecamp/basecamp-cli/actions/runs/${{ github.run_id }}"
echo "::error::Skills sync to basecamp/skills failed for ${REF_NAME}. See https://github.com/basecamp/basecamp-cli/actions/runs/${RUN_ID}"
11 changes: 9 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
jeremy marked this conversation as resolved.
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -82,7 +85,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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: |
Expand Down Expand Up @@ -130,7 +135,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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
Expand Down
28 changes: 21 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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 SDK provenance
run: make provenance-check
Expand Down Expand Up @@ -80,7 +82,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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
Expand Down Expand Up @@ -110,7 +114,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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
Expand Down Expand Up @@ -143,7 +149,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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 ./...
Expand Down Expand Up @@ -172,7 +180,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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
Expand Down Expand Up @@ -216,7 +226,9 @@ jobs:
go-version-file: 'go.mod'

- name: Configure git for private modules
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: |
Expand Down Expand Up @@ -281,7 +293,9 @@ jobs:

- name: Configure git for private modules
if: steps.filter.outputs.bench == 'true'
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'
Expand Down
Loading