The amd64 security source added in #61 used http://security.ubuntu.com,
reintroducing the clear-text apt source that #26 removed for
ports.ubuntu.com. security.ubuntu.com serves the same archive over HTTPS.
GitHub's Ubuntu mirror is sometimes slow: a FilmCraft CI run spent 5 min 42 s downloading
21 MB of build dependencies (storytold/filmcraft#2860), and FigCraft's apt step took 4 to 9
minutes in two of eight runs (craft-common-actions#60). The action points apt's archive
directory at a cached folder, so a run with a warm cache downloads only the package lists.
This reverts commit b86cdd3. Wayland drag-and-drop waits for winit 0.31,
and Flatpak portal drops will be resolved with zbus (already in every
app via rfd) rather than libdbus. Records the decision as ADR 0001;
tracking in #64.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(release): give riscv64 packages the glibc floor of their runner
The riscv64 job builds on ubuntu-24.04, so its GUI binaries need GLIBC_2.39,
but the apps' nfpm.yaml declares the 2.35 floor of the other Linux jobs.
Ubuntu 22.04 can't host the cross build: jammy's ncurses security update was
never built for riscv64, so libtinfo6 can't match across architectures.
Export GLIBC_FLOOR=2.39 on the riscv64 job for each app's package.sh and
nfpm.yaml to declare. Refs #44.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(release): say GLIBC_FLOOR reaches the packages only through the app's nfpm.yaml
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ubuntu published a zlib security update (1:1.3.dfsg-3.1ubuntu2.3) at 18:50
UTC on 2026-10-22. ports.ubuntu.com had the riscv64 build at once, but the
runner's amd64 mirror (azure.archive.ubuntu.com) still served 2.2. zlib1g is
Multi-Arch: same, so apt could not pair the versions and every release's
Linux riscv64 job failed with "held broken packages".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The identity step hashed the caller's source hash into the cache prefix and
restore-keys only matched that prefix, so any revision that touched Cargo.toml,
Cargo.lock, build.rs, .cargo, rust-toolchain or packaging/ started from an
empty cache. Releases bump a version in the manifests, and PRs that change a
dependency do too. Issue #33 records the consequence on the aarch64 leg:
compiling ~260 dependency crates takes 4h32m under emulation, and 93 PR runs
were cancelled at the 300-minute limit with 0 passing.
Keep the exact key as the primary match and add a second restore key that pins
only what decides artifact compatibility: the guest toolchain and packages, the
serialized configuration and the profile. Cargo's fingerprints still reject
stale artifacts, so restoring a previous revision only saves work.
Bump the cache namespace to v3 because the key layout changed. Existing v2
entries are not readable through the new prefixes, so the first run after this
merge starts cold, as for any namespace bump.
Refs #33, #39
* ci(release): cross-compile FreeBSD aarch64 in the x86_64 VM
The FreeBSD release job built aarch64 in an emulated aarch64 guest, which
ran into the 300-minute limit for most apps, so aarch64 was dropped from
releases (#39). #49 fixed the same problem in freebsd.yml by cross-compiling
in the KVM-accelerated x86_64 guest; this ports that setup to release.yml.
The release VM is now always x86_64. For aarch64 it pins the quarterly
package branch, installs lang/rust and runs the same "Set up the aarch64
cross toolchain" step as freebsd.yml; a test keeps the two copies identical.
The build step sets the version before loading the cross environment, so
xtask still builds for the guest. The built-in packager reads binaries from
target/<triple>/release, and the job fails if an archive does not match the
matrix architecture.
aarch64 stays opt-in through the new freebsd-architectures input (default
'["x86_64"]'): each app's package.sh still reads target/release and names
its archive after uname -m, so it must honor CARGO_BUILD_TARGET first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(release): reject unsupported FreeBSD architectures before the VM starts
The guest is always x86_64 and only aarch64 is cross-compiled, so any other
freebsd-architectures value would build x86_64 binaries and the fallback
packager would label them with that value. Check matrix.arch on the host
first and fail for anything but x86_64 and aarch64.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An app's craft-i18n.rev switches craft-i18n on. checkout-craft-i18n reads
it (trimmed: Windows checks it out with CRLF), checks out that SHA and sets
CRAFT_I18N_DIR and CRAFT_I18N_REQUIRED=1 for every build job in release.yml,
freebsd.yml and windows-arm64.yml; the FreeBSD guests export them too.
The i18n-gate workflow runs cargo test --workspace i18n_gate with
CRAFT_I18N_GATE=1 and fails if no test ran. Release packaging needs it,
skip-i18n-gate skips it on workflow_dispatch only, and freebsd.yml runs it
on pull requests that touch craft-i18n.rev. Without the pin file every
step is a no-op and the gate succeeds.
Claude-Session: https://claude.ai/code/session_01B5qjdPbdcyPMikAqXxAAdW
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The VM's shell is root, and root is never refused a file: a test that
locks a file and expects "permission denied" fails, and a permission bug
root would hide goes unseen. Default and custom tests now run as
craft-test through su; the build and packaging stay root.
Human-reviewed, AI-assisted, nobody's mailbox harmed.
* security(actions): stop evaluating tool-version inputs as shell/pwsh source
The install-wix, install-nfpm, install-trunk, and packaging-lint composite
actions interpolated their *-version (and deb-arch) inputs directly into
Bash/PowerShell source when constructing download URLs. A command substitution
or subexpression in those values executed on the runner before any checksum
verification (CWE-78).
install-trunk additionally placed the version into a URL path component, so
path-traversal segments could point both the archive and its fallback checksum
at an attacker-controlled repository (CWE-23).
Fixes:
- Pass every version/arch input through the step `env:` and read it with quoted
variable expansions instead of GitHub `${{ }}` interpolation in script bodies.
- Validate a strict version grammar (and an allow-list for deb-arch) before the
value is used, which also excludes slashes and other URL path-traversal
characters so the download publisher stays fixed.
No change to default versions, pinned checksums, or install behavior.
* security(actions): anchor version validation against the whole value
Address review feedback:
- The bash version checks used `printf %s | grep -Eq '^...$'`, which matches
line-by-line. A value like `2.49.0\ninvalid` passed and then flowed into the
download URL. Switch to a bash `[[ =~ ^...$ ]]` test, which anchors the entire
string and rejects any value containing a newline. Applied to install-nfpm,
install-trunk, and packaging-lint.
- The install-wix PowerShell check used `$`, which in .NET also matches before a
trailing newline. Use `\z` (absolute end of input) so a trailing-newline value
cannot bypass validation.
* security(arch-package): close symlink-flatten, su -c, and PKGBUILD injection
Three issues in the Arch packaging action, all reachable from the public
tarball input:
- arch-package.sh flatten step (CWE-61): the single-root-directory check used
`-d`, which follows symlinks. An archive could ship a symlink whose target is
an external directory; the subsequent `mv` moved external files into disposable
staging, and the EXIT trap then deleted them. The wrapper is now rejected when
it is a symlink, and its resolved path must stay inside staging before any
child is moved.
- arch-package.sh PKGBUILD (CWE-78): pkgname (from an archive-controlled
.desktop basename) and the app name were written into PKGBUILD unescaped, and
makepkg evaluates PKGBUILD as Bash. Both values are now validated against safe
identifier grammars and serialized with `printf %q`, matching the existing
handling of pkgdesc/url.
- docker-entrypoint.sh (CWE-78): input values were interpolated into a
single-quoted program passed to `su - builder -c`, so an embedded quote in a
value (e.g. the tarball path) could run commands as builder. The user switch
now uses a fixed argument vector via `runuser`/`su` with values passed through
`env` as data; HOME is set explicitly to the builder's home.
Verified with `bash -n` and shellcheck (clean), plus functional checks that the
validators reject injection payloads and accept legitimate names.
* security(arch-package): harden symlink/pkgname handling per review
Address review feedback:
- A sole symlinked wrapper entry skipped the flatten branch entirely and was
left in staging, where package() could copy it and later directory creation
could follow it outside $pkgdir. Reject a symlinked wrapper outright before
the flatten logic.
- The pkgname validator rejected uppercase letters, which broke legitimate
freedesktop IDs such as org.gnome.Nautilus (and the ai.storyteller.$app
fallback when $app had uppercase). makepkg accepts mixed case; uppercase does
not weaken the injection fix (the character set still excludes shell
metacharacters and printf %q escapes the value). Allow A-Z.
- docker-entrypoint referenced optional inputs bare under `set -u`, which could
fail if OUTPUT_DIR (or another optional) was unset. Resolve all inputs to
defaults once and pass the resolved values to the builder.
draft-release and resolve-version interpolated caller-provided metadata directly
into Bash source (CWE-78):
- draft-release embedded app-title into the `title=` assignment in the step that
has GH_TOKEN available.
- resolve-version embedded input-version into a `v=` assignment before it was
validated, and app-title into the job-summary `echo`.
Both inputs now travel through the step `env:` and are read with quoted variable
expansions; the version is validated before it is used to produce outputs, and
the summary is written with a fixed `printf` format. No change to outputs or
release behavior.
The aarch64 leg booted an emulated aarch64 guest, which compiles about 23
times slower than the KVM-accelerated x86_64 one, so it ran into the
300-minute limit in every caller (#33). GitHub's Arm runners cannot help:
none of them exposes nested virtualization.
Both legs now boot an x86_64 guest. The aarch64 leg pins the quarterly
package branch, installs lang/rust, copies the aarch64 standard library from
the aarch64 lang/rust package of the same version, builds an arm64 sysroot
from base.txz and the GUI library packages, and builds the workspace with
CARGO_BUILD_TARGET=aarch64-unknown-freebsd through base clang and lld. Stable
Rust, no -Zbuild-std.
The cross leg compiles every target, including the tests, but cannot run them
or package.sh, which builds for the guest's own architecture.
GitHub does not expose repository secrets (including SONAR_TOKEN) to pull
requests opened from forks. Because the reusable sonarcloud.yml declared
SONAR_TOKEN as a required secret and sonar-scan.yml called it with
`secrets: inherit` on every pull_request, fork PRs failed immediately with:
Error when evaluating 'secrets' ... Secret SONAR_TOKEN is required, but not
provided while calling.
This made every fork PR show a failing "SonarCloud Scan" check unrelated to the
PR's contents. (The separate SonarCloud GitHub App check, "SonarCloud Code
Analysis", is unaffected and continues to run.)
Fixes:
- Gate the caller job so it only runs on pushes to main and on same-repo PRs;
fork PRs now report a neutral skipped status instead of a hard failure.
- Make SONAR_TOKEN optional in the reusable workflow and guard the scan step on
the token being present, so a missing token never fails the workflow.
Verified with actionlint.
craft-common-actions holds reusable GitHub Actions (YAML), composite actions,
shell, and Python helper/test scripts with no unit-test coverage surface.
Exclude actions/, tests/, .github/, and shell/python from the coverage gate so
the new-code coverage condition doesn't block the project.
Fixes an accidental overwrite where the reusable workflow_call definition was
replaced by a caller stub. The reusable workflow lives at sonarcloud.yml; this
repo's own scan now runs via sonar-scan.yml calling the local reusable workflow.
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
Reusable workflow_call entrypoint for SonarCloud analysis across craft repos.
Consumer repos call it via storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main.
Derives project key as storytold_<repo> unless project-key input is provided.
release.yml and freebsd.yml default craft-fonts-ref to craft-fonts main
(bfcbf71: Japanese, Chinese, Arabic, Thai, Georgian), so one bump here
reaches every app's next release; an app sets the input only to pin
another commit. A test checks the two defaults agree and that every
release job that checks out craft-fonts builds with CRAFT_FONTS_REQUIRED=1.
Refs storytold/craft-libs#197
The emulated aarch64 VM takes over 5 hours for most apps, so the job is
cancelled at the limit. GitHub's ARM64 runners have no KVM, so they do not
help. Releases ship FreeBSD x86_64 only until #39 is fixed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#35 matched <app-name>-<version>-linux-<arch>.tar.gz exactly. craft-launcher
passes app-name craft-launcher but ships artcraft-launcher-*, so its 0.6.1
Arch and Flatpak jobs found nothing. Match *-<version>-linux-<arch>.tar.gz,
skip -cli- and -server- companion tarballs, and require exactly one.
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(release): pick the app's own Linux tarball by exact name
The Arch and Flatpak jobs globbed *-linux-<arch>.tar.gz. PDFCraft also ships
pdfcraft-cli-<ver>-linux-<arch>.tar.gz, so Arch saw two tarballs and failed,
and Flatpak aarch64 took the CLI tarball (no bin/pdfcraft). Select
<app-name>-<version>-linux-<arch>.tar.gz instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(release): pass tarball name parts through step env
Review (CodeRabbit, CWE-78): app-name is a free-form reusable-workflow
input, and interpolating it into run: puts caller text into the shell
source. The Flatpak and Arch steps now read APP_NAME, RELEASE_VERSION
and ARCH from env.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
aarch64-unknown-freebsd is a tier-3 Rust target, so rustup publishes no
toolchain for it: since the multiarch matrix (#17), every FreeBSD aarch64
job stopped in `prepare` with "installer for platform
'aarch64-unknown-freebsd' not found". The aarch64 leg now installs
FreeBSD's lang/rust package (1.98.1 on FreeBSD 15) and x86_64 keeps
rustup; `$HOME/.cargo/env`, which only rustup writes, is sourced only
when present. Applied to both freebsd.yml and release.yml.
Tests check that each architecture picks its installer and that the run
step works without a rustup environment; both fail on the previous
workflows.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(release): build Arch Linux package (.pkg.tar.zst) via makepkg from release tarball
* fix(arch-package): fix PKGBUILD provides quotes, add podman fallback and e2e test