Skip to content
Open
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
167 changes: 88 additions & 79 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@ concurrency:
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch: {}
workflow_dispatch:
inputs:
tag:
description: "Release tag: <major>.<minor>.<patch> or <major>.<minor>.<patch>+<n>, optional 'v' prefix (defaults to v<VERSION>)"
required: false
type: string
publish:
description: "Publish the GitHub release for the resolved tag"
required: false
default: false
type: boolean
push:
branches: [main]
paths: ["VERSION"]
pull_request:
paths-ignore:
- "**/*.md"
Expand Down Expand Up @@ -55,12 +68,12 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-ghostty-

- name: Use tip version
if: ${{ github.event_name == 'schedule' }}
run: ./bin/repo-management.sh tip-version

- name: Setup build environment
run: |
if [ "${{ github.event_name }}" == "schedule" ]; then
echo "tip" > VERSION
fi
./bin/setup-env.sh
run: ./bin/setup-env.sh

- name: Build Ghostty
run: |
Expand All @@ -70,6 +83,9 @@ jobs:
run: |
./bin/bundle-appimage.sh

- name: Validate AppImage
run: ./bin/repo-management.sh validate-appimage

- name: Upload AppImage Artifacts
uses: actions/upload-artifact@v4
with:
Expand All @@ -90,49 +106,59 @@ jobs:
steps:
- uses: actions/checkout@v7 # zizmor: ignore[artipacked]

- name: Clean-up Old Release Assets
run: |
gh release view tip --json assets --jq '.assets[].name' | while read -r asset; do
if [ -n "$asset" ]; then
gh release delete-asset tip "${asset}" -y
fi
done
- name: Update 'tip' release
env:
GH_TOKEN: ${{ github.token }}

- name: Create 'tip' tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -fa tip -m "Latest Continuous Release" "${GITHUB_SHA}"
git push --force origin tip
run: ./bin/repo-management.sh tag-tip

release_stable:
name: 👻 Release Ghostty AppImage (Stable)
if: >-
${{
(github.event_name == 'push' && github.ref_name == 'main')
|| github.event_name == 'release'
|| (github.event_name == 'workflow_dispatch' && inputs.publish)
}}
needs:
- build_appimage
permissions:
actions: read
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- name: Resolve release tag
id: release
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
set -eu
tag="$(./bin/repo-management.sh resolve-tag)"
if [ -n "${tag}" ]; then
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
fi

- uses: actions/download-artifact@v6
if: steps.release.outputs.tag != ''
with:
name: ghostty-appimage-aarch64

- uses: actions/download-artifact@v6
if: steps.release.outputs.tag != ''
with:
name: ghostty-appimage-x86_64

- name: Ghostty stable
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: "*.AppImage*"
tag: ${{ github.ref }}
overwrite: true
file_glob: true
- name: Publish release
if: steps.release.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
run: ./bin/repo-management.sh publish "${TAG}"

release_nightly:
name: 👻 Release Ghostty AppImage (Nightly)
Expand All @@ -154,14 +180,9 @@ jobs:
name: ghostty-appimage-x86_64

- name: Ghostty Tip ("Nightly")
uses: softprops/action-gh-release@v3.0.3
with:
name: '👻 Ghostty Tip ("Nightly")'
prerelease: true
tag_name: tip
target_commitish: ${{ github.sha }}
files: |
*.AppImage*
env:
GH_TOKEN: ${{ github.token }}
run: ./bin/repo-management.sh publish-tip

create_release_pr:
name: "👻 Upstream Stable Release"
Expand All @@ -179,7 +200,7 @@ jobs:
id: detect
run: |
set -eu
if version="$(./bin/check-upstream-release.sh)"; then
if version="$(./bin/repo-management.sh detect)"; then
echo "New upstream stable release: ${version}"
echo "release=true" >> "${GITHUB_OUTPUT}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
Expand All @@ -188,51 +209,39 @@ jobs:
echo "release=false" >> "${GITHUB_OUTPUT}"
fi

- name: Push release branch and tag
if: ${{ steps.detect.outputs.release == 'true' }}
env:
VERSION: ${{ steps.detect.outputs.version }}
run: |
set -eux
branch="release/${VERSION}"
tag="v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then
git checkout -B "${branch}" "origin/${branch}"
else
git checkout -b "${branch}"
printf '%s\n' "${VERSION}" > VERSION
git add VERSION
git commit -m "chore(release): set VERSION to ${VERSION}"
git push origin "${branch}"
fi

