From 73a8589b8a81419fa9bdc8ac8f0df594600db4db Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 08:02:11 +0530 Subject: [PATCH 1/8] feat(ci): publish stable release on VERSION merge Merging a release PR now triggers a push run scoped to VERSION: the AppImage is built once and a publish_release job creates the GitHub Release (auto notes + assets) when the v tag exists. The PR build stays skipped to avoid a duplicate run. Publishing in-run avoids relying on the release: published event, which GITHUB_TOKEN does not emit. --- .github/workflows/ci.yaml | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a3f15ea..7e26625 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,6 +7,9 @@ on: schedule: - cron: "0 0 * * *" workflow_dispatch: {} + push: + branches: [main] + paths: ["VERSION"] pull_request: paths-ignore: - "**/*.md" @@ -134,6 +137,52 @@ jobs: overwrite: true file_glob: true + publish_release: + name: ๐Ÿ‘ป Publish Ghostty AppImage (Release) + if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + needs: + - build_appimage + permissions: + actions: read + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 # zizmor: ignore[artipacked] + + - name: Resolve release tag + id: release + run: | + set -eu + version="$(cat VERSION)" + tag="v${version}" + if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + echo "Found tag ${tag}, publishing release" + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "version=${version}" >> "${GITHUB_OUTPUT}" + else + echo "No tag ${tag} for VERSION ${version}, nothing to release" + 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: Publish release + if: steps.release.outputs.tag != '' + uses: softprops/action-gh-release@v3.0.2 + with: + tag_name: ${{ steps.release.outputs.tag }} + name: Ghostty ${{ steps.release.outputs.version }} + generate_release_notes: true + files: | + *.AppImage* + release_nightly: name: ๐Ÿ‘ป Release Ghostty AppImage (Nightly) if: ${{ github.event_name == 'schedule' && github.ref_name == 'main' }} From 8b236b2e1fb49af9917225abce87151b74577159 Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 08:05:38 +0530 Subject: [PATCH 2/8] refactor(ci): drop manual release event path Stable releases are only produced by the merge-triggered push run, so remove the release: published trigger, the event-gated release_stable job, and the svenstaro upload action. The single release_stable job now publishes via softprops/action-gh-release. --- .github/workflows/ci.yaml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e26625..f29cc64 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,8 +15,6 @@ on: - "**/*.md" - "**/VERSION" types: [opened, synchronize, reopened] - release: - types: [published] jobs: build_appimage: @@ -112,33 +110,6 @@ jobs: release_stable: name: ๐Ÿ‘ป Release Ghostty AppImage (Stable) - needs: - - build_appimage - permissions: - actions: read - contents: write - runs-on: ubuntu-latest - if: ${{ github.event_name == 'release' }} - steps: - - uses: actions/download-artifact@v6 - with: - name: ghostty-appimage-aarch64 - - - uses: actions/download-artifact@v6 - 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 - - publish_release: - name: ๐Ÿ‘ป Publish Ghostty AppImage (Release) if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} needs: - build_appimage From e20b29669d0dffeb000adf88efaffba9785f08e7 Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 08:12:28 +0530 Subject: [PATCH 3/8] feat(ci): support +1 release event and manual publish override Restore the release: published trigger so manually published v+N releases build and attach assets via the existing flow, and add workflow_dispatch inputs (tag, publish) for manual overrides. A single release_stable job resolves the tag from the event: push uses v (requires the tag), release uses the published tag, and dispatch uses the optional tag input. Publishing uses softprops for create and asset upload, preserving the release body on the +1 path. --- .github/workflows/ci.yaml | 55 ++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f29cc64..fb9e8f1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,7 +6,17 @@ concurrency: on: schedule: - cron: "0 0 * * *" - workflow_dispatch: {} + workflow_dispatch: + inputs: + tag: + description: "Release tag to publish (defaults to v), e.g. v1.3.1+1" + 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"] @@ -15,6 +25,8 @@ on: - "**/*.md" - "**/VERSION" types: [opened, synchronize, reopened] + release: + types: [published] jobs: build_appimage: @@ -110,7 +122,12 @@ jobs: release_stable: name: ๐Ÿ‘ป Release Ghostty AppImage (Stable) - if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + if: >- + ${{ + (github.event_name == 'push' && github.ref_name == 'main') + || github.event_name == 'release' + || (github.event_name == 'workflow_dispatch' && inputs.publish) + }} needs: - build_appimage permissions: @@ -122,17 +139,27 @@ jobs: - 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 - version="$(cat VERSION)" - tag="v${version}" - if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then - echo "Found tag ${tag}, publishing release" - echo "tag=${tag}" >> "${GITHUB_OUTPUT}" - echo "version=${version}" >> "${GITHUB_OUTPUT}" + if [ "${EVENT_NAME}" = "release" ]; then + tag="${RELEASE_TAG}" + elif [ -n "${INPUT_TAG}" ]; then + tag="${INPUT_TAG}" else - echo "No tag ${tag} for VERSION ${version}, nothing to release" + tag="v$(cat VERSION)" + if [ "${EVENT_NAME}" = "push" ] && ! git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + echo "No tag ${tag} for VERSION $(cat VERSION), nothing to release" + exit 0 + fi fi + version="${tag#v}" + echo "Publishing release ${tag} (version ${version})" + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "version=${version}" >> "${GITHUB_OUTPUT}" - uses: actions/download-artifact@v6 if: steps.release.outputs.tag != '' @@ -145,7 +172,7 @@ jobs: name: ghostty-appimage-x86_64 - name: Publish release - if: steps.release.outputs.tag != '' + if: steps.release.outputs.tag != '' && github.event_name != 'release' uses: softprops/action-gh-release@v3.0.2 with: tag_name: ${{ steps.release.outputs.tag }} @@ -154,6 +181,14 @@ jobs: files: | *.AppImage* + - name: Upload release assets + if: steps.release.outputs.tag != '' && github.event_name == 'release' + uses: softprops/action-gh-release@v3.0.2 + with: + tag_name: ${{ steps.release.outputs.tag }} + files: | + *.AppImage* + release_nightly: name: ๐Ÿ‘ป Release Ghostty AppImage (Nightly) if: ${{ github.event_name == 'schedule' && github.ref_name == 'main' }} From b275e45d532cc133f34aec489df5ebd79b9bc9fc Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 08:17:05 +0530 Subject: [PATCH 4/8] feat(ci): strictly validate release tag pattern Resolve release tag now normalizes an optional 'v' prefix and fails unless the tag matches ^v..(\+)?$, the same pattern used by all existing vX.Y.Z / vX.Y.Z+N release tags. Applies to the workflow_dispatch tag input and the event-derived tags. --- .github/workflows/ci.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fb9e8f1..5ce244b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: tag: - description: "Release tag to publish (defaults to v), e.g. v1.3.1+1" + description: "Release tag: .. or ..+, optional 'v' prefix (defaults to v)" required: false type: string publish: @@ -156,6 +156,17 @@ jobs: exit 0 fi fi + + case "${tag}" in + v*) ;; + *) tag="v${tag}" ;; + esac + + if ! printf '%s\n' "${tag}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?$'; then + echo "Invalid release tag '${tag}': expected .. or ..+ (optional 'v' prefix)" >&2 + exit 1 + fi + version="${tag#v}" echo "Publishing release ${tag} (version ${version})" echo "tag=${tag}" >> "${GITHUB_OUTPUT}" From 4945a6d8c5ef09505c397dc88c04f6c63c9494ee Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 08:51:54 +0530 Subject: [PATCH 5/8] feat(ci): automate stable releases and consolidate release tooling - daily schedule detects upstream stable tags and opens release/ PRs; merge publishes the release with assets, no manual step - +N releases via release: published; workflow_dispatch tag/publish override with strict vX.Y.Z(+N) validation - move all release logic into bin/repo-management.sh verbs; ci.yaml is orchestration only, dropping softprops/svenstaro in favor of gh - enforce POSIX sh via shellcheck --severity=warning and add a PR-only lint job to ci.yaml - setup-env installs get-debloated-pkgs/quick-sharun when missing so local builds work - default GITHUB_REPOSITORY so local builds embed a valid UPINFO --- .github/workflows/ci.yaml | 160 ++++++++------------------ .pre-commit-config.yaml | 2 +- bin/build-ghostty.sh | 4 +- bin/bundle-appimage.sh | 4 +- bin/check-upstream-release.sh | 45 -------- bin/repo-management.sh | 208 ++++++++++++++++++++++++++++++++++ bin/setup-env.sh | 12 ++ 7 files changed, 277 insertions(+), 158 deletions(-) delete mode 100755 bin/check-upstream-release.sh create mode 100755 bin/repo-management.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5ce244b..0934ae5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -68,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: | @@ -103,22 +103,10 @@ 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) @@ -135,7 +123,9 @@ jobs: contents: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 # zizmor: ignore[artipacked] + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Resolve release tag id: release @@ -145,33 +135,11 @@ jobs: INPUT_TAG: ${{ inputs.tag }} run: | set -eu - if [ "${EVENT_NAME}" = "release" ]; then - tag="${RELEASE_TAG}" - elif [ -n "${INPUT_TAG}" ]; then - tag="${INPUT_TAG}" - else - tag="v$(cat VERSION)" - if [ "${EVENT_NAME}" = "push" ] && ! git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then - echo "No tag ${tag} for VERSION $(cat VERSION), nothing to release" - exit 0 - fi + tag="$(./bin/repo-management.sh resolve-tag)" + if [ -n "${tag}" ]; then + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" fi - case "${tag}" in - v*) ;; - *) tag="v${tag}" ;; - esac - - if ! printf '%s\n' "${tag}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?$'; then - echo "Invalid release tag '${tag}': expected .. or ..+ (optional 'v' prefix)" >&2 - exit 1 - fi - - version="${tag#v}" - echo "Publishing release ${tag} (version ${version})" - echo "tag=${tag}" >> "${GITHUB_OUTPUT}" - echo "version=${version}" >> "${GITHUB_OUTPUT}" - - uses: actions/download-artifact@v6 if: steps.release.outputs.tag != '' with: @@ -183,22 +151,11 @@ jobs: name: ghostty-appimage-x86_64 - name: Publish release - if: steps.release.outputs.tag != '' && github.event_name != 'release' - uses: softprops/action-gh-release@v3.0.2 - with: - tag_name: ${{ steps.release.outputs.tag }} - name: Ghostty ${{ steps.release.outputs.version }} - generate_release_notes: true - files: | - *.AppImage* - - - name: Upload release assets - if: steps.release.outputs.tag != '' && github.event_name == 'release' - uses: softprops/action-gh-release@v3.0.2 - with: - tag_name: ${{ steps.release.outputs.tag }} - files: | - *.AppImage* + 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) @@ -220,14 +177,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" @@ -245,7 +197,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}" @@ -254,51 +206,39 @@ jobs: echo "release=false" >> "${GITHUB_OUTPUT}" fi - - name: Push release branch and tag + - 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}" - tag="v${VERSION}" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + run: ./bin/repo-management.sh open-pr "${VERSION}" - 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 + 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 - 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 + - uses: actions/setup-python@v5 + id: python + with: + python-version: "3.x" - - 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}\`. + - 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d359fac..44adfe4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/bin/build-ghostty.sh b/bin/build-ghostty.sh index 6f89cd4..b6a6084 100755 --- a/bin/build-ghostty.sh +++ b/bin/build-ghostty.sh @@ -4,6 +4,7 @@ set -eux ARCH="$(uname -m)" GHOSTTY_VERSION="$(cat VERSION)" +GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-pkgforge-dev/ghostty-appimage}" PUB_KEY="RWQlAjJC23149WL2sEpT/l0QKy7hMIFhYdQOFy0Z7z7PbneUgvlsnYcV" rm -rf AppDir dist ghostty-* @@ -21,7 +22,8 @@ BUILD_ARGS=" -Dstrip=true" if [ "${GHOSTTY_VERSION}" = "tip" ]; then - export UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|tip|Ghostty-*$ARCH.AppImage.zsync" + UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|tip|Ghostty-*$ARCH.AppImage.zsync" + export UPINFO 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}')" diff --git a/bin/bundle-appimage.sh b/bin/bundle-appimage.sh index 16b6954..0f6797f 100755 --- a/bin/bundle-appimage.sh +++ b/bin/bundle-appimage.sh @@ -4,8 +4,10 @@ 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" +UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|latest|Ghostty-*$ARCH.AppImage.zsync" +export UPINFO export DEPLOY_OPENGL=1 export EXEC_WRAPPER=1 export URUNTIME_PRELOAD=1 diff --git a/bin/check-upstream-release.sh b/bin/check-upstream-release.sh deleted file mode 100755 index d6e3f8e..0000000 --- a/bin/check-upstream-release.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh - -set -eux - -UPSTREAM_REPO="${UPSTREAM_REPO:-ghostty-org/ghostty}" -RELEASE_BASE_URL="${RELEASE_BASE_URL:-https://release.files.ghostty.org}" - -get_latest_stable_tag() { - git ls-remote --tags --refs "https://github.com/${UPSTREAM_REPO}.git" | - sed 's#.*refs/tags/##' | - grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | - sed 's/^v//' | - sort -V | - tail -n1 -} - -release_artifact_exists() { - curl -fsI "${RELEASE_BASE_URL}/${1}/ghostty-${1}.tar.gz" >/dev/null 2>&1 -} - -current="$(cat VERSION)" -latest="$(get_latest_stable_tag)" - -if [ -z "${latest}" ]; then - echo "No upstream stable release found" >&2 - exit 1 -fi - -if ! release_artifact_exists "${latest}"; then - echo "Upstream ${latest} has no published release artifact yet" >&2 - exit 1 -fi - -if [ "${latest}" = "${current}" ]; then - echo "Already on latest upstream release ${current}" >&2 - exit 1 -fi - -newest="$(printf '%s\n%s\n' "${current}" "${latest}" | sort -V | tail -n1)" -if [ "${newest}" != "${latest}" ]; then - echo "Current version ${current} is newer than upstream ${latest}" >&2 - exit 1 -fi - -echo "${latest}" diff --git a/bin/repo-management.sh b/bin/repo-management.sh new file mode 100755 index 0000000..61a2bc7 --- /dev/null +++ b/bin/repo-management.sh @@ -0,0 +1,208 @@ +#!/bin/sh + +set -eux + +UPSTREAM_REPO="${UPSTREAM_REPO:-ghostty-org/ghostty}" +RELEASE_BASE_URL="${RELEASE_BASE_URL:-https://release.files.ghostty.org}" + +BOT_NAME="github-actions[bot]" +BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" + +log() { + echo "$*" >&2 +} + +get_latest_stable_tag() { + git ls-remote --tags --refs "https://github.com/${UPSTREAM_REPO}.git" | + sed 's#.*refs/tags/##' | + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | + sed 's/^v//' | + sort -V | + tail -n1 +} + +release_artifact_exists() { + curl -fsI "${RELEASE_BASE_URL}/${1}/ghostty-${1}.tar.gz" >/dev/null 2>&1 +} + +validate_tag() { + tag="${1:-}" + case "${tag}" in + v*) ;; + *) tag="v${tag}" ;; + esac + if ! printf '%s\n' "${tag}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?$'; then + log "Invalid release tag '${tag}': expected .. or ..+ (optional 'v' prefix)" + exit 1 + fi + printf '%s\n' "${tag}" +} + +tag_exists() { + git ls-remote --exit-code --tags origin "refs/tags/${1}" >/dev/null 2>&1 +} + +branch_exists() { + git ls-remote --exit-code --heads origin "refs/heads/${1}" >/dev/null 2>&1 +} + +require_assets() { + set -- ./*.AppImage* + if [ ! -e "${1}" ]; then + log "No AppImage assets found in $(pwd)" + exit 1 + fi +} + +detect_release() { + current="$(cat VERSION)" + latest="$(get_latest_stable_tag)" + if [ -z "${latest}" ]; then + log "No upstream stable release found" + return 1 + fi + if ! release_artifact_exists "${latest}"; then + log "Upstream ${latest} has no published release artifact yet" + return 1 + fi + if [ "${latest}" = "${current}" ]; then + log "Already on latest upstream release ${current}" + return 1 + fi + newest="$(printf '%s\n%s\n' "${current}" "${latest}" | sort -V | tail -n1)" + if [ "${newest}" != "${latest}" ]; then + log "Current version ${current} is newer than upstream ${latest}" + return 1 + fi + printf '%s\n' "${latest}" +} + +cmd_tip_version() { + echo "tip" >VERSION +} + +cmd_lint() { + pre-commit run --all-files --show-diff-on-failure +} + +cmd_resolve_tag() { + if [ "${EVENT_NAME:-}" = "release" ]; then + tag="${RELEASE_TAG:-}" + elif [ -n "${INPUT_TAG:-}" ]; then + tag="${INPUT_TAG}" + else + tag="v$(cat VERSION)" + if [ "${EVENT_NAME:-}" = "push" ] && ! tag_exists "${tag}"; then + log "No tag ${tag} for VERSION $(cat VERSION), nothing to release" + return 0 + fi + fi + validate_tag "${tag}" +} + +cmd_open_pr() { + version="${1:-}" + if [ -z "${version}" ]; then + if ! version="$(detect_release)"; then + log "Nothing to release" + return 0 + fi + fi + tag="$(validate_tag "v${version}")" + branch="release/${version}" + + git config user.name "${BOT_NAME}" + git config user.email "${BOT_EMAIL}" + + if branch_exists "${branch}"; 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 ! tag_exists "${tag}"; then + git tag -a "${tag}" -m "Ghostty ${version}" + git push origin "${tag}" + fi + + if [ -n "$(gh pr list --head "${branch}" --state open --json number --jq '.[0].number')" ]; then + log "Open pull request for ${branch} already exists" + return 0 + fi + + gh pr create \ + --base main \ + --head "${branch}" \ + --title "๐Ÿ‘ป Release Ghostty ${version}" \ + --body "Automated release PR for upstream Ghostty \`${version}\`. + +- Updates \`VERSION\` to \`${version}\` +- Adds tag \`v${version}\` + +Upstream tag: https://github.com/${UPSTREAM_REPO}/releases/tag/v${version}" +} + +cmd_publish() { + tag="$(validate_tag "${1:-}")" + version="${tag#v}" + + require_assets + + if gh release view "${tag}" >/dev/null 2>&1; then + log "Release ${tag} exists, uploading assets" + else + log "Creating release ${tag}" + gh release create "${tag}" --title "Ghostty ${version}" --generate-notes + fi + gh release upload "${tag}" ./*.AppImage* --clobber +} + +cmd_publish_tip() { + require_assets + + if gh release view tip >/dev/null 2>&1; then + log "Tip release exists, uploading assets" + else + log "Creating tip release" + gh release create tip --prerelease --title '๐Ÿ‘ป Ghostty Tip ("Nightly")' --notes "Latest nightly build of Ghostty" + fi + gh release upload tip ./*.AppImage* --clobber +} + +cmd_tag_tip() { + 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 + + git config user.name "${BOT_NAME}" + git config user.email "${BOT_EMAIL}" + git tag -fa tip -m "Latest Continuous Release" "${GITHUB_SHA}" + git push --force origin tip +} + +command="${1:-}" +if [ "$#" -gt 0 ]; then + shift +fi + +case "${command}" in +tip-version) cmd_tip_version "$@" ;; +lint) cmd_lint "$@" ;; +detect) detect_release "$@" ;; +validate-tag) validate_tag "$@" ;; +resolve-tag) cmd_resolve_tag "$@" ;; +open-pr) cmd_open_pr "$@" ;; +publish) cmd_publish "$@" ;; +publish-tip) cmd_publish_tip "$@" ;; +tag-tip) cmd_tag_tip "$@" ;; +*) + log "Usage: $0 [args]" + exit 1 + ;; +esac diff --git a/bin/setup-env.sh b/bin/setup-env.sh index ea5c43a..a286a2b 100755 --- a/bin/setup-env.sh +++ b/bin/setup-env.sh @@ -31,8 +31,20 @@ GH_BASE="https://github.com" MINISIGN_URL="${GH_BASE}/jedisct1/minisign/releases/download/${MINISIGN_VERSION}/minisign-${MINISIGN_VERSION}-linux.tar.gz" # Install Debloated Pkgs (get-debloated-pkgs provided by anylinux-setup-action) +if ! command -v get-debloated-pkgs >/dev/null 2>&1; then + echo "get-debloated-pkgs not found, installing..." + wget "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/get-debloated-pkgs.sh" -O /usr/local/bin/get-debloated-pkgs + chmod +x /usr/local/bin/get-debloated-pkgs +fi get-debloated-pkgs --add-common --prefer-nano +# quick-sharun (also provided by anylinux-setup-action) +if ! command -v quick-sharun >/dev/null 2>&1; then + echo "quick-sharun not found, installing..." + wget "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh" -O /usr/local/bin/quick-sharun + chmod +x /usr/local/bin/quick-sharun +fi + # minisign: https://github.com/jedisct1/minisign rm -rf /usr/local/bin/minisign wget "${MINISIGN_URL}" -O /tmp/minisign-linux.tar.gz From f69e9d3c088e9a0f48bccf15a1a5d94bee00f58a Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 08:58:53 +0530 Subject: [PATCH 6/8] fix(bundle): use tip tag for nightly AppImage UPINFO bundle-appimage.sh unconditionally embedded 'latest', so nightly AppImages tracked the newest stable release and never offered nightly updates. Pick the tag from the channel: 'latest' for a clean X.Y.Z VERSION (stable) and 'tip' for a snapshot version (X.Y.Z-main-+hash). Drop the dead 'tip' UPINFO from build-ghostty.sh, which ran in a separate step and was overwritten. --- bin/build-ghostty.sh | 3 --- bin/bundle-appimage.sh | 8 +++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/build-ghostty.sh b/bin/build-ghostty.sh index b6a6084..0988cf4 100755 --- a/bin/build-ghostty.sh +++ b/bin/build-ghostty.sh @@ -4,7 +4,6 @@ set -eux ARCH="$(uname -m)" GHOSTTY_VERSION="$(cat VERSION)" -GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-pkgforge-dev/ghostty-appimage}" PUB_KEY="RWQlAjJC23149WL2sEpT/l0QKy7hMIFhYdQOFy0Z7z7PbneUgvlsnYcV" rm -rf AppDir dist ghostty-* @@ -22,8 +21,6 @@ BUILD_ARGS=" -Dstrip=true" if [ "${GHOSTTY_VERSION}" = "tip" ]; then - UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|tip|Ghostty-*$ARCH.AppImage.zsync" - export UPINFO 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}')" diff --git a/bin/bundle-appimage.sh b/bin/bundle-appimage.sh index 0f6797f..5120d1b 100755 --- a/bin/bundle-appimage.sh +++ b/bin/bundle-appimage.sh @@ -6,7 +6,13 @@ ARCH="$(uname -m)" GHOSTTY_VERSION="$(cat VERSION)" GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-pkgforge-dev/ghostty-appimage}" -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 From 2a4c7cda8f5006cd6c3439561a577ce5360def82 Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 09:04:27 +0530 Subject: [PATCH 7/8] feat(ci): validate AppImage UPINFO and zsync after build Add a validate-appimage verb and run it in the build job right after bundling. It asserts the embedded update info matches the expected channel tag (latest for stable X.Y.Z, tip for snapshot versions) and that the zsync SHA-1 matches the AppImage, logging both for auditing. --- .github/workflows/ci.yaml | 3 +++ bin/repo-management.sh | 48 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0934ae5..cc34594 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,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: diff --git a/bin/repo-management.sh b/bin/repo-management.sh index 61a2bc7..d09906f 100755 --- a/bin/repo-management.sh +++ b/bin/repo-management.sh @@ -186,6 +186,51 @@ cmd_tag_tip() { git push --force origin tip } +cmd_validate_appimage() { + version="$(cat VERSION)" + if printf '%s' "${version}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + tag="latest" + else + tag="tip" + fi + + arch="$(uname -m)" + repo="${GITHUB_REPOSITORY:-pkgforge-dev/ghostty-appimage}" + expected="gh-releases-zsync|$(printf '%s' "${repo}" | tr '/' '|')|${tag}|Ghostty-*${arch}.AppImage.zsync" + + validated=0 + for appimage in ./dist/Ghostty-*-"${arch}".AppImage; do + [ -e "${appimage}" ] || continue + validated=1 + + actual="$("${appimage}" --appimage-updateinfo)" + if [ "${actual}" != "${expected}" ]; then + log "UPINFO mismatch for ${appimage}" + log " expected: ${expected}" + log " actual: ${actual}" + exit 1 + fi + log "UPINFO OK ${actual}" + + zsync="${appimage}.zsync" + [ -e "${zsync}" ] || continue + want_sha="$(grep -a '^SHA-1:' "${zsync}" | awk '{print $2}')" + got_sha="$(sha1sum "${appimage}" | awk '{print $1}')" + if [ "${want_sha}" != "${got_sha}" ]; then + log "SHA-1 mismatch for ${appimage}" + log " zsync: ${want_sha}" + log " sha1: ${got_sha}" + exit 1 + fi + log "SHA-1 OK ${got_sha}" + done + + if [ "${validated}" -eq 0 ]; then + log "No AppImage found for ${arch} in $(pwd)/dist" + exit 1 + fi +} + command="${1:-}" if [ "$#" -gt 0 ]; then shift @@ -194,6 +239,7 @@ fi case "${command}" in tip-version) cmd_tip_version "$@" ;; lint) cmd_lint "$@" ;; +validate-appimage) cmd_validate_appimage "$@" ;; detect) detect_release "$@" ;; validate-tag) validate_tag "$@" ;; resolve-tag) cmd_resolve_tag "$@" ;; @@ -202,7 +248,7 @@ publish) cmd_publish "$@" ;; publish-tip) cmd_publish_tip "$@" ;; tag-tip) cmd_tag_tip "$@" ;; *) - log "Usage: $0 [args]" + log "Usage: $0 [args]" exit 1 ;; esac From 4552b1db56a7ef9f8b4abf1d16ead681dbb04350 Mon Sep 17 00:00:00 2001 From: psadi Date: Wed, 16 Sep 2026 09:11:45 +0530 Subject: [PATCH 8/8] docs: add AGENTS.md --- AGENTS.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6f9275b --- /dev/null +++ b/AGENTS.md @@ -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//...` would break. +- Release tags must match `^v..(\+)?$` (`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-*.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/`, bumps `VERSION`, pushes `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/`, `fix/`, `release/`. `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-/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`.