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
215 changes: 215 additions & 0 deletions .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Pre-release: pushing a pre-release v* tag issues a pre-release
# automatically.
#
# git tag -a v0.2.0-rc.1 -m "TetherKit 0.2.0 rc 1" && git push origin v0.2.0-rc.1
#
# Cut rc tags only from current main: tag-filtered workflows are resolved
# from the TAGGED commit, not from main, so an rc tag on a commit older
# than this file runs the OLD release.yml -- its tags: ["v*"] matches every
# v* tag -- and would publish a stable Release and bump the tap. The v*-* /
# !v*-* split only guards tags cut from commits that carry it.
#
# Semver puts the pre-release part after a hyphen, so the trigger below is
# "tag contains a hyphen" (v*-*). release.yml excludes exactly those tags,
# so a tag push starts one of the two workflows, never both.
#
# Deliberate differences from release.yml:
#
# 1. The Release is created with --prerelease. GitHub never serves a
# marked pre-release from the releases/latest endpoint, and that
# endpoint is what UpdateChecker.swift queries, so the app's Check
# for Updates will not offer a pre-release, and the Latest badge
# stays on the stable release.
#
# 2. There is no update-tap job. The tap is what users `brew install`
# from, and it must point at stable versions only; a pre-release
# tag never drives it. The tap moves when the stable tag is cut
# from the same base version, via release.yml's update-tap job.
#
# Like release.yml, this workflow builds and unit-tests on its own (ctest
# plus the GUI packaging), so a tag cut after a main-branch regression is
# still caught at release time.

name: Pre-release

on:
push:
# A hyphen is semver's pre-release marker (v0.2.0-rc.1, v0.2.0-beta.2).
# release.yml excludes hyphenated tags, so exactly one of the two
# release workflows runs per tag push.
tags: ["v*-*"]

# Creating the Release and uploading assets need write access.
permissions:
contents: write

jobs:
prerelease:
name: Build and publish ${{ github.ref_name }} (pre-release)
runs-on: macos-26

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Same check as release.yml: the tag and CMakeLists' VERSION must
# agree, otherwise `tetherkit-cli --version` will not match the
# release. A pre-release tag carries a suffix (v0.2.0-rc.1) that
# CMake's project() VERSION cannot hold (components are numeric
# only), so the comparison uses the base version: everything before
# the first hyphen. The suffix stays tag metadata only; the binaries
# it produces still report the base version.
- name: Verify the tag matches the CMakeLists VERSION
run: |
tag_ver="${GITHUB_REF_NAME#v}"
base_ver="${tag_ver%%-*}"
cmake_ver=$(grep -E '^[[:space:]]+VERSION[[:space:]]+[0-9]' CMakeLists.txt \
| head -1 | awk '{print $2}')
echo "tag=${tag_ver} base=${base_ver} CMakeLists.txt=${cmake_ver}"
if [ "$base_ver" != "$cmake_ver" ]; then
echo "::error::pre-release tag (${tag_ver}) base version (${base_ver}) does not match CMakeLists.txt VERSION (${cmake_ver})"
exit 1
fi

- name: Install libusb
run: brew install libusb

- name: Build and test
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DTETHERKIT_WARNINGS_AS_ERRORS=ON
cmake --build build -j"$(sysctl -n hw.ncpu)"
ctest --test-dir build --output-on-failure

- name: Build the GUI
run: cmake --build build --target gui

- name: Package the command-line binary
id: pkg
run: |
name="tetherkit-cli-${GITHUB_REF_NAME}-macos-arm64.tar.gz"
tar -C build/bin -czf "$name" tetherkit-cli
{
echo "name=${name}"
echo "sha256=$(shasum -a 256 "$name" | awk '{print $1}')"
} >> "$GITHUB_OUTPUT"

- name: Package TetherKit.app
id: app
run: |
name="TetherKit-${GITHUB_REF_NAME}-macos-arm64.zip"
# ditto preserves the bundle structure and extended attributes; it is
# the standard way to package a .app.
ditto -c -k --keepParent dist/TetherKit.app "$name"
{
echo "name=${name}"
echo "sha256=$(shasum -a 256 "$name" | awk '{print $1}')"
} >> "$GITHUB_OUTPUT"

