# 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.