Session
@.github/workflows/release-cli.yml release seems to be failing
@.github/workflows/release-cli.yml
release seems to be failing
Let me investigate the CLI build setup and check for recent workflow run failures.
All 5 recent runs have failed. Let me check the latest failure details.
Let me also check if earlier runs had different failures.
Found the issues. There are two problems:
1. HOMEBREW_TAP_TOKEN secret is not set (current failure)
The logs show HOMEBREW_TAP_TOKEN: is empty. GitHub Actions would mask it as *** if it were set. The git push fails with:
remote: Invalid username or token. Password authentication is not supported for Git operations.
You need to create a GitHub Personal Access Token (classic) with repo scope for the JustinyAhin/homebrew-threadcast repo and add it as a secret named HOMEBREW_TAP_TOKEN in the threadcast repo settings.
2. Binary name mismatch in Homebrew formula (latent bug)
The tar archives contain files named threadcast-darwin-arm64, threadcast-linux-x64, etc. But the formula does:
def install
bin.install "threadcast" # looks for a file named "threadcast"
end
This will fail at brew install time because there's no file called threadcast inside the archive — it's called threadcast-darwin-arm64 (etc.).
Fix — rename the binary to threadcast before creating the tar.gz:
Now fix issue 1 — set the HOMEBREW_TAP_TOKEN secret. You'll need to:
- Go to github.com/JustinyAhin/threadcast → Settings → Secrets and variables → Actions
- Add a repository secret named
HOMEBREW_TAP_TOKENwith a Personal Access Token (classic) that hasreposcope and access toJustinyAhin/homebrew-threadcast
The binary naming fix is committed locally and ready to push when you're ready.
how to i create the pat?
- Go to github.com → click your profile picture → Settings
- Scroll down the left sidebar → Developer settings
- Personal access tokens → Tokens (classic)
- Click Generate new token → Generate new token (classic)
- Give it a name like
homebrew-tap - Set an expiration (or no expiration)
- Check the
reposcope (full control of private repositories) - Click Generate token
- Copy the token immediately (you won't see it again)
Then add it as a secret:
- Go to github.com/JustinyAhin/threadcast → Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
HOMEBREW_TAP_TOKEN - Value: paste the token
- Click Add secret
why classic instead of fine grained?
You're right, fine-grained is the better choice here. I defaulted to classic out of habit, but fine-grained lets you scope it tighter.
With a fine-grained token:
- Set Resource owner to
JustinyAhin - Repository access → Only select repositories → pick
homebrew-threadcast - Permissions → Repository permissions → Contents: Read and write
- That's it — no other permissions needed
This is more secure since it only grants write access to that one repo instead of all repos under your account.
JustinyAhin/threadcast is private though
that's okay?
Yes, that's fine. The HOMEBREW_TAP_TOKEN is only used to clone and push to homebrew-threadcast. It doesn't need access to threadcast itself.
The workflow already uses ${{ github.token }} (line 77) for downloading release assets from the private threadcast repo — that's provided automatically by GitHub Actions.
so now how can i install the package on my mac?
Once you've set the secret and run the release workflow successfully:
brew tap JustinyAhin/threadcast
brew install threadcast
If you want to install it right now without waiting for the release, you can build locally:
bun run --filter @threadcast/cli build
Then copy the binary from packages/cli/dist/threadcast-darwin-arm64 to somewhere on your PATH:
cp packages/cli/dist/threadcast-darwin-arm64 /usr/local/bin/threadcast
seems to have work?
Release looks good! All 8 assets (4 raw binaries + 4 tar.gz archives) are there and the workflow passed (green checkmark).
You should now be able to install via Homebrew:
brew tap JustinyAhin/threadcast
brew install threadcast
==> Auto-updating Homebrew...
Adjust how often this is run with $HOMEBREW_AUTO_UPDATE_SECS or disable with$HOMEBREW_NO_AUTO_UPDATE=1. Hide these hints with $HOMEBREW_NO_ENV_HINTS=1 (see man brew).
==> Auto-updated Homebrew!
Updated 7 taps (steipete/tap, hashicorp/tap, infisical/get-cli, sst/tap, stripe/stripe-cli, homebrew/core and homebrew/cask).
==> New Formulae
aoe: Terminal session manager for AI coding agents
grafanactl: CLI to interact with Grafana
infisical/get-cli/infisical@0.43.56
rtk: CLI proxy to minimize LLM token consumption
rustledger: Fast, pure Rust implementation of Beancount double-entry accounting
sss-cli: Shamir secret share command-line interface
whodb-cli: Database management CLI with TUI interface, MCP server support, AI, and more
zxing-cpp: Multi-format barcode image processing library written in C++
==> New Casks
brewy: Native app for managing Homebrew packages
iloader: iOS Sideloading Companion
You have 21 outdated formulae and 1 outdated cask installed.
==> Tapping justinyahin/threadcast
Cloning into '/opt/homebrew/Library/Taps/justinyahin/homebrew-threadcast'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 8 (delta 1), reused 8 (delta 1), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
Resolving deltas: 100% (1/1), done.
Tapped 1 formula (13 files, 8.2KB).
==> Fetching downloads for: threadcast
✘ Formula threadcast (0.0.1)
Error: Failed to download resource "threadcast (0.0.1)"
Download failed: https://github.com/JustinyAhin/threadcast/releases/download/cli-v0.0.1/threadcast-darwin-arm64.tar.gz
The repo is private, so Homebrew can't download the release assets — they require authentication.
You have a few options:
- Make the repo public — simplest fix, Homebrew will just work
- Host binaries elsewhere (S3, Cloudflare R2, etc.) — keep repo private but serve assets from a public URL
- Set
HOMEBREW_GITHUB_API_TOKEN— works for you locally, but anyone else installing would need to do the same
Which approach works for you?
how to do 1?
with r2
Here's what you need to set up:
R2 Setup
- Cloudflare Dashboard → R2 → Create bucket (e.g.
threadcast-releases) - Settings → Public access → enable either the
r2.devsubdomain or a custom domain - R2 → Manage R2 API Tokens → Create API token with Object Read & Write on that bucket
- Copy the Access Key ID, Secret Access Key, and note your account ID
GitHub Secrets
Add these to JustinyAhin/threadcast → Settings → Secrets:
R2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_ENDPOINT—https://<account-id>.r2.cloudflarestorage.comR2_PUBLIC_URL— your bucket's public URL (e.g.https://pub-xxx.r2.devor your custom domain)
Workflow Changes
Here's what changed:
releasejob — added an "Upload to R2" step that copies the.tar.gzfiles tos3://threadcast-releases/cli/v{version}/update-homebrewjob — downloads checksums from R2 instead of GitHub releases, and the formula URLs now point toR2_PUBLIC_URLinstead of GitHub
Secrets you need to add
| Secret | Value |
|---|---|
R2_ACCESS_KEY_ID |
From R2 API token |
R2_SECRET_ACCESS_KEY |
From R2 API token |
R2_ENDPOINT |
https://<account-id>.r2.cloudflarestorage.com |
R2_PUBLIC_URL |
Your bucket's public URL (e.g. https://pub-xxx.r2.dev) |
The R2 bucket name is hardcoded as threadcast-releases — adjust if you name it differently.
✔︎ JSON API formula.jws.json Downloaded 31.9MB/ 31.9MB
✔︎ JSON API cask.jws.json Downloaded 15.3MB/ 15.3MB
==> Fetching downloads for: threadcast
✘ Formula threadcast (0.0.1)
Error: Failed to download resource "threadcast (0.0.1)"
Download failed: https://github.com/JustinyAhin/threadcast/releases/download/cli-v0.0.1/threadcast-darwin-arm64.tar.gz
The formula in homebrew-threadcast still has the old GitHub URLs — the workflow changes haven't been released yet.
To get it working you need to:
- Create the R2 bucket + secrets (as described above)
- Push the workflow changes
- Run the release workflow again
Or if you want to test it right now before re-running the full release, you can manually upload the existing tarballs and update the formula:
# Download the tarballs from the existing GitHub release
gh release download cli-v0.0.1 --pattern "*.tar.gz" --dir /tmp/tc-release
# Upload to R2 (replace with your actual values)
for f in /tmp/tc-release/threadcast-*.tar.gz; do
aws s3 cp "$f" "s3://threadcast-releases/cli/v0.0.1/$(basename "$f")" \
--endpoint-url "https://<account-id>.r2.cloudflarestorage.com"
done
Then update the formula in homebrew-threadcast to point to your R2 public URL instead of the GitHub release URL.
already did those
Try installing again:
brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
cool
just did that
and ran threadcast
and i was already logged in
why?
Auth is stored at ~/.threadcast/config.json. Both the locally-built binary and the Homebrew-installed one read from the same path, so your previous login persisted.
haaa
not good haha
That's actually standard behavior for CLIs — gh, docker, aws all store config in ~/.{name}/. The Homebrew-installed binary is the same program, just installed to a different path.
Were you expecting it to require a fresh login, or is there a specific concern? Happy to change the behavior if needed.
i mean what about when i'm working locally while developing the project?
that means it'll be misc with my prod cli?
Ah yeah, good point. Simplest fix — add an env var override for the config dir so you can point dev to a separate location.
Now when developing locally you can run with:
THREADCAST_CONFIG_DIR=~/.threadcast-dev bun run packages/cli/src/index.tsx
Production (Homebrew) stays on ~/.threadcast, dev uses ~/.threadcast-dev. No overlap.
should we update stuff in package.json?
Yeah, set it in the dev script so you don't have to remember:
Now bun run dev (or bun dev:cli from the root) automatically uses ~/.threadcast-dev.
here too? package.json
Done. Both bun cli (root) and bun dev (packages/cli) now use ~/.threadcast-dev.
when sharing
it looks like locally it shares into threadcast instead of dev
API_BASE_URL is hardcoded to http://localhost:5173. That gets baked into the compiled binary too. Is there a production URL this should point to? It should probably read from an env var so dev hits localhost and the released binary hits prod.
i meant i just ran locally
logged in (saved config in threadcast-dev, good)
shared (saved in ~/.threadcast/shared.json, not good for local)
Same hardcoded ~/.threadcast path. Let me check if there are other files with the same issue.
Three places, only auth/config.ts respects the env var. Let me extract a shared helper and fix all of them.
Now export getConfigDir from auth/config.ts and update the references:
Also need to update the mkdir call in shared-sessions.ts since it was using the removed CONFIG_DIR:
Now all three files (auth/config.ts, shared-sessions.ts, session-index.ts) respect THREADCAST_CONFIG_DIR. Dev data stays in ~/.threadcast-dev, prod stays in ~/.threadcast.
for prod it saves http://localhost:5173/threads/ia1ocz5tnvcizbs66ivmkz7v
prod url is https://threadcast.dev
Now add the env var to the dev scripts:
Now:
- Prod binary: no env vars set → defaults to
https://threadcast.dev - Dev (
bun dev/bun cli): →http://localhost:5173
cool
so say i still release as 0.0.1
how do i update my version of the cli?
brew upgrade threadcast
Or if the version number hasn't changed (still 0.0.1), Homebrew won't see it as an upgrade. You'd need to:
brew reinstall threadcast
what about upgrading versions?
When you release a new version (e.g. 0.0.2), just:
brew upgrade threadcast
The release workflow already handles bumping the version in the Homebrew formula — it writes the new version + checksums and pushes to homebrew-threadcast. So brew upgrade will pick it up automatically.
i reinstalled
just shared something
but
~/.threadcast/shared.json
The current 0.0.1 release was built from the old code with localhost:5173 hardcoded. The API_BASE_URL fix hasn't been released yet — you need to push the changes and run a new release for the compiled binary to pick it up.
already did
ohh could it be due to in my oauth app?
The OAuth callback URL isn't the issue — the CLI uses Device Flow (no callback needed).
The localhost:5173 URL you're seeing is the API_BASE_URL being used to construct the share link. Let me check if the new release actually picked up the change. Did the latest release workflow pass?
Release passed. Let me check if the API_BASE_URL change was included in the commit that was released.
The change is in the released commit. The issue might be that Bun's bundler inlined process.env.THREADCAST_API_URL as undefined at build time, making it always fall back to https://threadcast.dev. But you're seeing localhost:5173...
Did you brew reinstall threadcast after the latest release ran? The Homebrew formula still points to the same version 0.0.1, so reinstall might have served a cached copy.
Try:
brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
Error: Refusing to untap justinyahin/threadcast because it contains the following installed formulae or casks:
threadcast
brew uninstall threadcast && brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
Uninstalling /opt/homebrew/Cellar/threadcast/0.0.1... (4 files, 66.2MB)
Untapping justinyahin/threadcast...
Untapped 1 formula (13 files, 8.7KB).
✔︎ JSON API formula.jws.json Downloaded 31.9MB/ 31.9MB
✔︎ JSON API cask.jws.json Downloaded 15.3MB/ 15.3MB
==> Tapping justinyahin/threadcast
Cloning into '/opt/homebrew/Library/Taps/justinyahin/homebrew-threadcast'...
remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 16 (delta 4), reused 15 (delta 3), pack-reused 0 (from 0)
Receiving objects: 100% (16/16), done.
Resolving deltas: 100% (4/4), done.
Tapped 1 formula (13 files, 9.3KB).
==> Fetching downloads for: threadcast
✘ Formula threadcast (0.0.1) Verifying 23.1MB/ 23.1MB
Error: Formula reports different checksum: 9c6368e02db6c4b87d3fad5f77fd9a06d3970d0bdcd46a22e5431b2573b9973a
SHA-256 checksum of downloaded file: f10160dac04b8a662c9a10de426d2bfc339aa85b3ad75dd8b28ff7639f60683f
Checksum mismatch — you ran the release 3 times for 0.0.1, each uploading different binaries to R2 (from different code). The formula's checksums are from one run but the R2 files are from another.
Easiest fix: run the release workflow one more time so the checksums and files are in sync. Or better, bump to 0.0.2 to avoid this kind of collision.
let's bump to 0.0.2
Updated to 0.0.2. Push this, then trigger the release workflow with version 0.0.2.
done
action inprogress
Once it passes:
brew uninstall threadcast && brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
brew uninstall threadcast && brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
Error: No such keg: /opt/homebrew/Cellar/threadcast
Already uninstalled from earlier. Just run:
brew untap justinyahin/threadcast && brew tap justinyahin/threadcast && brew install threadcast
whyyy
error: Upload failed: Internal Error
The API is returning a 500. Can you check the Cloudflare Worker logs? Either via:
bunx wrangler tail --name threadcast
Or in the Cloudflare dashboard → Workers → threadcast → Logs.
The most likely cause is the THREADS_BUCKET R2 binding not being configured in production. Let me check.
{
"level": "error",
"message": "POST https://threadcast.dev/api/threads",
"$workers": {
"event": {
"request": {
"cf": {
"requestHeaderNames": {},
"isEUCountry": false,
"httpProtocol": "HTTP/1.1",
"clientAcceptEncoding": "gzip, deflate, br",
"requestPriority": "",
"colo": "SIN",
"asOrganization": "UNIFI-HOME",
"country": "MY",
"city": "Petaling Jaya",
"continent": "AS",
"region": "Selangor",
"regionCode": "10",
"timezone": "Asia/Kuala_Lumpur",
"longitude": "101.60671",
"latitude": "3.10726",
"postalCode": "46547",
"tlsVersion": "TLSv1.3",
"tlsCipher": "AEAD-AES128-GCM-SHA256",
"tlsClientRandom": "mLhT7Loly/qQChKSOG8tWotr7Ak7h89pl6PcQmYDIUk=",
"tlsClientCiphersSha1": "z756bn+x21PiebYy319nCdFpEYo=",
"tlsClientExtensionsSha1": "g7CUyX4IGfVWT4opL1PEzZ2U1EQ=",
"tlsClientExtensionsSha1Le": "29pLmat8Ljr3wiUemHNTUe0plFA=",
"tlsExportedAuthenticator": {
"clientHandshake": "1d8926439f4c1720cd1fd5fc44033a6d9f0758eb827c710ce544082c583cb3c8",
"serverHandshake": "99859607de9c0ededeca6ede0708f0c0310c92d9a87995daed103cff427f0e70",
"clientFinished": "d593677a4eb4a61ceb924925b2e933639bb8b5e8fb9c73b887c0ee585b317213",
"serverFinished": "04f27a9686394ecc7907f55d1621c3c181fce921927107e78df7bd589401a855"
},
"tlsClientHelloLength": "524",
"tlsClientAuth": {
"certPresented": "0",
"certVerified": "NONE",
"certRevoked": "0",
"certIssuerDN": "",
"certSubjectDN": "",
"certIssuerDNRFC2253": "",
"certSubjectDNRFC2253": "",
"certIssuerDNLegacy": "",
"certSubjectDNLegacy": "",
"certSerial": "",
"certIssuerSerial": "",
"certSKI": "",
"certIssuerSKI": "",
"certFingerprintSHA1": "",
"certFingerprintSHA256": "",
"certNotBefore": "",
"certNotAfter": ""
},
"verifiedBotCategory": "",
"edgeRequestKeepAliveStatus": 1,
"clientTcpRtt": 20,
"asn": 4788
},
"url": "https://threadcast.dev/api/threads",
"method": "POST",
"headers": {
"accept": "/",
"accept-encoding": "gzip, br",
"authorization": "********",
"cf-connecting-ip": "180.75.234.166",
"cf-ipcountry": "MY",
"cf-ray": "9d029ddc0a0bc61e",
"cf-visitor": "{"scheme":"https"}",
"connection": "Keep-Alive",
"content-length": "3754",
"content-type": "application/json",
"host": "threadcast.dev",
"user-agent": "Bun/1.3.9",
"x-forwarded-proto": "https",
"x-real-ip": "180.75.234.166"
},
"path": "/api/threads"
},
"rayId": "9d029ddc0a0bc61e",
"response": {
"status": 500
}
},
"diagnosticsChannelEvents": [],
"truncated": false,
"scriptName": "threadcast",
"outcome": "ok",
"eventType": "fetch",
"executionModel": "stateless",
"scriptVersion": {
"id": "c7ee42d6-c7e8-49d0-847d-49863f04150b"
},
"requestId": "9d029ddc0a0bc61e",
"cpuTimeMs": 1,
"wallTimeMs": 1
},
"$metadata": {
"id": "01KHSYFSCEYR8051D4TXQFTYRE",
"requestId": "9d029ddc0a0bc61e",
"trigger": "POST /api/threads",
"service": "threadcast",
"level": "error",
"error": "POST https://threadcast.dev/api/threads",
"message": "POST https://threadcast.dev/api/threads",
"account": "e67f2be34790d1f6c255d622e3f39b60",
"type": "cf-worker-event",
"fingerprint": "f16a0620113bdf62ec39eb2b31246962",
"origin": "fetch",
"messageTemplate": "POST https://threadcast.dev/api/threads"
}
}
{
"level": "error",
"message": "\n\u001b[1;31m[500] POST /api/threads\u001b[0m\nReferenceError: process is not defined\n at .svelte-kit/output/server/chunks/index2.js (_worker.js:77493:5)\n at __init (_worker.js:9:56)\n at .svelte-kit/output/server/entries/endpoints/api/threads/_server.ts.js (_worker.js:78106:5)\n at __init (_worker.js:9:56)\n at _worker.js:83281:64\n at async internal_respond (_worker.js:82807:22)\n at async Object.fetch (_worker.js:83446:13)",
"$workers": {
"truncated": false,
"event": {
"request": {
"url": "https://threadcast.dev/api/threads",
"method": "POST",
"path": "/api/threads"
}
},
"outcome": "ok",
"scriptName": "threadcast",
"eventType": "fetch",
"executionModel": "stateless",
"scriptVersion": {
"id": "c7ee42d6-c7e8-49d0-847d-49863f04150b"
},
"requestId": "9d029ddc0a0bc61e"
},
"$metadata": {
"id": "01KHSYFSCEYR8051D4TXQFTYRF",
"requestId": "9d029ddc0a0bc61e",
"trigger": "POST /api/threads",
"service": "threadcast",
"level": "error",
"error": "\n\u001b[1;31m[500] POST /api/threads\u001b[0m\nReferenceError: process is not defined\n at .svelte-kit/output/server/chunks/index2.js (_worker.js:77493:5)\n at __init (_worker.js:9:56)\n at .svelte-kit/output/server/entries/endpoints/api/threads/_server.ts.js (_worker.js:78106:5)\n at __init (_worker.js:9:56)\n at _worker.js:83281:64\n at async internal_respond (_worker.js:82807:22)\n at async Object.fetch (_worker.js:83446:13)",
"message": "\n\u001b[1;31m[500] POST /api/threads\u001b[0m\nReferenceError: process is not defined\n at .svelte-kit/output/server/chunks/index2.js (_worker.js:77493:5)\n at __init (_worker.js:9:56)\n at .svelte-kit/output/server/entries/endpoints/api/threads/_server.ts.js (_worker.js:78106:5)\n at __init (_worker.js:9:56)\n at _worker.js:83281:64\n at async internal_respond (_worker.js:82807:22)\n at async Object.fetch (_worker.js:83446:13)",
"account": "e67f2be34790d1f6c255d622e3f39b60",
"type": "cf-worker",
"fingerprint": "5b2870bd96cdba3a16f5d1986c137291",
"origin": "fetch",
"messageTemplate": "\u001b[1;31m[500] POST /api/threads\u001b[0m\nReferenceError: process is not defined\n at .svelte-kit/output/server/chunks/<DOMAIN> (<DOMAIN>:77493:5)\n at __init (<DOMAIN>:9:56)\n at .svelte-kit/output/server/entries/endpoints/api/threads/<DOMAIN> (<DOMAIN>:78106:5)\n at __init (<DOMAIN>:9:56)\n at <DOMAIN>:83281:64\n at async internal_respond (<DOMAIN>:82807:22)\n at async <DOMAIN> (<DOMAIN>:83446:13)"
}
}
ReferenceError: process is not defined — Cloudflare Workers don't have process.env. The API_BASE_URL change in @threadcast/shared broke it because the shared package is also imported by the web app.
Since the web app doesn't use API_BASE_URL (only the CLI does), the fix is to move the env var check out of shared and into the CLI.
Now override it in the CLI's api-client instead:
Let me also check if API_BASE_URL is used elsewhere in the CLI.
Only used in api-client.ts. Push this and redeploy the web app — the process is not defined error will be fixed. The CLI will still respect THREADCAST_API_URL from the dev scripts.