Skip to content

fix: v0.3.1 — timestamp-version panic in collectApplied (BMW P0) - #5

Merged
intel352 merged 5 commits into
mainfrom
fix/v0.3.1-timestamp-versions
Apr 24, 2026
Merged

intel352 merged 5 commits into
mainfrom
fix/v0.3.1-timestamp-versions

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

  • P0 fix: 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.
  • Fix: Delete collectApplied. Introduce versionsInRange(dir, lo, hi, loNil) that walks the file source directory (same pattern as the existing listPendingVersions). No integer arithmetic for capacity allocation. Fix same bug class in Down() at the integer-range loops.
  • GHA: Add migrations-ci.yml — runs workflow-migrate up/down against ephemeral Postgres:16 on every PR using timestamp-based fixture migrations. This is the prevention gate that would have caught v0.3.0's panic before release.

Pre-fix panic (confirmed locally, postgres:16-alpine)

panic: runtime error: makeslice: cap out of range [recovered, repanicked]
collectApplied(0x0, 0x12688ff38541)
  internal/golangmigrate/driver.go:182 +0x54
(*Driver).Up(...)
  internal/golangmigrate/driver.go:47 +0x2bc

Post-fix test output

=== RUN TestDriver_Up_TimestampVersions
    applied: [20240101000001 20240201000001 20240301000001]
    Status after Up: current=20240301000001 pending=[]
--- PASS (0.14s)
=== RUN TestDriver_Down_TimestampVersions
    rolled back: [20240301000001]
    Status after Down: current=20240201000001 pending=[20240301000001]
--- PASS (0.13s)
=== RUN TestDriver_Down_TimestampVersions_ToNil
    rolled back: [20240301000001 20240201000001 20240101000001]
--- PASS (0.12s)
GOWORK=off go test -race -count=1 ./...
ok  github.com/GoCodeAlone/workflow-plugin-migrations/internal          2.890s
ok  github.com/GoCodeAlone/workflow-plugin-migrations/internal/atlas    2.188s
ok  github.com/GoCodeAlone/workflow-plugin-migrations/internal/golangmigrate  2.312s
ok  github.com/GoCodeAlone/workflow-plugin-migrations/internal/goose   1.601s
ok  github.com/GoCodeAlone/workflow-plugin-migrations/internal/steps    3.914s
ok  github.com/GoCodeAlone/workflow-plugin-migrations/pkg/conformance  3.983s
ok  github.com/GoCodeAlone/workflow-plugin-migrations/pkg/lint         1.487s

Verified locally against postgres:16-alpine via docker; test panics on pre-fix code and passes on fixed code.

Commit structure

  1. fa9ecbb — Regression test only (pre-fix — panics). Committed before fix so regression invariant is independently verifiable.
  2. fec39f4 — Fix: delete collectApplied, introduce versionsInRange
  3. e784a17 — migrations-ci.yml GHA workflow (2 jobs: golang-migrate timestamp + goose sequential)
  4. 42fdf01 — .golangci.yml + //nolint annotations for pre-existing patterns + CHANGELOG v0.3.1

Test plan

  • TestDriver_Up_TimestampVersions panics on fa9ecbb, passes on fec39f4 (code-reviewer verified independently)
  • TestDriver_Down_TimestampVersions passes post-fix
  • TestDriver_Down_TimestampVersions_ToNil passes post-fix
  • GOWORK=off go test -race -count=1 ./... — all packages pass
  • golangci-lint run — 0 issues
  • migrations-ci.yml YAML valid (python3 yaml.safe_load)
  • Code-reviewer approved (regression invariant live-verified)

🤖 Generated with Claude Code

intel352 and others added 4 commits April 24, 2026 19:07
…panics)

Add driver_timestamp_test.go with 3 integration tests using BMW-style
timestamp-based versions (20240101000001, 20240201000001, 20240301000001):
- TestDriver_Up_TimestampVersions
- TestDriver_Down_TimestampVersions
- TestDriver_Down_TimestampVersions_ToNil

PRE-FIX PANIC (confirmed locally against postgres:16-alpine):

  panic: runtime error: makeslice: cap out of range [recovered, repanicked]
  collectApplied(0x0, 0x12688ff38541)
    internal/golangmigrate/driver.go:182 +0x54
  (*Driver).Up(...)
    internal/golangmigrate/driver.go:47 +0x2bc

