rickub/clipublic Fork 0
1a1d4302402433417a40905bf2e2a7b85504872e
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
Initial import of the rickub CLI as a standalone public project 1a1d430Unverified · on 1a1d4302402433417a40905bf2e2a7b85504872e · Olivier Girardot · 9h ago
RELEASING.md · 228 lines · 9.2 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.


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

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

---

## ⚠️ 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 <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 `/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.