name: Automated Arch ISO Build on: schedule: - cron: "0 0 1 * *" # Runs monthly on the 1st day of the month workflow_dispatch: # Allows manual triggers (multiple times a month) concurrency: group: iso-build cancel-in-progress: false # Never kill a build that is mid-flight jobs: build: runs-on: archlinux permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup local AUR repo run: | set -euo pipefail mkdir -p /tmp/localrepo cp localrepo/*.pkg.tar.zst localrepo/local-aur.db* localrepo/local-aur.files* /tmp/localrepo/ sed -i 's|file:///home/alvin/alvinarch/localrepo|file:///tmp/localrepo|' pacman.conf - name: Install archiso and dependencies run: | set -euo pipefail pacman -Syu --noconfirm --needed archiso jq git-lfs zstd - name: Build Arch ISO run: | set -euo pipefail # Clean up immediately even if this step is interrupted (cancel/exit): # unmount leftovers first, then remove the multi-GB work dir. trap 'umount -R "$PWD/work" 2>/dev/null || true; rm -rf "$PWD/work"' EXIT HUP INT TERM rm -rf "$PWD/work" # Always start from an empty out dir so stale ISOs are never re-uploaded rm -rf "$PWD/out" mkdir -p "$PWD/out" mkarchiso -v -w "$PWD/work" -o "$PWD/out" -- "$PWD" - name: Generate Unique Dynamic Tag id: tag env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_API: http://192.168.1.168:3000/api/v1 REPOSITORY: ${{ github.repository }} run: | set -euo pipefail test -n "$GITEA_TOKEN" # Pre-flight: confirm the internal Gitea API is reachable so failures are clear if ! curl -sfS -o /dev/null "$GITEA_API/version"; then echo "❌ Cannot reach Gitea API at $GITEA_API" >&2 exit 1 fi BASE_DATE=$(date -u +'%Y.%m.%d') REVISION=1 TAG_NAME="iso-${BASE_DATE}-r${REVISION}" # Check if the tag already exists via Gitea API, increment revision loop if it does while curl -sf -o /dev/null -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_API/repos/$REPOSITORY/releases/tags/$TAG_NAME"; do REVISION=$((REVISION + 1)) TAG_NAME="iso-${BASE_DATE}-r${REVISION}" done echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" echo "release_name=Arch Linux ISO (${BASE_DATE} Rev ${REVISION})" >> "$GITHUB_OUTPUT" - name: Create Release and Upload ISO via Git LFS env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} # Use internal Gitea URL to bypass traefik proxy timeout on large uploads GITEA_API: http://192.168.1.168:3000/api/v1 GITEA_PUBLIC_URL: https://git.vilaythong.us REPOSITORY: ${{ github.repository }} RELEASES_REPO: alvinlollo/AlvinArch-releases TAG_NAME: ${{ steps.tag.outputs.tag_name }} RELEASE_NAME: ${{ steps.tag.outputs.release_name }} run: | set -euo pipefail shopt -s nullglob test -n "$GITEA_TOKEN" test -n "$TAG_NAME" test -n "$RELEASE_NAME" # Pre-flight: confirm the internal Gitea API is reachable so failures are clear if ! curl -sfS -o /dev/null "$GITEA_API/version"; then echo "❌ Cannot reach Gitea API at $GITEA_API" >&2 exit 1 fi RELEASES_OWNER="${RELEASES_REPO%%/*}" RELEASES_NAME="${RELEASES_REPO#*/}" GITEA_BASE="${GITEA_API%/api/v1}" GITEA_SCHEME="${GITEA_BASE%%://*}" GITEA_HOST="${GITEA_BASE#*://}" CLONE_URL="${GITEA_SCHEME}://${RELEASES_OWNER}:${GITEA_TOKEN}@${GITEA_HOST}/${RELEASES_REPO}.git" REPO_DIR=/tmp/alvinarch-releases trap 'rm -rf "$REPO_DIR"' EXIT # Collect the ISOs produced by the build isos=(out/*.iso) if (( ${#isos[@]} == 0 )); then echo "❌ No ISO found in out/ — nothing to upload, aborting." >&2 exit 1 fi # Compress each ISO with zstd and record its SHA-256 zst_files=() sha256sums=() for iso in "${isos[@]}"; do fname=$(basename "$iso") size=$(stat -c %s "$iso") # Sanity check: a real ISO is multi-GB; anything tiny is the wrong file if (( size < 100000000 )); then echo "❌ '$iso' is only $size bytes — refusing to upload (not an ISO)." >&2 exit 1 fi echo "Compressing $fname ($(( size / 1024 / 1024 )) MiB) with zstd..." zstd -T0 -19 -f "$iso" -o "$iso.zst" sha=$(sha256sum "$iso.zst" | awk '{print $1}') echo " ✅ ${fname}.zst: $(( $(stat -c %s "$iso.zst") / 1024 / 1024 )) MiB, sha256=$sha" zst_files+=("$iso.zst") sha256sums+=("$sha") done # Make sure the releases repo exists (create it via API if missing) if ! curl -sf -o /dev/null -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_API/repos/$RELEASES_REPO"; then echo "Creating releases repo '$RELEASES_REPO'..." curl -sf -X POST \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ "$GITEA_API/user/repos" \ -d "{\"name\": \"$RELEASES_NAME\", \"description\": \"Compressed Arch Linux live ISOs (Git LFS)\", \"auto_init\": true}" > /dev/null fi # Push the compressed ISOs to the releases repo via Git LFS if ! git clone --quiet "$CLONE_URL" "$REPO_DIR" 2>/dev/null; then echo "❌ Failed to clone $RELEASES_REPO (check token access)." >&2 exit 1 fi # Work on the 'main' branch (create it if the remote default differs) if git -C "$REPO_DIR" rev-parse --verify -q origin/main >/dev/null 2>&1; then git -C "$REPO_DIR" checkout -q -B main origin/main else git -C "$REPO_DIR" checkout -q -B main fi git -C "$REPO_DIR" lfs install --local git -C "$REPO_DIR" lfs track "*.iso.zst" mkdir -p "$REPO_DIR/releases/$TAG_NAME" for zst in "${zst_files[@]}"; do cp "$zst" "$REPO_DIR/releases/$TAG_NAME/" done # Seed a short README redirecting to the main repo (status badges on top) cat > "$REPO_DIR/README.md" <<'MDEOF' [![Scheduled ArchISO Build](https://git.vilaythong.us/alvinlollo/AlvinArch/actions/workflows/build-iso.yaml/badge.svg?branch=main)](https://git.vilaythong.us/alvinlollo/AlvinArch/actions?workflow=build-iso.yaml) [![Tests](https://git.vilaythong.us/alvinlollo/AlvinArch/actions/workflows/tests.yaml/badge.svg?branch=main)](https://git.vilaythong.us/alvinlollo/AlvinArch/actions?workflow=tests.yaml) # AlvinArch Releases Compressed (zstd) Arch Linux live ISOs built automatically by [alvinlollo/AlvinArch](https://git.vilaythong.us/alvinlollo/AlvinArch). **Main repository:** [alvinlollo/AlvinArch](https://git.vilaythong.us/alvinlollo/AlvinArch) MDEOF git -C "$REPO_DIR" config user.name "Gitea Actions" git -C "$REPO_DIR" config user.email "actions@gitea.local" git -C "$REPO_DIR" add .gitattributes README.md "releases/$TAG_NAME" if git -C "$REPO_DIR" diff --cached --quiet; then echo "⚠️ No changes to commit for $TAG_NAME — releasing as-is." >&2 else git -C "$REPO_DIR" commit --quiet -m "Add $TAG_NAME" fi if ! git -C "$REPO_DIR" push --quiet origin main 2>/dev/null; then echo "❌ Failed to push $TAG_NAME to $RELEASES_REPO." >&2 exit 1 fi # Verify each LFS object exists server-side (oid == the sha256 we recorded) for i in "${!zst_files[@]}"; do fname=$(basename "${zst_files[$i]}") size=$(stat -c %s "${zst_files[$i]}") oid=${sha256sums[$i]} lfs_payload=$(jq -nc --arg oid "$oid" --argjson sz "$size" \ '{operation:"download",transfers:["basic"],objects:[{oid:$oid,size:$sz}]}') obj=$(curl -sf -u "$RELEASES_OWNER:$GITEA_TOKEN" \ -H "Content-Type: application/vnd.git-lfs+json" \ -d "$lfs_payload" \ "$GITEA_BASE/$RELEASES_REPO.git/info/lfs/objects/batch") if [[ "$(jq -r '.objects[0] | has("actions")' <<< "$obj")" != "true" ]]; then echo "❌ Verification failed: LFS object for '$fname' ($size bytes) missing on server." >&2 exit 1 fi echo " ✅ Verified '$fname' ($size bytes) on $RELEASES_REPO" done # Create the release with a link to the file directory + SHA-256 checksums DOWNLOAD_URL="$GITEA_PUBLIC_URL/$RELEASES_REPO/src/branch/main/releases/$TAG_NAME" printf -v body 'Arch Linux live ISO %s\n\nDownload: %s\n\nChecksums (SHA-256):\n' \ "$RELEASE_NAME" "$DOWNLOAD_URL" for i in "${!zst_files[@]}"; do printf -v line -- '- `%s`: sha256 `%s`\n' "$(basename "${zst_files[$i]}")" "${sha256sums[$i]}" body+="$line" done payload=$(jq -nc --arg tag "$TAG_NAME" --arg name "$RELEASE_NAME" --arg body "$body" \ '{tag_name:$tag,name:$name,body:$body}') RELEASE_ID=$(curl -sf -X POST \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ "$GITEA_API/repos/$REPOSITORY/releases" \ -d "$payload" | jq -er '.id') echo " ✅ Released $TAG_NAME (ID: $RELEASE_ID) with download link + SHA-256 checksums" # Mirror the release tag on the releases repo for redundancy/discovery if ! git -C "$REPO_DIR" ls-remote --exit-code origin \ refs/tags/"$TAG_NAME" >/dev/null 2>&1; then git -C "$REPO_DIR" tag "$TAG_NAME" if ! git -C "$REPO_DIR" push --quiet origin "$TAG_NAME" 2>/dev/null; then echo "❌ Failed to push tag $TAG_NAME to $RELEASES_REPO." >&2 exit 1 fi fi if ! curl -sf -o /dev/null -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_API/repos/$RELEASES_REPO/releases/tags/$TAG_NAME"; then curl -sf -X POST \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ "$GITEA_API/repos/$RELEASES_REPO/releases" \ -d "$payload" > /dev/null echo " ✅ Mirrored $TAG_NAME release on $RELEASES_REPO" fi - name: Keep only latest 3 releases if: success() env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_API: http://192.168.1.168:3000/api/v1 REPOSITORY: ${{ github.repository }} RELEASES_REPO: alvinlollo/AlvinArch-releases run: | set -euo pipefail test -n "$GITEA_TOKEN" for repo in "$REPOSITORY" "$RELEASES_REPO"; do echo "Pruning old releases in $repo..." readarray -t ids < <(curl -sf -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_API/repos/$repo/releases" \ | jq -er 'sort_by(.created_at) | .[].id') to_delete=$(( ${#ids[@]} - 3 )) if (( to_delete > 0 )); then for ((i = 0; i < to_delete; i++)); do id=${ids[$i]} echo "Deleting old release ID: $id (in $repo)" curl -sf -X DELETE -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_API/repos/$repo/releases/$id" done fi done - name: Cleanup mkarchiso mounts and workdirs if: always() # Runs on success, failure, or cancellation run: | set -eu # Steps run as root (pacman is used directly); fall back to sudo if not if [ "$(id -u)" -eq 0 ]; then PRIV="" else PRIV="sudo" fi # Unmount any leftover loop devices or bind mounts in the work directory $PRIV umount -R "$PWD/work" 2>/dev/null || true # Clean up loop devices if any remain attached to the work dir $PRIV losetup -D || true # Remove working directories with proper permissions $PRIV rm -rf "$PWD/work" - name: Remove built ISO (success or cancellation) if: success() || cancelled() run: | set -eu # On failure the ISO is kept in out/ for manual upload rm -rf "$PWD/out"