From 92d4b4299b0d5cacc3a3272c03edca6f4cced381 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 31 Oct 2026 16:22:59 +0100 Subject: [PATCH] Add the release workflow, and cut 0.1.0 The release workflow builds macOS (universal), Windows (x64) and Linux (x64 and arm64), runs the smoke test on each build, and writes a draft GitHub release with the installers and the changelog section. Publishing the draft writes the tag. The macOS build is signed ad hoc, or with Developer ID and notarized if the APPLE_* secrets exist. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FW6WZZR7nYJaknHYwVDGAq --- .github/scripts/changelog-section.mjs | 22 +++ .github/scripts/collect-bundles.sh | 25 ++++ .github/workflows/release.yml | 189 ++++++++++++++++++++++++++ CHANGELOG.md | 2 + README.md | 14 ++ src-tauri/tauri.conf.json | 6 +- 6 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/changelog-section.mjs create mode 100755 .github/scripts/collect-bundles.sh create mode 100644 .github/workflows/release.yml diff --git a/.github/scripts/changelog-section.mjs b/.github/scripts/changelog-section.mjs new file mode 100644 index 0000000..b90ccde --- /dev/null +++ b/.github/scripts/changelog-section.mjs @@ -0,0 +1,22 @@ +/* + Prints the section of CHANGELOG.md for one version, without its heading. + Fails if there is no section, or if the section is empty. + + node .github/scripts/changelog-section.mjs 0.1.0 +*/ +import { readFileSync } from 'node:fs'; + +const version = (process.argv[2] ?? '').replace(/^v/, ''); +const lines = readFileSync(new URL('../../CHANGELOG.md', import.meta.url), 'utf8').split('\n'); +const start = lines.findIndex((l) => l.trim() === `## ${version}`); +if (!version || start < 0) { + console.error(`CHANGELOG.md has no "## ${version}" section`); + process.exit(1); +} +const end = lines.findIndex((l, i) => i > start && l.startsWith('## ')); +const body = lines.slice(start + 1, end < 0 ? undefined : end).join('\n').trim(); +if (!body) { + console.error(`the "## ${version}" section of CHANGELOG.md is empty`); + process.exit(1); +} +console.log(body); diff --git a/.github/scripts/collect-bundles.sh b/.github/scripts/collect-bundles.sh new file mode 100755 index 0000000..9b89b62 --- /dev/null +++ b/.github/scripts/collect-bundles.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# collect-bundles.sh +# +# Copies the installers that `tauri build` wrote into , under names +# without spaces: OpenDocument-Reader__.. +set -euo pipefail +bundle=$1 out=$2 version=$3 platform=$4 +mkdir -p "$out" +found=0 +while IFS= read -r -d '' file; do + name=$(basename "$file") + case "$name" in + *-setup.exe) ext=setup.exe ;; + *.AppImage) ext=AppImage ;; + *) ext=${name##*.} ;; + esac + cp "$file" "$out/OpenDocument-Reader_${version}_${platform}.${ext}" + found=$((found + 1)) +done < <(find "$bundle" -maxdepth 2 -type f \ + \( -name '*.dmg' -o -name '*.msi' -o -name '*-setup.exe' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' \) -print0) +if [ "$found" -eq 0 ]; then + echo "::error::no installers under $bundle" + exit 1 +fi +ls -la "$out" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2014100 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,189 @@ +name: release +# Builds the installers for every system, runs the smoke test on each build, and +# writes a draft GitHub release with the installers and the changelog section. +# Publishing the draft writes the v tag; this workflow writes no tag. +# +# The version must already be in package.json and have a section in +# CHANGELOG.md, so the repository records what went out. +# +# Signing: with the APPLE_* secrets, the macOS build is signed with Developer +# ID and notarized. Without them, it is signed ad hoc. Windows builds are not +# signed yet. +on: + workflow_dispatch: + inputs: + version: + description: version to release, e.g. 0.1.0 + type: string + required: true + dry_run: + description: build only, write no release + type: boolean + default: false + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v5 + + - name: Check the version + id: version + # Through the environment, because the input is outside text. + env: + given: ${{ inputs.version }} + run: | + version=${given#v} + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then + echo "::error::'$given' is not a version such as 0.1.0" + exit 1 + fi + in_package=$(node -p "require('./package.json').version") + if [ "$in_package" != "$version" ]; then + echo "::error::package.json says $in_package, not $version" + exit 1 + fi + if git ls-remote --exit-code --tags origin "v$version" > /dev/null 2>&1; then + echo "::error::the tag v$version exists already" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Check the changelog + run: node .github/scripts/changelog-section.mjs "${{ steps.version.outputs.version }}" + + build: + needs: check + strategy: + fail-fast: true + matrix: + include: + - os: macos-latest + platform: macos-universal + args: --target universal-apple-darwin + targets: aarch64-apple-darwin,x86_64-apple-darwin + bundle: src-tauri/target/universal-apple-darwin/release/bundle + app: src-tauri/target/universal-apple-darwin/release/opendocument-desktop + - os: windows-latest + platform: windows-x64 + bundle: src-tauri/target/release/bundle + app: src-tauri/target/release/opendocument-desktop.exe + # 22.04, so the packages run on systems with an older glibc too. + - os: ubuntu-22.04 + platform: linux-x64 + bundle: src-tauri/target/release/bundle + app: src-tauri/target/release/opendocument-desktop + - os: ubuntu-22.04-arm + platform: linux-arm64 + bundle: src-tauri/target/release/bundle + app: src-tauri/target/release/opendocument-desktop + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + + - name: Install the Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libxdo-dev xvfb + + - uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: npm + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.targets }} + + - name: Use the Apple signing secrets + if: runner.os == 'macOS' + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} + run: | + # Only set what exists: Tauri reads an empty variable as a value. + if [ -n "$APPLE_CERTIFICATE" ] && [ -n "$APPLE_SIGNING_IDENTITY" ]; then + { + echo "APPLE_CERTIFICATE=$APPLE_CERTIFICATE" + echo "APPLE_CERTIFICATE_PASSWORD=$APPLE_CERTIFICATE_PASSWORD" + echo "APPLE_SIGNING_IDENTITY=$APPLE_SIGNING_IDENTITY" + } >> "$GITHUB_ENV" + echo "Signing with Developer ID." + else + echo "::warning::no Developer ID secrets, so the macOS build is signed ad hoc" + fi + if [ -n "$APPLE_API_KEY_CONTENT" ] && [ -n "$APPLE_API_KEY" ] && [ -n "$APPLE_API_ISSUER" ]; then + key="$RUNNER_TEMP/AuthKey_$APPLE_API_KEY.p8" + printf '%s' "$APPLE_API_KEY_CONTENT" > "$key" + { + echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" + echo "APPLE_API_KEY=$APPLE_API_KEY" + echo "APPLE_API_KEY_PATH=$key" + } >> "$GITHUB_ENV" + echo "Notarizing." + fi + + - run: npm ci + - run: npm run check + - run: npx tauri build ${{ matrix.args }} + + - name: Smoke test + shell: bash + run: | + if [ "$RUNNER_OS" = Linux ]; then + xvfb-run -a node test/smoke.mjs "${{ matrix.app }}" + else + node test/smoke.mjs "${{ matrix.app }}" + fi + + - name: Collect the installers + shell: bash + run: .github/scripts/collect-bundles.sh "${{ matrix.bundle }}" release "${{ needs.check.outputs.version }}" "${{ matrix.platform }}" + + - uses: actions/upload-artifact@v5 + with: + name: release-${{ matrix.platform }} + path: release/ + if-no-files-found: error + + release: + needs: [check, build] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - uses: actions/download-artifact@v5 + with: + pattern: release-* + path: release + merge-multiple: true + + - name: Write the draft release + env: + GH_TOKEN: ${{ github.token }} + version: ${{ needs.check.outputs.version }} + run: | + node .github/scripts/changelog-section.mjs "$version" > notes.md + (cd release && sha256sum * > SHA256SUMS.txt) + gh release create "v$version" release/* \ + --draft \ + --target "$GITHUB_SHA" \ + --title "$version" \ + --notes-file notes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f56be25..8105b5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Before a release, a pull request changes that heading to the version. ## Unreleased +## 0.1.0 + - The first version of OpenDocument Reader for Windows, macOS and Linux. It opens OpenDocument, Word, Excel, PowerPoint, PDF, Pages, Numbers, Keynote and more, also with a password. diff --git a/README.md b/README.md index 91906af..593e3de 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,20 @@ npm run tauri build # writes the installers to src-tauri/target/release/bundle `node test/smoke.mjs src-tauri/target/release/opendocument-desktop` opens each document in `test/documents/` in the app and saves a copy. +## Release + +1. In a pull request, set the version in `package.json` (`npm version --no-git-tag-version 0.2.0`), + and change the `Unreleased` heading of `CHANGELOG.md` to that version. +2. Run the `release` workflow with that version. `dry_run` builds only. +3. The workflow writes a draft release with the installers for macOS (universal), + Windows (x64) and Linux (x64 and arm64). Publishing the draft writes the tag. + +The macOS build is signed with Developer ID and notarized if the repository has +the secrets `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, +`APPLE_SIGNING_IDENTITY`, `APPLE_API_ISSUER`, `APPLE_API_KEY` and +`APPLE_API_KEY_CONTENT`. Without them, it is signed ad hoc, and macOS asks the +user to confirm the first start. The Windows build is not signed. + ## Layout | Path | What | diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 329e673..59652e6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -144,6 +144,10 @@ "mimeType": "application/vnd.apple.keynote", "role": "Viewer" } - ] + ], + "macOS": { + "signingIdentity": "-", + "minimumSystemVersion": "11.0" + } } }