| Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 8h ago | 1 | # Releasing `rickub` |
| 2 | |
| 3 | This repository ships three CI workflows, all running on rickub's own |
| 4 | GitHub-Actions-compatible CI: |
| 5 | |
| 6 | | Workflow | Trigger | What it does | |
| 7 | | --- | --- | --- | |
| 8 | | [`.github/workflows/ci.yml`](.github/workflows/ci.yml) | every pull request, and pushes to `main` | `gofmt` check, `go vet`, `go test`, `go build` | |
| 9 | | [`.github/workflows/rc.yml`](.github/workflows/rc.yml) | every push to `main` | tests, cross-compiles all four targets, publishes a **prerelease** `v<BASE>-rc.<run_number>` | |
| 10 | | [`.github/workflows/release.yml`](.github/workflows/release.yml) | **manual only** (`workflow_dispatch`) | tests, cross-compiles, publishes a real release `v<version>` | |
| 11 | |
| 12 | Both release-producing workflows call the same script, |
| 13 | [`scripts/build-dist.sh`](scripts/build-dist.sh), so an RC and a real release |
| 14 | are byte-for-byte the same pipeline with a different version stamp. |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## ⚠️ Prerequisite: the CI proxy must allow `/gh/` |
| 19 | |
| 20 | **Release publishing from CI fails until an operator changes the rickub server.** |
| 21 | |
| 22 | The CI guest reaches the outside world through `web/ciproxy`, whose allowlist |
| 23 | today only covers the artifact/cache results service. The release REST shim |
| 24 | lives under the `/gh` prefix (`$GITHUB_API_URL`), and the proxy currently |
| 25 | answers **403** for those paths, so `softprops/action-gh-release@v2` cannot |
| 26 | create the release or upload assets. |
| 27 | |
| 28 | To unblock it, in the **rickhub server repo**: |
| 29 | |
| 30 | 1. `web/ciproxy/proxy.go` — add the release shim prefix to `allowedPathPrefixes`: |
| 31 | |
| 32 | ```go |
| 33 | var allowedPathPrefixes = []string{ |
| 34 | "/twirp/github.actions.results.api.v1.ArtifactService/", |
| 35 | "/twirp/github.actions.results.api.v1.CacheService/", |
| 36 | "/gh/", // GitHub-compatible REST shim: releases + asset upload |
| 37 | } |
| 38 | ``` |
| 39 | |
| 40 | 2. `web/ciproxy/proxy.go` — `allowedMethods` is currently |
| 41 | `GET / HEAD / POST / PUT` only. Release *creation* and asset upload are |
| 42 | `POST`, but editing a release is `PATCH` and removing an asset is `DELETE`; |
| 43 | add whichever verbs you intend to support. |
| 44 | |
| 45 | 3. `web/ciproxy/proxy_test.go` — add coverage for the new prefix: a `/gh/repos/…` |
| 46 | path is allowed, a non-`/gh` path is still denied, and path traversal such as |
| 47 | `/gh/../login` is still refused (`Allowed` rejects anything non-canonical). |
| 48 | Also re-run `web/ci_proxy_allowlist_conformance_test.go`, which cross-checks |
| 49 | the allowlist against the registered mux routes. |
| 50 | |
| 51 | Until that ships, `rc.yml` and `release.yml` will build and upload their |
| 52 | `actions/upload-artifact` bundle successfully and then fail on the |
| 53 | "Publish …" step. The archives are still downloadable from the run's artifacts, |
| 54 | so you can release by hand in the meantime. |
| 55 | |
| 56 | You can rehearse the whole pipeline without touching the shim by dispatching |
| 57 | `release.yml` with **`dry_run: true`** — it builds, tests, verifies the tag is |
| 58 | free and uploads the artifacts, but never calls the release API. |
| 59 | |
| 60 | --- |
| 61 | |
| 62 | ## The `VERSION` file |
| 63 | |
| 64 | [`VERSION`](VERSION) at the repository root holds the **next** version to be |
| 65 | released, as a bare `MAJOR.MINOR.PATCH` string with no leading `v`: |
| 66 | |
| 67 | ``` |
| 68 | 0.1.0 |
| 69 | ``` |
| 70 | |
| 71 | * `rc.yml` reads it to name release candidates: with `VERSION` = `0.1.0`, the |
| 72 | 17th push to `main` publishes the prerelease `v0.1.0-rc.17`. |
| 73 | * `release.yml` compares it against the dispatched version and emits a |
| 74 | **warning** (not a failure) on a mismatch, so an out-of-band hotfix is still |
| 75 | possible. |
| 76 | |
| 77 | **Bumping `VERSION` is a normal pull request.** After releasing `v0.1.0`, open a |
| 78 | PR setting `VERSION` to `0.2.0` (or `0.1.1`); once it merges, RCs on `main` |
| 79 | start counting toward the next release. Nothing in CI ever writes to this file. |
| 80 | |
| 81 | --- |
| 82 | |
| 83 | ## Release candidates (automatic) |
| 84 | |
| 85 | Every push to `main` runs `rc.yml`: |
| 86 | |
| 87 | 1. `go test ./...` |
| 88 | 2. `scripts/build-dist.sh <BASE>-rc.<run_number>` |
| 89 | 3. publishes a **prerelease** at tag `v<BASE>-rc.<run_number>`, with the four |
| 90 | `.tar.gz` archives and `SHA256SUMS` attached. |
| 91 | |
| 92 | The tag does not exist beforehand — the release shim creates it at |
| 93 | `target_commitish`, which the workflow sets to `${{ github.sha }}`, so CI cuts |
| 94 | its own RC tags. |
| 95 | |
| 96 | `concurrency: { group: rc, cancel-in-progress: true }` means only the newest |
| 97 | push to `main` is building at any moment; superseded RC runs are cancelled, so |
| 98 | RC numbers are not contiguous. That is expected — `run_number` is the source of |
| 99 | uniqueness, not a count of published RCs. |
| 100 | |
| 101 | Releases created by CI do **not** re-trigger workflows (the server has a |
| 102 | recursion guard), so an RC never kicks off another build. |
| 103 | |
| 104 | --- |
| 105 | |
| 106 | ## Cutting a real release |
| 107 | |
| 108 | 1. Make sure `main` is green and the latest RC is the build you want to ship. |
| 109 | 2. If needed, land a PR bumping [`VERSION`](VERSION) to the version you are |
| 110 | about to release. |
| 111 | 3. In the rickub web UI, open **Actions → Release → Run workflow**. |
| 112 | 4. Pick the branch/ref (normally `main`), enter the version as bare |
| 113 | `MAJOR.MINOR.PATCH` — e.g. `1.2.3`, **no leading `v`** — leave `dry_run` |
| 114 | unchecked, and run it. |
| 115 | |
| 116 | The workflow then: |
| 117 | |
| 118 | * validates the format and refuses a leading `v` or anything that is not |
| 119 | `X.Y.Z`; |
| 120 | * refuses to continue if the tag `v<version>` already exists locally or on the |
| 121 | remote, and additionally probes the releases API for that tag (a non-200, |
| 122 | non-404 answer is only a warning — the git tag check is the real gate); |
| 123 | * runs `go vet` and `go test`; |
| 124 | * builds the four archives + `SHA256SUMS` via `scripts/build-dist.sh`; |
| 125 | * publishes a non-prerelease release at `v<version>`, targeting |
| 126 | `${{ github.sha }}` — the exact commit of the ref you dispatched — so the tag |
| 127 | is created at that commit. |
| 128 | |
| 129 | There is no approval gate: rickub CI silently drops `environment:`, so the |
| 130 | permission to run this workflow *is* the permission to release. |
| 131 | |
| 132 | ### ⚠️ Use the web UI, not the dispatch API |
| 133 | |
| 134 | `release.yml` is the **only** workflow in this repository that declares |
| 135 | `workflow_dispatch`, and it must stay that way. |
| 136 | |
| 137 | The rickub dispatch API — `POST /api/v1/repos/{owner}/{repo}/actions/dispatch` |
| 138 | and the `rickub run dispatch` CLI — does not take a workflow name: it fires |
| 139 | **every** `workflow_dispatch` workflow on the ref. If a second dispatchable |
| 140 | workflow were added here, one `rickub run dispatch` would start both. Keeping |
| 141 | the trigger unique makes that failure mode impossible. |
| 142 | |
| 143 | The web UI's **Run workflow** button *can* target a single workflow, so it is |
| 144 | the supported way to cut a release. If you must use the API, remember it will |
| 145 | run `release.yml` (and only `release.yml`, as long as this rule holds), and it |
| 146 | still needs the `version` input. |
| 147 | |
| 148 | If you ever need a second manually-triggered pipeline, give it a |
| 149 | `workflow_call` trigger and invoke it as a reusable workflow from `release.yml` |
| 150 | rather than adding another `workflow_dispatch`. |
| 151 | |
| 152 | --- |
| 153 | |
| 154 | ## Artifacts |
| 155 | |
| 156 | `scripts/build-dist.sh` produces, in `dist/`: |
| 157 | |
| 158 | ``` |
| 159 | rickub_<version>_linux_amd64.tar.gz |
| 160 | rickub_<version>_linux_arm64.tar.gz |
| 161 | rickub_<version>_darwin_amd64.tar.gz |
| 162 | rickub_<version>_darwin_arm64.tar.gz |
| 163 | SHA256SUMS |
| 164 | ``` |
| 165 | |
| 166 | `<version>` is the bare version (no `v`): `1.2.3` for a release, |
| 167 | `0.1.0-rc.17` for a candidate. Each archive contains the `rickub` binary at the |
| 168 | top level plus `README.md` (and `LICENSE`, automatically, once this repo has |
| 169 | one). |
| 170 | |
| 171 | All four are cross-compiled on a single `ubuntu-latest` runner with |
| 172 | `CGO_ENABLED=0` and `-trimpath`. The rickub runner fleet is Linux/amd64 only — |
| 173 | there are no macOS or arm64 hosts — so the darwin and arm64 binaries are never |
| 174 | executed by CI. Keeping the CLI pure Go is what makes this work; introducing |
| 175 | cgo would break three of the four targets. |
| 176 | |
| 177 | The version is stamped with |
| 178 | `-ldflags "-X rickub.com/rickub/cli/cmd.Version=<version>"` (plus `-s -w`), so |
| 179 | `rickub version` reports the release version instead of the `dev` default. |
| 180 | |
| 181 | To verify a download: |
| 182 | |
| 183 | ```sh |
| 184 | sha256sum -c SHA256SUMS --ignore-missing |
| 185 | ``` |
| 186 | |
| 187 | Both workflows also upload `dist/` via `actions/upload-artifact@v4`, so the |
| 188 | binaries are retrievable from the run page even if the release API call fails. |
| 189 | |
| 190 | --- |
| 191 | |
| 192 | ## Building locally |
| 193 | |
| 194 | ```sh |
| 195 | # same script CI runs |
| 196 | scripts/build-dist.sh 1.2.3 |
| 197 | |
| 198 | # single local binary |
| 199 | go build -ldflags "-X rickub.com/rickub/cli/cmd.Version=$(cat VERSION)-dev" -o rickub . |
| 200 | ``` |
| 201 | |
| 202 | The script needs Go and `tar`; it uses `sha256sum` where available and falls |
| 203 | back to `shasum -a 256` on macOS. It stages into a temporary directory and |
| 204 | leaves nothing untracked behind — `dist/` is already in `.gitignore`. |
| 205 | |
| 206 | --- |
| 207 | |
| 208 | ## Troubleshooting |
| 209 | |
| 210 | **`403` publishing the release.** The `/gh` ciproxy allowlist prerequisite at |
| 211 | the top of this document has not landed yet. |
| 212 | |
| 213 | **`Resource not accessible by integration` / `404` on release creation.** The |
| 214 | `GITHUB_TOKEN` is read-only by default; the workflow must declare |
| 215 | `permissions: { contents: write }`. Both `rc.yml` and `release.yml` do. |
| 216 | |
| 217 | **`422` uploading an asset.** An asset with that name already exists on the |
| 218 | release — the shim rejects duplicates. This normally means a partially |
| 219 | completed run is being retried; delete the release (or the asset) and re-run. |
| 220 | |
| 221 | **Editing a release via `PATCH`.** The shim reads `draft` and `prerelease` |
| 222 | unconditionally, so **always send both fields** in a PATCH body or you will |
| 223 | silently flip a release to draft. |
| 224 | |
| 225 | **Nothing to release / `make_latest` ignored.** The shim does not implement |
| 226 | `make_latest` or `generate_release_notes`, and there is no |
| 227 | `GET /releases/latest`. The workflows therefore write their own release notes |
| 228 | and never ask the API to pick a "latest" release. |