Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ on:
permissions:
contents: write

concurrency:
group: usagebar-publish
cancel-in-progress: false

env:
BUN_VERSION: "1.3.13"
RUST_TOOLCHAIN: "1.94.1"
Expand Down Expand Up @@ -78,8 +82,14 @@ jobs:
- name: Release preflight
run: node ./scripts/release-preflight.mjs --release-tag "$RELEASE_TAG"

- name: Allow unsigned prerelease installer
if: runner.os == 'Windows' && contains(env.RELEASE_TAG, '-')
run: |
echo "USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER=1" >> "$GITHUB_ENV"
echo "Prerelease $RELEASE_TAG will publish an unsigned Windows installer until Authenticode signing is configured."

- name: Verify updater signing key is configured
if: runner.os == 'Windows' && !contains(env.RELEASE_TAG, '-')
if: runner.os == 'Windows'
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
Expand All @@ -88,11 +98,33 @@ jobs:
exit 1
fi

- name: Allow unsigned prerelease updater artifacts
if: runner.os == 'Windows' && contains(env.RELEASE_TAG, '-')
- name: Verify Windows Authenticode signing secrets are configured
if: runner.os == 'Windows'
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER: ${{ env.USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER }}
run: |
echo "No Tauri updater signing key configured; publishing this technical-preview installer without updater signatures."
echo "USAGEBAR_ALLOW_UNSIGNED_UPDATER=1" >> "$GITHUB_ENV"
if [[ "$USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER" == "1" ]]; then
echo "Skipping Authenticode secret requirement for unsigned prerelease installer."
exit 0
fi

if [[ -n "$WINDOWS_CERTIFICATE_THUMBPRINT" ]]; then
exit 0
fi

if [[ -z "$WINDOWS_CERTIFICATE_BASE64" && -z "$WINDOWS_CERTIFICATE" ]]; then
echo "Missing Windows Authenticode certificate secret. Set WINDOWS_CERTIFICATE_BASE64."
exit 1
fi

if [[ -z "$WINDOWS_CERTIFICATE_PASSWORD" ]]; then
echo "Missing WINDOWS_CERTIFICATE_PASSWORD secret."
exit 1
fi

- uses: tauri-apps/tauri-action@v0
env:
Expand All @@ -105,27 +137,48 @@ jobs:
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }}
USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER: "1"
USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER: ${{ env.USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER }}
with:
tagName: ${{ env.RELEASE_TAG }}
releaseName: ${{ env.RELEASE_TAG }}
releaseDraft: false
prerelease: ${{ contains(env.RELEASE_TAG, '-') }}
includeUpdaterJson: true
args: ${{ matrix.args }} ${{ contains(env.RELEASE_TAG, '-') && '--no-sign' || '' }}
args: ${{ matrix.args }}

- name: Verify updater assets uploaded
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ASSETS=$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | tr -d '\r')

if [[ "$USAGEBAR_ALLOW_UNSIGNED_UPDATER" != "1" ]]; then
printf '%s\n' "$ASSETS" | grep -Fxq 'latest.json' || { echo "Missing updater manifest: latest.json"; exit 1; }
printf '%s\n' "$ASSETS" | grep -Eq '\.sig$' || { echo "Missing updater signatures (.sig)"; exit 1; }
else
echo "Unsigned prerelease: updater manifest and signature assets are not required."
fi
printf '%s\n' "$ASSETS" | grep -Fxq 'latest.json' || { echo "Missing updater manifest: latest.json"; exit 1; }
printf '%s\n' "$ASSETS" | grep -Eq '\.sig$' || { echo "Missing updater signatures (.sig)"; exit 1; }
if [[ "$RUNNER_OS" == "Windows" ]]; then
printf '%s\n' "$ASSETS" | grep -Eq 'setup\.exe$' || { echo "Missing Windows setup executable"; exit 1; }
fi

- name: Publish updater channel manifest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CHANNEL_TAG="updater"
CHANNEL_DIR="$RUNNER_TEMP/usagebar-updater-channel"
mkdir -p "$CHANNEL_DIR"
gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern latest.json --dir "$CHANNEL_DIR"

if ! gh release view "$CHANNEL_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "$CHANNEL_TAG" \
--repo "$GITHUB_REPOSITORY" \
--prerelease \
--title "UsageBar updater channel" \
--notes "Machine-readable updater metadata. Install UsageBar from the versioned releases."
fi

gh release upload "$CHANNEL_TAG" \
--repo "$GITHUB_REPOSITORY" \
"$CHANNEL_DIR/latest.json" \
--clobber

