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
29 changes: 12 additions & 17 deletions .github/workflows/build-ghosttykit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: macos-15
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All @@ -28,27 +28,17 @@ jobs:
path: |
Frameworks/GhosttyKit.xcframework
Frameworks/ghostty-resources
key: ghosttykit-native-${{ steps.ghostty-sha.outputs.sha }}
Frameworks/.ghosttykit-provenance
key: ghosttykit-universal-v4-${{ hashFiles('ghosttykit-lock.json') }}

- name: Install Zig 0.15.2
- name: Install verified Zig
if: steps.cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
ZIG_REQUIRED="0.15.2"
if command -v zig >/dev/null 2>&1 && zig version 2>/dev/null | grep -q "^${ZIG_REQUIRED}"; then
echo "zig ${ZIG_REQUIRED} already installed"
else
echo "Installing zig ${ZIG_REQUIRED} from tarball"
curl -fSL "https://ziglang.org/download/${ZIG_REQUIRED}/zig-aarch64-macos-${ZIG_REQUIRED}.tar.xz" -o /tmp/zig.tar.xz
tar xf /tmp/zig.tar.xz -C /tmp
sudo mkdir -p /usr/local/bin /usr/local/lib
sudo cp -f /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/zig /usr/local/bin/zig
sudo cp -rf /tmp/zig-aarch64-macos-${ZIG_REQUIRED}/lib /usr/local/lib/zig
fi
zig version
ZIG_BIN="$(bash scripts/install-verified-zig.sh --prefix "$RUNNER_TEMP/zig")"
echo "$ZIG_BIN" >> "$GITHUB_PATH"

- name: Select Xcode
if: steps.cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
Expand All @@ -67,7 +57,10 @@ jobs:

- name: Build GhosttyKit
if: steps.cache.outputs.cache-hit != 'true'
run: bash scripts/build-ghostty.sh
run: bash scripts/build-ghostty.sh --universal

- name: Verify universal GhosttyKit
run: bash scripts/verify-ghosttykit.sh

- name: Upload GhosttyKit artifact
uses: actions/upload-artifact@v7
Expand All @@ -76,4 +69,6 @@ jobs:
path: |
Frameworks/GhosttyKit.xcframework
Frameworks/ghostty-resources
Frameworks/.ghosttykit-provenance
include-hidden-files: true
retention-days: 7
213 changes: 213 additions & 0 deletions .github/workflows/publish-ghosttykit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
name: Publish GhosttyKit

on:
workflow_dispatch:

permissions:
contents: write
id-token: write
attestations: write

concurrency:
group: ghosttykit-publish
cancel-in-progress: false

jobs:
publish:
environment: ghosttykit-publish
runs-on: macos-15
timeout-minutes: 75
steps:
- name: Checkout reviewed source and submodules
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
submodules: recursive

- name: Load candidate contract and reject an existing tag
id: contract
run: |
set -euo pipefail
source scripts/ghosttykit-contract.sh
ghosttykit_load_contract ghosttykit-lock.json
[[ "$MORI_GHOSTTYKIT_ARTIFACT_STATE" == candidate ]] || {
echo "Publishing requires a candidate lock with null artifact digests." >&2
exit 1
}
set +e
remote_tags="$(git ls-remote --exit-code --tags origin "refs/tags/$MORI_GHOSTTYKIT_ARTIFACT_TAG")"
remote_status=$?
set -e
if [[ "$remote_status" -eq 0 && -n "$remote_tags" ]]; then
echo "Refusing to overwrite existing GhosttyKit tag $MORI_GHOSTTYKIT_ARTIFACT_TAG." >&2
exit 1
fi
[[ "$remote_status" -eq 2 ]] || {
echo "Could not establish whether GhosttyKit tag $MORI_GHOSTTYKIT_ARTIFACT_TAG exists." >&2
exit 1
}
echo "tag=$MORI_GHOSTTYKIT_ARTIFACT_TAG" >> "$GITHUB_OUTPUT"
echo "name=$MORI_GHOSTTYKIT_ARTIFACT_NAME" >> "$GITHUB_OUTPUT"

- name: Fetch complete Ghostty source history
run: |
set -euo pipefail
if [[ "$(git -C vendor/ghostty rev-parse --is-shallow-repository)" == true ]]; then
git -C vendor/ghostty fetch --unshallow --tags
fi
source scripts/ghosttykit-contract.sh
ghosttykit_load_contract ghosttykit-lock.json
git -C vendor/ghostty cat-file -e "$MORI_GHOSTTYKIT_BASE_COMMIT^{commit}"