The cap out of range occurs because collectApplied(before=0, after=20240101000001)
allocates make([]string, 0, 20240101000001) — a ~2×10^13 element slice.

This test must be committed before the fix so code-reviewer can verify the
regression-catches-test invariant independently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sInRange

Root cause: 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^13 → makeslice: cap out
of range panic. Same bug appeared in Down() with integer-range loops.

Fix: introduce versionsInRange(dir, lo, hi, loNil) that walks the file
source (same pattern as the existing listPendingVersions helper) to
enumerate only versions actually present in the source directory.
Integer arithmetic is never used for capacity allocation.

- Up(): replace collectApplied(before, after) with versionsInRange
- Down(): replace integer-range loops with versionsInRange + reverse slice
- Delete collectApplied() entirely

POST-FIX test output (postgres:16-alpine, same harness):

  === RUN TestDriver_Up_TimestampVersions
      applied: [20240101000001 20240201000001 20240301000001]
      Status after Up: current=20240301000001 pending=[]
  --- PASS (0.14s)
  === RUN TestDriver_Down_TimestampVersions
      rolled back: [20240301000001]
      Status after Down: current=20240201000001 pending=[20240301000001]
  --- PASS (0.13s)
  === RUN TestDriver_Down_TimestampVersions_ToNil
      rolled back: [20240301000001 20240201000001 20240101000001]
  --- PASS (0.12s)

Full suite: GOWORK=off go test -race -count=1 ./... — all packages pass.
Verified locally against postgres:16-alpine via docker; test panics on
pre-fix code and passes on fixed code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry PR

Two jobs triggered on changes to migration driver code:

1. ephemeral-migrate (golang-migrate, timestamp versions)
   - Runs workflow-migrate up with timestamp-based fixture migrations
     (20240101000001, 20240201000001, 20240301000001) — the v0.3.0
     regression scenario.
   - Verifies tables created, runs down(1), checks table count.
   - Runs workflow-migrate status.

2. ephemeral-migrate-goose (goose driver, sequential versions)
   - Same shape using --driver goose and goose-format fixtures.
   - Verifies up creates 2 tables, down rolls back 1.

Without the v0.3.1 fix applied, the `Run workflow-migrate up` step
panics with `makeslice: cap out of range` — blocking merge. This is the
prevention mechanism that would have caught v0.3.0's panic before release.

