Skip to content

Commit 2560b1c

Browse files
committed
feat: add golang-migrate schema migrations and dedicated migrate image
Introduce db/migrations/ with the initial schema baseline. Dockerfile gains a multi-stage `migrate` target that bundles the migrate CLI and SQL files into a ~20MB image. CI builds and pushes both app and migrate images with matching tags. Devtest commands now use golang-migrate programmatically instead of raw schema.sql exec, validating migrations work in dev. Signed-off-by: Gabriel Harris-Rouquette <gabizou@me.com>
1 parent 3dfd558 commit 2560b1c

9 files changed

Lines changed: 156 additions & 25 deletions

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ jobs:
115115
username: ${{ github.actor }}
116116
password: ${{ secrets.GITHUB_TOKEN }}
117117

118-
- name: Docker metadata
119-
id: meta
118+
- name: App image metadata
119+
id: app-meta
120120
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
121121
with:
122122
images: ghcr.io/${{ github.repository }}
@@ -126,14 +126,39 @@ jobs:
126126
type=sha,prefix=dev-,enable=${{ github.ref == 'refs/heads/dev' }}
127127
type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }}
128128
129-
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
129+
- name: Migrate image metadata
130+
id: migrate-meta
131+
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
132+
with:
133+
images: ghcr.io/${{ github.repository }}/migrate
134+
tags: |
135+
type=sha,prefix=,enable=${{ github.ref == 'refs/heads/main' }}
136+
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
137+
type=sha,prefix=dev-,enable=${{ github.ref == 'refs/heads/dev' }}
138+
type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }}
139+
140+
- name: Build and push app image
141+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
130142
with:
131143
context: .
144+
target: app
132145
push: true
133146
platforms: linux/amd64,linux/arm64
134-
tags: ${{ steps.meta.outputs.tags }}
135-
labels: ${{ steps.meta.outputs.labels }}
136-
cache-from: type=gha
137-
cache-to: type=gha,mode=max
147+
tags: ${{ steps.app-meta.outputs.tags }}
148+
labels: ${{ steps.app-meta.outputs.labels }}
149+
cache-from: type=gha,scope=app
150+
cache-to: type=gha,scope=app,mode=max
138151
build-args: |
139152
VERSION=${{ github.sha }}
153+
154+
- name: Build and push migrate image
155+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
156+
with:
157+
context: .
158+
target: migrate
159+
push: true
160+
platforms: linux/amd64,linux/arm64
161+
tags: ${{ steps.migrate-meta.outputs.tags }}
162+
labels: ${{ steps.migrate-meta.outputs.labels }}
163+
cache-from: type=gha,scope=migrate
164+
cache-to: type=gha,scope=migrate,mode=max

.github/workflows/release-please.yml

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ jobs:
115115
username: ${{ github.actor }}
116116
password: ${{ secrets.GITHUB_TOKEN }}
117117

118-
- name: Docker metadata
119-
id: meta
118+
- name: App image metadata
119+
id: app-meta
120120
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
121121
with:
122122
images: ghcr.io/${{ github.repository }}
@@ -125,14 +125,38 @@ jobs:
125125
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.tag_name }}
126126
type=raw,value=latest
127127
128-
- uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c816612b # v6.18.0
128+
- name: Migrate image metadata
129+
id: migrate-meta
130+
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
131+
with:
132+
images: ghcr.io/${{ github.repository }}/migrate
133+
tags: |
134+
type=semver,pattern={{version}},value=${{ needs.release-please.outputs.tag_name }}
135+
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.tag_name }}
136+
type=raw,value=latest
137+
138+
- name: Build and push app image
139+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
129140
with:
130141
context: .
142+
target: app
131143
push: true
132144
platforms: linux/amd64,linux/arm64
133-
tags: ${{ steps.meta.outputs.tags }}
134-
labels: ${{ steps.meta.outputs.labels }}
135-
cache-from: type=gha
136-
cache-to: type=gha,mode=max
145+
tags: ${{ steps.app-meta.outputs.tags }}
146+
labels: ${{ steps.app-meta.outputs.labels }}
147+
cache-from: type=gha,scope=app
148+
cache-to: type=gha,scope=app,mode=max
137149
build-args: |
138150
VERSION=${{ needs.release-please.outputs.tag_name }}
151+
152+
- name: Build and push migrate image
153+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
154+
with:
155+
context: .
156+
target: migrate
157+
push: true
158+
platforms: linux/amd64,linux/arm64
159+
tags: ${{ steps.migrate-meta.outputs.tags }}
160+
labels: ${{ steps.migrate-meta.outputs.labels }}
161+
cache-from: type=gha,scope=migrate
162+
cache-to: type=gha,scope=migrate,mode=max

