Initial commit.
Build / Base Image (push) Failing after 1m59s
Build / CI Image (push) Has been skipped

This commit is contained in:
2026-05-14 22:15:28 -04:00
commit 67c15224ae
8 changed files with 447 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
name: Build
on:
push:
branches: [master]
schedule:
- cron: "0 0 * * 1"
permissions:
contents: read
# packages: write # not yet supported by Gitea
jobs:
base:
name: Base Image
runs-on: logaldeveloper-archlinux
outputs:
version_tag: ${{ steps.metadata.outputs.version_tag }}
base_digest: ${{ steps.build-image.outputs.digest }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
with:
driver-opts: network=host
- name: Log in to Gitea Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: git.logal.dev
username: ${{ gitea.repository_owner }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build rootfs
working-directory: archlinux-base
run: bash build-rootfs.sh
- name: Package rootfs
working-directory: archlinux-base
run: |
tar --numeric-owner --acls --xattrs \
--directory build/rootfs \
--create \
--file build/rootfs.tar \
.
rm -rf build/rootfs
- name: Generate image metadata
id: metadata
run: |
version_tag=$(date -u +%Y%m%d).${{ gitea.run_number }}
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf 'created=%s\n' "$created" | tee -a "$GITEA_OUTPUT"
printf 'version_tag=%s\n' "$version_tag" | tee -a "$GITEA_OUTPUT"
- name: Build image
id: build-image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: archlinux-base
network: host
push: true
build-args: |
OCI_CREATED=${{ steps.metadata.outputs.created }}
OCI_REVISION=${{ gitea.sha }}
OCI_VERSION=${{ steps.metadata.outputs.version_tag }}
tags: |
git.logal.dev/logaldeveloper/archlinux-base:${{ steps.metadata.outputs.version_tag }}
git.logal.dev/logaldeveloper/archlinux-base:latest
- name: Generate SBOM
working-directory: archlinux-base
env:
SYFT_CHECK_FOR_APP_UPDATE: "false"
run: |
syft scan registry:git.logal.dev/logaldeveloper/archlinux-base:${{ steps.metadata.outputs.version_tag }} \
--override-default-catalogers alpm-db-cataloger \
--source-name git.logal.dev/logaldeveloper/archlinux-base \
--source-version "${{ steps.metadata.outputs.version_tag }}" \
--output syft-table \
--output cyclonedx-json=archlinux-base-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json
sha256sum archlinux-base-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json
zstd -T0 --ultra -22 \
archlinux-base-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json
- name: Upload SBOM artifact
uses: christopherhx/gitea-upload-artifact@8818363695ca2d5782c64f6453273341374767b7 # v7
with:
name: archlinux-base-cyclonedx-${{ steps.metadata.outputs.version_tag }}
path: archlinux-base/archlinux-base-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json.zst
if-no-files-found: error
archive: "false"
ci:
name: CI Image
needs: base
runs-on: logaldeveloper-archlinux
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
with:
driver-opts: network=host
- name: Log in to Gitea Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: git.logal.dev
username: ${{ gitea.repository_owner }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Generate image metadata
id: metadata
run: |
version_tag="${{ needs.base.outputs.version_tag }}"
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf 'created=%s\n' "$created" | tee -a "$GITEA_OUTPUT"
printf 'version_tag=%s\n' "$version_tag" | tee -a "$GITEA_OUTPUT"
- name: Build image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: archlinux-ci
network: host
push: true
build-args: |
ARCHLINUX_BASE_IMAGE=git.logal.dev/logaldeveloper/archlinux-base:${{ steps.metadata.outputs.version_tag }}
OCI_BASE_DIGEST=${{ needs.base.outputs.base_digest }}
OCI_CREATED=${{ steps.metadata.outputs.created }}
OCI_REVISION=${{ gitea.sha }}
OCI_VERSION=${{ steps.metadata.outputs.version_tag }}
tags: |
git.logal.dev/logaldeveloper/archlinux-ci:${{ steps.metadata.outputs.version_tag }}
git.logal.dev/logaldeveloper/archlinux-ci:latest
- name: Generate SBOM
working-directory: archlinux-ci
env:
SYFT_CHECK_FOR_APP_UPDATE: "false"
run: |
syft scan registry:git.logal.dev/logaldeveloper/archlinux-ci:${{ steps.metadata.outputs.version_tag }} \
--override-default-catalogers alpm-db-cataloger \
--source-name git.logal.dev/logaldeveloper/archlinux-ci \
--source-version "${{ steps.metadata.outputs.version_tag }}" \
--output syft-table \
--output cyclonedx-json=archlinux-ci-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json
sha256sum archlinux-ci-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json
zstd -T0 --ultra -22 \
archlinux-ci-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json
- name: Upload SBOM artifact
uses: christopherhx/gitea-upload-artifact@8818363695ca2d5782c64f6453273341374767b7 # v7
with:
name: archlinux-ci-cyclonedx-${{ steps.metadata.outputs.version_tag }}
path: archlinux-ci/archlinux-ci-${{ steps.metadata.outputs.version_tag }}.cyclonedx.json.zst
if-no-files-found: error
archive: "false"
+5
View File
@@ -0,0 +1,5 @@
/archlinux-base/build/
/archlinux-base/*.cyclonedx.json
/archlinux-base/*.cyclonedx.json.zst
/archlinux-ci/*.cyclonedx.json
/archlinux-ci/*.cyclonedx.json.zst
+46
View File
@@ -0,0 +1,46 @@
# Arch Linux Images
Arch Linux container images for my projects and CI pipelines. These images are
tailored to my own repositories; they are not designed for general use and may
change without notice.
The images are rebuilt weekly and published to my container registry.
## Images
- `archlinux-base`: vanilla Arch Linux image assembled from the official Arch
bootstrap tarball.
- `archlinux-ci`: CI image built from `archlinux-base` with the development and
build tools I use in CI jobs.
Both images are published as `latest` and as `<yyyymmdd>.<run-number>` tags.
## Build Flow
The base image build downloads the latest official Arch bootstrap archive from
[my private Arch Linux mirror](https://logal.dev/projects/arch-linux-mirror/),
verifies its hashes and signature, updates the extracted rootfs with pacman,
then packages it into a tarball. The Docker image is built from `scratch` with
that rootfs.
The CI image is built after the base image, uses the same version tag, and adds:
- `7zip`
- `base-devel`
- `docker`
- `ffmpeg`
- `git`
- `hugo`
- `nodejs`
- `python`
- `shellcheck`
- `syft`
- `uv`
The CI image also installs the LogalNet internal certificate authority.
## Software Bill of Materials
Each build generates a CycloneDX Software Bill of Materials for the published
image, available from the corresponding
[CI job log](https://git.logal.dev/LogalDeveloper/Arch-Linux-Images/actions).
+2
View File
@@ -0,0 +1,2 @@
build/*
!build/rootfs.tar
+22
View File
@@ -0,0 +1,22 @@
FROM scratch
ARG OCI_CREATED=""
ARG OCI_REVISION=""
ARG OCI_SOURCE="https://git.logal.dev/LogalDeveloper/Arch-Linux-Images"
ARG OCI_VENDOR="Logan Fick <https://logal.dev/>"
ARG OCI_VERSION=""
LABEL org.opencontainers.image.created="${OCI_CREATED}" \
org.opencontainers.image.source="${OCI_SOURCE}" \
org.opencontainers.image.url="${OCI_SOURCE}" \
org.opencontainers.image.revision="${OCI_REVISION}" \
org.opencontainers.image.vendor="${OCI_VENDOR}" \
org.opencontainers.image.version="${OCI_VERSION}" \
org.opencontainers.image.title="Arch Linux Base" \
org.opencontainers.image.description="Vanilla Arch Linux container image built from the official Arch bootstrap tarball."
ADD build/rootfs.tar /
ENV LANG=C.UTF-8
CMD ["/usr/bin/bash"]
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
set -euo pipefail
if (( EUID != 0 )); then
printf 'error: %s must be run as root\n' "$0" >&2
exit 1
fi
# Keep all temporary files and the updated rootfs under this image directory so
# later CI steps can scan and package it from the Docker build context.
build_dir="$PWD/build"
rootfs_dir="$build_dir/rootfs"
bootstrap_base_url="https://mirrors.logal.dev/archlinux/iso/latest"
bootstrap_tar_name="archlinux-bootstrap-x86_64.tar.zst"
bootstrap_tar="$build_dir/$bootstrap_tar_name"
bootstrap_sig="$build_dir/$bootstrap_tar_name.sig"
sha256sums="$build_dir/sha256sums.txt"
b2sums="$build_dir/b2sums.txt"
# Download with retries because the bootstrap tarball is the largest and most
# failure-prone external input to this build.
download_file() {
local source_url=$1
local output_path=$2
printf 'Downloading %s\n' "$source_url"
curl --fail --location --show-error --retry 5 --retry-delay 3 \
--output "$output_path" \
"$source_url"
}
section() {
printf '\n[build-rootfs] %s\n' "$1"
}
# The bootstrap rootfs has its own placeholder resolver config. We replace it
# only while pacman needs network access inside the chroot.
restore_resolv_conf() {
if [[ -e "$rootfs_dir/etc/resolv.conf.bootstrap" ]]; then
chroot "$rootfs_dir" /usr/bin/mv /etc/resolv.conf.bootstrap /etc/resolv.conf
fi
}
# Remove the expanded rootfs only if the build fails; successful builds leave it
# behind for later CI steps such as scanning and packaging.
cleanup_target() {
if [[ -d "$rootfs_dir" ]]; then
restore_resolv_conf
rm -rf "$rootfs_dir"
fi
}
trap cleanup_target EXIT
# Start from a fresh rootfs directory.
section "Preparing build directory"
mkdir -p "$build_dir"
mkdir -p "$rootfs_dir"
# Fetch the official bootstrap archive, its signature, and both checksum files
# from the Logal mirror.
section "Downloading bootstrap files"
download_file "$bootstrap_base_url/$bootstrap_tar_name" "$bootstrap_tar"
download_file "$bootstrap_base_url/$bootstrap_tar_name.sig" "$bootstrap_sig"
download_file "$bootstrap_base_url/sha256sums.txt" "$sha256sums"
download_file "$bootstrap_base_url/b2sums.txt" "$b2sums"
bootstrap_sha256_line="$(grep -F " $bootstrap_tar_name" "$sha256sums")"
bootstrap_b2_line="$(grep -F " $bootstrap_tar_name" "$b2sums")"
# Verify both published hashes before trusting the downloaded tarball.
section "Verifying bootstrap archive"
printf 'Expected bootstrap SHA256: %s\n' "${bootstrap_sha256_line%% *}"
printf 'Actual bootstrap SHA256: '
sha256sum "$bootstrap_tar"
printf '%s\n' "$bootstrap_sha256_line" |
sed "s# $bootstrap_tar_name\$# $bootstrap_tar#" |
sha256sum --check --strict |
sed 's/: / (SHA256): /'
printf 'Expected bootstrap BLAKE2: %s\n' "${bootstrap_b2_line%% *}"
printf 'Actual bootstrap BLAKE2: '
b2sum "$bootstrap_tar"
printf '%s\n' "$bootstrap_b2_line" |
sed "s# $bootstrap_tar_name\$# $bootstrap_tar#" |
b2sum --check --strict |
sed 's/: / (BLAKE2): /'
# Verify the upstream pacman signature against the host pacman keyring.
pacman-key --verify "$bootstrap_sig" "$bootstrap_tar"
# Expand the rootfs without the archive's top-level root.x86_64 directory.
section "Extracting bootstrap rootfs"
tar --use-compress-program=unzstd \
--acls \
--xattrs \
--extract \
--file "$bootstrap_tar" \
--directory "$rootfs_dir" \
--strip-components=1
rm -f \
"$bootstrap_tar" \
"$bootstrap_sig" \
"$sha256sums" \
"$b2sums"
# The bootstrap tarball does not include an initialized pacman keyring.
section "Initializing pacman keyring"
rm -rf "$rootfs_dir/etc/pacman.d/gnupg"
pacman-key --gpgdir "$rootfs_dir/etc/pacman.d/gnupg" --init
pacman-key --gpgdir "$rootfs_dir/etc/pacman.d/gnupg" \
--populate-from "$rootfs_dir/usr/share/pacman/keyrings" \
--populate archlinux
# Pin package updates to the Logal Arch mirror as well.
section "Configuring rootfs for updates"
printf '%s\n' "Server = https://mirrors.logal.dev/archlinux/\$repo/os/\$arch" \
> "$rootfs_dir/etc/pacman.d/mirrorlist"
# Some install scripts write to /dev/null. The bootstrap archive ships an empty
# /dev, so provide a temporary node for the chrooted pacman transaction.
mknod -m 666 "$rootfs_dir/dev/null" c 1 3
# Give the chroot DNS resolution for package downloads, then restore the
# bootstrap resolver placeholder before packaging.
chroot "$rootfs_dir" /usr/bin/mv /etc/resolv.conf /etc/resolv.conf.bootstrap
cp /etc/resolv.conf "$rootfs_dir/etc/resolv.conf"
# Plain chroot lacks mount metadata, so CheckSpace cannot reliably inspect the
# target filesystem during the update.
cp "$rootfs_dir/etc/pacman.conf" "$rootfs_dir/etc/pacman-install.conf"
sed -i 's/^[[:space:]]*CheckSpace/#&/' "$rootfs_dir/etc/pacman-install.conf"
section "Updating rootfs packages"
chroot "$rootfs_dir" /usr/bin/pacman \
--config /etc/pacman-install.conf \
-Syu \
--disable-sandbox \
--noconfirm
chroot "$rootfs_dir" /usr/bin/rm /etc/pacman-install.conf
restore_resolv_conf
# Drop downloaded package files so they are not baked into the container image.
section "Cleaning package cache"
{ yes || true; } | chroot "$rootfs_dir" /usr/bin/pacman -Scc
section "Finalizing rootfs"
chroot "$rootfs_dir" /usr/bin/rm -f /dev/null
# Leave build/rootfs in place. CI packages it after any scan steps have run.
trap - EXIT
+41
View File
@@ -0,0 +1,41 @@
ARG ARCHLINUX_BASE_IMAGE=git.logal.dev/logaldeveloper/archlinux-base:latest
FROM ${ARCHLINUX_BASE_IMAGE}
ARG ARCHLINUX_BASE_IMAGE
ARG OCI_BASE_DIGEST=""
ARG OCI_CREATED=""
ARG OCI_REVISION=""
ARG OCI_SOURCE="https://git.logal.dev/LogalDeveloper/Arch-Linux-Images"
ARG OCI_VENDOR="Logan Fick <https://logal.dev/>"
ARG OCI_VERSION=""
LABEL org.opencontainers.image.created="${OCI_CREATED}" \
org.opencontainers.image.source="${OCI_SOURCE}" \
org.opencontainers.image.url="${OCI_SOURCE}" \
org.opencontainers.image.revision="${OCI_REVISION}" \
org.opencontainers.image.vendor="${OCI_VENDOR}" \
org.opencontainers.image.version="${OCI_VERSION}" \
org.opencontainers.image.title="Arch Linux CI" \
org.opencontainers.image.description="Arch Linux container image for LogalDeveloper's CI jobs." \
org.opencontainers.image.base.name="${ARCHLINUX_BASE_IMAGE}" \
org.opencontainers.image.base.digest="${OCI_BASE_DIGEST}"
COPY logalnet-internal-ca.crt /tmp/logalnet-internal-ca.crt
RUN pacman -Syu --needed --disable-sandbox --noconfirm \
base-devel \
7zip \
docker \
ffmpeg \
git \
hugo \
nodejs \
python \
shellcheck \
syft \
uv \
&& trust anchor --store /tmp/logalnet-internal-ca.crt \
&& rm /tmp/logalnet-internal-ca.crt \
&& { yes || true; } | pacman -Scc
CMD ["/usr/bin/bash"]
+17
View File
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICsTCCAjagAwIBAgIRAIRTi2u8EKDZNutOTc8H2ZUwCgYIKoZIzj0EAwMwbTEL
MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkxvZ2FsRGV2ZWxvcGVyMREwDwYDVQQLEwhM
b2dhbE5ldDEyMDAGA1UEAxMpTG9nYWxOZXQgSW50ZXJuYWwgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkwHhcNMjYwMzE1MjA0NzMyWhcNNDYwMzEwMjA0NzMyWjBtMQsw
CQYDVQQGEwJVUzEXMBUGA1UEChMOTG9nYWxEZXZlbG9wZXIxETAPBgNVBAsTCExv
Z2FsTmV0MTIwMAYDVQQDEylMb2dhbE5ldCBJbnRlcm5hbCBDZXJ0aWZpY2F0aW9u
IEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABHOYlJioKnPe6eRrnk4X
qEA6eCwVUqU/3A1vJhwZV7rfS+gs97zqIiJyYsiOsXl0EDoXLB0TVOdDm/R2r76j
4fJXtWupGXQczRAxVuAtoZfG5iCTCV5EFlX4sxbo+FZleqOBmTCBljAOBgNVHQ8B
Af8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU5OcAiltvnWIG
B/SNXy9CCLQu78kwUQYDVR0eAQH/BEcwRaARMA+CDW5ldC5sb2dhbC5kZXahMDAK
hwgAAAAAAAAAADAihyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAK
BggqhkjOPQQDAwNpADBmAjEAjd3B7ov6IyXxhgf3/yi+flBaHTBW9nGQvM2r/PfF
yMfzMlfN4+uPQZn4jjCcYb8hAjEAnu1uAT9bGhAtgz8N5iqV6yS4LkGScQBAlDrN
30YeI+zbnwVaiGwOJmvZlG/Lw23T
-----END CERTIFICATE-----