# Releasing `rickub` This repository ships three CI workflows, all running on rickub's own GitHub-Actions-compatible CI: | Workflow | Trigger | What it does | | --- | --- | --- | | [`.github/workflows/ci.yml`](.github/workflows/ci.yml) | every pull request, and pushes to `main` | `gofmt` check, `go vet`, `go test`, `go build` | | [`.github/workflows/rc.yml`](.github/workflows/rc.yml) | every push to `main` | tests, cross-compiles all four targets, publishes a **prerelease** `v-rc.` | | [`.github/workflows/release.yml`](.github/workflows/release.yml) | **manual only** (`workflow_dispatch`) | tests, cross-compiles, publishes a real release `v` | Both release-producing workflows call the same script, [`scripts/build-dist.sh`](scripts/build-dist.sh), so an RC and a real release are byte-for-byte the same pipeline with a different version stamp. --- ## ⚠️ Prerequisite: the CI proxy must allow `/gh/` **Release publishing from CI fails until an operator changes the rickub server.** The CI guest reaches the outside world through `web/ciproxy`, whose allowlist today only covers the artifact/cache results service. The release REST shim lives under the `/gh` prefix (`$GITHUB_API_URL`), and the proxy currently answers **403** for those paths, so `softprops/action-gh-release@v2` cannot create the release or upload assets. To unblock it, in the **rickhub server repo**: 1. `web/ciproxy/proxy.go` — add the release shim prefix to `allowedPathPrefixes`: ```go var allowedPathPrefixes = []string{ "/twirp/github.actions.results.api.v1.ArtifactService/", "/twirp/github.actions.results.api.v1.CacheService/", "/gh/", // GitHub-compatible REST shim: releases + asset upload } ``` 2. `web/ciproxy/proxy.go` — `allowedMethods` is currently `GET / HEAD / POST / PUT` only. Release *creation* and asset upload are `POST`, but editing a release is `PATCH` and removing an asset is `DELETE`; add whichever verbs you intend to support. 3. `web/ciproxy/proxy_test.go` — add coverage for the new prefix: a `/gh/repos/…` path is allowed, a non-`/gh` path is still denied, and path traversal such as `/gh/../login` is still refused (`Allowed` rejects anything non-canonical). Also re-run `web/ci_proxy_allowlist_conformance_test.go`, which cross-checks the allowlist against the registered mux routes. Until that ships, `rc.yml` and `release.yml` will build and upload their `actions/upload-artifact` bundle successfully and then fail on the "Publish …" step. The archives are still downloadable from the run's artifacts, so you can release by hand in the meantime. You can rehearse the whole pipeline without touching the shim by dispatching `release.yml` with **`dry_run: true`** — it builds, tests, verifies the tag is free and uploads the artifacts, but never calls the release API. --- ## The `VERSION` file [`VERSION`](VERSION) at the repository root holds the **next** version to be released, as a bare `MAJOR.MINOR.PATCH` string with no leading `v`: ``` 0.1.0 ``` * `rc.yml` reads it to name release candidates: with `VERSION` = `0.1.0`, the 17th push to `main` publishes the prerelease `v0.1.0-rc.17`. * `release.yml` compares it against the dispatched version and emits a **warning** (not a failure) on a mismatch, so an out-of-band hotfix is still possible. **Bumping `VERSION` is a normal pull request.** After releasing `v0.1.0`, open a PR setting `VERSION` to `0.2.0` (or `0.1.1`); once it merges, RCs on `main` start counting toward the next release. Nothing in CI ever writes to this file. --- ## Release candidates (automatic) Every push to `main` runs `rc.yml`: 1. `go test ./...` 2. `scripts/build-dist.sh -rc.` 3. publishes a **prerelease** at tag `v-rc.`, with the four `.tar.gz` archives and `SHA256SUMS` attached. The tag does not exist beforehand — the release shim creates it at `target_commitish`, which the workflow sets to `${{ github.sha }}`, so CI cuts its own RC tags. `concurrency: { group: rc, cancel-in-progress: true }` means only the newest push to `main` is building at any moment; superseded RC runs are cancelled, so RC numbers are not contiguous. That is expected — `run_number` is the source of uniqueness, not a count of published RCs. Releases created by CI do **not** re-trigger workflows (the server has a recursion guard), so an RC never kicks off another build. --- ## Cutting a real release 1. Make sure `main` is green and the latest RC is the build you want to ship. 2. If needed, land a PR bumping [`VERSION`](VERSION) to the version you are about to release. 3. In the rickub web UI, open **Actions → Release → Run workflow**. 4. Pick the branch/ref (normally `main`), enter the version as bare `MAJOR.MINOR.PATCH` — e.g. `1.2.3`, **no leading `v`** — leave `dry_run` unchecked, and run it. The workflow then: * validates the format and refuses a leading `v` or anything that is not `X.Y.Z`; * refuses to continue if the tag `v` already exists locally or on the remote, and additionally probes the releases API for that tag (a non-200, non-404 answer is only a warning — the git tag check is the real gate); * runs `go vet` and `go test`; * builds the four archives + `SHA256SUMS` via `scripts/build-dist.sh`; * publishes a non-prerelease release at `v`, targeting `${{ github.sha }}` — the exact commit of the ref you dispatched — so the tag is created at that commit. There is no approval gate: rickub CI silently drops `environment:`, so the permission to run this workflow *is* the permission to release. ### ⚠️ Use the web UI, not the dispatch API `release.yml` is the **only** workflow in this repository that declares `workflow_dispatch`, and it must stay that way. The rickub dispatch API — `POST /api/v1/repos/{owner}/{repo}/actions/dispatch` and the `rickub run dispatch` CLI — does not take a workflow name: it fires **every** `workflow_dispatch` workflow on the ref. If a second dispatchable workflow were added here, one `rickub run dispatch` would start both. Keeping the trigger unique makes that failure mode impossible. The web UI's **Run workflow** button *can* target a single workflow, so it is the supported way to cut a release. If you must use the API, remember it will run `release.yml` (and only `release.yml`, as long as this rule holds), and it still needs the `version` input. If you ever need a second manually-triggered pipeline, give it a `workflow_call` trigger and invoke it as a reusable workflow from `release.yml` rather than adding another `workflow_dispatch`. --- ## Artifacts `scripts/build-dist.sh` produces, in `dist/`: ``` rickub__linux_amd64.tar.gz rickub__linux_arm64.tar.gz rickub__darwin_amd64.tar.gz rickub__darwin_arm64.tar.gz SHA256SUMS ``` `` is the bare version (no `v`): `1.2.3` for a release, `0.1.0-rc.17` for a candidate. Each archive contains the `rickub` binary at the top level plus `README.md` (and `LICENSE`, automatically, once this repo has one). All four are cross-compiled on a single `ubuntu-latest` runner with `CGO_ENABLED=0` and `-trimpath`. The rickub runner fleet is Linux/amd64 only — there are no macOS or arm64 hosts — so the darwin and arm64 binaries are never executed by CI. Keeping the CLI pure Go is what makes this work; introducing cgo would break three of the four targets. The version is stamped with `-ldflags "-X rickub.com/rickub/cli/cmd.Version="` (plus `-s -w`), so `rickub version` reports the release version instead of the `dev` default. To verify a download: ```sh sha256sum -c SHA256SUMS --ignore-missing ``` Both workflows also upload `dist/` via `actions/upload-artifact@v4`, so the binaries are retrievable from the run page even if the release API call fails. --- ## Building locally ```sh # same script CI runs scripts/build-dist.sh 1.2.3 # single local binary go build -ldflags "-X rickub.com/rickub/cli/cmd.Version=$(cat VERSION)-dev" -o rickub . ``` The script needs Go and `tar`; it uses `sha256sum` where available and falls back to `shasum -a 256` on macOS. It stages into a temporary directory and leaves nothing untracked behind — `dist/` is already in `.gitignore`. --- ## Troubleshooting **`403` publishing the release.** The `/gh` ciproxy allowlist prerequisite at the top of this document has not landed yet. **`Resource not accessible by integration` / `404` on release creation.** The `GITHUB_TOKEN` is read-only by default; the workflow must declare `permissions: { contents: write }`. Both `rc.yml` and `release.yml` do. **`422` uploading an asset.** An asset with that name already exists on the release — the shim rejects duplicates. This normally means a partially completed run is being retried; delete the release (or the asset) and re-run. **Editing a release via `PATCH`.** The shim reads `draft` and `prerelease` unconditionally, so **always send both fields** in a PATCH body or you will silently flip a release to draft. **Nothing to release / `make_latest` ignored.** The shim does not implement `make_latest` or `generate_release_notes`, and there is no `GET /releases/latest`. The workflows therefore write their own release notes and never ask the API to pick a "latest" release.