Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
120 changes: 120 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
toolchain: ${{ steps.decide.outputs.toolchain }}
kernel: ${{ steps.decide.outputs.kernel }}
boot: ${{ steps.decide.outputs.boot }}
release: ${{ steps.decide.outputs.release }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -99,6 +100,15 @@ jobs:
- 'tests/qemu/test_executive_integral.sh'
- 'tests/qemu/test_distrib_boot.sh'
- 'tests/uat/**'
release:
- '.github/workflows/**'
- 'tools/cut-release.sh'
- 'distro/Dockerfile.bootable'
- 'tools/ovmx_kit_pack.c'
- 'tools/vmsfs_master.c'
- 'src/libvms/include/ovmx_identity.h'
- 'src/libvms/include/ovmx_kit_format.h'
- 'src/kernel/vmsfs/vmsfs_ondisk.h'

- name: Decide (force every output true on non-PR events)
id: decide
Expand All @@ -109,13 +119,15 @@ jobs:
echo "toolchain=${{ steps.filter.outputs.toolchain }}"
echo "kernel=${{ steps.filter.outputs.kernel }}"
echo "boot=${{ steps.filter.outputs.boot }}"
echo "release=${{ steps.filter.outputs.release }}"
} >> "$GITHUB_OUTPUT"
else
{
echo "core=true"
echo "toolchain=true"
echo "kernel=true"
echo "boot=true"
echo "release=true"
} >> "$GITHUB_OUTPUT"
fi
# -----------------------------------------------------------------------
Expand Down Expand Up @@ -349,6 +361,114 @@ jobs:
ovmx-boot:latest \
/test.sh

# -----------------------------------------------------------------------
# cut-release-reproducible (vms-d73, epic vms-a84 RELEASE ENGINEERING)
#
# tools/cut-release.sh is the FIRST piece of release-engineering machinery:
# a repeatable "cut a release" command replacing the previous hand-rolled
# per-cut process (build the image, copy artifacts out, hand-write
# checksums). This job is that command's ground-source proof:
#
# 1. Run cut-release.sh TWICE, from the SAME commit, each with
# --no-cache so the two builds share nothing (a cache hit would prove
# cache reuse, not reproducibility) — and diff the four release
# artifacts (vmlinuz, initramfs-ovmx-slim.cpio.gz, ovmx-distrib.img,
# ovmx-os.kit) byte-for-byte.
# 2. Recompute SHA256SUMS against the actual bundle bytes and compare
# against what the script wrote, so a bug in the script's own
# checksum step cannot self-certify.
# 3. Reuse the EXISTING boot-smoke harness (tests/qemu/test_distrib_boot.sh
# — the same one persistent-boot above runs against a normal build) to
# boot the freshly-cut bundle's own vmlinuz/initramfs/distrib.img to a
# real SYSTEM login prompt, rather than reinventing that proof.
#
# Two full containerized builds (~25-30 min cold each per boot.sh) make this
# the heaviest job in the workflow; it is gated behind the `release` path
# filter so ordinary PRs that do not touch release-engineering surfaces
# don't pay for it, and still runs in full on push/merge_group/schedule.
# -----------------------------------------------------------------------
cut-release-reproducible:
name: cut-release is byte-reproducible + boots (vms-d73)
needs: changes
if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.release == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 150

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# First independent cut. --no-cache on both cuts is the point: this
# proves the BUILD is reproducible, not that BuildKit's cache is.
- name: Cut release A (clean tree, no cache)
run: |
tools/cut-release.sh --no-cache --out-dir "${{ github.workspace }}/dist/release-a"

- name: Cut release B (independent clean tree, no cache)
run: |
tools/cut-release.sh --no-cache --out-dir "${{ github.workspace }}/dist/release-b"

# The manifest's own "cut_at_utc" field legitimately differs between
# the two invocations (it is wall-clock metadata ABOUT the cut, not a
# build input) -- the reproducibility claim is scoped to the four real
# release artifacts and their checksums, which is exactly what this
# step compares. SHA256SUMS also carries no such field, so it is
# compared byte-for-byte too, not just semantically.
- name: Assert the two cuts are byte-identical
run: |
set -euo pipefail
A="dist/release-a"
B="dist/release-b"
FAIL=0
for f in vmlinuz initramfs-ovmx-slim.cpio.gz ovmx-distrib.img ovmx-os.kit ovmx-os.kit.manifest.txt SHA256SUMS; do
if cmp -s "$A/$f" "$B/$f"; then
echo "OK: $f is byte-identical across both cuts"
else
echo "FAIL: $f DIFFERS between the two independent cuts"
cmp "$A/$f" "$B/$f" || true
FAIL=1
fi
done
[ "$FAIL" -eq 0 ] || { echo "Reproducibility check FAILED"; exit 1; }
echo "All release artifacts + SHA256SUMS are byte-identical across two independent --no-cache cuts."