if ! git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
git tag -a "${tag}" -m "Ghostty ${VERSION}"
git push origin "${tag}"
fi

- name: Open release pull request
if: ${{ steps.detect.outputs.release == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.detect.outputs.version }}
run: |
set -eux
branch="release/${VERSION}"
if [ -n "$(gh pr list --head "${branch}" --state open --json number --jq '.[0].number')" ]; then
echo "Open pull request for ${branch} already exists"
exit 0
fi
gh pr create \
--base main \
--head "${branch}" \
--title "👻 Release Ghostty ${VERSION}" \
--body "Automated release PR for upstream Ghostty \`${VERSION}\`.
run: ./bin/repo-management.sh open-pr "${VERSION}"

lint:
name: 🧹 pre-commit
if: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-python@v5
id: python
with:
python-version: "3.x"

- name: Cache pre-commit environments
uses: actions/cache@v6
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-${{ runner.os }}-${{ steps.python.outputs.python-version }}-

- Updates \`VERSION\` to \`${VERSION}\`
- Adds tag \`v${VERSION}\`
- name: Install pre-commit
run: pip install pre-commit

Upstream tag: https://github.com/ghostty-org/ghostty/releases/tag/v${VERSION}"
- name: Run pre-commit
run: ./bin/repo-management.sh lint
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
rev: v0.10.0.1
hooks:
- id: shellcheck
args: ["--severity=error"]
args: ["--severity=warning", "--shell=sh"]

- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.10.0-2
Expand Down
57 changes: 57 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# AGENTS.md

## What this repo is

Build/packaging repo for an unofficial **Ghostty AppImage**. There is no application source here, no package manager, and **no test suite** — it fetches upstream Ghostty source and produces AppImages for `x86_64` and `aarch64`. The deliverable is a GitHub release.

- All scripts are POSIX `sh` (`#!/bin/sh`) under `bin/` and must stay POSIX.
- Build artifacts are gitignored (`AppDir/`, `dist/`, `ghostty-*/`, `quick-sharun`, `appinfo`, `*.AppImage`, `*.tar.gz`). Never try to commit them.

## Commands

- Lint/format everything: `pre-commit run --all-files` (alias: `./bin/repo-management.sh lint`). Install hooks once with `pre-commit install`.
- Build — must run as **root inside an Arch container** (e.g. `ghcr.io/pkgforge-dev/archlinux`, which CI uses) because `setup-env.sh` calls `pacman`:
1. `./bin/setup-env.sh`
2. `./bin/build-ghostty.sh`
3. `./bin/bundle-appimage.sh`
4. `./bin/repo-management.sh validate-appimage`
- Focused verification after a build: `./bin/repo-management.sh validate-appimage` asserts the embedded `UPINFO` matches the expected channel and the zsync `SHA-1` matches the AppImage. `./dist/*.AppImage --appimage-updateinfo` prints the raw update info.
- `./bin/repo-management.sh` with no args prints usage.

## `bin/repo-management.sh` is the single CLI for release logic

All release/CI shell logic lives here as verbs; `ci.yaml` is orchestration only (triggers, permissions, matrix, artifact actions). **Do not inline release shell back into the workflow.**

Verbs: `tip-version`, `lint`, `validate-appimage`, `detect`, `validate-tag`, `resolve-tag`, `open-pr`, `publish`, `publish-tip`, `tag-tip`.

It reads env supplied by the workflow (`EVENT_NAME`, `RELEASE_TAG`, `INPUT_TAG`, `GITHUB_REPOSITORY`, `GITHUB_SHA`, `GH_TOKEN`) and honors `UPSTREAM_REPO` / `RELEASE_BASE_URL` overrides for local testing.

## VERSION, tags, UPINFO

- `VERSION` holds the upstream version. Stable = a clean `X.Y.Z`. Nightly: `tip-version` writes `tip`, then `build-ghostty.sh` overwrites `VERSION` with the tip snapshot (`X.Y.Z-main-+hash`).
- `+N` releases: `VERSION` stays the upstream `X.Y.Z`; only the git **tag** gets `+N` (e.g. `v1.2.0+1`). Never put `+N` in `VERSION` — the source URL `release.files.ghostty.org/<VERSION>/...` would break.
- Release tags must match `^v<major>.<minor>.<patch>(\+<n>)?$` (`validate_tag` enforces; invalid input fails). Non-version tags `tip`, `glfw`, `soar-nest` are utility and not releases.
- `bundle-appimage.sh` picks the UPINFO channel from `VERSION`: `latest` for clean `X.Y.Z` (stable), `tip` for snapshot versions (nightly). The asset glob `Ghostty-*<arch>.AppImage.zsync` absorbs the daily-changing nightly filename.