Dockerfile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,24 @@ ARG VERSION=dev
1212
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.Version=${VERSION}" -o /out/server ./cmd/server && \
1313
CGO_ENABLED=0 go build -ldflags "-s -w -X main.Version=${VERSION}" -o /out/worker ./cmd/worker
1414

15-
FROM alpine:3.22
15+
# --- App image (default target) ---
16+
FROM alpine:3.22 AS app
1617

1718
RUN apk add --no-cache ca-certificates git tzdata
1819

1920
COPY --from=builder /out/server /app/server
2021
COPY --from=builder /out/worker /app/worker
2122

2223
ENTRYPOINT ["/app/server"]
24+
25+
# --- Migrate image (built with --target migrate) ---
26+
FROM migrate/migrate:v4.18.3 AS migrate-bin
27+
28+
FROM alpine:3.22 AS migrate
29+
30+
RUN apk add --no-cache ca-certificates
31+
32+
COPY --from=migrate-bin /usr/local/bin/migrate /usr/local/bin/migrate
33+
COPY db/migrations /migrations
34+
35+
ENTRYPOINT ["migrate", "-path", "/migrations"]

cmd/devtest-enrichment/main.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"encoding/json"
2121
"encoding/xml"
22+
"errors"
2223
"fmt"
2324
"log/slog"
2425
"net/http"
@@ -27,6 +28,9 @@ import (
2728
"strings"
2829
"time"
2930

31+
"github.com/golang-migrate/migrate/v4"
32+
_ "github.com/golang-migrate/migrate/v4/database/postgres"
33+
_ "github.com/golang-migrate/migrate/v4/source/file"
3034
"github.com/jackc/pgx/v5/pgxpool"
3135
"github.com/testcontainers/testcontainers-go"
3236
"github.com/testcontainers/testcontainers-go/modules/postgres"
@@ -156,14 +160,14 @@ func run(ctx context.Context) error {
156160
}
157161
defer pool.Close()
158162

159-
schemaSQL, err := os.ReadFile("db/schema.sql")
163+
m, err := migrate.New("file://db/migrations", connStr)
160164
if err != nil {
161-
return fmt.Errorf("reading schema: %w", err)
165+
return fmt.Errorf("creating migrator: %w", err)
162166
}
163-
if _, err := pool.Exec(ctx, string(schemaSQL)); err != nil {
164-
return fmt.Errorf("applying schema: %w", err)
167+
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
168+
return fmt.Errorf("running migrations: %w", err)
165169
}
166-
slog.InfoContext(ctx, "PostgreSQL ready")
170+
slog.InfoContext(ctx, "PostgreSQL ready (migrations applied)")
167171

168172
repo := repository.NewRepository(pool)
169173

cmd/devtest-load/main.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ package main
1313

1414
import (
1515
"context"
16+
"errors"
1617
"fmt"
1718
"log/slog"
1819
"net/http"
1920
"os"
2021
"os/signal"
2122
"time"
2223

24+
"github.com/golang-migrate/migrate/v4"
25+
_ "github.com/golang-migrate/migrate/v4/database/postgres"
26+
_ "github.com/golang-migrate/migrate/v4/source/file"
2327
"github.com/jackc/pgx/v5/pgxpool"
2428
"github.com/testcontainers/testcontainers-go"
2529
"github.com/testcontainers/testcontainers-go/modules/postgres"
@@ -151,14 +155,14 @@ func run(ctx context.Context) error {
151155
}
152156
defer pool.Close()
153157

154-
schemaSQL, err := os.ReadFile("db/schema.sql")
158+
m, err := migrate.New("file://db/migrations", connStr)
155159
if err != nil {
156-
return fmt.Errorf("reading schema: %w", err)
160+
return fmt.Errorf("creating migrator: %w", err)
157161
}
158-
if _, err := pool.Exec(ctx, string(schemaSQL)); err != nil {
159-
return fmt.Errorf("applying schema: %w", err)
162+
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
163+
return fmt.Errorf("running migrations: %w", err)
160164
}
161-
slog.InfoContext(ctx, "PostgreSQL ready")
165+
slog.InfoContext(ctx, "PostgreSQL ready (migrations applied)")
162166

163167
repo := repository.NewRepository(pool)
164168
svc := app.NewService(repo)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
DROP INDEX IF EXISTS idx_versions_artifact_recommended_sort;
2+
DROP INDEX IF EXISTS idx_versions_artifact_sort;
3+
DROP INDEX IF EXISTS idx_versioned_tags_key_value;
4+
5+
DROP TABLE IF EXISTS artifact_versioned_tags;
6+
DROP TABLE IF EXISTS artifact_versioned_assets;
7+
DROP TABLE IF EXISTS artifact_versions;
8+
DROP TABLE IF EXISTS artifacts;
9+
DROP TABLE IF EXISTS groups;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
CREATE TABLE groups (
2+
maven_id TEXT PRIMARY KEY,
3+
name TEXT NOT NULL,
4+
website TEXT
5+
);
6+
7+
CREATE TABLE artifacts (
8+
id BIGSERIAL PRIMARY KEY,
9+
group_id TEXT NOT NULL REFERENCES groups(maven_id),
10+
artifact_id TEXT NOT NULL,
11+
name TEXT NOT NULL,
12+
website TEXT,
13+
issues TEXT,
14+
git_repositories JSONB NOT NULL DEFAULT '[]',
15+
version_schema JSONB,
16+
UNIQUE(group_id, artifact_id)
17+
);
18+
19+
CREATE TABLE artifact_versions (
20+
id BIGSERIAL PRIMARY KEY,
21+
artifact_id BIGINT NOT NULL REFERENCES artifacts(id),
22+
version TEXT NOT NULL,
23+
sort_order INTEGER NOT NULL DEFAULT 0,
24+
recommended BOOLEAN NOT NULL DEFAULT false,
25+
commit_body JSONB,
26+
UNIQUE(artifact_id, version)
27+
);
28+
29+
CREATE TABLE artifact_versioned_assets (
30+
id BIGSERIAL PRIMARY KEY,
31+
artifact_version_id BIGINT NOT NULL REFERENCES artifact_versions(id),
32+
classifier TEXT,
33+
sha256 TEXT,
34+
download_url TEXT NOT NULL
35+
);
36+
37+
CREATE TABLE artifact_versioned_tags (
38+
artifact_version_id BIGINT NOT NULL REFERENCES artifact_versions(id),
39+
tag_key TEXT NOT NULL,
40+
tag_value TEXT NOT NULL,
41+
PRIMARY KEY (artifact_version_id, tag_key)
42+
);
43+
44+
CREATE INDEX idx_versioned_tags_key_value ON artifact_versioned_tags(tag_key, tag_value, artifact_version_id);
45+
CREATE INDEX idx_versions_artifact_sort ON artifact_versions(artifact_id, sort_order DESC);
46+
CREATE INDEX idx_versions_artifact_recommended_sort ON artifact_versions(artifact_id, recommended, sort_order DESC);

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/spongepowered/systemofadownload
33
go 1.26.1
44

55
require (
6+
github.com/golang-migrate/migrate/v4 v4.19.1
67
github.com/google/go-cmp v0.7.0
78
github.com/jackc/pgx/v5 v5.8.0
89
github.com/oapi-codegen/runtime v1.3.0
@@ -55,6 +56,7 @@ require (
5556
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
5657
github.com/jackc/puddle/v2 v2.2.2 // indirect
5758
github.com/klauspost/compress v1.18.4 // indirect
59+
github.com/lib/pq v1.10.9 // indirect
5860
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
5961
github.com/magiconair/properties v1.8.10 // indirect
6062
github.com/moby/docker-image-spec v1.3.1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
3131
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3232
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
3333
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34+
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
35+
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
3436
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
3537
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
3638
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
@@ -55,6 +57,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
5557
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
5658
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
5759
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
60+
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
61+
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
5862
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
5963
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
6064
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=

0 commit comments

Comments
 (0)