Skip to content

Bump golang from 1.26.4-alpine3.23 to 1.26.5-alpine3.23 #253

Bump golang from 1.26.4-alpine3.23 to 1.26.5-alpine3.23

Bump golang from 1.26.4-alpine3.23 to 1.26.5-alpine3.23 #253

Workflow file for this run

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