- name: Install verified Zig
run: |
set -euo pipefail
ZIG_BIN="$(bash scripts/install-verified-zig.sh --prefix "$RUNNER_TEMP/zig")"
echo "$ZIG_BIN" >> "$GITHUB_PATH"

- name: Select Xcode
run: |
set -euo pipefail
if [[ -d "/Applications/Xcode.app/Contents/Developer" ]]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
[[ -n "$XCODE_APP" ]] || { echo "No Xcode.app found under /Applications" >&2; exit 1; }
XCODE_DIR="$XCODE_APP/Contents/Developer"
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
xcodebuild -version

- name: Build and verify a clean universal GhosttyKit
run: |
set -euo pipefail
bash scripts/build-ghostty.sh --clean --universal
bash scripts/verify-ghosttykit.sh

- name: Package GhosttyKit candidate
run: |
set -euo pipefail
bash scripts/package-ghosttykit.sh --output "$RUNNER_TEMP/ghosttykit-package"

- name: Attest GhosttyKit archive
uses: actions/attest-build-provenance@v3
with:
subject-path: ${{ runner.temp }}/ghosttykit-package/${{ steps.contract.outputs.name }}

- name: Atomically reserve the release tag
id: tag
env:
GH_TOKEN: ${{ github.token }}
run: |
bash scripts/github-tag-reservation.sh --reserve \
--repo "$GITHUB_REPOSITORY" \
--tag "${{ steps.contract.outputs.tag }}" \
--sha "$GITHUB_SHA"
echo "reserved=true" >> "$GITHUB_OUTPUT"

- name: Verify reserved tag ownership
env:
GH_TOKEN: ${{ github.token }}
run: |
bash scripts/github-tag-reservation.sh --verify \
--repo "$GITHUB_REPOSITORY" \
--tag "${{ steps.contract.outputs.tag }}" \
--sha "$GITHUB_SHA"

- name: Create and upload draft release
id: draft
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
archive="$RUNNER_TEMP/ghosttykit-package/${{ steps.contract.outputs.name }}"
manifest="${archive%.zip}.manifest.json"
bash scripts/github-tag-reservation.sh --verify \
--repo "$GITHUB_REPOSITORY" \
--tag "${{ steps.contract.outputs.tag }}" \
--sha "$GITHUB_SHA"
output="$(bash scripts/github-release-draft.sh --create-draft \
--repo "$GITHUB_REPOSITORY" \
--tag "${{ steps.contract.outputs.tag }}" \
--target "$GITHUB_SHA" \
--title "GhosttyKit ${{ steps.contract.outputs.tag }}" \
--prerelease \
--make-latest false \
--require-immutable \
--defer-asset-verification \
--asset "$archive" \
--asset "$manifest")"
release_id="${output#release_id=}"
[[ "$release_id" =~ ^[0-9]+$ ]]
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"

- name: Verify server digests and uploaded bytes
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
archive="$RUNNER_TEMP/ghosttykit-package/${{ steps.contract.outputs.name }}"
manifest="${archive%.zip}.manifest.json"
bash scripts/github-release-draft.sh --verify-draft-assets \
--repo "$GITHUB_REPOSITORY" \
--release-id "${{ steps.draft.outputs.release_id }}" \
--download-assets \
--asset "$archive" \
--asset "$manifest"

- name: Publish immutable GhosttyKit release once
id: publish
env:
GH_TOKEN: ${{ github.token }}
run: |
bash scripts/github-release-draft.sh --publish-draft \
--repo "$GITHUB_REPOSITORY" \
--release-id "${{ steps.draft.outputs.release_id }}" \
--require-immutable

- name: Record lock values
if: success()
run: |
set -euo pipefail
archive="$RUNNER_TEMP/ghosttykit-package/${{ steps.contract.outputs.name }}"
manifest="${archive%.zip}.manifest.json"
archive_sha256="$(shasum -a 256 "$archive" | awk '{print $1}')"
framework_tree_sha256="$(python3 - "$manifest" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as stream:
print(json.load(stream)["artifact"]["frameworkTreeSha256"])
PY
)"
cat >> "$GITHUB_STEP_SUMMARY" <<EOF
## Published GhosttyKit lock values

