ci: read the Go version from go.mod instead of six literal pins #1128
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test: | |
| strategy: | |
| matrix: | |
| platform: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - name: Set git to use LF | |
| run: git config --global core.autocrlf false | |
| - uses: actions/checkout@v7 | |
| # Single source of truth for the Go version: go.mod's `go` directive. | |
| # setup-go exports GOTOOLCHAIN=local, so the installed toolchain cannot | |
| # upgrade itself to satisfy go.mod — a literal pin here that drifts below | |
| # that directive is a hard build failure, not a silent downgrade. Reading | |
| # the directive means the two cannot disagree. | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| - name: Build | |
| run: make build | |
| - name: Test | |
| run: make test | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: golangci/golangci-lint-action@v9 | |
| with: | |
| version: v2.11.4 | |
| pr-template: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Validate PR template | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: pip install requests && python .github/scripts/check_pr_template.py | |
| secrets: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install gitleaks | |
| run: | | |
| GITLEAKS_VERSION="8.28.0" | |
| cd /tmp | |
| curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o gitleaks.tar.gz | |
| tar xzf gitleaks.tar.gz gitleaks | |
| sudo mv gitleaks /usr/local/bin/ | |
| rm -f gitleaks.tar.gz | |
| - name: Run gitleaks | |
| run: gitleaks detect --source . --redact=10 --verbose | |
| govulncheck: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| - name: Install govulncheck | |
| run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 | |
| - name: Run govulncheck | |
| # Allowlist GO-2026-5932 ONLY: golang.org/x/crypto/openpgp is unmaintained | |
| # ("Fixed in: N/A") and pulled in transitively by go-selfupdate for a PGP | |
| # validator the CLI does not use — the updater verifies SHA256 checksums | |
| # (ChecksumValidator), not PGP, so openpgp is never reached with untrusted | |
| # input. Any OTHER advisory still fails this job. Removal tracked in API-566. | |
| run: | | |
| out=$(govulncheck ./... 2>&1) && rc=0 || rc=$? | |
| echo "$out" | |
| if [ "$rc" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| # govulncheck exits 3 when vulnerabilities are found; any other non-zero | |
| # code is a tool/build failure and must not be swallowed by the allowlist. | |
| if [ "$rc" -ne 3 ]; then | |
| echo "::error::govulncheck failed to run (exit $rc)" | |
| exit 1 | |
| fi | |
| ids=$(printf '%s\n' "$out" | grep -oE 'GO-[0-9]{4}-[0-9]+' | sort -u) | |
| others=$(printf '%s\n' "$ids" | grep -vx 'GO-2026-5932' || true) | |
| if [ -z "$ids" ] || [ -n "$others" ]; then | |
| echo "::error::Unallowlisted or unrecognized govulncheck result (ids: ${ids:-none})" | |
| exit 1 | |
| fi | |
| echo "Only the allowlisted advisory GO-2026-5932 is present; passing." | |
| goreleaser-check: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: goreleaser/goreleaser-action@v7 | |
| with: | |
| version: "~> v2" | |
| args: check | |
| # Reports what a PR does to the generated command surface. Advisory only: it | |
| # never fails, because reading the result needs judgment CI cannot supply — a | |
| # removal can be correct when the same PR adds a deprecated alias. Blocking | |
| # would also deadlock the codegen sync bot, which cannot add that alias itself. | |
| surface-report: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # Full history: the report diffs against the base commit. | |
| fetch-depth: 0 | |
| - name: Report command-surface changes | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -uo pipefail | |
| # Only speak up when the generated surface could have moved. | |
| if git diff --quiet "$BASE_SHA" HEAD -- gen/; then | |
| echo "gen/ untouched; nothing to report." | |
| exit 0 | |
| fi | |
| marker="<!-- command-surface-report -->" | |
| if ! scripts/release-surface.sh report "$BASE_SHA" HEAD > /tmp/surface-report.md 2>/tmp/surface-err.txt; then | |
| # Say so in the comment rather than only in the log. A silent | |
| # failure leaves the previous report sitting there looking current, | |
| # and the check stays green either way. | |
| { | |
| echo "$marker" | |
| echo "### Command surface: report failed to generate" | |
| echo | |
| echo "This check could not read the surface, so it is telling you nothing about this PR." | |
| echo "Run \`scripts/release-surface.sh diff <base> HEAD\` locally before merging." | |
| echo | |
| echo '```' | |
| tail -5 /tmp/surface-err.txt 2>/dev/null || true | |
| echo '```' | |
| } > /tmp/surface-report.md | |
| echo "::warning::surface report failed to generate; posting a failure notice, not blocking the PR" | |
| fi | |
| cat /tmp/surface-report.md >> "$GITHUB_STEP_SUMMARY" | |
| # Update this job's own previous comment rather than posting a new one | |
| # on every push. Keyed on the marker the report emits, not on author or | |
| # position, so interleaved comments cannot confuse it. | |
| # --paginate because issue comments come back 30 per page by default, | |
| # so on a busy PR the marker falls off page one and this would post a | |
| # duplicate every push - the exact thing the marker exists to prevent. | |
| # --jq runs once PER PAGE, so emit every match and take the last | |
| # overall; a per-page `last` would concatenate ids into a broken URL. | |
| existing=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" \ | |
| --jq ".[] | select(.body | contains(\"$marker\")) | .id" 2>/dev/null | tail -1 || true) | |
| if [ -n "$existing" ]; then | |
| gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${existing}" \ | |
| -F body=@/tmp/surface-report.md >/dev/null \ | |
| && echo "updated comment $existing" \ | |
| || echo "::warning::could not update the surface comment (fork PR?); summary still written" | |
| else | |
| gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ | |
| -F body=@/tmp/surface-report.md >/dev/null \ | |
| && echo "posted surface comment" \ | |
| || echo "::warning::could not post the surface comment (fork PR?); summary still written" | |
| fi |