# Defense in depth against a bug in the script's own checksum step:
# recompute sha256 directly against the shipped bytes and compare
# against what release-manifest.json / SHA256SUMS claim.
- name: Recompute checksums and compare against the emitted manifest
run: |
set -euo pipefail
cd dist/release-a
sha256sum -c SHA256SUMS
for f in vmlinuz initramfs-ovmx-slim.cpio.gz ovmx-distrib.img ovmx-os.kit; do
REAL=$(sha256sum "$f" | awk '{print $1}')
# Each artifact is emitted as a single JSON line (cut-release.sh
# printf's one object per line, no embedded newlines) -- a plain
# grep+sed extraction is exact and needs no JSON library.
CLAIMED=$(grep "\"component\": \"$f\"" release-manifest.json | sed -n 's/.*"sha256": "\([a-f0-9]*\)".*/\1/p')
[ -n "$CLAIMED" ] || { echo "FAIL: $f not found in release-manifest.json"; exit 1; }
[ "$REAL" = "$CLAIMED" ] || { echo "FAIL: $f real=$REAL manifest=$CLAIMED"; exit 1; }
echo "OK: release-manifest.json sha256 for $f matches the actual artifact bytes"
done

# THE BOOT PROOF. Reuses tests/qemu/test_distrib_boot.sh verbatim
# against the CUT bundle's own artifacts (bind-mounted at the fixed
# /boot/ paths the script expects) instead of reinventing it — the same
# QEMU boot-to-login assertions persistent-boot already runs against a
# normal (non-cut) build.
- name: Boot the freshly-cut bundle to a login prompt
run: |
docker run --rm \
-v ${{ github.workspace }}/dist/release-a/vmlinuz:/boot/vmlinuz:ro \
-v ${{ github.workspace }}/dist/release-a/initramfs-ovmx-slim.cpio.gz:/boot/initramfs-ovmx-slim.cpio.gz:ro \
-v ${{ github.workspace }}/dist/release-a/ovmx-distrib.img:/boot/ovmx-distrib.img:ro \
-v ${{ github.workspace }}/tests/qemu/test_distrib_boot.sh:/test.sh:ro \
ubuntu:24.04 \
bash -c "apt-get update -qq && apt-get install -y --no-install-recommends qemu-system-x86 qemu-system-arm qemu-efi-aarch64 cpio >/dev/null 2>&1 && bash /test.sh"

# -----------------------------------------------------------------------
# Job 2b: PARTS 0.2 Demo E2E (vms-1c8, epic vms-2579/vms-5dd)
#
Expand Down
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,19 @@ install(DIRECTORY distro/rootfs/vms/ DESTINATION /vms
DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)

# `make release` — convenience wrapper for tools/cut-release.sh (vms-d73,
# epic vms-a84 RELEASE ENGINEERING). The script is the actual machinery
# (git-archives HEAD to a clean tree, runs the containerized build, emits
# dist/release-<version>/ with SHA256SUMS + release-manifest.json) -- this
# target only makes it reachable as `make release` / `ninja release` from an
# already-configured build dir. Not part of ALL; run it explicitly. For
# options (--ref, --out-dir, --no-cache, ...) call tools/cut-release.sh
# directly -- see its --help.
add_custom_target(release
COMMAND ${CMAKE_SOURCE_DIR}/tools/cut-release.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Cutting an OVMX release bundle via tools/cut-release.sh"
VERBATIM
USES_TERMINAL
)
53 changes: 47 additions & 6 deletions distro/Dockerfile.bootable
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# link-native-cmake-x86_64 job uses; OVMX_LINK_NATIVE auto-detects ON only
# when `$CC -dumpmachine` reports an aarch64/x86_64 *-musl triple, which
# Ubuntu's musl-gcc wrapper does not.
FROM --platform=linux/amd64 alpine:3.20 AS link-native

Check warning on line 45 in distro/Dockerfile.bootable

View workflow job for this annotation

GitHub Actions / PARTS 0.2 Demo E2E (real boot, RMS load, DIRECTORY corroboration)

FROM --platform flag should not use a constant value

FromPlatformFlagConstDisallowed: FROM --platform flag should not use constant value "linux/amd64" More info: https://docs.docker.com/go/dockerfile/rule/from-platform-flag-const-disallowed/

Check warning on line 45 in distro/Dockerfile.bootable

View workflow job for this annotation

GitHub Actions / VMS User Acceptance Test

FROM --platform flag should not use a constant value

FromPlatformFlagConstDisallowed: FROM --platform flag should not use constant value "linux/amd64" More info: https://docs.docker.com/go/dockerfile/rule/from-platform-flag-const-disallowed/

Check warning on line 45 in distro/Dockerfile.bootable

View workflow job for this annotation

GitHub Actions / Persistent Boot Smoke Test

