Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,24 @@ jobs:
run: cargo audit --deny warnings

cross:
name: Cross-compile for the NAS
name: Cross-compile for the NAS, and package it
runs-on: ubuntu-latest
# This is where a C dependency breaks first: SQLite is compiled from source, and
# armv7-musl is the least forgiving target we ship. Catching it here beats catching
# it while cutting a release.
# it while cutting a release. The DSM packages are assembled on the same job for the
# same reason — they wrap these exact binaries and cost a few seconds more.
steps:
- uses: actions/checkout@v4

- name: Toolchain
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup target add armv7-unknown-linux-musleabihf
rustup target add armv7-unknown-linux-gnueabihf
# The x86_64 musl build is here for the package check below: it runs on the
# runner, which is what makes "the packaged binary reports the version INFO
# claims" a real assertion rather than a re-read of the same string.
rustup target add x86_64-unknown-linux-musl

- uses: actions/cache@v4
with:
Expand All @@ -138,13 +143,46 @@ jobs:
echo "$HOME/zig" >> "$GITHUB_PATH"
command -v cargo-zigbuild >/dev/null || cargo install cargo-zigbuild --locked

# The glibc floor is the point of this build, not an incidental flag: Synology's ARMv7
# kernels are 3.10 and answer the time64 syscalls with EINVAL rather than ENOSYS, so
# musl 1.2 never falls back and every clock call fails on the machine this project
# exists for. glibc on 32-bit uses the time32 syscalls.
- name: Build
run: cargo zigbuild --release --target armv7-unknown-linux-musleabihf
run: cargo zigbuild --release --target armv7-unknown-linux-gnueabihf.2.17

- name: It must be static, or DSM will not run it
- name: It must not need a glibc the NAS does not have
run: |
set -euo pipefail
BIN=target/armv7-unknown-linux-musleabihf/release/rescriptum
BIN=target/armv7-unknown-linux-gnueabihf/release/rescriptum
file "$BIN"
file "$BIN" | grep -q 'statically linked'
file "$BIN" | grep -q 'ARM'
# DSM 7 on armada38x ships glibc 2.20. Asking for anything newer fails at exec
# time, on the NAS, with an error that names a symbol version and nothing else.
WANT=$(readelf --dyn-syms "$BIN" | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1)
echo "needs at most $WANT"
[ "$(printf '%s\n' 'GLIBC_2.17' "$WANT" | sort -V | tail -1)" = 'GLIBC_2.17' ]
echo "size: $(stat -c%s "$BIN") bytes"

- name: Build x86_64-musl too
run: cargo zigbuild --release --target x86_64-unknown-linux-musl

# Packaging breaks on the PR that breaks it, rather than at tag time. This is the
# cheap half of "does this package work"; the other half is installing it, which
# only a DSM machine can answer.
- name: Assemble the DSM packages
run: |
set -euo pipefail
packaging/dsm/make-spk.sh armv7
packaging/dsm/make-spk.sh x86_64

- name: Check them structurally
run: packaging/dsm/check-spk.sh

# Everything the package's *scripts* decide is testable without DSM — the env file
# written once and only once, the wizard's values and their absence, the service
# surviving its own start script, the exit codes Package Center reads, an upgrade
# that must not touch a hand-edited configuration, an uninstall that must not touch
# the answers. That is where the expensive mistakes live, so it runs on every push.
# What is left for a real machine is DSM's own machinery: packaging/dsm/vm/.
- name: Drive the package lifecycle
run: packaging/dsm/lifecycle-test.sh dist/rescriptum-*-x86_64.spk
65 changes: 65 additions & 0 deletions .github/workflows/dsm-rig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: DSM rig

