Files
go-telegram/.github/workflows/ci.yml
T
lukaszraczylo 53afd9890f ci: pin govulncheck to v1.7.0
govulncheck@latest resolves to v1.8.0, which declares go 1.26.0 in its
module file. Every job in this workflow pins go-version 1.25.x, so the
install step fails before any scanning runs, which fails ci-success and
skips the release job.

v1.7.0 declares go 1.25.0 and installs on the pinned toolchain. This also
matches the convention used for staticcheck, which is already pinned to
an exact version. Pinning the binary does not freeze the vulnerability
database, which govulncheck fetches at run time.

Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U
2026-09-15 11:34:33 +01:00

275 lines
9.5 KiB
YAML

name: ci
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
dry-run-release:
description: "Compute release version, do not tag or release"
type: boolean
default: false
permissions:
contents: write
pull-requests: read
packages: write
# Cancel any in-flight CI runs for the same branch when a new push lands.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go vet ./...
staticcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install honnef.co/go/tools/cmd/[email protected]
- run: staticcheck ./...
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
# Pinned, matching staticcheck above. govulncheck@latest is v1.8.0,
# which declares go 1.26.0 and therefore cannot be installed by the
# 1.25.x toolchain this workflow pins - the install step fails before
# any scanning happens. v1.7.0 declares go 1.25.0. Pinning the binary
# does not freeze the vulnerability database, which govulncheck
# fetches at run time. Re-pin deliberately when bumping go-version.
- run: go install golang.org/x/vuln/cmd/[email protected]
- run: govulncheck ./...
gosec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install github.com/securego/gosec/v2/cmd/gosec@latest
# G404: math/rand/v2 jitter in transport/backoff.go — intentional (not crypto)
# G304: os.ReadFile from CLI flag variable — intentional (tool, not server)
# G306: 0o644 on generated doc artifacts in cmd/scrape — intentional
# G204: git subprocess in cmd/audit uses CLI flag path — intentional (operator tool)
# G706: log.Printf with values from Telegram/env in examples — illustrative,
# library users are expected to sanitise before logging in production
- run: gosec -quiet -exclude=G404,G304,G306,G204,G706 -exclude-dir=testdata ./...
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go test -race -coverprofile=coverage.out ./...
- name: Build all examples
run: go build ./examples/...
- uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage.out
codegen-clean:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- name: Regenerate against pinned snapshot
run: make regen-from-fixture
- name: Assert clean diff
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # need history for drift comparison
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- name: Audit fallbacks
run: make audit
- name: Audit drift vs base
# On PRs: compare against the merge base (origin/<base>).
# On push to main: compare against the parent commit.
# Drift is informational; doesn't fail CI.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="origin/${{ github.base_ref }}"
else
BASE="HEAD~1"
fi
echo "Drift base: $BASE"
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
# Aggregate gate — depends on every check above. Used as a single
# required status check in branch protection AND as a dependency for the
# release job below.
ci-success:
runs-on: ubuntu-latest
needs: [vet, staticcheck, govulncheck, gosec, test, codegen-clean, audit]
if: always()
steps:
- name: All checks passed
if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: echo "ci-success"
- name: At least one check failed
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "Failed/cancelled jobs:"
echo '${{ toJSON(needs) }}'
exit 1
# Auto-release fires on every clean push to main (and on manual
# workflow_dispatch for testing). Computes next SemVer from commit
# history via lukaszraczylo/semver-generator, dual-tags
# (v<X.Y.Z> + bot-api-v<A.B>), runs GoReleaser.
release:
needs: ci-success
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
# Action interface from
# https://github.com/lukaszraczylo/semver-generator/blob/main/action.yml
# Inputs: repository_local: true (use already-cloned repo)
# existing: true (respect existing tags as base)
# Output: semantic_version (bare version string, no "v" prefix)
- name: Compute next SemVer
id: semver
uses: lukaszraczylo/semver-generator@v1
with:
repository_local: true
existing: true
config_file: .semver.yaml
- name: Read Bot API version
id: api_version
run: |
VERSION=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "tag=" >> "$GITHUB_OUTPUT"
else
echo "tag=bot-api-v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Dry-run summary
if: github.event_name == 'workflow_dispatch' && inputs.dry-run-release == true
run: |
echo "Would release: v${{ steps.semver.outputs.semantic_version }}"
if [ -n "${{ steps.api_version.outputs.tag }}" ]; then
echo "Would also tag: ${{ steps.api_version.outputs.tag }} (Bot API ${{ steps.api_version.outputs.version }})"
fi
echo "Skipping tag + release (dry-run)."
- name: Tag library + bot-api versions
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
env:
LIB_TAG: v${{ steps.semver.outputs.semantic_version }}
API_TAG: ${{ steps.api_version.outputs.tag }}
API_VER: ${{ steps.api_version.outputs.version }}
run: |
# Bot API version (currently $API_VER) is intentionally NOT
# tagged separately. semver-generator picks the most recent
# tag as the version base; a non-SemVer marker like
# bot-api-vX.Y poisons that and restarts numbering from
# v0.0.x. Bot API version stays as a comment in the lib tag
# message and in the release notes.
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$LIB_TAG" -m "Release $LIB_TAG (Bot API $API_VER)"
git push origin "$LIB_TAG"
- name: Run GoReleaser
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BOT_API_VERSION: ${{ steps.api_version.outputs.version }}