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
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/headless/Dockerfile.linux
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion apps/headless/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
3 changes: 2 additions & 1 deletion apps/headless/Tests/linux-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
Expand Down
125 changes: 125 additions & 0 deletions apps/headless/Tests/linux-installer.sh
Original file line number Diff line number Diff line change
@@ -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"
101 changes: 21 additions & 80 deletions apps/headless/install-linux.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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"
Loading
Loading