# Release pipeline, after PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). # # Push to `release` to build packages and create/update a DRAFT GitHub Release. The version # comes from [workspace.package] in Cargo.toml. An agent verifies the artifacts and publishes # the draft separately; see docs/releasing.md. Tags and pushes to main do not trigger releases. # A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet. # # Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .tar.gz packaging/linux # FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd # macOS universal .dmg, and the CLI as a .zip packaging/macos # Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows # Web a .zip of the static site packaging/web # # A dry run: dispatch with draft=false (the default). Only the release branch may create drafts. name: Release on: push: branches: [release] workflow_dispatch: inputs: draft: description: "Create/update a draft (release branch only; off: build artifacts only)" type: boolean default: false permissions: contents: read concurrency: group: release-${{ github.ref }} cancel-in-progress: false env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 jobs: 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - id: v name: Resolve version env: REQUEST_DRAFT: ${{ inputs.draft || false }} run: | . packaging/env.sh if ! printf '%s' "$VERSION" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?$'; then echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1" exit 1 fi if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/release ]; then echo "::error::Draft releases must be built from the release branch" exit 1 fi case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac { echo "version=$VERSION"; echo "date=$(date -u +%Y-%m-%d)"; echo "prerelease=$pre"; } >>"$GITHUB_OUTPUT" echo "### SolveCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" # The app icon in every size, drawn by solvecraft-cli (no image files are committed), for the # Windows jobs: the ARM64 build is cross-compiled and can't run there to draw its own. icons: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-icons - run: cargo build --release --locked -p solvecraft-cli - run: | . packaging/env.sh make_icons target/release/solvecraft-cli icons - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: icons path: icons 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. runs-on: ${{ matrix.runner }} 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: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} NFPM_VERSION: 2.47.0 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install packaging tools run: | sudo apt-get update sudo apt-get install -y --no-install-recommends desktop-file-utils appstream zsync rpm 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@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-linux-${{ matrix.arch }} - name: Build and package (AppImage, deb, rpm, tar.gz) 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/solvecraft$|usr/bin/solvecraft-cli$|metainfo|applications|THIRD-PARTY-LICENSES.txt' rpm -qip dist/release/*.rpm rpm -qlp dist/release/*.rpm | grep -E '/usr/bin/solvecraft$|THIRD-PARTY-LICENSES.txt' tar -tzf dist/release/*.tar.gz | grep -E '/bin/solvecraft$|THIRD-PARTY-LICENSES.txt' ldd target/release/solvecraft for appimage in dist/release/*.AppImage; do chmod +x "$appimage" APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version "$appimage" --appimage-updateinformation | grep -F 'gh-releases-zsync|' test -s "$appimage.zsync" done - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 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 than 22.04 ships. runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - { arch: x86_64, runner: ubuntu-24.04 } - { arch: aarch64, runner: ubuntu-24.04-arm } env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-linux-${{ matrix.arch }} path: dist/linux - name: Install flatpak-builder run: | sudo apt-get update 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/solvecraft-$SOLVECRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-flatpak-${{ matrix.arch }} path: dist/release/*.flatpak if-no-files-found: error freebsd: name: FreeBSD x86_64 needs: version # GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM. runs-on: ubuntu-latest timeout-minutes: 180 env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Build and package in a FreeBSD VM (tar.gz) # vmactions/freebsd-vm v1.5.9, pinned by commit (it runs our build). uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc with: release: "14.3" usesh: true cpu: 4 mem: 12288 # Passed into the VM by name; never templated into the script below. envs: "SOLVECRAFT_VERSION SOLVECRAFT_BUILD_DATE" prepare: | pkg install -y bash git curl pkgconf libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable run: | set -e . "$HOME/.cargo/env" git config --global --add safe.directory "$PWD" # Build outside the checkout so only dist/ is copied back out of the VM. 4 jobs: more # runs the VM out of memory. export CARGO_TARGET_DIR=/var/tmp/solvecraft-target CARGO_BUILD_JOBS=4 bash packaging/freebsd/package.sh tar -tzf dist/release/solvecraft-*-freebsd-x86_64.tar.gz \ | grep -E '/bin/solvecraft$|/bin/solvecraft-cli$|/share/applications/ai\.storyteller\.solvecraft\.desktop$|/share/doc/solvecraft/THIRD-PARTY-LICENSES\.txt$' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-freebsd-x86_64 path: dist/release/* if-no-files-found: error macos: name: macOS universal needs: version runs-on: macos-14 env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} SOLVECRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin, x86_64-apple-darwin - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-macos-universal - name: Build and package (dmg, CLI zip; the dmg is mounted and checked) run: packaging/macos/package.sh --arch universal - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-macos path: dist/release/* if-no-files-found: error windows: name: Windows ${{ matrix.arch }} needs: [version, icons] runs-on: windows-latest 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. - { arch: arm64, target: aarch64-pc-windows-msvc } env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: icons path: icons - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-windows-${{ matrix.arch }} - name: Install WiX shell: pwsh run: | dotnet tool install --global wix --version 5.0.2 wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2 - name: Build and package (msi, portable zip) shell: pwsh env: SOLVECRAFT_ICONS: ${{ github.workspace }}/icons run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-windows-${{ matrix.arch }} path: dist/release/* if-no-files-found: error web: name: Web (wasm) needs: version runs-on: ubuntu-latest env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} TRUNK_VERSION: 0.21.14 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 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 - name: Build and zip run: packaging/web/package.sh - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-web path: dist/release/* if-no-files-found: error release: name: Draft GitHub Release or dry-run artifacts needs: [version, linux, flatpak, freebsd, macos, windows, web] runs-on: ubuntu-latest 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/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: release-* path: artifacts merge-multiple: true - name: Checksums and notes run: | (cd artifacts && sha256sum -- * | tee SHA256SUMS.txt) sed "s/{version}/$VERSION/g" .github/release-notes.md >notes.md - name: Create or update the draft release if: github.ref == 'refs/heads/release' && (github.event_name == 'push' || inputs.draft) working-directory: artifacts run: | tag="v$VERSION" # Refuse existing tags: a draft must point at this build, never an older tag. # matching-refs returns [] for an absent tag and fails on API/auth errors. existing_refs="$(gh api "repos/$GH_REPO/git/matching-refs/tags/$tag" \ --jq '.[].ref')" if printf '%s\n' "$existing_refs" | grep -Fxq "refs/tags/$tag"; then echo "::error::Tag $tag already exists. Choose a new version; never move a release tag." exit 1 fi 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 before releasing." exit 1 fi # Replace the complete draft asset set, including assets removed in this build. assets="$(gh release view "$tag" --json assets --jq '.assets[].name')" while IFS= read -r asset; do if [ -n "$asset" ]; then gh release delete-asset "$tag" "$asset" --yes; fi done <<<"$assets" gh release edit "$tag" --draft --title "SolveCraft v$VERSION" \ --target "$GITHUB_SHA" --prerelease="$PRERELEASE" gh release upload "$tag" --clobber -- * else gh release create "$tag" --draft --title "SolveCraft v$VERSION" \ --target "$GITHUB_SHA" --notes-file ../notes.md --generate-notes "${pre[@]}" -- * fi { echo "Draft **SolveCraft v$VERSION** at \`${GITHUB_SHA::9}\`:" echo sed 's/^/ /' SHA256SUMS.txt } >>"$GITHUB_STEP_SUMMARY" - name: Keep the files (dry run) if: github.event_name == 'workflow_dispatch' && !inputs.draft run: cp notes.md artifacts/release-notes.md - if: github.event_name == 'workflow_dispatch' && !inputs.draft uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-assets path: artifacts/*