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
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,27 @@ env:
CARGO_TERM_COLOR: always

jobs:
verify:
name: Verify version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Cheap gate ahead of the build matrix: a tag that disagrees with
# Cargo.toml ships a binary whose --version lies about the release.
- name: Tag must match Cargo.toml version
run: |
tag="${GITHUB_REF_NAME#v}"
crate="$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)"
if [ "$tag" != "$crate" ]; then
echo "::error::Tag v$tag does not match Cargo.toml version $crate. Bump Cargo.toml and Cargo.lock, then re-tag."
exit 1
fi
echo "Tag v$tag matches Cargo.toml version $crate"

build:
name: Build ${{ matrix.target }}
needs: verify
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -93,3 +112,40 @@ jobs:
with:
generate_release_notes: true
files: artifacts/*

formula:
name: Update Homebrew formula
needs: release
runs-on: ubuntu-latest
steps:
# The tag is a detached commit; the formula lives on main, so check that
# out instead and commit the update there.
- uses: actions/checkout@v4
with:
ref: main
# Full history: this job rebases onto origin/main before pushing,
# which a shallow clone cannot do reliably.
fetch-depth: 0

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

- name: Update formula
run: ./scripts/update-formula.py "${GITHUB_REF_NAME#v}" artifacts

- name: Commit and push
run: |
if git diff --quiet Formula/ghdash.rb; then
echo "Formula already up to date for ${GITHUB_REF_NAME}"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/ghdash.rb
git commit -m "chore(brew): update formula to ${GITHUB_REF_NAME}"
# main may have moved while the matrix was building.
git pull --rebase origin main
git push origin HEAD:main
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ghdash"
version = "0.3.1"
version = "0.3.3"
edition = "2024"
license = "Apache-2.0"

Expand Down
133 changes: 133 additions & 0 deletions scripts/update-formula.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Point the Homebrew formula at a released version.

Sets `version` and, for every `url` line in the formula, the `sha256` of the
matching release artifact. Run by the release workflow once the artifacts are
built; also runnable by hand:

scripts/update-formula.py 0.3.3 artifacts/

Every artifact the formula references must be present in the artifact
directory — a missing one is an error, never a silently kept stale checksum.
"""

import argparse
import hashlib
import pathlib
import re
import sys

URL_RE = re.compile(r'^(\s*)url ".*/(?P<asset>[^/"]+)"\s*$')
# Matches a checksum line in any state we may find one: a real digest, a
# placeholder, or either of those commented out. All are replaced outright, so
# a stale checksum never survives next to a fresh one.
SHA_RE = re.compile(r'^\s*#?\s*sha256 "[^"]*"\s*(#.*)?$')
VERSION_RE = re.compile(r'^(\s*version )"[^"]*"(\s*)$')


def sha256(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()


def update(formula: str, version: str, artifact_dir: pathlib.Path) -> tuple[str, int]:
lines = formula.splitlines(keepends=True)
out: list[str] = []
updated = 0
missing: list[str] = []
saw_version = False

i = 0
while i < len(lines):
line = lines[i]

m = VERSION_RE.match(line)
if m and not saw_version:
out.append(f'{m.group(1)}"{version}"{m.group(2)}')
saw_version = True
i += 1
continue

m = URL_RE.match(line)
if not m:
out.append(line)
i += 1
continue

# The formula interpolates #{version} into the URL, so resolve it the
# same way Homebrew would before looking the artifact up on disk.
asset = m.group("asset").replace("#{version}", version)
indent = m.group(1)
out.append(line)
i += 1

artifact = artifact_dir / asset
if not artifact.is_file():
missing.append(asset)
# Drop any existing checksum rather than leave a stale one behind.
if i < len(lines) and SHA_RE.match(lines[i]):
i += 1
continue

digest = sha256(artifact)
replacement = f'{indent}sha256 "{digest}"\n'
if i < len(lines) and SHA_RE.match(lines[i]):
if lines[i] != replacement:
updated += 1
i += 1
else:
updated += 1
out.append(replacement)

if not saw_version:
sys.exit("error: no `version \"...\"` line found in the formula")
if missing:
sys.exit(
"error: artifacts referenced by the formula are missing from "
f"{artifact_dir}: {', '.join(missing)}"
)

return "".join(out), updated


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("version", help='release version without the "v" prefix')
parser.add_argument("artifact_dir", type=pathlib.Path, help="directory holding the release artifacts")
parser.add_argument(
"--formula",
type=pathlib.Path,
default=pathlib.Path("Formula/ghdash.rb"),
help="formula to rewrite (default: Formula/ghdash.rb)",
)
parser.add_argument(
"--check",
action="store_true",
help="exit non-zero if the formula would change, without writing it",
)
args = parser.parse_args()

if args.version.startswith("v"):
sys.exit(f'error: pass the version without the "v" prefix, got {args.version!r}')
if not args.artifact_dir.is_dir():
sys.exit(f"error: artifact directory not found: {args.artifact_dir}")

original = args.formula.read_text()
updated, changed_checksums = update(original, args.version, args.artifact_dir)

if updated == original:
print(f"{args.formula} is already up to date for {args.version}")
return

if args.check:
sys.exit(f"error: {args.formula} is out of date for {args.version}")

args.formula.write_text(updated)
print(f"{args.formula} updated to {args.version} ({changed_checksums} checksum(s) changed)")


if __name__ == "__main__":
main()
Loading