# Unlike release.yml, no formula consumes this sha256: the tap is not
# touched by pre-releases. It still goes into the notes so anyone
# building the same tag from source can verify their tarball.
- name: Compute the source tarball sha256
id: src
run: |
url="https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${GITHUB_REF_NAME}.tar.gz"
# A freshly pushed tag can take a few seconds before GitHub generates
# the archive; retry a few times.
for i in 1 2 3 4 5; do
if curl -fsSL "$url" -o src.tar.gz; then break; fi
echo "attempt ${i}: source archive not fetched yet, retrying in 5s…"
sleep 5
done
test -s src.tar.gz || { echo "::error::could not fetch the source archive ${url}"; exit 1; }
echo "sha256=$(shasum -a 256 src.tar.gz | awk '{print $1}')" >> "$GITHUB_OUTPUT"

# The quoted heredoc keeps the shell from expanding anything, and
# nothing is interpolated into the script itself: the checksum values
# and asset names travel as env and are appended with printf, so their
# content is data, never shell text.
#
# No Homebrew install section: the tap does not carry this version, so
# pointing the reader at it would be a dead end. The assets and a source
# build of the same tag are the two ways to get the binaries.
- name: Assemble the release notes
env:
SRC_SHA256: ${{ steps.src.outputs.sha256 }}
PKG_NAME: ${{ steps.pkg.outputs.name }}
PKG_SHA256: ${{ steps.pkg.outputs.sha256 }}
APP_NAME: ${{ steps.app.outputs.name }}
APP_SHA256: ${{ steps.app.outputs.sha256 }}
run: |
cat > notes.md <<'EOF'
## Pre-release

This is a **pre-release**, cut from a development tag for testing
ahead of the stable release. Expect rough edges; when you report
one, include the tag name you are running.

The Homebrew tap is **not** updated for pre-releases. It only ever
points at stable versions, so `brew install` / `brew upgrade` will
not offer this build; the tap moves when the stable release is cut
from this same base version.

## Install

There is no Homebrew route (see above), so the choices are the
assets below or a source build of the same tag. The assets are
**unsigned**, so Gatekeeper quarantines them after a browser
download; remove the quarantine manually to run them (locally
built artifacts do not carry the flag):

```bash
xattr -d com.apple.quarantine tetherkit-cli # CLI
xattr -dr com.apple.quarantine TetherKit.app # GUI (recursive)
```

## Command line

```bash
tetherkit-cli --list # list recognized devices, no root needed
sudo tetherkit-cli # start the driver, needs root
```

## Requirements

- CLI: macOS 13.3+; GUI: macOS 14+ (all assets are arm64-only)
- The CLI needs libusb at runtime: `brew install libusb`. The .app
embeds its own copy, so no extra install for the GUI.

## Checksums

| File | SHA-256 |
|---|---|
EOF
{
printf "| %s | \`%s\` |\n" "Source tarball (for a source build of this tag)" "${SRC_SHA256}"
printf "| \`%s\` | \`%s\` |\n" "${PKG_NAME}" "${PKG_SHA256}"
printf "| \`%s\` | \`%s\` |\n" "${APP_NAME}" "${APP_SHA256}"
} >> notes.md

# --prerelease: GitHub marks the Release as a pre-release. A marked
# pre-release is never served by releases/latest (the endpoint
# UpdateChecker.swift queries), so the app's Check for Updates will
# not offer this build; it also keeps the Latest badge on the stable
# release.
- name: Create the Release
env:
GH_TOKEN: ${{ github.token }}
PKG_NAME: ${{ steps.pkg.outputs.name }}
APP_NAME: ${{ steps.app.outputs.name }}
run: |
gh release create "${GITHUB_REF_NAME}" \
--title "TetherKit ${GITHUB_REF_NAME}" \
--prerelease \
--notes-file notes.md \
--generate-notes \
--verify-tag \
"${PKG_NAME}" \
"${APP_NAME}"

