rickub/clipublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
ci: use the marketplace actions with plain @vN tags, as a normal job would 900561eUnverified · on main · Olivier Girardot · Sep 2, 2026
RELEASING.md · 224 lines · 9.4 KBmarkdown
Blame HistoryOpen raw

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.


How the pipeline reaches the release API

The workflows are written as ordinary GitHub Actions jobs — actions/checkout@v4,
actions/setup-go@v5, actions/upload-artifact@v4,
softprops/action-gh-release@v2 — and rickub's CI is expected to run them as
such. Two platform facts matter when a run fails before or at publishing:

Fetching uses: actions. rickub's runner fetches every action from
github.com. A @vN tag is resolved with a git ls-remote against github.com;
when that call is anonymous, GitHub rate-limits it per datacenter IP and the run
dies in "Set up Go" with could not read Username for 'https://github.com'.
That is a fleet configuration matter, not a workflow one: the runner
authenticates those fetches when RICKUB_ACTIONS_GITHUB_TOKEN is set
(web/actions/runtime/stage.go in the rickub server). Do not work around it by
pinning SHAs here — the point of this repository's workflows is to be a normal
job.

Publishing goes through the node's CI proxy. The CI guest reaches the
control plane only through the results proxy (web/ciproxy in the rickub
server), and $GITHUB_API_URL — what softprops/action-gh-release talks to —
points at that proxy with the /gh REST shim behind it. The proxy must forward
the /gh/ prefix: shipped in rickub PR
ogirardot/rickub#3, active on a
node once its rickub-fleetd has been rebuilt from that change. On a node
running an older fleetd, both workflows build, test and upload their
actions/upload-artifact bundle, then fail on the "Publish …" step with a
403; the archives are still downloadable from the run's artifacts, so a
release can be published by hand in the meantime.

You can rehearse the whole pipeline without publishing 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.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 <BASE>-rc.<run_number>
  3. publishes a prerelease at tag v<BASE>-rc.<run_number>, 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 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<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 vet and go test;
  • builds the four archives + SHA256SUMS via scripts/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 node's CI proxy does not forward /gh/
yet — see "Publishing goes through the node's CI proxy" above.

could not read Username for 'https://github.com' in "Set up Go". The
fleet is fetching actions from GitHub anonymously and got rate-limited — see
"Fetching uses: actions" above; the fix is on the runner, not here.

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 creating the release or uploading an asset. A release for that tag,
or an asset with that name, already exists — 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
# 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<BASE>-rc.<run_number>` |
| [`.github/workflows/release.yml`](.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`](scripts/build-dist.sh), so an RC and a real release
are byte-for-byte the same pipeline with a different version stamp.

---

## How the pipeline reaches the release API

The workflows are written as ordinary GitHub Actions jobs — `actions/checkout@v4`,
`actions/setup-go@v5`, `actions/upload-artifact@v4`,
`softprops/action-gh-release@v2` — and rickub's CI is expected to run them as
such. Two platform facts matter when a run fails before or at publishing:

**Fetching `uses:` actions.** rickub's runner fetches every action from
github.com. A `@vN` tag is resolved with a `git ls-remote` against github.com;
when that call is anonymous, GitHub rate-limits it per datacenter IP and the run
dies in "Set up Go" with `could not read Username for 'https://github.com'`.
That is a fleet configuration matter, not a workflow one: the runner
authenticates those fetches when `RICKUB_ACTIONS_GITHUB_TOKEN` is set
(`web/actions/runtime/stage.go` in the rickub server). Do not work around it by
pinning SHAs here — the point of this repository's workflows is to be a normal
job.

**Publishing goes through the node's CI proxy.** The CI guest reaches the
control plane only through the results proxy (`web/ciproxy` in the rickub
server), and `$GITHUB_API_URL` — what `softprops/action-gh-release` talks to —
points at that proxy with the `/gh` REST shim behind it. The proxy must forward
the `/gh/` prefix: shipped in rickub PR
[ogirardot/rickub#3](https://github.com/ogirardot/rickub/pull/3), active on a
node once its `rickub-fleetd` has been rebuilt from that change. On a node
running an older fleetd, both workflows build, test and upload their
`actions/upload-artifact` bundle, then fail on the "Publish …" step with a
`403`; the archives are still downloadable from the run's artifacts, so a
release can be published by hand in the meantime.

You can rehearse the whole pipeline without publishing 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 <BASE>-rc.<run_number>`
3. publishes a **prerelease** at tag `v<BASE>-rc.<run_number>`, 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<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 vet` and `go test`;
* builds the four archives + `SHA256SUMS` via `scripts/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:

```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 node's CI proxy does not forward `/gh/`
yet — see "Publishing goes through the node's CI proxy" above.

**`could not read Username for 'https://github.com'` in "Set up Go".** The
fleet is fetching actions from GitHub anonymously and got rate-limited — see
"Fetching `uses:` actions" above; the fix is on the runner, not here.

**`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` creating the release or uploading an asset.** A release for that tag,
or an asset with that name, already exists — 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.