mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
d84d00bb44
The gate passes no secrets to release.yml, so the macOS and Windows signing steps skip as they do on a fork (no signing secret, no SignPath quota), and the new packages_only input skips the release job (checksums, update signing, publishing). The call still grants contents: write, which GitHub checks for the skipped job before the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
762 lines
36 KiB
YAML
762 lines
36 KiB
YAML
# 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
|
|
# 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/<name> 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 <branch> -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
|
|
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
|
shell: bash
|
|
run: scripts/fetch-smlib.sh
|
|
- 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: Fetch SMLib (crates/smlib/SMLIB_REV)
|
|
shell: bash
|
|
run: scripts/fetch-smlib.sh
|
|
- 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
|
|
|
|
|
|
|
|
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/ai\.storyteller\.solvecraft\.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: Fetch SMLib (crates/smlib/SMLIB_REV)
|
|
shell: bash
|
|
run: scripts/fetch-smlib.sh ${{ matrix.target }}
|
|
- 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: Fetch SMLib (crates/smlib/SMLIB_REV)
|
|
shell: bash
|
|
run: scripts/fetch-smlib.sh ${{ matrix.target }}
|
|
- 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: Fetch SMLib (crates/smlib/SMLIB_REV)
|
|
shell: bash
|
|
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
|
- 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, 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/<name>, which must not change between
|
|
# releases: solidcraft-<version>-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)
|
|
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" </dev/null
|
|
rm -f "$RUNNER_TEMP/update.key"
|
|
if [ -n "$public_key" ]; then
|
|
minisign -V -P "$public_key" -m artifacts/SHA256SUMS.txt -x artifacts/SHA256SUMS.txt.minisig
|
|
fi
|
|
- name: Publish nightly pre-release
|
|
if: github.event_name == 'schedule'
|
|
working-directory: artifacts
|
|
run: |
|
|
tag="v$VERSION"
|
|
sha256sum --check SHA256SUMS.txt
|
|
# Stage all packages before publishing. Published tags are never replaced.
|
|
gh release create "$tag" --draft --prerelease --latest=false \
|
|
--title "SolidCraft v$VERSION (nightly)" --target "$GITHUB_SHA" \
|
|
--notes-file ../notes.md --generate-notes -- *
|
|
gh release edit "$tag" --draft=false --prerelease --latest=false
|
|
echo "### Published nightly: $tag (\`${GITHUB_SHA::9}\`)" >>"$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/*
|