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" + } } }