Skip to content

Commit 821a97c

Browse files
michaelmcneesclaudegithub-actions[bot]
authored
chore: migrate goreleaser to v2 APIs and add release documentation (#122)
* chore: migrate goreleaser to v2 APIs and add release documentation - Migrate archives.format (scalar) to formats (list) per goreleaser v2 deprecation - Migrate dockers + docker_manifests to dockers_v2; update Dockerfile.goreleaser to use $TARGETPLATFORM for multi-arch build context - Add order fields to changelog groups (Features=0, Bug Fixes=1) - Bundle LICENSE in release archives to satisfy BUSL-1.1 notice requirement - Add RELEASING.md with full release process documentation - Add docs/superpowers/specs/ files to version control Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: address CodeRabbit review comments on spec files - Add language identifiers to unlabeled fenced code blocks (MD040) in config-system, docker-compose, and go-project-scaffold specs - Align Validate() signature to Validate(*ParseResult) throughout workflow-validation-design.md - Fix singular/plural table name: workflow_execution → workflow_executions in multi-node-distribution-design.md - Reword EngineConfig field to avoid implying a duplicate struct field in workflow-composition-design.md - Vary sentence structure in ai-tool-use validation rule bullets Co-authored-by: Michael McNees <michaelmcnees@users.noreply.github.com> * docs: fix MD040 fenced block languages and continue_on_error spec accuracy - Add `text` language identifier to three unlabeled fenced code blocks - Replace inaccurate `continue_on_failure` note with accurate `continue_on_error` description Co-authored-by: Michael McNees <michaelmcnees@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Michael McNees <michaelmcnees@users.noreply.github.com>
1 parent 80d4c14 commit 821a97c

18 files changed

Lines changed: 3020 additions & 41 deletions

RELEASING.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Releasing Mantle
2+
3+
Mantle uses [Changesets](https://github.com/changesets/changesets) for versioning and automated releases.
4+
The CI pipeline is fully automated once the Version PR is merged — no manual tagging or publishing required.
5+
6+
## Prerequisites
7+
8+
Before performing a manual release (bootstrap or re-tag), confirm you have:
9+
10+
- **Push access** to `main` and tags on `github.com/dvflw/mantle`
11+
- **`gh` CLI** installed and authenticated (`gh auth status`)
12+
- **GHCR write access** — your GitHub account must have write access to `ghcr.io/dvflw` (granted via org membership)
13+
14+
The automated CI path (Version PR → merge) requires no local tooling beyond git.
15+
16+
## How It Works
17+
18+
```
19+
1. changeset file added during dev
20+
21+
2. push to main → release-please.yml creates/updates "Version PR"
22+
23+
3. merge Version PR
24+
25+
4. release-tags.yml detects version bumps, pushes package-scoped tags
26+
27+
5a. release-engine.yml → goreleaser: binaries + Docker + GitHub Release + Trivy
28+
5b. release-helm.yml → helm push OCI + GitHub Release
29+
```
30+
31+
> **Note on workflow naming:** `release-please.yml` is named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename to `changeset-version.yml` is tracked in [the relevant issue].
32+
33+
## Step 1: Add a Changeset (During Development)
34+
35+
Every PR that changes user-visible behavior needs a changeset. Run this from the repo root:
36+
37+
```bash
38+
bun run changeset
39+
```
40+
41+
The interactive CLI asks:
42+
1. **Which packages changed?** Select from `@mantle/engine`, `@mantle/helm-chart`, `@mantle/site`
43+
2. **Bump type per package:** `major` / `minor` / `patch`
44+
3. **Summary:** One-line description of the change (appears in CHANGELOG)
45+
46+
This creates a file like `.changeset/fuzzy-lions-dance.md`. Commit it alongside your code.
47+
48+
> **`@mantle/site`:** changesets version the docs site but do not trigger a separate release workflow. The `site/v*` tag is pushed automatically but there is no equivalent `release-site.yml` — site deploys happen via the Astro/hosting pipeline.
49+
50+
**When to use each bump type:**
51+
- `patch` — bug fixes, internal refactors, docs updates
52+
- `minor` — new features, new CLI commands, new config fields
53+
- `major` — breaking changes (workflow YAML format, CLI flags, API shape)
54+
55+
If a PR touches only CI, tests, or tooling with no user impact, skip the changeset.
56+
57+
## Step 2: The Version PR
58+
59+
After any push to `main` that contains pending changesets, the `release-please.yml` workflow automatically creates (or updates) a PR titled **"ci: version packages"**.
60+
61+
This PR:
62+
- Bumps `version` in each affected `package.json` (taking the highest bump across all pending changesets)
63+
- Generates / appends to `packages/*/CHANGELOG.md`
64+
- Deletes the consumed changeset files
65+
66+
Review the PR to confirm the version bumps and changelog entries are correct. If more changesets land on `main` before you merge, CI updates the PR automatically.
67+
68+
> **Loop guard:** the workflow has an `if` condition that skips runs where the triggering commit message starts with `"ci: version packages"`. This prevents the workflow from retriggering on its own Version PR merge commit. If you ever rename the commit message in the workflow, update the guard condition in `release-please.yml` to match.
69+
70+
## Step 3: Merge the Version PR
71+
72+
When you're ready to cut a release, **merge the Version PR**. That's the release trigger.
73+
74+
## Step 4: Tags and Artifacts (Automated)
75+
76+
After the Version PR merges, `release-tags.yml` fires and:
77+
78+
1. Detects which `package.json` files changed version
79+
2. If the engine version bumped, syncs `Chart.yaml appVersion` and commits it back to `main`
80+
3. Pushes package-scoped tags: `engine/v<version>`, `helm-chart/v<version>`, `site/v<version>`
81+
82+
Those tags trigger the package-specific release workflows:
83+
84+
| Tag pattern | Workflow | What it produces |
85+
|---|---|---|
86+
| `engine/v*` | `release-engine.yml` | goreleaser builds Linux/macOS amd64/arm64 binaries + checksums + LICENSE, pushes multi-arch `ghcr.io/dvflw/mantle:<version>` Docker image (+ floating tags on stable), creates GitHub Release, runs Trivy CVE scan |
87+
| `helm-chart/v*` | `release-helm.yml` | Packages and pushes `oci://ghcr.io/dvflw/helm-charts/mantle:<version>`, creates GitHub Release |
88+
89+
**How the engine tag translates to a clean version:** the workflow strips the `engine/` prefix, sets `GORELEASER_CURRENT_TAG=v<version>`, and creates a local `git tag v<version>` alias so goreleaser can validate the tag against the current commit. Only the namespaced tag (`engine/v<version>`) is permanent in the remote.
90+
91+
**Floating tags** (`major.minor`, `major`, `latest`) are only pushed for stable releases. For pre-release versions (e.g. `0.5.0-rc.1`) the versioned image is pushed but floating tags are skipped.
92+
93+
**Platform-specific image tags:** the pipeline no longer publishes per-arch tags like `ghcr.io/dvflw/mantle:<version>-amd64`. Only the multi-arch manifest tag (`ghcr.io/dvflw/mantle:<version>`) is pushed. Update any CI or deploy configs that reference the old suffixed tags.
94+
95+
**Trivy scan policy:** the `trivy` job runs after `release` with `exit-code: 1` on `CRITICAL` or `HIGH` CVEs. A failure marks the workflow run as failed but does not retract the already-published release. If a CVE scan fails post-release, open a patch release issue immediately.
96+
97+
**Partial-failure recovery:** if `release-tags.yml` fails after pushing some but not all tags, push the missing tags manually:
98+
99+
```bash
100+
git tag helm-chart/v<version>
101+
git push origin helm-chart/v<version>
102+
```
103+
104+
The tag-triggered workflows are safe to retrigger — goreleaser will fail cleanly if the GitHub Release already exists.
105+
106+
## First Release of a New Version (No Pending Changesets)
107+
108+
This applies when `package.json` is already at the target version but `.changeset/` contains no pending changeset files — for example, the first-ever release of a newly bootstrapped repo, or after manually editing `package.json` outside the changeset flow.
109+
110+
In this state the `release-please.yml` workflow has nothing to process and will not create a Version PR. Push the tags directly:
111+
112+
```bash
113+
git tag engine/v<version>
114+
git push origin engine/v<version>
115+
116+
# If also releasing the Helm chart at the same version:
117+
git tag helm-chart/v<version>
118+
git push origin helm-chart/v<version>
119+
```
120+
121+
After the initial tag, use the full changeset flow for all subsequent releases.
122+
123+
## Verifying a Release
124+
125+
After the workflows complete, substitute your version for `<version>`:
126+
127+
```bash
128+
# Check GitHub Releases
129+
gh release list
130+
131+
# Verify Docker image
132+
docker pull ghcr.io/dvflw/mantle:<version>
133+
docker run --rm ghcr.io/dvflw/mantle:<version> mantle version
134+
135+
# Verify Helm chart
136+
helm show chart oci://ghcr.io/dvflw/helm-charts/mantle --version <version>
137+
```
138+
139+
## Pre-releases
140+
141+
To cut a pre-release (e.g. `0.5.0-rc.1`), no changeset is needed — bump the version manually and tag directly:
142+
143+
```bash
144+
# 1. Set the pre-release version in package.json
145+
# Edit packages/engine/package.json: "version": "0.5.0-rc.1"
146+
git add packages/engine/package.json
147+
git commit -m "chore: bump engine to 0.5.0-rc.1"
148+
git push origin main
149+
150+
# 2. Tag and push
151+
git tag engine/v0.5.0-rc.1
152+
git push origin engine/v0.5.0-rc.1
153+
```
154+
155+
goreleaser's `prerelease: auto` publishes the GitHub Release as a pre-release when the version contains a pre-release identifier. The versioned Docker image (`0.5.0-rc.1`) is pushed; floating tags (`latest`, `major`, `major.minor`) are not.
156+
157+
## Rollback
158+
159+
Releases are immutable — GitHub Releases and pushed OCI images cannot be unpublished retroactively for users who have already pulled them. Rolling back means directing users to the last known-good version and cutting a patch.
160+
161+
**1. Communicate immediately** with the last known-good version and the broken version to avoid.
162+
163+
**2. Optionally hide the broken release from the GitHub Releases UI:**
164+
165+
```bash
166+
# Mark as pre-release so it no longer shows as the "latest" release
167+
gh release edit engine/v<broken-version> --prerelease
168+
169+
# Or move to draft (still accessible by direct URL/tag, just hidden from listing)
170+
gh release edit engine/v<broken-version> --draft
171+
```
172+
173+
> These do not retract published Docker images or Helm charts from GHCR. Downstream users who have already pulled the image are not affected.
174+
175+
**3. Cut a patch release** as the authoritative fix via the normal changeset flow.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Design: CI/CD Pipeline — test, vet, lint
2+
3+
> Linear issue: [DVFLW-275](https://linear.app/dvflw/issue/DVFLW-275/cicd-pipeline-test-vet-lint)
4+
> Date: 2026-03-18
5+
6+
## Goal
7+
8+
Add GitHub Actions CI pipeline that runs tests, vet, and lint on every push and PR. Add golangci-lint configuration. Add CI status badge to README.
9+
10+
## Acceptance Criteria
11+
12+
- `go test ./...` runs on every push and PR
13+
- `go vet ./...` and `golangci-lint` run on every push and PR
14+
- Integration tests run against Postgres via testcontainers (when added)
15+
- CI fails on lint errors or test failures
16+
17+
## Files
18+
19+
- Create: `.github/workflows/ci.yml`
20+
- Create: `.golangci.yml`
21+
- Modify: `README.md` — add CI badge
22+
23+
## GitHub Actions Workflow
24+
25+
`.github/workflows/ci.yml` — triggered on push and pull_request to all branches.
26+
27+
Three parallel jobs on `ubuntu-latest` with Go 1.24:
28+
29+
### test
30+
- Checkout, setup Go
31+
- `go test -race ./...`
32+
- Docker available by default for future testcontainers tests
33+
34+
### vet
35+
- Checkout, setup Go
36+
- `go vet ./...`
37+
38+
### lint
39+
- Checkout, setup Go
40+
- Uses `golangci/golangci-lint-action` with pinned version
41+
42+
## golangci-lint Config
43+
44+
`.golangci.yml` — conservative linter set that catches real bugs:
45+
46+
```yaml
47+
run:
48+
timeout: 5m
49+
50+
linters:
51+
enable:
52+
- errcheck
53+
- govet
54+
- staticcheck
55+
- unused
56+
- ineffassign
57+
- gosimple
58+
```
59+
60+
## README Badge
61+
62+
Add CI status badge at the top of README.md, right after the `# Mantle` heading:
63+
64+
```markdown
65+
[![CI](https://github.com/dvflw/mantle/actions/workflows/ci.yml/badge.svg)](https://github.com/dvflw/mantle/actions/workflows/ci.yml)
66+
```
67+
68+
## What's NOT Included
69+
70+
- Release/deploy workflows (Phase 4)
71+
- Code coverage reporting
72+
- Go module caching (add later if CI is slow)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Design: Config System — mantle.yaml and CLI Flags
2+
3+
> Linear issue: [DVFLW-272](https://linear.app/dvflw/issue/DVFLW-272/config-system-mantleyaml-and-cli-flags)
4+
> Date: 2026-03-18
5+
6+
## Goal
7+
8+
Add a configuration system using Viper that loads settings from `mantle.yaml`, with environment variable and CLI flag overrides.
9+
10+
## Acceptance Criteria
11+
12+
- `mantle.yaml` supports Postgres connection string, API listen address, log level
13+
- CLI flags override config file values
14+
- Env vars (e.g., `MANTLE_DATABASE_URL`) override both config file and defaults
15+
- Config file path configurable via `--config` flag
16+
17+
## Package Structure
18+
19+
```text
20+
internal/
21+
config/
22+
config.go # Config struct, Load() function, Viper setup
23+
config_test.go # Tests for loading, env overrides, flag overrides, defaults
24+
```
25+
26+
## Config Struct
27+
28+
```go
29+
type Config struct {
30+
Database DatabaseConfig `mapstructure:"database"`
31+
API APIConfig `mapstructure:"api"`
32+
Log LogConfig `mapstructure:"log"`
33+
}
34+
35+
type DatabaseConfig struct {
36+
URL string `mapstructure:"url"`
37+
}
38+
39+
type APIConfig struct {
40+
Address string `mapstructure:"address"`
41+
}
42+
43+
type LogConfig struct {
44+
Level string `mapstructure:"level"`
45+
}
46+
```
47+
48+
## Default Values
49+
50+
| Key | Default | Env Var |
51+
|-----|---------|---------|
52+
| `database.url` | `postgres://localhost:5432/mantle?sslmode=disable` | `MANTLE_DATABASE_URL` |
53+
| `api.address` | `:8080` | `MANTLE_API_ADDRESS` |
54+
| `log.level` | `info` | `MANTLE_LOG_LEVEL` |
55+
56+
## Example mantle.yaml
57+
58+
```yaml
59+
database:
60+
url: "postgres://localhost:5432/mantle?sslmode=disable"
61+
62+
api:
63+
address: ":8080"
64+
65+
log:
66+
level: "info"
67+
```
68+
69+
## Load Function
70+
71+
`Load(configPath string) (*Config, error)` — takes the `--config` flag value.
72+
73+
Steps:
74+
1. Set defaults for all keys
75+
2. If `configPath` is provided, read that file; otherwise search current directory for `mantle.yaml`
76+
3. Set `MANTLE_` env prefix via `SetEnvPrefix("MANTLE")`
77+
4. Explicitly bind env vars with `BindEnv` for each key (not `AutomaticEnv`, which cannot reliably resolve nested keys with underscore delimiters):
78+
- `viper.BindEnv("database.url", "MANTLE_DATABASE_URL")`
79+
- `viper.BindEnv("api.address", "MANTLE_API_ADDRESS")`
80+
- `viper.BindEnv("log.level", "MANTLE_LOG_LEVEL")`
81+
5. Unmarshal into `Config` struct and return
82+
83+
### Missing Config File Behavior
84+
85+
- If `--config` is provided and the file does not exist: **hard error** (user explicitly asked for this file)
86+
- If `--config` is not provided and no `mantle.yaml` in current directory: **silent fallback to defaults** (config file is optional)
87+
88+
### Validation
89+
90+
`Load()` returns an error only for YAML parse failures and type mismatches. It does **not** perform semantic validation (e.g., whether `database.url` is a valid Postgres connection string). Semantic validation belongs in the consuming code.
91+
92+
## Override Precedence
93+
94+
Viper's built-in precedence (highest to lowest):
95+
1. CLI flags (bound to Viper keys via `BindPFlag`)
96+
2. Environment variables (`MANTLE_` prefix, explicit `BindEnv` calls)
97+
3. Config file (`mantle.yaml`)
98+
4. Defaults
99+
100+
## CLI Flags
101+
102+
Add these persistent flags on the root command:
103+
- `--database-url` — binds to Viper key `database.url`
104+
- `--api-address` — binds to Viper key `api.address`
105+
- `--log-level` — binds to Viper key `log.level`
106+
107+
Flags are bound to Viper keys via `viper.BindPFlag()` inside `Load()`, which receives the `*cobra.Command` to access its flags. Updated signature:
108+
109+
`Load(cmd *cobra.Command) (*Config, error)` — reads `--config` flag from `cmd`, binds other flags to Viper.
110+
111+
## Cobra Integration
112+
113+
In `internal/cli/root.go`:
114+
- Add `--database-url`, `--api-address`, `--log-level` as persistent flags
115+
- `PersistentPreRunE` on the root command calls `config.Load(cmd)` which reads `--config`, binds flags, and loads config
116+
- The resulting `*Config` is stored on the command's context via `context.WithValue`
117+
118+
### Context Key
119+
120+
Defined in `internal/config/`:
121+
122+
```go
123+
type contextKey struct{}
124+
125+
// WithContext returns a new context with the config attached.
126+
func WithContext(ctx context.Context, cfg *Config) context.Context {
127+
return context.WithValue(ctx, contextKey{}, cfg)
128+
}
129+
130+
// FromContext retrieves the config from context. Returns nil if not set.
131+
func FromContext(ctx context.Context) *Config {
132+
cfg, _ := ctx.Value(contextKey{}).(*Config)
133+
return cfg
134+
}
135+
```
136+
137+
The context key type is unexported, preventing collisions. Helper functions are in the `config` package so subcommands depend on `config` (not the other way around).
138+
139+
## Dependencies
140+
141+
- `github.com/spf13/viper` — config loading, env var binding, YAML parsing (must be added to go.mod)
142+
143+
## What's NOT Included
144+
145+
- Semantic validation of config values (belongs in consuming code)
146+
- Hot reload / file watching
147+
- Config fields beyond database.url, api.address, log.level (added by future phases)

0 commit comments

Comments
 (0)