Bump golang from 1.26.4-alpine3.23 to 1.26.5-alpine3.23 #253
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: Build and Release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - dev | |
| pull_request: | |
| branches: | |
| - main | |
| - dev | |
| jobs: | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| # docs/TEST_PLAN_2.md Phase 1: pull-requests: write lets the | |
| # "Post coverage comment" step post the per-PR delta. The default | |
| # GITHUB_TOKEN has only read access to PRs on pull_request events | |
| # from forks; the build job (which doesn't run on PRs anyway) | |
| # carries its own contents: write override. | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6 | |
| # docs/TEST_PLAN_2.md Phase 1: fetch full history so the | |
| # "Compute coverage delta" step can check out the PR's base | |
| # branch to re-run tests against it. | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| # Pinned to match go.mod's `go` directive and the GitLab test | |
| # job. Bump in lockstep with that file when the toolchain | |
| # upgrades. | |
| go-version: '1.25.8' | |
| - name: Verify Go Modules | |
| run: go mod verify | |
| - name: Vet | |
| run: go vet ./... | |
| # docs/TEST_PLAN_2.md Phase 4: fail before `go test` if any | |
| # top-level Test function in a non-annotated file omits | |
| # t.Parallel(). Cheap pre-test gate; the alternative is finding | |
| # out via a flaky -race failure once enough state accumulates. | |
| - name: Check test parallelism | |
| run: bash scripts/check-parallel.sh | |
| # docs/TEST_PLAN.md Phase 5 retired the legacy subprocess test suite | |
| # so `go test ./...` now runs fully in-process. No pre-built binary | |
| # required. | |
| # | |
| # Phase 6c: -coverpkg=./... credits cross-package coverage so that | |
| # integration tests boot via app.NewEngine — and the handler/model | |
| # code they exercise — count toward the totals. Without this flag | |
| # Go only credits each package's tests for its own package, which | |
| # left app/ at 0.0% and understated handlers/ and model/. | |
| # -timeout=20m: -race + -coverpkg=./... together slow each package's | |
| # tests by ~2.5-3x. handlers/ and tests/integration/ run ~10-12m | |
| # under this combination, which exceeds Go's default 10m per-package | |
| # timeout. | |
| - name: Test | |
| run: go test -race -coverpkg=./... -coverprofile=coverage.out -timeout=20m ./... | |
| - name: Upload coverage | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage | |
| path: coverage.out | |
| - name: Coverage summary | |
| if: always() | |
| run: go tool cover -func=coverage.out | tail -1 | |
| # docs/TEST_PLAN_2.md Phase 1: gate on a documented coverage floor so | |
| # PRs that drop the bar fail review at CI time, not in a follow-up | |
| # audit. Floor is floor(total - 1.0) where total is the honest | |
| # `-coverpkg=./...` reading from this same job on the dev branch | |
| # — see audits/TESTING.md "Phase 6c recalibration" for why the | |
| # honest cross-tree number (~12%) is the right denominator and | |
| # not the per-package number (~55%) `go test -cover` reports | |
| # without `-coverpkg=./...`. Bump the floor in PRs that earn it; | |
| # never lower it (see CONTRIBUTING.md "Coverage floor"). | |
| - name: Enforce coverage floor | |
| run: | | |
| total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') | |
| floor=11.0 | |
| awk -v t="$total" -v f="$floor" 'BEGIN { exit !(t+0 >= f+0) }' \ | |
| || { echo "::error::coverage $total% < floor $floor%"; exit 1; } | |
| echo "coverage $total% >= floor $floor%" | |
| # docs/TEST_PLAN_2.md Phase 1: surface a coverage delta as a PR | |
| # comment so reviewers see the change at review time. The base | |
| # coverage is recomputed by checking out the target branch and | |
| # re-running tests; this avoids depending on an external artifact | |
| # store and works for forks (which can't read the repo's artifact | |
| # bucket). | |
| - name: Compute coverage delta | |
| if: github.event_name == 'pull_request' | |
| id: coverage_delta | |
| run: | | |
| pr_total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') | |
| mv coverage.out coverage-pr.out | |
| git fetch origin "$GITHUB_BASE_REF" | |
| git -c advice.detachedHead=false checkout "origin/$GITHUB_BASE_REF" -- . | |
| go test -coverpkg=./... -coverprofile=coverage-base.out -timeout=20m ./... > /dev/null 2>&1 || true | |
| if [ -s coverage-base.out ]; then | |
| base_total=$(go tool cover -func=coverage-base.out | awk '/^total:/ {gsub("%","",$3); print $3}') | |
| else | |
| base_total="0.0" | |
| fi | |
| delta=$(awk -v p="$pr_total" -v b="$base_total" 'BEGIN { printf "%+.2f", p-b }') | |
| echo "pr=$pr_total base=$base_total delta=$delta" | |
| echo "comment=Coverage: ${pr_total}% (base ${base_total}%, delta ${delta}%)" >> "$GITHUB_OUTPUT" | |
| # Restore the PR's checkout so subsequent steps see PR code. | |
| git checkout -- . | |
| mv coverage-pr.out coverage.out | |
| - name: Post coverage comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: '${{ steps.coverage_delta.outputs.comment }}' | |
| }) | |
| test-postgres: | |
| name: Test (Postgres) | |
| runs-on: ubuntu-latest | |
| # docs/TEST_PLAN.md Phase 5: every dialect-branching path runs against | |
| # real PostgreSQL on every PR. Without this matrix the project's | |
| # second supported backend has only ever been exercised against a | |
| # SetDriverForTesting flag, not a real server. | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: postgres | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.25.8' | |
| - name: Verify Go Modules | |
| run: go mod verify | |
| # docs/TEST_PLAN_2.md Phase 4: same parallelism lint as the | |
| # SQLite test job — keeps the postgres job from passing while a | |
| # non-annotated test slips through. | |
| - name: Check test parallelism | |
| run: bash scripts/check-parallel.sh | |
| # `-run Postgres` keeps the SQLite-only tests in tests/integration, | |
| # handlers, and watcher out of the test binary's run set: under | |
| # the integration_postgres tag both kinds of tests compile in, | |
| # but they share the model.dbDriver global via SetDriverForTesting. | |
| # Filtering by name is the contract that lets them coexist in | |
| # one package — every Postgres test in the watcher and handlers | |
| # packages added by docs/TEST_PLAN_2.md Phase 7 carries "Postgres" | |
| # in its function name so this filter still picks them up. | |
| - name: Test (Postgres) | |
| run: go test -race -tags=integration_postgres -coverpkg=./... -coverprofile=coverage-postgres.out -run Postgres -timeout=20m ./tests/integration/... ./model/... ./handlers/... ./watcher/... | |
| env: | |
| ISLEY_TEST_DB_HOST: localhost | |
| ISLEY_TEST_DB_PORT: '5432' | |
| ISLEY_TEST_DB_USER: postgres | |
| ISLEY_TEST_DB_PASSWORD: test | |
| - name: Coverage summary (Postgres) | |
| if: always() | |
| run: go tool cover -func=coverage-postgres.out | tail -1 | |
| # docs/TEST_PLAN_2.md Phase 1: this floor was originally set when | |
| # `test-postgres` only exercised smoke + migrations + | |
| # sqlite_to_postgres (a small fraction of the codebase). Phase 7 | |
| # added dialect-branched handler/watcher tests to this job; once | |
| # the first dev-branch run reports the post-Phase-7 number, raise | |
| # this floor to floor(total - 1.0) per CONTRIBUTING.md "Coverage | |
| # floor". Until then the existing 2.0 holds — bumping speculatively | |
| # would risk false negatives. Bump on earn, never lower. | |
| - name: Enforce coverage floor (Postgres) | |
| run: | | |
| total=$(go tool cover -func=coverage-postgres.out | awk '/^total:/ {gsub("%","",$3); print $3}') | |
| floor=2.0 | |
| awk -v t="$total" -v f="$floor" 'BEGIN { exit !(t+0 >= f+0) }' \ | |
| || { echo "::error::postgres coverage $total% < floor $floor%"; exit 1; } | |
| echo "postgres coverage $total% >= floor $floor%" | |
| build: | |
| name: Build and Release Artifacts | |
| needs: [test, test-postgres] | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # Required for creating releases | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v6 | |
| with: | |
| # fetch full history and tags so git describe and tag pushes work correctly | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.25.8' | |
| - name: Verify Go Modules | |
| run: go mod verify | |
| - name: Check Translation Keys | |
| run: | | |
| # Run i18n_sync in dry-run mode; if any output is produced, keys are missing | |
| output=$(go run ./scripts/i18n_sync.go -path=utils/locales 2>&1) | |
| if [ -n "$output" ]; then | |
| echo "$output" | |
| echo "::error::Translation keys are out of sync. Run: go run ./scripts/i18n_sync.go -path=utils/locales -apply" | |
| exit 1 | |
| fi | |
| - name: Set Version | |
| id: version | |
| run: | | |
| # On main use the VERSION file (fail if missing); on other branches, use 'dev' | |
| if [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| if [ -f VERSION ]; then | |
| echo "version=$(cat VERSION)" >> $GITHUB_ENV | |
| else | |
| echo "VERSION file is missing on main branch" >&2 | |
| exit 1 | |
| fi | |
| else | |
| echo "version=dev" >> $GITHUB_ENV | |
| fi | |
| - name: Print Version | |
| run: echo "Building version ${{ env.version }}" | |
| - name: Preflight version check (Main Branch Only) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| VERSION="${{ env.version }}" | |
| TAG="v$VERSION" | |
| # Strict SemVer (MAJOR.MINOR.PATCH with optional pre-release/build). | |
| if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then | |
| echo "::error::VERSION '$VERSION' is not strict SemVer (MAJOR.MINOR.PATCH[-prerelease][+build]). Bump VERSION before pushing to main." | |
| exit 1 | |
| fi | |
| # Reject if the release tag already exists. | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "::error::Tag $TAG already exists. Bump VERSION before pushing to main." | |
| exit 1 | |
| fi | |
| # Require a matching CHANGELOG section. Heading format: '## [X.Y.Z] - YYYY-MM-DD' | |
| if [ ! -f CHANGELOG.md ]; then | |
| echo "::error::CHANGELOG.md is missing." | |
| exit 1 | |
| fi | |
| if ! grep -Eq "^## \[$(echo "$VERSION" | sed -e 's/[.+]/\\&/g')\] - [0-9]{4}-[0-9]{2}-[0-9]{2}" CHANGELOG.md; then | |
| echo "::error::CHANGELOG.md has no stamped section for [$VERSION]. Run scripts/cut-release.sh $VERSION on a release/v$VERSION branch." | |
| exit 1 | |
| fi | |
| echo "Preflight OK: $TAG is new, well-formed, and has a CHANGELOG entry." | |
| - name: Build Linux Binary | |
| run: | | |
| GOOS=linux GOARCH=amd64 go build -ldflags="-w -s -X main.version=${{ env.version }}" -o isley-linux | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Set Docker Tags | |
| id: docker_tags | |
| run: | | |
| if [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| echo "tags=dwot/isley:${{ env.version }},dwot/isley:latest" >> $GITHUB_OUTPUT | |
| else | |
| SHORT_SHA=${GITHUB_SHA::8} | |
| echo "tags=dwot/isley:dev,dwot/isley:dev-${SHORT_SHA}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and Push Multi-Platform Docker Image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.docker_tags.outputs.tags }} | |
| labels: | | |
| org.opencontainers.image.title=isley | |
| org.opencontainers.image.source=https://github.com/dwot/isley | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.version=${{ env.version }} | |
| org.opencontainers.image.ref.name=${{ github.ref_name }} | |
| org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Extract Release Notes from CHANGELOG (Main Branch Only) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| VERSION="${{ env.version }}" | |
| # Escape regex metacharacters in the version so '.' etc. match literally. | |
| VERSION_RE=$(echo "$VERSION" | sed -e 's/[][(){}.*+?|^$\\]/\\&/g') | |
| # Extract the CHANGELOG section for this version. Stops at the next | |
| # top-level heading (## ...) or end of file. The heading itself is | |
| # dropped — the release UI already shows the version/tag. | |
| awk -v ver_re="$VERSION_RE" ' | |
| $0 ~ "^## \\[" ver_re "\\]" { found=1; next } | |
| found && /^## / { exit } | |
| found { print } | |
| ' CHANGELOG.md > release_notes.md | |
| if [ ! -s release_notes.md ]; then | |
| echo "::error::Extracted release notes for $VERSION are empty. Check CHANGELOG.md formatting." | |
| exit 1 | |
| fi | |
| echo "---- release_notes.md ----" | |
| cat release_notes.md | |
| echo "--------------------------" | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@v4.1.2 | |
| - name: Sign Docker Images | |
| env: | |
| TAGS: ${{ steps.docker_tags.outputs.tags }} | |
| COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} | |
| COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} | |
| run: | | |
| for tag in ${TAGS//,/ }; do | |
| cosign sign --yes --key env://COSIGN_PRIVATE_KEY "$tag" | |
| done | |
| - name: Import GPG Key | |
| uses: crazy-max/ghaction-import-gpg@v7 | |
| with: | |
| gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} | |
| passphrase: ${{ secrets.GPG_PASSPHRASE }} | |
| # When passphrase is supplied, the action configures loopback | |
| # pinentry and a passphrase-injecting gpg wrapper for us, so | |
| # `git tag -s` and `git commit -S` work non-interactively. | |
| git_user_signingkey: true | |
| git_tag_gpgsign: true | |
| - name: Create and push tag (Main Branch Only) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| TAG="v${{ env.version }}" | |
| # Sign binary with Cosign (Shared Trust). --yes auto-accepts the | |
| # Rekor transparency-log upload prompt, which has no TTY in CI. | |
| # cosign v3 (installed via cosign-installer@v4) defaults to the new | |
| # Sigstore bundle format: --output-signature/--output-certificate are | |
| # gone and --bundle (cert + signature + tlog entry in one | |
| # .sigstore.json) is required. Verify with: | |
| # cosign verify-blob --bundle isley-linux.bundle --key cosign.pub isley-linux | |
| cosign sign-blob --yes --key env://COSIGN_PRIVATE_KEY --bundle isley-linux.bundle isley-linux | |
| sha256sum isley-linux > isley-linux.sha256 | |
| # Use our release identity | |
| git config user.name "isley-release-bot" | |
| git config user.email "68145+dwot@users.noreply.github.com" | |
| git tag -s "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| env: | |
| COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} | |
| COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} | |
| - name: Release Artifacts (Main Branch Only) | |
| if: github.ref == 'refs/heads/main' | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: v${{ env.version }} | |
| name: v${{ env.version }} | |
| body_path: release_notes.md | |
| files: | | |
| isley-linux | |
| isley-linux.bundle | |
| isley-linux.sha256 |