|
| 1 | +# Publishing Fishez |
| 2 | + |
| 3 | +How a new Fishez version gets out to users. One source of truth: bump the |
| 4 | +version once, push a tag, and the pieces below ship each channel. There are |
| 5 | +three distribution channels: |
| 6 | + |
| 7 | +| Channel | Trigger | Automated? | |
| 8 | +|---|---|---| |
| 9 | +| GitHub Release (binaries) | pushing `vX.Y.Z` tag | yes — `.github/workflows/release.yml` | |
| 10 | +| Homebrew tap | `scripts/update-tap.sh` after the release | script, not CI | |
| 11 | +| crates.io | `cargo publish` | manual (not wired into CI) | |
| 12 | + |
| 13 | +## Prerequisites |
| 14 | + |
| 15 | +- `gh` CLI authenticated: `gh auth status` |
| 16 | +- `git` push access via SSH (`gh` uses `git@github.com:` remotes) |
| 17 | +- crates.io: `cargo login` once, or a `CARGO_REGISTRY_TOKEN` (see [crates.io](#cratesio)) |
| 18 | +- For local brew verification: Homebrew installed |
| 19 | + |
| 20 | +## 1. Bump version and create the tag |
| 21 | + |
| 22 | +```bash |
| 23 | +./scripts/release.sh 0.5.0 |
| 24 | +``` |
| 25 | + |
| 26 | +From a **clean working tree**. It: |
| 27 | + |
| 28 | +1. Bumps `version` in `Cargo.toml` |
| 29 | +2. Updates `CHANGELOG.md` (moves `[Unreleased]` into a dated `[0.5.0]` section) |
| 30 | +3. Refreshes `Cargo.lock`, commits `Release v0.5.0`, creates annotated tag `v0.5.0` |
| 31 | + |
| 32 | +## 2. Push — this is the trigger for the GitHub release |
| 33 | + |
| 34 | +```bash |
| 35 | +git push origin main |
| 36 | +git push origin v0.5.0 |
| 37 | +``` |
| 38 | + |
| 39 | +Pushing the `v*` tag triggers `.github/workflows/release.yml`. That workflow |
| 40 | +builds `cargo build --release` on 4 runners and uploads these assets to the |
| 41 | +GitHub Release: |
| 42 | + |
| 43 | +```text |
| 44 | +fishez-macos-aarch64 |
| 45 | +fishez-macos-x86_64 |
| 46 | +fishez-linux-aarch64 |
| 47 | +fishez-linux-x86_64 |
| 48 | +``` |
| 49 | + |
| 50 | +**Do not publish any other channel before this finishes** — the Homebrew and |
| 51 | +crates.io steps need the tag, and the tap script verifies shas against the |
| 52 | +release. |
| 53 | + |
| 54 | +Verify the run: |
| 55 | + |
| 56 | +```bash |
| 57 | +gh run list --repo ioma8/fishez --workflow release.yml |
| 58 | +gh release view v0.5.0 --repo ioma8/fishez --json assets |
| 59 | +``` |
| 60 | + |
| 61 | +Check the run's `headSha` matches the tag commit — that is the proof the |
| 62 | +binaries were built from the released code (local builds are not |
| 63 | +bit-reproducible, so don't compare hashes to a local build). |
| 64 | + |
| 65 | +## 3. Homebrew tap |
| 66 | + |
| 67 | +The tap `ioma8/homebrew-tap` (`brew install ioma8/tap/fishez`) ships the |
| 68 | +release binaries. Update it with the one-command script: |
| 69 | + |
| 70 | +```bash |
| 71 | +./scripts/update-tap.sh 0.5.0 # version arg optional, defaults to 0.4.0 |
| 72 | +``` |
| 73 | + |
| 74 | +The script clones the tap via `gh`, then: |
| 75 | + |
| 76 | +1. Fetches the authoritative sha256 digests for all 4 assets from the |
| 77 | + **GitHub API** (`gh release view --json assets`) — never from curl on the |
| 78 | + download URLs, which raced CDN caching / in-flight uploads and once served |
| 79 | + stale v0.3.1 bytes (see Troubleshooting). |
| 80 | +2. Bumps `version` and all four `sha256` lines in `Formula/fishez.rb` |
| 81 | + (platform order: mac-arm, mac-intel, linux-arm, linux-intel). |
| 82 | +3. **Self-verifies** the formula shas equal the API digests before pushing — |
| 83 | + it refuses to push on mismatch. |
| 84 | +4. Commits `Update fishez to 0.5.0` and pushes to `ioma8/homebrew-tap`. |
| 85 | + |
| 86 | +Verify from the remote: |
| 87 | + |
| 88 | +```bash |
| 89 | +gh api repos/ioma8/homebrew-tap/contents/Formula/fishez.rb --jq '.content' \ |
| 90 | + | base64 -d | grep -E 'version "|sha256' |
| 91 | +``` |
| 92 | + |
| 93 | +### Local machines with a path-based tap |
| 94 | + |
| 95 | +If the tap was installed from a local directory |
| 96 | +(`brew tap ioma8/tap ~/projects/customs/homebrew-tap`), brew's clone at |
| 97 | +`/opt/homebrew/Library/Taps/ioma8/homebrew-tap` has that local path as its |
| 98 | +origin and goes stale. Refresh it: |
| 99 | + |
| 100 | +```bash |
| 101 | +cd /opt/homebrew/Library/Taps/ioma8/homebrew-tap && git pull |
| 102 | +brew info ioma8/tap/fishez # should show the new version |
| 103 | +``` |
| 104 | + |
| 105 | +Other users who do a plain `brew tap ioma8/tap` get the GitHub remote and are |
| 106 | +always current. |
| 107 | + |
| 108 | +## 4. crates.io |
| 109 | + |
| 110 | +Publishing to crates.io is **manual** — nothing in CI does it yet. |
| 111 | + |
| 112 | +### One-time setup |
| 113 | + |
| 114 | +```bash |
| 115 | +cargo login # paste token from https://crates.io/settings/tokens |
| 116 | +``` |
| 117 | + |
| 118 | +Or export the token for CI use (see below). |
| 119 | + |
| 120 | +### Publish |
| 121 | + |
| 122 | +```bash |
| 123 | +cargo publish --dry-run # sanity check, run from a clean tree |
| 124 | +cargo publish |
| 125 | +``` |
| 126 | + |
| 127 | +The crate metadata in `Cargo.toml` (`description`, `license`, `repository`, |
| 128 | +`readme`, `keywords`, `categories`, `exclude`) is already set up; the |
| 129 | +`exclude` list keeps `demo.gif`, `docs/`, `scripts/`, `tests/` etc. out of the |
| 130 | +published crate. |
| 131 | + |
| 132 | +### Optional: automate it in CI |
| 133 | + |
| 134 | +Add a publish job to `.github/workflows/release.yml` (after `build`): |
| 135 | + |
| 136 | +```yaml |
| 137 | + publish: |
| 138 | + runs-on: ubuntu-24.04 |
| 139 | + needs: build |
| 140 | + steps: |
| 141 | + - uses: actions/checkout@v4 |
| 142 | + - uses: dtolnay/rust-toolchain@stable |
| 143 | + - run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} |
| 144 | +``` |
| 145 | +
|
| 146 | +and add the `CARGO_REGISTRY_TOKEN` secret in repo Settings → Secrets and |
| 147 | +variables → Actions. Until then, publish locally after each release. |
| 148 | + |
| 149 | +## 5. Verify everything |
| 150 | + |
| 151 | +| Channel | Command | Expect | |
| 152 | +|---|---|---| |
| 153 | +| GitHub | `gh release view v0.5.0` | 4 assets, run on tag commit | |
| 154 | +| Homebrew | `brew install ioma8/tap/fishez` | installs 0.5.0 | |
| 155 | +| Homebrew | `fishez --init \| grep 'fz()'` | prints `fz()` wrapper | |
| 156 | +| crates.io | `cargo info fishez` | version 0.5.0 | |
| 157 | +| crates.io | `CARGO_HOME="$(mktemp -d)" cargo install fishez` | builds from registry, no git deps | |
| 158 | + |
| 159 | +Run the Homebrew check on a machine without Rust to prove the binary (not a |
| 160 | +build) is what ships. |
| 161 | + |
| 162 | +## Release checklist (new version, end to end) |
| 163 | + |
| 164 | +```bash |
| 165 | +# 1. version + tag (clean tree required) |
| 166 | +./scripts/release.sh 0.5.0 |
| 167 | +
|
| 168 | +# 2. trigger GitHub release |
| 169 | +git push origin main |
| 170 | +git push origin v0.5.0 |
| 171 | +
|
| 172 | +# 3. wait for release.yml to finish, confirm assets + tag commit |
| 173 | +gh run watch --repo ioma8/fishez |
| 174 | +gh release view v0.5.0 --repo ioma8/fishez --json assets |
| 175 | +
|
| 176 | +# 4. homebrew tap |
| 177 | +./scripts/update-tap.sh 0.5.0 |
| 178 | +
|
| 179 | +# 5. crates.io (manual) |
| 180 | +cargo publish |
| 181 | +
|
| 182 | +# 6. verify (section above), including a brew install smoke test |
| 183 | +``` |
| 184 | + |
| 185 | +## Troubleshooting |
| 186 | + |
| 187 | +- **Tag pushed but no release**: check `gh run list --workflow release.yml`; |
| 188 | + the workflow only fires on tags matching `v*`. Confirm the tag is on the |
| 189 | + remote: `git ls-remote --tags origin`. |
| 190 | +- **Tap script: "formula shas do not match the release"**: do not force it — |
| 191 | + the release binaries changed or the API digests are what the formula must |
| 192 | + equal. Re-run after the release workflow finishes. |
| 193 | +- **Stale shas in the formula**: historical issue — the old script fetched |
| 194 | + hashes with `curl` from download URLs while the release was still |
| 195 | + uploading, and served v0.3.1 bytes for v0.4.0. The current script uses the |
| 196 | + GitHub API only, so it cannot regress this way. |
| 197 | +- **`brew info` shows the old version on this machine**: path-based tap is |
| 198 | + stale — `git pull` in `/opt/homebrew/Library/Taps/ioma8/homebrew-tap` |
| 199 | + (see above). |
| 200 | +- **crates.io rejects the package**: run `cargo publish --dry-run` and read |
| 201 | + the errors; usually a missing `repository`/`license` field or a bad |
| 202 | + `exclude` — all already configured for fishez. |
| 203 | + |
| 204 | +## Rollback |
| 205 | + |
| 206 | +GitHub: delete the release (`gh release delete v0.5.0`) and the tag |
| 207 | +(`git push origin :refs/tags/v0.5.0`). Homebrew: point the formula back at the |
| 208 | +previous version's shas (re-run `update-tap.sh` with the old version — it |
| 209 | +reads whatever the release API serves). crates.io: versions are immutable — |
| 210 | +you can `cargo yank` a bad version instead. |
| 211 | + |
| 212 | +## Related docs |
| 213 | + |
| 214 | +- `docs/DISTRIBUTION.md` — `jpgfromraw-lib` publishing and clean-machine verification |
| 215 | +- `docs/RELEASE_GUIDE.md` — legacy GitLab CI-era guide, superseded by this file |
| 216 | +- `.github/workflows/release.yml` — the tag-triggered binary release |
| 217 | +- `scripts/release.sh`, `scripts/update-tap.sh` — the two release scripts |
0 commit comments