BC-250 console image: custom GNOME recipe with remote admin
Build Bazzite BC-250 / Check Bazzite channel digests (push) Has been cancelled
Build Bazzite BC-250 / Build Custom Image (push) Has been cancelled
Build Bazzite BC-250 / Build Custom 40CU Image (push) Has been cancelled
Build Bazzite BC-250 / Save Bazzite channel digest cache (push) Has been cancelled
Build Bazzite BC-250 / Publish GitHub Release (push) Has been cancelled
Build Bazzite BC-250 / Check Bazzite channel digests (push) Has been cancelled
Build Bazzite BC-250 / Build Custom Image (push) Has been cancelled
Build Bazzite BC-250 / Build Custom 40CU Image (push) Has been cancelled
Build Bazzite BC-250 / Save Bazzite channel digest cache (push) Has been cancelled
Build Bazzite BC-250 / Publish GitHub Release (push) Has been cancelled
Based on 62fixolab/Latest-Bazzite-AMD-BC-250-Patched-Images @ 347fd4d. Adds on top of the fork: - recipes/bc250-console-gnome.yml: governor + gnome-remote-desktop + openssh-server, sshd enabled, hhd.service masked, no signing module (local build + ISO path) - files/console/usr/bin/bc250-remote-setup: one-time on-box SSH/RDP setup - files/console/usr/lib/bootc/kargs.d/50-bc250-ttm.toml: ttm memory kargs - BUILD-CONSOLE.md: build -> ISO -> validation instructions files/console/ is separate from files/system/ so the 40-CU unlock tooling stays out of this stable 24-CU image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
PUBLISH=0
|
||||
OWNER=""
|
||||
REPO=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/backfill-releases.sh [--publish]
|
||||
|
||||
Creates local release-preview/*.md files for every date found in GHCR.
|
||||
With --publish, creates or updates one GitHub Release per date.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--publish)
|
||||
PUBLISH=1
|
||||
shift
|
||||
;;
|
||||
--owner)
|
||||
OWNER="${2:?Missing value for --owner}"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
REPO="${2:?Missing value for --repo}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OWNER" || -z "$REPO" ]]; then
|
||||
OWNER="${OWNER:-$(gh repo view --json owner --jq '.owner.login')}"
|
||||
REPO="${REPO:-$(gh repo view --json name --jq '.name')}"
|
||||
fi
|
||||
|
||||
dates=()
|
||||
while IFS= read -r release_date; do
|
||||
dates+=("$release_date")
|
||||
done < <("$ROOT/scripts/generate-release-notes.py" --owner "$OWNER" --repo "$REPO" --list-dates)
|
||||
|
||||
for release_date in "${dates[@]}"; do
|
||||
cmd=("$ROOT/scripts/publish-release.sh" --owner "$OWNER" --repo "$REPO" --date "$release_date")
|
||||
|
||||
package_commit="$(
|
||||
"$ROOT/scripts/generate-release-notes.py" \
|
||||
--owner "$OWNER" \
|
||||
--repo "$REPO" \
|
||||
--date "$release_date" \
|
||||
--print-package-commit-tag
|
||||
)"
|
||||
if [[ -n "$package_commit" ]]; then
|
||||
target_commit="$(git rev-parse --verify --quiet "${package_commit}^{commit}" || true)"
|
||||
if [[ -n "$target_commit" ]]; then
|
||||
cmd+=(--target "$target_commit")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$PUBLISH" -eq 1 ]]; then
|
||||
cmd+=(--publish)
|
||||
fi
|
||||
"${cmd[@]}"
|
||||
done
|
||||
Executable
+536
@@ -0,0 +1,536 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate grouped GitHub release notes for published BC-250 images."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CHANNELS = [
|
||||
{
|
||||
"channel": "stable",
|
||||
"label": "Stable",
|
||||
"suffix": "",
|
||||
},
|
||||
{
|
||||
"channel": "testing",
|
||||
"label": "Testing",
|
||||
"suffix": "-testing",
|
||||
},
|
||||
{
|
||||
"channel": "unstable",
|
||||
"label": "Unstable",
|
||||
"suffix": "-unstable",
|
||||
},
|
||||
]
|
||||
|
||||
BASE_PACKAGES = [
|
||||
{
|
||||
"kind": "normal",
|
||||
"family": "Deck",
|
||||
"variant": "Deck / Game Mode",
|
||||
"name": "bazzite-bc250-patched-deck",
|
||||
},
|
||||
{
|
||||
"kind": "normal",
|
||||
"family": "GNOME",
|
||||
"variant": "GNOME",
|
||||
"name": "bazzite-bc250-patched-gnome",
|
||||
},
|
||||
{
|
||||
"kind": "normal",
|
||||
"family": "KDE",
|
||||
"variant": "KDE",
|
||||
"name": "bazzite-bc250-patched-kde",
|
||||
},
|
||||
{
|
||||
"kind": "40cu",
|
||||
"family": "Deck",
|
||||
"variant": "Deck / Game Mode 40CU",
|
||||
"name": "bazzite-bc250-patched-deck-40cu",
|
||||
},
|
||||
{
|
||||
"kind": "40cu",
|
||||
"family": "GNOME",
|
||||
"variant": "GNOME 40CU",
|
||||
"name": "bazzite-bc250-patched-gnome-40cu",
|
||||
},
|
||||
{
|
||||
"kind": "40cu",
|
||||
"family": "KDE",
|
||||
"variant": "KDE 40CU",
|
||||
"name": "bazzite-bc250-patched-kde-40cu",
|
||||
},
|
||||
]
|
||||
|
||||
PACKAGES = [
|
||||
{
|
||||
**base_package,
|
||||
"channel": channel["channel"],
|
||||
"channel_label": channel["label"],
|
||||
"name": f"{base_package['name']}{channel['suffix']}",
|
||||
}
|
||||
for channel in CHANNELS
|
||||
for base_package in BASE_PACKAGES
|
||||
]
|
||||
|
||||
SPONSORS = """# 🎉 Sponsors
|
||||
|
||||
## Printer Tools App
|
||||
[](https://printertools.app)
|
||||
|
||||
## Scooter Tools App
|
||||
[](https://scootertools.app)
|
||||
|
||||
## AdMate App
|
||||
[](https://admate.dev)
|
||||
"""
|
||||
|
||||
|
||||
def normalize_date(value: str | None) -> tuple[str, str]:
|
||||
if value is None:
|
||||
today = dt.datetime.now(dt.timezone.utc).date()
|
||||
return today.strftime("%Y%m%d"), today.strftime("%Y.%m.%d")
|
||||
|
||||
raw = value.strip()
|
||||
match = re.fullmatch(r"(\d{4})[.-]?(\d{2})[.-]?(\d{2})", raw)
|
||||
if not match:
|
||||
raise SystemExit(f"Invalid date '{value}'. Use YYYY.MM.DD, YYYY-MM-DD, or YYYYMMDD.")
|
||||
|
||||
y, m, d = match.groups()
|
||||
dt.date(int(y), int(m), int(d))
|
||||
return f"{y}{m}{d}", f"{y}.{m}.{d}"
|
||||
|
||||
|
||||
def run_json(command: list[str], retries: int, retry_delay: int) -> object:
|
||||
last_error = ""
|
||||
|
||||
for attempt in range(1, retries + 1):
|
||||
proc = subprocess.run(command, text=True, capture_output=True, check=False)
|
||||
if proc.returncode == 0:
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
last_error = proc.stderr.strip() or proc.stdout.strip()
|
||||
if "HTTP 404" in last_error or "Not Found" in last_error:
|
||||
raise FileNotFoundError(last_error)
|
||||
|
||||
if attempt < retries:
|
||||
time.sleep(retry_delay)
|
||||
|
||||
raise RuntimeError(f"Command failed after {retries} attempts: {' '.join(command)}\n{last_error}")
|
||||
|
||||
|
||||
def fetch_package_versions(owner: str, package: str, retries: int, retry_delay: int) -> list[dict]:
|
||||
errors: list[str] = []
|
||||
|
||||
for scope in ("users", "orgs"):
|
||||
path = f"/{scope}/{owner}/packages/container/{package}/versions?per_page=100"
|
||||
try:
|
||||
data = run_json(["gh", "api", path], retries, retry_delay)
|
||||
except FileNotFoundError as exc:
|
||||
errors.append(str(exc))
|
||||
continue
|
||||
except RuntimeError as exc:
|
||||
errors.append(str(exc))
|
||||
continue
|
||||
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
|
||||
if any("HTTP 404" in error or "Not Found" in error for error in errors):
|
||||
return []
|
||||
|
||||
raise RuntimeError("\n".join(errors))
|
||||
|
||||
|
||||
def version_tags(version: dict) -> list[str]:
|
||||
metadata = version.get("metadata") or {}
|
||||
container = metadata.get("container") or {}
|
||||
tags = container.get("tags") or []
|
||||
return [str(tag) for tag in tags]
|
||||
|
||||
|
||||
def collect_entries(owner: str, date_ymd: str, retries: int, retry_delay: int) -> list[dict]:
|
||||
entries: list[dict] = []
|
||||
exact_tag_re = re.compile(rf"^{re.escape(date_ymd)}-(\d+)$")
|
||||
|
||||
for package in PACKAGES:
|
||||
versions = fetch_package_versions(owner, package["name"], retries, retry_delay)
|
||||
|
||||
for version in versions:
|
||||
tags = version_tags(version)
|
||||
exact_tags = sorted(tag for tag in tags if exact_tag_re.fullmatch(tag))
|
||||
if not exact_tags:
|
||||
continue
|
||||
|
||||
exact_tag = exact_tags[-1]
|
||||
os_version = exact_tag.rsplit("-", 1)[-1]
|
||||
commit_tags = sorted(
|
||||
tag
|
||||
for tag in tags
|
||||
if re.fullmatch(r"(?!\d{8}-\d+$)[0-9a-f]{7,40}-\d+", tag)
|
||||
)
|
||||
digest = str(version.get("name", ""))
|
||||
if not digest.startswith("sha256:"):
|
||||
digest = f"sha256:{digest}"
|
||||
|
||||
entries.append(
|
||||
{
|
||||
**package,
|
||||
"exact_tag": exact_tag,
|
||||
"os_version": os_version,
|
||||
"base_version": f"F{os_version}.{date_ymd}",
|
||||
"digest": digest,
|
||||
"created_at": version.get("created_at", ""),
|
||||
"commit_tag": commit_tags[-1] if commit_tags else "",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def list_dates(owner: str, retries: int, retry_delay: int) -> list[str]:
|
||||
dates: set[str] = set()
|
||||
date_tag_re = re.compile(r"^(\d{4})(\d{2})(\d{2})(?:-\d+)?$")
|
||||
|
||||
for package in PACKAGES:
|
||||
versions = fetch_package_versions(owner, package["name"], retries, retry_delay)
|
||||
for version in versions:
|
||||
for tag in version_tags(version):
|
||||
match = date_tag_re.fullmatch(tag)
|
||||
if match:
|
||||
dates.add(".".join(match.groups()))
|
||||
|
||||
return sorted(dates)
|
||||
|
||||
|
||||
def unique_package_commit_tag(entries: list[dict]) -> str:
|
||||
commit_tags = sorted(
|
||||
{
|
||||
entry["commit_tag"].split("-", 1)[0]
|
||||
for entry in entries
|
||||
if entry.get("commit_tag")
|
||||
}
|
||||
)
|
||||
return commit_tags[0] if len(commit_tags) == 1 else ""
|
||||
|
||||
|
||||
def package_url(repo_url: str, package: str) -> str:
|
||||
return f"{repo_url}/pkgs/container/{package}"
|
||||
|
||||
|
||||
def image_ref(owner: str, package: str, tag: str) -> str:
|
||||
return f"ghcr.io/{owner}/{package}:{tag}"
|
||||
|
||||
|
||||
def install_block(owner: str, entry: dict, tag: str) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
f"{entry['variant']}:",
|
||||
"",
|
||||
"```bash",
|
||||
f"rpm-ostree rebase ostree-image-signed:docker://{image_ref(owner, entry['name'], tag)}",
|
||||
"systemctl reboot",
|
||||
"```",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def markdown_table(entries: list[dict], owner: str, repo_url: str) -> str:
|
||||
lines = [
|
||||
"| Variant | Bazzite base | Image | Exact tag | Digest |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
|
||||
for entry in entries:
|
||||
image = f"ghcr.io/{owner}/{entry['name']}"
|
||||
package_link = package_url(repo_url, entry["name"])
|
||||
lines.append(
|
||||
f"| {entry['variant']} | `{entry['base_version']}` | [`{image}`]({package_link}) | "
|
||||
f"`{entry['exact_tag']}` | `{entry['digest']}` |"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def base_version_summary(entries: list[dict]) -> str:
|
||||
channel_order = ["stable", "testing", "unstable"]
|
||||
channel_labels = {channel["channel"]: channel["label"] for channel in CHANNELS}
|
||||
family_order = ["Deck", "GNOME", "KDE"]
|
||||
grouped: dict[str, dict[str, set[str]]] = {}
|
||||
|
||||
for entry in entries:
|
||||
channel_group = grouped.setdefault(entry["channel"], {})
|
||||
channel_group.setdefault(entry["base_version"], set()).add(entry["family"])
|
||||
|
||||
def base_sort_key(item: tuple[str, set[str]]) -> tuple[int, str]:
|
||||
base_version, families = item
|
||||
ordered_families = [family_order.index(family) for family in families if family in family_order]
|
||||
return (min(ordered_families) if ordered_families else 99, base_version)
|
||||
|
||||
parts: list[str] = []
|
||||
for channel in channel_order:
|
||||
if channel not in grouped:
|
||||
continue
|
||||
|
||||
channel_parts: list[str] = []
|
||||
for base_version, families in sorted(grouped[channel].items(), key=base_sort_key):
|
||||
label = "/".join(family for family in family_order if family in families)
|
||||
channel_parts.append(f"{label} `{base_version}`")
|
||||
|
||||
parts.append(f"{channel_labels[channel]}: {', '.join(channel_parts)}")
|
||||
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def channel_labels_for_entries(entries: list[dict]) -> str:
|
||||
channel_order = [channel["channel"] for channel in CHANNELS]
|
||||
channel_labels = {channel["channel"]: channel["label"] for channel in CHANNELS}
|
||||
present = {entry["channel"] for entry in entries}
|
||||
return ", ".join(channel_labels[channel] for channel in channel_order if channel in present)
|
||||
|
||||
|
||||
def channel_entries(entries: list[dict], channel: str, kind: str) -> list[dict]:
|
||||
return [entry for entry in entries if entry["channel"] == channel and entry["kind"] == kind]
|
||||
|
||||
|
||||
def family_summary(entries: list[dict]) -> str:
|
||||
family_order = ["Deck", "GNOME", "KDE"]
|
||||
present = {entry["family"] for entry in entries}
|
||||
return ", ".join(family for family in family_order if family in present)
|
||||
|
||||
|
||||
def render_notes(
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
date_display: str,
|
||||
entries: list[dict],
|
||||
commit: str | None,
|
||||
run_url: str | None,
|
||||
) -> str:
|
||||
repo_url = f"https://github.com/{owner}/{repo}"
|
||||
normal_entries = [entry for entry in entries if entry["kind"] == "normal"]
|
||||
experimental_entries = [entry for entry in entries if entry["kind"] == "40cu"]
|
||||
|
||||
lines: list[str] = [
|
||||
SPONSORS.rstrip(),
|
||||
"",
|
||||
f"# Bazzite BC-250 Patched Images {date_display}",
|
||||
"",
|
||||
"New Bazzite AMD BC-250 patched image batch published to GHCR.",
|
||||
"",
|
||||
"> [!IMPORTANT]",
|
||||
"> These are OCI images, not ISOs. Install or update with `rpm-ostree rebase`.",
|
||||
"",
|
||||
"## What changed",
|
||||
"",
|
||||
"- Rebuilt from updated official Bazzite channel bases.",
|
||||
f"- Bazzite base versions: {base_version_summary(entries)}.",
|
||||
f"- Published updated channels: {channel_labels_for_entries(entries)}.",
|
||||
]
|
||||
|
||||
if normal_entries:
|
||||
lines.append(f"- Published updated normal images: {family_summary(normal_entries)}.")
|
||||
|
||||
if experimental_entries:
|
||||
lines.append(f"- Published updated optional experimental `-40cu` images: {family_summary(experimental_entries)}.")
|
||||
|
||||
non_stable_channels = {entry["channel"] for entry in entries if entry["channel"] != "stable"}
|
||||
if non_stable_channels:
|
||||
lines.append("- Added separate Bazzite `testing`/`unstable` packages so users can test those channels without replacing stable `latest` packages.")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Included in every image",
|
||||
"",
|
||||
"- `cyan-skillfish-governor-smu`.",
|
||||
"- GPU frequency scaling for AMD BC-250 boards.",
|
||||
"- MangoHud/radeontop `655%` GPU usage telemetry fix.",
|
||||
"- Signed images for `ostree-image-signed` rebases.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for channel in CHANNELS:
|
||||
normal = channel_entries(entries, channel["channel"], "normal")
|
||||
experimental = channel_entries(entries, channel["channel"], "40cu")
|
||||
|
||||
if normal:
|
||||
section_name = "Recommended stable images" if channel["channel"] == "stable" else f"{channel['label']} images"
|
||||
lines.extend(
|
||||
[
|
||||
f"## {section_name}",
|
||||
"",
|
||||
"Use these unless you are deliberately testing extra CUs." if channel["channel"] == "stable" else "Use these only if you deliberately want this Bazzite update channel.",
|
||||
"",
|
||||
markdown_table(normal, owner, repo_url),
|
||||
"",
|
||||
f"### Install {channel['label'].lower()} images",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for entry in normal:
|
||||
lines.append(install_block(owner, entry, "latest"))
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"Pin this exact build by replacing `latest` with the exact tag from the table above.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
if experimental:
|
||||
section_name = (
|
||||
"Experimental stable 40CU images"
|
||||
if channel["channel"] == "stable"
|
||||
else f"Experimental {channel['label']} 40CU images"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
f"## {section_name}",
|
||||
"",
|
||||
"> [!CAUTION]",
|
||||
"> The `-40cu` images do not force 40CU on boot. They include tools to test extra CUs. 32CU/40CU stability is silicon lottery.",
|
||||
"",
|
||||
markdown_table(experimental, owner, repo_url),
|
||||
"",
|
||||
f"Full 40CU guide: {repo_url}/blob/main/docs/40cu.md",
|
||||
"",
|
||||
f"### Install {channel['label'].lower()} experimental 40CU images",
|
||||
"",
|
||||
"Use these only if you want to test optional CU unlock tooling.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for entry in experimental:
|
||||
lines.append(install_block(owner, entry, "latest"))
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"Pin this exact build by replacing `latest` with the exact tag from the table above.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"## Documentation",
|
||||
"",
|
||||
f"- Main README: {repo_url}",
|
||||
f"- Bazzite channel/package guide: {repo_url}/blob/main/docs/update-channels.md",
|
||||
]
|
||||
)
|
||||
|
||||
if experimental_entries:
|
||||
lines.extend(
|
||||
[
|
||||
f"- Full 40CU guide: {repo_url}/blob/main/docs/40cu.md",
|
||||
"",
|
||||
"> [!WARNING]",
|
||||
"> If you install a `-40cu` image, read the 40CU guide before saving any boot profile. The image includes testing tools, but extra CUs are still silicon lottery.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
else:
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"## Package pages",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
lines.append(f"- {package_url(repo_url, entry['name'])}")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Source build",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
if run_url:
|
||||
lines.append(f"- Workflow run: {run_url}")
|
||||
if commit:
|
||||
lines.append(f"- Commit: `{commit}`")
|
||||
else:
|
||||
commit_tag = unique_package_commit_tag(entries)
|
||||
if commit_tag:
|
||||
lines.append(f"- Package commit tag: `{commit_tag}`")
|
||||
lines.append(f"- Build date: `{date_display}`")
|
||||
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--owner", required=True, help="GitHub/GHCR owner, for example 62fixolab.")
|
||||
parser.add_argument("--repo", default="Latest-Bazzite-AMD-BC-250-Patched-Images")
|
||||
parser.add_argument("--date", help="Release date as YYYY.MM.DD, YYYY-MM-DD, or YYYYMMDD. Defaults to UTC today.")
|
||||
parser.add_argument("--commit", help="Commit SHA to show in release notes.")
|
||||
parser.add_argument("--run-url", help="Workflow run URL to show in release notes.")
|
||||
parser.add_argument("--output", help="Write release notes to this file. Defaults to stdout.")
|
||||
parser.add_argument("--list-dates", action="store_true", help="List known publish dates and exit.")
|
||||
parser.add_argument("--print-package-commit-tag", action="store_true", help="Print the unique package commit tag for a date and exit.")
|
||||
parser.add_argument("--retries", type=int, default=8)
|
||||
parser.add_argument("--retry-delay", type=int, default=10)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
if args.list_dates:
|
||||
for date_value in list_dates(args.owner, args.retries, args.retry_delay):
|
||||
print(date_value)
|
||||
return 0
|
||||
|
||||
date_ymd, date_display = normalize_date(args.date)
|
||||
entries = collect_entries(args.owner, date_ymd, args.retries, args.retry_delay)
|
||||
if not entries:
|
||||
raise SystemExit(f"No package tags found for {date_display}.")
|
||||
|
||||
if args.print_package_commit_tag:
|
||||
commit_tag = unique_package_commit_tag(entries)
|
||||
if commit_tag:
|
||||
print(commit_tag)
|
||||
return 0
|
||||
|
||||
notes = render_notes(
|
||||
owner=args.owner,
|
||||
repo=args.repo,
|
||||
date_display=date_display,
|
||||
entries=entries,
|
||||
commit=args.commit,
|
||||
run_url=args.run_url,
|
||||
)
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(notes, encoding="utf-8")
|
||||
else:
|
||||
sys.stdout.write(notes)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare GitHub Actions matrices for changed Bazzite channel digests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
NORMAL_RECIPES = {
|
||||
"deck": ("bazzite-deck-patched.yml", "bazzite-bc250-patched-deck"),
|
||||
"gnome": ("bazzite-gnome-patched.yml", "bazzite-bc250-patched-gnome"),
|
||||
"kde": ("bazzite-kde-patched.yml", "bazzite-bc250-patched-kde"),
|
||||
}
|
||||
|
||||
FORTYCU_RECIPES = {
|
||||
"deck": ("bazzite-deck-patched-40cu.yml", "bazzite-bc250-patched-deck-40cu"),
|
||||
"gnome": ("bazzite-gnome-patched-40cu.yml", "bazzite-bc250-patched-gnome-40cu"),
|
||||
"kde": ("bazzite-kde-patched-40cu.yml", "bazzite-bc250-patched-kde-40cu"),
|
||||
}
|
||||
|
||||
|
||||
def read_digest_file(path: Path) -> dict[tuple[str, str], str]:
|
||||
digests: dict[tuple[str, str], str] = {}
|
||||
if not path.exists():
|
||||
return digests
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Current format: channel<TAB>variant<TAB>base_image<TAB>digest
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 4:
|
||||
channel, variant, _base_image, digest = parts[:4]
|
||||
digests[(channel, variant)] = digest
|
||||
continue
|
||||
|
||||
# Legacy stable cache format kept for migration from the previous workflow.
|
||||
if "=" in line:
|
||||
variant, digest = line.split("=", 1)
|
||||
digests[("stable", variant)] = digest
|
||||
|
||||
return digests
|
||||
|
||||
|
||||
def read_current_entries(path: Path) -> list[dict[str, str]]:
|
||||
entries: list[dict[str, str]] = []
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 4:
|
||||
raise SystemExit(f"Invalid current digest line: {raw_line}")
|
||||
|
||||
channel, variant, base_image, digest = parts
|
||||
entries.append(
|
||||
{
|
||||
"channel": channel,
|
||||
"variant": variant,
|
||||
"base_image": base_image,
|
||||
"base_digest": digest,
|
||||
}
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def channel_package_name(base_name: str, channel: str) -> str:
|
||||
if channel == "stable":
|
||||
return base_name
|
||||
return f"{base_name}-{channel}"
|
||||
|
||||
|
||||
def matrix_entry(entry: dict[str, str], recipe: str, base_package: str) -> dict[str, str]:
|
||||
return {
|
||||
**entry,
|
||||
"recipe": recipe,
|
||||
"image_name": channel_package_name(base_package, entry["channel"]),
|
||||
}
|
||||
|
||||
|
||||
def github_output(values: dict[str, str]) -> None:
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with Path(output_path).open("a", encoding="utf-8") as handle:
|
||||
for key, value in values.items():
|
||||
handle.write(f"{key}={value}\n")
|
||||
else:
|
||||
for key, value in values.items():
|
||||
print(f"{key}={value}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--current", required=True, type=Path)
|
||||
parser.add_argument("--previous", type=Path)
|
||||
parser.add_argument("--force", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
current_entries = read_current_entries(args.current)
|
||||
previous = read_digest_file(args.previous) if args.previous else {}
|
||||
|
||||
normal: list[dict[str, str]] = []
|
||||
fortycu: list[dict[str, str]] = []
|
||||
|
||||
for entry in current_entries:
|
||||
key = (entry["channel"], entry["variant"])
|
||||
previous_digest = previous.get(key)
|
||||
if not args.force and previous_digest == entry["base_digest"]:
|
||||
continue
|
||||
|
||||
variant = entry["variant"]
|
||||
normal_recipe, normal_package = NORMAL_RECIPES[variant]
|
||||
fortycu_recipe, fortycu_package = FORTYCU_RECIPES[variant]
|
||||
|
||||
normal.append(matrix_entry(entry, normal_recipe, normal_package))
|
||||
fortycu.append(matrix_entry(entry, fortycu_recipe, fortycu_package))
|
||||
|
||||
normal_matrix = {"include": normal}
|
||||
fortycu_matrix = {"include": fortycu}
|
||||
total_count = len(normal) + len(fortycu)
|
||||
|
||||
github_output(
|
||||
{
|
||||
"normal-matrix": json.dumps(normal_matrix, separators=(",", ":")),
|
||||
"normal-count": str(len(normal)),
|
||||
"fortycu-matrix": json.dumps(fortycu_matrix, separators=(",", ":")),
|
||||
"fortycu-count": str(len(fortycu)),
|
||||
"should-build": "true" if total_count else "false",
|
||||
}
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
DATE=""
|
||||
PUBLISH=0
|
||||
OWNER=""
|
||||
REPO=""
|
||||
TARGET=""
|
||||
RUN_URL=""
|
||||
OUTPUT=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/publish-release.sh --date YYYY.MM.DD [--publish]
|
||||
|
||||
Options:
|
||||
--date DATE Release date, accepts YYYY.MM.DD, YYYY-MM-DD, or YYYYMMDD.
|
||||
--publish Create or update the GitHub Release. Omit for dry-run.
|
||||
--owner OWNER GitHub/GHCR owner. Defaults to current repo owner.
|
||||
--repo REPO GitHub repo name. Defaults to current repo name.
|
||||
--target SHA Target commit for the Release tag. Defaults to HEAD.
|
||||
--run-url URL Optional workflow run URL for release notes.
|
||||
--output FILE Dry-run notes output path.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--date)
|
||||
DATE="${2:?Missing value for --date}"
|
||||
shift 2
|
||||
;;
|
||||
--publish)
|
||||
PUBLISH=1
|
||||
shift
|
||||
;;
|
||||
--owner)
|
||||
OWNER="${2:?Missing value for --owner}"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
REPO="${2:?Missing value for --repo}"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
TARGET="${2:?Missing value for --target}"
|
||||
shift 2
|
||||
;;
|
||||
--run-url)
|
||||
RUN_URL="${2:?Missing value for --run-url}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT="${2:?Missing value for --output}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$DATE" ]]; then
|
||||
echo "--date is required" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "$OWNER" || -z "$REPO" ]]; then
|
||||
OWNER="${OWNER:-$(gh repo view --json owner --jq '.owner.login')}"
|
||||
REPO="${REPO:-$(gh repo view --json name --jq '.name')}"
|
||||
fi
|
||||
|
||||
release_tag="$(
|
||||
python3 - "$DATE" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
raw = sys.argv[1]
|
||||
match = re.fullmatch(r"(\d{4})[.-]?(\d{2})[.-]?(\d{2})", raw)
|
||||
if not match:
|
||||
raise SystemExit(f"Invalid date: {raw}")
|
||||
print(".".join(match.groups()))
|
||||
PY
|
||||
)"
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT="$ROOT/release-preview/$release_tag.md"
|
||||
fi
|
||||
|
||||
args=(
|
||||
"$ROOT/scripts/generate-release-notes.py"
|
||||
--owner "$OWNER"
|
||||
--repo "$REPO"
|
||||
--date "$release_tag"
|
||||
--output "$OUTPUT"
|
||||
)
|
||||
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
args+=(--commit "$TARGET")
|
||||
fi
|
||||
|
||||
if [[ -n "$RUN_URL" ]]; then
|
||||
args+=(--run-url "$RUN_URL")
|
||||
fi
|
||||
|
||||
python3 "${args[@]}"
|
||||
|
||||
if [[ "$PUBLISH" -eq 0 ]]; then
|
||||
echo "Dry-run release notes written to: $OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if gh release view "$release_tag" >/dev/null 2>&1; then
|
||||
edit_args=(release edit "$release_tag" --title "$release_tag" --notes-file "$OUTPUT")
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
edit_args+=(--target "$TARGET")
|
||||
fi
|
||||
gh "${edit_args[@]}"
|
||||
else
|
||||
create_args=(release create "$release_tag" --title "$release_tag" --notes-file "$OUTPUT")
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
create_args+=(--target "$TARGET")
|
||||
fi
|
||||
gh "${create_args[@]}"
|
||||
fi
|
||||
|
||||
echo "Published GitHub Release: $release_tag"
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a BlueBuild recipe for a specific Bazzite channel and package name."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def replace_prefixed_line(lines: list[str], prefix: str, replacement: str) -> list[str]:
|
||||
replaced = False
|
||||
rendered: list[str] = []
|
||||
|
||||
for line in lines:
|
||||
if line.startswith(prefix):
|
||||
rendered.append(replacement)
|
||||
replaced = True
|
||||
else:
|
||||
rendered.append(line)
|
||||
|
||||
if not replaced:
|
||||
raise SystemExit(f"Could not find required line starting with {prefix!r}.")
|
||||
|
||||
return rendered
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--recipe", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--channel", required=True)
|
||||
parser.add_argument("--name", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
lines = args.recipe.read_text(encoding="utf-8").splitlines()
|
||||
lines = replace_prefixed_line(lines, "name:", f"name: {args.name}")
|
||||
lines = replace_prefixed_line(lines, "image-version:", f"image-version: {args.channel}")
|
||||
lines = [
|
||||
line.replace(" stable image ", f" {args.channel} image ")
|
||||
if line.startswith("description:")
|
||||
else line
|
||||
for line in lines
|
||||
]
|
||||
|
||||
args.output.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
print(f"Rendered {args.output} for {args.name}:{args.channel}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKDIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORKDIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sync_repo() {
|
||||
local repo="$1"
|
||||
local name="$2"
|
||||
local clone_dir="$WORKDIR/$name"
|
||||
|
||||
gh repo clone "$repo" "$clone_dir" -- --depth 1
|
||||
|
||||
rsync -a --delete --exclude='.git' "$clone_dir/" "$ROOT/vendor/$name/"
|
||||
rsync -a --delete --exclude='.git' "$clone_dir/" "$ROOT/files/system/usr/share/bc250-40cu/vendor/$name/"
|
||||
|
||||
git -C "$clone_dir" rev-parse HEAD > "$ROOT/vendor/$name.UPSTREAM_REF"
|
||||
git -C "$clone_dir" rev-parse HEAD > "$ROOT/files/system/usr/share/bc250-40cu/vendor/$name.UPSTREAM_REF"
|
||||
}
|
||||
|
||||
mkdir -p "$ROOT/vendor" "$ROOT/files/system/usr/share/bc250-40cu/vendor"
|
||||
|
||||
sync_repo duggasco/bc250-40cu-unlock bc250-40cu-unlock
|
||||
sync_repo WinnieLV/bc250-cu-live-manager bc250-cu-live-manager
|
||||
|
||||
echo "Updated 40CU vendor sources."
|
||||
Reference in New Issue
Block a user