Files
craft-common-actions/.github/workflows/release.yml
T
FredApps 7d19a7f5e4 release.yml: a publish input publishes a complete draft at the end of the run (#209)
* release.yml: a publish input publishes a complete draft at the end of the run

Part E of the faster-release plan: no one waits by the run to publish. Only on the release
branch, not in a dry run, only when no build job failed (the job already requires that) or was
cancelled, and only when the draft has at least as many assets as the last published release, so
a skipped build job can't publish an incomplete release. Off by default; apps opt in.

* publish: compare with the most recently published release, and say why in the summary

gh release list orders by creation, so the newest-created release was the baseline; an older
draft published later would have been missed. Every refusal now also writes its reason to the
job summary, not only an error annotation.

* tests: publish doesn't change the FreeBSD build, so it stays out of the cache key
2026-11-13 14:09:41 +01:00

1294 lines
59 KiB
YAML

name: "Release"
on:
workflow_call:
inputs:
app-name:
description: "Application repository name (e.g. photocraft, cadcraft)"
type: string
required: true
app-title:
description: "Application display name (e.g. PhotoCraft, CADCraft)"
type: string
required: true
craft-fonts-ref:
# The suite's craft-fonts pin. Bump it here, in freebsd.yml and in gate.yml together.
description: "Git ref for storytold/craft-fonts embedded font assets. Defaults to the suite's pin; override it only for an app that needs another commit. Desktop builds run with CRAFT_FONTS_DIR and CRAFT_FONTS_REQUIRED=1 either way"
type: string
required: false
default: "bfcbf714e45a3ea8e98f88ab2b7971372a576414"
web-craft-fonts:
description: "Build the web release with craft-fonts too (CRAFT_FONTS_DIR); needs craft-fonts-ref. Off by default: each app's build.rs picks which fonts its web build embeds"
type: boolean
required: false
default: false
version:
description: "Version override for a test run (empty: read from Cargo.toml)"
type: string
required: false
default: ""
contributors-artifact:
description: "Optional contributor snapshot artifact from refresh-contributors.yml in this workflow run; installed before every source build"
type: string
required: false
default: ""
has-web:
description: "Whether to build the WebAssembly (wasm) release"
type: boolean
required: false
default: true
has-riscv64:
description: "Whether to build the Linux riscv64 release"
type: boolean
required: false
default: true
has-freebsd:
description: "Whether to build the FreeBSD release"
type: boolean
required: false
default: true
has-arch:
description: "Whether to build the Arch Linux package (.pkg.tar.zst) via makepkg"
type: boolean
required: false
default: true
wix-version:
description: "Version of WiX tool to install"
type: string
required: false
default: "5.0.2"
nfpm-version:
description: "Version of nfpm tool to install"
type: string
required: false
default: "2.47.0"
trunk-version:
description: "Version of trunk tool to install"
type: string
required: false
default: "0.21.14"
freebsd-release:
description: "FreeBSD OS version to run in VM"
type: string
required: false
default: "15.1-STABLE"
freebsd-architectures:
# aarch64 is cross-compiled in the x86_64 VM, so packaging/freebsd/package.sh must build
# with CARGO_BUILD_TARGET and name its archive after it, not after uname -m (#39).
description: 'JSON array of FreeBSD release architectures; packaging/freebsd/package.sh must honor CARGO_BUILD_TARGET for each listed architecture'
type: string
required: false
default: '["x86_64","aarch64"]'
has-server:
description: "Whether to build and release the headless Linux server"
type: boolean
required: false
default: false
publish:
description: >-
Publish the draft when the run is done, instead of leaving it for a maintainer: only on
the release branch, only when no build job failed or was cancelled, and only when the
draft has at least as many assets as the last published release (a skipped job leaves the
run green).
type: boolean
required: false
default: false
dry-run:
description: >-
Build and check without releasing: no signing, no `release` environment, no macOS (its
larger runner is paid for), no Flatpak OCI push and no draft. For a nightly run on main
that finds release-only breakage (riscv64, Flatpak, FreeBSD, packaging) before release day.
type: boolean
required: false
default: false
secrets:
APPLE_CERTIFICATE:
required: false
APPLE_CERTIFICATE_PASSWORD:
required: false
KEYCHAIN_PASSWORD:
required: false
APPLE_ID:
required: false
APPLE_PASSWORD:
required: false
APPLE_TEAM_ID:
required: false
WINDOWS_CERTIFICATE:
required: false
WINDOWS_CERTIFICATE_PASSWORD:
required: false
AZURE_TENANT_ID:
required: false
AZURE_CLIENT_ID:
required: false
AZURE_CLIENT_SECRET:
required: false
AZURE_SIGNING_ENDPOINT:
required: false
AZURE_SIGNING_ACCOUNT:
required: false
AZURE_CERT_PROFILE:
required: false
# ASCII-armored OpenPGP secret key without a passphrase (a signing subkey is enough). It signs
# every Linux package format, the Flatpak bundle's commit and OCI image, and SHA256SUMS.txt.
LINUX_SIGNING_KEY:
required: false
DOCKERHUB_USERNAME:
description: "Username for publishing Flatpak OCI layouts to Docker Hub"
required: false
DOCKERHUB_TOKEN:
description: "Personal access token for publishing Flatpak OCI layouts to Docker Hub"
required: false
outputs:
version:
description: "Resolved release version without a leading v"
value: ${{ jobs.version.outputs.version }}
prerelease:
description: "Whether the resolved version is a prerelease"
value: ${{ jobs.version.outputs.prerelease }}
permissions:
contents: read
jobs:
# Fails fast before any build: git dependencies must come from github.com/storytold, and the
# lock file must not declare source replacement, patches or path overrides that could silently
# swap one. Lives here (not in each app's release.yml) so every craftapp gets the gate at @main.
# Every build job needs `version`, so these builds are gated transitively.
git-sources:
name: Git dependencies come from storytold
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Git dependencies come from storytold
run: |
set -euo pipefail
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
# Source replacement, patches and path overrides in cargo config never show up
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
# Cargo still reads the legacy .cargo/config.
python3 - <<'PY'
import os, sys, tomllib
for path in (".cargo/config", ".cargo/config.toml"):
if not os.path.exists(path):
continue
config = tomllib.load(open(path, "rb"))
replaced = [name for name, source in config.get("source", {}).items()
if isinstance(source, dict) and "replace-with" in source]
if replaced or "patch" in config or "paths" in config:
print(f"{path} replaces, patches or overrides a source")
sys.exit(1)
PY
# Fails when Cargo.lock is out of date with the manifests.
cargo metadata --locked --format-version 1 > /dev/null
python3 - <<'PY'
import sys, tomllib
lock = tomllib.load(open("Cargo.lock", "rb"))
bad = sorted({p["source"] for p in lock.get("package", [])
if p.get("source", "").startswith("git+")
and not p["source"].startswith("git+https://github.com/storytold/")})
if bad:
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
sys.exit(1)
PY
version:
timeout-minutes: 10
name: version
needs: git-sources
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
date: ${{ steps.v.outputs.date }}
prerelease: ${{ steps.v.outputs.prerelease }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: v
name: Resolve version
uses: storytold/craft-common-actions/actions/resolve-version@main
with:
input-version: ${{ inputs.version }}
app-title: ${{ inputs.app-title }}
macos:
timeout-minutes: 90
name: macOS universal
if: ${{ !inputs.dry-run }}
needs: [version]
runs-on: macos-15-xlarge
environment: ${{ !inputs.dry-run && 'release' || '' }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
# A signed job reads only the secrets the caller lists in its `secrets:` block; values come
# from the repo's `release` environment. An unlisted name is empty, and the app's packaging
# would then warn and ship unsigned, so on the release branch fail here instead.
- name: Require signing secrets on the release branch
if: github.ref == 'refs/heads/release'
shell: bash
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
missing=""
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD KEYCHAIN_PASSWORD APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
[ -n "${!name}" ] || missing="$missing $name"
done
if [ -n "$missing" ]; then
echo "::error::macOS release builds must be signed and notarized, but these secrets are empty:$missing. List them in the caller's secrets: block and set them in the repo's release environment."
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- name: Check out craft-fonts
if: inputs.craft-fonts-ref != ''
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.craft-fonts-ref }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
with:
toolchain: stable
targets: aarch64-apple-darwin, x86_64-apple-darwin
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
with:
key: release-macos-universal
- name: Apply version override
if: inputs.version != ''
run: cargo xtask version set "$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:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
run: packaging/macos/package.sh --arch universal
- name: Verify signatures and notarization
if: hashFiles('packaging/macos/verify.sh') != ''
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
for attempt in 1 2 3 4; do
if packaging/macos/verify.sh --arch universal; then
exit 0
fi
if [ "$attempt" -eq 4 ]; then
exit 1
fi
echo "macOS verification failed (attempt $attempt of 4); retrying in 20 seconds" >&2
sleep 20
done
- name: Note missing macOS verification
if: github.ref == 'refs/heads/release' && hashFiles('packaging/macos/verify.sh') == ''
run: echo "::warning::packaging/macos/verify.sh is missing, so this release was not checked for a signature or notarization ticket."
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
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:
timeout-minutes: 90
name: Windows ${{ matrix.arch }}
needs: [version]
# arm64 builds natively on GitHub's Arm runner (free for public repositories). Cross-compiled
# on x64 it was the slowest job of the 11 Nov 2026 releases (30 to 39 minutes).
runs-on: ${{ matrix.runner }}
environment: ${{ !inputs.dry-run && 'release' || '' }}
strategy:
fail-fast: false
matrix:
include:
- { arch: x64, target: x86_64-pc-windows-msvc, runner: windows-latest }
- { arch: arm64, target: aarch64-pc-windows-msvc, runner: windows-11-arm }
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
# See the macOS job: unlisted secrets are empty, and the build would ship unsigned.
# Signing needs either the PFX pair or all six Azure Trusted Signing values.
- name: Require signing secrets on the release branch
if: github.ref == 'refs/heads/release'
shell: bash
env:
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: |
if [ -n "$WINDOWS_CERTIFICATE" ] && [ -n "$WINDOWS_CERTIFICATE_PASSWORD" ]; then exit 0; fi
missing=""
for name in AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_SIGNING_ENDPOINT AZURE_SIGNING_ACCOUNT AZURE_CERT_PROFILE; do
[ -n "${!name}" ] || missing="$missing $name"
done
if [ -n "$missing" ]; then
echo "::error::Windows release builds must be signed, but there is no WINDOWS_CERTIFICATE pair and these Azure Trusted Signing secrets are empty:$missing. List them in the caller's secrets: block and set them in the repo's release environment."
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- name: Check out craft-fonts
if: inputs.craft-fonts-ref != ''
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.craft-fonts-ref }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
with:
toolchain: stable
targets: ${{ matrix.target }}
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
with:
key: release-windows-${{ matrix.arch }}
- name: Apply version override
if: inputs.version != ''
shell: bash
run: cargo xtask version set "$VERSION"
- name: Install WiX
uses: storytold/craft-common-actions/actions/install-wix@main
with:
wix-version: ${{ inputs.wix-version }}
- name: Build, sign, package
shell: pwsh
env:
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 }}
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
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
linux:
timeout-minutes: 90
name: Linux ${{ matrix.arch }}
needs: [version]
# The signing key lives in the release environment, as it does for the macOS, Windows and
# flatpak jobs; without this the Linux packages are built unsigned.
environment: ${{ !inputs.dry-run && 'release' || '' }}
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:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- name: Check out craft-fonts
if: inputs.craft-fonts-ref != ''
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.craft-fonts-ref }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- uses: storytold/craft-common-actions/actions/install-linux-deps@main
with:
deb-arch: ${{ matrix.deb }}
nfpm-version: ${{ inputs.nfpm-version }}
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
with:
toolchain: stable
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
with:
key: release-linux-${{ matrix.arch }}
- name: Apply version override
if: inputs.version != ''
run: cargo xtask version set "$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: Import the Linux signing key
if: github.ref == 'refs/heads/release'
id: linux_signing
uses: storytold/craft-common-actions/actions/import-signing-key@main
with:
key: ${{ secrets.LINUX_SIGNING_KEY }}
- name: Sign the RPM packages
if: steps.linux_signing.outputs.enabled == 'true'
uses: storytold/craft-common-actions/actions/sign-rpms@main
with:
path: dist/release
- name: Check the packages
run: |
dpkg-deb --info dist/release/*.deb || true
dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/|metainfo|applications' || true
for appimage in dist/release/*.AppImage; do
if [ -f "$appimage" ]; then
chmod +x "$appimage"
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version || true
fi
done
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-linux-${{ matrix.arch }}
path: dist/release/*
if-no-files-found: error
flatpak:
timeout-minutes: 90
name: Flatpak ${{ matrix.arch }}
needs: [version, linux]
runs-on: ${{ matrix.runner }}
# LINUX_SIGNING_KEY comes from the repo's release environment, as the macOS secrets do.
environment: ${{ !inputs.dry-run && 'release' || '' }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- { arch: x86_64, runner: ubuntu-24.04 }
- { arch: aarch64, runner: ubuntu-24.04-arm }
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
OCI_SIGNATURES_READY: ${{ vars.LINUX_OCI_SIGNATURES_READY }}
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-linux-${{ matrix.arch }}
path: dist/linux
- name: Configure apt mirrors
uses: storytold/craft-common-actions/actions/configure-apt-mirrors@main
- name: Install flatpak-builder
run: |
sudo apt-get update
# elfutils is only a Recommends of flatpak-builder, but flatpak-builder runs its eu-strip
# on every module it builds with debuginfo, such as the apps' cups-client.
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils librsvg2-common skopeo
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- name: Import the Linux signing key
if: github.ref == 'refs/heads/release'
# flatpak-bundle.sh signs the commit and embeds the public key when SIGNING_FPR is set.
uses: storytold/craft-common-actions/actions/import-signing-key@main
with:
key: ${{ secrets.LINUX_SIGNING_KEY }}
- name: Build the Flatpak bundle
id: flatpak_bundle
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
ARCH: ${{ matrix.arch }}
run: |
shopt -s nullglob
# The app's own tarball: <file-prefix>-<version>-linux-<arch>.tar.gz. The file prefix can
# differ from app-name (craft-launcher ships artcraft-launcher-*), and CLI or server
# tarballs (pdfcraft-cli-*, figcraft-server-*) sit beside it.
tarballs=()
for f in dist/linux/*-"${RELEASE_VERSION}"-linux-"${ARCH}".tar.gz; do
case "${f##*/}" in *-cli-*|*-server-*) ;; *) tarballs+=("$f") ;; esac
done
if [ "${#tarballs[@]}" -ne 1 ]; then
echo "::error::expected one app tarball for ${RELEASE_VERSION} in release-linux-${ARCH}, found ${#tarballs[@]}: ${tarballs[*]}"
exit 1
fi
tarball="${tarballs[0]}"
packaging/linux/flatpak-bundle.sh "$tarball"
printf 'tarball=%s\n' "$tarball" >> "$GITHUB_OUTPUT"
- name: Publish Flatpak OCI layout to GHCR and Docker Hub
if: github.event_name != 'pull_request' && !inputs.dry-run
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
ARCH: ${{ matrix.arch }}
REGISTRY: ghcr.io/${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
TARBALL: ${{ steps.flatpak_bundle.outputs.tarball }}
run: |
if [ -x packaging/linux/flatpak-oci.sh ]; then
REGISTRY="${REGISTRY,,}"
creds="${GITHUB_ACTOR:-storytold}:$GITHUB_TOKEN"
# The app's script only builds the layout; the tags are pushed here. Older copies of
# flatpak-oci.sh move stable-<arch> on every --push, so a prerelease or a test run
# from any branch would become what stable-<arch> clients install.
packaging/linux/flatpak-oci.sh "$TARBALL"
base="$(basename "$TARBALL" .tar.gz)"
layout="dist/release/$base-oci"
if [ ! -d "$layout" ]; then
echo "::error::packaging/linux/flatpak-oci.sh did not write the OCI layout $layout"
exit 1
fi
echo "==> Pushing to $REGISTRY:$RELEASE_VERSION-$ARCH"
skopeo copy --format oci --dest-creds "$creds" "oci:$layout" "docker://$REGISTRY:$RELEASE_VERSION-$ARCH"
# Only a stable version built from the release branch may move the stable tag.
if [[ "$RELEASE_VERSION" != *-* && "$GITHUB_REF" == refs/heads/release ]]; then
echo "==> Pushing to $REGISTRY:stable-$ARCH"
skopeo copy --format oci --dest-creds "$creds" "oci:$layout" "docker://$REGISTRY:stable-$ARCH"
else
echo "Leaving $REGISTRY:stable-$ARCH unchanged: $RELEASE_VERSION from $GITHUB_REF is not a stable release-branch build"
fi
# The pushed manifest, byte for byte: the release job signs its digest for Flatpak's
# signature lookaside, and the release keeps it so the digest can be checked.
skopeo inspect --raw --creds "$creds" \
"docker://$REGISTRY:$RELEASE_VERSION-$ARCH" >"dist/release/$base.oci-manifest.json"
if [ -n "${DOCKERHUB_USERNAME:-}" ] && [ -n "${DOCKERHUB_TOKEN:-}" ]; then
dh_registry="docker.io/storytold/${REGISTRY##*/}"
dh_creds="$DOCKERHUB_USERNAME:$DOCKERHUB_TOKEN"
echo "==> Pushing to $dh_registry:$RELEASE_VERSION-$ARCH"
skopeo copy --format oci --dest-creds "$dh_creds" "oci:$layout" "docker://$dh_registry:$RELEASE_VERSION-$ARCH"
if [[ "$RELEASE_VERSION" != *-* && "$GITHUB_REF" == refs/heads/release ]]; then
echo "==> Pushing to $dh_registry:stable-$ARCH"
skopeo copy --format oci --dest-creds "$dh_creds" "oci:$layout" "docker://$dh_registry:stable-$ARCH"
else
echo "Leaving $dh_registry:stable-$ARCH unchanged: $RELEASE_VERSION from $GITHUB_REF is not a stable release-branch build"
fi
fi
fi
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-flatpak-${{ matrix.arch }}
path: |
dist/release/*.flatpak
dist/release/*.oci-manifest.json
if-no-files-found: error
arch:
name: Arch Linux ${{ matrix.arch }}
if: inputs.has-arch
needs: [version, linux]
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-linux-${{ matrix.arch }}
path: dist/linux
- name: Find the release tarball
id: tarball
env:
RELEASE_VERSION: ${{ needs.version.outputs.version }}
ARCH: ${{ matrix.arch }}
run: |
shopt -s nullglob
# The app's own tarball: <file-prefix>-<version>-linux-<arch>.tar.gz. The file prefix can
# differ from app-name (craft-launcher ships artcraft-launcher-*), and CLI or server
# tarballs (pdfcraft-cli-*, figcraft-server-*) sit beside it.
tarballs=()
for f in dist/linux/*-"${RELEASE_VERSION}"-linux-"${ARCH}".tar.gz; do
case "${f##*/}" in *-cli-*|*-server-*) ;; *) tarballs+=("$f") ;; esac
done
if [ "${#tarballs[@]}" -ne 1 ]; then
echo "::error::expected one app tarball for ${RELEASE_VERSION} in release-linux-${ARCH}, found ${#tarballs[@]}: ${tarballs[*]}"
exit 1
fi
tarball="${tarballs[0]}"
echo "path=$tarball" >>"$GITHUB_OUTPUT"
- uses: storytold/craft-common-actions/actions/arch-package@main
with:
tarball: ${{ steps.tarball.outputs.path }}
arch: ${{ matrix.arch }}
- 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
freebsd:
name: FreeBSD ${{ matrix.arch }}
if: inputs.has-freebsd
needs: [version]
runs-on: ubuntu-latest
timeout-minutes: 300
strategy:
fail-fast: false
matrix:
# aarch64 is in the default freebsd-architectures; callers can narrow it (#39).
arch: ${{ fromJSON(inputs.freebsd-architectures) }}
env:
APP_NAME: ${{ inputs.app-name }}
CARGO_HOME: /var/tmp/craft-cargo
CARGO_TARGET_DIR: /var/tmp/craft-target
CARGO_BUILD_JOBS: 4
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- name: Check the FreeBSD architecture
env:
ARCH: ${{ matrix.arch }}
run: |
# The guest is always x86_64 and only aarch64 is cross-compiled: any other value
# would build x86_64 binaries and package them under its own name.
case "$ARCH" in
x86_64 | aarch64) ;;
*)
echo "::error::unsupported FreeBSD architecture '$ARCH' in freebsd-architectures; use x86_64 or aarch64"
exit 1
;;
esac
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- name: Check out craft-fonts
if: inputs.craft-fonts-ref != ''
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.craft-fonts-ref }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- name: Start the FreeBSD VM
uses: step-security/freebsd-vm@a2f5754275f7b0d5cea7f2762c9960727950f128 # v1.5.9
with:
release: ${{ inputs.freebsd-release == '15.1-STABLE' && '15.1' || inputs.freebsd-release }}
# Always an x86_64 guest, which KVM accelerates: aarch64 is cross-compiled in it,
# because an emulated aarch64 guest takes over 5 hours for most apps (#39).
usesh: true
cpu: 4
mem: 12288
copyback: false
envs: "APP_NAME CARGO_HOME CARGO_TARGET_DIR CARGO_INCREMENTAL CARGO_BUILD_JOBS VERSION BUILD_DATE BUILD_SHA INPUT_VERSION ARCH ${{ steps.app-env.outputs.names }}"
prepare: |
# Keep rustup/proxies outside the Cargo data cache.
unset CARGO_HOME CARGO_TARGET_DIR
if [ "${{ matrix.arch }}" = aarch64 ]; then
# The host compiler and the aarch64 standard library come from the amd64 and
# aarch64 lang/rust packages, which must be the same build. "latest" can build
# them days apart; the quarterly branch keeps them in step.
mkdir -p /usr/local/etc/pkg/repos
printf 'FreeBSD-ports: { url: "https://pkg.FreeBSD.org/${ABI}/quarterly" }\n' > /usr/local/etc/pkg/repos/FreeBSD.conf
pkg update -f -q
fi
pkg install -y bash git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib
# aarch64-unknown-freebsd is a tier-3 Rust target: rustup publishes no toolchain
# or standard library for it ("installer for platform not found"), so use
# FreeBSD's lang/rust package.
if [ "${{ matrix.arch }}" = aarch64 ]; then
pkg install -y rust
else
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
fi
# Keep this step identical to the one in freebsd.yml; tests/test_freebsd.py checks it.
- name: Set up the aarch64 cross toolchain
if: matrix.arch == 'aarch64'
shell: freebsd {0}
run: |
set -eu
# Stable Rust, no -Zbuild-std: the aarch64 standard library is copied from FreeBSD's
# aarch64 lang/rust package, and C code links with base clang and lld against an
# arm64 sysroot (base.txz plus the libraries the prepare step installs).
target=aarch64-unknown-freebsd
X=/var/tmp/craft-cross
S=$X/sysroot
mkdir -p "$X/db" "$X/cache" "$S"
release=$(freebsd-version -u | sed 's/-p[0-9]*$//')
P="-o ABI=FreeBSD:${release%%.*}:aarch64 -o OSVERSION=$(uname -U) -o PKG_DBDIR=$X/db -o PKG_CACHEDIR=$X/cache -o IGNORE_OSVERSION=yes"
pkg $P update -q
host=$(pkg query %v rust)
cross=$(pkg $P rquery %v rust)
if [ "$host" != "$cross" ]; then
echo "error: lang/rust is $host for amd64 but $cross for aarch64; the standard library must match the compiler" >&2
exit 1
fi
pkg $P fetch -y -q rust
# pkg links each name-version.pkg to its hashed file.
tar -xf "$X/cache/rust-$cross.pkg" -C "$X" --include "*usr/local/lib/rustlib/$target/*"
cp -R "$X/usr/local/lib/rustlib/$target" /usr/local/lib/rustlib/
base=https://download.freebsd.org/releases/arm64/aarch64/$release
fetch -q -o "$X/MANIFEST" "$base/MANIFEST"
fetch -q -o "$X/base.txz" "$base/base.txz"
if [ "$(awk '$1 == "base.txz" { print $2 }' "$X/MANIFEST")" != "$(sha256 -q "$X/base.txz")" ]; then
echo "error: arm64 base.txz does not match its MANIFEST" >&2
exit 1
fi
tar -xf "$X/base.txz" -C "$S" ./lib ./usr/lib ./usr/include ./usr/libdata
# The libraries the prepare step installs; the build tools and fonts stay host-only.
pkg $P fetch -y -q -d libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib
for p in "$X"/cache/*.pkg; do
[ ! -L "$p" ] || continue
case "$p" in */rust-*) continue ;; esac
tar -xf "$p" -C "$S" --exclude '+*'
done
for tool in cc c++; do
printf '#!/bin/sh\nexec %s --target=%s%s --sysroot=%s -fuse-ld=lld -I%s/usr/local/include -L%s/usr/local/lib -Wl,-rpath-link,%s/usr/local/lib "$@"\n' \
"$tool" "$target" "${release%%-*}" "$S" "$S" "$S" "$S" > "$X/$tool"
chmod +x "$X/$tool"
done
cat > "$X/env" <<EOF
export CARGO_BUILD_TARGET=$target
export CARGO_TARGET_AARCH64_UNKNOWN_FREEBSD_LINKER=$X/cc
export CC_aarch64_unknown_freebsd=$X/cc CXX_aarch64_unknown_freebsd=$X/c++ AR_aarch64_unknown_freebsd=/usr/bin/ar
export PKG_CONFIG_ALLOW_CROSS=1 PKG_CONFIG_SYSROOT_DIR=$S PKG_CONFIG_LIBDIR=$S/usr/local/libdata/pkgconfig:$S/usr/libdata/pkgconfig
EOF
rustc --print target-libdir --target "$target"
- name: Restore FreeBSD release Cargo cache
id: cargo-cache
continue-on-error: true
timeout-minutes: 10
uses: storytold/craft-common-actions/actions/freebsd-cargo-cache@main
with:
profile: release
# Only what changes the FreeBSD build. Not toJSON(inputs): contributors-artifact names
# this run (contributor-snapshot-<run id>-<attempt>), so no later run could restore the
# entry, and dry-run must share it, so that a dry run warms the release's cache.
configuration: ${{ format('{{"app-name":{0},"arch":{1},"craft-fonts-ref":{2},"freebsd-release":{3}}}', toJSON(inputs.app-name), toJSON(matrix.arch), toJSON(inputs.craft-fonts-ref), toJSON(inputs.freebsd-release)) }}
target: ${{ matrix.arch == 'aarch64' && 'aarch64-unknown-freebsd' || '' }}
source-hash: ${{ hashFiles('**/Cargo.lock', '**/Cargo.toml', '**/build.rs', '.cargo/**', 'rust-toolchain*', 'packaging/**', '.github/workflows/release.yml') }}
- name: Build and package
shell: freebsd {0}
env:
ARCH: ${{ matrix.arch }}
run: |
set -eu
# Only rustup writes this; the lang/rust package installs into /usr/local/bin.
[ ! -f "$HOME/.cargo/env" ] || . "$HOME/.cargo/env"
# A failed restore may leave partial files. Rebuild from an empty cache.
if [ "${{ steps.cargo-cache.outcome }}" = failure ]; then
rm -rf /var/tmp/craft-target /var/tmp/craft-cargo/registry /var/tmp/craft-cargo/git
fi
git config --global --add safe.directory "*"
# Before the cross environment: xtask has to run on the guest.
if [ -n "$INPUT_VERSION" ]; then cargo xtask version set "$VERSION" || cargo run -q -p xtask -- version set "$VERSION"; fi
# Written by "Set up the aarch64 cross toolchain": cargo then builds for aarch64.
[ ! -f /var/tmp/craft-cross/env ] || . /var/tmp/craft-cross/env
# Cargo puts a cross build's binaries under target/<triple>/release.
rel="${CARGO_BUILD_TARGET:+$CARGO_BUILD_TARGET/}release"
export CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1
if [ -f craft-i18n.rev ]; then export CRAFT_I18N_DIR="$PWD/craft-i18n" CRAFT_I18N_REQUIRED=1; fi
if [ -f packaging/freebsd/package.sh ]; then
bash packaging/freebsd/package.sh
else
set -- -p "$APP_NAME"
if cargo metadata --format-version 1 --no-deps | python3 -c 'import json, sys; sys.exit(not any(p["name"] == sys.argv[1] for p in json.load(sys.stdin)["packages"]))' "$APP_NAME-cli"; then
set -- "$@" -p "$APP_NAME-cli"
fi
cargo build --release --locked "$@"
name="$APP_NAME-$VERSION-freebsd-$ARCH"
mkdir -p "dist/release/$name"
cp "target/$rel/$APP_NAME" "dist/release/$name/" 2>/dev/null || cp "/var/tmp/craft-target/$rel/$APP_NAME" "dist/release/$name/"
if [ -f "target/$rel/$APP_NAME-cli" ]; then
cp "target/$rel/$APP_NAME-cli" "dist/release/$name/"
elif [ -f "/var/tmp/craft-target/$rel/$APP_NAME-cli" ]; then
cp "/var/tmp/craft-target/$rel/$APP_NAME-cli" "dist/release/$name/"
fi
cp README.md LICENSE-MIT LICENSE-APACHE NOTICE ATTRIBUTION.md "dist/release/$name/" 2>/dev/null || true
if [ -f "packaging/linux/ai.storyteller.$APP_NAME.desktop" ]; then
cp "packaging/linux/ai.storyteller.$APP_NAME.desktop" "dist/release/$name/"
fi
mkdir -p "dist/release/$name/icons"
cp -R assets/app-icon/hicolor "dist/release/$name/icons/" 2>/dev/null || true
tar -C dist/release -czf "dist/release/$name.tar.gz" "$name"
rm -rf "dist/release/$name"
fi
# The guest is x86_64 either way: a package.sh that names its archive after uname -m
# instead of CARGO_BUILD_TARGET would ship an x86_64-labelled aarch64 build, or worse.
found=
for archive in dist/release/*-freebsd-*.tar.gz; do
[ -f "$archive" ] || continue
case "$archive" in
*-freebsd-$ARCH.tar.gz) found=1 ;;
*)
echo "error: $archive is not a FreeBSD $ARCH archive; packaging/freebsd/package.sh must honor CARGO_BUILD_TARGET" >&2
exit 1
;;
esac
done
if [ -z "$found" ]; then
echo "error: no FreeBSD $ARCH archive was produced" >&2
exit 1
fi
- name: Copy release archives from the VM
run: |
mkdir -p dist/release
rsync -a --timeout=1800 --rsync-path='env PATH=/usr/local/bin:/usr/bin:/bin rsync' \
-e 'ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=60 -o ConnectTimeout=120' \
"freebsd:$GITHUB_WORKSPACE/dist/release/" dist/release/
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-freebsd-${{ matrix.arch }}
path: dist/release/*
if-no-files-found: error
- name: Save FreeBSD release Cargo cache
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' && steps.cargo-cache.outputs.key != '' && steps.cargo-cache.outputs.cache-hit != 'true'
continue-on-error: true
timeout-minutes: 10
uses: storytold/craft-common-actions/actions/freebsd-cargo-cache@main
with:
phase: save
profile: release
# Only what changes the FreeBSD build. Not toJSON(inputs): contributors-artifact names
# this run (contributor-snapshot-<run id>-<attempt>), so no later run could restore the
# entry, and dry-run must share it, so that a dry run warms the release's cache.
configuration: ${{ format('{{"app-name":{0},"arch":{1},"craft-fonts-ref":{2},"freebsd-release":{3}}}', toJSON(inputs.app-name), toJSON(matrix.arch), toJSON(inputs.craft-fonts-ref), toJSON(inputs.freebsd-release)) }}
target: ${{ matrix.arch == 'aarch64' && 'aarch64-unknown-freebsd' || '' }}
source-hash: ${{ hashFiles('**/Cargo.lock', '**/Cargo.toml', '**/build.rs', '.cargo/**', 'rust-toolchain*', 'packaging/**', '.github/workflows/release.yml') }}
key: ${{ steps.cargo-cache.outputs.key }}
web:
timeout-minutes: 90
name: Web (wasm)
if: inputs.has-web
needs: [version]
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
with:
toolchain: stable
targets: wasm32-unknown-unknown
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
with:
key: release-web
- uses: storytold/craft-common-actions/actions/install-trunk@main
with:
trunk-version: ${{ inputs.trunk-version }}
- name: Check out craft-fonts
if: inputs.web-craft-fonts && inputs.craft-fonts-ref != ''
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.craft-fonts-ref }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- name: Apply version override
if: inputs.version != ''
run: cargo xtask version set "$VERSION"
- name: Build and zip
if: ${{ !(inputs.web-craft-fonts && inputs.craft-fonts-ref != '') }}
run: packaging/web/package.sh
- name: Build and zip (with craft-fonts)
if: inputs.web-craft-fonts && inputs.craft-fonts-ref != ''
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
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
linux-riscv64:
timeout-minutes: 90
name: Linux riscv64
if: inputs.has-riscv64
needs: [version]
environment: ${{ !inputs.dry-run && 'release' || '' }}
# Not 22.04 like the other Linux jobs: Ubuntu never built jammy's ncurses security update for
# riscv64, so the amd64 and riscv64 libtinfo6 can't match and the cross deps don't install (#44).
runs-on: ubuntu-24.04
env:
# Built on 24.04, the binaries need GLIBC_2.39. The deb and rpm declare it only where the
# app's nfpm.yaml reads ${GLIBC_FLOOR}; an app still hard-coding 2.35 keeps declaring 2.35.
GLIBC_FLOOR: "2.39"
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- name: Check out craft-fonts
if: inputs.craft-fonts-ref != ''
uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main
with:
ref: ${{ inputs.craft-fonts-ref }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
with:
toolchain: stable
targets: riscv64gc-unknown-linux-gnu
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
with:
key: release-linux-riscv64
- name: Apply version override
if: inputs.version != ''
run: cargo xtask version set "$VERSION"
- uses: storytold/craft-common-actions/actions/install-riscv64-cross@main
- name: Install nfpm (SHA256-verified)
uses: storytold/craft-common-actions/actions/install-nfpm@main
with:
nfpm-version: ${{ inputs.nfpm-version }}
deb-arch: amd64
- name: Build and package (deb, rpm, tar.gz)
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
PKG_CONFIG_SYSROOT_DIR: /
PKG_CONFIG_PATH: /usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
PKG_CONFIG_ALLOW_CROSS: "1"
CROSS_ARCH: riscv64
CROSS_TARGET: riscv64gc-unknown-linux-gnu
CROSS_COMPILE: riscv64-linux-gnu-
EMULATOR: qemu-riscv64
QEMU_LD_PREFIX: /usr/riscv64-linux-gnu
run: packaging/linux/package.sh --formats "deb rpm tar"
- name: Import the Linux signing key
if: github.ref == 'refs/heads/release'
id: linux_signing
uses: storytold/craft-common-actions/actions/import-signing-key@main
with:
key: ${{ secrets.LINUX_SIGNING_KEY }}
- name: Sign the RPM packages
if: steps.linux_signing.outputs.enabled == 'true'
uses: storytold/craft-common-actions/actions/sign-rpms@main
with:
path: dist/release
- name: Check the packages
run: |
dpkg-deb --info dist/release/*-linux-riscv64.deb 2>/dev/null || true
dpkg-deb --contents dist/release/*-linux-riscv64.deb 2>/dev/null | grep -E 'usr/bin/|metainfo|applications' || true
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-linux-riscv64
path: dist/release/*
if-no-files-found: error
server:
timeout-minutes: 90
name: Server Linux ${{ matrix.arch }}
if: inputs.has-server
needs: [version]
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { arch: x86_64, runner: ubuntu-22.04 }
- { arch: aarch64, runner: ubuntu-22.04-arm }
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
VERSION: ${{ needs.version.outputs.version }}
BUILD_DATE: ${{ needs.version.outputs.date }}
BUILD_SHA: ${{ github.sha }}
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load refreshed contributor credits
if: inputs.contributors-artifact != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.contributors-artifact }}
path: contributors
- name: Export the app's build variables
id: app-env
# <PREFIX>_VERSION, _BUILD_DATE and _BUILD_SHA, with the prefix the app's packaging/env.sh
# reads, so this workflow lists no apps and a new app needs no change here.
uses: storytold/craft-common-actions/actions/export-app-build-env@main
with:
app-name: ${{ inputs.app-name }}
version: ${{ needs.version.outputs.version }}
date: ${{ needs.version.outputs.date }}
- name: Check out craft-i18n
uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
with:
toolchain: stable
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
with:
key: release-server-${{ matrix.arch }}
- name: Apply version override
if: inputs.version != ''
run: cargo xtask version set "$VERSION" || cargo run --locked -q -p xtask -- version set "$VERSION" || true
- name: Build and package (tar.gz)
run: packaging/server/package.sh
- name: Check the archive
shell: bash
env:
APP_NAME: ${{ inputs.app-name }}
run: |
tar -tzf "dist/release/$APP_NAME-server-$VERSION-linux-${{ matrix.arch }}.tar.gz" || true
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-server-linux-${{ matrix.arch }}
path: dist/release/*.tar.gz
if-no-files-found: error
release:
timeout-minutes: 10
name: Draft GitHub Release
if: "!inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure')"
needs: [version, macos, windows, linux, linux-riscv64, flatpak, arch, freebsd, web, server]
runs-on: ubuntu-24.04
environment: ${{ !inputs.dry-run && 'release' || '' }}
permissions:
contents: write
steps:
- name: Harden runner
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
with:
egress-policy: audit
- uses: storytold/craft-common-actions/actions/draft-release@main
with:
version: ${{ needs.version.outputs.version }}
app-title: ${{ inputs.app-title }}
prerelease: ${{ needs.version.outputs.prerelease }}
github-token: ${{ secrets.GITHUB_TOKEN }}
signing-key: ${{ github.ref == 'refs/heads/release' && secrets.LINUX_SIGNING_KEY || '' }}
- name: Publish the release
if: inputs.publish && !inputs.dry-run && github.ref == 'refs/heads/release'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: v${{ needs.version.outputs.version }}
CANCELLED: ${{ contains(needs.*.result, 'cancelled') }}
run: |
set -euo pipefail
refuse() {
echo "::error::$1"
echo "Not published: $1" >> "$GITHUB_STEP_SUMMARY"
exit 1
}
if [ "$CANCELLED" != false ]; then
refuse "a build job was cancelled: $TAG stays a draft"
fi
now="$(gh release view "$TAG" --json assets --jq '.assets | length')"
# The most recently published release; gh lists by creation, not publication.
last="$(gh release list --exclude-drafts --exclude-pre-releases --limit 50 --json tagName,publishedAt --jq 'sort_by(.publishedAt) | last | .tagName // ""')"
if [ -n "$last" ] && [ "$last" != "$TAG" ]; then
before="$(gh release view "$last" --json assets --jq '.assets | length')"
if [ "$now" -lt "$before" ]; then
refuse "$TAG has $now assets where $last had $before: it stays a draft"
fi
fi
gh release edit "$TAG" --draft=false
echo "Published $TAG ($now assets)." >> "$GITHUB_STEP_SUMMARY"