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
188 changes: 188 additions & 0 deletions .github/workflows/migrations-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Migrations CI

# Runs workflow-migrate against an ephemeral Postgres on every PR that touches
# migration driver code. This is the regression gate that catches panics like
# the v0.3.0 collectApplied() overflow before merge.

permissions:
contents: read

on:
pull_request:
paths:
- 'internal/golangmigrate/**'
- 'internal/goose/**'
- 'internal/atlas/**'
- 'cmd/workflow-migrate/**'
- 'pkg/**'
- 'go.mod'
- 'go.sum'
- '.github/workflows/migrations-ci.yml'

jobs:
ephemeral-migrate:
name: workflow-migrate up/down (golang-migrate, timestamp versions)
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci
POSTGRES_DB: ci_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ci"
--health-interval 2s
--health-timeout 5s
--health-retries 20

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build workflow-migrate
run: |
CGO_ENABLED=0 go build -o /tmp/workflow-migrate ./cmd/workflow-migrate

- name: Prepare fixture migrations (timestamp-based — the v0.3.0 regression scenario)
run: |
mkdir -p /tmp/fixture-migrations

cat > /tmp/fixture-migrations/20240101000001_init.up.sql <<'SQL'
CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL);
SQL
cat > /tmp/fixture-migrations/20240101000001_init.down.sql <<'SQL'
DROP TABLE IF EXISTS users;
SQL

cat > /tmp/fixture-migrations/20240201000001_posts.up.sql <<'SQL'
CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INT);
SQL
cat > /tmp/fixture-migrations/20240201000001_posts.down.sql <<'SQL'
DROP TABLE IF EXISTS posts;
SQL

cat > /tmp/fixture-migrations/20240301000001_comments.up.sql <<'SQL'
CREATE TABLE comments (id SERIAL PRIMARY KEY, post_id INT, body TEXT);
SQL
cat > /tmp/fixture-migrations/20240301000001_comments.down.sql <<'SQL'
DROP TABLE IF EXISTS comments;
SQL

- name: Run workflow-migrate up — timestamp-based versions (the v0.3.0 panic scenario)
env:
DATABASE_URL: postgres://ci:ci@localhost:5432/ci_test?sslmode=disable
run: |
/tmp/workflow-migrate up --source-dir /tmp/fixture-migrations

- name: Verify tables created
env:
PGPASSWORD: ci
run: |
PSQL_OUTPUT=$(psql -h localhost -U ci -d ci_test -tAc '\dt')
echo "$PSQL_OUTPUT"
echo "$PSQL_OUTPUT" | grep -q 'users' || { echo "FAIL: users table missing after up"; exit 1; }
echo "$PSQL_OUTPUT" | grep -q 'posts' || { echo "FAIL: posts table missing after up"; exit 1; }
echo "$PSQL_OUTPUT" | grep -q 'comments' || { echo "FAIL: comments table missing after up"; exit 1; }
echo "All 3 tables present after up ✓"

- name: Run workflow-migrate down — rollback smoke test
env:
DATABASE_URL: postgres://ci:ci@localhost:5432/ci_test?sslmode=disable
PGPASSWORD: ci
run: |
/tmp/workflow-migrate down --source-dir /tmp/fixture-migrations
# One migration rolled back — comments should be gone.
REMAINING=$(psql -h localhost -U ci -d ci_test -tAc \
"SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename != 'schema_migrations'")
REMAINING=$(echo "$REMAINING" | tr -d '[:space:]')
test "$REMAINING" = "2" || { echo "FAIL: expected 2 tables after down(1), got $REMAINING"; exit 1; }
echo "Correct table count ($REMAINING) after down(1) ✓"

- name: Run workflow-migrate status
env:
DATABASE_URL: postgres://ci:ci@localhost:5432/ci_test?sslmode=disable
run: |
/tmp/workflow-migrate status --source-dir /tmp/fixture-migrations

ephemeral-migrate-goose:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
name: workflow-migrate up/down (goose driver, sequential versions)
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci
POSTGRES_DB: ci_goose
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ci"
--health-interval 2s
--health-timeout 5s
--health-retries 20

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build workflow-migrate
run: |
CGO_ENABLED=0 go build -o /tmp/workflow-migrate ./cmd/workflow-migrate

- name: Prepare goose fixture migrations
run: |
mkdir -p /tmp/goose-migrations

cat > /tmp/goose-migrations/00001_users.sql <<'SQL'
-- +goose Up
CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL);
-- +goose Down
DROP TABLE IF EXISTS users;
SQL