Note: uses `workflow-migrate` binary directly. When wfctl gains a
`migrations validate` subcommand (task #78), this workflow can be updated
to invoke wfctl instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add .golangci.yml (golangci-lint v2 config, previously absent).
Add //nolint:errcheck to all existing defer .Close() calls — these are
unactionable cleanup paths and the pattern was already present throughout
the codebase. Add //nolint:unused to goreleaser-injected version vars
(set via -ldflags at build time). Also update CHANGELOG for v0.3.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 23:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a panic in the internal/golangmigrate driver when migration versions are timestamp-based (non-sequential), and adds CI coverage to prevent regressions.

Changes:

  • Replace sequential arithmetic/range assumptions with filesystem-based enumeration for applied/rolled-back versions.
  • Add regression tests covering timestamp-based versions for Up() and Down().
  • Add a GitHub Actions workflow to run workflow-migrate up/down against Postgres 16 with both golang-migrate (timestamp) and goose (sequential) fixtures.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/golangmigrate/driver.go Removes sequential-version assumptions by enumerating versions from the source directory via versionsInRange.
internal/golangmigrate/driver_timestamp_test.go Adds regression tests validating timestamp-based version behavior for Up/Down paths.
.github/workflows/migrations-ci.yml Adds CI jobs that exercise workflow-migrate against ephemeral Postgres with representative fixtures.
.golangci.yml Introduces golangci-lint config and exclusions/nolints for pre-existing errcheck/unused patterns.
pkg/cli/test.go Adds //nolint:errcheck on stdout print path.
pkg/cli/tenant_ensure.go Adds //nolint:errcheck on db.Close().
internal/goose/driver.go Adds //nolint:errcheck on db.Close().
internal/atlas/revisions.go Adds //nolint:errcheck on rows.Close().
cmd/workflow-plugin-migrations/main.go Adds //nolint:unused for build-time injected version var.
cmd/workflow-plugin-atlas-migrate/main.go Adds //nolint:unused for build-time injected version var.
CHANGELOG.md Documents the v0.3.1 fix and the new CI workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/golangmigrate/driver.go Outdated
Comment on lines +76 to +93
// versionsInRange returns versions in (after, before] ascending order;
// we reverse to produce highest-first (the order rolled back).
ascending, _ := versionsInRange(req.Source.Dir, after, before, afterNil)
for i := len(ascending) - 1; i >= 0; i-- {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2cd24b7. All five findings from the Copilot review are addressed in that commit.

Comment on lines +195 to +199
v, err := s.First()
for err == nil {
if (loNil || v > lo) && v <= hi {
result = append(result, fmt.Sprintf("%d", v))
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2cd24b7. All five findings from the Copilot review are addressed in that commit.


// Capture the current version before applying.
before, _, _ := m.Version()
before, _, beforeErr := m.Version()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2cd24b7. All five findings from the Copilot review are addressed in that commit.

Comment thread internal/golangmigrate/driver.go Outdated
Comment on lines +47 to +51
after, _, _ := m.Version()
applied := collectApplied(before, after)

// Walk the source directory to enumerate applied versions; this is safe
// for timestamp-based version numbers where after-before can be 10^13.
applied, _ := versionsInRange(req.Source.Dir, before, after, beforeNil)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2cd24b7. All five findings from the Copilot review are addressed in that commit.

Finding 1+2 (migrations-ci.yml): add top-level `permissions: contents: read`
— minimum CodeQL-required scope.

Finding 3 (Up — before version): propagate non-ErrNilVersion errors from
m.Version() before migration. Previously a DB error set before=0 and silently
produced a wrong applied list.

Finding 4 (Up — after version + versionsInRange error): propagate
non-ErrNilVersion afterErr; log (not swallow) versionsInRange error when
migration succeeded but enumeration fails.

Finding 5 (Down — symmetric): same before/after error handling applied to
Down(); log versionsInRange enumeration error.

Finding 6 (versionsInRange + listPendingVersions — end-of-stream sentinel):
source.Driver.Next() returns os.ErrNotExist to signal end-of-stream per the
golang-migrate source.Driver contract (source/driver.go:57). Rewrite iteration
loop to distinguish os.ErrNotExist (normal break) from real errors (propagated
to caller). Apply same fix to listPendingVersions for consistency.

All existing tests pass; golangci-lint 0 issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a production P0 panic in the golang-migrate driver when migration versions are timestamp-based (large numeric gaps), by replacing sequential integer arithmetic/loops with filesystem source enumeration, and adds CI coverage to prevent regressions.

Changes:

  • Replace collectApplied() and integer-range loops with versionsInRange() that walks the migration source directory (safe for timestamp versions).
  • Add regression tests covering Up()/Down() behavior with timestamp-based versions.
  • Add a GitHub Actions workflow to run workflow-migrate up/down against ephemeral Postgres on PRs touching migration code, plus introduce a repo-wide .golangci.yml and targeted //nolint annotations.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/cli/test.go Adds //nolint:errcheck for stdout printing to satisfy errcheck linting.
pkg/cli/tenant_ensure.go Adds //nolint:errcheck for deferred DB close to align with lint configuration.
internal/goose/driver.go Adds //nolint:errcheck on deferred DB closes.
internal/golangmigrate/driver_timestamp_test.go Adds regression tests for timestamp-based version behavior in Up/Down.
internal/golangmigrate/driver.go Removes sequential version assumptions; enumerates versions via source walking to avoid overflow/panic.
internal/atlas/revisions.go Adds //nolint:errcheck for deferred rows close.
cmd/workflow-plugin-migrations/main.go Marks build-time version var as intentionally unused for linting.
cmd/workflow-plugin-atlas-migrate/main.go Marks build-time version var as intentionally unused for linting.
CHANGELOG.md Documents v0.3.1 fix and CI/lint additions.
.golangci.yml Adds golangci-lint configuration (errcheck/govet/staticcheck/unused + exclusions).
.github/workflows/migrations-ci.yml Adds CI workflow running up/down against Postgres with timestamp and sequential fixtures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@intel352
intel352 merged commit 8dd61ef into main Apr 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants