From 8978ffcadac5052903bf21bdadc28e31f50b2fb4 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 23:55:26 +0530 Subject: [PATCH 1/2] ci: sync verified Headless releases --- .github/workflows/update-cask.yml | 45 +++++++++++++++ README.md | 15 ++++- scripts/update-cask.sh | 94 +++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/update-cask.yml create mode 100755 scripts/update-cask.sh diff --git a/.github/workflows/update-cask.yml b/.github/workflows/update-cask.yml new file mode 100644 index 0000000..8363e2a --- /dev/null +++ b/.github/workflows/update-cask.yml @@ -0,0 +1,45 @@ +name: Update cask + +on: + schedule: + - cron: "17,47 * * * *" + workflow_dispatch: + inputs: + version: + description: Optional exact Headless release version + required: false + type: string + +permissions: + contents: write + +concurrency: + group: update-headless-cask + cancel-in-progress: false + +jobs: + update: + runs-on: macos-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - name: Verify release and render cask + env: + VERSION: ${{ inputs.version }} + run: ./scripts/update-cask.sh "$VERSION" + - name: Validate cask style + run: brew style --cask Casks/headless.rb + - name: Commit update + run: | + set -euo pipefail + if git diff --quiet -- Casks/headless.rb; then + echo "Cask is already current" + exit 0 + fi + rendered_version="$(sed -n 's/^ version "\([^"]*\)"$/\1/p' Casks/headless.rb)" + test -n "$rendered_version" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Casks/headless.rb + git commit -m "headless $rendered_version" + git push origin HEAD:main diff --git a/README.md b/README.md index 20c35ab..3a22052 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ -# homebrew-headless -Homebrew tap for Headless +# Homebrew tap for Headless + +Install the latest signed and notarized Headless release: + +```sh +brew install --cask LockInTime/headless/headless +``` + +The cask is generated from the latest public +[`LockInTime/headless`](https://github.com/LockInTime/headless) release. The +update workflow verifies the release checksum, universal architectures, +Developer ID signature, stapled notarization ticket, and Gatekeeper assessment +before committing a new version. diff --git a/scripts/update-cask.sh b/scripts/update-cask.sh new file mode 100755 index 0000000..4b1cc47 --- /dev/null +++ b/scripts/update-cask.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -euo pipefail + +VERSION="${1:-}" +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +REPOSITORY="LockInTime/headless" +SEMVER_PATTERN='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' +TEMP_DIRECTORY="$(mktemp -d "${TMPDIR:-/tmp}/headless-cask-update.XXXXXX")" +trap 'rm -rf "$TEMP_DIRECTORY"' EXIT + +curl_release() { + curl --fail --silent --show-error --location \ + --proto '=https' --tlsv1.2 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "$1" +} + +if [[ -z "$VERSION" ]]; then + VERSION="$(curl_release "https://api.github.com/repos/$REPOSITORY/releases/latest" | jq -r '.tag_name')" + VERSION="${VERSION#v}" +fi +if ! printf '%s\n' "$VERSION" | grep -Eq "$SEMVER_PATTERN"; then + echo "Homebrew update: invalid semantic version: $VERSION" >&2 + exit 64 +fi + +ASSET="Headless-${VERSION}-macos.zip" +BASE_URL="https://github.com/$REPOSITORY/releases/download/v${VERSION}" +curl_release "$BASE_URL/SHA256SUMS" > "$TEMP_DIRECTORY/SHA256SUMS" +curl_release "$BASE_URL/$ASSET" > "$TEMP_DIRECTORY/$ASSET" + +EXPECTED_SHA256="$(awk -v asset="$ASSET" '$2 == asset { print $1 }' "$TEMP_DIRECTORY/SHA256SUMS")" +if [[ "$(printf '%s\n' "$EXPECTED_SHA256" | sed '/^$/d' | wc -l | tr -d ' ')" != 1 ]] \ + || ! printf '%s\n' "$EXPECTED_SHA256" | grep -Eq '^[0-9a-f]{64}$'; then + echo "Homebrew update: release manifest must contain one exact checksum for $ASSET" >&2 + exit 65 +fi +ACTUAL_SHA256="$(shasum -a 256 "$TEMP_DIRECTORY/$ASSET" | awk '{ print $1 }')" +[[ "$ACTUAL_SHA256" == "$EXPECTED_SHA256" ]] || { + echo "Homebrew update: checksum mismatch for $ASSET" >&2 + exit 65 +} + +ditto -x -k "$TEMP_DIRECTORY/$ASSET" "$TEMP_DIRECTORY/unpacked" +APP="$TEMP_DIRECTORY/unpacked/Headless.app" +[[ -d "$APP" ]] || { echo "Homebrew update: archive does not contain Headless.app" >&2; exit 65; } +codesign --verify --deep --strict "$APP" +CODESIGN_DETAILS="$(codesign --display --verbose=4 "$APP" 2>&1)" +printf '%s\n' "$CODESIGN_DETAILS" | grep -q '^Authority=Developer ID Application:' \ + || { echo "Homebrew update: app is not Developer ID signed" >&2; exit 65; } +printf '%s\n' "$CODESIGN_DETAILS" | grep -q 'flags=.*runtime' \ + || { echo "Homebrew update: hardened runtime is missing" >&2; exit 65; } +for executable in \ + "$APP/Contents/MacOS/Headless" \ + "$APP/Contents/Resources/bin/headless" \ + "$APP/Contents/Resources/bin/headless-mcp"; do + ARCHITECTURES="$(lipo -archs "$executable")" + for architecture in arm64 x86_64; do + printf '%s\n' "$ARCHITECTURES" | grep -Eq "(^| )$architecture( |$)" \ + || { echo "Homebrew update: $executable is missing $architecture" >&2; exit 65; } + done +done +xcrun stapler validate "$APP" +spctl --assess --type execute --verbose=4 "$APP" + +mkdir -p "$ROOT/Casks" +TEMP_CASK="$ROOT/Casks/.headless.rb.tmp" +cat > "$TEMP_CASK" <= :ventura" + + app "Headless.app" + binary "#{appdir}/Headless.app/Contents/Resources/bin/headless", target: "headless" + binary "#{appdir}/Headless.app/Contents/Resources/bin/headless-mcp", target: "headless-mcp" + + zap trash: [ + "~/Library/Application Support/com.headless.app", + "~/Library/Preferences/com.headless.app.plist", + ] +end +CASK +mv "$TEMP_CASK" "$ROOT/Casks/headless.rb" + +echo "Rendered Headless $VERSION cask after distribution verification" From f268926ed9fecb84ed89b77de60850a4d3317073 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 23:56:04 +0530 Subject: [PATCH 2/2] ci: validate cask updater changes --- .github/workflows/update-cask.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/update-cask.yml b/.github/workflows/update-cask.yml index 8363e2a..bc886ae 100644 --- a/.github/workflows/update-cask.yml +++ b/.github/workflows/update-cask.yml @@ -1,6 +1,10 @@ name: Update cask on: + pull_request: + paths: + - ".github/workflows/update-cask.yml" + - "scripts/update-cask.sh" schedule: - cron: "17,47 * * * *" workflow_dispatch: @@ -18,7 +22,18 @@ concurrency: cancel-in-progress: false jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + - name: Validate updater + run: | + bash -n scripts/update-cask.sh + shellcheck scripts/update-cask.sh + update: + if: github.event_name != 'pull_request' runs-on: macos-latest timeout-minutes: 15 steps: