diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c1de0eb..f67c3533 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: - name: Validate cmux scheme test configuration run: ./tests/test_ci_scheme_testaction_debug.sh + - name: Validate GhosttyKit checksum verification + run: ./tests/test_ci_ghosttykit_checksum_verification.sh + web-typecheck: runs-on: ubuntu-latest defaults: @@ -70,31 +73,8 @@ jobs: xcrun --sdk macosx --show-sdk-path - name: Download pre-built GhosttyKit.xcframework - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -euo pipefail - GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD) - TAG="xcframework-$GHOSTTY_SHA" - URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz" - echo "Downloading xcframework for ghostty $GHOSTTY_SHA" - MAX_RETRIES=30 - RETRY_DELAY=20 - for i in $(seq 1 $MAX_RETRIES); do - if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then - echo "Download succeeded on attempt $i" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2 - exit 1 - fi - echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - done - tar xzf GhosttyKit.xcframework.tar.gz - rm GhosttyKit.xcframework.tar.gz - test -d GhosttyKit.xcframework + ./scripts/download-prebuilt-ghosttykit.sh - name: Clean DerivedData run: | @@ -203,31 +183,8 @@ jobs: xcodebuild -version - name: Download pre-built GhosttyKit.xcframework - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -euo pipefail - GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD) - TAG="xcframework-$GHOSTTY_SHA" - URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz" - echo "Downloading xcframework for ghostty $GHOSTTY_SHA" - MAX_RETRIES=30 - RETRY_DELAY=20 - for i in $(seq 1 $MAX_RETRIES); do - if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then - echo "Download succeeded on attempt $i" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2 - exit 1 - fi - echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - done - tar xzf GhosttyKit.xcframework.tar.gz - rm GhosttyKit.xcframework.tar.gz - test -d GhosttyKit.xcframework + ./scripts/download-prebuilt-ghosttykit.sh - name: Clean DerivedData run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3b9a0866..18caadc3 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -116,31 +116,8 @@ jobs: npm install --global "create-dmg@${CREATE_DMG_VERSION}" - name: Download pre-built GhosttyKit.xcframework - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -euo pipefail - GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD) - TAG="xcframework-$GHOSTTY_SHA" - URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz" - echo "Downloading xcframework for ghostty $GHOSTTY_SHA" - MAX_RETRIES=30 - RETRY_DELAY=20 - for i in $(seq 1 $MAX_RETRIES); do - if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then - echo "Download succeeded on attempt $i" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2 - exit 1 - fi - echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - done - tar xzf GhosttyKit.xcframework.tar.gz - rm GhosttyKit.xcframework.tar.gz - test -d GhosttyKit.xcframework + ./scripts/download-prebuilt-ghosttykit.sh - name: Cache Swift packages uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 200f003a..ec935c63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,31 +103,8 @@ jobs: - name: Download pre-built GhosttyKit.xcframework if: steps.guard_release_assets.outputs.skip_all != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - set -euo pipefail - GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD) - TAG="xcframework-$GHOSTTY_SHA" - URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz" - echo "Downloading xcframework for ghostty $GHOSTTY_SHA" - MAX_RETRIES=30 - RETRY_DELAY=20 - for i in $(seq 1 $MAX_RETRIES); do - if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then - echo "Download succeeded on attempt $i" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2 - exit 1 - fi - echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - done - tar xzf GhosttyKit.xcframework.tar.gz - rm GhosttyKit.xcframework.tar.gz - test -d GhosttyKit.xcframework + ./scripts/download-prebuilt-ghosttykit.sh - name: Cache Swift packages if: steps.guard_release_assets.outputs.skip_all != 'true' diff --git a/scripts/download-prebuilt-ghosttykit.sh b/scripts/download-prebuilt-ghosttykit.sh new file mode 100755 index 00000000..cc3c520b --- /dev/null +++ b/scripts/download-prebuilt-ghosttykit.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +if [ -n "${GHOSTTY_SHA:-}" ]; then + GHOSTTY_SHA="$GHOSTTY_SHA" +else + if [ ! -d "$REPO_ROOT/ghostty" ] || ! git -C "$REPO_ROOT/ghostty" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Missing ghostty submodule. Run ./scripts/setup.sh or git submodule update --init --recursive first." >&2 + exit 1 + fi + GHOSTTY_SHA="$(git -C "$REPO_ROOT/ghostty" rev-parse HEAD)" +fi + +TAG="xcframework-$GHOSTTY_SHA" +ARCHIVE_NAME="${GHOSTTYKIT_ARCHIVE_NAME:-GhosttyKit.xcframework.tar.gz}" +OUTPUT_DIR="${GHOSTTYKIT_OUTPUT_DIR:-GhosttyKit.xcframework}" +CHECKSUMS_FILE="${GHOSTTYKIT_CHECKSUMS_FILE:-$SCRIPT_DIR/ghosttykit-checksums.txt}" +DOWNLOAD_URL="${GHOSTTYKIT_URL:-https://github.com/manaflow-ai/ghostty/releases/download/$TAG/$ARCHIVE_NAME}" +DOWNLOAD_RETRIES="${GHOSTTYKIT_DOWNLOAD_RETRIES:-30}" +DOWNLOAD_RETRY_DELAY="${GHOSTTYKIT_DOWNLOAD_RETRY_DELAY:-20}" + +if [ ! -f "$CHECKSUMS_FILE" ]; then + echo "Missing checksum file: $CHECKSUMS_FILE" >&2 + exit 1 +fi + +EXPECTED_SHA256="$( + awk -v sha="$GHOSTTY_SHA" ' + $1 == sha { + print $2 + found = 1 + exit + } + END { + if (!found) { + exit 1 + } + } + ' "$CHECKSUMS_FILE" || true +)" + +if [ -z "$EXPECTED_SHA256" ]; then + echo "Missing pinned GhosttyKit checksum for ghostty $GHOSTTY_SHA in $CHECKSUMS_FILE" >&2 + exit 1 +fi + +echo "Downloading $ARCHIVE_NAME for ghostty $GHOSTTY_SHA" +curl --fail --show-error --location \ + --retry "$DOWNLOAD_RETRIES" \ + --retry-delay "$DOWNLOAD_RETRY_DELAY" \ + --retry-all-errors \ + -o "$ARCHIVE_NAME" \ + "$DOWNLOAD_URL" + +ACTUAL_SHA256="$(shasum -a 256 "$ARCHIVE_NAME" | awk '{print $1}')" +if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then + echo "$ARCHIVE_NAME checksum mismatch" >&2 + echo "Expected: $EXPECTED_SHA256" >&2 + echo "Actual: $ACTUAL_SHA256" >&2 + exit 1 +fi + +rm -rf "$OUTPUT_DIR" +tar xzf "$ARCHIVE_NAME" +rm "$ARCHIVE_NAME" +test -d "$OUTPUT_DIR" + +echo "Verified and extracted $OUTPUT_DIR" diff --git a/scripts/ghosttykit-checksums.txt b/scripts/ghosttykit-checksums.txt new file mode 100644 index 00000000..29794d12 --- /dev/null +++ b/scripts/ghosttykit-checksums.txt @@ -0,0 +1,4 @@ +# Pinned GhosttyKit.xcframework.tar.gz checksums keyed by ghostty submodule SHA. +# Update this file in a reviewed PR whenever the ghostty submodule SHA changes. +# Format: +7dd589824d4c9bda8265355718800cccaf7189a0 3915af4256850a0a7bee671c3ba0a47cbfee5dbfc6d71caf952acefdf2ee4207 diff --git a/tests/test_ci_ghosttykit_checksum_verification.sh b/tests/test_ci_ghosttykit_checksum_verification.sh new file mode 100755 index 00000000..1eba6ecf --- /dev/null +++ b/tests/test_ci_ghosttykit_checksum_verification.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# Regression test for the pinned GhosttyKit artifact verification helper. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$ROOT_DIR/scripts/download-prebuilt-ghosttykit.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +WORKFLOWS=( + "$ROOT_DIR/.github/workflows/ci.yml" + "$ROOT_DIR/.github/workflows/nightly.yml" + "$ROOT_DIR/.github/workflows/release.yml" +) + +FIXTURE_SHA="7dd589824d4c9bda8265355718800cccaf7189a0" +FIXTURE_DIR="$TMP_DIR/fixture" +SUCCESS_DIR="$TMP_DIR/success" +MISMATCH_DIR="$TMP_DIR/mismatch" +MISSING_ENTRY_DIR="$TMP_DIR/missing-entry" +BIN_DIR="$TMP_DIR/bin" +CHECKSUMS_FILE="$TMP_DIR/ghosttykit-checksums.txt" +SUCCESS_LOG="$TMP_DIR/curl-success.log" +MISMATCH_LOG="$TMP_DIR/curl-mismatch.log" +MISMATCH_OUTPUT="$TMP_DIR/mismatch.out" +MISSING_ENTRY_OUTPUT="$TMP_DIR/missing-entry.out" + +mkdir -p "$FIXTURE_DIR/GhosttyKit.xcframework" "$SUCCESS_DIR" "$MISMATCH_DIR" "$MISSING_ENTRY_DIR" "$BIN_DIR" +printf 'fixture\n' > "$FIXTURE_DIR/GhosttyKit.xcframework/marker.txt" +(cd "$FIXTURE_DIR" && tar czf "$TMP_DIR/GhosttyKit.xcframework.tar.gz" GhosttyKit.xcframework) +ACTUAL_SHA256="$(shasum -a 256 "$TMP_DIR/GhosttyKit.xcframework.tar.gz" | awk '{print $1}')" +printf '%s %s\n' "$FIXTURE_SHA" "$ACTUAL_SHA256" > "$CHECKSUMS_FILE" + +for workflow in "${WORKFLOWS[@]}"; do + if ! grep -Fq './scripts/download-prebuilt-ghosttykit.sh' "$workflow"; then + echo "FAIL: $workflow must call download-prebuilt-ghosttykit.sh" + exit 1 + fi +done + +cat > "$BIN_DIR/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +LOG_FILE="${TEST_CURL_LOG:?}" +FIXTURE_ARCHIVE="${TEST_FIXTURE_ARCHIVE:?}" +OUTPUT="" + +while [ "$#" -gt 0 ]; do + case "$1" in + -o) + OUTPUT="$2" + shift 2 + ;; + *) + printf '%s\n' "$1" >> "$LOG_FILE" + shift + ;; + esac +done + +if [ -z "$OUTPUT" ]; then + echo "curl stub missing -o output path" >&2 + exit 1 +fi + +cp "$FIXTURE_ARCHIVE" "$OUTPUT" +EOF +chmod +x "$BIN_DIR/curl" + +( + cd "$SUCCESS_DIR" + PATH="$BIN_DIR:$PATH" \ + TEST_CURL_LOG="$SUCCESS_LOG" \ + TEST_FIXTURE_ARCHIVE="$TMP_DIR/GhosttyKit.xcframework.tar.gz" \ + GHOSTTY_SHA="$FIXTURE_SHA" \ + GHOSTTYKIT_CHECKSUMS_FILE="$CHECKSUMS_FILE" \ + "$SCRIPT" +) + +if [ ! -f "$SUCCESS_DIR/GhosttyKit.xcframework/marker.txt" ]; then + echo "FAIL: verification helper did not extract GhosttyKit.xcframework" + exit 1 +fi + +if [ -f "$SUCCESS_DIR/GhosttyKit.xcframework.tar.gz" ]; then + echo "FAIL: verification helper did not clean up the downloaded archive" + exit 1 +fi + +for expected_arg in --retry --retry-delay --retry-all-errors; do + if ! grep -Fxq -- "$expected_arg" "$SUCCESS_LOG"; then + echo "FAIL: curl invocation missing $expected_arg" + exit 1 + fi +done + +printf '%s %s\n' "$FIXTURE_SHA" "0000000000000000000000000000000000000000000000000000000000000000" > "$CHECKSUMS_FILE" + +if ( + cd "$MISMATCH_DIR" + PATH="$BIN_DIR:$PATH" \ + TEST_CURL_LOG="$MISMATCH_LOG" \ + TEST_FIXTURE_ARCHIVE="$TMP_DIR/GhosttyKit.xcframework.tar.gz" \ + GHOSTTY_SHA="$FIXTURE_SHA" \ + GHOSTTYKIT_CHECKSUMS_FILE="$CHECKSUMS_FILE" \ + "$SCRIPT" +) >"$MISMATCH_OUTPUT" 2>&1; then + echo "FAIL: verification helper succeeded with an invalid pinned checksum" + exit 1 +fi + +if ! grep -Fq "GhosttyKit.xcframework.tar.gz checksum mismatch" "$MISMATCH_OUTPUT"; then + echo "FAIL: verification helper did not report checksum mismatch" + exit 1 +fi + +printf '%s %s\n' "0000000000000000000000000000000000000000" "$ACTUAL_SHA256" > "$CHECKSUMS_FILE" + +if ( + cd "$MISSING_ENTRY_DIR" + PATH="$BIN_DIR:$PATH" \ + TEST_CURL_LOG="$MISMATCH_LOG" \ + TEST_FIXTURE_ARCHIVE="$TMP_DIR/GhosttyKit.xcframework.tar.gz" \ + GHOSTTY_SHA="$FIXTURE_SHA" \ + GHOSTTYKIT_CHECKSUMS_FILE="$CHECKSUMS_FILE" \ + "$SCRIPT" +) >"$MISSING_ENTRY_OUTPUT" 2>&1; then + echo "FAIL: verification helper succeeded without a pinned checksum entry" + exit 1 +fi + +if ! grep -Fq "Missing pinned GhosttyKit checksum for ghostty $FIXTURE_SHA" "$MISSING_ENTRY_OUTPUT"; then + echo "FAIL: verification helper did not report a missing pinned checksum entry" + exit 1 +fi + +echo "PASS: GhosttyKit verification helper enforces pinned checksums"