\`ghosttykit-lock.json\` remains a candidate until these returned values are reviewed and committed:

\`\`\`json
{
"artifact": {
"tag": "${{ steps.contract.outputs.tag }}",
"name": "${{ steps.contract.outputs.name }}",
"sha256": "$archive_sha256",
"frameworkTreeSha256": "$framework_tree_sha256"
}
}
\`\`\`
EOF

- name: Delete incomplete draft and reserved tag
if: failure() && steps.tag.outputs.reserved == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ -n "${{ steps.draft.outputs.release_id }}" ]]; then
bash scripts/github-release-draft.sh --cleanup-draft \
--repo "$GITHUB_REPOSITORY" \
--release-id "${{ steps.draft.outputs.release_id }}"
fi
bash scripts/github-tag-reservation.sh --cleanup \
--repo "$GITHUB_REPOSITORY" \
--tag "${{ steps.contract.outputs.tag }}" \
--sha "$GITHUB_SHA"
49 changes: 49 additions & 0 deletions .github/workflows/release-draft-preflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release Draft Preflight

on:
workflow_dispatch:

permissions:
contents: write

concurrency:
group: release-draft-preflight
cancel-in-progress: false

jobs:
draft-flow:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Create, download, verify, and delete a multi-asset draft
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="release-draft-preflight-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
assets="$RUNNER_TEMP/release-draft-preflight-assets"
mkdir -p "$assets"
python3 - "$assets/${tag}.zip" <<'PY'
import sys
import zipfile
with zipfile.ZipFile(sys.argv[1], "w", compression=zipfile.ZIP_DEFLATED) as archive:
archive.writestr("preflight.txt", "Mori release draft preflight\n")
PY
python3 - "$assets/${tag}.manifest.json" "$tag" <<'PY'
import json
import sys
with open(sys.argv[1], "w", encoding="utf-8") as stream:
json.dump({"schemaVersion": 1, "tag": sys.argv[2]}, stream)
PY
bash scripts/github-release-draft.sh --preflight \
--repo "$GITHUB_REPOSITORY" \
--tag "$tag" \
--target "$GITHUB_SHA" \
--title "Mori release draft preflight $tag" \
--make-latest false \
--download-assets \
--asset "$assets/${tag}.zip" \
--asset "$assets/${tag}.manifest.json"
54 changes: 47 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,54 @@ jobs:
name: appcast
path: mori-appcast.xml

- name: Create GitHub Release
- name: Verify release event tag ownership
run: |
set -euo pipefail
[[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" == v* ]]
[[ "$(git rev-parse "$GITHUB_REF_NAME^{commit}")" == "$GITHUB_SHA" ]] || {
echo "Release event tag does not resolve to the checked-out commit." >&2
exit 1
}

uses: softprops/action-gh-release@v2
with:
files: |
Mori-*-macos-arm64.zip
Mori-*-macos-arm64.dmg
generate_release_notes: true
- name: Create and verify draft release
id: release-draft
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
output="$(bash scripts/github-release-draft.sh --create-draft \
--repo "$GITHUB_REPOSITORY" \
--tag "$GITHUB_REF_NAME" \
--target "$GITHUB_SHA" \
--title "Mori $VERSION" \
--generate-notes \
--make-latest true \
--require-immutable \
--asset "Mori-${VERSION}-macos-arm64.zip" \
--asset "Mori-${VERSION}-macos-arm64.dmg")"
release_id="${output#release_id=}"
[[ "$release_id" =~ ^[0-9]+$ ]]
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"

- name: Publish complete immutable release
id: release-publish
env:
GH_TOKEN: ${{ github.token }}
run: |
bash scripts/github-release-draft.sh --publish-draft \
--repo "$GITHUB_REPOSITORY" \
--release-id "${{ steps.release-draft.outputs.release_id }}" \
--require-immutable

- name: Delete incomplete draft release
if: failure() && steps.release-draft.outputs.release_id != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
bash scripts/github-release-draft.sh --cleanup-draft \
--repo "$GITHUB_REPOSITORY" \
--release-id "${{ steps.release-draft.outputs.release_id }}"

- name: Cleanup keychain
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "vendor/ghostty"]
path = vendor/ghostty
url = https://github.com/ghostty-org/ghostty.git
url = https://github.com/h3nock/remux-ghostty.git
Loading
Loading