mirror of
https://github.com/storytold/craft-common-actions.git
synced 2026-11-15 15:02:24 +00:00
* 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
1294 lines
59 KiB
YAML
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"
|