# No update-tap job, deliberately: pre-releases never touch the tap. The
# tap is what users `brew install` from, and it must point at stable
# versions only -- that is also what keeps a pre-release out of every
# user's `brew upgrade`. release.yml's update-tap job moves the tap when
# the stable tag cut from the same base version lands.
40 changes: 30 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Release: pushing a v* tag issues a release automatically.
# Release: pushing a stable v* tag issues a release automatically.
#
# git tag -a v0.2.0 -m "TetherKit 0.2.0" && git push origin v0.2.0
#
# Pre-release tags (a hyphen marks the semver pre-release part, e.g.
# v0.2.0-rc.1) are excluded below and handled by pre-release.yml, which
# publishes a marked pre-release and never touches the Homebrew tap.
#
# This workflow builds and unit-tests on its own (ctest plus the GUI
# packaging), so a tag cut after a main-branch regression is still caught at
# release time. It deliberately does NOT run the whole matrix - no Swift
Expand All @@ -12,7 +16,12 @@ name: Release

on:
push:
tags: ["v*"]
# Stable tags only. The exclusion is what keeps a pre-release tag
# (v*-*: the hyphen is semver's pre-release marker) from starting this
# workflow -- otherwise it would publish a stable Release AND bump the
# tap. Hyphenated tags belong to pre-release.yml, so exactly one of the
# two workflows runs per tag push.
tags: ["v*", "!v*-*"]

# Creating the Release and uploading assets need write access.
permissions:
Expand Down Expand Up @@ -93,10 +102,17 @@ jobs:
test -s src.tar.gz || { echo "::error::could not fetch the source archive ${url}"; exit 1; }
echo "sha256=$(shasum -a 256 src.tar.gz | awk '{print $1}')" >> "$GITHUB_OUTPUT"

# The quoted heredoc keeps the shell from expanding anything; ${{ }} is
# substituted by Actions before the shell runs, so the step outputs above
# still resolve.
# The quoted heredoc keeps the shell from expanding anything, and
# nothing is interpolated into the script itself: the checksum values
# and asset names travel as env and are appended with printf, so their
# content is data, never shell text.
- name: Assemble the release notes
env:
SRC_SHA256: ${{ steps.src.outputs.sha256 }}
PKG_NAME: ${{ steps.pkg.outputs.name }}
PKG_SHA256: ${{ steps.pkg.outputs.sha256 }}
APP_NAME: ${{ steps.app.outputs.name }}
APP_SHA256: ${{ steps.app.outputs.sha256 }}
run: |
cat > notes.md <<'EOF'
## Install
Expand Down Expand Up @@ -149,22 +165,26 @@ jobs:

| File | SHA-256 |
|---|---|
| Source tarball (used by the Homebrew formula) | `${{ steps.src.outputs.sha256 }}` |
| `${{ steps.pkg.outputs.name }}` | `${{ steps.pkg.outputs.sha256 }}` |
| `${{ steps.app.outputs.name }}` | `${{ steps.app.outputs.sha256 }}` |
EOF
{
printf "| %s | \`%s\` |\n" "Source tarball (used by the Homebrew formula)" "${SRC_SHA256}"
printf "| \`%s\` | \`%s\` |\n" "${PKG_NAME}" "${PKG_SHA256}"
printf "| \`%s\` | \`%s\` |\n" "${APP_NAME}" "${APP_SHA256}"
} >> notes.md

- name: Create the Release
env:
GH_TOKEN: ${{ github.token }}
PKG_NAME: ${{ steps.pkg.outputs.name }}
APP_NAME: ${{ steps.app.outputs.name }}
run: |
gh release create "${GITHUB_REF_NAME}" \
--title "TetherKit ${GITHUB_REF_NAME}" \
--notes-file notes.md \
--generate-notes \
--verify-tag \
"${{ steps.pkg.outputs.name }}" \
"${{ steps.app.outputs.name }}"
"${PKG_NAME}" \
"${APP_NAME}"

# After the Release is created, sync the Homebrew tap to the new version.
#
Expand Down
Loading
Loading