* 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
The desktop release jobs check out storytold/craft-fonts at craft-fonts-ref and build with CRAFT_FONTS_DIR. The web job never did, so an app's web build could not embed any of its fonts.
This adds a boolean input, web-craft-fonts, defaulting to false. When it's true and craft-fonts-ref is set, the web job checks out craft-fonts and runs packaging/web/package.sh with CRAFT_FONTS_DIR and CRAFT_FONTS_REQUIRED=1.
Co-authored-by: kulveersingh <kulveer.singh@gmail.com>
SoundCraft links ALSA through cpal; its pre-shared-workflow release.yml
installed libasound2-dev (and :riscv64). Without them the Linux x86_64,
aarch64 and riscv64 release jobs fail with "unable to find library
-lasound". Apps that don't use ALSA don't link it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- macOS and Windows jobs fail on refs/heads/release when their signing secrets
are empty, instead of letting the app's packaging warn and ship unsigned.
A caller that doesn't list a secret name gets it empty, even though the value
is in its release environment: craft-launcher 0.3.1 built unsigned Windows
MSIs this way.
- linux-riscv64 calls package.sh --formats "deb rpm tar". Every app's
package.sh splits formats on spaces, so deb,rpm,tar matched nothing and the
job failed with an empty dist/release.
- README caller example lists the secrets instead of secrets: inherit.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dtolnay/rust-toolchain derives the toolchain from the ref it is called at
(@stable, @1.90). Pinned to a commit SHA, that ref carries no toolchain, so
the action fails with "'toolchain' is a required input". This broke the
Windows ARM64 workflow on every craft app's main (e.g. storytold/wordcraft
run on 860f4ca) and would break the macOS, Windows, Linux, web and riscv64
release jobs the same way. Pass toolchain: stable at every call site.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>