# The half of the package's tests that needs a DSM machine — the data-share worker and its
# ACL, the port-config worker, the generated systemd unit, logrotate against a live
# descriptor, and whether Package Center accepts the archive at all.
#
# **It is inert until a rig exists.** It runs only by hand, only on a self-hosted runner
# labelled `dsm-rig`, and that runner is expected to sit next to a DSM 7 VM (see
# packaging/dsm/vm/README.md) with the same toolchain a developer has: Rust, and Zig plus
# cargo-zigbuild if you point it at the ARMv7 machine. Everything that does *not* need a
# machine already runs on every push, in ci.yml.
#
# It is destructive on the target by design: it upgrades over a hand-edited configuration
# and then uninstalls. Point it at a machine whose answers nobody cares about.
on:
workflow_dispatch:
inputs:
host:
description: "user@host of the DSM machine (the VM, or the NAS)"
required: true
default: "admin@localhost"
port:
description: "SSH port (2222 for the QEMU rig)"
required: false
default: "2222"
abi:
description: "x86_64, armv7 or aarch64 — blank asks the machine"
required: false
default: ""

jobs:
rig:
name: Install, upgrade and uninstall on DSM
runs-on: [self-hosted, dsm-rig]
steps:
- uses: actions/checkout@v4

- name: The key for the rig
env:
KEY: ${{ secrets.DSM_RIG_SSH_KEY }}
run: |
set -euo pipefail
if [ -z "${KEY:-}" ]; then
echo "::error::set the DSM_RIG_SSH_KEY secret to a key that can reach the rig"
exit 1
fi
install -m 700 -d "$HOME/.ssh"
printf '%s\n' "$KEY" > "$HOME/.ssh/dsm-rig"
chmod 600 "$HOME/.ssh/dsm-rig"
# The rig is a machine on the maintainer's own network, reinstalled often.
ssh-keyscan -p "${{ inputs.port }}" -H "$(echo '${{ inputs.host }}' | cut -d@ -f2)" \
>> "$HOME/.ssh/known_hosts" 2>/dev/null || true

- name: Run the checks
run: |
set -euo pipefail
ABI=""
[ -n "${{ inputs.abi }}" ] && ABI="--abi ${{ inputs.abi }}"
# shellcheck disable=SC2086
packaging/dsm/vm/on-dsm.sh "${{ inputs.host }}" -p "${{ inputs.port }}" \
-i "$HOME/.ssh/dsm-rig" $ABI

- name: Forget the key
if: always()
run: rm -f "$HOME/.ssh/dsm-rig"
85 changes: 76 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ on:
tag:
description: "Tag to build (e.g. v0.1.0)"
required: true
# An SPK version is all-numeric segments, and its last one is a package build
# number. `verify` requires the tag to equal Cargo.toml, so v0.1.0 can only ever
# produce 0.1.0-1: a packaging-only fix has no tag to stand on. Bumping this and
# dispatching by hand attaches rescriptum-0.1.0-2-<abi>.spk to the same Release.
spk_build:
description: "SPK build number (bump for a packaging-only repack)"
required: false
default: "1"

