-
Notifications
You must be signed in to change notification settings - Fork 0
chore: migrate goreleaser to v2 APIs and add release documentation #122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a21fe70
491743c
d05ab0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| # Releasing Mantle | ||
|
|
||
| Mantle uses [Changesets](https://github.com/changesets/changesets) for versioning and automated releases. | ||
| The CI pipeline is fully automated once the Version PR is merged — no manual tagging or publishing required. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| Before performing a manual release (bootstrap or re-tag), confirm you have: | ||
|
|
||
| - **Push access** to `main` and tags on `github.com/dvflw/mantle` | ||
| - **`gh` CLI** installed and authenticated (`gh auth status`) | ||
| - **GHCR write access** — your GitHub account must have write access to `ghcr.io/dvflw` (granted via org membership) | ||
|
|
||
| The automated CI path (Version PR → merge) requires no local tooling beyond git. | ||
|
|
||
| ## How It Works | ||
|
|
||
| ``` | ||
| 1. changeset file added during dev | ||
| ↓ | ||
| 2. push to main → release-please.yml creates/updates "Version PR" | ||
| ↓ | ||
| 3. merge Version PR | ||
| ↓ | ||
| 4. release-tags.yml detects version bumps, pushes package-scoped tags | ||
| ↓ | ||
| 5a. release-engine.yml → goreleaser: binaries + Docker + GitHub Release + Trivy | ||
| 5b. release-helm.yml → helm push OCI + GitHub Release | ||
| ``` | ||
|
|
||
| > **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]. | ||
|
|
||
| ## Step 1: Add a Changeset (During Development) | ||
|
|
||
| Every PR that changes user-visible behavior needs a changeset. Run this from the repo root: | ||
|
|
||
| ```bash | ||
| bun run changeset | ||
| ``` | ||
|
|
||
| The interactive CLI asks: | ||
| 1. **Which packages changed?** Select from `@mantle/engine`, `@mantle/helm-chart`, `@mantle/site` | ||
| 2. **Bump type per package:** `major` / `minor` / `patch` | ||
| 3. **Summary:** One-line description of the change (appears in CHANGELOG) | ||
|
|
||
| This creates a file like `.changeset/fuzzy-lions-dance.md`. Commit it alongside your code. | ||
|
|
||
| > **`@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. | ||
|
|
||
| **When to use each bump type:** | ||
| - `patch` — bug fixes, internal refactors, docs updates | ||
| - `minor` — new features, new CLI commands, new config fields | ||
| - `major` — breaking changes (workflow YAML format, CLI flags, API shape) | ||
|
|
||
| If a PR touches only CI, tests, or tooling with no user impact, skip the changeset. | ||
|
|
||
| ## Step 2: The Version PR | ||
|
|
||
| After any push to `main` that contains pending changesets, the `release-please.yml` workflow automatically creates (or updates) a PR titled **"ci: version packages"**. | ||
|
|
||
| This PR: | ||
| - Bumps `version` in each affected `package.json` (taking the highest bump across all pending changesets) | ||
| - Generates / appends to `packages/*/CHANGELOG.md` | ||
| - Deletes the consumed changeset files | ||
|
|
||
| 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. | ||
|
|
||
| > **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. | ||
|
|
||
| ## Step 3: Merge the Version PR | ||
|
|
||
| When you're ready to cut a release, **merge the Version PR**. That's the release trigger. | ||
|
|
||
| ## Step 4: Tags and Artifacts (Automated) | ||
|
|
||
| After the Version PR merges, `release-tags.yml` fires and: | ||
|
|
||
| 1. Detects which `package.json` files changed version | ||
| 2. If the engine version bumped, syncs `Chart.yaml appVersion` and commits it back to `main` | ||
| 3. Pushes package-scoped tags: `engine/v<version>`, `helm-chart/v<version>`, `site/v<version>` | ||
|
|
||
| Those tags trigger the package-specific release workflows: | ||
|
|
||
| | Tag pattern | Workflow | What it produces | | ||
| |---|---|---| | ||
| | `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 | | ||
| | `helm-chart/v*` | `release-helm.yml` | Packages and pushes `oci://ghcr.io/dvflw/helm-charts/mantle:<version>`, creates GitHub Release | | ||
|
|
||
| **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. | ||
|
|
||
| **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. | ||
|
|
||
| **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. | ||
|
|
||
| **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. | ||
|
|
||
| **Partial-failure recovery:** if `release-tags.yml` fails after pushing some but not all tags, push the missing tags manually: | ||
|
|
||
| ```bash | ||
| git tag helm-chart/v<version> | ||
| git push origin helm-chart/v<version> | ||
| ``` | ||
|
|
||
| The tag-triggered workflows are safe to retrigger — goreleaser will fail cleanly if the GitHub Release already exists. | ||
|
|
||
| ## First Release of a New Version (No Pending Changesets) | ||
|
|
||
| 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. | ||
|
|
||
| In this state the `release-please.yml` workflow has nothing to process and will not create a Version PR. Push the tags directly: | ||
|
|
||
| ```bash | ||
| git tag engine/v<version> | ||
| git push origin engine/v<version> | ||
|
|
||
| # If also releasing the Helm chart at the same version: | ||
| git tag helm-chart/v<version> | ||
| git push origin helm-chart/v<version> | ||
| ``` | ||
|
|
||
| After the initial tag, use the full changeset flow for all subsequent releases. | ||
|
|
||
| ## Verifying a Release | ||
|
|
||
| After the workflows complete, substitute your version for `<version>`: | ||
|
|
||
| ```bash | ||
| # Check GitHub Releases | ||
| gh release list | ||
|
|
||
| # Verify Docker image | ||
| docker pull ghcr.io/dvflw/mantle:<version> | ||
| docker run --rm ghcr.io/dvflw/mantle:<version> mantle version | ||
|
|
||
| # Verify Helm chart | ||
| helm show chart oci://ghcr.io/dvflw/helm-charts/mantle --version <version> | ||
| ``` | ||
|
|
||
| ## Pre-releases | ||
|
|
||
| To cut a pre-release (e.g. `0.5.0-rc.1`), no changeset is needed — bump the version manually and tag directly: | ||
|
|
||
| ```bash | ||
| # 1. Set the pre-release version in package.json | ||
| # Edit packages/engine/package.json: "version": "0.5.0-rc.1" | ||
| git add packages/engine/package.json | ||
| git commit -m "chore: bump engine to 0.5.0-rc.1" | ||
| git push origin main | ||
|
|
||
| # 2. Tag and push | ||
| git tag engine/v0.5.0-rc.1 | ||
| git push origin engine/v0.5.0-rc.1 | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
|
Comment on lines
+139
to
+156
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Standardize “pre-release” wording in prose. The document mixes “pre-release” and “prerelease.” Keep 🧰 Tools🪛 LanguageTool[uncategorized] ~155-~155: Do not mix variants of the same word (‘prerelease’ and ‘pre-release’) within a single text. (EN_WORD_COHERENCY) 🤖 Prompt for AI Agents |
||
| ## Rollback | ||
|
|
||
| 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. | ||
|
|
||
| **1. Communicate immediately** with the last known-good version and the broken version to avoid. | ||
|
|
||
| **2. Optionally hide the broken release from the GitHub Releases UI:** | ||
|
|
||
| ```bash | ||
| # Mark as pre-release so it no longer shows as the "latest" release | ||
| gh release edit engine/v<broken-version> --prerelease | ||
|
|
||
| # Or move to draft (still accessible by direct URL/tag, just hidden from listing) | ||
| gh release edit engine/v<broken-version> --draft | ||
| ``` | ||
|
|
||
| > These do not retract published Docker images or Helm charts from GHCR. Downstream users who have already pulled the image are not affected. | ||
|
|
||
| **3. Cut a patch release** as the authoritative fix via the normal changeset flow. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Design: CI/CD Pipeline — test, vet, lint | ||
|
|
||
| > Linear issue: [DVFLW-275](https://linear.app/dvflw/issue/DVFLW-275/cicd-pipeline-test-vet-lint) | ||
| > Date: 2026-03-18 | ||
|
|
||
| ## Goal | ||
|
|
||
| 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. | ||
|
|
||
| ## Acceptance Criteria | ||
|
|
||
| - `go test ./...` runs on every push and PR | ||
| - `go vet ./...` and `golangci-lint` run on every push and PR | ||
| - Integration tests run against Postgres via testcontainers (when added) | ||
| - CI fails on lint errors or test failures | ||
|
|
||
| ## Files | ||
|
|
||
| - Create: `.github/workflows/ci.yml` | ||
| - Create: `.golangci.yml` | ||
| - Modify: `README.md` — add CI badge | ||
|
|
||
| ## GitHub Actions Workflow | ||
|
|
||
| `.github/workflows/ci.yml` — triggered on push and pull_request to all branches. | ||
|
|
||
| Three parallel jobs on `ubuntu-latest` with Go 1.24: | ||
|
|
||
| ### test | ||
| - Checkout, setup Go | ||
| - `go test -race ./...` | ||
| - Docker available by default for future testcontainers tests | ||
|
|
||
| ### vet | ||
| - Checkout, setup Go | ||
| - `go vet ./...` | ||
|
|
||
| ### lint | ||
| - Checkout, setup Go | ||
| - Uses `golangci/golangci-lint-action` with pinned version | ||
|
|
||
| ## golangci-lint Config | ||
|
|
||
| `.golangci.yml` — conservative linter set that catches real bugs: | ||
|
|
||
| ```yaml | ||
| run: | ||
| timeout: 5m | ||
|
|
||
| linters: | ||
| enable: | ||
| - errcheck | ||
| - govet | ||
| - staticcheck | ||
| - unused | ||
| - ineffassign | ||
| - gosimple | ||
| ``` | ||
|
|
||
| ## README Badge | ||
|
|
||
| Add CI status badge at the top of README.md, right after the `# Mantle` heading: | ||
|
|
||
| ```markdown | ||
| [](https://github.com/dvflw/mantle/actions/workflows/ci.yml) | ||
| ``` | ||
|
|
||
| ## What's NOT Included | ||
|
|
||
| - Release/deploy workflows (Phase 4) | ||
| - Code coverage reporting | ||
| - Go module caching (add later if CI is slow) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Design: Config System — mantle.yaml and CLI Flags | ||
|
|
||
| > Linear issue: [DVFLW-272](https://linear.app/dvflw/issue/DVFLW-272/config-system-mantleyaml-and-cli-flags) | ||
| > Date: 2026-03-18 | ||
|
|
||
| ## Goal | ||
|
|
||
| Add a configuration system using Viper that loads settings from `mantle.yaml`, with environment variable and CLI flag overrides. | ||
|
|
||
| ## Acceptance Criteria | ||
|
|
||
| - `mantle.yaml` supports Postgres connection string, API listen address, log level | ||
| - CLI flags override config file values | ||
| - Env vars (e.g., `MANTLE_DATABASE_URL`) override both config file and defaults | ||
| - Config file path configurable via `--config` flag | ||
|
|
||
| ## Package Structure | ||
|
|
||
| ``` | ||
| internal/ | ||
| config/ | ||
| config.go # Config struct, Load() function, Viper setup | ||
| config_test.go # Tests for loading, env overrides, flag overrides, defaults | ||
| ``` | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| ## Config Struct | ||
|
|
||
| ```go | ||
| type Config struct { | ||
| Database DatabaseConfig `mapstructure:"database"` | ||
| API APIConfig `mapstructure:"api"` | ||
| Log LogConfig `mapstructure:"log"` | ||
| } | ||
|
|
||
| type DatabaseConfig struct { | ||
| URL string `mapstructure:"url"` | ||
| } | ||
|
|
||
| type APIConfig struct { | ||
| Address string `mapstructure:"address"` | ||
| } | ||
|
|
||
| type LogConfig struct { | ||
| Level string `mapstructure:"level"` | ||
| } | ||
| ``` | ||
|
|
||
| ## Default Values | ||
|
|
||
| | Key | Default | Env Var | | ||
| |-----|---------|---------| | ||
| | `database.url` | `postgres://localhost:5432/mantle?sslmode=disable` | `MANTLE_DATABASE_URL` | | ||
| | `api.address` | `:8080` | `MANTLE_API_ADDRESS` | | ||
| | `log.level` | `info` | `MANTLE_LOG_LEVEL` | | ||
|
|
||
| ## Example mantle.yaml | ||
|
|
||
| ```yaml | ||
| database: | ||
| url: "postgres://localhost:5432/mantle?sslmode=disable" | ||
|
|
||
| api: | ||
| address: ":8080" | ||
|
|
||
| log: | ||
| level: "info" | ||
| ``` | ||
|
|
||
| ## Load Function | ||
|
|
||
| `Load(configPath string) (*Config, error)` — takes the `--config` flag value. | ||
|
|
||
| Steps: | ||
| 1. Set defaults for all keys | ||
| 2. If `configPath` is provided, read that file; otherwise search current directory for `mantle.yaml` | ||
| 3. Set `MANTLE_` env prefix via `SetEnvPrefix("MANTLE")` | ||
| 4. Explicitly bind env vars with `BindEnv` for each key (not `AutomaticEnv`, which cannot reliably resolve nested keys with underscore delimiters): | ||
| - `viper.BindEnv("database.url", "MANTLE_DATABASE_URL")` | ||
| - `viper.BindEnv("api.address", "MANTLE_API_ADDRESS")` | ||
| - `viper.BindEnv("log.level", "MANTLE_LOG_LEVEL")` | ||
| 5. Unmarshal into `Config` struct and return | ||
|
|
||
| ### Missing Config File Behavior | ||
|
|
||
| - If `--config` is provided and the file does not exist: **hard error** (user explicitly asked for this file) | ||
| - If `--config` is not provided and no `mantle.yaml` in current directory: **silent fallback to defaults** (config file is optional) | ||
|
|
||
| ### Validation | ||
|
|
||
| `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. | ||
|
|
||
| ## Override Precedence | ||
|
|
||
| Viper's built-in precedence (highest to lowest): | ||
| 1. CLI flags (bound to Viper keys via `BindPFlag`) | ||
| 2. Environment variables (`MANTLE_` prefix, explicit `BindEnv` calls) | ||
| 3. Config file (`mantle.yaml`) | ||
| 4. Defaults | ||
|
|
||
| ## CLI Flags | ||
|
|
||
| Add these persistent flags on the root command: | ||
| - `--database-url` — binds to Viper key `database.url` | ||
| - `--api-address` — binds to Viper key `api.address` | ||
| - `--log-level` — binds to Viper key `log.level` | ||
|
|
||
| Flags are bound to Viper keys via `viper.BindPFlag()` inside `Load()`, which receives the `*cobra.Command` to access its flags. Updated signature: | ||
|
|
||
| `Load(cmd *cobra.Command) (*Config, error)` — reads `--config` flag from `cmd`, binds other flags to Viper. | ||
|
|
||
| ## Cobra Integration | ||
|
|
||
| In `internal/cli/root.go`: | ||
| - Add `--database-url`, `--api-address`, `--log-level` as persistent flags | ||
| - `PersistentPreRunE` on the root command calls `config.Load(cmd)` which reads `--config`, binds flags, and loads config | ||
| - The resulting `*Config` is stored on the command's context via `context.WithValue` | ||
|
|
||
| ### Context Key | ||
|
|
||
| Defined in `internal/config/`: | ||
|
|
||
| ```go | ||
| type contextKey struct{} | ||
|
|
||
| // WithContext returns a new context with the config attached. | ||
| func WithContext(ctx context.Context, cfg *Config) context.Context { | ||
| return context.WithValue(ctx, contextKey{}, cfg) | ||
| } | ||
|
|
||
| // FromContext retrieves the config from context. Returns nil if not set. | ||
| func FromContext(ctx context.Context) *Config { | ||
| cfg, _ := ctx.Value(contextKey{}).(*Config) | ||
| return cfg | ||
| } | ||
| ``` | ||
|
|
||
| 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). | ||
|
|
||
| ## Dependencies | ||
|
|
||
| - `github.com/spf13/viper` — config loading, env var binding, YAML parsing (must be added to go.mod) | ||
|
|
||
| ## What's NOT Included | ||
|
|
||
| - Semantic validation of config values (belongs in consuming code) | ||
| - Hot reload / file watching | ||
| - Config fields beyond database.url, api.address, log.level (added by future phases) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace placeholder link text with an actual URL.
Line 31 references
[the relevant issue]but does not point to a concrete issue link, so readers cannot follow up.🤖 Prompt for AI Agents