# 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. # At midnight Eastern, changed main commits build and publish a nightly pre-release. # A version with a suffix (0.2.0-rc.1) is a pre-release. # The macOS app is signed and notarized when the APPLE_* secrets exist. The Windows binaries and # MSI are Authenticode-signed through SignPath Foundation when the SIGNPATH_API_TOKEN secret exists # (see the windows job). Nothing else is signed yet. # # Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux # Arch Linux x86_64, aarch64 .pkg.tar.zst, repackaged from the .tar.gz the arch job # FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd # macOS universal .dmg, and the CLI as a .zip (both archs built in parallel) 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 # # The main installers are also attached without the version in their names # (solidcraft-windows-x64.msi, solidcraft-linux-x86_64.AppImage, ...) so that # releases/latest/download/ links in the README keep working across releases. # # A dry run: dispatch with draft=false (the default). Only the release branch may create drafts. # The release gate (release-gate.yml) calls it as such a dry run, without secrets (unsigned) and # without the release job (packages_only). # A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists: # gh workflow run release.yml --ref -f draft=false [-f signpath_signing_policy=test-signing] name: Release on: schedule: - cron: '0 0 * * *' timezone: America/New_York push: branches: [release] workflow_dispatch: inputs: draft: description: "Create/update a draft (release branch only; off: build artifacts only)" type: boolean default: false signpath_signing_policy: description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)" type: string default: "" # The release gate (release-gate.yml): a dry run (draft false) of every package and its checks. # The event stays the caller's (workflow_dispatch), so a call runs as a dispatched dry run does. # The gate passes no secrets, so nothing is signed (the signing steps skip without their # secrets, as on a fork), and packages_only skips the release job (checksums, update signing, # publishing). workflow_call: inputs: packages_only: description: "Build and check every package only: no release job" type: boolean default: false draft: description: "Create/update a draft (release branch only; off: build artifacts only)" type: boolean default: false signpath_signing_policy: description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)" type: string default: "" 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: build: ${{ steps.changed.outputs.build }} version: ${{ steps.v.outputs.version }} date: ${{ steps.v.outputs.date }} prerelease: ${{ steps.v.outputs.prerelease }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - id: changed name: Skip unchanged nightly builds env: GH_TOKEN: ${{ github.token }} run: bash packaging/nightly.sh - id: v if: steps.changed.outputs.build == 'true' name: Resolve version env: REQUEST_DRAFT: ${{ inputs.draft || false }} run: | . packaging/env.sh if [ "$GITHUB_EVENT_NAME" = schedule ]; then # Unique SemVer without committing daily version bumps to main. VERSION="${VERSION%%-*}-nightly.$(TZ=America/New_York date +%Y%m%d).$GITHUB_RUN_NUMBER.$GITHUB_RUN_ATTEMPT" fi 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 "### SolidCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" # The app icon in every size, drawn by solidcraft-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: needs: version if: needs.version.outputs.build == 'true' 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 solidcraft-cli - run: | . packaging/env.sh make_icons target/release/solidcraft-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 if: needs.version.outputs.build == 'true' # 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: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} SOLIDCRAFT_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/solidcraft$|usr/bin/solidcraft-cli$|metainfo|applications|THIRD-PARTY-LICENSES.txt' # Compatibility shims (docs/compatibility.md): the old CLI name and the package rename. dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/solvecraft-cli$' dpkg-deb --field dist/release/*.deb Replaces Conflicts Provides | grep -F solvecraft rpm -qip dist/release/*.rpm rpm -qlp dist/release/*.rpm | grep -E '/usr/bin/solidcraft$|THIRD-PARTY-LICENSES.txt' tar -tzf dist/release/*.tar.gz | grep -E '/bin/solidcraft$|THIRD-PARTY-LICENSES.txt' ldd target/release/solidcraft 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 # Legacy AppImage update alias (docs/compatibility.md). ls dist/release/solvecraft-*-linux-*.AppImage.zsync - 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] if: needs.version.outputs.build == 'true' # 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: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} SOLIDCRAFT_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 xvfb xauth weston dbus-x11 # 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/solidcraft-$SOLIDCRAFT_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 arch: name: Arch Linux ${{ matrix.arch }} needs: [version, linux] if: needs.version.outputs.build == 'true' # makepkg only repackages the tarball's prebuilt binaries, so both architectures build here. runs-on: ubuntu-24.04 # A pre-release or nightly without an Arch package is still worth having; a stable release isn't. continue-on-error: ${{ contains(needs.version.outputs.version, '-') }} strategy: fail-fast: false matrix: arch: [x86_64, aarch64] env: VERSION: ${{ needs.version.outputs.version }} 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 # The Craft apps' shared Arch packaging (storytold/craft-common-actions, MIT OR Apache-2.0): # makepkg in an archlinux:base-devel container. The package is named after the tarball's # .desktop file (io.tangencylabs.solidcraft) and written as # dist/release/solidcraft--linux-.pkg.tar.zst. # An Arch version can't hold a hyphen, and the action reads the version from the tarball's name # (a hyphenated one comes out as 0.0.0): 0.40.0-rc.1 goes in as 0.40.0rc.1, which pacman sorts # before 0.40.0, and the package keeps the release's name. - id: pkgver name: Arch version run: | pkgver="${VERSION//-/}" echo "pkgver=$pkgver" >>"$GITHUB_OUTPUT" if [ "$pkgver" != "$VERSION" ]; then cp "dist/linux/solidcraft-$VERSION-linux-$ARCH.tar.gz" "dist/linux/solidcraft-$pkgver-linux-$ARCH.tar.gz" fi env: ARCH: ${{ matrix.arch }} - uses: storytold/craft-common-actions/actions/arch-package@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 # main with: tarball: dist/linux/solidcraft-${{ steps.pkgver.outputs.pkgver }}-linux-${{ matrix.arch }}.tar.gz arch: ${{ matrix.arch }} - name: The package under the release's version if: steps.pkgver.outputs.pkgver != env.VERSION env: ARCH: ${{ matrix.arch }} PKGVER: ${{ steps.pkgver.outputs.pkgver }} run: sudo mv "dist/release/solidcraft-$PKGVER-linux-$ARCH.pkg.tar.zst" "dist/release/solidcraft-$VERSION-linux-$ARCH.pkg.tar.zst" - name: Check the package env: PKG: dist/release/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.pkg.tar.zst run: | tar --zstd -tf "$PKG" | grep -E '^usr/bin/solidcraft$|^usr/bin/solidcraft-cli$|^usr/share/applications/io\.tangencylabs\.solidcraft\.desktop$' tar --zstd -xOf "$PKG" .PKGINFO | grep -E '^(pkgname|pkgver|arch) = ' tar --zstd -xOf "$PKG" .PKGINFO | grep -qx 'pkgname = io.tangencylabs.solidcraft' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-arch-${{ matrix.arch }} path: dist/release/*.pkg.tar.zst if-no-files-found: error snap: name: Snap ${{ matrix.arch }} needs: [version, linux] if: needs.version.outputs.build == 'true' # The Snap Store hasn't accepted SolidCraft yet, so a snap failure never holds up a release. continue-on-error: true # Repackages the Linux job's tarball, like the Flatpak. On 24.04 because the core24 base is # Ubuntu 24.04, which lets snapcraft build on the runner itself (--destructive-mode). 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: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} SOLIDCRAFT_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 snapcraft and the smoke-test tools run: | sudo snap install snapcraft --classic sudo apt-get update sudo apt-get install -y --no-install-recommends xvfb xauth weston dbus-x11 - name: Build the snap run: | sudo --preserve-env=SOLIDCRAFT_VERSION,SOLIDCRAFT_BUILD_DATE env "PATH=$PATH" \ packaging/linux/snap-pack.sh --destructive-mode "dist/linux/solidcraft-$SOLIDCRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz" sudo chown -R "$(id -u):$(id -g)" dist target - name: Install it and start the app on headless X11 and Wayland run: | snap_file="dist/release/solidcraft-$SOLIDCRAFT_VERSION-linux-${{ matrix.arch }}.snap" sudo snap install --dangerous "$snap_file" snap info --verbose "$snap_file" | head -n 12 snap connections solidcraft solidcraft.cli --version # Snaps expect the login session's runtime directory, which runners don't create. sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "/run/user/$(id -u)" python3 packaging/linux/snap-smoke.py --out snap-smoke - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: snap-smoke-${{ matrix.arch }} path: snap-smoke/*.png if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-snap-${{ matrix.arch }} path: dist/release/*.snap if-no-files-found: error freebsd: name: FreeBSD x86_64 needs: version if: needs.version.outputs.build == 'true' # GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM. runs-on: ubuntu-latest timeout-minutes: 180 env: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} SOLIDCRAFT_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: "SOLIDCRAFT_VERSION SOLIDCRAFT_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/solidcraft-target CARGO_BUILD_JOBS=4 bash packaging/freebsd/package.sh tar -tzf dist/release/solidcraft-*-freebsd-x86_64.tar.gz \ | grep -E '/bin/solidcraft$|/bin/solidcraft-cli$|/share/applications/io\.tangencylabs\.solidcraft\.desktop$|/share/doc/solidcraft/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 # The two halves of the universal binary build in parallel on their own runners (each alone # took about half of the old single job's 45 minutes); `macos` below joins and packages them. macos-build: name: macOS build ${{ matrix.arch }} needs: version if: needs.version.outputs.build == 'true' runs-on: macos-15 strategy: fail-fast: false matrix: include: - { arch: aarch64, target: aarch64-apple-darwin } - { arch: x86_64, target: x86_64-apple-darwin } env: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-macos-${{ matrix.arch }} - name: Build run: packaging/macos/package.sh --build-only ${{ matrix.target }} # Not named release-*: the release job collects every release-* artifact as an asset. - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: macos-bin-${{ matrix.arch }} path: | target/${{ matrix.target }}/release/solidcraft target/${{ matrix.target }}/release/solidcraft-cli target/${{ matrix.target }}/release/solvecraft-cli if-no-files-found: error macos: name: macOS universal needs: [version, macos-build] if: needs.version.outputs.build == 'true' runs-on: macos-15 env: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: macos-bin-aarch64 path: target/aarch64-apple-darwin/release - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: macos-bin-x86_64 path: target/x86_64-apple-darwin/release - name: Set up Developer ID signing and notarization # Optional until the APPLE_* secrets exist (docs/releasing.md, "macOS signing"). Without # them (forks, dry runs) the app keeps its ad-hoc signature and is not notarized. env: APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: packaging/macos/signing.sh --setup - name: Join, sign, package and check (dmg, CLI zip; the dmg is mounted and checked) run: | # Artifacts don't keep the executable bit. chmod +x target/*-apple-darwin/release/solidcraft target/*-apple-darwin/release/solidcraft-cli target/*-apple-darwin/release/solvecraft-cli packaging/macos/package.sh --arch universal --skip-build ${MACOS_SIGN_IDENTITY:+--sign} - name: Remove the signing keychain and API key if: always() run: packaging/macos/signing.sh --cleanup - 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] if: needs.version.outputs.build == 'true' # Must stay a GitHub-hosted runner, like every job before it: SignPath's open-source policy # only signs builds whose jobs all ran on GitHub-hosted runners. runs-on: windows-latest # actions: read lets the SignPath action read this job's details and fetch the uploaded # artifact with GITHUB_TOKEN (the workflow default above grants contents: read only). permissions: contents: read actions: read 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: SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }} # Authenticode signing through SignPath Foundation (docs/releasing.md, "Windows signing"). # Only when the API token secret exists: forks and repositories without it build unsigned, # exactly as before. `if:` can't read secrets, so the check goes through this variable. SIGN_WINDOWS: ${{ secrets.SIGNPATH_API_TOKEN != '' }} # The one switch between the self-signed test certificate (test-signing) and the real one # (release-signing): a dispatch input, else the SIGNPATH_SIGNING_POLICY repository variable, # else test-signing. SIGNPATH_SIGNING_POLICY: ${{ inputs.signpath_signing_policy || vars.SIGNPATH_SIGNING_POLICY || 'test-signing' }} SIGNPATH_ORGANIZATION_ID: fa603ff4-c702-404b-8588-a524371c7a4e SIGNPATH_PROJECT_SLUG: solidcraft 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) if: env.SIGN_WINDOWS != 'true' shell: pwsh env: SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} # ---- signed path --------------------------------------------------------------------------- # SignPath's GitHub integration (docs.signpath.io/trusted-build-systems/github): the files go # up as a workflow artifact, SignPath fetches that artifact itself (so it can verify it came # from this workflow run), signs it, and the action downloads the signed copy. GitHub stores # artifacts as zips, so packaging/windows/signpath-artifact-configuration.xml (pasted into the # SignPath project) describes a zip holding the files at its top level. # Two rounds: the .exe files first, so the MSI, the portable zip and the plain .exe all carry # signed binaries; then the MSI built from them. - name: Build and stage the binaries for signing if: env.SIGN_WINDOWS == 'true' shell: pwsh env: SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase stage - name: Upload the unsigned binaries id: unsigned-exes if: env.SIGN_WINDOWS == 'true' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: signpath-unsigned-exes-windows-${{ matrix.arch }} path: target/windows-package/${{ matrix.arch }}/*.exe if-no-files-found: error retention-days: 3 - name: Sign the binaries (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }}) if: env.SIGN_WINDOWS == 'true' uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }} github-artifact-id: ${{ steps.unsigned-exes.outputs.artifact-id }} wait-for-completion: true # release-signing requests wait for a person to approve them in SignPath; give that an # hour instead of the default 10 minutes. test-signing requests finish in seconds. wait-for-completion-timeout-in-seconds: 3600 output-artifact-directory: target/windows-package/signed-exes-${{ matrix.arch }} - name: Put the signed binaries in place if: env.SIGN_WINDOWS == 'true' shell: pwsh run: | $ErrorActionPreference = 'Stop' $stage = "target/windows-package/${{ matrix.arch }}" foreach ($exe in 'solidcraft.exe', 'solidcraft-cli.exe', 'solvecraft-cli.exe') { $signed = "target/windows-package/signed-exes-${{ matrix.arch }}/$exe" # The artifact configuration matches each file optionally, so check that every file # actually came back signed rather than trusting the request's success. $sig = Get-AuthenticodeSignature $signed if (-not $sig.SignerCertificate) { throw "$exe came back from SignPath without a signature ($($sig.Status))" } # The test certificate is self-signed, so Status is not Valid until release-signing. Write-Output "$exe signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))" Copy-Item -Force $signed "$stage/$exe" } - name: Package the signed binaries (msi, portable zip) if: env.SIGN_WINDOWS == 'true' shell: pwsh env: SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase package - name: Upload the unsigned MSI id: unsigned-msi if: env.SIGN_WINDOWS == 'true' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: signpath-unsigned-msi-windows-${{ matrix.arch }} path: dist/release/*.msi if-no-files-found: error retention-days: 3 - name: Sign the MSI (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }}) if: env.SIGN_WINDOWS == 'true' uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }} github-artifact-id: ${{ steps.unsigned-msi.outputs.artifact-id }} wait-for-completion: true wait-for-completion-timeout-in-seconds: 3600 output-artifact-directory: target/windows-package/signed-msi-${{ matrix.arch }} - name: Put the signed MSI in place if: env.SIGN_WINDOWS == 'true' shell: pwsh run: | $ErrorActionPreference = 'Stop' $msi = Get-Item "dist/release/*.msi" $signed = "target/windows-package/signed-msi-${{ matrix.arch }}/$($msi.Name)" $sig = Get-AuthenticodeSignature $signed if (-not $sig.SignerCertificate) { throw "$($msi.Name) came back from SignPath without a signature ($($sig.Status))" } Write-Output "$($msi.Name) signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))" Copy-Item -Force $signed $msi.FullName "Windows ${{ matrix.arch }}: Authenticode-signed through SignPath (policy $env:SIGNPATH_SIGNING_POLICY, $($sig.SignerCertificate.Subject))." >> $env:GITHUB_STEP_SUMMARY - name: Note the unsigned build if: env.SIGN_WINDOWS != 'true' shell: pwsh run: '"Windows ${{ matrix.arch }}: unsigned (no SIGNPATH_API_TOKEN secret)." >> $env:GITHUB_STEP_SUMMARY' - 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 if: needs.version.outputs.build == 'true' runs-on: ubuntu-latest env: SOLIDCRAFT_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: GitHub Release or dry-run artifacts needs: [version, linux, flatpak, arch, snap, freebsd, macos, windows, web] # Not for the release gate (packages_only): it builds and checks the packages, nothing more. if: needs.version.outputs.build == 'true' && !inputs.packages_only 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: Version-less copies for permanent download links # The README links to releases/latest/download/, which must not change between # releases: solidcraft--windows-x64.msi is also uploaded as # solidcraft-windows-x64.msi, and so on for the main installers. working-directory: artifacts run: | shopt -s nullglob for f in "solidcraft-$VERSION-"*; do case "$f" in *.msi | *-portable.zip | *.dmg | *.AppImage | *.deb | *.rpm | *.flatpak | *.snap | *.pkg.tar.zst) cp -- "$f" "solidcraft-${f#"solidcraft-$VERSION-"}" ;; esac done for f in solidcraft-windows-x64.msi solidcraft-macos-universal.dmg \ solidcraft-linux-x86_64.AppImage solidcraft-linux-x86_64.deb solidcraft-linux-x86_64.rpm; do test -s "$f" || { echo "::error::missing version-less copy $f"; exit 1; } done ls -l solidcraft-[!0-9]* - name: Checksums and notes run: | (cd artifacts && sha256sum -- * | tee SHA256SUMS.txt) sed "s/{version}/$VERSION/g" .github/release-notes.md >notes.md - name: Sign the checksum list for in-app updates # Optional until the UPDATE_SIGNING_KEY secret exists (docs/releasing.md, "Update # signing"). The trusted comment names the release, which the app checks. env: UPDATE_SIGNING_KEY: ${{ secrets.UPDATE_SIGNING_KEY }} run: | # The key the app checks against (crates/ui-egui/src/update/install.rs, PUBLIC_KEY). public_key=$(sed -n 's/^pub const PUBLIC_KEY: Option<&str> = Some("\([^"]*\)");/\1/p' crates/ui-egui/src/update/install.rs) if [ -z "$UPDATE_SIGNING_KEY" ]; then if [ -n "$public_key" ]; then echo "::error::the app expects signed updates (PUBLIC_KEY is set) but the UPDATE_SIGNING_KEY secret is missing" exit 1 fi echo "UPDATE_SIGNING_KEY isn't set: SHA256SUMS.txt stays unsigned" exit 0 fi sudo apt-get update -q sudo apt-get install -y --no-install-recommends minisign umask 077 printf '%s\n' "$UPDATE_SIGNING_KEY" >"$RUNNER_TEMP/update.key" minisign -S -s "$RUNNER_TEMP/update.key" -m artifacts/SHA256SUMS.txt \ -x artifacts/SHA256SUMS.txt.minisig -t "SolidCraft v$VERSION" >"$GITHUB_STEP_SUMMARY" - 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 "SolidCraft v$VERSION" \ --target "$GITHUB_SHA" --prerelease="$PRERELEASE" gh release upload "$tag" --clobber -- * else gh release create "$tag" --draft --title "SolidCraft v$VERSION" \ --target "$GITHUB_SHA" --notes-file ../notes.md --generate-notes "${pre[@]}" -- * fi { echo "Draft **SolidCraft v$VERSION** at \`${GITHUB_SHA::9}\`:" echo sed 's/^/ /' SHA256SUMS.txt } >>"$GITHUB_STEP_SUMMARY" # Snap Store: nightlies go to the edge channel and release drafts to candidate, only after # every package built and the GitHub release was staged. Publishing the GitHub release # promotes candidate to stable (snap-store.yml). Dry runs never upload. Skipped until the # owner adds the SNAPCRAFT_STORE_CREDENTIALS secret (docs/releasing.md). - id: snapstore name: Check for Snap Store credentials env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} run: | if [ -n "$SNAPCRAFT_STORE_CREDENTIALS" ]; then echo "configured=true" >>"$GITHUB_OUTPUT" else echo "No SNAPCRAFT_STORE_CREDENTIALS secret: skipping the Snap Store upload." fi - name: Upload the snaps to the Snap Store if: >- steps.snapstore.outputs.configured == 'true' && (github.event_name == 'schedule' || (github.ref == 'refs/heads/release' && (github.event_name == 'push' || inputs.draft))) env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} working-directory: artifacts run: | sudo snap install snapcraft --classic channel=candidate if [ "$GITHUB_EVENT_NAME" = schedule ]; then channel=edge; fi for arch in x86_64 aarch64; do snapcraft upload --release="$channel" "solidcraft-$VERSION-linux-$arch.snap" done echo "Uploaded the snaps to the Snap Store's $channel channel." >>"$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/*