Skip to content
Merged
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
87 changes: 83 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,54 @@ name: Release

on:
push:
tags: ['v*']
branches: [main]
workflow_dispatch:

permissions:
contents: write

jobs:
# A minute of Linux in front of six minutes of Windows. Most pushes to main are not releases, and
# this is what tells them apart before anything is compiled.
decide:
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.check.outputs.publish }}
tag: ${{ steps.check.outputs.tag }}
steps:
- uses: actions/checkout@v4

- name: Is this version already released?
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
version=$(jq -r .version package.json)
tag="v$version"
echo "tag=$tag" >> "$GITHUB_OUTPUT"

# A run started by hand is a rehearsal whatever the version says: it builds and signs and
# publishes nothing, which is what makes it safe to press.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "::notice::Rehearsal of $tag. Nothing will be published."
exit 0
fi

if gh api "repos/${{ github.repository }}/git/ref/tags/$tag" >/dev/null 2>&1; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "::notice::$tag is already released. Bump version in src-tauri/tauri.conf.json to cut a new one."
exit 0
fi

echo "publish=true" >> "$GITHUB_OUTPUT"
echo "::notice::$tag has no release yet. Building one."

release:
needs: decide
# A push whose version is already out has nothing to do; a rehearsal still builds, because
# finding out that a release *would* work is the whole point of pressing the button.
if: needs.decide.outputs.publish == 'true' || github.event_name == 'workflow_dispatch'
runs-on: windows-latest
defaults:
run:
Expand Down Expand Up @@ -65,6 +105,23 @@ jobs:
# naming a dependency that was added after it was last written.
# `npm run licenses` shells out to `cargo license`, which no runner image carries. Without it
# the notices cannot be regenerated, and they are what the licences require to ship.
# The check above cannot tell a wrong password from a right one -- only that a key is there.
# Signing a throwaway file can, and it costs a second against six minutes of build before the
# bundler reaches the same conclusion. The signature is public; the key never leaves the env.
- name: Check the key and its password sign something
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
Set-Content -Path signing-probe.txt -Value 'probe'
npx tauri signer sign signing-probe.txt
if ($LASTEXITCODE -ne 0) {
Write-Host '::error::The key and TAURI_SIGNING_PRIVATE_KEY_PASSWORD do not match. Set the password the key was generated with, or generate a new pair and update plugins.updater.pubkey with its public half.'
exit 1
}
Remove-Item signing-probe.txt, signing-probe.txt.sig -ErrorAction SilentlyContinue
Write-Host 'Key and password agree.'

- name: Install cargo-license
run: cargo install cargo-license --locked

Expand All @@ -85,8 +142,9 @@ jobs:
run: |
$setup = Get-ChildItem target/release/bundle/nsis/*-setup.exe | Select-Object -First 1
$signature = Get-Content "$($setup.FullName).sig" -Raw
$version = "${{ github.ref_name }}".TrimStart('v')
$url = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/$($setup.Name)"
$tag = '${{ needs.decide.outputs.tag }}'
$version = $tag.TrimStart('v')
$url = "https://github.com/${{ github.repository }}/releases/download/$tag/$($setup.Name)"
$manifest = [ordered]@{
version = $version
notes = "See the release notes for $version."
Expand All @@ -97,10 +155,31 @@ jobs:
}
$manifest | ConvertTo-Json -Depth 5 | Out-File latest.json -Encoding utf8

- uses: softprops/action-gh-release@v2
# `tag_name` on a tag that does not exist yet creates it at this commit, so nobody has to
# remember to tag by hand — bumping the version in tauri.conf.json is the whole ceremony.
- name: Publish the release
if: needs.decide.outputs.publish == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.decide.outputs.tag }}
files: |
target/release/bundle/nsis/*-setup.exe
target/release/bundle/nsis/*-setup.exe.sig
latest.json
fail_on_unmatched_files: true

- name: Keep what the rehearsal produced
if: needs.decide.outputs.publish != 'true'
uses: actions/upload-artifact@v4
with:
name: release-rehearsal
path: |
target/release/bundle/nsis/*-setup.exe
target/release/bundle/nsis/*-setup.exe.sig
latest.json
if-no-files-found: error

- name: Say that nothing was published
if: needs.decide.outputs.publish != 'true'
run: |
Write-Host '::notice::Rehearsal only. The installer, its signature and latest.json are attached to this run as an artifact. A release happens when a push to main carries a version that has no tag yet.'
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,18 @@ npx tauri signer generate -w ~/.tauri/openeventviewer.key

The public half goes into `plugins.updater.pubkey` in `src-tauri/tauri.conf.json`; the private half
and its password are the GitHub secrets `TAURI_SIGNING_PRIVATE_KEY` and
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` and never enter the repository. `release.yml` builds on a `v*`
tag and uploads the installer, its `.sig` and a `latest.json` written from that signature.
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` and never enter the repository.

**Cutting a release is bumping a number.** `package.json` holds the version; `tauri.conf.json`
points at it and the interface reads it through `__APP_VERSION__`, so it is written once. A push to
`main` whose version has no tag yet builds, signs, tags at that commit and publishes the installer,
its `.sig` and a `latest.json` written from that signature. A push whose version is already out
stops in under a minute. Running the workflow by hand is a rehearsal: it builds and signs and
attaches the result to the run, and publishes nothing.

```bash
npm version patch # or minor, or major
```

## Commands

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenEventViewer",
"version": "0.1.0",
"version": "../package.json",
"identifier": "com.thorstenalpers.openeventviewer",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/views/info-view.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const t = $derived(i18n.t);

let filter = $state('');
const APP_VERSION = '0.1.0';
const APP_VERSION = __APP_VERSION__;

let notices = $state<string | null>(null);
let noticesError = $state<string | null>(null);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/views/settings-view.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { settings, MAX_ROW_CHOICES } from '$lib/stores/settings.svelte';
import { updater } from '$lib/stores/updater.svelte';

const APP_VERSION = '0.1.0';
const APP_VERSION = __APP_VERSION__;

const t = $derived(i18n.t);

Expand Down
3 changes: 3 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/// <reference types="vite/client" />

/** The version in package.json, substituted at build time — see `define` in vite.config.ts. */
declare const __APP_VERSION__: string;
6 changes: 6 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';

// One version, in package.json, which tauri.conf.json also points at. A number typed a second time
// into a view is a number that goes stale the first time anyone bumps the first one.
const { version } = JSON.parse(readFileSync('package.json', 'utf8')) as { version: string };

export default defineConfig({
define: { __APP_VERSION__: JSON.stringify(version) },
plugins: [tailwindcss(), sveltekit()],
server: {
// Pinned because tauri.conf.json waits for exactly this URL: on a taken port Vite would
Expand Down
Loading