From 5fe8cb276789373ff59de8b8e6e858f5d6e40f20 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 20:58:02 +0200 Subject: [PATCH] docs(plan): Plan 4c release pipeline implementation plan --- .../plans/2026-04-14-release-pipeline.md | 789 ++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-14-release-pipeline.md diff --git a/docs/superpowers/plans/2026-04-14-release-pipeline.md b/docs/superpowers/plans/2026-04-14-release-pipeline.md new file mode 100644 index 0000000..5de5909 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-release-pipeline.md @@ -0,0 +1,789 @@ +# Release Pipeline Implementation Plan (Plan 4c) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship versioned Archduino releases to self-hosted Gitea with one-command `make release`, and let users install via `curl | bash`. + +**Architecture:** `bootstrap.sh` (curl-piped installer) + `.goreleaser.yaml` (cross-compile/archive/publish) + `Makefile` (svu-driven bump) + `.gitea/workflows/release.yml` (tag-triggered CI). Binary version injected via `-ldflags` into existing `main.version` var. + +**Tech Stack:** Go 1.x, goreleaser v2, svu, Gitea Actions, cobra (already in tree). + +**Spec:** `docs/superpowers/specs/2026-04-14-release-pipeline-design.md` + +--- + +## File Structure + +**New files:** +- `bootstrap.sh` — curl-piped installer +- `.goreleaser.yaml` — goreleaser config +- `Makefile` — release/snapshot/completions/build targets +- `.gitea/workflows/release.yml` — tag-triggered release workflow +- `LICENSE` — repo license (MIT; confirm with user during Task 7) +- `.gitignore` additions: `dist/`, `completions/` + +**Modified files:** +- `cmd/archduino/main.go` — add `commit`/`date` ldflag-injected vars; ensure `version` cobra subcommand prints them; ensure `completion` subcommand registered +- `README.md` — install-via-bootstrap section, `svu` dev note + +**Removed/archived files:** +- `install.sh` → `install.sh.legacy` (kept for reference, renamed so it's not confused with entry point) + +--- + +## Task 1: Build-info ldflag wiring + version subcommand + +**Files:** +- Modify: `cmd/archduino/main.go` +- Test: manual smoke (no Go test — version output is a trivial glue) + +- [ ] **Step 1: Add commit/date vars and `version` subcommand** + +Edit `cmd/archduino/main.go`. Change the `version` declaration and add two new package-level vars just below it: + +```go +var ( + version = "dev" + commit = "none" + date = "unknown" +) +``` + +Add a `newVersionCmd` function at the bottom of the file: + +```go +func newVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print version, commit, and build date", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("archduino %s\ncommit: %s\ndate: %s\n", version, commit, date) + }, + } +} +``` + +Register it in `main()` alongside the other `root.AddCommand` calls: + +```go +root.AddCommand(newVersionCmd()) +``` + +- [ ] **Step 2: Verify `completion` subcommand is available** + +Cobra registers a `completion` command automatically unless disabled. Run: + +```bash +cd /home/marvin/git-clones/ArduinoArchSetup +go run ./cmd/archduino completion bash | head -5 +``` + +Expected: bash completion script output (starts with `# bash completion V2 for archduino`). + +If the command is missing, it means `CompletionOptions.DisableDefaultCmd` was set somewhere — grep for it and remove. + +- [ ] **Step 3: Smoke-test the version command** + +```bash +go run ./cmd/archduino version +``` + +Expected output: +``` +archduino dev +commit: none +date: unknown +``` + +Then verify ldflag injection works: +```bash +go run -ldflags "-X main.version=v0.9.9 -X main.commit=abc123 -X main.date=2026-04-14" ./cmd/archduino version +``` + +Expected: +``` +archduino v0.9.9 +commit: abc123 +date: 2026-04-14 +``` + +- [ ] **Step 4: Commit** + +```bash +git add cmd/archduino/main.go +git commit -m "feat(cmd): add version subcommand with ldflag-injected build info" +``` + +--- + +## Task 2: .gitignore + LICENSE + completions dir + +**Files:** +- Modify: `.gitignore` +- Create: `LICENSE` +- Create: `completions/.gitkeep` + +- [ ] **Step 1: Update .gitignore** + +Append to `.gitignore` (create the file if missing): + +``` +# goreleaser output +dist/ + +# generated shell completions +completions/ +``` + +- [ ] **Step 2: Ask user for LICENSE choice** + +Before writing `LICENSE`, pause and ask the user which license (MIT, Apache-2.0, GPL-3.0, etc.). Default suggestion: MIT. Do not proceed with a guess. + +Once confirmed, write `LICENSE` with the standard text for the chosen license. For MIT: + +``` +MIT License + +Copyright (c) 2026 MDietermann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +- [ ] **Step 3: Commit** + +```bash +git add .gitignore LICENSE +git commit -m "chore: add LICENSE and ignore dist/ and completions/" +``` + +--- + +## Task 3: .goreleaser.yaml + +**Files:** +- Create: `.goreleaser.yaml` + +- [ ] **Step 1: Write the goreleaser config** + +Create `.goreleaser.yaml`: + +```yaml +version: 2 + +project_name: archduino + +before: + hooks: + - mkdir -p completions + - sh -c "go run ./cmd/archduino completion bash > completions/archduino.bash" + - sh -c "go run ./cmd/archduino completion zsh > completions/_archduino" + - sh -c "go run ./cmd/archduino completion fish > completions/archduino.fish" + +builds: + - id: archduino + main: ./cmd/archduino + binary: archduino + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{ .Version }} + - -X main.commit={{ .Commit }} + - -X main.date={{ .Date }} + +archives: + - id: default + name_template: "archduino_{{ .Version }}_linux_{{ .Arch }}" + formats: [tar.gz] + files: + - README.md + - LICENSE + - src: completions/archduino.bash + dst: completions/archduino.bash + - src: completions/_archduino + dst: completions/_archduino + - src: completions/archduino.fish + dst: completions/archduino.fish + +checksum: + name_template: "checksums.txt" + algorithm: sha256 + +changelog: + use: git + sort: asc + filters: + exclude: + - '^chore:' + - '^docs:' + - '^test:' + - '^ci:' + - '^style:' + - '^refactor:' + - 'Merge pull request' + - 'Merge branch' + groups: + - title: Features + regexp: '^feat(\(.*\))?:' + order: 0 + - title: Bug fixes + regexp: '^(fix|perf)(\(.*\))?:' + order: 1 + - title: Others + order: 999 + +release: + gitea_urls: + api: https://gitlab.hive-brain.de/api/v1 + download: https://gitlab.hive-brain.de + draft: false + prerelease: auto +``` + +- [ ] **Step 2: Install goreleaser locally if not present** + +```bash +command -v goreleaser || go install github.com/goreleaser/goreleaser/v2@latest +goreleaser --version +``` + +Expected: prints a version ≥ v2. + +- [ ] **Step 3: Validate the config** + +```bash +cd /home/marvin/git-clones/ArduinoArchSetup +goreleaser check +``` + +Expected: `• config is valid`. If errors, fix them before proceeding. + +- [ ] **Step 4: Run a snapshot build** + +```bash +goreleaser release --snapshot --clean +``` + +Expected: `dist/` populated with two tar.gz archives (`archduino_*_linux_amd64.tar.gz`, `archduino_*_linux_arm64.tar.gz`), `checksums.txt`, and extracted binary subdirs. No errors. + +- [ ] **Step 5: Inspect archive contents** + +```bash +tar -tzf dist/archduino_*_linux_amd64.tar.gz +``` + +Expected file list: `archduino`, `README.md`, `LICENSE`, `completions/archduino.bash`, `completions/_archduino`, `completions/archduino.fish`. + +- [ ] **Step 6: Verify binary has injected version** + +```bash +tar -xzf dist/archduino_*_linux_amd64.tar.gz -C /tmp/archduino-smoke --one-top-level +/tmp/archduino-smoke/archduino_*_linux_amd64/archduino version +``` + +Expected: prints a snapshot version (looks like `0.0.0-next-` or similar) and a real commit + date. + +- [ ] **Step 7: Commit** + +```bash +git add .goreleaser.yaml +git commit -m "build: add goreleaser config for Gitea releases" +``` + +--- + +## Task 4: Makefile + +**Files:** +- Create: `Makefile` + +- [ ] **Step 1: Install svu locally** + +```bash +command -v svu || go install github.com/caarlos0/svu@latest +svu --version +``` + +Expected: prints a version. + +- [ ] **Step 2: Write the Makefile** + +Create `Makefile`: + +```makefile +.PHONY: build snapshot release completions clean help + +GO ?= go +BIN := archduino + +help: ## Show this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-14s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +build: ## Local dev build into ./archduino + $(GO) build -o $(BIN) ./cmd/archduino + +completions: ## Regenerate shell completions into completions/ + @mkdir -p completions + $(GO) run ./cmd/archduino completion bash > completions/archduino.bash + $(GO) run ./cmd/archduino completion zsh > completions/_archduino + $(GO) run ./cmd/archduino completion fish > completions/archduino.fish + +snapshot: ## Local goreleaser dry run into dist/ + goreleaser release --snapshot --clean + +clean: ## Remove build artifacts + rm -rf dist/ completions/ $(BIN) + +release: ## Compute next SemVer from commits, tag, and push + @command -v svu >/dev/null || { echo "svu not found. Run: go install github.com/caarlos0/svu@latest"; exit 1; } + @test -z "$$(git status --porcelain)" || { echo "working tree is dirty; commit or stash first"; exit 1; } + @branch=$$(git rev-parse --abbrev-ref HEAD); \ + if [ "$$branch" != "master" ]; then echo "must be on master (currently on $$branch)"; exit 1; fi + @current=$$(svu current 2>/dev/null || echo "none"); \ + next=$$(svu next); \ + echo "Current: $$current"; \ + echo "Next: $$next"; \ + echo; \ + echo "Commits since $$current:"; \ + if [ "$$current" = "none" ]; then git log --oneline; else git log --oneline $$current..HEAD; fi; \ + echo; \ + printf "Tag %s and push? [y/N] " "$$next"; read ans; \ + if [ "$$ans" != "y" ] && [ "$$ans" != "Y" ]; then echo "aborted"; exit 1; fi; \ + git tag "$$next" && git push origin "$$next" +``` + +- [ ] **Step 3: Smoke-test non-destructive targets** + +```bash +make help +make build && ./archduino version +make completions && ls completions/ +``` + +Expected: `help` lists targets; `build` produces `./archduino`; `completions` generates three files. + +- [ ] **Step 4: Dry-run the release guard** + +```bash +# With dirty tree (touch a file first if tree is clean) +touch /tmp/dirt && make release || echo "guard worked as expected" +``` + +Expected: aborts with "working tree is dirty". Do NOT actually run `make release` to completion yet — that creates a real tag. + +- [ ] **Step 5: Commit** + +```bash +git add Makefile +git commit -m "build: add Makefile with release, snapshot, completions targets" +``` + +--- + +## Task 5: bootstrap.sh + +**Files:** +- Create: `bootstrap.sh` + +- [ ] **Step 1: Write bootstrap.sh** + +Create `bootstrap.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +REPO="MDietermann/Archduino" +BASE_URL="${ARCHDUINO_RELEASE_BASE:-https://gitlab.hive-brain.de}" +API_URL="${BASE_URL}/api/v1" + +err() { echo "bootstrap: error: $*" >&2; exit 1; } +msg() { echo "==> $*"; } + +# Arch gate +[ -f /etc/arch-release ] || err "Archduino v1 supports Arch Linux only." + +# Arch detect +case "$(uname -m)" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) err "unsupported architecture: $(uname -m)" ;; +esac + +# Prereqs +msg "Installing prerequisites (sudo, git, curl, ca-certificates)" +sudo pacman -Sy --needed --noconfirm sudo git curl ca-certificates + +# Discover latest release +msg "Querying latest release from Gitea" +API_ENDPOINT="${API_URL}/repos/${REPO}/releases/latest" +RELEASE_JSON=$(curl -fsSL "$API_ENDPOINT") || err "failed to fetch ${API_ENDPOINT}" + +# Extract tag_name without requiring jq +TAG=$(printf '%s' "$RELEASE_JSON" | grep -o '"tag_name":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/') +[ -n "$TAG" ] || err "could not parse tag_name from release metadata" +VERSION="${TAG#v}" +msg "Latest release: ${TAG}" + +# Download +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +TARBALL="archduino_${VERSION}_linux_${ARCH}.tar.gz" +CHECKSUMS="checksums.txt" +DL_BASE="${BASE_URL}/${REPO}/releases/download/${TAG}" + +msg "Downloading ${TARBALL}" +curl -fsSL -o "${TMP}/${TARBALL}" "${DL_BASE}/${TARBALL}" || err "tarball download failed" +curl -fsSL -o "${TMP}/${CHECKSUMS}" "${DL_BASE}/${CHECKSUMS}" || err "checksums download failed" + +# Verify +msg "Verifying checksum" +(cd "$TMP" && sha256sum --check --ignore-missing "$CHECKSUMS") || err "checksum verification failed" + +# Extract +msg "Extracting" +tar -xzf "${TMP}/${TARBALL}" -C "$TMP" + +# Install +msg "Installing to /usr/local/bin (sudo)" +sudo install -m 0755 "${TMP}/archduino" /usr/local/bin/archduino + +msg "Installing shell completions" +sudo install -Dm 0644 "${TMP}/completions/archduino.bash" /usr/share/bash-completion/completions/archduino +sudo install -Dm 0644 "${TMP}/completions/_archduino" /usr/share/zsh/site-functions/_archduino +sudo install -Dm 0644 "${TMP}/completions/archduino.fish" /usr/share/fish/vendor_completions.d/archduino.fish + +msg "Launching: archduino install $*" +exec /usr/local/bin/archduino install "$@" +``` + +- [ ] **Step 2: Make it executable and shellcheck it** + +```bash +chmod +x bootstrap.sh +command -v shellcheck && shellcheck bootstrap.sh || echo "(shellcheck not installed — skipping)" +``` + +Expected: `shellcheck` passes with no warnings. If warnings appear, fix them (common ones: quote expansions, etc.). + +- [ ] **Step 3: Smoke-test the non-root path in a container** + +This step requires a snapshot tarball. Skip if Docker is unavailable — the workflow will validate end-to-end. + +```bash +make snapshot +# Serve dist/ locally +(cd dist && python3 -m http.server 8000 &) ; SERVER_PID=$! +sleep 1 +# In a throwaway Arch container, verify the prereq + download logic. +# Since Gitea API isn't reachable in the container, we only eyeball the +# script behavior up to the API call: +docker run --rm -v "$PWD/bootstrap.sh:/bootstrap.sh:ro" archlinux bash -c \ + 'head -40 /bootstrap.sh && echo "--- script is syntactically valid shell ---" && bash -n /bootstrap.sh' +kill $SERVER_PID 2>/dev/null || true +``` + +Expected: `bash -n` reports no syntax errors. (Full e2e test is gated on Task 8.) + +- [ ] **Step 4: Commit** + +```bash +git add bootstrap.sh +git commit -m "feat: add bootstrap.sh curl-piped installer" +``` + +--- + +## Task 6: .gitea/workflows/release.yml + +**Files:** +- Create: `.gitea/workflows/release.yml` + +- [ ] **Step 1: Write the workflow** + +Create `.gitea/workflows/release.yml`: + +```yaml +name: release + +on: + push: + tags: + - 'v*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run goreleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} +``` + +- [ ] **Step 2: Lint the YAML** + +```bash +command -v yamllint && yamllint .gitea/workflows/release.yml || python3 -c "import yaml,sys; yaml.safe_load(open('.gitea/workflows/release.yml'))" && echo "yaml ok" +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add .gitea/workflows/release.yml +git commit -m "ci: add Gitea Actions release workflow" +``` + +--- + +## Task 7: Retire install.sh and update README + +**Files:** +- Rename: `install.sh` → `install.sh.legacy` +- Modify: `README.md` + +- [ ] **Step 1: Rename install.sh** + +```bash +git mv install.sh install.sh.legacy +``` + +Add a one-line header to the top of `install.sh.legacy` explaining the rename. Use Edit with these exact lines (the first original line is `#!/bin/bash` or similar — verify and preserve): + +Read the first 3 lines first to know exact content: +```bash +head -3 install.sh.legacy +``` + +Then use Edit to prepend: +```bash +# DEPRECATED: This is the legacy Bash installer. The active installer is +# bootstrap.sh (curl-piped shim) + the archduino Go binary. Kept for reference. +``` + +Inserted as the second line (below the shebang). + +- [ ] **Step 2: Update README install section** + +Read the current install section: +```bash +grep -n -A 20 -i "install" README.md | head -40 +``` + +Replace whatever install instructions currently exist with a block that contains: + +```markdown +## Installation + +One-line install on Arch Linux: + +```bash +curl -fsSL https://gitlab.hive-brain.de/MDietermann/Archduino/raw/branch/master/bootstrap.sh | bash +``` + +This fetches the latest release, verifies its checksum, installs `archduino` +to `/usr/local/bin`, installs shell completions, and launches +`archduino install`. + +### Development + +Local build: + +```bash +make build +./archduino --help +``` + +Releasing (maintainers only): + +```bash +go install github.com/caarlos0/svu@latest +make release # prompts with computed next version, tags, pushes +``` +``` + +Use Edit to splice this in at the appropriate location. If the README has no install section, append it after the title/intro. + +- [ ] **Step 3: Commit** + +```bash +git add install.sh.legacy README.md +git rm install.sh 2>/dev/null || true # git mv already handles this +git commit -m "docs: update README for bootstrap.sh install; retire install.sh" +``` + +--- + +## Task 8: End-to-end snapshot verification + +**Files:** none (validation only) + +- [ ] **Step 1: Clean snapshot** + +```bash +make clean +make snapshot +ls dist/ +``` + +Expected: `checksums.txt`, two `.tar.gz` archives, build dirs. + +- [ ] **Step 2: Verify tarball contents match bootstrap.sh expectations** + +```bash +tar -tzf dist/archduino_*_linux_amd64.tar.gz | sort +``` + +Expected (order may vary): +``` +LICENSE +README.md +archduino +completions/_archduino +completions/archduino.bash +completions/archduino.fish +``` + +The paths must match what `bootstrap.sh` extracts and installs. If the archive layout differs (e.g. everything under a top-level dir), adjust `.goreleaser.yaml` `archives.wrap_in_directory: false` or update `bootstrap.sh`'s install paths. + +- [ ] **Step 3: Verify checksums file format** + +```bash +cat dist/checksums.txt +``` + +Expected: one line per archive, ` archduino__linux_.tar.gz`. This is what `sha256sum --check` consumes. + +- [ ] **Step 4: Bash-syntax-check bootstrap.sh against a snapshot in a container** + +```bash +docker run --rm -v "$PWD:/work:ro" archlinux bash -c ' + set -e + pacman -Sy --needed --noconfirm bash curl >/dev/null + bash -n /work/bootstrap.sh + echo "syntax ok" +' +``` + +Expected: `syntax ok`. (Full network-dependent e2e is gated on the first real release.) + +- [ ] **Step 5: Commit nothing; verification is informational** + +No commit. If any step failed, go back to the relevant task and fix. + +--- + +## Task 9: Register Gitea Actions runner and first release + +**Files:** none (operational) + +This task is operational, performed once. Coordinate with the user before executing. + +- [ ] **Step 1: Confirm with the user** + +Before running any of the steps below, confirm with the user that they want to proceed with a real release. Gitea Actions runner registration and the first tag are both irreversible-ish actions that affect the remote repo. + +- [ ] **Step 2: Register a Gitea Actions runner** + +Runner registration is a one-time step and is performed on a host the user controls (likely the Gitea server or a persistent machine). High-level steps (user runs these — agent does not execute on remote hosts): + +1. In Gitea UI → Repo Settings → Actions → Runners → generate a registration token. +2. On the runner host, install `act_runner` (https://gitea.com/gitea/act_runner). +3. `act_runner register --instance https://gitlab.hive-brain.de --token `. +4. `act_runner daemon &` (or set up a systemd unit). + +Verify in the Gitea UI that the runner shows as online. + +- [ ] **Step 3: Create a repo-scoped release token** + +1. Gitea UI → User Settings → Applications → Generate Token. +2. Scope: `write:repository` (and `write:package` if future-proofing). +3. Copy the token. +4. Repo → Settings → Actions → Secrets → New secret named `RELEASE_TOKEN`, paste the token value. + +- [ ] **Step 4: Seed the first tag** + +```bash +# Confirm branch/working-tree +git checkout master +git pull +git status +# Seed v0.1.0 manually so svu has a baseline +git tag v0.1.0 +git push origin v0.1.0 +``` + +Watch the Gitea Actions tab — the `release` workflow should pick up the tag and run goreleaser. Expected outcome: a new Release page at `https://gitlab.hive-brain.de/MDietermann/Archduino/releases/tag/v0.1.0` with two tarballs and `checksums.txt` attached. + +- [ ] **Step 5: Smoke-test bootstrap.sh against the real release** + +In a throwaway Arch container: + +```bash +docker run --rm -it archlinux bash -c ' + pacman -Sy --noconfirm curl >/dev/null + curl -fsSL https://gitlab.hive-brain.de/MDietermann/Archduino/raw/branch/master/bootstrap.sh | bash -s -- --dry-run +' +``` + +Expected: prereqs install, release detected, tarball downloaded, checksum verified, `archduino install --dry-run` runs and exits cleanly. + +- [ ] **Step 6: Merge to master** + +Once the first real release is verified, merge the worktree branch back to `master`: + +```bash +git checkout master +git merge --no-ff feat/plan4c-release +git push origin master +``` + +(Branch name may differ — use whichever worktree was created.) + +--- + +## Notes for the executing agent + +- **No Go-side tests** are created in this plan. The deliverables are configs, scripts, and a CI workflow — validation is via `goreleaser check`, `make snapshot`, `bash -n`, and `shellcheck`. +- **Conventional Commits are required from here on.** `svu` and the goreleaser changelog both depend on them. Commit messages in this plan use that format as a model. +- **Do not run `make release` during implementation** — it creates real git tags that would pollute the release history. The first real release happens in Task 9 after user confirmation. +- **If Task 3 snapshot fails** because `completion` isn't registered, revisit Task 1 Step 2 — cobra may have had its default subcommand disabled. +- **If Task 8 reveals archive layout mismatches**, the fix is in `.goreleaser.yaml` (files stanza or `wrap_in_directory`) rather than `bootstrap.sh`, because goreleaser's layout is the source of truth.