Merge remote-tracking branch 'upstream/master' into spark/aarch64-settings-boot-dropins

# Conflicts:
#	.github/workflows/test.yml
This commit is contained in:
Jim Martin committed 2026-09-22 18:35:30 -05:00
commit 19976635bc
145 files changed
+5560 -158

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# Self-test for helpers/artifact-helpers.sh: package files survive the
# artifact hop between build-pr.yml and publish.yml with makepkg's names
# intact, including the colon an epoch puts in them.
set -euo pipefail
ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$ROOT/helpers/artifact-helpers.sh"
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
pass() { echo "PASS: $1"; }
fail() { echo "FAIL: $1"; exit 1; }
EPOCH='cursor-cli-1:2026.09.18.1.9a7762b-1-x86_64.pkg.tar.zst'
PLAIN='beta-1.0-1-x86_64.pkg.tar.zst'
mkdir -p "$T/built" "$T/artifact" "$T/out"
echo epoch > "$T/built/$EPOCH"
echo plain > "$T/built/$PLAIN"
echo sig > "$T/built/$PLAIN.sig"
echo db > "$T/built/omarchy.db.tar.zst"
pack_packages "$T/built" "$T/artifact/packages.tar" || fail "pack"
[[ "$(tar -tf "$T/artifact/packages.tar" | sort | tr '\n' ' ')" == "$PLAIN $EPOCH " ]] \
&& pass "packages.tar holds the packages only, no signature or database" || fail "tar contents: $(tar -tf "$T/artifact/packages.tar" | tr '\n' ' ')"
[[ "$(ls "$T/artifact")" == "packages.tar" ]] && pass "the artifact path carries no colon" || fail "artifact listing"
unpack_packages "$T/artifact" "$T/out" || fail "unpack"
[[ "$(ls "$T/out" | sort | tr '\n' ' ')" == "$PLAIN $EPOCH " && "$(cat "$T/out/$EPOCH")" == epoch ]] \
&& pass "epoch filename and bytes survive the round trip" || fail "round trip: $(ls "$T/out" | tr '\n' ' ')"
# An artifact uploaded before packing existed: bare package files.
mkdir -p "$T/old" "$T/out2"; cp "$T/built"/*.pkg.tar.zst "$T/old/"
unpack_packages "$T/old" "$T/out2" && [[ "$(ls "$T/out2" | sort | tr '\n' ' ')" == "$PLAIN $EPOCH " ]] \
&& pass "a bare pre-packing artifact still unpacks" || fail "bare artifact"
# The workflows call these bare under `bash -e`, so any non-zero status
# inside them ends the step. (The first version used `shopt -p nullglob`,
# which exits 1 when the option is off; the tests above never saw it because
# `||` suppresses errexit.)
mkdir -p "$T/out4" "$T/out5"
bash -e -c "source '$ROOT/helpers/artifact-helpers.sh'; pack_packages '$T/built' '$T/out4/packages.tar'; tar -tf '$T/out4/packages.tar' >/dev/null; unpack_packages '$T/out4' '$T/out5'" \
&& [[ "$(ls "$T/out5" | sort | tr '\n' ' ')" == "$PLAIN $EPOCH " ]] \
&& pass "pack and unpack succeed under bash -e, as the workflows call them" || fail "bash -e"
mkdir -p "$T/empty"
if pack_packages "$T/empty" "$T/x.tar" 2>/dev/null; then fail "packing an empty build dir should fail"; else pass "empty build dir refused"; fi
if unpack_packages "$T/empty" "$T/out3" 2>/dev/null; then fail "an empty artifact should fail"; else pass "empty artifact refused"; fi
+130
View File
@@ -0,0 +1,130 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, utimesSync, writeFileSync } = require('node:fs');
const { tmpdir } = require('node:os');
const { join } = require('node:path');
const { test } = require('node:test');
const root = join(__dirname, '..');
const workflow = readFileSync(join(root, '.github/workflows/builder-images.yml'), 'utf8');
function fixture(t) {
const directory = mkdtempSync(join(tmpdir(), 'builder-image-test-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
mkdirSync(join(directory, 'bin'));
mkdirSync(join(directory, 'build'));
cpSync(join(root, 'helpers'), join(directory, 'helpers'), { recursive: true });
cpSync(join(root, 'bin/builder-image'), join(directory, 'bin/builder-image'));
writeFileSync(join(directory, 'build/Dockerfile'), 'FROM scratch\nCOPY input /input\n');
writeFileSync(join(directory, 'build/input'), 'original input\n');
const engine = join(directory, 'engine');
mkdirSync(engine);
const log = join(directory, 'engine.jsonl');
writeFileSync(join(engine, 'docker'), `#!/usr/bin/env node
const fs = require('node:fs');
const args = process.argv.slice(2);
fs.appendFileSync(process.env.ENGINE_LOG, JSON.stringify(args) + '\\n');
if (args[0] === 'manifest' && process.env.PRIVATE_IMAGE === '1') process.exit(1);
if (args[0] === 'push' && process.env.PUSH_FAIL === '1') process.exit(1);
if (args[0] === 'image' && args[1] === 'inspect') console.log('ghcr.io/omacom/omarchy-pkg-builder@sha256:' + 'a'.repeat(64));
`);
chmodSync(join(engine, 'docker'), 0o755);
const env = {
...process.env, PATH: `${engine}:${process.env.PATH}`, CONTAINER_ENGINE: 'docker',
ENGINE_LOG: log, ARCH: 'x86_64', MIRROR: 'edge',
};
const run = (args, extraEnv = {}) => spawnSync(join(directory, 'bin/builder-image'), args, {
cwd: directory, env: { ...env, ...extraEnv }, encoding: 'utf8',
});
const key = (...args) => {
const result = run(['key', ...args]);
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
};
const calls = () => readFileSync(log, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
return { directory, env, run, key, calls };
}
test('image keys are stable across checkout location and timestamp changes', t => {
const a = fixture(t);
const b = fixture(t);
const key = a.key();
assert.match(key, /^v1-x86_64-edge-[a-f0-9]{64}$/);
utimesSync(join(b.directory, 'build/input'), new Date(0), new Date(0));
assert.equal(b.key(), key);
});
test('image keys separate architectures, mirrors, content, modes and symlink targets', t => {
const f = fixture(t);
const keys = new Set([f.key(), f.key('--arch', 'aarch64'), f.key('--mirror', 'rc'), f.key('--mirror', 'stable')]);
writeFileSync(join(f.directory, 'build/input'), 'new input\n');
keys.add(f.key());
chmodSync(join(f.directory, 'build/input'), 0o755);
keys.add(f.key());
symlinkSync('input', join(f.directory, 'build/link'));
keys.add(f.key());
rmSync(join(f.directory, 'build/link'));
symlinkSync('Dockerfile', join(f.directory, 'build/link'));
keys.add(f.key());
assert.equal(keys.size, 8);
mkdirSync(join(f.directory, 'pkgbuilds/example'), { recursive: true });
const key = f.key();
writeFileSync(join(f.directory, 'pkgbuilds/example/PKGBUILD'), 'pkgver=2\n');
assert.equal(f.key(), key, 'package changes must not invalidate the build environment');
});
test('fresh builds refresh package layers and record their compatibility key', t => {
const f = fixture(t);
const key = f.key('--arch', 'aarch64');
const result = f.run(['build', '--arch', 'aarch64', '--tag', 'candidate:test', '--fresh']);
assert.equal(result.status, 0, result.stderr);
const build = f.calls().find(args => args[0] === 'buildx');
assert.ok(build.includes('--no-cache'));
assert.ok(build.includes('--pull'));
assert.ok(build.includes('--load'));
assert.ok(build.includes('--platform=linux/arm64'));
assert.ok(build.includes(`org.omarchy.builder.key=${key}`));
assert.ok(build.includes('candidate:test'));
});
test('invalid targets fail before starting an image build', t => {
const f = fixture(t);
for (const args of [['key', '--arch', 'invalid'], ['build', '--mirror', 'invalid'], ['key', '--fresh']]) {
assert.notEqual(f.run(args).status, 0);
}
});
function publish(f, extraEnv = {}) {
const script = workflow.split(' - name: Publish tested image\n')[1].split(' run: |\n')[1]
.replaceAll('${{ matrix.arch }}', 'x86_64');
return spawnSync('bash', ['-e', '-o', 'pipefail', '-c', script], {
cwd: f.directory, encoding: 'utf8', env: {
...f.env, REGISTRY_IMAGE: 'ghcr.io/omacom/omarchy-pkg-builder', CANDIDATE_IMAGE: 'candidate:test',
GH_TOKEN: 'fixture', GH_ACTOR: 'fixture', DOCKER_CONFIG: join(f.directory, 'auth'),
RUNNER_TEMP: f.directory, GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '1',
GITHUB_STEP_SUMMARY: join(f.directory, 'summary'), ...extraEnv,
},
});
}
test('a public tested image gets a version tag before the compatible-image tag advances', t => {
const f = fixture(t);
const key = f.key();
const result = publish(f);
assert.equal(result.status, 0, result.stderr);
const calls = f.calls();
assert.deepEqual(calls.filter(args => args[0] === 'push').map(args => args[1]), [
`ghcr.io/omacom/omarchy-pkg-builder:${key}-123-1`, `ghcr.io/omacom/omarchy-pkg-builder:${key}`,
]);
assert.ok(calls.findIndex(args => args[0] === 'manifest') < calls.findLastIndex(args => args[0] === 'push'));
});
test('a private image or failed push never replaces the previous compatible-image tag', t => {
for (const extraEnv of [{ PRIVATE_IMAGE: '1' }, { PUSH_FAIL: '1' }]) {
const f = fixture(t);
const key = f.key();
const result = publish(f, extraEnv);
assert.notEqual(result.status, 0);
assert.equal(f.calls().some(args => args[0] === 'push' && args[1] === `ghcr.io/omacom/omarchy-pkg-builder:${key}`), false);
}
});
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Self-test for ci/controller.sh: every decision, no cloud.
#
# The controller's two API functions are overridden with canned responses and
# a recorder, then each scenario asserts which creates and deletes it issued.
set -euo pipefail
ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
export REPO=o/r DIGITALOCEAN_TOKEN=x GITHUB_TOKEN=x
export CLOUD_INIT="$ROOT/ci/runner-cloud-init.yaml" LOCK=/tmp/controller-test.lock
CONTROLLER_LIBRARY_ONLY=1 source "$ROOT/ci/controller.sh"
# Calls are recorded to a file: the controller invokes the API functions
# inside command substitutions, and a subshell cannot append to an array.
CALLS_FILE=$(mktemp); trap 'rm -f "$CALLS_FILE"' EXIT
NOW=$(date -u +%FT%TZ)
OLD=$(date -u -d '5 hours ago' +%FT%TZ)
# Scenario state: DROPLETS is "id status created" lines, QUEUED a count,
# BUSY a count.
do_api() {
local path=$1; shift
echo "do $path $*" >>"$CALLS_FILE"
case "$path" in
droplets\?*) printf '%s\n' "$DROPLETS" | jq -Rs '{droplets: [split("\n")[] | select(length>0) | split(" ") | {id: .[0]|tonumber, status: .[1], created_at: .[2]}]}' ;;
droplets) echo '{"droplet":{"id":999}}' ;;
droplets/*) echo '{}' ;;
esac
}
gh_api() {
local path=$1; shift
echo "gh $path $*" >>"$CALLS_FILE"
case "$path" in
*/actions/runs\?*) jq -nc --argjson n "$QUEUED" '{workflow_runs: [range($n) | {id: .}]}' ;;
*/actions/runs/*/jobs) echo '{"jobs":[{"id":1,"status":"queued","labels":["self-hosted","omarchy-builder"]}]}' ;;
*/actions/runners\?*) jq -nc --argjson n "$BUSY" '{runners: [range($n) | {busy: true, labels: [{name: "omarchy-builder"}]}]}' ;;
*/registration-token) echo '{"token":"T"}' ;;
esac
}
creates() { grep -c '^do droplets -X POST' "$CALLS_FILE" || true; }
deletes() { grep -c '^do droplets/.* -X DELETE' "$CALLS_FILE" || true; }
run() { : >"$CALLS_FILE"; controller_tick >/dev/null; }
check() { # check <name> <expected creates> <expected deletes>
local c d; c=$(creates); d=$(deletes)
if [[ "$c" == "$2" && "$d" == "$3" ]]; then echo "PASS: $1"; else echo "FAIL: $1 (creates=$c want $2, deletes=$d want $3)"; cat "$CALLS_FILE"; exit 1; fi
}
DROPLETS="" QUEUED=0 BUSY=0; run; check "idle: nothing queued, nothing to reap" 0 0
DROPLETS="" QUEUED=2 BUSY=0; run; check "two queued, none live: create two" 2 0
DROPLETS="1 active $NOW" QUEUED=1 BUSY=1; run; check "one queued, one live but busy: create one" 1 0
DROPLETS="1 active $NOW" QUEUED=1 BUSY=0; run; check "one queued, one live and idle: it will take it" 0 0
DROPLETS="1 off $NOW" QUEUED=0 BUSY=0; run; check "powered-off droplet reaped" 0 1
DROPLETS="1 active $OLD" QUEUED=0 BUSY=0; run; check "over-age droplet reaped even if active" 0 1
DROPLETS=$'1 active '"$NOW"$'\n2 active '"$NOW"$'\n3 active '"$NOW"$'\n4 active '"$NOW" QUEUED=3 BUSY=4; MAX_DROPLETS=4; run; check "at cap: no creates" 0 0
DROPLETS=$'1 active '"$NOW"$'\n2 active '"$NOW" QUEUED=5 BUSY=2; MAX_DROPLETS=3; run; check "cap limits creates to remaining room" 1 0
DROPLETS="1 off $NOW" QUEUED=1 BUSY=0; MAX_DROPLETS=4; run; check "off droplet is not capacity: reaped and replaced" 1 1
# The create body must carry the tag (reaper scope) and substituted user-data.
BODY_FILE=$(mktemp); trap 'rm -f "$CALLS_FILE" "$BODY_FILE"' EXIT
do_api() { if [[ $1 == droplets ]]; then printf '%s' "${*: -1}" >"$BODY_FILE"; echo '{"droplet":{"id":1}}'; else echo '{"droplets":[]}'; fi; }
gh_api() { echo '{"token":"TOK"}'; }
create_droplet >/dev/null
jq -e '.tags == ["omarchy-builder"] and .size == "g5-32vcpu-64gb-50gb" and (.user_data | test("--token \"TOK\"")) and (.user_data | test("__") | not)' "$BODY_FILE" >/dev/null \
&& echo "PASS: create body carries tag, size, substituted user-data" \
|| { echo "FAIL: create body"; jq . "$BODY_FILE" | head -20; exit 1; }
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
set -euo pipefail
REPO_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
INSTALL_SCRIPT="$REPO_ROOT/pkgbuilds/dell-xps-touchpad-haptics/dell-xps-touchpad-haptics.install"
TEST_ROOT=$(mktemp -d)
trap 'rm -rf "$TEST_ROOT"' EXIT
# shellcheck source=/dev/null
source "$INSTALL_SCRIPT"
home="$TEST_ROOT/home"
config_dir="$home/.config/omarchy"
config_path="$config_dir/dell-haptic.conf"
protected_file="$TEST_ROOT/protected"
runuser_call="$TEST_ROOT/runuser-call"
chown_call="$TEST_ROOT/chown-call"
runuser_stub="$TEST_ROOT/runuser"
mkdir -p "$config_dir"
printf 'must remain unchanged\n' >"$protected_file"
ln -s "$protected_file" "$config_path"
chown() {
printf '%s\n' "$*" >>"$chown_call"
}
_ensure_user_config test-user "$home"
[[ ! -e $chown_call ]]
[[ ! -e $runuser_call ]]
[[ $(cat "$protected_file") == 'must remain unchanged' ]]
rm "$config_path"
printf '%s\n' \
'#!/bin/bash' \
'set -euo pipefail' \
'[[ $1 == --user && $2 == test-user && $3 == -- && $4 == /usr/bin/env ]]' \
'[[ $5 == "HOME=$EXPECTED_HOME" && $6 == USER=test-user && $7 == LOGNAME=test-user ]]' \
'[[ $8 == /usr/bin/dell-xps-touchpad-haptics && $9 == set && ${10} == high ]]' \
'printf "%s\n" "$*" >>"$RUNUSER_CALL"' \
'printf "INTENSITY=100\n" >"$EXPECTED_CONFIG"' >"$runuser_stub"
chmod +x "$runuser_stub"
export EXPECTED_HOME="$home"
export EXPECTED_CONFIG="$config_path"
export RUNUSER_CALL="$runuser_call"
_runuser_path="$runuser_stub"
_ensure_user_config test-user "$home"
[[ ! -e $chown_call ]]
[[ -f $config_path && ! -L $config_path ]]
[[ $(cat "$config_path") == 'INTENSITY=100' ]]
grep -q '^--user test-user -- /usr/bin/env ' "$runuser_call"
echo 'PASS: user config creation drops privileges and never chowns symlink targets'
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""Removal regression fixtures. No real systemd manager, user home or package is touched."""
import os
from pathlib import Path
import socket
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
# Characters that make systemd's shell_maybe_quote() quote a value, a copy of
# SHELL_NEED_ESCAPE, GLOB_CHARS and the rest of SHELL_NEED_QUOTES in escape.h.
SHELL_NEED_QUOTES = '"\\`$*?[]' + "'()<>|&;!"
def systemd_environment_value(value):
r"""Return VALUE as ``systemctl show-environment`` would print it.
print_variable() in systemctl-set-environment.c hands every value to
shell_maybe_quote(SHELL_ESCAPE_POSIX) quotes special values and uses
cescape_char() for control bytes.
"""
if not any(c in SHELL_NEED_QUOTES or c.isspace() or ord(c) < 0x20 or c == "\x7f"
for c in value):
return value
escapes = dict(zip("\a\b\f\n\r\t\v\\'", (r"\a", r"\b", r"\f", r"\n", r"\r", r"\t", r"\v", r"\\", r"\'")))
return "$'" + "".join(escapes.get(c, f"\\{ord(c):03o}" if ord(c) < 0x20 or c == "\x7f" else c)
for c in value) + "'"
class Removal(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="oma-removal-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.home = self.root / "home"
self.runtime = self.root / "runtime"
self.units = self.home / ".config/systemd/user"
self.units.mkdir(parents=True)
self.runtime.mkdir()
self.bin = self.root / "bin"
self.bin.mkdir()
self.log = self.root / "calls"
self.env = dict(os.environ, PATH=f"{self.bin}:{os.environ['PATH']}", CALLS=str(self.log))
self.executable("systemctl", '''#!/bin/bash
printf '%s\\n' "$*" >> "$CALLS"
case "$*" in
*show-environment*) [[ -z ${MANAGER_FAIL-} ]] || exit 1
# print_variable() prints every value the way a shell would read it, so the
# fixture, not this stub, decides how the value is quoted.
echo "XDG_CONFIG_HOME=${CONFIG_HOME_RAW-${CONFIG_HOME-}}" ;;
*property=ExecStart*) echo "${EFFECTIVE-}" ;;
*property=LoadState*) echo "${LOAD_STATE-loaded}" ;;
# A unit that failed stays failed after it is stopped, and systemd refuses
# reset-failed for every other state, reporting the unit as not loaded.
*property=ActiveState*) echo "${ACTIVE_STATE-inactive}" ;;
*" reset-failed "*) [[ ${ACTIVE_STATE-inactive} == failed && -z ${RESET_FAIL-} ]] || {
printf 'Failed to reset failed state of unit: Unit is not loaded.\n' >&2; exit 1; } ;;
*" stop "*) [[ -z ${STOP_FAIL-} ]] || exit 1 ;;
esac
''')
def executable(self, name, source):
path = self.bin / name
path.write_text(source)
path.chmod(0o755)
def online(self):
sock = socket.socket(socket.AF_UNIX)
sock.bind(str(self.runtime / "bus"))
self.addCleanup(sock.close)
def install(self, app, binary=None):
unit = self.units / f"{app}.service"
unit.write_text(f'[Service]\nExecStart="{binary or "/usr/bin/" + app}" --config "{self.home}/config.toml" daemon\n')
target = self.units / "graphical-session.target.wants"
target.mkdir(exist_ok=True)
link = target / unit.name
link.symlink_to(f"../{unit.name}")
return unit, link
def run_remove(self, app):
self.log.unlink(missing_ok=True) # Every run is judged on its own calls.
return subprocess.run(["bash", str(ROOT / f"pkgbuilds/{app}-bin/package-remove"),
"--user", app, str(self.home), str(self.runtime)],
env=self.env, text=True, capture_output=True)
def test_logged_out_users_and_data_preservation(self):
for app in ("omawake", "omaspeak"):
unit, link = self.install(app)
config = self.home / f"{app}.toml"
config.write_text("keep settings and models")
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
self.assertEqual(config.read_text(), "keep settings and models")
self.assertFalse(self.log.exists(), "offline cleanup contacted systemd")
def test_active_unit_is_stopped_before_removing_it(self):
self.online()
for app in ("omawake", "omaspeak"):
unit, link = self.install(app)
self.env["EFFECTIVE"] = f"{{ path=/usr/bin/{app} ; argv[]=/usr/bin/{app} daemon ; }}"
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
calls = self.log.read_text().splitlines()
self.assertLess(calls.index(f"--user stop {app}.service"), calls.index(f"--user disable {app}.service"))
self.assertEqual(calls[-1], "--user daemon-reload")
def test_failed_stop_prevents_unit_deletion_and_fails_hook(self):
self.online()
unit, link = self.install("omawake")
self.env.update(STOP_FAIL="1", EFFECTIVE="{ path=/usr/bin/omawake ; }")
result = self.run_remove("omawake")
self.assertNotEqual(result.returncode, 0)
self.assertTrue(unit.exists())
self.assertTrue(link.is_symlink())
self.assertNotIn("disable", self.log.read_text())
def test_reset_failed_is_requested_only_for_a_unit_that_failed(self):
self.online()
for app in ("omawake", "omaspeak"):
unit, link = self.install(app)
self.env["EFFECTIVE"] = f"{{ path=/usr/bin/{app} ; argv[]=/usr/bin/{app} daemon ; }}"
# A loaded unit that never failed is not failed, and asking systemd to
# reset it fails with "Unit is not loaded": that must not abort removal.
self.env["ACTIVE_STATE"] = "active"
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
calls = [call for call in self.log.read_text().splitlines() if call.startswith("--user")]
self.assertNotIn(f"--user reset-failed {app}.service", calls)
# Reading the manager's state is welcome; the only changes asked for
# are the stop, the disable and the reload that follow them.
self.assertEqual([call for call in calls
if "show-environment" not in call and "--property=" not in call],
[f"--user stop {app}.service",
f"--user disable {app}.service", "--user daemon-reload"])
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
# A unit that failed does keep that state once stopped, and there the
# reset belongs between stopping the service and disabling the unit.
unit, link = self.install(app)
self.env["ACTIVE_STATE"] = "failed"
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
calls = self.log.read_text().splitlines()
reset = f"--user reset-failed {app}.service"
self.assertIn(reset, calls)
self.assertLess(calls.index(f"--user stop {app}.service"), calls.index(reset))
self.assertLess(calls.index(reset), calls.index(f"--user disable {app}.service"))
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
del self.env["ACTIVE_STATE"]
def test_reset_refused_by_a_healthy_manager_still_fails_the_transaction(self):
self.online()
unit, link = self.install("omaspeak")
self.env.update(EFFECTIVE="{ path=/usr/bin/omaspeak ; }",
ACTIVE_STATE="failed", RESET_FAIL="1")
result = self.run_remove("omaspeak")
self.assertNotEqual(result.returncode, 0)
self.assertTrue(unit.exists())
self.assertTrue(link.is_symlink())
def test_custom_build_and_mask_are_preserved(self):
unit, link = self.install("omawake", "/home/user/dev/omawake")
self.assertEqual(self.run_remove("omawake").returncode, 0)
self.assertTrue(unit.exists())
self.assertTrue(link.is_symlink())
unit.unlink()
unit.symlink_to("/dev/null")
self.assertEqual(self.run_remove("omawake").returncode, 0)
self.assertTrue(unit.is_symlink())
self.assertFalse(self.log.exists())
def test_effective_override_and_missing_online_unit(self):
self.online()
unit, _ = self.install("omaspeak")
self.env["EFFECTIVE"] = "{ path=/home/user/development/omaspeak ; }"
self.assertEqual(self.run_remove("omaspeak").returncode, 0)
self.assertTrue(unit.exists())
self.assertNotIn(" stop ", self.log.read_text())
unit.unlink()
self.env.update(EFFECTIVE="", LOAD_STATE="not-found")
self.assertEqual(self.run_remove("omaspeak").returncode, 0)
self.assertNotIn(" stop ", self.log.read_text())
def test_manager_config_home_and_unavailable_manager(self):
self.online()
default = self.units
self.units = self.home / "custom-config/systemd/user"
self.units.mkdir(parents=True)
unit, link = self.install("omawake")
self.env.update(CONFIG_HOME=str(self.home / "custom-config"), MANAGER_FAIL="1")
self.assertNotEqual(self.run_remove("omawake").returncode, 0)
self.assertTrue(unit.exists())
del self.env["MANAGER_FAIL"]
self.assertEqual(self.run_remove("omawake").returncode, 0)
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
self.assertTrue(default.exists())
def test_root_dispatch_drops_privileges_and_propagates_failure(self):
passwd = f"fixture:x:12345:12345::{self.home}:/bin/bash"
self.executable("getent", f"#!/bin/sh\nprintf '%s\\n' '{passwd}'\n")
self.executable("runuser", '#!/bin/sh\nprintf "%s\\n" "$*" >> "$CALLS"\nexit 1\n')
result = subprocess.run(["bash", str(ROOT / "pkgbuilds/omawake-bin/package-remove"), "omawake"], env=self.env, capture_output=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("-u fixture -- env", self.log.read_text())
self.assertIn("--user omawake", self.log.read_text())
def test_offline_executable_overrides_are_preserved(self):
for app in ("omawake", "omaspeak"):
for spacing in ("ExecStart=\nExecStart={command}",
# systemd's parser throws the whitespace around an
# assignment away, so both of these spellings still
# name a development build, exactly as the first does.
"ExecStart =\nExecStart = {command}",
"\tExecStart\t=\t{command}"):
unit, link = self.install(app)
dropins = Path(str(unit) + ".d")
dropins.mkdir(exist_ok=True)
(dropins / "override.conf").write_text("[Service]\n" + spacing.format(
command=f"/home/user/build/{app} daemon") + "\n")
try:
with self.subTest(app=app, spacing=spacing):
self.assertEqual(self.run_remove(app).returncode, 0)
self.assertTrue(unit.exists(), "removed a service with an override")
self.assertTrue(link.is_symlink(), "unlinked a service with an override")
finally:
unit.unlink(missing_ok=True)
link.unlink(missing_ok=True)
self.assertFalse(self.log.exists(), "offline cleanup contacted systemd")
def test_shell_quoted_manager_config_home_is_resolved(self):
self.online()
default_units = self.units
for app in ("omawake", "omaspeak"):
config_home = self.home / f"{app} custom's \\ config"
self.units = config_home / "systemd/user"
self.units.mkdir(parents=True)
unit, link = self.install(app)
# A unit in the directory the manager reads nothing from is no unit of
# the manager's, and the helper has no business reaching for it.
stray = default_units / f"{app}.service"
stray.write_text(f'[Service]\nExecStart="/usr/bin/{app}" daemon\n')
printed = self.env["CONFIG_HOME_RAW"] = systemd_environment_value(str(config_home))
with self.subTest(app=app, printed=printed):
self.assertTrue(printed.startswith("$'") and printed.endswith("'"),
"a path of spaces is not what a plain value looks like")
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
self.assertTrue(stray.is_file(), "guessed at a directory no manager reads")
del self.env["CONFIG_HOME_RAW"]
def test_control_character_manager_config_home_is_resolved(self):
self.online()
for app in ("omawake", "omaspeak"):
for suffix in ("tab\tpath", "newline\npath\n", "\a\b\f\r\v", "\x01\x1b\x7f"):
config_home = self.home / (app + suffix)
self.units = config_home / "systemd/user"
self.units.mkdir(parents=True)
unit, link = self.install(app)
self.env["CONFIG_HOME_RAW"] = systemd_environment_value(str(config_home))
with self.subTest(app=app, suffix=suffix):
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
def test_unreadable_manager_config_home_aborts_the_cleanup(self):
self.online()
for app in ("omawake", "omaspeak"):
unit, link = self.install(app)
# Truncated output must not make cleanup guess at a directory.
self.env["CONFIG_HOME_RAW"] = "$'" + str(self.home / f"broken {app} config")
with self.subTest(app=app):
result = self.run_remove(app)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(unit.exists())
self.assertTrue(link.is_symlink())
self.assertNotIn(" stop ", self.log.read_text())
del self.env["CONFIG_HOME_RAW"]
def test_relative_manager_config_home_keeps_the_default_directory(self):
self.online()
for app in ("omawake", "omaspeak"):
unit, link = self.install(app)
# An XDG_CONFIG_HOME that is not absolute is no setting at all: the
# manager itself reads the home's .config directory then.
self.env["CONFIG_HOME_RAW"] = systemd_environment_value(f"relative {app} config")
with self.subTest(app=app):
result = self.run_remove(app)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(unit.exists())
self.assertFalse(link.is_symlink())
del self.env["CONFIG_HOME_RAW"]
def test_packaging_installs_hooks_and_helpers(self):
import shutil
for app, version in (("omawake", "0.0.3"), ("omaspeak", "0.0.3")):
source = self.root / app / "src"
package = self.root / app / "pkg"
release = source / f"{app}-{version}-linux-x86_64"
release.mkdir(parents=True)
for path in [app, "lib/libaudiocpp.so.0.1.0", f"packaging/systemd/{app}.service",
"README.md", "INSTALL.md", "ACCELERATOR_SETUP.md", "CHANGELOG.md",
"RELEASE_NOTES.md", "DEMO.md", "RUNTIME.md", "config.example.toml",
"licenses/LICENSE", "assets/fixture", "benchmarks/fixture"]:
target = release / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("fixture")
directory = ROOT / f"pkgbuilds/{app}-bin"
for name in ("package-remove", "remove-user-services.hook"):
shutil.copyfile(directory / name, source / name)
env = dict(self.env, srcdir=str(source), pkgdir=str(package), CARCH="x86_64")
result = subprocess.run(["bash", "-c", 'source "$1"; package', "package-fixture", str(directory / "PKGBUILD")], env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
helper = package / f"usr/lib/{app}/package-remove"
self.assertEqual(helper.read_bytes(), (directory / "package-remove").read_bytes())
self.assertEqual(helper.stat().st_mode & 0o777, 0o755)
self.assertTrue((package / f"usr/share/libalpm/hooks/30-{app}-remove-user-services.hook").exists())
def test_hook_contract_and_package_release(self):
scripts = []
for app in ("omawake", "omaspeak"):
directory = ROOT / f"pkgbuilds/{app}-bin"
hook = (directory / "remove-user-services.hook").read_text()
self.assertIn("Operation = Remove", hook)
self.assertNotIn("Operation = Upgrade", hook)
self.assertIn("When = PreTransaction", hook)
self.assertIn("AbortOnFail", hook)
self.assertIn(f"Exec = /usr/lib/{app}/package-remove {app}", hook)
self.assertIn("pkgrel=4", (directory / "PKGBUILD").read_text())
scripts.append((directory / "package-remove").read_bytes())
self.assertEqual(*scripts)
if __name__ == "__main__":
unittest.main()
+314
View File
@@ -0,0 +1,314 @@
const assert = require('node:assert/strict');
const { readFileSync, mkdtempSync, rmSync } = require('node:fs');
const { join } = require('node:path');
const { tmpdir } = require('node:os');
const { test } = require('node:test');
const { execFileSync, spawnSync } = require('node:child_process');
const approve = require('../.github/scripts/approve-pr-workflows.cjs');
const BUILD = '.github/workflows/build-pr.yml';
const TESTS = '.github/workflows/test.yml';
const time = '2026-09-19T02:47:52Z';
const earlier = '2026-09-19T02:36:04Z';
const pr = {
number: 390, state: 'open', updated_at: time,
head: { sha: 'reviewed-sha', ref: 'ghost', repo: { id: 42 } },
labels: [{ name: 'build-approved' }],
};
const clone = value => structuredClone(value);
const run = (id, path, overrides = {}) => ({
id, path, event: 'pull_request', head_sha: pr.head.sha,
head_repository: { id: 42 }, head_branch: 'ghost', pull_requests: [],
status: 'completed', conclusion: 'action_required', created_at: time,
...overrides,
});
function fixture(initial = [run(1, TESTS, { created_at: earlier }), run(2, BUILD)], options = {}) {
const state = { pr: clone(pr), runs: clone(initial), approved: [], reads: 0, tick: 0, transitions: [] };
const repo = { owner: 'omacom', repo: 'omarchy-pkgs' };
const github = {
rest: {
pulls: { get: async args => {
assert.deepEqual(args, { ...repo, pull_number: 390 });
state.reads++;
options.onRead?.(state);
return { data: clone(state.pr) };
} },
actions: {
listWorkflowRunsForRepo() {},
getWorkflowRun: async ({ run_id }) => {
const current = state.runs.find(run => run.id === run_id);
state.transitions.push([run_id, current.status]);
return { data: clone(current) };
},
approveWorkflowRun: async args => {
assert.deepEqual(args, { ...repo, run_id: args.run_id });
options.onApprove?.(state, args.run_id);
const current = state.runs.find(run => run.id === args.run_id);
assert.equal(current.conclusion, 'action_required');
state.approved.push(current.id);
current.status = 'queued';
current.conclusion = null;
},
},
},
paginate: async (method, args) => {
assert.equal(method, github.rest.actions.listWorkflowRunsForRepo);
assert.deepEqual(args, { ...repo, event: 'pull_request', head_sha: pr.head.sha, per_page: 100 });
return clone(state.runs).reverse(); // GitHub returns newest first.
},
};
const invoke = overrides => approve({
github, context: { repo, payload: { action: 'labeled', pull_request: clone(pr) } },
core: { info() {} }, vouchStatus: 'unknown', attempts: 6,
sleep: async () => {
state.tick++;
for (const current of state.runs) {
if (current.status === 'queued' && state.tick >= (options.queueUntil ?? 1)) current.status = 'in_progress';
}
options.onSleep?.(state);
},
...overrides,
});
return { state, invoke, github };
}
test('an unvouched, labeled fork PR releases both required workflows', async () => {
const { state, invoke } = fixture();
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('waits for the label-triggered build instead of stopping at the old build', async () => {
const { state, invoke } = fixture([
run(1, BUILD, { created_at: earlier }), run(2, TESTS, { created_at: earlier }),
], {
onSleep(state) {
if (state.tick === 2) {
assert.deepEqual(state.approved, []);
state.runs.push(run(3, BUILD));
}
},
queueUntil: 4,
onApprove(state, id) {
if (id === 3) assert.equal(state.runs.find(run => run.id === 1).status, 'in_progress');
},
});
await invoke({ attempts: 10 });
assert.deepEqual(state.approved, [1, 2, 3]);
assert.ok(state.transitions.some(([id, status]) => id === 1 && status === 'queued'));
});
test('approves only the two known workflows for this fork, branch, PR and SHA', async () => {
const unrelated = [
{ path: '.github/workflows/publish.yml' }, { event: 'push' },
{ head_sha: 'other-sha' }, { head_repository: { id: 99 } },
{ head_branch: 'other-branch' }, { pull_requests: [{ number: 391 }] },
].map((overrides, i) => run(10 + i, BUILD, overrides));
const { state, invoke } = fixture([run(1, TESTS), run(2, BUILD), ...unrelated]);
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('accepts a run explicitly associated with this PR', async () => {
const { state, invoke } = fixture([
run(1, TESTS), run(2, BUILD, { pull_requests: [{ number: 390 }] }),
]);
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('does not restart running or completed workflows', async () => {
const { state, invoke } = fixture([
run(1, TESTS, { conclusion: 'success' }),
run(2, BUILD, { status: 'in_progress', conclusion: null }),
]);
await invoke();
assert.deepEqual(state.approved, []);
});
test('an obsolete build hold cannot cancel a newer build that was already released', async () => {
for (const current of [
{ status: 'queued', conclusion: null }, { status: 'in_progress', conclusion: null },
{ status: 'completed', conclusion: 'success' }, { status: 'completed', conclusion: 'failure' },
]) {
const { state, invoke } = fixture([
run(1, BUILD, { created_at: earlier }), run(2, TESTS), run(3, BUILD, current),
]);
await invoke();
assert.deepEqual(state.approved, [2]);
}
});
for (const status of ['denounced', '', undefined, 'unexpected']) {
test(`vouch status ${String(status)} fails closed`, async () => {
const { state, invoke } = fixture();
await assert.rejects(invoke({ vouchStatus: status }), /Cannot approve workflows/);
assert.deepEqual(state.approved, []);
});
}
for (const status of ['bot', 'collaborator', 'vouched']) {
test(`a labeled ${status} can also clear GitHub's approval gate`, async () => {
const { state, invoke } = fixture();
await invoke({ vouchStatus: status });
assert.deepEqual(state.approved, [1, 2]);
});
}
for (const [name, change] of [
['removed label', pr => { pr.labels = []; }],
['changed head', pr => { pr.head.sha = 'new-sha'; }],
['closed PR', pr => { pr.state = 'closed'; }],
]) {
test(`${name} stops approval, including changes immediately before a write`, async () => {
for (const read of [1, 2]) {
const { state, invoke } = fixture(undefined, { onRead(state) {
if (state.reads === read) change(state.pr);
} });
await invoke();
assert.deepEqual(state.approved, []);
}
});
}
test('revocation between approvals prevents releasing further workflows', async () => {
const { state, invoke } = fixture(undefined, { onSleep(state) { state.pr.labels = []; } });
await invoke();
assert.deepEqual(state.approved, [1]);
});
test('a delayed tests workflow is also awaited', async () => {
const { state, invoke } = fixture([run(2, BUILD)], {
onSleep(state) { if (state.tick === 2) state.runs.push(run(1, TESTS)); },
});
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('reopening a labeled PR waits for its new tests, even if old tests passed at the same SHA', async () => {
const { state, invoke } = fixture([
run(1, TESTS, { created_at: earlier, conclusion: 'success' }), run(2, BUILD),
], { onSleep(state) { if (state.tick === 2) state.runs.push(run(3, TESTS)); } });
await invoke({ context: { repo: { owner: 'omacom', repo: 'omarchy-pkgs' },
payload: { action: 'reopened', pull_request: clone(pr) } } });
assert.deepEqual(state.approved, [2, 3]);
});
test('missing current runs time out without approving stale builds', async () => {
const { state, invoke } = fixture([run(1, TESTS), run(2, BUILD, { created_at: earlier })]);
await assert.rejects(invoke(), /Timed out/);
assert.deepEqual(state.approved, []);
});
test('API failure is reported rather than silently treated as approval', async () => {
const { state, invoke } = fixture(undefined, { onApprove() { throw new Error('Forbidden'); } });
await assert.rejects(invoke(), /Forbidden/);
assert.deepEqual(state.approved, []);
});
// Execute the actual build workflow's approval script and shell gate. This
// covers the stale event payload that originally accompanied held PR runs.
const workflow = readFileSync(join(__dirname, '../.github/workflows/build-pr.yml'), 'utf8');
const approvalScript = workflow.match(/- id: approval[\s\S]*?script: \|\n([\s\S]*?)(?= # One matrix)/)[1]
.split('\n').map(line => line.replace(/^ /, '')).join('\n');
const gateScript = workflow.match(/ case "\$STATUS" in[\s\S]*? esac/)[0] + '\nprintf "%s" "$trusted"';
test('the build reads the live label rather than its pre-label event payload', async () => {
const execute = new (Object.getPrototypeOf(async function () {}).constructor)('github', 'context', 'core', approvalScript);
for (const [current, approved] of [
[pr, true], [{ ...pr, labels: [] }, false],
[{ ...pr, head: { ...pr.head, sha: 'new-sha' } }, false],
[{ ...pr, state: 'closed' }, false],
]) {
const outputs = {};
await execute({ rest: { pulls: { get: async () => ({ data: current }) } } },
{ repo: {}, payload: { pull_request: { ...pr, labels: [] } } },
{ setOutput: (key, value) => { outputs[key] = value; } });
assert.equal(outputs.approved, approved);
}
});
test('the build gate permits a missing vouch only with approval, never a denouncement or lookup failure', () => {
for (const [status, approved, expected] of [
['unknown', 'true', 'true'], ['unknown', 'false', 'false'],
['denounced', 'true', 'false'], ['', 'true', 'false'], ['unexpected', 'true', 'false'],
['vouched', 'false', 'true'], ['collaborator', 'false', 'true'],
['bot', 'false', 'true'], ['dispatch', 'false', 'true'],
]) {
assert.equal(execFileSync('bash', ['-c', gateScript], {
env: { ...process.env, STATUS: status, APPROVED: approved }, encoding: 'utf8',
}), expected);
}
});
// Exercise the actual reporting job, including its GitHub check name: a
// successful/skipped check called "result" would accidentally allow merging
// a PR whose build never ran. GitHub keeps a missing required check pending.
const resultJob = workflow.slice(workflow.indexOf('\n result:\n'));
const resultName = resultJob.match(/^ name: (.+)$/m)[1];
const resultScript = resultJob.split(' - run: |\n')[1];
function report({ trusted = 'false', vouch = 'unknown', empty = 'false', changes = 'success', build = 'skipped' } = {}) {
const needs = {
changes: { result: changes, outputs: { trusted, vouch_status: vouch, empty } },
build: { result: build },
};
// The reporting expressions use &&, || and string equality, with the
// same semantics in JavaScript and Actions for these string-only fixtures.
const render = text => text.replace(/\$\{\{(.*?)\}\}/g, (_, expression) =>
new Function('needs', `return (${expression})`)(needs));
const directory = mkdtempSync(join(tmpdir(), 'build-approval-report-'));
const summaryPath = join(directory, 'summary');
try {
const result = spawnSync('bash', ['-e', '-c', render(resultScript)], {
env: { ...process.env, GITHUB_STEP_SUMMARY: summaryPath }, encoding: 'utf8',
});
return { name: render(resultName), ...result,
summary: result.stdout.includes('::notice::') ? readFileSync(summaryPath, 'utf8') : '' };
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
test('an unvouched PR waits without publishing a passing or failing required result', () => {
const result = report();
assert.equal(result.name, 'Awaiting build approval');
assert.equal(result.status, 0);
assert.match(result.stdout, /::notice::Awaiting maintainer build approval/);
assert.doesNotMatch(result.stdout, /::error::/);
assert.match(result.summary, /required \*\*result\*\* check remains pending/);
});
test('applying build-approved transitions the waiting PR to the required build result', () => {
assert.notEqual(report().name, 'result');
const approved = report({ trusted: 'true', build: 'success' });
assert.equal(approved.name, 'result');
assert.equal(approved.status, 0);
const failed = report({ trusted: 'true', build: 'failure' });
assert.equal(failed.name, 'result');
assert.notEqual(failed.status, 0);
});
test('trusted tooling-only PRs still satisfy the required result without a package build', () => {
const result = report({ trusted: 'true', vouch: 'vouched' });
assert.equal(result.name, 'result');
assert.equal(result.status, 0);
});
for (const [name, overrides] of [
['denounced author', { vouch: 'denounced' }],
['failed trust lookup', { vouch: '', changes: 'failure' }],
['missing trust result', { vouch: '' }],
['missing gate output', { trusted: '' }],
['failed planning', { changes: 'failure' }],
['cancelled planning', { changes: 'cancelled' }],
['empty PR', { empty: 'true' }],
['cancelled build', { trusted: 'true', build: 'cancelled' }],
]) {
test(`${name} fails the required result instead of masquerading as pending approval`, () => {
const result = report(overrides);
assert.equal(result.name, 'result');
assert.notEqual(result.status, 0);
assert.doesNotMatch(result.stdout, /::notice::Awaiting maintainer build approval/);
});
}
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Self-test for bin/publish-artifact against a local directory as the remote.
# Needs repo-add, gpg, rclone, bsdtar (run in the Arch builder/test container).
set -euo pipefail
ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
T=$(mktemp -d); chmod 755 "$T"; trap 'rm -rf "$T"' EXIT
REMOTE="$T/r2"; mkdir -p "$REMOTE"
# throwaway signing key
export GNUPGHOME="$T/g"; mkdir -m700 "$GNUPGHOME"
gpg --batch --quiet --passphrase '' --quick-gen-key 'Test <t@t>' ed25519 sign 0 2>/dev/null
export GPG_PRIVATE_KEY=$(gpg --batch --armor --export-secret-keys 'Test <t@t>') GPG_PASSPHRASE=''
unset GNUPGHOME
# minimal real packages via makepkg
mkpkg() { # mkpkg <name> <pkgrel> <arch> [payload]
local d="$T/src/$1-$2${4:+-$4}"; mkdir -p "$d"; cd "$d"
printf 'pkgname=%s\npkgver=1.0\npkgrel=%s\narch=(%s)\npackage(){ install -Dm644 /dev/null "$pkgdir/usr/share/%s-%s"; echo "%s" > "$pkgdir/usr/share/%s-%s"; }\n' "$1" "$2" "$3" "$1" "$2" "${4:-payload}" "$1" "$2" > PKGBUILD
# CARCH so the PKGINFO records the requested arch (--ignorearch would
# stamp the host's).
# makepkg refuses to run as root (the CI test container does); build the
# fixture as an unprivileged user in that case.
if (( EUID == 0 )); then
id -u fixture >/dev/null 2>&1 || useradd -m fixture
chmod 755 "$T/src"; chown -R fixture "$d"
runuser -u fixture -- env CARCH=$3 makepkg -f --nodeps --ignorearch >/dev/null 2>&1
else
CARCH=$3 makepkg -f --nodeps --ignorearch >/dev/null 2>&1
fi
ls "$d"/*.pkg.tar.zst
}
A1=$(mkpkg alpha 1 any); A2=$(mkpkg alpha 2 any); B1=$(mkpkg beta 1 x86_64); C1=$(mkpkg gamma 1 aarch64)
pub() { "$ROOT/bin/publish-artifact" --remote "$REMOTE" --mirror edge --arch x86_64 "$@" >"$T/out" 2>&1; }
entries() { tar -tf "$REMOTE/edge/x86_64/omarchy.db.tar.zst" | grep '/$' | sort | tr '\n' ' '; }
pass() { echo "PASS: $1"; }
fail() { echo "FAIL: $1"; cat "$T/out"; exit 1; }
pub "$A1" && [[ "$(entries)" == "alpha-1.0-1/ " ]] && [[ -f "$REMOTE/edge/x86_64/$(basename "$A1").sig" ]] \
&& pass "first publish creates db with one entry and a signature" || fail "first publish"
sum_before=$(sha256sum "$REMOTE/edge/x86_64/$(basename "$A1")")
pub "$B1" && [[ "$(entries)" == "alpha-1.0-1/ beta-1.0-1/ " ]] && [[ "$(sha256sum "$REMOTE/edge/x86_64/$(basename "$A1")")" == "$sum_before" ]] \
&& pass "second package added incrementally; first file untouched" || fail "incremental add"
pub "$A2" && [[ "$(entries)" == "alpha-1.0-2/ beta-1.0-1/ " ]] && [[ -f "$REMOTE/edge/x86_64/$(basename "$A1")" ]] \
&& pass "new pkgrel replaces the db entry, old file remains on remote" || fail "replace entry"
# Same bytes again: allowed, idempotent (this is how a fast-ring artifact
# reaches rc and stable after edge, and how a re-run recovers).
pub "$A2" && grep -q 'identical bytes' "$T/out" && [[ "$(entries)" == "alpha-1.0-2/ beta-1.0-1/ " ]] \
&& pass "identical bytes under an existing name: accepted, db unchanged" || fail "identical republish"
# Orphan repair: a file that reached the remote but whose db entry was lost
# (a concurrent publish overwrote the db) is fixed by publishing it again.
( cd "$REMOTE/edge/x86_64" && repo-remove --quiet omarchy.db.tar.zst alpha >/dev/null 2>&1 )
[[ "$(entries)" == "beta-1.0-1/ " ]] || fail "fixture: could not drop alpha from the db"
pub "$A2" && [[ "$(entries)" == "alpha-1.0-2/ beta-1.0-1/ " ]] \
&& pass "orphaned file regains its db entry on republish" || fail "orphan repair"
# Different bytes under an existing name: refused. Build alpha-2 again with
# a different payload (makepkg is reproducible, so the content must change).
A2b=$(mkpkg alpha 2 any different-payload)
[[ "$(md5sum < "$A2")" != "$(md5sum < "$A2b")" ]] || { echo "fixture: rebuilt package is byte-identical, cannot test"; exit 1; }
if pub "$A2b"; then fail "different bytes under same filename should refuse"; else grep -q 'DIFFERENT bytes' "$T/out" && pass "different bytes under an existing name refused" || fail "wrong refusal reason"; fi
if pub "$C1"; then fail "aarch64 package into x86_64 should refuse"; else grep -q 'publishing to x86_64' "$T/out" && pass "wrong-arch package refused" || fail "wrong-arch reason"; fi
cp "$B1" "$T/renamed-1.0-1-x86_64.pkg.tar.zst"
if pub "$T/renamed-1.0-1-x86_64.pkg.tar.zst"; then fail "filename/PKGINFO mismatch should refuse"; else grep -q 'does not match PKGINFO' "$T/out" && pass "filename must match PKGINFO" || fail "mismatch reason"; fi
# db must verify: pacman can read it and each package's signature checks
gpg --batch --quiet --import <<<"$GPG_PRIVATE_KEY" 2>/dev/null || true
( cd "$REMOTE/edge/x86_64" && for f in *.pkg.tar.zst; do gpg --batch --quiet --verify "$f.sig" "$f" 2>/dev/null || { echo "FAIL: signature $f"; exit 1; }; done ) && pass "all signatures verify"
+66
View File
@@ -330,5 +330,71 @@ os.execv(os.environ['REAL_GIT'], ['git', *args])
self.assertEqual(metadata_file.read_bytes(), original)
class T3CodeHookTest(unittest.TestCase):
"""Keep both desktop architectures on the same complete upstream release."""
def setUp(self):
work = tempfile.TemporaryDirectory()
self.addCleanup(work.cleanup)
self.root = Path(work.name)
self.recipe = self.root / "PKGBUILD"
self.recipe.write_text("pkgver=0.0.41\n")
self.feed = self.root / "latest-linux.yml"
self.feed.write_text("version: 0.0.42\npath: T3-Code-0.0.42-x86_64.AppImage\n")
self.arm_feed = self.root / "latest-linux-arm64.yml"
self.arm_feed.write_text("version: 0.0.42\npath: T3-Code-0.0.42-arm64.AppImage\n")
for arch in ("x86_64", "arm64"):
(self.root / f"T3-Code-0.0.42-{arch}.AppImage").write_text(arch)
# Serve only fixture assets, and record the requested release URLs.
curl = self.root / "curl"
curl.write_text('#!/bin/bash\nurl="${@: -1}"\nprintf "%s\\n" "$url" >> requests\ncat "${url##*/}"\n')
curl.chmod(0o755)
self.env = dict(os.environ, PATH=f"{self.root}:{os.environ['PATH']}")
def run_hook(self):
return subprocess.run(
['bash', str(ROOT / 'pkgbuilds/t3code-bin/.omarchy/upstream.sh')],
cwd=self.root, env=self.env, text=True, capture_output=True,
)
def test_hashes_both_architectures_from_one_release(self):
result = self.run_hook()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout), {
'pkgver': '0.0.42',
'sha256sums': {
arch: [w.hash_file(self.root / f'T3-Code-0.0.42-{asset_arch}.AppImage', 'sha256')]
for arch, asset_arch in [('x86_64', 'x86_64'), ('aarch64', 'arm64')]
},
})
self.assertIn('/download/v0.0.42/latest-linux-arm64.yml', (self.root / 'requests').read_text())
def test_current_version_does_not_download_assets(self):
self.recipe.write_text('pkgver=0.0.42\n')
result = self.run_hook()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout), {})
self.assertEqual(len((self.root / 'requests').read_text().splitlines()), 1)
def test_incomplete_or_mismatched_arm_release_reports_no_update(self):
for bad_feed in ('', 'version: 0.0.43\npath: T3-Code-0.0.42-arm64.AppImage\n',
'version: 0.0.42\npath: renamed.AppImage\n'):
with self.subTest(feed=bad_feed):
self.arm_feed.write_text(bad_feed)
result = self.run_hook()
self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.stdout, '')
self.arm_feed.unlink()
result = self.run_hook()
self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.stdout, '')
def test_missing_arm_asset_reports_no_update(self):
(self.root / 'T3-Code-0.0.42-arm64.AppImage').unlink()
result = self.run_hook()
self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.stdout, '')
if __name__ == '__main__':
unittest.main(verbosity=2)