CHANNEL_ASSET=$(gh release view "$CHANNEL_TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | tr -d '\r')
printf '%s\n' "$CHANNEL_ASSET" | grep -Fxq 'latest.json' || { echo "Updater channel is missing latest.json"; exit 1; }
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ playwright-report/
.env*
!.env.example

# Tauri updater signing keys
/usagebar.key
/usagebar.key.pub

# Agent working files
docs/choices.md
docs/breadcrumbs.md
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ UsageBar v0.1.1 is a Windows-first public release.
- Windows is the primary tested platform for this fork. macOS and Linux remain secondary until the Windows release path is boring.
- Provider coverage is uneven: `Supported` means the Windows path is intended to work; `Experimental` means setup, API shape, or live-account validation may still change.
- Some providers report usage directly; others estimate from local history, known quota pools, telemetry logs, or manually supplied session cookies. Provider docs describe the source per integration.
- Signed updater metadata is the primary update path. UsageBar verifies the published asset digest, downloads the Windows installer, then restarts after the app exits.
- Published releases use a signed updater channel. UsageBar downloads an available update, then installs it after you select `Restart to update`.
- Authenticode-signed Windows artifacts, live Edge-account validation, and full crash-recovery expectations remain future work.

## Architecture
Expand Down Expand Up @@ -163,9 +163,9 @@ bun run release:check -- --release-tag v0.1.1
bun run build:release -- --bundles nsis
```

If `TAURI_SIGNING_PRIVATE_KEY` is unset, the helper automatically adds `--no-sign` so the local build can skip Tauri updater signatures. Set `USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER=1` for local builds without an Authenticode certificate. The helper signs the final setup executable when Windows signing material exists. The setup executable lands under `src-tauri/target/release/bundle/nsis/`.
Release builds require `TAURI_SIGNING_PRIVATE_KEY`. Pass `--no-sign` only for an explicit local installer smoke build. Set `USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER=1` for local builds without an Authenticode certificate. The helper signs the final setup executable when Windows signing material exists. The setup executable lands under `src-tauri/target/release/bundle/nsis/`.

GitHub publishes unsigned Windows installers until the project gets an Authenticode certificate. These installers can show `Unknown publisher` and trigger Windows SmartScreen's "unrecognized app" warning. Stable releases still require signed Tauri updater metadata; see [docs/releasing.md](docs/releasing.md).
GitHub publishes unsigned Windows installers until the project gets an Authenticode certificate. These installers can show `Unknown publisher` and trigger Windows SmartScreen's "unrecognized app" warning. All published releases still require signed Tauri updater metadata; see [docs/releasing.md](docs/releasing.md).

Before pushing a release tag, run the same preflight with `--require-clean` so the tag is cut from a clean worktree.

Expand Down
19 changes: 11 additions & 8 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The preflight currently verifies:

- `package.json`, [src-tauri/tauri.conf.json](../src-tauri/tauri.conf.json), and [src-tauri/Cargo.toml](../src-tauri/Cargo.toml) agree on the same version
- the release tag matches that version
- the Tauri product branding and updater endpoint still point at `UsageBar` and `luisleineweber/usagebar`
- the Tauri product branding is `UsageBar` and the updater endpoint is the fixed signed channel at `https://github.com/luisleineweber/usagebar/releases/download/updater/latest.json`
- [CHANGELOG.md](../CHANGELOG.md) contains a section for the version being released
- bundled plugins exist under `src-tauri/resources/bundled_plugins`

Expand All @@ -29,11 +29,12 @@ The preflight currently verifies:
Build the Windows installer locally before the first publish of a version:

```powershell
$env:TAURI_SIGNING_PRIVATE_KEY = Get-Content .\\usagebar.key -Raw
$env:USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER = "1"
bun run build:release -- --bundles nsis
```

If `TAURI_SIGNING_PRIVATE_KEY` is unset, the helper automatically adds `--no-sign` so local builds can complete without Tauri updater signatures. Local Windows builds need an explicit unsigned-build opt-in when no Authenticode material exists. GitHub publishes set this option for prerelease and stable tags. Unsigned artifacts can show `Unknown publisher` and can trigger Windows SmartScreen's "unrecognized app" warning.
Release builds require `TAURI_SIGNING_PRIVATE_KEY`. Pass `--no-sign` only for an explicit local installer smoke build that will not support in-app updates. Local Windows builds need an explicit unsigned-build opt-in when no Authenticode material exists. GitHub prerelease publishes set this option automatically. Unsigned artifacts can show `Unknown publisher` and can trigger Windows SmartScreen's "unrecognized app" warning.

## Windows Code Signing

Expand Down Expand Up @@ -71,13 +72,15 @@ The workflow runs the same release preflight, builds platform artifacts, and ver

- a Windows setup executable ending in `setup.exe`

Stable releases require `TAURI_SIGNING_PRIVATE_KEY` and updater signature assets. For prerelease tags, the workflow passes `--no-sign` and publishes without updater assets. All Windows publishes allow an unsigned installer while Authenticode signing remains unavailable.
All published releases require `TAURI_SIGNING_PRIVATE_KEY`, `latest.json`, and updater signature assets. Prerelease tags still allow an unsigned Windows installer while Authenticode signing remains unavailable.

Current updater channel note:
The release workflow copies the signed `latest.json` from each versioned release to the fixed `updater` release. The app reads that channel, downloads first, and installs only after you select `Restart to update`.

- Signed Tauri updater metadata is the primary update path.
- GitHub's `releases/latest` alias does not resolve prereleases, so UsageBar queries the release API when a prerelease has no signed updater metadata.
- The Windows fallback accepts only the exact `UsageBar_<version>_x64-setup.exe` asset and verifies GitHub's SHA-256 digest before installation.
Current updater channel:

- GitHub's `releases/latest` alias does not resolve prereleases.
- The publish workflow copies each signed `latest.json` to the fixed `updater` release.
- UsageBar reads `https://github.com/luisleineweber/usagebar/releases/download/updater/latest.json`.

## Release Gate

Expand Down Expand Up @@ -111,7 +114,7 @@ This is a public alpha for Windows users who want to test UsageBar before a full

- Some providers are experimental and may need manual cookie/API-key setup
- Some costs or usage buckets may be estimated or partial
- Prerelease updates may use the GitHub installer fallback when signed updater metadata is unavailable
- Prerelease updates use the signed updater channel
- UI polish, crash recovery, and signed-build coverage are not final

### Privacy
Expand Down
45 changes: 25 additions & 20 deletions scripts/build-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,23 @@ if (signingKeyValue && existsSync(signingKeyValue)) {

const resolvedArgs = [...args]
if (!env.TAURI_SIGNING_PRIVATE_KEY && !resolvedArgs.includes("--no-sign")) {
resolvedArgs.push("--no-sign")
console.log("No TAURI_SIGNING_PRIVATE_KEY found; building without Tauri updater signatures.")
console.error(
"Missing TAURI_SIGNING_PRIVATE_KEY. Set the updater signing key or pass --no-sign for an explicit installer-only smoke build."
)
process.exit(1)
}

function hasWindowsSigningMaterial() {
return Boolean(
env.WINDOWS_CERTIFICATE_THUMBPRINT ||
env.WINDOWS_CERTIFICATE_BASE64 ||
env.WINDOWS_CERTIFICATE
env.WINDOWS_CERTIFICATE_THUMBPRINT || env.WINDOWS_CERTIFICATE_BASE64 || env.WINDOWS_CERTIFICATE
)
}

function allowsUnsignedWindowsInstaller() {
return env.USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER === "1" ||
return (
env.USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER === "1" ||
env.USAGEBAR_ALLOW_UNSIGNED_WINDOWS_INSTALLER?.toLowerCase() === "true"
)
}

function requestsWindowsInstaller() {
Expand Down Expand Up @@ -128,19 +130,23 @@ function signWindowsInstallerArtifacts(artifactDirs) {
console.log("Signing Windows installer artifacts after build:")

for (const artifact of artifacts) {
const signer = spawnSync("powershell", [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
windowsSignScript,
"-TargetPath",
artifact,
], {
cwd: repoRoot,
env,
stdio: "inherit",
})
const signer = spawnSync(
"powershell",
[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
windowsSignScript,
"-TargetPath",
artifact,
],
{
cwd: repoRoot,
env,
stdio: "inherit",
}
)

if (signer.error) {
console.error("Failed to launch Windows installer signing:", signer.error)
Expand Down Expand Up @@ -194,7 +200,6 @@ child.on("exit", (code, signal) => {

process.exit(code ?? 0)
})

child.on("error", (error) => {
console.error("Failed to launch Tauri release build:", error)
process.exit(1)
Expand Down
18 changes: 15 additions & 3 deletions scripts/release-preflight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ if (!semverPattern.test(version)) {
}

if (tauriConf.version !== version) {
fail(`src-tauri/tauri.conf.json version (${tauriConf.version}) does not match package.json (${version})`)
fail(
`src-tauri/tauri.conf.json version (${tauriConf.version}) does not match package.json (${version})`
)
}

if (cargoVersion !== version) {
Expand All @@ -102,8 +104,18 @@ if (tauriConf.productName !== "UsageBar") {
}

const updaterEndpoints = tauriConf.plugins?.updater?.endpoints ?? []
if (!updaterEndpoints.some((endpoint) => String(endpoint).includes("github.com/luisleineweber/usagebar/releases"))) {
fail("Updater endpoint is not pointed at luisleineweber/usagebar releases")
const expectedUpdaterEndpoint =
"https://github.com/luisleineweber/usagebar/releases/download/updater/latest.json"
if (updaterEndpoints.length !== 1 || updaterEndpoints[0] !== expectedUpdaterEndpoint) {
fail(`Updater endpoint must be ${expectedUpdaterEndpoint}`)
}

if (tauriConf.bundle?.createUpdaterArtifacts !== true) {
fail("Tauri updater artifacts must be enabled")
}

if (!tauriConf.plugins?.updater?.pubkey) {
fail("Tauri updater public key is missing")
}

if (!changelog.includes(`## ${version}`)) {
Expand Down
1 change: 0 additions & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ reqwest = { version = "0.13", features = ["blocking"] }
rquickjs = { version = "0.11" }
tauri-plugin-store = "2.4.2"
base64 = "0.22"
sha2 = "0.10"
aes-gcm = "0.10"
uuid = { version = "1", features = ["v4"] }
tauri-plugin-log = "2"
Expand Down
Loading
Loading