Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 55 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -360,18 +360,18 @@ jobs:
retention-days: 1

# -------------------------------------------------------------------
# Publish artifacts and compute checksums
# Publish per-arch artifacts and compute checksums
# -------------------------------------------------------------------
publish-artifacts:
publish-per-arch:
if: github.ref == 'refs/heads/main'
runs-on: ${{ matrix.target == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
needs: [build-iso]
strategy:
fail-fast: false
matrix:
target: [amd64, arm64, both]
target: [amd64, arm64]
env:
ARCH: ${{ matrix.target == 'both' && 'amd64' || matrix.target }}
ARCH: ${{ matrix.target }}
TARGET: ${{ matrix.target }}
steps:
- name: Checkout code
Expand All @@ -382,43 +382,87 @@ jobs:
- name: Load shared config
run: cat .github/config.env >> "$GITHUB_ENV"

- name: Download kernel artifacts
uses: actions/download-artifact@v4
with:
name: kernel-${{ matrix.target }}
path: mkosi.output

- name: Download initramfs artifacts
uses: actions/download-artifact@v4
with:
name: initramfs-${{ matrix.target }}
path: out

- name: Download ISO artifact
uses: actions/download-artifact@v4
with:
name: iso-${{ matrix.target }}
path: out

- name: Install Python dependencies
run: pip install -r requirements.txt

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

Comment thread
jacobweinstock marked this conversation as resolved.
- name: Publish artifacts to GHCR
run: ./build.py release publish
Comment thread
jacobweinstock marked this conversation as resolved.

Comment thread
jacobweinstock marked this conversation as resolved.
# -------------------------------------------------------------------
# Publish combined multi-arch image (reuses per-arch registry blobs)
# -------------------------------------------------------------------
publish-combined:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: [publish-per-arch]
env:
ARCH: amd64
TARGET: combined
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Load shared config
run: cat .github/config.env >> "$GITHUB_ENV"

- name: Download kernel artifacts (amd64)
if: matrix.target == 'amd64' || matrix.target == 'both'
uses: actions/download-artifact@v4
with:
name: kernel-amd64
path: mkosi.output

- name: Download initramfs artifacts (amd64)
if: matrix.target == 'amd64' || matrix.target == 'both'
uses: actions/download-artifact@v4
with:
name: initramfs-amd64
path: out

- name: Download ISO artifact (amd64)
if: matrix.target == 'amd64' || matrix.target == 'both'
uses: actions/download-artifact@v4
with:
name: iso-amd64
path: out

- name: Download kernel artifacts (arm64)
if: matrix.target == 'arm64' || matrix.target == 'both'
uses: actions/download-artifact@v4
with:
name: kernel-arm64
path: mkosi.output

- name: Download initramfs artifacts (arm64)
if: matrix.target == 'arm64' || matrix.target == 'both'
uses: actions/download-artifact@v4
with:
name: initramfs-arm64
path: out

- name: Download ISO artifact (arm64)
if: matrix.target == 'arm64' || matrix.target == 'both'
uses: actions/download-artifact@v4
with:
name: iso-arm64
Expand All @@ -434,8 +478,5 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

Comment thread
jacobweinstock marked this conversation as resolved.
- name: Install crane
uses: imjasonh/setup-crane@v0.4

- name: Publish artifacts to GHCR
- name: Publish combined image to GHCR
Comment thread
jacobweinstock marked this conversation as resolved.
run: ./build.py release publish
Comment thread
jacobweinstock marked this conversation as resolved.
Comment thread
jacobweinstock marked this conversation as resolved.
9 changes: 3 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,23 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Install crane
uses: imjasonh/setup-crane@v0.4

- name: Load shared config
run: cat .github/config.env >> "$GITHUB_ENV"

- name: Install Python dependencies
run: pip install -r requirements.txt

- name: Pull release artifacts (both)
- name: Pull release artifacts (combined)
env:
VERSION_EXCLUDE: ${{ github.ref_name }}
run: ./build.py release pull --target both --pull-output artifacts/both
run: ./build.py release pull --target combined --pull-output artifacts/combined

Comment thread
jacobweinstock marked this conversation as resolved.
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ github.ref_name }}" \
artifacts/both/* \
artifacts/combined/* \
--generate-notes \
--title "${{ github.ref_name }}"

Expand Down
38 changes: 16 additions & 22 deletions Dockerfile.release
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
# Lightweight container for OCI release operations (publish, index, pull, tag).
# Contains crane, Python 3, git, and configargparse — nothing else.
# Contains buildah, skopeo, Python 3, git, and configargparse — nothing else.
#
# Usage:
# docker build -f Dockerfile.release -t captainos-release .
# docker run --rm -v $(pwd):/work captainos-release release publish
FROM python:3.12-slim

