2026-08-04 03:27:05 -04:00
|
|
|
name: CI
|
|
|
|
|
|
|
|
|
|
# Forgejo reads .forgejo/workflows before .github/workflows. Actions referenced
|
|
|
|
|
# bare (actions/checkout, actions/cache, ...) resolve through the instance's
|
|
|
|
|
# DEFAULT_ACTIONS_URL, which points at data.forgejo.org, so nothing here reaches
|
|
|
|
|
# out to github.com.
|
|
|
|
|
|
|
|
|
|
on:
|
|
|
|
|
push:
|
2026-08-04 03:40:19 -04:00
|
|
|
# Pushing a branch whose name starts with Release cuts a release: the version
|
|
|
|
|
# comes from Cargo.toml and the release job creates the tag itself. Pushing a
|
|
|
|
|
# v* tag by hand still works and takes the same path. The lowercase pattern is
|
|
|
|
|
# there because branch names are case-sensitive and a silent no-op would be a
|
|
|
|
|
# miserable thing to debug, as is the fact that * does not match / in these
|
|
|
|
|
# filters - Release/0.9.1 needs the ** form to be seen at all.
|
|
|
|
|
branches: [master, 'Release*', 'Release/**', 'release*', 'release/**']
|
2026-08-04 03:27:05 -04:00
|
|
|
tags: ['v*']
|
|
|
|
|
pull_request:
|
|
|
|
|
branches: [master]
|
|
|
|
|
workflow_dispatch:
|
|
|
|
|
|
|
|
|
|
concurrency:
|
|
|
|
|
group: ci-${{ github.ref }}
|
|
|
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
|
|
|
|
|
|
env:
|
|
|
|
|
CARGO_TERM_COLOR: always
|
|
|
|
|
# Incremental artifacts are never reused between CI runs and would bloat the
|
|
|
|
|
# cached target/ tree for nothing.
|
|
|
|
|
CARGO_INCREMENTAL: '0'
|
|
|
|
|
RUST_BACKTRACE: '1'
|
2026-08-04 23:33:28 -04:00
|
|
|
# Baked into the binaries by crates/quicksearch-gui/build.rs and shown in the
|
|
|
|
|
# GUI status bar, so a screenshot or a bug report identifies the exact build.
|
|
|
|
|
# Handed over rather than shelled out to git: checkout leaves a shallow clone
|
|
|
|
|
# this would otherwise have to trust, and the runner already knows the SHA.
|
|
|
|
|
# The version itself still comes from [workspace.package], as everywhere else.
|
|
|
|
|
QS_COMMIT: ${{ github.sha }}
|
2026-08-04 03:27:05 -04:00
|
|
|
|
|
|
|
|
jobs:
|
|
|
|
|
# ---------------------------------------------------------------- linux ----
|
|
|
|
|
#
|
|
|
|
|
# The container base is load-bearing: packaging/build-deb.sh derives the
|
|
|
|
|
# package's libc6 floor with objdump from the binary it just built, so the
|
|
|
|
|
# .deb inherits the *builder's* glibc. Ubuntu 22.04 fixes that floor at 2.35,
|
|
|
|
|
# which covers 22.04 LTS and newer plus Debian 12 and newer. Packages built by
|
|
|
|
|
# hand on a dev machine declared libc6 (>= 2.43) and installed on almost
|
|
|
|
|
# nothing.
|
|
|
|
|
#
|
|
|
|
|
# catthehacker/ubuntu is the act-compatible image family. A bare ubuntu:22.04
|
|
|
|
|
# will not work: JS actions need Node already present in the image, and no
|
|
|
|
|
# step can install it before actions/checkout runs.
|
|
|
|
|
linux:
|
2026-08-04 03:50:31 -04:00
|
|
|
runs-on: self-hosted
|
2026-08-04 03:27:05 -04:00
|
|
|
container:
|
|
|
|
|
image: catthehacker/ubuntu:act-22.04
|
2026-08-04 18:08:27 -04:00
|
|
|
# A container-level `options: --cap-drop=...` was tried here first and did
|
|
|
|
|
# not take effect - the runner does not pass it through to the daemon that
|
|
|
|
|
# creates the job container. The capabilities are dropped inside the Test
|
|
|
|
|
# step instead, where nothing can ignore them.
|
2026-08-04 03:27:05 -04:00
|
|
|
env:
|
|
|
|
|
# crates/quicksearch-core/src/config.rs has a test that expects a home
|
|
|
|
|
# directory and panics without one.
|
|
|
|
|
HOME: /root
|
|
|
|
|
# The highest libc6 version the .deb is allowed to require.
|
|
|
|
|
MAX_GLIBC: '2.35'
|
2026-08-04 18:20:17 -04:00
|
|
|
# full_index.rs asserts a heavy indexing root cannot stall a light one, and
|
|
|
|
|
# measures that as wall-clock stall. The 100 ms default is calibrated on a
|
|
|
|
|
# developer machine; this runner measured 188 ms for the same correct
|
|
|
|
|
# behaviour. 600 ms keeps the check meaningful - the regression it exists to
|
|
|
|
|
# catch is ~6x the healthy figure, so it would land near 1.2 s here.
|
|
|
|
|
QSB_STALL_BUDGET_MS: '600'
|
2026-08-04 03:27:05 -04:00
|
|
|
steps:
|
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
|
|
2026-08-04 03:40:19 -04:00
|
|
|
- name: Check the tag matches the workspace version
|
|
|
|
|
# Asset names come from [workspace.package] version, not from the tag, so
|
|
|
|
|
# tagging v0.9.0 against version 0.8.8 would publish a release called
|
|
|
|
|
# v0.9.0 full of 0.8.8 files. Fails in seconds, before anything is built.
|
|
|
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
run: |
|
|
|
|
|
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
|
|
|
|
|
tag="${GITHUB_REF#refs/tags/}"
|
|
|
|
|
if [ "v$version" != "$tag" ]; then
|
|
|
|
|
echo "ERROR: tag $tag does not match the workspace version $version." >&2
|
|
|
|
|
echo "Bump [workspace.package] version in Cargo.toml, refresh Cargo.lock," >&2
|
|
|
|
|
echo "commit, delete the tag and re-tag." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "OK: $tag matches the workspace version"
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
- name: Install build dependencies
|
|
|
|
|
run: |
|
|
|
|
|
apt-get update -qq
|
|
|
|
|
# rusqlite's bundled-sqlcipher-vendored-openssl and keyring's vendored
|
|
|
|
|
# feature compile SQLCipher, OpenSSL and libdbus from source, so a C
|
|
|
|
|
# toolchain plus perl covers them and no -dev packages are needed.
|
|
|
|
|
# winit and glutin dlopen the whole display stack, so there are no X11
|
|
|
|
|
# or Wayland headers here either. The rest is what build-deb.sh checks
|
|
|
|
|
# for before it will run.
|
2026-08-04 18:08:27 -04:00
|
|
|
# libcap2-bin provides capsh, which the Test step uses to drop the two
|
|
|
|
|
# DAC capabilities so root obeys permission bits.
|
2026-08-09 19:01:13 -04:00
|
|
|
# jq parses the tags API for the releasable check below. Named rather
|
|
|
|
|
# than assumed: the release job gets it from its image, and a base
|
|
|
|
|
# image change that dropped it would turn that check into a silent
|
|
|
|
|
# pass rather than a failure.
|
2026-08-07 20:44:07 -04:00
|
|
|
# zsync and appstream are for build-appimage.sh: appimagetool shells
|
|
|
|
|
# out to zsyncmake rather than bundling it, and reports success while
|
|
|
|
|
# writing nothing when it is absent, so the script checks for it up
|
|
|
|
|
# front. appstream provides the appstreamcli that validates the
|
|
|
|
|
# metainfo both packages ship.
|
2026-08-04 03:27:05 -04:00
|
|
|
apt-get install -y --no-install-recommends \
|
|
|
|
|
build-essential perl pkg-config \
|
2026-08-07 20:44:07 -04:00
|
|
|
binutils dpkg-dev desktop-file-utils gzip libcap2-bin \
|
2026-08-09 19:01:13 -04:00
|
|
|
zsync appstream jq
|
|
|
|
|
|
|
|
|
|
- name: Check the release tag is free
|
|
|
|
|
# The other half of the guard above, for the path that actually cuts
|
|
|
|
|
# most releases: pushing a Release* branch. There the tag comes from
|
|
|
|
|
# [workspace.package] rather than the ref, so the mistake is not a
|
|
|
|
|
# mismatched tag but a *forgotten bump* — the version still points at a
|
|
|
|
|
# release that already shipped.
|
|
|
|
|
#
|
|
|
|
|
# The release job checks this too and remains the authority; it just
|
|
|
|
|
# cannot check it until both build jobs are green, so forgetting the
|
|
|
|
|
# bump used to cost two full release builds, packaging and an artifact
|
|
|
|
|
# upload before anything said so. This says so in seconds.
|
|
|
|
|
#
|
|
|
|
|
# Deliberately as lenient as the release job: a tag at *this* commit is
|
|
|
|
|
# a re-run and fine, and an unreachable API reads as "not found" and
|
|
|
|
|
# lets the build proceed rather than failing on a network hiccup. An
|
|
|
|
|
# early check that blocks a good release is worse than one that misses
|
|
|
|
|
# a bad one, because the late check still catches it.
|
|
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
|
|
|
|
env:
|
|
|
|
|
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
|
run: |
|
|
|
|
|
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
|
|
|
|
|
[ -n "$version" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
|
|
|
|
|
tag="v$version"
|
|
|
|
|
api="${GITHUB_API_URL:-$GITHUB_SERVER_URL/api/v1}"
|
|
|
|
|
at=$(curl -sS -H "Authorization: token $RELEASE_TOKEN" \
|
|
|
|
|
"$api/repos/$GITHUB_REPOSITORY/tags/$tag" | jq -r '.commit.sha // empty')
|
|
|
|
|
if [ -n "$at" ] && [ "$at" != "$GITHUB_SHA" ]; then
|
|
|
|
|
echo "ERROR: $tag already exists at $at, not $GITHUB_SHA." >&2
|
|
|
|
|
echo "Bump [workspace.package] version in Cargo.toml, refresh Cargo.lock" >&2
|
|
|
|
|
echo "with 'cargo update -w', commit and push again." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "OK: $tag is free (or already at this commit)"
|
2026-08-04 03:27:05 -04:00
|
|
|
|
|
|
|
|
- name: Trust the workspace
|
|
|
|
|
# checkout writes as root into a directory git then considers dubiously
|
|
|
|
|
# owned; build-deb.sh and the version scrape both shell out to git.
|
|
|
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
|
|
|
|
|
|
|
|
- name: Install Rust
|
|
|
|
|
run: |
|
|
|
|
|
# --default-toolchain none defers to rust-toolchain.toml, so the pinned
|
|
|
|
|
# channel and its targets are downloaded exactly once.
|
|
|
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
|
|
|
| sh -s -- -y --profile minimal --default-toolchain none
|
|
|
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
|
# Materialise the pinned toolchain here rather than partway through the
|
|
|
|
|
# build, so a toolchain problem shows up as its own failed step.
|
|
|
|
|
"$HOME/.cargo/bin/rustup" show
|
|
|
|
|
|
|
|
|
|
- uses: actions/cache@v4
|
|
|
|
|
with:
|
|
|
|
|
path: |
|
|
|
|
|
~/.cargo/registry/index
|
|
|
|
|
~/.cargo/registry/cache
|
|
|
|
|
~/.cargo/git/db
|
|
|
|
|
target
|
|
|
|
|
# Keyed per job so this tree never collides with the cross-compiled one.
|
|
|
|
|
key: ${{ github.job }}-cargo-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}
|
|
|
|
|
restore-keys: ${{ github.job }}-cargo-
|
|
|
|
|
|
2026-08-04 17:15:00 -04:00
|
|
|
- name: Sync the lockfile to the workspace version
|
|
|
|
|
# Bumping [workspace.package] version leaves Cargo.lock pinning the old
|
|
|
|
|
# member versions, and --locked then refuses to build. `cargo update -w`
|
|
|
|
|
# re-resolves only the workspace members and leaves every third-party pin
|
|
|
|
|
# alone - it reports the others as "unchanged dependencies behind latest"
|
|
|
|
|
# rather than bumping them. So --locked below still catches the case that
|
|
|
|
|
# actually matters: a dependency added or bumped without committing the
|
|
|
|
|
# lockfile, which this cannot and should not paper over.
|
|
|
|
|
run: cargo update -w
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
- name: Build
|
|
|
|
|
run: cargo build --release --locked -p quicksearch-gui
|
|
|
|
|
|
|
|
|
|
- name: Test
|
|
|
|
|
# Release mode is not a nicety: tests/encrypted.rs derives an Argon2id key
|
|
|
|
|
# at m=64 MiB, t=3, which takes about half a second in release and minutes
|
|
|
|
|
# in debug. tests/snippet_perf.rs self-skips without QSB_SNIPPET_PERF=1.
|
2026-08-04 18:08:27 -04:00
|
|
|
#
|
|
|
|
|
# capsh drops CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH from the bounding
|
|
|
|
|
# set before exec. Without that, uid 0 reads a mode-000 file or directory
|
|
|
|
|
# regardless of its permissions, and the tests that build one with
|
|
|
|
|
# platform::deny_read see a perfectly readable tree: they assert an
|
|
|
|
|
# unreadable directory is *reported* rather than looking empty, because an
|
|
|
|
|
# empty listing deletes index rows. Dropping from the bounding set is what
|
|
|
|
|
# makes it stick - a root process re-derives its permitted set from the
|
|
|
|
|
# bounding set on execve, so the test binaries cannot regain them.
|
|
|
|
|
run: |
|
|
|
|
|
echo "capabilities before: $(grep CapEff /proc/self/status | tr -d '\t')"
|
|
|
|
|
capsh --drop=cap_dac_override,cap_dac_read_search -- -c '
|
|
|
|
|
echo "capabilities in test shell: $(grep CapEff /proc/self/status | tr -d "\t")"
|
|
|
|
|
cargo test --release --locked --workspace'
|
2026-08-04 03:27:05 -04:00
|
|
|
|
2026-08-09 19:01:13 -04:00
|
|
|
# Everything from here down produces release assets, so it runs only
|
|
|
|
|
# where a release can actually come out: a v* tag or a Release* branch.
|
|
|
|
|
# On master and pull requests the job stops after Build and Test, which
|
|
|
|
|
# is what those runs are for — the packaging that used to follow built a
|
|
|
|
|
# .deb, an AppImage, a tarball and a 14-day artifact upload that nothing
|
|
|
|
|
# would ever download, because the release job is skipped there anyway.
|
|
|
|
|
#
|
|
|
|
|
# The condition is repeated rather than hoisted into an env var: Actions
|
|
|
|
|
# has no workflow-level expression alias, YAML anchors are not supported,
|
|
|
|
|
# and `env.X` inside `if:` would fail *closed* on a runner that did not
|
|
|
|
|
# populate it — silently skipping packaging on a real release. Spelled
|
|
|
|
|
# out, it is the same form the release job's own gate uses.
|
2026-08-04 03:27:05 -04:00
|
|
|
- name: Build the .deb
|
2026-08-09 19:01:13 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:27:05 -04:00
|
|
|
# --no-build reuses the binaries from the Build step rather than
|
|
|
|
|
# recompiling. SOURCE_DATE_EPOCH pins the generated changelog date so
|
|
|
|
|
# repeat builds of the same commit are byte-identical.
|
|
|
|
|
run: |
|
|
|
|
|
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)" \
|
|
|
|
|
./packaging/build-deb.sh --no-build
|
|
|
|
|
|
|
|
|
|
- name: Check the glibc floor
|
2026-08-09 19:01:13 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:27:05 -04:00
|
|
|
# The whole point of pinning the container. If someone bumps the image,
|
|
|
|
|
# this fails loudly instead of quietly shipping an uninstallable package.
|
2026-08-09 19:01:13 -04:00
|
|
|
# Gated with the .deb it inspects — there is no package to read without
|
|
|
|
|
# the step above.
|
2026-08-04 03:27:05 -04:00
|
|
|
run: |
|
|
|
|
|
deb=$(ls dist/*.deb)
|
|
|
|
|
depends=$(dpkg-deb -f "$deb" Depends)
|
|
|
|
|
echo "$depends"
|
|
|
|
|
floor=$(printf '%s' "$depends" | sed -n 's/.*libc6 (>= \([0-9][0-9.]*\)).*/\1/p')
|
|
|
|
|
[ -n "$floor" ] || { echo "could not read the libc6 floor from $deb" >&2; exit 1; }
|
|
|
|
|
# dpkg's own comparator, so 2.9 does not sort above 2.35.
|
|
|
|
|
if ! dpkg --compare-versions "$floor" le "$MAX_GLIBC"; then
|
|
|
|
|
echo "ERROR: the .deb requires glibc $floor, above the $MAX_GLIBC target." >&2
|
|
|
|
|
echo "The build container base has probably changed." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "OK: glibc floor $floor <= $MAX_GLIBC"
|
|
|
|
|
|
2026-08-07 20:44:07 -04:00
|
|
|
- name: Build the AppImage
|
2026-08-09 19:01:13 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-07 20:44:07 -04:00
|
|
|
# After the glibc gate, so the cheaper check still fails first. --no-build
|
|
|
|
|
# reuses the binaries from the Build step, as the .deb step does, and
|
|
|
|
|
# SOURCE_DATE_EPOCH pins the date substituted into the AppStream release
|
|
|
|
|
# entry the same way it pins the .deb changelog.
|
|
|
|
|
#
|
|
|
|
|
# The script downloads appimagetool and the AppImage runtime, both pinned
|
|
|
|
|
# by sha256, and needs no FUSE: it runs appimagetool with
|
|
|
|
|
# APPIMAGE_EXTRACT_AND_RUN so the container needs no /dev/fuse.
|
|
|
|
|
run: |
|
|
|
|
|
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)" \
|
|
|
|
|
./packaging/build-appimage.sh --no-build
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
- name: Package the binaries
|
2026-08-09 19:01:13 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:27:05 -04:00
|
|
|
# A tarball for anyone not installing the .deb, stripped to match what
|
|
|
|
|
# build-deb.sh ships.
|
|
|
|
|
run: |
|
|
|
|
|
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
|
|
|
|
|
[ -n "$version" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
|
|
|
|
|
stage="quicksearch-$version-linux-x86_64"
|
|
|
|
|
mkdir -p "dist/$stage"
|
|
|
|
|
for bin in quicksearch quicksearch-cli; do
|
|
|
|
|
install -m755 "target/release/$bin" "dist/$stage/$bin"
|
|
|
|
|
strip --strip-unneeded "dist/$stage/$bin"
|
|
|
|
|
done
|
|
|
|
|
install -m644 README.md config_example.toml LICENSE "dist/$stage/"
|
|
|
|
|
tar -czf "dist/$stage.tar.gz" -C dist "$stage"
|
|
|
|
|
rm -rf "dist/$stage"
|
|
|
|
|
ls -l dist/
|
|
|
|
|
|
2026-08-04 18:38:41 -04:00
|
|
|
# Not v4, and not plain v3 either. @actions/artifact v2+ (which backs
|
|
|
|
|
# upload/download-artifact v4) inspects the API host, decides anything that
|
|
|
|
|
# is not github.com is GitHub Enterprise Server, and refuses outright with
|
|
|
|
|
# GHESNotSupportedError - it never reaches the server, so whether Forgejo
|
|
|
|
|
# implements the v4 artifact API is beside the point. Plain v3 runs on
|
|
|
|
|
# node16, which current Forgejo runner images no longer ship; the
|
|
|
|
|
# -node20 tags are Forgejo's builds for precisely this combination.
|
|
|
|
|
- uses: actions/upload-artifact@v3-node20
|
2026-08-09 19:01:13 -04:00
|
|
|
# `if-no-files-found: error` below would fail every master push once the
|
|
|
|
|
# packaging steps above are gated, so this carries the same gate.
|
|
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:27:05 -04:00
|
|
|
with:
|
|
|
|
|
name: linux-x86_64
|
2026-08-07 20:44:07 -04:00
|
|
|
# The .zsync is not optional: the update URL baked into every AppImage
|
|
|
|
|
# points at it, so leaving it unpublished breaks AppImageUpdate for
|
|
|
|
|
# everyone who already installed one. It is the one asset named without
|
|
|
|
|
# a version, because that URL has to keep resolving across releases.
|
2026-08-04 03:27:05 -04:00
|
|
|
path: |
|
|
|
|
|
dist/*.deb
|
|
|
|
|
dist/*.tar.gz
|
2026-08-07 20:44:07 -04:00
|
|
|
dist/*.AppImage
|
|
|
|
|
dist/*.zsync
|
2026-08-04 03:27:05 -04:00
|
|
|
if-no-files-found: error
|
|
|
|
|
retention-days: 14
|
|
|
|
|
|
|
|
|
|
# -------------------------------------------------------- windows-cross ----
|
|
|
|
|
#
|
|
|
|
|
# Deliberately a newer base than the linux job. This job emits a PE binary
|
|
|
|
|
# linked against msvcrt.dll, so the container's glibc cannot affect what the
|
|
|
|
|
# .exe runs on; pinning it to 22.04 would buy nothing while forcing the build
|
|
|
|
|
# through mingw-w64 10.3 instead of 13.2.
|
|
|
|
|
windows-cross:
|
2026-08-04 03:50:31 -04:00
|
|
|
runs-on: self-hosted
|
2026-08-04 03:27:05 -04:00
|
|
|
container:
|
|
|
|
|
image: catthehacker/ubuntu:act-24.04
|
|
|
|
|
env:
|
|
|
|
|
HOME: /root
|
|
|
|
|
TARGET: x86_64-pc-windows-gnu
|
|
|
|
|
# rusqlite, zstd-sys and openssl-src all shell out to a C compiler, which
|
|
|
|
|
# has to be the cross one rather than the host's cc.
|
|
|
|
|
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
|
|
|
|
|
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
|
|
|
|
|
AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar
|
|
|
|
|
steps:
|
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
|
|
|
|
|
|
- name: Install build dependencies
|
|
|
|
|
run: |
|
|
|
|
|
apt-get update -qq
|
2026-08-04 23:33:28 -04:00
|
|
|
# nsis is what compiles the Windows installer, and it compiles it here
|
|
|
|
|
# rather than on Windows: makensis is a Linux binary that emits a PE,
|
|
|
|
|
# so both Windows assets come out of this one job.
|
2026-08-04 03:27:05 -04:00
|
|
|
apt-get install -y --no-install-recommends \
|
|
|
|
|
build-essential perl make pkg-config \
|
2026-08-04 23:33:28 -04:00
|
|
|
gcc-mingw-w64-x86-64 zip nsis
|
2026-08-04 03:27:05 -04:00
|
|
|
|
|
|
|
|
- name: Trust the workspace
|
|
|
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
|
|
|
|
|
|
|
|
- name: Install Rust
|
|
|
|
|
run: |
|
|
|
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
|
|
|
| sh -s -- -y --profile minimal --default-toolchain none
|
|
|
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
|
# Materialise the pinned toolchain here rather than partway through the
|
|
|
|
|
# build, so a toolchain problem shows up as its own failed step.
|
|
|
|
|
"$HOME/.cargo/bin/rustup" show
|
|
|
|
|
|
|
|
|
|
- uses: actions/cache@v4
|
|
|
|
|
with:
|
|
|
|
|
path: |
|
|
|
|
|
~/.cargo/registry/index
|
|
|
|
|
~/.cargo/registry/cache
|
|
|
|
|
~/.cargo/git/db
|
|
|
|
|
target
|
|
|
|
|
key: ${{ github.job }}-cargo-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}
|
|
|
|
|
restore-keys: ${{ github.job }}-cargo-
|
|
|
|
|
|
2026-08-04 17:15:00 -04:00
|
|
|
- name: Sync the lockfile to the workspace version
|
|
|
|
|
# Same reasoning as the linux job: workspace members only, third-party
|
|
|
|
|
# pins untouched, so --locked keeps its teeth.
|
|
|
|
|
run: cargo update -w
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
- name: Build
|
|
|
|
|
# rust-toolchain.toml already lists the target, so no `rustup target add`.
|
|
|
|
|
run: cargo build --release --locked -p quicksearch-gui --target "$TARGET"
|
|
|
|
|
|
|
|
|
|
- name: Check for non-system DLL dependencies
|
|
|
|
|
# Debian's default mingw alternative uses posix threads, which can pull
|
|
|
|
|
# in libwinpthread-1.dll or libgcc_s_seh-1.dll and produce an .exe that
|
|
|
|
|
# refuses to start on a clean Windows machine. The binaries are currently
|
|
|
|
|
# clean - every import is a system DLL - and this keeps them that way.
|
|
|
|
|
run: |
|
|
|
|
|
for exe in "target/$TARGET"/release/quicksearch.exe "target/$TARGET"/release/quicksearch-cli.exe; do
|
|
|
|
|
dlls=$(x86_64-w64-mingw32-objdump -p "$exe" | sed -n 's/^[[:space:]]*DLL Name: //p' | sort -fu)
|
|
|
|
|
printf '%s:\n%s\n\n' "$exe" "$dlls"
|
|
|
|
|
if printf '%s\n' "$dlls" | grep -qiE '^(libgcc|libwinpthread|libstdc\+\+|libssp)'; then
|
|
|
|
|
echo "ERROR: $exe imports a non-system DLL and will not run on a clean Windows install." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
2026-08-09 19:01:13 -04:00
|
|
|
# As in the linux job, the asset-producing steps run only where a release
|
|
|
|
|
# can come out. The DLL check above deliberately stays ungated: it
|
|
|
|
|
# validates the .exe itself rather than packaging it, costs an objdump,
|
|
|
|
|
# and is exactly the kind of regression worth catching on master rather
|
|
|
|
|
# than at release time.
|
2026-08-04 23:33:28 -04:00
|
|
|
- name: Build the installer
|
2026-08-09 19:01:13 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 23:33:28 -04:00
|
|
|
# --no-build reuses the binaries from the Build step rather than
|
|
|
|
|
# cross-compiling them a second time. The installer and the .zip below
|
|
|
|
|
# are alternatives, not a two-step download: the installer puts the app
|
|
|
|
|
# in Program Files with a Start menu entry and an uninstall entry, the
|
|
|
|
|
# .zip is the same binaries for anyone who wants them unpacked by hand
|
|
|
|
|
# or run portably.
|
|
|
|
|
run: ./packaging/build-installer.sh --no-build
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
- name: Package the binaries
|
2026-08-09 19:01:13 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:27:05 -04:00
|
|
|
run: |
|
|
|
|
|
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
|
|
|
|
|
[ -n "$version" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
|
|
|
|
|
stage="quicksearch-$version-windows-x86_64"
|
|
|
|
|
mkdir -p "dist/$stage"
|
|
|
|
|
for exe in quicksearch.exe quicksearch-cli.exe; do
|
|
|
|
|
install -m755 "target/$TARGET/release/$exe" "dist/$stage/$exe"
|
|
|
|
|
x86_64-w64-mingw32-strip --strip-unneeded "dist/$stage/$exe"
|
|
|
|
|
done
|
|
|
|
|
install -m644 README.md config_example.toml LICENSE "dist/$stage/"
|
|
|
|
|
(cd dist && zip -qr "$stage.zip" "$stage")
|
|
|
|
|
rm -rf "dist/$stage"
|
|
|
|
|
ls -l dist/
|
|
|
|
|
|
2026-08-04 18:38:41 -04:00
|
|
|
# Not v4, and not plain v3 either. @actions/artifact v2+ (which backs
|
|
|
|
|
# upload/download-artifact v4) inspects the API host, decides anything that
|
|
|
|
|
# is not github.com is GitHub Enterprise Server, and refuses outright with
|
|
|
|
|
# GHESNotSupportedError - it never reaches the server, so whether Forgejo
|
|
|
|
|
# implements the v4 artifact API is beside the point. Plain v3 runs on
|
|
|
|
|
# node16, which current Forgejo runner images no longer ship; the
|
|
|
|
|
# -node20 tags are Forgejo's builds for precisely this combination.
|
|
|
|
|
- uses: actions/upload-artifact@v3-node20
|
2026-08-09 19:01:13 -04:00
|
|
|
# Same gate as the packaging steps, for the same `if-no-files-found`
|
|
|
|
|
# reason as the linux job.
|
|
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:27:05 -04:00
|
|
|
with:
|
|
|
|
|
name: windows-x86_64
|
2026-08-04 23:33:28 -04:00
|
|
|
path: |
|
|
|
|
|
dist/*-setup.exe
|
|
|
|
|
dist/*.zip
|
2026-08-04 03:27:05 -04:00
|
|
|
if-no-files-found: error
|
|
|
|
|
retention-days: 14
|
|
|
|
|
|
|
|
|
|
# -------------------------------------------------------------- release ----
|
2026-08-04 03:40:19 -04:00
|
|
|
#
|
|
|
|
|
# Two ways in, one path out:
|
|
|
|
|
# push a Release* branch -> the tag is derived from the workspace version
|
|
|
|
|
# push a v* tag by hand -> that tag is used as-is
|
|
|
|
|
#
|
|
|
|
|
# Either way this runs only after linux and windows-cross are green, so a
|
|
|
|
|
# failing test cannot produce a release.
|
|
|
|
|
#
|
|
|
|
|
# The tag is created by the same API call that creates the release. Creating it
|
|
|
|
|
# in an earlier step would not work: a tag pushed with CI's own token does not
|
|
|
|
|
# re-trigger workflows, so a design that tagged first and waited for the tag
|
|
|
|
|
# event would stall with no release and no error.
|
2026-08-04 03:27:05 -04:00
|
|
|
release:
|
|
|
|
|
needs: [linux, windows-cross]
|
2026-08-04 03:40:19 -04:00
|
|
|
if: >-
|
|
|
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/Release')
|
|
|
|
|
|| startsWith(github.ref, 'refs/heads/release')
|
2026-08-04 03:50:31 -04:00
|
|
|
runs-on: self-hosted
|
2026-08-04 03:27:05 -04:00
|
|
|
container:
|
|
|
|
|
image: catthehacker/ubuntu:act-24.04
|
|
|
|
|
steps:
|
2026-08-04 03:40:19 -04:00
|
|
|
# Only needed to read the workspace version on a Release* branch push.
|
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
|
|
2026-08-04 18:38:41 -04:00
|
|
|
# Must match the uploader's major version - see the note on the upload steps.
|
|
|
|
|
- uses: actions/download-artifact@v3-node20
|
2026-08-04 03:27:05 -04:00
|
|
|
with:
|
|
|
|
|
path: artifacts
|
|
|
|
|
|
|
|
|
|
- name: Publish the release
|
|
|
|
|
# Forgejo populates GITHUB_API_URL, GITHUB_REPOSITORY and the token by
|
|
|
|
|
# itself, so this needs no configuration. Talking to the API directly
|
|
|
|
|
# keeps the release step off any third-party action's release cadence.
|
|
|
|
|
env:
|
|
|
|
|
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
|
run: |
|
|
|
|
|
api="${GITHUB_API_URL:-$GITHUB_SERVER_URL/api/v1}"
|
|
|
|
|
auth="Authorization: token $RELEASE_TOKEN"
|
2026-08-04 03:40:19 -04:00
|
|
|
repo="$GITHUB_REPOSITORY"
|
|
|
|
|
|
|
|
|
|
# One source of truth for the version: the same [workspace.package]
|
|
|
|
|
# field build-deb.sh reads and the artifact names already carry. The
|
|
|
|
|
# branch name only signals intent - nothing is parsed out of it.
|
|
|
|
|
case "$GITHUB_REF" in
|
|
|
|
|
refs/tags/*)
|
|
|
|
|
tag="${GITHUB_REF#refs/tags/}" ;;
|
|
|
|
|
*)
|
|
|
|
|
version=$(sed -n '/^\[workspace\.package\]/,/^\[/{ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p }' Cargo.toml)
|
|
|
|
|
[ -n "$version" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
|
|
|
|
|
tag="v$version" ;;
|
|
|
|
|
esac
|
|
|
|
|
echo "release tag: $tag commit: $GITHUB_SHA"
|
|
|
|
|
|
|
|
|
|
# A published version is immutable. If the tag already exists at some
|
|
|
|
|
# other commit, stop rather than ship two different builds under one
|
|
|
|
|
# version - the usual cause is forgetting to bump Cargo.toml.
|
|
|
|
|
at=$(curl -sS -H "$auth" "$api/repos/$repo/tags/$tag" | jq -r '.commit.sha // empty')
|
|
|
|
|
if [ -n "$at" ] && [ "$at" != "$GITHUB_SHA" ]; then
|
|
|
|
|
echo "ERROR: tag $tag already exists at $at, not $GITHUB_SHA." >&2
|
|
|
|
|
echo "Bump [workspace.package] version in Cargo.toml, refresh Cargo.lock" >&2
|
|
|
|
|
echo "with 'cargo update -w', commit and push again." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-08-04 03:27:05 -04:00
|
|
|
|
|
|
|
|
mkdir -p dist
|
|
|
|
|
find artifacts -type f -exec mv -t dist -- {} +
|
|
|
|
|
ls -l dist/
|
|
|
|
|
|
2026-08-04 03:40:19 -04:00
|
|
|
# Creating a release whose tag_name does not exist yet makes Forgejo
|
|
|
|
|
# create the tag at target_commitish, so this one call both tags and
|
|
|
|
|
# publishes. Reuse an existing release so a re-run replaces assets
|
|
|
|
|
# instead of failing.
|
|
|
|
|
id=$(curl -sS -H "$auth" "$api/repos/$repo/releases/tags/$tag" | jq -r '.id // empty')
|
2026-08-04 03:27:05 -04:00
|
|
|
if [ -z "$id" ]; then
|
2026-08-04 03:40:19 -04:00
|
|
|
id=$(curl -fsS -X POST "$api/repos/$repo/releases" \
|
2026-08-04 03:27:05 -04:00
|
|
|
-H "$auth" -H 'Content-Type: application/json' \
|
2026-08-04 03:40:19 -04:00
|
|
|
-d "$(jq -n --arg t "$tag" --arg c "$GITHUB_SHA" \
|
|
|
|
|
'{tag_name: $t, target_commitish: $c, name: $t, draft: false, prerelease: false}')" \
|
2026-08-04 03:27:05 -04:00
|
|
|
| jq -r '.id // empty')
|
|
|
|
|
fi
|
|
|
|
|
[ -n "$id" ] || { echo "could not create or find a release for $tag" >&2; exit 1; }
|
|
|
|
|
|
|
|
|
|
for f in dist/*; do
|
|
|
|
|
name=$(basename "$f")
|
|
|
|
|
echo "uploading $name"
|
|
|
|
|
# Replace an asset of the same name left by an earlier run.
|
2026-08-04 03:40:19 -04:00
|
|
|
old=$(curl -sS -H "$auth" "$api/repos/$repo/releases/$id/assets" \
|
2026-08-04 03:27:05 -04:00
|
|
|
| jq -r --arg n "$name" '.[] | select(.name == $n) | .id')
|
|
|
|
|
[ -z "$old" ] || curl -fsS -X DELETE -H "$auth" \
|
2026-08-04 03:40:19 -04:00
|
|
|
"$api/repos/$repo/releases/$id/assets/$old"
|
|
|
|
|
curl -fsS -X POST "$api/repos/$repo/releases/$id/assets?name=$name" \
|
2026-08-04 03:27:05 -04:00
|
|
|
-H "$auth" -F "attachment=@$f" -o /dev/null
|
|
|
|
|
done
|
|
|
|
|
echo "published $tag"
|