diff --git a/.github/scripts/perform_deletions.py b/.github/scripts/perform_deletions.py new file mode 100644 index 0000000..20926c5 --- /dev/null +++ b/.github/scripts/perform_deletions.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Parse candidates.txt (TAB-separated, preserves empty fields) and perform deletions. +Reads environment variables: DRY_RUN, OWNER, PACKAGE, GITHUB_TOKEN. +This version reads the header (a # comment line) to determine column order +so it matches the workflow's output exactly. +""" +import os +import subprocess +import sys + +DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true" +OWNER = os.environ.get("OWNER") +PACKAGE = os.environ.get("PACKAGE") +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + +if OWNER is None or PACKAGE is None: + print("Missing OWNER or PACKAGE environment variables; aborting.") + raise SystemExit(1) + +print(f"DRY_RUN={DRY_RUN}, OWNER={OWNER}, PACKAGE={PACKAGE}") + +path = "candidates.txt" +if not os.path.exists(path): + print(f"{path} not found; aborting.") + raise SystemExit(1) + +with open(path, "r", newline="") as f: + for raw in f: + line = raw.rstrip("\n") + if not line or line.startswith("#"): + continue + fields = line.split("\t") + if len(fields) < 4: + continue + # Workflow writes: id, digest, created_at, selected + id_, digest, created, selected = fields[0], fields[1], fields[2], fields[3] + + if selected == 'yes': + if DRY_RUN: + print(f"[dry-run] Would delete package version (id: {id_}) (digest: {digest}) (created: {created})") + else: + print(f"Deleting package version (id: {id_}) (digest: {digest}) (created: {created})") + cmd = [ + 'curl', '-s', '-L','-X', 'DELETE', + '-H', f'Authorization: Bearer {GITHUB_TOKEN}', + '-H', 'Accept: application/vnd.github+json', + f'https://api.github.com/users/{OWNER}/packages/container/{PACKAGE}/versions/{id_}', + ] + try: + subprocess.run(cmd, check=False) + except Exception as e: + print('Warning: curl failed:', e) + else: + print(f"Candidate (id: {id_}) (created: {created}) is not older than 30 days; skipping") diff --git a/.github/workflows/cleanup.yaml b/.github/workflows/cleanup.yaml new file mode 100644 index 0000000..f8a3c54 --- /dev/null +++ b/.github/workflows/cleanup.yaml @@ -0,0 +1,174 @@ +permissions: + contents: read + packages: write +name: Cleanup old untagged GHCR images. +on: + schedule: + # Runs weekly on Sunday at 00:00 UTC + - cron: '0 0 * * 0' + workflow_dispatch: + inputs: + dry_run: + description: 'If "true", perform a dry-run (no deletes). Manual runs default to dry-run for safety.' + required: false + default: 'true' +jobs: + cleanup: + name: Cleanup untagged GHCR images older than 30 days + runs-on: ubuntu-latest + # Ensure scheduled runs only execute on `main`; allow manual dispatch on any branch + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.ref == 'refs/heads/main') + env: + IMAGE_NAME: framework-fedora-bootc + # For scheduled runs DRY_RUN will be 'false'. For manual dispatch, the input controls it. + DRY_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run) || 'false' }} + steps: + - name: Checkout (for context) + uses: actions/checkout@v6 + + - name: Install jq + run: | + sudo apt-get update; + sudo apt-get install -y jq + - name: Gather candidate versions + id: gather + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + PACKAGE: ${{ env.IMAGE_NAME }} + run: | + set -euo pipefail + out=candidates.txt + # Log uses id first (stable key); digest is included for visibility. + printf '# id\tdigest\tcreated_at\tselected\n' > "$out" + # Determine whether owner is an Organization or User + owner_type=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/users/$OWNER" | jq -r '.type') + if [ "$owner_type" = "Organization" ]; then + base="orgs/$OWNER" + else + base="users/$OWNER" + fi + + per_page=100 + page=1 + cutoff_ts=$(date -d "30 days ago" +%s) + while :; do + url="https://api.github.com/${base}/packages/container/${PACKAGE}/versions?per_page=${per_page}&page=${page}" + resp=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" "$url") + count=$(echo "$resp" | jq 'length') + if [ "$count" -eq 0 ]; then + break + fi + + # For each version, decide whether it's a candidate: + # - No tags => candidate + # - Tags exist and ALL tags look like commit hashes (7-40 hex chars) => candidate + # - Otherwise skip + echo "$resp" | jq -c '.[]' | while read -r item; do + id=$(echo "$item" | jq -r '.id') + digest=$(echo "$item" | jq -r '.metadata.container.digest // empty') + created=$(echo "$item" | jq -r '.created_at') + created_ts=$(date -d "$created" +%s) + + # gather tags as space-separated list + tags=$(echo "$item" | jq -r '.metadata.container.tags // [] | join(" ")') + + consider=false + if [ -z "$tags" ]; then + consider=true + else + # check whether ALL tags look like commit hashes (7-40 hex chars) + all_hashes=true + for t in $tags; do + if ! [[ "$t" =~ ^[0-9a-fA-F]{40}$ ]]; then + all_hashes=false + break + fi + done + if [ "$all_hashes" = true ]; then + consider=true + fi + fi + + if [ "$consider" = true ]; then + if [ "$created_ts" -lt "$cutoff_ts" ]; then + selected=yes + else + selected=no + fi + # Write id first (stable key), then digest, created, selected + printf "%s\t%s\t%s\t%s\n" "$id" "$digest" "$created" "$selected" >> "$out" + fi + done + + page=$((page+1)) + done + + # Add a summary header with counts selected vs total + total=$(tail -n +2 "$out" | wc -l | tr -d ' ') + # selected is now the 4th column + selected_count=$(tail -n +2 "$out" | awk -F $'\t' '$4=="yes"{c++}END{print c+0}') + not_selected=$((total - selected_count)) + tmp="${out}.tmp" + printf '# summary: selected=%s\tnot_selected=%s\ttotal=%s\n' "$selected_count" "$not_selected" "$total" > "$tmp" + cat "$out" >> "$tmp" + mv "$tmp" "$out" + + echo "Collected candidate versions (with summary):" + cat "$out" + + - name: Upload candidate list artifact + uses: actions/upload-artifact@v7 + with: + name: ghcr-candidates-${{ github.run_id }} + path: candidates.txt + + - name: Post summary to PR (if running in a PR context) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name || github.repository }} + RUN_ID: ${{ github.run_id }} + run: | + set -euo pipefail + # Try to find a PR associated with this run/commit + pr_number="" + # First, check if event payload has pull_request + if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ]; then + pr_number=$(jq -r .pull_request.number < "$GITHUB_EVENT_PATH" || true) + fi + # If not found, try commits -> pulls API + if [ -z "$pr_number" ]; then + resp=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/${OWNER}/${REPO}/commits/${GITHUB_SHA}/pulls") + pr_number=$(echo "$resp" | jq -r '.[0].number // empty') + fi + + if [ -z "$pr_number" ]; then + echo "No PR found for this run/commit; skipping PR comment." + exit 0 + fi + + # Read the summary header from candidates.txt (first line) + summary_line=$(head -n 1 candidates.txt || true) + # Build the comment body with printf to avoid YAML/heredoc parsing issues in editors + printf -v body 'Cleanup candidates summary (run %s):\n\n%s\n\nCandidate list artifact: ghcr-candidates-%s\n\nSee the artifact for the full list of candidate versions (id, created_at, selected).' \ + "$RUN_ID" "$summary_line" "$RUN_ID" + + echo "Posting summary to PR #${pr_number}" + curl -s -X POST -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \ + -d "{\"body\": $(jq -Rn --arg str "$body" '$str') }" \ + "https://api.github.com/repos/${OWNER}/${REPO}/issues/${pr_number}/comments" > /dev/null + + - name: Perform deletions (reads candidate list) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ env.DRY_RUN }} + PACKAGE: ${{ env.IMAGE_NAME }} + OWNER: ${{ github.repository_owner }} + run: | + set -euo pipefail + echo "DRY_RUN=${DRY_RUN}" + # Use an embedded Python script to parse TAB-separated fields (preserves empty fields) + # and perform deletions (or dry-run messages). This avoids awk quoting/syntax issues. + # Call the deletion helper script (keeps workflow YAML clean) + python3 .github/scripts/perform_deletions.py diff --git a/.github/workflows/main-build.yaml b/.github/workflows/main-build.yaml index 404da96..052db7c 100644 --- a/.github/workflows/main-build.yaml +++ b/.github/workflows/main-build.yaml @@ -17,31 +17,9 @@ concurrency: cancel-in-progress: true jobs: - pr-check: - name: Check for open PR for this commit - runs-on: ubuntu-latest - outputs: - has_pr: ${{ steps.check.outputs.has_pr }} - steps: - - name: Check for PRs referencing this commit - id: check - run: | - # Query GitHub API for pull requests that include this commit - resp=$(curl -s -H "Accept: application/vnd.github+json" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls") - count=$(echo "$resp" | python -c "import sys,json; print(len(json.load(sys.stdin)))") - if [ "$count" -gt 0 ]; then - echo "has_pr=true" >> $GITHUB_OUTPUT - else - echo "has_pr=false" >> $GITHUB_OUTPUT - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build: name: Build and publish image runs-on: ubuntu-latest - needs: pr-check - if: needs.pr-check.outputs.has_pr == 'false' env: IMAGE_NAME: framework-fedora-bootc REGISTRY: ghcr.io/compphy @@ -59,8 +37,7 @@ jobs: uses: redhat-actions/buildah-build@v2 with: image: ${{ env.IMAGE_NAME }} - # On main tag both 'latest' and '42'; on release-42 tag '42'. - tags: ${{ github.ref == 'refs/heads/main' && 'latest 42 ' || (github.ref == 'refs/heads/release-42' && '42 ' || '') }}${{ github.sha }} + tags: ${{ github.ref == 'refs/heads/release-42' && '42 ' || 'latest 43 ' }}${{ github.sha }} layers: True containerfiles: | ./Containerfile diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index a1eb174..652bd09 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -8,6 +8,7 @@ on: paths: - Containerfile - .github/workflows/pr-build.yaml + - bootc-image-builder concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -36,4 +37,4 @@ jobs: image: ${{ env.IMAGE_NAME }} layers: True containerfiles: | - ./Containerfile \ No newline at end of file + ./Containerfile diff --git a/Containerfile b/Containerfile index 80b3b97..7423cbe 100644 --- a/Containerfile +++ b/Containerfile @@ -1,7 +1,7 @@ -FROM quay.io/fedora/fedora-bootc:42@sha256:d82d8363bd69d668eb48e64cdfae23b5e6c5de9305a0bf01ad07505fab3454b5 AS builder +FROM quay.io/fedora/fedora-bootc:43@sha256:f804bd7a5c680b65e77ce7272cf0f04ca77e049f836df4e9added920fc733fcc AS builder # https://bugzilla.redhat.com/show_bug.cgi?id=2381864 -RUN dnf upgrade --enablerepo=updates-testing --refresh --advisory=FEDORA-2025-77e737a366 -RUN dnf install -y --exclude rootfiles @kde-desktop-environment @development-tools @container-management @system-tools @games; dnf clean all +RUN dnf upgrade -y --refresh +RUN dnf install -y --exclude rootfiles @kde-desktop-environment @development-tools @container-management @system-tools @games && dnf clean all RUN systemctl disable abrtd atd mcelog RUN systemctl set-default graphical.target RUN ln -snf ../usr/share/zoneinfo/America/New_York /etc/localtime @@ -10,9 +10,7 @@ RUN bootc container lint FROM builder COPY files/vscode.repo /etc/yum.repos.d/ -RUN dnf install -y code firefox terminator && dnf clean all RUN dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm RUN dnf config-manager setopt fedora-cisco-openh264.enabled=1 -RUN dnf install -y steam && dnf clean all +RUN dnf install -y code firefox terminator wireguard-tools steam solaar && dnf clean all RUN bootc container lint - diff --git a/Makefile b/Makefile index 81a728f..6950abf 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -OCI_IMAGE ?= ghcr.io/compphy/framework-fedora-bootc:latest -DISK_TYPE ?= anaconda-iso +OCI_IMAGE ?= ghcr.io/compphy/framework-fedora-bootc:latest +DISK_TYPE ?= raw ROOTFS ?= ext4 ARCH ?= amd64 # Use upsteam build: @@ -10,6 +10,7 @@ BIB_IMAGE ?= localhost/bootc-image-builder:latest # See https://github.com/osbuild/bootc-image-builder .PHONY: disk-image disk-image: + podman build -t framework-fedora-bootc $(CURDIR) podman build -t bootc-image-builder $(CURDIR)/bootc-image-builder mkdir -p ./output mkdir -p /var/lib/containers/storage @@ -28,4 +29,4 @@ disk-image: --type $(DISK_TYPE) \ --rootfs $(ROOTFS) \ --use-librepo \ - $(OCI_IMAGE) \ No newline at end of file + localhost/framework-fedora-bootc:latest \ No newline at end of file diff --git a/bootc-image-builder b/bootc-image-builder index 5b2ef48..ee18461 160000 --- a/bootc-image-builder +++ b/bootc-image-builder @@ -1 +1 @@ -Subproject commit 5b2ef48e08077fb76d34102ce976373b0c4a91dc +Subproject commit ee184614c4bd00034aec1543990997da9c153315