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 |
every pull request, and pushes to main |
gofmt check, go vet, go test, go build |
.github/workflows/rc.yml |
every push to main |
tests, cross-compiles all four targets, publishes a prerelease v<BASE>-rc.<run_number> |
.github/workflows/release.yml |
manual only (workflow_dispatch) |
tests, cross-compiles, publishes a real release v<version> |
Both release-producing workflows call the same script,
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:
-
web/ciproxy/proxy.go— add the release shim prefix toallowedPathPrefixes: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 } -
web/ciproxy/proxy.go—allowedMethodsis currently
GET / HEAD / POST / PUTonly. Release creation and asset upload are
POST, but editing a release isPATCHand removing an asset isDELETE;
add whichever verbs you intend to support. -
web/ciproxy/proxy_test.go— add coverage for the new prefix: a/gh/repos/…
path is allowed, a non-/ghpath is still denied, and path traversal such as
/gh/../loginis still refused (Allowedrejects anything non-canonical).
Also re-runweb/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 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.ymlreads it to name release candidates: withVERSION=0.1.0, the
17th push tomainpublishes the prereleasev0.1.0-rc.17.release.ymlcompares 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:
go test ./...scripts/build-dist.sh <BASE>-rc.<run_number>- publishes a prerelease at tag
v<BASE>-rc.<run_number>, with the four
.tar.gzarchives andSHA256SUMSattached.
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
- Make sure
mainis green and the latest RC is the build you want to ship. - If needed, land a PR bumping
VERSIONto the version you are
about to release. - In the rickub web UI, open Actions → Release → Run workflow.
- Pick the branch/ref (normally
main), enter the version as bare
MAJOR.MINOR.PATCH— e.g.1.2.3, no leadingv— leavedry_run
unchecked, and run it.
The workflow then:
- validates the format and refuses a leading
vor anything that is not
X.Y.Z; - refuses to continue if the tag
v<version>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 vetandgo test; - builds the four archives +
SHA256SUMSviascripts/build-dist.sh; - publishes a non-prerelease release at
v<version>, 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_<version>_linux_amd64.tar.gz
rickub_<version>_linux_arm64.tar.gz
rickub_<version>_darwin_amd64.tar.gz
rickub_<version>_darwin_arm64.tar.gz
SHA256SUMS
<version> 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=<version>" (plus -s -w), so
rickub version reports the release version instead of the dev default.
To verify a download:
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
# 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
|