# Release pipeline: every push to `release` builds signed installers for every platform and # creates or updates the draft GitHub Release "WordCraft v". See docs/releasing.md. # # The signing jobs (macOS, Windows) and the job that drafts the release run in the `release` # environment (restricted to the `release` branch), which holds the signing secrets. Every secret # is optional: missing ones produce unsigned artifacts and a warning, never a failed build. The # jobs that sign nothing (version, Linux, Flatpak, FreeBSD, web) get no environment and no # secrets, so a `workflow_dispatch` on any branch dry-runs them; there the signing jobs are refused # by the environment's branch rule, and the release job, which needs them, is skipped. name: Release on: push: branches: [release] workflow_dispatch: inputs: version: description: "Version override for a test run, e.g. 0.2.0-rc.1 (empty: Cargo.toml)" type: string default: "" permissions: contents: read concurrency: group: release-${{ github.ref }} # Never cancel a run mid-notarization or mid-upload; a newer push waits its turn. cancel-in-progress: false env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 WORDCRAFT_BUILD_SHA: ${{ github.sha }} INPUT_VERSION: ${{ inputs.version }} jobs: version: name: version runs-on: ubuntu-latest outputs: version: ${{ steps.v.outputs.version }} date: ${{ steps.v.outputs.date }} prerelease: ${{ steps.v.outputs.prerelease }} steps: - uses: actions/checkout@v4 - id: v name: Resolve version run: | . packaging/env.sh v="${INPUT_VERSION#v}" v="${v:-$VERSION}" if ! printf '%s' "$v" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then echo "::error::'$v' is not a version like 1.2.3 or 1.2.3-rc.1" exit 1 fi case "$v" in *-*) pre=true ;; *) pre=false ;; esac { echo "version=$v" echo "date=$(date -u +%Y-%m-%d)" echo "prerelease=$pre" } >>"$GITHUB_OUTPUT" echo "### WordCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" macos: name: macOS universal needs: version runs-on: macos-15-xlarge # M2 Pro larger runner; use only in release.yml (actions_macos_xl budget caps spend) environment: release env: WORDCRAFT_VERSION: ${{ needs.version.outputs.version }} WORDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 # Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it. - name: Check out craft-fonts uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin, x86_64-apple-darwin - uses: Swatinem/rust-cache@v2 with: key: release-macos-universal - name: Apply version override if: inputs.version != '' run: cargo xtask version set "$WORDCRAFT_VERSION" - name: Import signing certificate env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: packaging/macos/import-cert.sh - name: Build, sign, notarize env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_REQUIRED: "1" APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: packaging/macos/package.sh --arch universal - uses: actions/upload-artifact@v4 with: name: release-macos path: dist/release/* if-no-files-found: error - name: Remove temporary keychain if: always() run: | if [ -n "${MACOS_KEYCHAIN:-}" ]; then security delete-keychain "$MACOS_KEYCHAIN" || true; fi windows: name: Windows ${{ matrix.arch }} needs: version runs-on: windows-latest environment: release strategy: fail-fast: false matrix: include: - { arch: x64, target: x86_64-pc-windows-msvc } - { arch: x86, target: i686-pc-windows-msvc } # Windows on ARM: cross-compiled on the x64 runner, so signing and WiX run as above. # .github/workflows/windows-arm64.yml installs and runs it on ARM64 hardware. - { arch: arm64, target: aarch64-pc-windows-msvc } env: WORDCRAFT_VERSION: ${{ needs.version.outputs.version }} WORDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 # Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it. - name: Check out craft-fonts uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 with: key: release-windows-${{ matrix.arch }} - name: Apply version override if: inputs.version != '' shell: bash run: cargo xtask version set "$WORDCRAFT_VERSION" - name: Install WiX shell: pwsh run: | $PSNativeCommandUseErrorActionPreference = $true # stop at the first failing command dotnet tool install --global wix --version 5.0.2 # Installer dialogs and the finish page's "Launch WordCraft" (packaging/windows/wordcraft.wxs). wix extension add -g WixToolset.UI.wixext/5.0.2 wix extension add -g WixToolset.Util.wixext/5.0.2 - name: Build, sign, package shell: pwsh env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_REQUIRED: "1" WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} - uses: actions/upload-artifact@v4 with: name: release-windows-${{ matrix.arch }} path: dist/release/* if-no-files-found: error linux: name: Linux ${{ matrix.arch }} needs: version # The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35 (Ubuntu 22.04+, # Debian 12+, Fedora 36+, RHEL 10). See docs/releasing.md. runs-on: ${{ matrix.runner }} # No `environment: release`: this job signs nothing, so it gets no release secrets. strategy: fail-fast: false matrix: include: - { arch: x86_64, runner: ubuntu-22.04, deb: amd64 } - { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 } env: WORDCRAFT_VERSION: ${{ needs.version.outputs.version }} WORDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} NFPM_VERSION: 2.47.0 steps: - uses: actions/checkout@v4 # Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it. - name: Check out craft-fonts uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 path: craft-fonts - name: Install build and packaging tools run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev \ desktop-file-utils appstream python3-yaml zsync curl -fsSL -o "$RUNNER_TEMP/nfpm.deb" \ "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${{ matrix.deb }}.deb" sudo dpkg -i "$RUNNER_TEMP/nfpm.deb" - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: key: release-linux-${{ matrix.arch }} - name: Apply version override if: inputs.version != '' run: cargo xtask version set "$WORDCRAFT_VERSION" - name: Build and package (AppImage, deb, rpm, tar.gz) env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_REQUIRED: "1" run: packaging/linux/package.sh - name: Check the packages run: | dpkg-deb --info dist/release/*.deb dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/wordcraft$|metainfo|applications' ldd target/release/wordcraft python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.wordcraft.yml')); assert m['id'] == 'ai.storyteller.wordcraft'" for appimage in dist/release/*.AppImage; do chmod +x "$appimage" APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version # Update information is embedded, and the .zsync for delta updates sits beside it. "$appimage" --appimage-updateinformation | grep -F 'gh-releases-zsync|' test -s "$appimage.zsync" done - uses: actions/upload-artifact@v4 with: name: release-linux-${{ matrix.arch }} path: dist/release/* if-no-files-found: error flatpak: name: Flatpak ${{ matrix.arch }} needs: [version, linux] # Repackages the Linux job's tarball (the same binaries as the AppImage/deb/rpm); no Rust # build here. 24.04 for a newer flatpak-builder (1.4) than 22.04 ships. runs-on: ${{ matrix.runner }} # No `environment: release`: this job signs nothing, so it gets no release secrets. strategy: fail-fast: false matrix: include: - { arch: x86_64, runner: ubuntu-24.04 } - { arch: aarch64, runner: ubuntu-24.04-arm } env: WORDCRAFT_VERSION: ${{ needs.version.outputs.version }} WORDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: release-linux-${{ matrix.arch }} path: dist/linux - name: Install flatpak-builder run: | sudo apt-get update # librsvg2-common: the SVG pixbuf loader. flatpak-builder runs the host's # `appstreamcli compose`, which renders the scalable icon and fails without it. sudo apt-get install -y --no-install-recommends flatpak flatpak-builder librsvg2-common # Ubuntu 24.04's AppArmor blocks the unprivileged user namespaces bubblewrap needs. sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Build the Flatpak bundle run: packaging/linux/flatpak-bundle.sh "dist/linux/wordcraft-$WORDCRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz" - uses: actions/upload-artifact@v4 with: name: release-flatpak-${{ matrix.arch }} path: dist/release/*.flatpak if-no-files-found: error web: name: Web (wasm) needs: version runs-on: ubuntu-latest # No `environment: release`: this job signs nothing, so it gets no release secrets. env: WORDCRAFT_VERSION: ${{ needs.version.outputs.version }} WORDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} TRUNK_VERSION: 0.21.14 steps: - uses: actions/checkout@v4 # Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it. - name: Check out craft-fonts uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: key: release-web - name: Install trunk run: | curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \ | tar -xz -C "$HOME/.cargo/bin" trunk trunk --version - name: Apply version override if: inputs.version != '' run: cargo xtask version set "$WORDCRAFT_VERSION" - name: Build and zip env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_REQUIRED: "1" run: packaging/web/package.sh - uses: actions/upload-artifact@v4 with: name: release-web path: dist/release/* if-no-files-found: error freebsd: name: FreeBSD x86_64 needs: version runs-on: ubuntu-latest timeout-minutes: 300 # No `environment: release`: this job signs nothing, so it gets no release secrets. env: WORDCRAFT_VERSION: ${{ needs.version.outputs.version }} WORDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 # Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it. # Synced into the VM with the sources. - name: Check out craft-fonts uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 path: craft-fonts - name: Build in a FreeBSD VM uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 with: release: "14.3" usesh: true cpu: 4 mem: 12288 envs: "WORDCRAFT_VERSION WORDCRAFT_BUILD_DATE WORDCRAFT_BUILD_SHA" copyback: true prepare: | pkg install -y git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal run: | . "$HOME/.cargo/env" git config --global --add safe.directory "*" cargo run -q -p xtask -- version set "$WORDCRAFT_VERSION" CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 cargo build --release --locked -p wordcraft -p wordcraft-cli name="wordcraft-$WORDCRAFT_VERSION-freebsd-x86_64" mkdir -p "dist/release/$name" cp target/release/wordcraft target/release/wordcraft-cli LICENSE-MIT LICENSE-APACHE NOTICE ATTRIBUTION.md README.md "dist/release/$name/" cp packaging/linux/ai.storyteller.wordcraft.desktop "dist/release/$name/" cp assets/app-icon/hicolor/256x256/apps/ai.storyteller.wordcraft.png "dist/release/$name/" # The embedded craft-fonts' licences, as in the Linux packages (OFL-.txt). for f in craft-fonts/fonts/*/OFL.txt; do cp "$f" "dist/release/$name/OFL-$(basename "$(dirname "$f")").txt"; done tar -C dist/release -czf "dist/release/$name.tar.gz" "$name" rm -rf "dist/release/$name" target - uses: actions/upload-artifact@v4 with: name: release-freebsd path: dist/release/*.tar.gz if-no-files-found: error release: name: Draft GitHub Release # Needs the signing jobs, which only run on `release`: a branch dry run never gets here. needs: [version, macos, windows, linux, flatpak, freebsd, web] runs-on: ubuntu-latest environment: release permissions: contents: write env: VERSION: ${{ needs.version.outputs.version }} PRERELEASE: ${{ needs.version.outputs.prerelease }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} steps: - uses: actions/download-artifact@v4 with: pattern: release-* path: artifacts merge-multiple: true - name: Checksums working-directory: artifacts run: | sha256sum -- * | tee SHA256SUMS.txt - name: Create or update the draft release working-directory: artifacts run: | tag="v$VERSION" title="WordCraft v$VERSION" pre=() if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then if [ "$draft" != "true" ]; then echo "::error::Release $tag is already published. Bump the version (cargo xtask version set) before pushing to release." exit 1 fi echo "Updating draft $tag: replacing its assets" gh release view "$tag" --json assets --jq '.assets[].name' | while read -r asset; do gh release delete-asset "$tag" "$asset" --yes done gh release edit "$tag" --draft --title "$title" --target "$GITHUB_SHA" --prerelease="$PRERELEASE" gh release upload "$tag" --clobber -- * else echo "Creating draft $tag" gh release create "$tag" --draft --title "$title" --target "$GITHUB_SHA" --generate-notes "${pre[@]}" -- * fi { echo "Draft release **$title** at \`${GITHUB_SHA::9}\`:" echo sed 's/^/ /' SHA256SUMS.txt } >>"$GITHUB_STEP_SUMMARY"