Skip to content

fix(CI): stop re-uploading release assets to already-published releases - #18

Merged
almostintuitive merged 1 commit into
mainfrom
fix/release-assets-immutable-conflict
Aug 19, 2026
Merged

almostintuitive merged 1 commit into
mainfrom
fix/release-assets-immutable-conflict

Conversation

@almostintuitive

Copy link
Copy Markdown
Contributor

Summary

  • ci.yml's release job already builds every OS/arch binary and publishes the GitHub release on tag push.
  • release-assets.yml also triggers on release: published, rebuilding the same binaries and trying to upload them again — which GitHub now rejects once a release is published ("Cannot upload asset ... to an immutable release"), as seen in run #32262281202.
  • The only part of release-assets.yml that isn't redundant is the README release-link update (added in feature(CI): update release workflow to refresh README asset links #15), so this trims the workflow down to just that.

Test plan

  • Push a new tag (or trigger workflow_dispatch with an existing tag) and confirm release-assets.yml only updates README links, without attempting a duplicate asset upload
  • Confirm ci.yml's release job still publishes assets successfully

ci.yml already builds and publishes every binary when a version tag is
pushed. release-assets.yml's `release: published` trigger then fired
again and tried to rebuild and re-upload the same assets, which
GitHub now rejects with "Cannot upload asset ... to an immutable
release" once a release is published. Trim release-assets.yml down to
just the README link update it's actually needed for.
@almostintuitive
almostintuitive merged commit f7dc98a into main Aug 19, 2026
10 checks passed
@almostintuitive
almostintuitive deleted the fix/release-assets-immutable-conflict branch August 19, 2026 14:24
almostintuitive added a commit that referenced this pull request Aug 27, 2026
## Problem

`curl -fsSL .../install.sh | bash` installs a **9-byte text file
containing `Not Found`** as `/usr/local/bin/aperiodic`:

```
$ file /usr/local/bin/aperiodic
/usr/local/bin/aperiodic: ASCII text, with no line terminators
$ aperiodic --version
/usr/local/bin/aperiodic: line 1: Not: command not found
```

Every release since **v1.0.4 has shipped with zero binaries**, so
`releases/latest/download/aperiodic-darwin-arm64` 404s:

| tag | assets |
|---|---|
| v1.0.5 | none |
| v1.0.4 | none |
| v1.0.3 | 6 binaries |

## Root cause

Three failures compounding:

1. **`ci.yml`'s release job has never run.** `bump.yml` pushes the tag
with the default `GITHUB_TOKEN`, and GitHub deliberately does not
trigger workflows from `GITHUB_TOKEN` events. Every `ci.yml` run in this
repo's history is `event: push` on `main` or `pull_request` — not one
tag push.

2. **Immutable releases reject post-publish uploads.** With no automated
release, tags were published by hand, firing `release-assets.yml` on
`release: published`. That's too late — [run
32262281202](https://github.com/aperiodic-io/cli/actions/runs/32262281202)
built all six binaries and then died on:

   ```
   Cannot upload asset aperiodic-linux-arm64 to an immutable release.
   GitHub only allows asset uploads before a release is published.
   ```

#18 responded by deleting the upload logic rather than moving it before
publication, so nothing has uploaded binaries since.

3. **`install.sh` failed silently.** It used `curl -L` without `-f`, so
curl exits `0` on a 404 and writes the error body to disk. The script
then `chmod +x`'d it and moved it into `/usr/local/bin`.

## Changes

**`bump.yml` now owns the entire release.** Tag → build all six targets
→ attach them to a **draft** release (permitted under immutability) →
verify the asset count → publish. Keeping it in one workflow sidesteps
the `GITHUB_TOKEN` trigger problem entirely, with no PAT required.

The asset-count check means a partial upload fails the run instead of
quietly publishing an empty release — the exact failure mode that went
unnoticed for two releases.

**`ci.yml`'s `release` job is removed.** It could never fire from a
`GITHUB_TOKEN` tag push, and on a manual tag push it would now race
`bump.yml` and fail on immutability regardless.

**`install.sh` fails loudly.** `curl -fL`, plus explicit rejection of
empty and HTML/error-page responses, and temp-file cleanup on every exit
path. `INSTALL_DIR` is now overridable, which makes the happy path
testable without touching the system.

## PagerDuty

Adds a `notify-failure` job, on `ubuntu-latest` to match this repo's
runners. A silent release failure is precisely what this bug was.


## Verification

`install.sh` against the currently-broken `latest` — now exits 1 and
leaves the system untouched:

```
Error: Failed to download binary from https://github.com/.../aperiodic-darwin-arm64
The release may not exist, or it may not have assets for darwin-arm64.
EXIT CODE: 1
```

Happy path against `v1.0.3`, which still has assets:

```
$ INSTALL_DIR=/tmp/testbin bash install.sh v1.0.3
Installation complete.
$ file /tmp/testbin/aperiodic
/tmp/testbin/aperiodic: Mach-O 64-bit executable arm64
```

`actionlint` clean across all workflows. `go build ./...` passes; `go
test ./...` gives 33 passed / 5 failed, all five being pre-existing
integration tests gated on `APERIODIC_API_KEY`, which CI supplies.

## Follow-ups (not in this PR)

- **v1.0.4 and v1.0.5 stay assetless** — immutable releases can't be
amended. The next `bump` run produces the first working release; until
then `install.sh` correctly refuses rather than installing garbage.
- **`release-assets.yml`** (the README link updater) triggers on
`release: published`. Since `bump.yml` publishes via `GITHUB_TOKEN`, it
won't fire either — same root cause as #1, worth folding into `bump.yml`
separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant