initial
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
name: Update LibreWolf cask
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out tap
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Update from latest stable release
|
||||||
|
run: python3 scripts/update_librewolf.py
|
||||||
|
|
||||||
|
- name: Commit update
|
||||||
|
run: |
|
||||||
|
if git diff --quiet -- Casks/librewolf.rb; then
|
||||||
|
echo "LibreWolf is already up to date"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git config user.name "librewolf-update-bot"
|
||||||
|
git config user.email "librewolf-update-bot@users.noreply.gitea.com"
|
||||||
|
git add Casks/librewolf.rb
|
||||||
|
git commit -m "Update LibreWolf cask"
|
||||||
|
git push
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
cask "librewolf" do
|
||||||
|
arch arm: "arm64", intel: "x86_64"
|
||||||
|
os macos: "macos-#{arch}-package.dmg", linux: "linux-#{arch}-appimage.AppImage"
|
||||||
|
|
||||||
|
version "153.0.3,1"
|
||||||
|
sha256 arm: "cf28a2c610cd8be0f698b52c1fb6e530d17be09fb43aba798e762d8423b35950",
|
||||||
|
intel: "031050b89658c380d91f2658bfae7ff7f1297be4fabc240f45f15ef506d2aa75",
|
||||||
|
arm64_linux: "5a20e87564950dc6edcceba9969206664d4df24f7811c4ef545b6cd217fa0807",
|
||||||
|
x86_64_linux: "85da8f1bca931c4f1a2cea9140dd846da9cf2868afe085f556b0e5d637e8467d"
|
||||||
|
|
||||||
|
on_macos do
|
||||||
|
disable! date: "2026-09-01", because: :fails_gatekeeper_check
|
||||||
|
|
||||||
|
app "LibreWolf.app"
|
||||||
|
command_wrapper "librewolf",
|
||||||
|
executable: "#{appdir}/LibreWolf.app/Contents/MacOS/librewolf"
|
||||||
|
|
||||||
|
zap trash: [
|
||||||
|
"~/.librewolf",
|
||||||
|
"~/Library/Application Support/LibreWolf",
|
||||||
|
"~/Library/Caches/LibreWolf Community",
|
||||||
|
"~/Library/Caches/LibreWolf",
|
||||||
|
"~/Library/Preferences/io.gitlab.librewolf-community.librewolf.plist",
|
||||||
|
"~/Library/Saved Application State/io.gitlab.librewolf-community.librewolf.savedState",
|
||||||
|
]
|
||||||
|
end
|
||||||
|
on_linux do
|
||||||
|
app_image "librewolf-#{version.tr(",", "-")}-linux-#{arch}-appimage.AppImage",
|
||||||
|
target: "LibreWolf.AppImage"
|
||||||
|
end
|
||||||
|
|
||||||
|
url "https://codeberg.org/api/packages/librewolf/generic/librewolf/#{version.tr(",", "-")}/librewolf-#{version.tr(",", "-")}-#{os}"
|
||||||
|
name "LibreWolf"
|
||||||
|
desc "Web browser"
|
||||||
|
homepage "https://librewolf.net/"
|
||||||
|
|
||||||
|
# There can be a notable gap between when a version is tagged and a
|
||||||
|
# corresponding release is created, so we check the "latest" release instead
|
||||||
|
# of the Git tags.
|
||||||
|
livecheck do
|
||||||
|
url "https://codeberg.org/api/v1/repos/librewolf/bsys6/releases/latest"
|
||||||
|
regex(/^v?(\d+(?:[.-]\d+)+)$/i)
|
||||||
|
strategy :json do |json, regex|
|
||||||
|
json["tag_name"]&.[](regex, 1)&.tr("-", ",")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Copyright © 2013, Paul Hinze & Contributors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
Executable
+167
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Update the LibreWolf cask from the latest stable bsys6 release."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DEFAULT_CASK = ROOT / "Casks" / "librewolf.rb"
|
||||||
|
# Forgejo API for https://librewolf.dev/librewolf/bsys6/releases.
|
||||||
|
LATEST_RELEASE_API = "https://librewolf.dev/api/v1/repos/librewolf/bsys6/releases/latest"
|
||||||
|
PACKAGE_BASE_URL = "https://codeberg.org/api/packages/librewolf/generic/librewolf"
|
||||||
|
USER_AGENT = "homebrew-librewolf-release-updater/1.0"
|
||||||
|
VERSION_RE = re.compile(r'^ version "([^"]+)"$', re.MULTILINE)
|
||||||
|
SHA256_RE = re.compile(
|
||||||
|
r'^ sha256 arm:\s+"[0-9a-f]{64}",\n'
|
||||||
|
r'\s+intel:\s+"[0-9a-f]{64}",\n'
|
||||||
|
r'\s+arm64_linux:\s+"[0-9a-f]{64}",\n'
|
||||||
|
r'\s+x86_64_linux:\s+"[0-9a-f]{64}"$',
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
STABLE_TAG_RE = re.compile(
|
||||||
|
r"^(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?-(?P<build>\d+)$"
|
||||||
|
)
|
||||||
|
CHECKSUM_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
||||||
|
ASSET_SUFFIXES = {
|
||||||
|
"arm": "macos-arm64-package.dmg",
|
||||||
|
"intel": "macos-x86_64-package.dmg",
|
||||||
|
"arm64_linux": "linux-arm64-appimage.AppImage",
|
||||||
|
"x86_64_linux": "linux-x86_64-appimage.AppImage",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url: str, limit: int) -> bytes:
|
||||||
|
request = Request(
|
||||||
|
url,
|
||||||
|
headers={"Accept": "application/json", "User-Agent": USER_AGENT},
|
||||||
|
)
|
||||||
|
with urlopen(request, timeout=30) as response:
|
||||||
|
body = response.read(limit + 1)
|
||||||
|
if len(body) > limit:
|
||||||
|
raise ValueError(f"response from {url} exceeds {limit} bytes")
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def latest_stable_release() -> dict[str, Any]:
|
||||||
|
release = json.loads(fetch(LATEST_RELEASE_API, 2_000_000))
|
||||||
|
if not isinstance(release, dict):
|
||||||
|
raise ValueError("latest release API did not return an object")
|
||||||
|
if release.get("draft") or release.get("prerelease"):
|
||||||
|
raise ValueError("latest release is not stable")
|
||||||
|
|
||||||
|
raw_tag = release.get("tag_name")
|
||||||
|
if not isinstance(raw_tag, str):
|
||||||
|
raise ValueError("latest release has no tag_name")
|
||||||
|
tag = raw_tag.removeprefix("v")
|
||||||
|
if not STABLE_TAG_RE.fullmatch(tag):
|
||||||
|
raise ValueError(f"latest release tag is not a stable version: {raw_tag!r}")
|
||||||
|
release["stable_tag"] = tag
|
||||||
|
return release
|
||||||
|
|
||||||
|
|
||||||
|
def version_key(version: str) -> tuple[int, int, int, int]:
|
||||||
|
normalized = version.removeprefix("v").replace(",", "-")
|
||||||
|
match = STABLE_TAG_RE.fullmatch(normalized)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"invalid cask version: {version!r}")
|
||||||
|
return (
|
||||||
|
int(match["major"]),
|
||||||
|
int(match["minor"]),
|
||||||
|
int(match["patch"] or 0),
|
||||||
|
int(match["build"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def release_checksums(release: dict[str, Any]) -> dict[str, str]:
|
||||||
|
assets = release.get("assets")
|
||||||
|
if not isinstance(assets, list):
|
||||||
|
raise ValueError("latest release has no asset list")
|
||||||
|
|
||||||
|
by_name: dict[str, dict[str, Any]] = {}
|
||||||
|
for asset in assets:
|
||||||
|
if not isinstance(asset, dict) or not isinstance(asset.get("name"), str):
|
||||||
|
continue
|
||||||
|
name = asset["name"]
|
||||||
|
if name in by_name:
|
||||||
|
raise ValueError(f"duplicate release asset: {name}")
|
||||||
|
by_name[name] = asset
|
||||||
|
|
||||||
|
tag = release["stable_tag"]
|
||||||
|
checksums: dict[str, str] = {}
|
||||||
|
for cask_arch, suffix in ASSET_SUFFIXES.items():
|
||||||
|
package_name = f"librewolf-{tag}-{suffix}"
|
||||||
|
if package_name not in by_name:
|
||||||
|
raise ValueError(f"missing release asset: {package_name}")
|
||||||
|
|
||||||
|
checksum_name = f"{package_name}.sha256sum"
|
||||||
|
if checksum_name not in by_name:
|
||||||
|
raise ValueError(f"missing release asset: {checksum_name}")
|
||||||
|
checksum_url = f"{PACKAGE_BASE_URL}/{tag}/{checksum_name}"
|
||||||
|
|
||||||
|
checksum_text = fetch(checksum_url, 4096).decode("ascii").strip()
|
||||||
|
checksum = checksum_text.split(maxsplit=1)[0] if checksum_text else ""
|
||||||
|
if not CHECKSUM_RE.fullmatch(checksum):
|
||||||
|
raise ValueError(f"invalid checksum for {package_name}")
|
||||||
|
checksums[cask_arch] = checksum.lower()
|
||||||
|
|
||||||
|
return checksums
|
||||||
|
|
||||||
|
|
||||||
|
def update_cask(cask_path: Path) -> bool:
|
||||||
|
source = cask_path.read_text(encoding="utf-8")
|
||||||
|
version_match = VERSION_RE.search(source)
|
||||||
|
if version_match is None:
|
||||||
|
raise ValueError(f"could not find version stanza in {cask_path}")
|
||||||
|
|
||||||
|
current_version = version_match.group(1)
|
||||||
|
release = latest_stable_release()
|
||||||
|
latest_tag = release["stable_tag"]
|
||||||
|
latest_key = version_key(latest_tag)
|
||||||
|
current_key = version_key(current_version)
|
||||||
|
comparison = (latest_key > current_key) - (latest_key < current_key)
|
||||||
|
if comparison <= 0:
|
||||||
|
state = "current" if comparison == 0 else "newer than the latest stable release"
|
||||||
|
print(f"LibreWolf {current_version} is {state}; no update needed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
checksums = release_checksums(release)
|
||||||
|
cask_version = latest_tag.replace("-", ",")
|
||||||
|
updated, version_count = VERSION_RE.subn(f' version "{cask_version}"', source, count=1)
|
||||||
|
sha256_stanza = (
|
||||||
|
f' sha256 arm: "{checksums["arm"]}",\n'
|
||||||
|
f' intel: "{checksums["intel"]}",\n'
|
||||||
|
f' arm64_linux: "{checksums["arm64_linux"]}",\n'
|
||||||
|
f' x86_64_linux: "{checksums["x86_64_linux"]}"'
|
||||||
|
)
|
||||||
|
updated, sha256_count = SHA256_RE.subn(sha256_stanza, updated, count=1)
|
||||||
|
if version_count != 1 or sha256_count != 1:
|
||||||
|
raise ValueError(f"could not update version and sha256 stanzas in {cask_path}")
|
||||||
|
|
||||||
|
temporary_path = cask_path.with_suffix(f"{cask_path.suffix}.tmp")
|
||||||
|
temporary_path.write_text(updated, encoding="utf-8")
|
||||||
|
temporary_path.replace(cask_path)
|
||||||
|
print(f"Updated LibreWolf from {current_version} to {cask_version}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--cask", type=Path, default=DEFAULT_CASK)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
update_cask(args.cask)
|
||||||
|
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as error:
|
||||||
|
print(f"error: {error}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from scripts import update_librewolf
|
||||||
|
|
||||||
|
|
||||||
|
class VersionOrderingTest(unittest.TestCase):
|
||||||
|
def test_patch_release_is_newer_than_rebuild_without_patch(self) -> None:
|
||||||
|
self.assertLess(
|
||||||
|
update_librewolf.version_key("153.0-3"),
|
||||||
|
update_librewolf.version_key("153.0.1-1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cask_and_release_versions_have_same_ordering(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
update_librewolf.version_key("153.0.3,1"),
|
||||||
|
update_librewolf.version_key("153.0.3-1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StableReleaseTest(unittest.TestCase):
|
||||||
|
@patch.object(update_librewolf, "fetch")
|
||||||
|
def test_prerelease_is_rejected(self, fetch) -> None:
|
||||||
|
fetch.return_value = json.dumps(
|
||||||
|
{
|
||||||
|
"tag_name": "154.0-1",
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": True,
|
||||||
|
}
|
||||||
|
).encode()
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, "not stable"):
|
||||||
|
update_librewolf.latest_stable_release()
|
||||||
|
|
||||||
|
|
||||||
|
class CaskUpdateTest(unittest.TestCase):
|
||||||
|
CASK = '''cask "librewolf" do
|
||||||
|
version "152.0.6,1"
|
||||||
|
sha256 arm: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
intel: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
arm64_linux: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||||
|
x86_64_linux: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||||
|
end
|
||||||
|
'''
|
||||||
|
CHECKSUMS = {
|
||||||
|
"arm": "1" * 64,
|
||||||
|
"intel": "2" * 64,
|
||||||
|
"arm64_linux": "3" * 64,
|
||||||
|
"x86_64_linux": "4" * 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch.object(update_librewolf, "release_checksums")
|
||||||
|
@patch.object(update_librewolf, "latest_stable_release")
|
||||||
|
def test_new_stable_release_updates_version_and_all_hashes(
|
||||||
|
self, latest_stable_release, release_checksums
|
||||||
|
) -> None:
|
||||||
|
latest_stable_release.return_value = {"stable_tag": "153.0.3-1"}
|
||||||
|
release_checksums.return_value = self.CHECKSUMS
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
cask = Path(directory) / "librewolf.rb"
|
||||||
|
cask.write_text(self.CASK)
|
||||||
|
|
||||||
|
self.assertTrue(update_librewolf.update_cask(cask))
|
||||||
|
updated = cask.read_text()
|
||||||
|
|
||||||
|
self.assertIn('version "153.0.3,1"', updated)
|
||||||
|
for checksum in self.CHECKSUMS.values():
|
||||||
|
self.assertIn(checksum, updated)
|
||||||
|
|
||||||
|
@patch.object(update_librewolf, "release_checksums")
|
||||||
|
@patch.object(update_librewolf, "latest_stable_release")
|
||||||
|
def test_older_release_does_not_downgrade_cask(
|
||||||
|
self, latest_stable_release, release_checksums
|
||||||
|
) -> None:
|
||||||
|
latest_stable_release.return_value = {"stable_tag": "151.0.3-1"}
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
cask = Path(directory) / "librewolf.rb"
|
||||||
|
cask.write_text(self.CASK)
|
||||||
|
self.assertFalse(update_librewolf.update_cask(cask))
|
||||||
|
self.assertEqual(cask.read_text(), self.CASK)
|
||||||
|
|
||||||
|
release_checksums.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user