FROM --platform flag should not use a constant value

FromPlatformFlagConstDisallowed: FROM --platform flag should not use constant value "linux/amd64" More info: https://docs.docker.com/go/dockerfile/rule/from-platform-flag-const-disallowed/

RUN apk add --no-cache cmake gcc g++ musl-dev binutils make linux-headers

Expand Down Expand Up @@ -109,6 +109,19 @@

ENV DEBIAN_FRONTEND=noninteractive

# Reproducible-build clock (vms-d73, tools/cut-release.sh). Every wall-clock
# timestamp this stage bakes into a shipped artifact -- the OVMX kit header's
# kh_build_time (tools/ovmx_kit_pack.c) and the mastered VMSFS image's
# fh_created/fh_modified/fh_accessed fields (tools/vmsfs_master.c) -- reads
# this instead of time(NULL) when it is set, and every cpio/initramfs pack
# step below normalizes its input file timestamps to it. Default 0 (the Unix
# epoch) so ordinary builds (boot.sh, other CI jobs) that never pass the
# build-arg still produce a deterministic, harmless value; cut-release.sh
# passes the real HEAD commit time so a release's artifacts carry an honest,
# reproducible date instead of 1970.
ARG SOURCE_DATE_EPOCH=0
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}

RUN apt-get update && apt-get install -y --no-install-recommends \
cmake gcc make musl-tools linux-libc-dev \
linux-headers-generic linux-image-generic \
Expand Down Expand Up @@ -301,8 +314,21 @@
echo "OK: shipped PARTS.EXE is VMS-native (EM_X86_64, zero DT_NEEDED/DT_HASH)" && \
[ -f "/initramfs-fat/vms/SYS0/SYSCOMMON/SYSUPD/PARTS_SETUP.COM" ] || { echo "FAIL: PARTS_SETUP.COM missing from SYS\$UPDATE: (SYSUPD)"; exit 1; } && \
echo "OK: PARTS_SETUP.COM present under SYS\$UPDATE: (SYSUPD)" && \
# Pack the fat initramfs
cd /initramfs-fat && find . | cpio -o -H newc 2>/dev/null | gzip > /boot/initramfs-ovmx.cpio.gz && \
# Pack the fat initramfs. Reproducibility (vms-d73): normalize every file's
# mtime to SOURCE_DATE_EPOCH (cp resets mtime to build wall-clock, which
# the cpio "newc" format embeds per entry); sort the cpio member order --
# find(1)'s raw readdir order is not a byte-reproducibility guarantee even
# when the create sequence is identical; drop gzip's embedded mtime (-n);
# and --reproducible (measured necessary, not theoretical -- vms-d73 CI
# caught a real diff without it), which is GNU cpio's own name for
# --ignore-devno --ignore-dirnlink --renumber-inodes. Without it, cpio's
# "newc" format embeds the REAL filesystem inode number of every entry
# (verified directly: two byte-identical directory trees, created
# independently with identical mtimes, still produced different cpio
# archives purely from inode allocation differing across builds) -- file
# CONTENT and mtimes being reproducible is not sufficient on its own.
find /initramfs-fat -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} + && \
cd /initramfs-fat && find . | sort | cpio -o -H newc --reproducible 2>/dev/null | gzip -n > /boot/initramfs-ovmx.cpio.gz && \
echo "FAT initramfs: $(ls -lh /boot/initramfs-ovmx.cpio.gz | awk '{print $5}')"

