diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1228c8..0bcfc44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ on: - "apps/headless/build.sh" - "apps/headless/build-linux.sh" - "apps/headless/install-linux.sh" + - "apps/headless/install.sh" + - "apps/headless/Tests/linux-installer.sh" - "apps/headless/Package.swift" - "apps/headless/Sources/HeadlessProtocol/ProductVersion.swift" - "apps/headless/VERSION" @@ -172,6 +174,8 @@ jobs: with: path: dist merge-multiple: true + - name: Add Linux bootstrap installer + run: cp apps/headless/install.sh dist/install.sh - name: Generate and verify checksums run: ./.github/scripts/release-checksums.sh "${{ needs.version.outputs.version }}" dist - name: Create GitHub Release @@ -188,14 +192,16 @@ jobs: | `Headless-${{ needs.version.outputs.version }}-macos.zip` | macOS (ad-hoc signed `.app`) | | `headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz` | Linux x86_64 | | `headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz` | Linux arm64 | + | `install.sh` | Verified Linux bootstrap installer | ### Notes - **macOS:** Unzip and run `Headless.app`. Gatekeeper may warn (ad-hoc signature; notarization not included yet). CLI: `Headless.app/Contents/Resources/bin/headless`. - - **Linux tarball:** Does not bundle Chromium. Install with `./install-linux.sh` after ensuring non-Snap Chromium and FFmpeg are available. The Docker production image remains the self-contained runtime (not published by this release). + - **Linux:** `curl -fsSL https://github.com/LockInTime/headless/releases/latest/download/install.sh | sh`. The installer verifies the selected tarball against `SHA256SUMS`. Chromium and FFmpeg remain system dependencies. files: | dist/Headless-${{ needs.version.outputs.version }}-macos.zip dist/headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz dist/headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz dist/SHA256SUMS + dist/install.sh fail_on_unmatched_files: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 6beaf4b..de45def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Cutting that release is tracked in ### Added +- A checksum-verifying Linux bootstrap installer now selects the correct + amd64/arm64 release package and delegates to the shared runtime preflight. - Tagged releases now include a verified `SHA256SUMS` manifest covering every downloadable macOS and Linux package. - A single cross-engine conformance scenario now runs against real WKWebView diff --git a/README.md b/README.md index a74d4f5..cdb9bc4 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,22 @@ and a compatible SDK before compiling. ### Linux +```sh +curl -fsSL https://github.com/LockInTime/headless/releases/latest/download/install.sh | sh +``` + +The release bootstrap supports Linux `amd64` and `arm64`, downloads the matching +tarball, verifies it against the release's `SHA256SUMS`, rejects unexpected +archive contents, and delegates to the packaged runtime preflight. Install a +specific release or prefix with: + +```sh +curl -fsSL https://github.com/LockInTime/headless/releases/latest/download/install.sh \ + | HEADLESS_VERSION=1.1.0 sh -s -- --prefix /opt/headless +``` + +To build locally instead: + ```sh ./apps/headless/build-linux.sh apps/headless/build/linux/install-linux.sh @@ -196,7 +212,9 @@ distribution. Ubuntu's `/snap/bin/chromium` launcher resolves to Chromium's inherited DevTools pipe. If automatic detection is unsuitable, set `HEADLESS_CHROMIUM_EXECUTABLE` to an absolute, executable, non-Snap binary. Invalid overrides fail closed instead of silently selecting a different -browser. Verify the exact selection before starting the host: +browser. FFmpeg is likewise selected only from its explicit absolute override +or the runtime allow-list, never an arbitrary PATH entry. Verify the exact +browser selection before starting the host: ```sh headless runtime @@ -222,9 +240,10 @@ The tag is embedded as the product version in every binary. Verify an install with `headless --version`; wire protocol compatibility is versioned independently. See [CHANGELOG.md](CHANGELOG.md) for release history. -Assets: macOS `Headless.app` zip, Linux amd64/arm64 tarballs, and -`SHA256SUMS`. Download the manifest beside the selected package and verify it -before installing: +Assets: macOS `Headless.app` zip, Linux amd64/arm64 tarballs, the Linux +`install.sh` bootstrap, and `SHA256SUMS`. The bootstrap verifies the selected +Linux package automatically. For manual installation, download the manifest +beside the selected package and verify it before installing: ```sh sha256sum --ignore-missing -c SHA256SUMS # Linux diff --git a/apps/headless/Dockerfile.linux b/apps/headless/Dockerfile.linux index a70acb0..507f950 100644 --- a/apps/headless/Dockerfile.linux +++ b/apps/headless/Dockerfile.linux @@ -13,7 +13,7 @@ COPY MCP ./MCP COPY Tests ./Tests COPY docs ./docs COPY tools ./tools -COPY build.sh build-linux.sh install-linux.sh benchmark.sh test.sh package.json headless.entitlements Dockerfile.linux .dockerignore ./ +COPY build.sh build-linux.sh install.sh install-linux.sh benchmark.sh test.sh package.json headless.entitlements Dockerfile.linux .dockerignore ./ RUN mkdir -p build Headless.app RUN HEADLESS_VERSION="${HEADLESS_BUILD_VERSION:-$(cat VERSION)}" \ swift build -c release --static-swift-stdlib --product headless-protocol-tests \ diff --git a/apps/headless/Package.swift b/apps/headless/Package.swift index a56332a..e4a00c3 100644 --- a/apps/headless/Package.swift +++ b/apps/headless/Package.swift @@ -61,7 +61,7 @@ let package = Package( exclude: [ "Package.swift", "Sources", "Tests", "tools", "VersionSupport", "VERSION", "build.sh", "package.json", "headless.entitlements", "build", "docs", "test.sh", - "LinuxHost", "Dockerfile.linux", "Headless.app", "build-linux.sh", "install-linux.sh", "benchmark.sh", ".dockerignore", + "LinuxHost", "Dockerfile.linux", "Headless.app", "build-linux.sh", "install.sh", "install-linux.sh", "benchmark.sh", ".dockerignore", "MCP", "node_modules", ], sources: ["main.swift", "Host/AgentBridge.swift", "Host/QADiagnosticsBridge.swift"], diff --git a/apps/headless/Tests/linux-e2e.sh b/apps/headless/Tests/linux-e2e.sh index 5e5ed0f..c480d76 100755 --- a/apps/headless/Tests/linux-e2e.sh +++ b/apps/headless/Tests/linux-e2e.sh @@ -36,7 +36,8 @@ if SNAP_INSTALL="$(HEADLESS_CHROMIUM_EXECUTABLE=/snap/bin/chromium \ echo "Snap Chromium was accepted by the installer" >&2 exit 1 fi -echo "$SNAP_INSTALL" | grep -q 'Snap Chromium is not supported' +echo "$SNAP_INSTALL" | grep -q 'UNSUPPORTED_BROWSER_RUNTIME' +echo "$SNAP_INSTALL" | grep -q 'Snap Chromium is not reliable' headless runtime | grep -q '"executable":"/usr/lib/chromium/chromium"' headless runtime | grep -q '"transport":"inherited-devtools-pipe"' diff --git a/apps/headless/Tests/linux-installer.sh b/apps/headless/Tests/linux-installer.sh new file mode 100755 index 0000000..f05b43e --- /dev/null +++ b/apps/headless/Tests/linux-installer.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +ROOT="$(mktemp -d "${TMPDIR:-/tmp}/headless-installer-tests.XXXXXX")" +trap 'rm -rf "$ROOT"' EXIT INT TERM +TOOLS="$ROOT/tools" +RELEASE="$ROOT/release" +BUNDLE="$ROOT/bundle" +mkdir -p "$TOOLS" "$RELEASE" "$BUNDLE/Headless_HeadlessProtocol.resources" + +cat > "$TOOLS/uname" <<'EOF' +#!/bin/sh +if [ "$1" = "-s" ]; then printf '%s\n' "${FAKE_SYSTEM:-Linux}"; else printf '%s\n' "${FAKE_ARCH:-x86_64}"; fi +EOF +cat > "$TOOLS/curl" <<'EOF' +#!/bin/sh +output="" +write_out="" +url="" +while [ "$#" -gt 0 ]; do + case "$1" in + --output) output="$2"; shift 2 ;; + --write-out) write_out="$2"; shift 2 ;; + --proto) shift 2 ;; + --tlsv1.2|--fail|--location|--silent|--show-error) shift ;; + *) url="$1"; shift ;; + esac +done +if [ -n "$write_out" ]; then + printf '%s' 'https://github.com/LockInTime/headless/releases/tag/v9.8.7' +else + cp "$INSTALLER_FIXTURES/${url##*/}" "$output" +fi +EOF +chmod 0755 "$TOOLS/uname" "$TOOLS/curl" + +cat > "$BUNDLE/headless" <<'EOF' +#!/bin/sh +if [ "${1:-}" = "runtime" ]; then + printf '%s\n' '{"engine":"chromium","executable":"/fixture/chromium","supported":true}' + exit 0 +fi +exit 64 +EOF +printf '#!/bin/sh\nexit 0\n' > "$BUNDLE/headless-host" +printf '#!/bin/sh\nexit 0\n' > "$BUNDLE/headless-mcp" +printf '#!/bin/sh\nexit 0\n' > "$ROOT/ffmpeg" +printf 'fixture runtime\n' > "$BUNDLE/Headless_HeadlessProtocol.resources/AgentRuntime.js" +printf 'P1\n' > "$BUNDLE/P1.md" +printf 'P2\n' > "$BUNDLE/P2.md" +cp install-linux.sh "$BUNDLE/install-linux.sh" +chmod 0755 "$BUNDLE/headless" "$BUNDLE/headless-host" "$BUNDLE/headless-mcp" \ + "$BUNDLE/install-linux.sh" "$ROOT/ffmpeg" + +ASSET="headless-9.8.7-linux-amd64.tar.gz" +tar -czf "$RELEASE/$ASSET" -C "$BUNDLE" \ + headless headless-host headless-mcp Headless_HeadlessProtocol.resources install-linux.sh P1.md P2.md +if command -v sha256sum >/dev/null 2>&1; then + (cd "$RELEASE" && sha256sum "$ASSET") > "$RELEASE/SHA256SUMS" +else + (cd "$RELEASE" && shasum -a 256 "$ASSET") > "$RELEASE/SHA256SUMS" +fi + +run_installer() { + PATH="$TOOLS:$PATH" INSTALLER_FIXTURES="$RELEASE" \ + HEADLESS_FFMPEG_EXECUTABLE="$ROOT/ffmpeg" "$@" +} + +PREFIX_EXPLICIT="$ROOT/explicit" +EXPLICIT_OUTPUT="$(run_installer env HEADLESS_VERSION=9.8.7 sh install.sh --prefix "$PREFIX_EXPLICIT")" +grep -q "Headless installed" <<< "$EXPLICIT_OUTPUT" +test -x "$PREFIX_EXPLICIT/bin/headless" +test -x "$PREFIX_EXPLICIT/bin/headless-host" +test -x "$PREFIX_EXPLICIT/bin/headless-mcp" +test -r "$PREFIX_EXPLICIT/bin/Headless_HeadlessProtocol.resources/AgentRuntime.js" + +PREFIX_LATEST="$ROOT/latest" +LATEST_OUTPUT="$(run_installer env -u HEADLESS_VERSION sh install.sh --prefix "$PREFIX_LATEST")" +grep -q "Headless installed" <<< "$LATEST_OUTPUT" +test -x "$PREFIX_LATEST/bin/headless" + +cp "$RELEASE/$ASSET" "$RELEASE/original.tar.gz" +printf 'corruption\n' >> "$RELEASE/$ASSET" +if run_installer env HEADLESS_VERSION=9.8.7 sh install.sh --prefix "$ROOT/corrupt" >/dev/null 2>&1; then + echo "bootstrap accepted a release with a mismatched checksum" >&2 + exit 1 +fi +mv "$RELEASE/original.tar.gz" "$RELEASE/$ASSET" + +cp "$RELEASE/$ASSET" "$RELEASE/safe.tar.gz" +printf 'unexpected\n' > "$BUNDLE/unexpected" +tar -czf "$RELEASE/$ASSET" -C "$BUNDLE" \ + headless headless-host headless-mcp Headless_HeadlessProtocol.resources install-linux.sh P1.md P2.md unexpected +if command -v sha256sum >/dev/null 2>&1; then + (cd "$RELEASE" && sha256sum "$ASSET") > "$RELEASE/SHA256SUMS" +else + (cd "$RELEASE" && shasum -a 256 "$ASSET") > "$RELEASE/SHA256SUMS" +fi +if run_installer env HEADLESS_VERSION=9.8.7 sh install.sh --prefix "$ROOT/unexpected" >/dev/null 2>&1; then + echo "bootstrap accepted an unexpected archive path" >&2 + exit 1 +fi +mv "$RELEASE/safe.tar.gz" "$RELEASE/$ASSET" + +if run_installer env FAKE_ARCH=riscv64 HEADLESS_VERSION=9.8.7 sh install.sh \ + --prefix "$ROOT/unsupported" >/dev/null 2>&1; then + echo "bootstrap accepted an unsupported architecture" >&2 + exit 1 +fi +for invalid_version in 09.8.7 9.08.7 9.8.07 9.8.7-01 9.8.7-.beta 9.8.7-beta. 9.8.7+build..1; do + if run_installer env HEADLESS_VERSION="$invalid_version" sh install.sh \ + --prefix "$ROOT/version" >/dev/null 2>&1; then + echo "bootstrap accepted an invalid semantic version: $invalid_version" >&2 + exit 1 + fi +done +if PATH="$TOOLS:$PATH" HEADLESS_FFMPEG_EXECUTABLE=relative/ffmpeg \ + HEADLESS_CHROMIUM_EXECUTABLE=/fixture/chromium \ + sh "$BUNDLE/install-linux.sh" --prefix "$ROOT/ffmpeg-relative" >/dev/null 2>&1; then + echo "package installer accepted a relative FFmpeg override" >&2 + exit 1 +fi + +echo "Linux bootstrap installer tests passed" diff --git a/apps/headless/install-linux.sh b/apps/headless/install-linux.sh index 5354fb4..078ce86 100755 --- a/apps/headless/install-linux.sh +++ b/apps/headless/install-linux.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" PREFIX="${HEADLESS_INSTALL_PREFIX:-$HOME/.local}" if [ "$#" -gt 0 ]; then @@ -34,89 +34,29 @@ else exit 69 fi -CHROMIUM="" -SNAP_CHROMIUM="" - -consider_chromium() { - candidate="$1" - case "$candidate" in - /snap|/snap/*|/var/lib/snapd/snap|/var/lib/snapd/snap/*|/usr/bin/snap) - SNAP_CHROMIUM="$candidate" - return 1 - ;; - esac - [ -x "$candidate" ] && [ -f "$candidate" ] || return 1 - resolved="$(readlink -f "$candidate" 2>/dev/null || true)" - [ -n "$resolved" ] || resolved="$candidate" - case "$resolved" in - /snap|/snap/*|/var/lib/snapd/snap|/var/lib/snapd/snap/*|/usr/bin/snap) - SNAP_CHROMIUM="$candidate" - return 1 - ;; - esac - case "$(sed -n '1p' "$resolved" 2>/dev/null || true)" in - '#!'*) - if grep -Eq '/snap/bin/|/usr/bin/snap|snap run ' "$resolved"; then - SNAP_CHROMIUM="$candidate" - return 1 - fi - ;; - esac - CHROMIUM="$resolved" - return 0 -} +if ! RUNTIME="$(HEADLESS_HOST_EXECUTABLE="$SOURCE_DIR/headless-host" "$SOURCE_DIR/headless" runtime 2>&1)"; then + echo "headless install: unsupported Chromium runtime" >&2 + echo "$RUNTIME" >&2 + exit 69 +fi -if [ "${HEADLESS_CHROMIUM_EXECUTABLE+x}" = x ]; then - case "$HEADLESS_CHROMIUM_EXECUTABLE" in - /*) ;; - *) - echo "headless install: HEADLESS_CHROMIUM_EXECUTABLE must be an absolute path" >&2 - exit 69 - ;; +FFMPEG="" +if [ "${HEADLESS_FFMPEG_EXECUTABLE+x}" = x ]; then + case "$HEADLESS_FFMPEG_EXECUTABLE" in + /*) FFMPEG="$HEADLESS_FFMPEG_EXECUTABLE" ;; + *) echo "headless install: HEADLESS_FFMPEG_EXECUTABLE must be an absolute path" >&2; exit 69 ;; esac - if ! consider_chromium "$HEADLESS_CHROMIUM_EXECUTABLE"; then - if [ -n "$SNAP_CHROMIUM" ]; then - echo "headless install: Ubuntu Snap Chromium is not supported with the inherited DevTools pipe: $SNAP_CHROMIUM" >&2 - else - echo "headless install: HEADLESS_CHROMIUM_EXECUTABLE is not an executable regular file: $HEADLESS_CHROMIUM_EXECUTABLE" >&2 - fi - echo "headless install: use the bundled Docker runtime or a native distribution Chromium binary" >&2 - exit 69 - fi else - for candidate in \ - "$SCRIPT_DIR/runtime/chromium/chromium" \ - /usr/lib/chromium/chromium \ - /usr/lib64/chromium/chromium \ - /usr/lib64/chromium-browser/chromium-browser \ - /opt/google/chrome/chrome \ - /opt/google/chrome/google-chrome \ - /usr/bin/chromium \ - /usr/bin/chromium-browser \ - /usr/bin/google-chrome-stable \ - /usr/bin/google-chrome - do - if consider_chromium "$candidate"; then break; fi - done - if [ -z "$CHROMIUM" ]; then - for command_name in chromium chromium-browser google-chrome-stable google-chrome; do - candidate="$(command -v "$command_name" 2>/dev/null || true)" - [ -n "$candidate" ] || continue - if consider_chromium "$candidate"; then break; fi - done - fi - if [ -z "$CHROMIUM" ]; then - if [ -n "$SNAP_CHROMIUM" ]; then - echo "headless install: Ubuntu Snap Chromium is not supported with the inherited DevTools pipe: $SNAP_CHROMIUM" >&2 - else - echo "headless install: no supported Chromium runtime was found" >&2 + for candidate in /usr/local/bin/ffmpeg /usr/bin/ffmpeg; do + if [ -x "$candidate" ] && [ -f "$candidate" ]; then + FFMPEG="$candidate" + break fi - echo "headless install: use the bundled Docker runtime or install a native distribution Chromium binary" >&2 - exit 69 - fi + done fi -if ! command -v ffmpeg >/dev/null 2>&1; then - echo "headless install: FFmpeg is required for recording; install ffmpeg with your system package manager" >&2 +if [ -z "$FFMPEG" ] || [ ! -x "$FFMPEG" ] || [ ! -f "$FFMPEG" ]; then + echo "headless install: FFmpeg was not found in an allowed absolute location" >&2 + echo "headless install: install ffmpeg with the system package manager or set HEADLESS_FFMPEG_EXECUTABLE" >&2 exit 69 fi @@ -130,6 +70,7 @@ install -m 0644 "$SOURCE_DIR/Headless_HeadlessProtocol.resources/AgentRuntime.js "$BIN_DIR/Headless_HeadlessProtocol.resources/AgentRuntime.js" echo "Headless installed in $BIN_DIR" -echo "Browser runtime: $CHROMIUM (supported inherited DevTools pipe)" +echo "Browser runtime verified: $RUNTIME" +echo "Recording runtime: $FFMPEG" echo "Run: $BIN_DIR/headless capabilities" echo "Check: $BIN_DIR/headless runtime" diff --git a/apps/headless/install.sh b/apps/headless/install.sh new file mode 100755 index 0000000..77be463 --- /dev/null +++ b/apps/headless/install.sh @@ -0,0 +1,82 @@ +#!/bin/sh +set -eu + +REPOSITORY="LockInTime/headless" +RELEASE_ROOT="https://github.com/$REPOSITORY/releases" + +fail() { + echo "headless bootstrap: $1" >&2 + exit "${2:-69}" +} + +command -v curl >/dev/null 2>&1 || fail "curl is required" +command -v tar >/dev/null 2>&1 || fail "tar is required" +[ "$(uname -s)" = "Linux" ] || fail "this installer is for Linux" + +case "$(uname -m)" in + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) fail "unsupported architecture: $(uname -m)" ;; +esac + +VERSION="${HEADLESS_VERSION:-}" +if [ -z "$VERSION" ]; then + LATEST_URL="$(curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \ + --output /dev/null --write-out '%{url_effective}' "$RELEASE_ROOT/latest")" + TAG="${LATEST_URL##*/}" + case "$TAG" in + v*) VERSION="${TAG#v}" ;; + *) fail "latest release did not resolve to a v-prefixed tag" ;; + esac +fi +printf '%s\n' "$VERSION" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+)(\.[0-9A-Za-z-]+)*)?$' \ + || fail "HEADLESS_VERSION must be a semantic version without a v prefix" 64 + +ASSET="headless-${VERSION}-linux-${ARCH}.tar.gz" +DOWNLOAD_ROOT="$RELEASE_ROOT/download/v${VERSION}" +TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/headless-bootstrap.XXXXXX")" +trap 'rm -rf "$TEMP_DIR"' EXIT INT TERM + +download() { + curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \ + --output "$2" "$1" +} + +download "$DOWNLOAD_ROOT/SHA256SUMS" "$TEMP_DIR/SHA256SUMS" +download "$DOWNLOAD_ROOT/$ASSET" "$TEMP_DIR/$ASSET" + +EXPECTED="$(awk -v asset="$ASSET" '$2 == asset { print $1 }' "$TEMP_DIR/SHA256SUMS")" +[ "$(printf '%s\n' "$EXPECTED" | wc -l | tr -d ' ')" -eq 1 ] \ + || fail "SHA256SUMS must contain exactly one entry for $ASSET" +printf '%s\n' "$EXPECTED" | grep -Eq '^[0-9a-fA-F]{64}$' \ + || fail "SHA256SUMS contains an invalid digest for $ASSET" + +if command -v sha256sum >/dev/null 2>&1; then + ACTUAL="$(sha256sum "$TEMP_DIR/$ASSET" | awk '{ print $1 }')" +elif command -v shasum >/dev/null 2>&1; then + ACTUAL="$(shasum -a 256 "$TEMP_DIR/$ASSET" | awk '{ print $1 }')" +else + fail "sha256sum or shasum is required to verify the release" +fi +[ "$ACTUAL" = "$EXPECTED" ] || fail "checksum verification failed for $ASSET" + +CONTENTS="$TEMP_DIR/archive-contents" +tar -tzf "$TEMP_DIR/$ASSET" > "$CONTENTS" +EXPECTED_CONTENTS="$TEMP_DIR/expected-contents" +cat > "$EXPECTED_CONTENTS" <<'EOF' +Headless_HeadlessProtocol.resources/ +Headless_HeadlessProtocol.resources/AgentRuntime.js +P1.md +P2.md +headless +headless-host +headless-mcp +install-linux.sh +EOF +LC_ALL=C sort -u "$CONTENTS" > "$CONTENTS.sorted" +LC_ALL=C sort -u "$EXPECTED_CONTENTS" > "$EXPECTED_CONTENTS.sorted" +cmp -s "$CONTENTS.sorted" "$EXPECTED_CONTENTS.sorted" \ + || fail "release archive contains unexpected or missing paths" + +tar -xzf "$TEMP_DIR/$ASSET" -C "$TEMP_DIR" +sh "$TEMP_DIR/install-linux.sh" "$@" diff --git a/apps/headless/test.sh b/apps/headless/test.sh index 86d66a6..1ea9666 100755 --- a/apps/headless/test.sh +++ b/apps/headless/test.sh @@ -3,6 +3,7 @@ set -euo pipefail cd "$(dirname "$0")" ./Tests/release-checksums.sh +./Tests/linux-installer.sh for tool in swift swiftc; do if ! command -v "$tool" >/dev/null 2>&1; then diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index af645f2..0527a7b 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -363,13 +363,14 @@ Owner-decided scope: package managers, no hosted service. `build.sh:96-114`); universal binary (today `uname -m` on an arm64 runner); Homebrew tap/cask. Resolve the `com.headless.app` provisioning question for the passkey entitlement (`.hermes` plan flagged it; `build.sh:97-109`). -- **E2.** ([#40](https://github.com/LockInTime/headless/issues/40)) Linux `curl | sh` installer wrapping the existing tarball + - preflight (`install-linux.sh` already does runtime checks — reuse); - align its ffmpeg policy with the runtime's allow-list (installer accepts any - PATH ffmpeg, `install-linux.sh:118`, runtime doesn't, - `HP/Recording.swift:225-227`); single-source the Snap-detection logic - currently duplicated in shell (`install-linux.sh:40-67` vs - `HP/ChromiumRuntime.swift:167-184`). +- **E2.** [x] ([#40](https://github.com/LockInTime/headless/issues/40)) ~~Linux `curl | sh` installer wrapping the existing tarball + + preflight; align its FFmpeg policy with the runtime allow-list; single-source + Snap detection.~~ **Done:** the release bootstrap resolves amd64/arm64, + verifies the versioned tarball against `SHA256SUMS`, rejects archive-shape + drift, and delegates to the packaged installer. That installer now calls the + product's Chromium runtime resolver and restricts FFmpeg to the recording + allow-list or a validated absolute override. Offline tests cover latest and + pinned versions, checksums, archive contents, architecture, and overrides. - **E3.** ([#41](https://github.com/LockInTime/headless/issues/41)) Publish the Docker `production` image to GHCR on tag (release notes currently say "not published"); this is also the interim Windows story. - **E4.** ([#42](https://github.com/LockInTime/headless/issues/42)) ~~`SHA256SUMS` for all release assets — the QA evidence bundle already ships