diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index bb99c294..76004411 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,9 +1,6 @@ # Single tagged-release pipeline: Linux release assets, Windows + macOS Electron # builds, Flatpak, Android APKs (dev/master track tags via android-apk-tag.yml), SLSA # provenance (generic generator: Linux, desktop, optional Android+Flatpak), optional cosign bundles, and one draft GitHub release. -# Optional: same draft upload/ tree is mirrored to bunny.net Edge Storage for master/dev -# tags when BUNNY_STORAGE_ACCESS_KEY is set (see draft job). -rc tags always use the -# dev/ prefix; previous release folders under master/ and dev/ are pruned after each upload. # One workflow run per tag keeps the release graph immutable. # # Pinned first-party actions (bump tag and SHA together when upgrading): @@ -569,33 +566,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - with: - fetch-depth: 0 - - - name: Resolve tag release track - id: release_track - run: | - set -euo pipefail - git fetch --no-tags origin dev master - sha="$(git rev-parse HEAD)" - on_m=false - on_d=false - while IFS= read -r line; do - ref="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - case "$ref" in - origin/master) on_m=true ;; - origin/dev) on_d=true ;; - esac - done < <(git branch -r --contains "${sha}") - if [[ "${on_m}" == true ]]; then - track=master - elif [[ "${on_d}" == true ]]; then - track=dev - else - track=none - fi - echo "track=${track}" >> "${GITHUB_OUTPUT}" - echo "Resolved tag ${GITHUB_REF_NAME} -> track=${track}" - name: Download Linux release assets (x64) uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 @@ -692,29 +662,6 @@ jobs: GH_TOKEN: ${{ github.token }} run: bash scripts/ci/github-draft-release-upload-assets.sh upload - - name: Upload release assets to Bunny Storage - if: >- - !startsWith(github.ref_name, 'preview-') && - (steps.release_track.outputs.track == 'master' || steps.release_track.outputs.track == 'dev') - env: - BUNNY_STORAGE_ACCESS_KEY: ${{ secrets.BUNNY_STORAGE_ACCESS_KEY }} - BUNNY_STORAGE_BASE_URL: ${{ vars.BUNNY_STORAGE_BASE_URL }} - BUNNY_RELEASE_TRACK: ${{ steps.release_track.outputs.track }} - run: | - set -euo pipefail - if [[ -z "${BUNNY_STORAGE_ACCESS_KEY:-}" ]]; then - echo "::notice::Skipping Bunny Storage (set repository secret BUNNY_STORAGE_ACCESS_KEY to enable)." - exit 0 - fi - export BUNNY_STORAGE_BASE_URL="${BUNNY_STORAGE_BASE_URL:-https://la.storage.bunnycdn.com/meshchatx}" - _ref_lc="${GITHUB_REF_NAME,,}" - if [[ "${_ref_lc}" == *"-rc"* ]]; then - export BUNNY_STORAGE_OBJECT_PREFIX="dev/${GITHUB_REF_NAME}" - else - export BUNNY_STORAGE_OBJECT_PREFIX="${BUNNY_RELEASE_TRACK}/${GITHUB_REF_NAME}" - fi - python3 scripts/ci/github-upload-bunny-storage-release-assets.py upload - release-artifact-gate: name: Verify required tagged release artifacts if: >- diff --git a/scripts/ci/github-upload-bunny-storage-release-assets.py b/scripts/ci/github-upload-bunny-storage-release-assets.py deleted file mode 100644 index 1fac40f8..00000000 --- a/scripts/ci/github-upload-bunny-storage-release-assets.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -"""Upload a directory tree to bunny.net Edge Storage (HTTP PUT per object).""" - -from __future__ import annotations - -import hashlib -import json -import mimetypes -import os -import sys -import time -import urllib.error -import urllib.request -from pathlib import Path -from urllib.parse import quote - - -def encode_object_rel(rel: str) -> str: - return "/".join(quote(part, safe="") for part in rel.split("/")) - - -def mime_for(path: Path) -> str: - if path.suffix.lower() == ".wasm": - return "application/wasm" - guessed, _enc = mimetypes.guess_type(path.name) - return guessed or "application/octet-stream" - - -def get_json(url: str, access_key: str, timeout: int = 120) -> object: - req = urllib.request.Request( - url, - method="GET", - headers={"AccessKey": access_key}, - ) - with urllib.request.urlopen(req, timeout=timeout) as resp: - body = resp.read() - return json.loads(body.decode("utf-8")) - - -def delete_path(url: str, access_key: str, timeout: int = 120) -> None: - req = urllib.request.Request( - url, - method="DELETE", - headers={"AccessKey": access_key}, - ) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - code = resp.getcode() - except urllib.error.HTTPError as e: - code = e.code - if code == 404: - return - e.read(500) - raise SystemExit(f"HTTP {code} DELETE {url}") from e - else: - if code not in (200, 201, 204): - raise SystemExit(f"unexpected DELETE status {code} for {url}") - - -def prune_other_versions( - base: str, - access_key: str, - track: str, - keep_version: str, -) -> None: - """Remove every version directory under ``track/`` except ``keep_version``.""" - base = base.rstrip("/") - list_url = f"{base}/{encode_object_rel(track)}/" - try: - listing = get_json(list_url, access_key) - except urllib.error.HTTPError as e: - if e.code == 404: - return - raise - if not isinstance(listing, list): - return - seen: set[str] = set() - for item in listing: - if not isinstance(item, dict): - continue - if not item.get("IsDirectory"): - continue - name = item.get("ObjectName") - if not name or not isinstance(name, str): - continue - if name in seen: - continue - seen.add(name) - if name == keep_version: - continue - rel = f"{track}/{name}" - delete_url = f"{base}/{encode_object_rel(rel)}" - print(f"prune: DELETE {delete_url}", file=sys.stderr) - delete_path(delete_url, access_key) - - -def put_file( - url: str, - body: bytes, - access_key: str, - content_type: str, - max_attempts: int = 4, -) -> None: - checksum = hashlib.sha256(body).hexdigest().upper() - timeout = 600 if len(body) > 50_000_000 else 120 - for attempt in range(1, max_attempts + 1): - req = urllib.request.Request( - url, - data=body, - method="PUT", - headers={ - "AccessKey": access_key, - "Content-Type": content_type, - "Checksum": checksum, - }, - ) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - code = resp.getcode() - except urllib.error.HTTPError as e: - code = e.code - err_body = e.read(500) - if code in (200, 201): - return - if 500 <= code < 600 and attempt < max_attempts: - time.sleep(0.5 * (2 ** (attempt - 1))) - continue - raise SystemExit( - f"HTTP {code} for {url}: {err_body!r}", - ) from e - except (urllib.error.URLError, TimeoutError) as e: - if attempt < max_attempts: - time.sleep(0.5 * (2 ** (attempt - 1))) - continue - raise SystemExit(f"request failed for {url}: {e}") from e - else: - if code in (200, 201): - return - raise SystemExit(f"unexpected status {code} for {url}") - - -def main() -> None: - base = os.environ.get("BUNNY_STORAGE_BASE_URL", "").rstrip("/") - key = os.environ.get("BUNNY_STORAGE_ACCESS_KEY", "") - prefix = os.environ.get("BUNNY_STORAGE_OBJECT_PREFIX", "").strip("/") - if not base or not key: - print( - "BUNNY_STORAGE_BASE_URL and BUNNY_STORAGE_ACCESS_KEY must be set", - file=sys.stderr, - ) - sys.exit(1) - if prefix: - seg = prefix.split("/", 1) - if len(seg) == 2 and seg[0] in ("master", "dev"): - prune_other_versions(base, key, seg[0], seg[1]) - root = Path(sys.argv[1]).resolve() - if not root.is_dir(): - print(f"not a directory: {root}", file=sys.stderr) - sys.exit(1) - - files = sorted(p for p in root.rglob("*") if p.is_file()) - if not files: - print(f"no files under {root}", file=sys.stderr) - sys.exit(1) - - for path in files: - rel = path.relative_to(root).as_posix() - if prefix: - object_rel = f"{prefix}/{rel}" - else: - object_rel = rel - url = f"{base}/{encode_object_rel(object_rel)}" - body = path.read_bytes() - put_file(url, body, key, mime_for(path)) - print(url) - - -if __name__ == "__main__": - main()