# -- OVMX OS kit (vms-0b6) ------------------------------------------------------
Expand Down Expand Up @@ -334,6 +360,12 @@
echo "$LISTING" | grep -q "$name" || { echo "FAIL: ovmx-os.kit manifest missing $name"; exit 1; }; \
echo "OK: ovmx-os.kit manifest names $name"; \
done && \
# Persist the listing as a build artifact (vms-d73, tools/cut-release.sh):
# the release script folds this into release-manifest.json and re-checks
# DCL.EXE/LOGINOUT.EXE/STARTUP.COM against the SHIPPED kit's own listing,
# not just this build-time variable. SOURCE_DATE_EPOCH normalization above
# makes the embedded "Built:" line (kh_build_time) reproducible too.
echo "$LISTING" > /boot/ovmx-os.kit.manifest.txt && \
# Round-trip the kit itself here too (not just in ctest): extract and
# byte-compare every payload file against the staged tree that was
# packed. A flag file (not a loop exit code) carries failure out of the
Expand Down Expand Up @@ -448,8 +480,9 @@
# Minimal config — SYSUAF + manager files (STARTUP.EXE reads these at boot)
cp distro/rootfs/vms/SYS0/SYSCOMMON/SYSEXE/SYSUAF.DAT /initramfs-slim/vms/SYS0/SYSCOMMON/SYSEXE/ && \
cp -r distro/rootfs/vms/SYS0/SYSCOMMON/SYSMGR/* /initramfs-slim/vms/SYS0/SYSCOMMON/SYSMGR/ && \
# Pack the slim initramfs
cd /initramfs-slim && find . | cpio -o -H newc 2>/dev/null | gzip > /boot/initramfs-ovmx-slim.cpio.gz && \
# Pack the slim initramfs (reproducibility normalization, see FAT step above)
find /initramfs-slim -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} + && \
cd /initramfs-slim && find . | sort | cpio -o -H newc --reproducible 2>/dev/null | gzip -n > /boot/initramfs-ovmx-slim.cpio.gz && \
echo "SLIM initramfs: $(ls -lh /boot/initramfs-ovmx-slim.cpio.gz | awk '{print $5}')"

# -- NOEXEC initramfs (negative control) ---------------------------------------
Expand All @@ -464,7 +497,8 @@
# precisely the state OVMX exists to make unreachable (CLAUDE.md Rule 9).
RUN cp -a /initramfs-fat /initramfs-noexec && \
rm -f /initramfs-noexec/lib/modules/vms.ko && \
cd /initramfs-noexec && find . | cpio -o -H newc 2>/dev/null | gzip > /boot/initramfs-ovmx-noexec.cpio.gz && \
find /initramfs-noexec -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} + && \
cd /initramfs-noexec && find . | sort | cpio -o -H newc --reproducible 2>/dev/null | gzip -n > /boot/initramfs-ovmx-noexec.cpio.gz && \
echo "NOEXEC initramfs: $(ls -lh /boot/initramfs-ovmx-noexec.cpio.gz | awk '{print $5}')"

# -- NODEV initramfs (negative control) -----------------------------------------
Expand All @@ -480,7 +514,8 @@
# Test artifact, not a runtime -- same rule as NOEXEC above.
RUN cp -a /initramfs-fat /initramfs-nodev && \
cp src/kernel/vmsfs/vmsfs.ko /initramfs-nodev/lib/modules/vms.ko && \
cd /initramfs-nodev && find . | cpio -o -H newc 2>/dev/null | gzip > /boot/initramfs-ovmx-nodev.cpio.gz && \
find /initramfs-nodev -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} + && \
cd /initramfs-nodev && find . | sort | cpio -o -H newc --reproducible 2>/dev/null | gzip -n > /boot/initramfs-ovmx-nodev.cpio.gz && \
echo "NODEV initramfs: $(ls -lh /boot/initramfs-ovmx-nodev.cpio.gz | awk '{print $5}')"

# Ensure vmlinuz symlink exists
Expand Down Expand Up @@ -510,6 +545,12 @@
# carried here so the build's kit artifact is retrievable like the other
# `-o dist` outputs, not buried inside the builder stage.
COPY --from=builder /boot/ovmx-os.kit /boot/ovmx-os.kit
# The kit's own internal file listing (vms-d73, tools/cut-release.sh) --
# carried out alongside the kit itself so a release cut can fold "the kit's
# internal manifest names DCL.EXE/LOGINOUT.EXE/STARTUP.COM" into
# release-manifest.json from the actual shipped kit, without needing
# ovmx_kit_pack on the release host.
COPY --from=builder /boot/ovmx-os.kit.manifest.txt /boot/ovmx-os.kit.manifest.txt
# Negative controls for test_executive_integral.sh only — never a runtime.
COPY --from=builder /boot/initramfs-ovmx-noexec.cpio.gz /boot/initramfs-ovmx-noexec.cpio.gz
COPY --from=builder /boot/initramfs-ovmx-nodev.cpio.gz /boot/initramfs-ovmx-nodev.cpio.gz
Expand All @@ -528,7 +569,7 @@
# boot.sh mounts a host directory at /data so the container owns the disk
# file and can write freely (avoids rootless Docker permission issues).
# When run standalone (no /data mount), falls back to /tmp/sysdisk.img.
CMD ARCH=$(uname -m) && \

Check warning on line 572 in distro/Dockerfile.bootable

View workflow job for this annotation

GitHub Actions / PARTS 0.2 Demo E2E (real boot, RMS load, DIRECTORY corroboration)

JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals

JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals More info: https://docs.docker.com/go/dockerfile/rule/json-args-recommended/

Check warning on line 572 in distro/Dockerfile.bootable

View workflow job for this annotation

GitHub Actions / VMS User Acceptance Test

JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals

JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals More info: https://docs.docker.com/go/dockerfile/rule/json-args-recommended/

Check warning on line 572 in distro/Dockerfile.bootable

View workflow job for this annotation

GitHub Actions / Persistent Boot Smoke Test

JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals

JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals More info: https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
QEMU=qemu-system-aarch64; \
MACHINE="-machine virt -cpu cortex-a57"; \
Expand Down
Loading
Loading