From 133c02adbc2c315abcf882eb74d19821fe3a1f08 Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 19:00:12 +0300 Subject: [PATCH 1/2] build: add distribution packaging assets --- .github/workflows/release.yml | 151 ++++++++++++++++++++++++++- .gitignore | 1 + README.md | 95 ++++------------- packaging/README.md | 32 ++++++ packaging/homebrew/README.md | 27 +++-- packaging/homebrew/brave-updater.rb | 5 +- packaging/pypi/README.md | 21 ++++ packaging/scoop/README.md | 22 ++++ packaging/scoop/brave-updater.json | 15 +++ packaging/windows/README.md | 24 +++++ packaging/windows/entrypoint.py | 5 + packaging/winget/README.md | 22 ++++ scripts/render_distribution_files.py | 58 ++++++++++ src/brave_updater/distribution.py | 115 ++++++++++++++++++++ tests/test_distribution.py | 65 ++++++++++++ 15 files changed, 564 insertions(+), 94 deletions(-) create mode 100644 packaging/README.md create mode 100644 packaging/pypi/README.md create mode 100644 packaging/scoop/README.md create mode 100644 packaging/scoop/brave-updater.json create mode 100644 packaging/windows/README.md create mode 100644 packaging/windows/entrypoint.py create mode 100644 packaging/winget/README.md create mode 100644 scripts/render_distribution_files.py create mode 100644 src/brave_updater/distribution.py create mode 100644 tests/test_distribution.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9e41cf..1497071 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,10 +6,8 @@ on: - "v*" jobs: - build: + build-python: runs-on: ubuntu-latest - permissions: - contents: write steps: - name: Checkout repository @@ -38,14 +36,157 @@ jobs: ) print("Validated release tag {} against package version {}".format(tag, __version__)) - - name: Build distributions + - name: Run unit tests + run: python -m unittest discover -v + + - name: Build Python distributions run: | python -m pip install --upgrade pip python -m pip install build twine python -m build python -m twine check dist/* + - name: Upload Python distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: python-dist + path: dist/* + + build-windows-portable: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Validate tag matches package version + shell: python + run: | + import os + import pathlib + import sys + + sys.path.insert(0, str(pathlib.Path("src").resolve())) + from brave_updater import __version__ + + tag = os.environ["GITHUB_REF_NAME"] + expected = tag[1:] if tag.startswith("v") else tag + if __version__ != expected: + raise SystemExit( + "Release tag {} does not match brave_updater.__version__ {}".format(tag, __version__) + ) + print("Validated release tag {} against package version {}".format(tag, __version__)) + + - name: Build Windows portable executable + run: | + python -m pip install --upgrade pip + python -m pip install pyinstaller + python -m PyInstaller --noconfirm --clean --onefile --name brave-updater --paths src packaging/windows/entrypoint.py + + - name: Package Windows portable zip + shell: python + run: | + import pathlib + import zipfile + + output_dir = pathlib.Path("dist/windows") + output_dir.mkdir(parents=True, exist_ok=True) + zip_path = output_dir / "brave-updater-windows-x64.zip" + + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + archive.write("dist/brave-updater.exe", "brave-updater.exe") + archive.write("LICENSE", "LICENSE") + archive.write("README.md", "README.md") + + print("Created {}".format(zip_path)) + + - name: Upload Windows portable artifact + uses: actions/upload-artifact@v4 + with: + name: windows-portable + path: dist/windows/brave-updater-windows-x64.zip + + publish-pypi: + needs: build-python + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: Download Python distribution artifacts + uses: actions/download-artifact@v4 + with: + name: python-dist + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist + + publish-release: + needs: [build-python, build-windows-portable] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Download Python distribution artifacts + uses: actions/download-artifact@v4 + with: + name: python-dist + path: release-assets/python + + - name: Download Windows portable artifact + uses: actions/download-artifact@v4 + with: + name: windows-portable + path: release-assets/windows + + - name: Generate SHA256SUMS + shell: python + run: | + import hashlib + import pathlib + + root = pathlib.Path("release-assets") + outputs = sorted(path for path in root.rglob("*") if path.is_file()) + lines = [] + for path in outputs: + digest = hashlib.sha256(path.read_bytes()).hexdigest() + lines.append("{} {}".format(digest, path.relative_to(root))) + sums_path = root / "SHA256SUMS.txt" + sums_path.write_text("\n".join(lines) + "\n") + print("Wrote {}".format(sums_path)) + + - name: Render Homebrew and Scoop files + run: | + python scripts/render_distribution_files.py \ + --version "${GITHUB_REF_NAME}" \ + --repo "HackEAC/braver-tz" \ + --source-archive "release-assets/python/brave_updater-${GITHUB_REF_NAME#v}.tar.gz" \ + --windows-zip "release-assets/windows/brave-updater-windows-x64.zip" \ + --output-dir "release-assets/manifests" + - name: Upload release assets uses: softprops/action-gh-release@v2 with: - files: dist/* + files: | + release-assets/python/* + release-assets/windows/* + release-assets/SHA256SUMS.txt + release-assets/manifests/homebrew/brave-updater.rb + release-assets/manifests/scoop/brave-updater.json diff --git a/.gitignore b/.gitignore index d263db8..65428fb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__/ build/ dist/ test-downloads/ +*.spec diff --git a/README.md b/README.md index e698898..317fbd6 100644 --- a/README.md +++ b/README.md @@ -2,42 +2,14 @@ `brave-updater` is an unofficial cross-platform CLI for checking, downloading, and updating Brave Browser from Brave-controlled release sources. -This project is intended for environments where the usual Brave installation flow is inconvenient or blocked, but GitHub-hosted Brave releases are still reachable. +It is meant for situations where the normal Brave install flow is inconvenient, blocked, or harder to automate. Important: - This project is not affiliated with or endorsed by Brave Software. -- It does not rehost Brave binaries. -- It downloads Brave installers directly from Brave-controlled GitHub release assets at runtime. -- Homebrew already ships the official macOS `brave-browser` cask. This project is a separate updater/downloader CLI, not a replacement for that cask. - -## Current status - -- Packaged Python CLI with a `src/` layout and console entrypoint: `brave-updater` -- Backward-compatible wrapper retained: `python3 braver.py ...` -- True updater behavior: - - Detects installed Brave version when possible - - Compares it to the latest stable release - - Skips update work when the installed version is already current -- Cross-platform installer selection: - - macOS: `.dmg` / `.pkg` - - Windows: `.exe` - - Linux: `.deb` / `.rpm` on Debian-like and RHEL-like systems -- Safer downloads: - - SHA256 verification - - redirect host validation - - temp-file download + atomic replace - - safe recovery when resume requests are ignored - -## Repository transfer - -The canonical repository is now `HackEAC/braver-tz`. - -Post-transfer notes: -- keep `origin` pointed at `git@github.com:HackEAC/braver-tz.git` -- do not recreate the previous repository path after the transfer, because that would break GitHub's redirect -- follow the post-transfer checklist in [`docs/repository-transfer.md`](docs/repository-transfer.md) - -## Installation +- It does not bundle or mirror Brave binaries. +- It downloads Brave installers directly from Brave-controlled release assets at runtime. + +## Install ### From source @@ -47,30 +19,24 @@ cd braver-tz python3 -m pip install . ``` -After installation: - -```bash -brave-updater check -``` - ### Legacy wrapper -You can still use the old entrypoint directly from the repo: +The original entrypoint still works from the repository: ```bash python3 braver.py --print-only python3 braver.py --install ``` -### Homebrew tap +### Release channels -The intended tap command once the tap is published is: +This repository now includes release packaging for: -```bash -brew install HackEAC/brave-updater/brave-updater -``` +- `PyPI` / `pipx` +- `Homebrew` +- Windows portable releases for `Scoop` and `WinGet` -The starter formula and tap notes live under [`packaging/homebrew`](packaging/homebrew). +Release-channel notes and packaging assets live in [packaging/README.md](/Users/maotora/Projects/brave-updater/packaging/README.md). ## Usage @@ -101,24 +67,13 @@ brave-updater update --yes brave-updater install /path/to/Brave-Browser-universal.dmg ``` -### Source strategy +## Platform notes -`brave-updater` currently defaults to GitHub Releases because that is the supported product direction for this project. +- macOS: `.dmg` and `.pkg` installers are supported +- Windows: `.exe` installers are supported +- Linux: `.deb` and `.rpm` installers are supported on Debian-like and RHEL-like systems -Available source modes: -- `github` -- `official` -- `auto` - -At the moment, `official` and `auto` resolve through Brave's official GitHub release feed. The code is structured so distinct official non-GitHub providers can be added later without redesigning the CLI. - -### Linux support - -Linux installs are currently supported on: -- Debian-like distributions -- RHEL-like distributions - -Arch-like and other unsupported Linux families fail fast with a clear error instead of downloading the wrong package format. +Unsupported Linux families fail fast instead of downloading the wrong package type. ## Development @@ -128,25 +83,13 @@ Run the test suite: python3 -m unittest discover -v ``` -Build distributions: +Build Python distributions: ```bash python3 -m pip install build python3 -m build ``` -## CI and release flow - -- CI runs deterministic unit tests on Linux, macOS, and Windows. -- A scheduled smoke workflow hits the live Brave release endpoint with read-only checks. -- Tagging `v*` builds and uploads sdist/wheel artifacts to GitHub Releases. - -## Legal and policy notes - -- This project should avoid Brave logos and branded artwork unless you have explicit permission. -- The project should continue to present itself as unofficial and non-affiliated. -- The project should continue to download installers from Brave-controlled endpoints rather than mirror them. - ## License -MIT. See [`LICENSE`](LICENSE). +MIT. See [LICENSE](/Users/maotora/Projects/brave-updater/LICENSE). diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..510b427 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,32 @@ +# Packaging + +This directory contains packaging assets and release-channel notes for `brave-updater`. + +## Current channels + +- `PyPI` +- `Homebrew` +- `Windows portable zip` +- `Scoop` +- `WinGet` submission notes + +Linux users can use Homebrew on Linux or Python package managers such as `pipx`, so there is no separate distro-specific Linux packaging here right now. + +## Layout + +- `homebrew/`: tap template and notes +- `pypi/`: PyPI release notes +- `scoop/`: Scoop manifest template and notes +- `winget/`: WinGet submission notes +- `windows/`: Windows portable build entrypoint and notes + +## Release outputs + +On each tagged release, the release workflow should publish: + +- `brave_updater-.tar.gz` +- `brave_updater--py3-none-any.whl` +- `brave-updater-windows-x64.zip` +- `SHA256SUMS.txt` +- rendered Homebrew formula +- rendered Scoop manifest diff --git a/packaging/homebrew/README.md b/packaging/homebrew/README.md index 8928d56..e09340d 100644 --- a/packaging/homebrew/README.md +++ b/packaging/homebrew/README.md @@ -1,23 +1,28 @@ -# Homebrew Tap Starter +# Homebrew -This repository is not itself the Homebrew tap. +Use this directory as the source for the Homebrew tap repo, not as the tap repo itself. -The intended tap repository is: +## Target tap repo - `HackEAC/homebrew-brave-updater` ## Intended install command ```bash +brew tap HackEAC/brave-updater brew install HackEAC/brave-updater/brave-updater ``` -## How to use these files +## Release workflow -1. Create the tap repository `HackEAC/homebrew-brave-updater`. -2. Copy [`brave-updater.rb`](brave-updater.rb) into the tap repo's `Formula/` directory. -3. Replace the release URL and `sha256` with the actual tagged release archive and checksum. -4. Test with: +The release workflow generates a release-specific formula from the tagged sdist and uploads it as a release asset. + +If you need to do it locally instead: + +1. Create the tap repo `HackEAC/homebrew-brave-updater`. +2. Copy the rendered `brave-updater.rb` from the release assets into the tap repo's `Formula/` directory. +3. Commit and push the tap repo update. +4. Test locally: ```bash brew tap HackEAC/brave-updater /path/to/homebrew-brave-updater @@ -27,6 +32,6 @@ brew test brave-updater ## Notes -- Homebrew already provides the official `brave-browser` cask on macOS. -- This formula packages the updater CLI, not the browser itself. -- The formula uses Homebrew-managed Python so end users do not need to manually install Python first. +- The formula installs the CLI, not Brave Browser itself. +- It relies on Homebrew-managed Python, so end users do not need to install Python manually. +- The static `brave-updater.rb` file in this repo is a template; use the rendered release asset for actual tap updates. diff --git a/packaging/homebrew/brave-updater.rb b/packaging/homebrew/brave-updater.rb index d83d860..3b48583 100644 --- a/packaging/homebrew/brave-updater.rb +++ b/packaging/homebrew/brave-updater.rb @@ -3,8 +3,9 @@ class BraveUpdater < Formula desc "Unofficial CLI to download and update Brave Browser from Brave-controlled release sources" homepage "https://github.com/HackEAC/braver-tz" - url "https://github.com/HackEAC/braver-tz/archive/refs/tags/v1.1.0.tar.gz" - sha256 "REPLACE_WITH_RELEASE_SHA256" + version "REPLACE_WITH_VERSION" + url "https://github.com/HackEAC/braver-tz/releases/download/v#{version}/brave_updater-#{version}.tar.gz" + sha256 "REPLACE_WITH_SDIST_SHA256" license "MIT" depends_on "python@3.12" diff --git a/packaging/pypi/README.md b/packaging/pypi/README.md new file mode 100644 index 0000000..e01230a --- /dev/null +++ b/packaging/pypi/README.md @@ -0,0 +1,21 @@ +# PyPI + +The release workflow is set up to publish the Python package to PyPI from tagged releases. + +## Before the first release + +Configure a Trusted Publisher for this repository on PyPI so the GitHub Actions release workflow can publish without storing a long-lived API token. + +## Expected install commands + +```bash +python3 -m pip install brave-updater +pipx install brave-updater +``` + +## Release artifacts + +The PyPI publish step uses: + +- `brave_updater-.tar.gz` +- `brave_updater--py3-none-any.whl` diff --git a/packaging/scoop/README.md b/packaging/scoop/README.md new file mode 100644 index 0000000..6bcfdb6 --- /dev/null +++ b/packaging/scoop/README.md @@ -0,0 +1,22 @@ +# Scoop + +The Scoop manifest in this directory is a template. + +For real releases, use the release-generated manifest uploaded by the release workflow. It will already contain the correct version and SHA256 for `brave-updater-windows-x64.zip`. + +## Intended bucket + +- `HackEAC/scoop-bucket` + +## Intended install flow + +```bash +scoop bucket add hackeac https://github.com/HackEAC/scoop-bucket +scoop install brave-updater +``` + +## Notes + +- Scoop packages the Windows CLI only. +- It does not bundle Brave Browser. +- The manifest points at the portable zip from GitHub Releases. diff --git a/packaging/scoop/brave-updater.json b/packaging/scoop/brave-updater.json new file mode 100644 index 0000000..495cf94 --- /dev/null +++ b/packaging/scoop/brave-updater.json @@ -0,0 +1,15 @@ +{ + "version": "REPLACE_WITH_VERSION", + "description": "Unofficial CLI to download and update Brave Browser from Brave-controlled release sources.", + "homepage": "https://github.com/HackEAC/braver-tz", + "license": "MIT", + "url": "https://github.com/HackEAC/braver-tz/releases/download/vREPLACE_WITH_VERSION/brave-updater-windows-x64.zip", + "hash": "REPLACE_WITH_WINDOWS_ZIP_SHA256", + "bin": "brave-updater.exe", + "checkver": { + "github": "https://github.com/HackEAC/braver-tz" + }, + "autoupdate": { + "url": "https://github.com/HackEAC/braver-tz/releases/download/v$version/brave-updater-windows-x64.zip" + } +} diff --git a/packaging/windows/README.md b/packaging/windows/README.md new file mode 100644 index 0000000..a0cf6c6 --- /dev/null +++ b/packaging/windows/README.md @@ -0,0 +1,24 @@ +# Windows Packaging + +The release workflow builds a standalone Windows portable zip named: + +- `brave-updater-windows-x64.zip` + +The zip is intended to contain: + +- `brave-updater.exe` +- `LICENSE` +- `README.md` + +## Why this exists + +Windows package managers work much better with a real installer or a portable executable archive than with a `pip install` command. + +This portable zip is the release asset used for: + +- `Scoop` +- `WinGet` submissions + +## Build source + +The executable is built with `PyInstaller` using [`entrypoint.py`](entrypoint.py), which calls the packaged CLI directly. diff --git a/packaging/windows/entrypoint.py b/packaging/windows/entrypoint.py new file mode 100644 index 0000000..3c3fe91 --- /dev/null +++ b/packaging/windows/entrypoint.py @@ -0,0 +1,5 @@ +from brave_updater.cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packaging/winget/README.md b/packaging/winget/README.md new file mode 100644 index 0000000..b692faf --- /dev/null +++ b/packaging/winget/README.md @@ -0,0 +1,22 @@ +# WinGet + +`brave-updater` is a good fit for WinGet once a tagged release publishes the Windows portable zip: + +- `brave-updater-windows-x64.zip` + +## Recommended package identity + +- `HackEAC.BraveUpdater` + +## Submission flow + +1. Create a tagged release so the Windows portable zip exists on GitHub Releases. +2. Use `wingetcreate` to generate the manifest from the release asset URL. +3. Validate the manifest locally. +4. Submit the manifest PR to `microsoft/winget-pkgs`. + +## Notes + +- Treat WinGet as a distribution channel for the CLI, not for Brave Browser itself. +- Prefer the portable zip release asset instead of trying to wrap `pip install` in a package manager manifest. +- Keep the package description explicitly unofficial and non-affiliated. diff --git a/scripts/render_distribution_files.py b/scripts/render_distribution_files.py new file mode 100644 index 0000000..e3e5c80 --- /dev/null +++ b/scripts/render_distribution_files.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Render release-specific package-manager files from built assets.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" + +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) + +from brave_updater import __version__ +from brave_updater.distribution import DEFAULT_REPOSITORY, sha256_for_file, write_distribution_files + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Render Homebrew and Scoop files for a release.") + parser.add_argument("--version", default=__version__, help="Release version (default: package version)") + parser.add_argument("--repo", default=DEFAULT_REPOSITORY, help="GitHub repository path, e.g. HackEAC/braver-tz") + parser.add_argument("--source-archive", required=True, help="Path to the release sdist tar.gz") + parser.add_argument("--windows-zip", required=True, help="Path to the Windows portable zip") + parser.add_argument("--output-dir", required=True, help="Directory to write rendered files into") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + + source_archive = Path(args.source_archive).expanduser().resolve() + windows_zip = Path(args.windows_zip).expanduser().resolve() + output_dir = Path(args.output_dir).expanduser().resolve() + + if not source_archive.exists(): + raise FileNotFoundError("Source archive not found: {}".format(source_archive)) + if not windows_zip.exists(): + raise FileNotFoundError("Windows zip not found: {}".format(windows_zip)) + + written = write_distribution_files( + output_dir=output_dir, + version=args.version, + repository=args.repo, + source_sha256=sha256_for_file(source_archive), + windows_sha256=sha256_for_file(windows_zip), + ) + + for label, path in written.items(): + print("{}: {}".format(label, path)) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/brave_updater/distribution.py b/src/brave_updater/distribution.py new file mode 100644 index 0000000..7b8e9c9 --- /dev/null +++ b/src/brave_updater/distribution.py @@ -0,0 +1,115 @@ +"""Helpers for rendering package-manager distribution files.""" + +from __future__ import annotations + +import json +import hashlib +from pathlib import Path +from typing import Dict + + +DEFAULT_REPOSITORY = "HackEAC/braver-tz" +WINDOWS_ZIP_NAME = "brave-updater-windows-x64.zip" + + +def normalize_version(version: str) -> str: + return version.lstrip("v").strip() + + +def source_distribution_name(version: str) -> str: + return "brave_updater-{}.tar.gz".format(normalize_version(version)) + + +def windows_distribution_name() -> str: + return WINDOWS_ZIP_NAME + + +def release_asset_url(repository: str, version: str, filename: str) -> str: + clean_version = normalize_version(version) + return "https://github.com/{}/releases/download/v{}/{}".format(repository, clean_version, filename) + + +def sha256_for_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + while True: + chunk = handle.read(1024 * 1024) + if not chunk: + break + digest.update(chunk) + return digest.hexdigest() + + +def render_homebrew_formula(version: str, repository: str, source_sha256: str) -> str: + clean_version = normalize_version(version) + source_name = source_distribution_name(clean_version) + source_url = release_asset_url(repository, clean_version, source_name) + return """class BraveUpdater < Formula + include Language::Python::Virtualenv + + desc "Unofficial CLI to download and update Brave Browser from Brave-controlled release sources" + homepage "https://github.com/{repository}" + version "{version}" + url "{url}" + sha256 "{sha256}" + license "MIT" + + depends_on "python@3.12" + + def install + virtualenv_install_with_resources + end + + test do + output = shell_output("#{{bin}}/brave-updater --help") + assert_match "update", output + end +end +""".format(repository=repository, version=clean_version, url=source_url, sha256=source_sha256) + + +def render_scoop_manifest(version: str, repository: str, windows_sha256: str) -> str: + clean_version = normalize_version(version) + url = release_asset_url(repository, clean_version, windows_distribution_name()) + payload = { + "version": clean_version, + "description": "Unofficial CLI to download and update Brave Browser from Brave-controlled release sources.", + "homepage": "https://github.com/{}".format(repository), + "license": "MIT", + "url": url, + "hash": windows_sha256, + "bin": "brave-updater.exe", + "checkver": { + "github": "https://github.com/{}".format(repository), + }, + "autoupdate": { + "url": "https://github.com/{}/releases/download/v$version/{}".format(repository, windows_distribution_name()), + }, + } + return json.dumps(payload, indent=2, sort_keys=False) + "\n" + + +def write_distribution_files( + output_dir: Path, + version: str, + repository: str, + source_sha256: str, + windows_sha256: str, +) -> Dict[str, Path]: + output_dir.mkdir(parents=True, exist_ok=True) + + homebrew_dir = output_dir / "homebrew" + scoop_dir = output_dir / "scoop" + homebrew_dir.mkdir(parents=True, exist_ok=True) + scoop_dir.mkdir(parents=True, exist_ok=True) + + homebrew_formula = homebrew_dir / "brave-updater.rb" + homebrew_formula.write_text(render_homebrew_formula(version, repository, source_sha256)) + + scoop_manifest = scoop_dir / "brave-updater.json" + scoop_manifest.write_text(render_scoop_manifest(version, repository, windows_sha256)) + + return { + "homebrew": homebrew_formula, + "scoop": scoop_manifest, + } diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..b812571 --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,65 @@ +from pathlib import Path +from tempfile import TemporaryDirectory +import unittest + +from brave_updater.distribution import ( + release_asset_url, + render_homebrew_formula, + render_scoop_manifest, + sha256_for_file, + source_distribution_name, + windows_distribution_name, + write_distribution_files, +) + + +class DistributionTests(unittest.TestCase): + def test_release_asset_url_uses_tagged_release_assets(self) -> None: + url = release_asset_url("HackEAC/braver-tz", "1.1.0", "artifact.zip") + self.assertEqual(url, "https://github.com/HackEAC/braver-tz/releases/download/v1.1.0/artifact.zip") + + def test_render_homebrew_formula_uses_release_sdist(self) -> None: + formula = render_homebrew_formula("1.1.0", "HackEAC/braver-tz", "a" * 64) + + self.assertIn('version "1.1.0"', formula) + self.assertIn(source_distribution_name("1.1.0"), formula) + self.assertIn("releases/download/v1.1.0", formula) + self.assertIn('"{}"'.format("a" * 64), formula) + + def test_render_scoop_manifest_uses_windows_zip(self) -> None: + manifest = render_scoop_manifest("1.1.0", "HackEAC/braver-tz", "b" * 64) + + self.assertIn('"version": "1.1.0"', manifest) + self.assertIn(windows_distribution_name(), manifest) + self.assertIn('"hash": "{}"'.format("b" * 64), manifest) + self.assertIn('"bin": "brave-updater.exe"', manifest) + + def test_write_distribution_files_writes_expected_outputs(self) -> None: + with TemporaryDirectory() as temp_dir: + output_dir = Path(temp_dir) / "release-manifests" + written = write_distribution_files( + output_dir=output_dir, + version="1.1.0", + repository="HackEAC/braver-tz", + source_sha256="c" * 64, + windows_sha256="d" * 64, + ) + + self.assertTrue(written["homebrew"].exists()) + self.assertTrue(written["scoop"].exists()) + self.assertIn("BraveUpdater < Formula", written["homebrew"].read_text()) + self.assertIn('"version": "1.1.0"', written["scoop"].read_text()) + + def test_sha256_for_file(self) -> None: + with TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "payload.txt" + path.write_text("brave-updater") + + self.assertEqual( + sha256_for_file(path), + "2032a3e6e499399ad94754846dd1e72979bba185ac8318e2729fd3e3c7f2f10c", + ) + + +if __name__ == "__main__": + unittest.main() From 9506370d9f0006e17f420e6e660d0fdbe64d5629 Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 20:12:58 +0300 Subject: [PATCH 2/2] ci: dispatch packaging sync after releases --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++++ packaging/README.md | 9 +++++++++ packaging/homebrew/README.md | 2 ++ packaging/scoop/README.md | 1 + 4 files changed, 41 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1497071..1d3fb30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -190,3 +190,32 @@ jobs: release-assets/SHA256SUMS.txt release-assets/manifests/homebrew/brave-updater.rb release-assets/manifests/scoop/brave-updater.json + + dispatch-packaging-sync: + needs: publish-release + runs-on: ubuntu-latest + + steps: + - name: Dispatch tap and bucket sync workflows + shell: bash + env: + PACKAGING_SYNC_TOKEN: ${{ secrets.PACKAGING_SYNC_TOKEN }} + run: | + if [ -z "${PACKAGING_SYNC_TOKEN}" ]; then + echo "PACKAGING_SYNC_TOKEN is not configured; skipping tap and bucket dispatch." + exit 0 + fi + + curl --fail-with-body --silent --show-error \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${PACKAGING_SYNC_TOKEN}" \ + https://api.github.com/repos/HackEAC/scoop-bucket/dispatches \ + -d "{\"event_type\":\"sync-from-release\",\"client_payload\":{\"source_repository\":\"${GITHUB_REPOSITORY}\",\"tag\":\"${GITHUB_REF_NAME}\"}}" + + curl --fail-with-body --silent --show-error \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${PACKAGING_SYNC_TOKEN}" \ + https://api.github.com/repos/HackEAC/homebrew-brave-updater/dispatches \ + -d "{\"event_type\":\"sync-from-release\",\"client_payload\":{\"source_repository\":\"${GITHUB_REPOSITORY}\",\"tag\":\"${GITHUB_REF_NAME}\"}}" diff --git a/packaging/README.md b/packaging/README.md index 510b427..f1132d3 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -30,3 +30,12 @@ On each tagged release, the release workflow should publish: - `SHA256SUMS.txt` - rendered Homebrew formula - rendered Scoop manifest + +## Cross-repo sync + +The Homebrew tap and Scoop bucket live in separate repositories: + +- `HackEAC/homebrew-brave-updater` +- `HackEAC/scoop-bucket` + +`HackEAC/braver-tz` can notify both repos immediately after a tagged release by using the `PACKAGING_SYNC_TOKEN` repository secret. Without that secret, the external repos still poll the latest release on a 6-hour schedule. diff --git a/packaging/homebrew/README.md b/packaging/homebrew/README.md index e09340d..a321817 100644 --- a/packaging/homebrew/README.md +++ b/packaging/homebrew/README.md @@ -17,6 +17,8 @@ brew install HackEAC/brave-updater/brave-updater The release workflow generates a release-specific formula from the tagged sdist and uploads it as a release asset. +If `HackEAC/braver-tz` has the `PACKAGING_SYNC_TOKEN` secret configured, the release workflow also sends a `repository_dispatch` event to `HackEAC/homebrew-brave-updater` so the tap can sync immediately. Otherwise, the tap repo falls back to its scheduled sync job. + If you need to do it locally instead: 1. Create the tap repo `HackEAC/homebrew-brave-updater`. diff --git a/packaging/scoop/README.md b/packaging/scoop/README.md index 6bcfdb6..245bbde 100644 --- a/packaging/scoop/README.md +++ b/packaging/scoop/README.md @@ -20,3 +20,4 @@ scoop install brave-updater - Scoop packages the Windows CLI only. - It does not bundle Brave Browser. - The manifest points at the portable zip from GitHub Releases. +- If `HackEAC/braver-tz` has the `PACKAGING_SYNC_TOKEN` secret configured, releases can notify `HackEAC/scoop-bucket` immediately by `repository_dispatch`; otherwise the bucket repo falls back to its scheduled sync job.