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

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:
2026-07-06 18:54:30 -04:00
co-authored by Claude Fable 5
commit 4e452acc7d
66 changed files with 13574 additions and 0 deletions
+76
View File
@@ -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
+536
View File
@@ -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
[![Banner - Printer Tools App](https://github.com/62fixolab/62fixolab/raw/master/assets/banner-printer-tools.png)](https://printertools.app)
## Scooter Tools App
[![Banner - Scooter Tools App](https://github.com/62fixolab/62fixolab/raw/master/assets/banner-scooter-tools.png)](https://scootertools.app)
## AdMate App
[![Banner - AdMate App](https://github.com/62fixolab/62fixolab/raw/master/assets/banner-admate.png)](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())
+147
View File
@@ -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())
+135
View File
@@ -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"
+55
View File
@@ -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())
+31
View File
@@ -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."