ARG CRANE_VERSION=v0.21.2

# Install git (needed for version tag computation) and tar (for crane export)
# Install buildah, skopeo, and git
RUN apt-get update && apt-get install -y --no-install-recommends \
buildah \
skopeo \
git \
ca-certificates \
curl \
tar \
&& rm -rf /var/lib/apt/lists/* \
&& git config --global --add safe.directory /work

# Install crane (with checksum verification)
RUN DPKG_ARCH="$(dpkg --print-architecture)" \
&& case "$DPKG_ARCH" in \
amd64) CRANE_ARCH="x86_64" ;; \
arm64) CRANE_ARCH="arm64" ;; \
*) echo "Unsupported arch: $DPKG_ARCH" >&2; exit 1 ;; \
esac \
&& CRANE_TARBALL="go-containerregistry_Linux_${CRANE_ARCH}.tar.gz" \
&& CRANE_BASE_URL="https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}" \
&& curl -fsSL "${CRANE_BASE_URL}/${CRANE_TARBALL}" -o /tmp/${CRANE_TARBALL} \
&& curl -fsSL "${CRANE_BASE_URL}/checksums.txt" -o /tmp/checksums.txt \
&& grep " ${CRANE_TARBALL}$" /tmp/checksums.txt | (cd /tmp && sha256sum -c -) \
&& tar -xz -f /tmp/${CRANE_TARBALL} -C /usr/local/bin crane \
&& chmod +x /usr/local/bin/crane \
&& rm -f /tmp/${CRANE_TARBALL} /tmp/checksums.txt \
&& crane version
# Configure rootless storage driver and chroot isolation (no user-namespace
# required — we only assemble scratch images, never RUN anything inside them).
RUN printf '[storage]\ndriver = "vfs"\nrunroot = "/var/tmp/buildah-runroot"\ngraphroot = "/var/tmp/buildah-storage"\n' \
> /etc/containers/storage.conf
ENV BUILDAH_ISOLATION=chroot

# Buildah 1.39+ on Debian requires netavark but we never need networking
# (all images are FROM scratch with no RUN steps). A no-op stub satisfies
# the startup check.
RUN mkdir -p /usr/libexec/podman \
&& printf '#!/bin/sh\nexit 0\n' > /usr/libexec/podman/netavark \
&& chmod +x /usr/libexec/podman/netavark

# Install Python dependencies
COPY requirements.txt /tmp/requirements.txt
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Each artifact file is pushed as its own OCI layer. Deterministic tar creation (z
All three images are multi-arch OCI indexes with `linux/amd64` and `linux/arm64` platform entries pointing to the same content, so any platform can pull them. Images are compatible with:

- **containerd** — valid `rootfs.diff_ids` in the config; Kubernetes image-volume mounts work
- **crane export** — extracts individual artifact files for release workflows
- **skopeo** — extracts individual artifact files for release workflows

### GitHub Release

Expand All @@ -130,7 +130,7 @@ When a `v*` tag is pushed, the release workflow:
./build.py release publish --target amd64

# Pull and extract artifacts
./build.py release pull --target both --pull-output ./out/release/
./build.py release pull --target combined --pull-output ./out/release/

# Tag all artifact images with a release version
./build.py release tag v1.0.0
Comment thread
jacobweinstock marked this conversation as resolved.
Expand Down Expand Up @@ -184,7 +184,8 @@ Each stage can be executed in one of three modes:
│ ├── tools.py # Binary tool downloader
│ ├── artifacts.py # Artifact collection & checksums
│ ├── oci.py # OCI artifact publish/pull/tag
│ ├── crane.py # crane CLI wrapper
│ ├── buildah.py # buildah CLI wrapper (image construction)
│ ├── skopeo.py # skopeo CLI wrapper (inspect/copy/export)
│ ├── iso.py # ISO image assembly
│ ├── qemu.py # QEMU boot testing
│ ├── log.py # Colored logging
Expand Down
8 changes: 6 additions & 2 deletions captain/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,13 @@ def collect_checksums(
digest = _sha256(path)
lines.append(f"{digest} {path.name}")
if lines:
content = "\n".join(lines) + "\n"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("\n".join(lines) + "\n")
_log.log(f"Wrote checksums to {output}")
if output.is_file() and output.read_text() == content:
_log.log(f"Checksums unchanged: {output}")
else:
output.write_text(content)
_log.log(f"Wrote checksums to {output}")
for line in lines:
_log.log(f" {line}")
else:
Expand Down
Loading