Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ffd5d0d82
|
||
|
|
8484bddcf2
|
@@ -42,6 +42,14 @@ jobs:
|
||||
- name: Build Arch ISO
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Pin one timestamp for the whole job: mkarchiso derives the ISO label,
|
||||
# volume UUID, file mtimes, and ext4 hash seed from SOURCE_DATE_EPOCH.
|
||||
# Exporting it explicitly makes every timestamp-derived value traceable
|
||||
# (and lets later steps log the same value via $GITHUB_ENV).
|
||||
SOURCE_DATE_EPOCH="$(date -u +%s)"
|
||||
export SOURCE_DATE_EPOCH
|
||||
echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" >> "$GITHUB_ENV"
|
||||
echo "Building with SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH"
|
||||
# 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
|
||||
@@ -51,6 +59,80 @@ jobs:
|
||||
mkdir -p "$PWD/out"
|
||||
mkarchiso -v -w "$PWD/work" -o "$PWD/out" -- "$PWD"
|
||||
|
||||
- name: Verify ISO search metadata
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
isos=(out/*.iso)
|
||||
if (( ${#isos[@]} != 1 )); then
|
||||
echo "❌ Expected exactly 1 ISO in out/, found ${#isos[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
iso=${isos[0]}
|
||||
|
||||
# blkid exits non-zero when a field is missing; keep going so the
|
||||
# explicit guard below can report a useful error.
|
||||
vol_uuid=$(blkid -o value -s UUID "$iso" || true)
|
||||
vol_label=$(blkid -o value -s LABEL "$iso" || true)
|
||||
if [ -z "$vol_uuid" ]; then
|
||||
echo "❌ blkid found no UUID for $iso — volume date chain broken?" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Volume UUID: $vol_uuid"
|
||||
echo "Volume label: ${vol_label:-<none>}"
|
||||
echo "SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH:-<unset>}"
|
||||
|
||||
fail() {
|
||||
echo "❌ $1" >&2
|
||||
echo " If you changed boot configs (efiboot/, syslinux/, grub/) on purpose," >&2
|
||||
echo " update this 'Verify ISO search metadata' step to match." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1) /boot/<uuid>.uuid (the archisosearchfilename fallback key) must
|
||||
# exist inside the ISO and its stem must equal the volume UUID.
|
||||
uuid_refs=0
|
||||
while IFS= read -r member; do
|
||||
[ -z "$member" ] && continue
|
||||
member=${member#/}
|
||||
stem=$(basename "$member" .uuid)
|
||||
if [ "$stem" != "$vol_uuid" ]; then
|
||||
fail "$member: stem '$stem' != volume UUID '$vol_uuid'"
|
||||
fi
|
||||
echo " ✅ $member matches volume UUID"
|
||||
uuid_refs=$((uuid_refs + 1))
|
||||
done < <(bsdtar -tf "$iso" | grep -E '(^|/)boot/[0-9A-Za-z-]+\.uuid$' || true)
|
||||
if (( uuid_refs == 0 )); then
|
||||
fail "no /boot/<uuid>.uuid file found inside the ISO"
|
||||
fi
|
||||
|
||||
# 2) Every archisosearchuuid= in shipped boot configs must equal the
|
||||
# volume UUID; every %ARCHISO% placeholder must have been substituted.
|
||||
search_refs=0
|
||||
while IFS= read -r member; do
|
||||
[ -z "$member" ] && continue
|
||||
member=${member#/}
|
||||
if ! content=$(bsdtar -xOf "$iso" "$member" 2>/dev/null); then
|
||||
fail "could not extract $member from the ISO"
|
||||
fi
|
||||
if printf '%s\n' "$content" | grep -q '%ARCHISO'; then
|
||||
fail "$member contains an unsubstituted %ARCHISO% placeholder"
|
||||
fi
|
||||
while IFS= read -r val; do
|
||||
[ -z "$val" ] && continue
|
||||
search_refs=$((search_refs + 1))
|
||||
if [ "$val" != "$vol_uuid" ]; then
|
||||
fail "$member: archisosearchuuid=$val != volume UUID '$vol_uuid'"
|
||||
fi
|
||||
echo " ✅ $member: archisosearchuuid=$val matches volume UUID"
|
||||
done < <(printf '%s\n' "$content" | grep -o 'archisosearchuuid=[^[:space:]]*' | cut -d= -f2- || true)
|
||||
done < <(bsdtar -tf "$iso" | grep -E '(^|/)(loader/entries/.*\.conf|boot/.*\.(cfg|conf))$' || true)
|
||||
if (( search_refs == 0 )); then
|
||||
fail "no archisosearchuuid= references found in shipped boot configs (params removed without updating this check?)"
|
||||
fi
|
||||
|
||||
echo "✅ ISO search metadata consistent: $uuid_refs uuid file(s), $search_refs search reference(s)"
|
||||
|
||||
- name: Generate Unique Dynamic Tag
|
||||
id: tag
|
||||
env:
|
||||
@@ -82,6 +164,7 @@ jobs:
|
||||
echo "release_name=Arch Linux ISO (${BASE_DATE} Rev ${REVISION})" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Release and Upload ISO
|
||||
id: release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
# Use internal Gitea URL to bypass traefik proxy timeout on large uploads
|
||||
@@ -141,6 +224,8 @@ jobs:
|
||||
# Build the release body with SHA-256 checksums
|
||||
printf -v body 'Arch Linux live ISO %s\n\nChecksums (SHA-256):\n' "$RELEASE_NAME"
|
||||
for i in "${!isos[@]}"; do
|
||||
# Release body: markdown backticks are literal (printf format below)
|
||||
# shellcheck disable=SC2016
|
||||
printf -v line -- '- `%s`: sha256 `%s`\n' "$(basename "${isos[$i]}")" "${sha256sums[$i]}"
|
||||
body+="$line"
|
||||
done
|
||||
@@ -160,6 +245,7 @@ jobs:
|
||||
echo "❌ Release response contained no id" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
||||
echo " ✅ Created release $TAG_NAME (ID: $RELEASE_ID)"
|
||||
|
||||
# Upload each ISO as a release asset, up to 3 attempts per file
|
||||
@@ -224,6 +310,124 @@ jobs:
|
||||
echo "✅ All ISOs uploaded to release $TAG_NAME"
|
||||
exit 0
|
||||
|
||||
- name: Verify uploaded ISO checksums
|
||||
if: success()
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
# Internal origin bypasses traefik (and its Anubis bot wall) — the
|
||||
# same host the upload step already uses.
|
||||
GITEA_API: http://192.168.1.168:3000/api/v1
|
||||
GITEA_ORIGIN: http://192.168.1.168:3000
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
TAG_NAME: ${{ steps.tag.outputs.tag_name }}
|
||||
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
for var in GITEA_TOKEN TAG_NAME RELEASE_ID; do
|
||||
if [ -z "${!var:-}" ]; then
|
||||
echo "❌ $var is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
case "$RELEASE_ID" in
|
||||
''|*[!0-9]*) echo "❌ RELEASE_ID is not numeric: $RELEASE_ID" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# fail_verify <reason...>: job fails but the release is kept — used
|
||||
# for transport/API errors where the upload itself may be fine.
|
||||
fail_verify() {
|
||||
echo "❌ $1" >&2
|
||||
echo " Release $TAG_NAME (ID $RELEASE_ID) left in place (not proven corrupt)." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# abort_release <reason...>: the asset is provably wrong — delete the
|
||||
# release so a corrupt asset can never be published, then fail.
|
||||
abort_release() {
|
||||
echo "❌ $1" >&2
|
||||
echo "Deleting release $TAG_NAME (ID $RELEASE_ID) so the bad upload cannot be used..." >&2
|
||||
if ! curl -sf -X DELETE -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_API/repos/$REPOSITORY/releases/$RELEASE_ID" -o /dev/null; then
|
||||
echo "⚠️ Could not delete release $RELEASE_ID — delete it manually!" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
isos=(out/*.iso)
|
||||
if (( ${#isos[@]} == 0 )); then
|
||||
echo "❌ No ISO in out/ to verify" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Recompute local checksums
|
||||
declare -A local_sha local_size
|
||||
for iso in "${isos[@]}"; do
|
||||
fname=$(basename "$iso")
|
||||
local_sha["$fname"]=$(sha256sum "$iso" | awk '{print $1}')
|
||||
local_size["$fname"]=$(stat -c %s "$iso")
|
||||
if [ -z "${local_sha[$fname]}" ]; then
|
||||
echo "❌ Failed to compute SHA-256 for '$iso'" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# List the release's assets as uploaded (API error = transient, no delete)
|
||||
if ! curl -sf -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_API/repos/$REPOSITORY/releases/$RELEASE_ID/assets" \
|
||||
-o "$tmpdir/assets.json"; then
|
||||
fail_verify "Failed to list assets of release $RELEASE_ID (API/transport error)"
|
||||
fi
|
||||
|
||||
for iso in "${isos[@]}"; do
|
||||
fname=$(basename "$iso")
|
||||
api_size=$(jq -er --arg name "$fname" \
|
||||
'.[] | select(.name == $name) | .size' "$tmpdir/assets.json" 2>/dev/null) \
|
||||
|| abort_release "Asset '$fname' missing from release $RELEASE_ID"
|
||||
if [ "$api_size" != "${local_size[$fname]}" ]; then
|
||||
abort_release "Asset '$fname' size mismatch: server=$api_size local=${local_size[$fname]}"
|
||||
fi
|
||||
|
||||
# Download from the internal origin (traefik/Anubis bypassed).
|
||||
# iso_name contains a space ("Alvin archlinux"), so the filename
|
||||
# must be percent-encoded or curl rejects the URL outright.
|
||||
enc_fname=$(jq -rn --arg v "$fname" '$v|@uri')
|
||||
url="$GITEA_ORIGIN/$REPOSITORY/releases/download/$TAG_NAME/$enc_fname"
|
||||
downloaded=0
|
||||
for attempt in 1 2 3; do
|
||||
# Try header auth, then ?token=, then anonymous (public repo).
|
||||
if curl -sfL --max-time 600 -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$url" -o "$tmpdir/$fname"; then
|
||||
downloaded=1
|
||||
elif curl -sfL --max-time 600 "$url?token=$GITEA_TOKEN" \
|
||||
-o "$tmpdir/$fname"; then
|
||||
downloaded=1
|
||||
elif curl -sfL --max-time 600 "$url" -o "$tmpdir/$fname"; then
|
||||
downloaded=1
|
||||
fi
|
||||
[ "$downloaded" -eq 1 ] && break
|
||||
echo " ⚠️ Download of $fname failed (attempt $attempt/3)" >&2
|
||||
[ "$attempt" -lt 3 ] && sleep $(( attempt * 10 ))
|
||||
done
|
||||
if [ "$downloaded" -ne 1 ]; then
|
||||
fail_verify "Could not download asset '$fname' back from release $RELEASE_ID (auth/transport error, not proven corrupt)"
|
||||
fi
|
||||
|
||||
actual=$(sha256sum "$tmpdir/$fname" | awk '{print $1}')
|
||||
expected=${local_sha[$fname]}
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
abort_release "SHA-256 mismatch for '$fname': server=$actual local=$expected"
|
||||
fi
|
||||
echo " ✅ $fname: size and SHA-256 match"
|
||||
rm -f "$tmpdir/$fname"
|
||||
done
|
||||
|
||||
echo "✅ All uploaded assets verified against local ISO checksums"
|
||||
exit 0
|
||||
|
||||
- name: Keep only latest 2 releases
|
||||
if: success()
|
||||
env:
|
||||
|
||||
@@ -40,6 +40,7 @@ trap 'gum style --foreground 9 --bold "Failed at line $LINENO. See $LOG"' ERR
|
||||
die() { gum style --foreground 9 --bold "ERROR: $*"; exit 1; }
|
||||
info() { gum style --foreground 12 --bold "$*"; }
|
||||
ok() { gum style --foreground 10 --bold "$*"; }
|
||||
warn() { gum style --foreground 11 --bold "$*"; }
|
||||
|
||||
header() {
|
||||
gum style \
|
||||
@@ -85,10 +86,18 @@ screen_keymap() {
|
||||
header "Keyboard Layout"
|
||||
echo ""
|
||||
gum style --foreground 15 \
|
||||
"Select your console keymap." \
|
||||
"This affects passwords and the LUKS passphrase at boot."
|
||||
"Select the keyboard layout of your physical keyboard." \
|
||||
"This affects passwords and the LUKS passphrase at boot." \
|
||||
"" \
|
||||
"The default is the US layout (us)."
|
||||
echo ""
|
||||
KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...")
|
||||
if gum confirm --affirmative "Use us (default)" --negative "Choose another layout" \
|
||||
"Use US keyboard layout (us)?"; then
|
||||
KEYMAP="us"
|
||||
else
|
||||
KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...") \
|
||||
|| die "No keymap selected."
|
||||
fi
|
||||
[[ -z "$KEYMAP" ]] && die "No keymap selected."
|
||||
loadkeys "$KEYMAP" 2>/dev/null || true
|
||||
log "Keymap: $KEYMAP"
|
||||
@@ -357,15 +366,48 @@ install_pacstrap() {
|
||||
|
||||
local pkglist="/run/archiso/bootmnt/arch/pkglist.x86_64.txt"
|
||||
local pkgs=()
|
||||
local dropped=()
|
||||
local filter_ok=false
|
||||
|
||||
if [[ -f "$pkglist" ]]; then
|
||||
while IFS= read -r pkg; do
|
||||
[[ -z "$pkg" ]] && continue
|
||||
# The medium's pkglist comes from `pacman -Q`: lines are "name version".
|
||||
# Parse only the first field, then drop names not resolvable in the live
|
||||
# session's repos (e.g. local-aur packages are not configured there).
|
||||
info "Syncing package databases for resolvability check..."
|
||||
if pacman -Syy --noconfirm >>"$LOG" 2>&1; then
|
||||
declare -A available=()
|
||||
local db_line
|
||||
while IFS= read -r db_line; do
|
||||
[[ -n "$db_line" ]] && available["$db_line"]=1
|
||||
done < <(pacman -Slq 2>>"$LOG" || true)
|
||||
(( ${#available[@]} > 0 )) && filter_ok=true
|
||||
else
|
||||
warn "pacman -Syy failed (see $LOG) — installing without resolvability filter."
|
||||
fi
|
||||
|
||||
local line pkg
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
pkg=${line%% *}
|
||||
skip "$pkg" && continue
|
||||
if $filter_ok && [[ -z "${available[$pkg]:-}" ]]; then
|
||||
dropped+=("$pkg")
|
||||
continue
|
||||
fi
|
||||
pkgs+=("$pkg")
|
||||
done < "$pkglist"
|
||||
fi
|
||||
|
||||
if (( ${#dropped[@]} > 0 )); then
|
||||
warn "Skipped ${#dropped[@]} pkglist package(s) not found in configured repos:"
|
||||
gum style --foreground 11 "${dropped[@]/#/ - }"
|
||||
local d
|
||||
for d in "${dropped[@]}"; do
|
||||
log "Pacstrap: skipped unresolvable: $d"
|
||||
done
|
||||
fi
|
||||
|
||||
# Hardcoded essentials are never filtered — they must be in allowed repos.
|
||||
for p in base linux linux-firmware linux-cachyos linux-cachyos-headers \
|
||||
networkmanager grub efibootmgr sudo git intel-ucode amd-ucode \
|
||||
mkinitcpio cryptsetup dosfstools e2fsprogs btrfs-progs \
|
||||
@@ -378,7 +420,7 @@ install_pacstrap() {
|
||||
done
|
||||
|
||||
info "Installing ${#pkgs[@]} packages (this may take a while)..."
|
||||
log "Pacstrap: ${#pkgs[@]} packages"
|
||||
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
|
||||
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG"
|
||||
|
||||
ok "Packages installed."
|
||||
|
||||
Reference in New Issue
Block a user