cat > /tmp/goose-migrations/00002_posts.sql <<'SQL'
-- +goose Up
CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INT);
-- +goose Down
DROP TABLE IF EXISTS posts;
SQL

- name: Run workflow-migrate up (goose driver)
env:
DATABASE_URL: postgres://ci:ci@localhost:5432/ci_goose?sslmode=disable
run: |
/tmp/workflow-migrate up --driver goose --source-dir /tmp/goose-migrations

- name: Verify goose tables created
env:
PGPASSWORD: ci
run: |
PSQL_OUTPUT=$(psql -h localhost -U ci -d ci_goose -tAc '\dt')
echo "$PSQL_OUTPUT"
echo "$PSQL_OUTPUT" | grep -q 'users' || { echo "FAIL: users missing"; exit 1; }
echo "$PSQL_OUTPUT" | grep -q 'posts' || { echo "FAIL: posts missing"; exit 1; }
echo "Goose: both tables present after up ✓"

- name: Run workflow-migrate down (goose driver)
env:
DATABASE_URL: postgres://ci:ci@localhost:5432/ci_goose?sslmode=disable
PGPASSWORD: ci
run: |
/tmp/workflow-migrate down --driver goose --source-dir /tmp/goose-migrations
REMAINING=$(psql -h localhost -U ci -d ci_goose -tAc \
"SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename != 'goose_db_version'")
REMAINING=$(echo "$REMAINING" | tr -d '[:space:]')
test "$REMAINING" = "1" || { echo "FAIL: expected 1 table after goose down(1), got $REMAINING"; exit 1; }
echo "Goose: correct table count ($REMAINING) after down(1) ✓"
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
26 changes: 26 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
version: "2"

run:
timeout: 5m

linters:
default: none
enable:
- errcheck
- govet
- staticcheck
- unused

issues:
exclusions:
rules:
# defer Close() calls — these are cleanup paths where errors are
# unactionable and the pattern is idiomatic Go (pre-existing).
- linters: [errcheck]
text: "Error return value of.*\\.Close"
# fmt.Fprintln to stdout in CLI test helper (pre-existing).
- linters: [errcheck]
text: "Error return value of `fmt\\.Fprintln`"
# goreleaser-injected version vars set via -ldflags at build time.
- linters: [unused]
text: "var version is unused"
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.1] - 2026-04-24

### Fixed

- **P0: golang-migrate driver panics on timestamp-based migration versions** — `collectApplied(before, after uint)` pre-allocated `make([]string, 0, after-before)`, assuming sequential version numbers. BMW uses timestamp-based versions (e.g. `20240101000001`), so `after-before = 2×10¹³` → `makeslice: cap out of range` panic. **`v0.3.0` is defective and should not be used with timestamp-based migrations.** The fix replaces the integer-range loops in `Up()` and `Down()` with file-system source walking (the same pattern already used by `listPendingVersions`). New shared helper: `versionsInRange(dir, lo, hi, loNil)`.

### Added

- **`migrations-ci.yml` GHA workflow** — runs `workflow-migrate up` + `down` against ephemeral Postgres:16 on every PR touching migration driver code, using timestamp-based fixture migrations. This is the prevention mechanism that would have caught v0.3.0's panic before release.
- **`.golangci.yml`** — adds golangci-lint v2 config (previously absent) with errcheck exclusions for idiomatic `defer .Close()` patterns.

## [0.3.0] - 2026-04-24

### Added
Expand Down
2 changes: 1 addition & 1 deletion cmd/workflow-plugin-atlas-migrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// version is set at build time via -X main.version=<version>.
var version string
var version string //nolint:unused

func main() {
sdk.Serve(atlasplugin.NewPlugin())
Expand Down
2 changes: 1 addition & 1 deletion cmd/workflow-plugin-migrations/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

// version is set at build time via -X main.version=<version>.
var version string
var version string //nolint:unused

func main() {
sdk.ServePluginFull(internal.NewPlugin(), cli.NewCLIProvider(), nil)
Expand Down
2 changes: 1 addition & 1 deletion internal/atlas/revisions.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (r *sqlRevisionRW) ReadRevisions(ctx context.Context) ([]*atlmigrate.Revisi
if err != nil {
return nil, err
}
defer rows.Close()
defer rows.Close() //nolint:errcheck
return scanRevisions(rows)
}

Expand Down
Loading
Loading