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

22 KiB

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:

var (
	version = "dev"
	commit  = "none"
	date    = "unknown"
)

Add a newVersionCmd function at the bottom of the file:

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:

root.AddCommand(newVersionCmd())
  • Step 2: Verify completion subcommand is available

Cobra registers a completion command automatically unless disabled. Run:

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
go run ./cmd/archduino version

Expected output:

archduino dev
commit: none
date:   unknown

Then verify ldflag injection works:

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
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
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:

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
command -v goreleaser || go install github.com/goreleaser/goreleaser/v2@latest
goreleaser --version

Expected: prints a version ≥ v2.

  • Step 3: Validate the config
cd /home/marvin/git-clones/ArduinoArchSetup
goreleaser check

Expected: • config is valid. If errors, fix them before proceeding.

  • Step 4: Run a snapshot build
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
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
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
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

command -v svu || go install github.com/caarlos0/svu@latest
svu --version

Expected: prints a version.

  • Step 2: Write the Makefile

Create 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
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
# 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
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:

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

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
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:

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

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:

head -3 install.sh.legacy

Then use Edit to prepend:

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

grep -n -A 20 -i "install" README.md | head -40

Replace whatever install instructions currently exist with a block that contains:

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

make build
./archduino --help

Releasing (maintainers only):

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
make clean
make snapshot
ls dist/

Expected: checksums.txt, two .tar.gz archives, build dirs.

  • Step 2: Verify tarball contents match bootstrap.sh expectations
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
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
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
# 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:

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:

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.