Files
Archduino/docs/superpowers/plans/2026-04-14-release-pipeline.md

790 lines
22 KiB
Markdown

# 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-<commit>` 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, `<sha256> archduino_<version>_linux_<arch>.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 <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.