permissions:
contents: write
Expand Down Expand Up @@ -50,8 +58,11 @@ jobs:
fail-fast: false
matrix:
include:
# The DS416j this was written for.
- target: armv7-unknown-linux-musleabihf
# The DS416j this was written for. glibc, not musl, and the version is
# deliberate: Synology's 3.10 kernels answer the time64 syscalls with EINVAL
# rather than ENOSYS, so musl 1.2 never falls back and every clock call fails.
- target: armv7-unknown-linux-gnueabihf
zig_target: armv7-unknown-linux-gnueabihf.2.17
os: ubuntu-latest
cross: true
- target: aarch64-unknown-linux-musl
Expand Down Expand Up @@ -92,7 +103,8 @@ jobs:
run: |
set -euo pipefail
if [ "${{ matrix.cross }}" = "true" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
# zig_target carries the glibc floor where the Rust target cannot.
cargo zigbuild --release --target "${{ matrix.zig_target || matrix.target }}"
else
cargo build --release --target ${{ matrix.target }}
fi
Expand All @@ -118,9 +130,56 @@ jobs:
path: dist/*.tar.gz*
retention-days: 7

package-dsm:
name: Synology packages
needs: [verify, build]
runs-on: ubuntu-latest
# Nothing is compiled here: the binaries are already built and statically linked, and
# an .spk is a release format — the same artifact, wrapped for one platform's package
# manager. See packaging/dsm/ for why we assemble it ourselves rather than through
# pkgscripts-ng.
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
# The SPK's CHANGELOG is generated from git log between the previous tag and
# this one, and Package Center is the only place a DSM user ever sees it.
fetch-depth: 0

- uses: actions/download-artifact@v4
with:
path: artifacts

- name: Assemble
run: |
set -euo pipefail
VERSION="${{ needs.verify.outputs.version }}"
SPK_BUILD="${{ inputs.spk_build || '1' }}"
mkdir -p bins
# One .spk per *build*; the arch line inside each covers the platforms that
# build serves. aarch64 (arch="armv8") joins this list once the binary has been
# run on one of its platforms — make-spk.sh already knows the mapping.
for pair in "x86_64:x86_64-unknown-linux-musl" "armv7:armv7-unknown-linux-musleabihf"; do
abi="${pair%%:*}"; target="${pair#*:}"
tar -xzf "artifacts/$target/rescriptum-$VERSION-$target.tar.gz" -C bins
packaging/dsm/make-spk.sh "$abi" \
--bin "bins/rescriptum-$VERSION-$target/rescriptum" \
--version "$VERSION" --spk-build "$SPK_BUILD"
done
ls -l dist

- name: Check them structurally
run: packaging/dsm/check-spk.sh

- uses: actions/upload-artifact@v4
with:
name: dsm-packages
path: dist/*.spk*
retention-days: 7

publish:
name: Publish the release
needs: [verify, build]
needs: [verify, build, package-dsm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -135,7 +194,7 @@ jobs:
run: |
set -euo pipefail
mkdir -p dist
find artifacts -type f -name '*.tar.gz*' -exec cp {} dist/ \;
find artifacts -type f \( -name '*.tar.gz*' -o -name '*.spk*' \) -exec cp {} dist/ \;
ls -l dist

- name: Release
Expand All @@ -145,7 +204,15 @@ jobs:
set -euo pipefail
TAG="${{ inputs.tag || github.ref_name }}"
# `gh` is preinstalled on the runner, so this needs no third-party action.
gh release create "$TAG" dist/* \
--title "rescriptum $TAG" \
--generate-notes \
--verify-tag
#
# A dispatch that repacks — a packaging-only fix shipping as spk_build=2, or a
# re-run after a job failed — meets a Release that already exists. Uploading
# into it is the documented path for that, and `gh release create` would fail.
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" dist/* --clobber
else
gh release create "$TAG" dist/* \
--title "rescriptum $TAG" \
--generate-notes \
--verify-tag
fi
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,20 @@ equivalent), follow the notabene review protocol:
Non-negotiable: only process comments with `status: "open"` and `hold: false`; never
delete the store; never commit without being asked.
<!-- notabene:end -->

## Changing the Synology package

`packaging/dsm/` is shell that runs as root-adjacent code on someone's NAS, and **the local
harness cannot prove it**. Two of the three real defects found so far were invisible to a
fake-tree test and only appeared on a real DSM.

- The procedure is
[`packaging/dsm/vm/README.md`](packaging/dsm/vm/README.md) → *Changing the package? This is
the procedure*. Follow it rather than inventing a shortcut.
- A **DSM 7.2.2 virtual machine already exists on the maintainer's machine**, in Docker, with
a `clean` snapshot to restore. It is set up by `packaging/dsm/vm/bootstrap.sh` and driven
by `packaging/dsm/vm/on-dsm.sh`; neither needs anything outside Docker.
- The machine checks are **destructive on purpose** — they upgrade over a hand-edited
configuration and then uninstall. Restore the snapshot before and after.
- **Never name a DSM account after the package user** (`rescriptum`): DSM deletes it with the
package. The rig's account is `rigadmin`.
Loading
Loading