Tests / tests (push) Successful in 2m22s
Create a matching git tag + release in alvinlollo/AlvinArch-releases next to the main repo release, seed a short README there pointing back to AlvinArch (workflow status badges on top), and extend release pruning to the releases repo so both stay capped at 3.
301 lines
13 KiB
YAML
301 lines
13 KiB
YAML
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'
|
|
[](https://git.vilaythong.us/alvinlollo/AlvinArch/actions?workflow=build-iso.yaml)
|
|
[](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" |