## CI / release flow (`ci.yaml`)

- **Daily schedule** builds tip (`tip-version`); a `tag` job force-updates the `tip` tag and clears its assets, then `release_nightly` publishes the prerelease.
- The same schedule runs `create_release_pr`: if upstream `ghostty-org/ghostty` has a newer stable tag **and** its CDN artifact exists, it creates `release/<v>`, bumps `VERSION`, pushes `v<v>`, and opens a PR.
- **Merging that PR** (push to `main` touching `VERSION`) builds once and `release_stable` publishes the GitHub release with assets — the only manual step.
- `release: published` handles manually published `+N` releases; `workflow_dispatch` inputs `tag`/`publish` are the manual override.
- PRs are intentionally skipped for VERSION-only changes (`paths-ignore`), so the release PR does not build. The `lint` job runs on PRs only.

## Conventions

- Commits: Conventional Commits (`feat(ci): ...`, `fix(bundle): ...`, `chore(deps): ...`).
- Branches: `feature/<x>`, `fix/<x>`, `release/<version>`. `no-commit-to-branch` blocks direct commits to `main`.
- Action updates are handled by Renovate (branches `renovate/*`, `chore(deps): update ...` PRs). Don't hand-edit action pins.

## Gotchas

- shellcheck runs at `--severity=warning --shell=sh`: bashisms (`[[ ]]`, arrays, `local`) fail. `export VAR="$(cmd)"` fails SC2155 — assign first, then `export`.
- `setup-env.sh` self-installs `get-debloated-pkgs` / `quick-sharun` when absent (CI provides them via `anylinux-setup-action`), so local builds work.
- `GITHUB_REPOSITORY` defaults to `pkgforge-dev/ghostty-appimage` in `build-ghostty.sh` / `bundle-appimage.sh`; without it local builds embed a broken `UPINFO`.
- `setup-env.sh` strips `.sframe`/`.rela.sframe` from `/usr/lib/*crt*.o` — required for GCC 15+ with Zig's self-hosted linker. Keep it.
- `build-ghostty.sh` derives the Zig version from `ghostty-<v>/build.zig.zon` and installs it under `/opt`, symlinking `/usr/local/bin/zig`.
- Workflows are checked by `actionlint` + `zizmor`; `.github/actionlint.yaml` whitelists the `ubuntu-24.04-arm` label. A checkout that persists credentials needs `# zizmor: ignore[artipacked]`, or set `persist-credentials: false`.
1 change: 0 additions & 1 deletion bin/build-ghostty.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ BUILD_ARGS="
-Dstrip=true"

if [ "${GHOSTTY_VERSION}" = "tip" ]; then
export UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|tip|Ghostty-*$ARCH.AppImage.zsync"
wget "https://github.com/ghostty-org/ghostty/releases/download/tip/ghostty-source.tar.gz" -O "ghostty-${GHOSTTY_VERSION}.tar.gz"
wget "https://github.com/ghostty-org/ghostty/releases/download/tip/ghostty-source.tar.gz.minisig" -O "ghostty-${GHOSTTY_VERSION}.tar.gz.minisig"
GHOSTTY_VERSION="$(tar -tf "ghostty-${GHOSTTY_VERSION}.tar.gz" --wildcards "*zig.zon.txt" | awk '-F[-/]' '{print $2"-"$3"-"$4}')"
Expand Down
10 changes: 9 additions & 1 deletion bin/bundle-appimage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ set -eux

ARCH="$(uname -m)"
GHOSTTY_VERSION="$(cat VERSION)"
GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-pkgforge-dev/ghostty-appimage}"

export UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|latest|Ghostty-*$ARCH.AppImage.zsync"
if printf '%s' "${GHOSTTY_VERSION}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
UPINFO_TAG="latest"
else
UPINFO_TAG="tip"
fi

UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|${UPINFO_TAG}|Ghostty-*$ARCH.AppImage.zsync"
export UPINFO
export DEPLOY_OPENGL=1
export EXEC_WRAPPER=1
export URUNTIME_PRELOAD=1
Expand Down
45 changes: 0 additions & 45 deletions bin/check-upstream-release.sh

This file was deleted.

Loading
Loading