Skip to content

refactor: full rewrite of the template - #383

Open
Siddhj2206 wants to merge 64 commits into
projectbluefin:mainfrom
Siddhj2206:main
Open

Siddhj2206 wants to merge 64 commits into
projectbluefin:mainfrom
Siddhj2206:main

Conversation

@Siddhj2206

Copy link
Copy Markdown
Collaborator

What this is

A full rewrite of the template, developed on my fork and offered back as one
piece. It changes the build substrate, CI and the release flow, the test suite,
the runtime custom layer, the ISO config, and all of the documentation.

It is deliberately one PR. The phases, the release flow, and the docs describe
one design; split apart, the docs would ship describing a tree that does not
exist yet.

Why

Three problems drove it.

  • The build was one script doing everything. build/10-build.sh layered the
    OCI images, installed packages, enabled services, and cleaned up. Activating
    one of the examples meant editing a file that already had five jobs.
  • The docs were spread across four placesREADME.md, AGENTS.md, 13
    prefixed skills under .agents/skills/, and symlinks under docs/skills/
    restating the same facts until they disagreed. AGENTS.md alone was 164 lines
    of instructions, checklist, and policy.
  • Promotion rebuilt. stable ran its own build instead of promoting the
    digest main had already tested.

What changed

Build

build/10-build.sh is gone. The Containerfile names each phase in its own RUN
block, so order comes from the Containerfile and the numbers only communicate
intent:

Phase Does
00-image-info.sh Writes the image identity into os-release and image-info.json.
10-overlay.sh Overlays projectbluefin/common and the Brew integration, copies this template's declarations, and enables the units that consume them. Installs no packages.
20-packages-and-services.sh Installs the default RPM and COPR packages and enables their services.
90-cleanup.sh Finalises sources, prunes artifacts, prepares for bootc container lint.

Packages sit in their own phase, so an overlay edit cannot invalidate the package
layer. clean-stage.sh is folded into 90-cleanup.sh.

The base image is now the single source of truth for Fedora identity: the
Containerfile owns IMAGE_NAME/IMAGE_VENDOR, and just build reads the base
image name and tag from the FROM line. The Fedora major comes from the base
image's own os-release during the build, so a FROM-only bump cannot ship a
wrong fedora-version.

Four examples ship inactive: 30-tailscale, 40-gnome-extensions, 50-nvidia,
60-desktop-swap. build/README.md has the phase table and the activation
shape.

CI and the release flow

execute-release.yml is new. It promotes the exact candidate digest main
already built and signed, and refuses any push to stable that is not a
promotion — so stable never rebuilds and never receives a hand-edit.

The promotion PR is merged by hand and the auto-merge enrollment is gone. The
machinery the manual-merge flow made redundant is deleted:
approve-trusted-promotion-runs.yml, label-enforcement.yml, and the
check-token-health composite action.

Validators now point at a single implementation each. renovate.json follows
whatever base image the Containerfile uses instead of restating it, and automerges
everything below a major.

Tests

The suite is split by what it protects:

  • tests/contract/ — the template's promises (image identity, the rename
    contract, the validators). 34 tests.
  • tests/template/ — the build phases and the Justfile recipes. 175 tests.

just test-contract and just test-template replace the single just test-unit.
The old tests/unit/ files for phases that no longer exist are deleted; the new
phase scripts have executed coverage.

Runtime layer

custom/ gained a config seam (custom/config/environment.d/) and READMEs for
each directory. custom-system.just is reworked around the seams the build
actually creates. The custom-apps.just and custom-system.just recipes are
covered by tests.

ISO

iso/iso.toml no longer hardcodes a bootc switch ref. The installer stays
interactive, and the Justfile reads the image reference from the built image's
image-info.json, so a renamed project does not install upstream's image.

Documentation

README.md drops from 446 to 182 lines and AGENTS.md from 164 to 53. The 13
prefixed skills and the docs/skills/ symlinks are replaced by six skills under
.agents/skills/: overview, onboarding, customize, build, ci,
troubleshooting.

The rule the rewrite follows: one statement per fact. The entry points route, the
skills hold the procedures, and each directory's README owns its local format and
gotchas. The onboarding skill carries both a gh command and a full
GitHub-website walkthrough for every setup step, because not everyone has or
wants the CLI.

CONTEXT.md, docs/, SETUP_CHECKLIST.md, copilot-instructions.md, and
commit-convention.md are deleted.

Validation

  • just test-unit (contract + template): 209 tests, 0 failures.
  • just check, just lint, actionlint, and hadolint clean.
  • Rename drill: grepping the project name reaches only the three guarded sites
    (Containerfile, Justfile, artifacthub-repo.yml), and just test-contract
    fails when they disagree.
  • The main build publishes :stable-testing; the promotion PR promotes the
    digest.

Notes for review

  • The commit history is kept so each piece can be read on its own, but the diff
    is the thing to review. I am happy to squash it if you would rather.
  • README.md still carries the "What Makes this Raptor Different?" placeholder
    section; it is the template's, not mine.

Assisted-by: DeepSeek V4.1 Flash via OpenCode

Siddhj2206 and others added 30 commits September 6, 2026 18:08
Assisted-by: OpenAI o3 via Delta
Assisted-by: OpenAI o3 via Delta
Assisted-by: OpenAI o3 via Delta
Assisted-by: OpenAI o3 via Delta
Assisted-by: OpenAI o3 via Delta
Merge projectbluefin/finpilot main (be4b0eb) into the fork so the
shared Actions pins and validation tooling stay current.

- Bump every projectbluefin/actions pin from 1ca03ea to 2564c7bb,
  which carries the registry-normalization fix the promotion release
  gate needs and unblocks Renovate self-update.
- Keep the fork's Actions foundation per
  docs/actions-baseline-assessment.md: a thin reusable-renovate
  caller instead of upstream's inline steps, and no local
  check-token-health composite.
- Adopt upstream's .shellcheck-scope manifest, Justfile shell-sources
  recipe, and build/validate-flatpaks.sh.
- Drop the upstream-added check-token-health tests and the
  .shellcheck-scope entry and assertions for the composite the fork
  removed.

Assisted-by: Claude via Delta
Make the Containerfile the visible source of truth for image assembly and
separate runtime integration from package installation.

- Add build/10-overlay.sh: overlay common/shared (never bluefin/ or nvidia/)
  and the Brew integration files, copy the template's Brewfiles, Flatpak
  preinstalls and /etc/skel/.config seeds, merge custom ujust recipes
  deterministically, and enable the Brew/Flatpak/podman units.
- Add build/20-packages-and-services.sh, which owns RPM/COPR installation and
  for now delegates to build/10-build.sh for the current default set.
- Rename the cleanup phase to build/90-cleanup.sh.
- Move OCI and ArtifactHub metadata into the Containerfile; the Justfile now
  passes only dynamic values (IMAGE_CREATED plus explicit overrides).
- Rewrite build/00-image-info.sh to replace the base os-release identity
  instead of appending, keeping the generic ublue image-info.json schema.
- Bump the common, brew and silverblue digests to current.

Assisted-by: Claude via Delta
Give 20-packages-and-services.sh its real implementation and retire the
transitional provider.

- Install just, gum, fzf and jq, so ujust and its interactive prompts work on
  a fresh image.
- Install uupd from the ublue-os/packages COPR and enable uupd.timer and
  uupd-resume.timer, so the update policy Common already ships takes effect.
- Remove build/10-build.sh; package installation now lives in 20.
- Enable the shared first-boot setup framework (ublue-system-setup and
  ublue-user-setup) in 10-overlay.sh.

Assisted-by: Claude via Delta
Make 90-cleanup.sh the real final phase and fix how Flathub is provided.

- Replace the clean-stage.sh wrapper with a real 90-cleanup.sh: disable and
  validate third-party repositories, keep the Fedora Flatpak remote off the
  image, disable the base automatic-update timer so uupd owns updates, and
  prune build artifacts mount-aware.
- Add the custom/files/ system-file seam (mirrors / when overlaid) and ship
  flatpak-add-flathub-repos.service, a first-boot unit that adds the Flathub
  remote before flatpak-preinstall.service runs.
- Drop build/clean-stage.sh.

Build-time Flatpak remote state cannot work here: the /var prune removes it
and bootc container lint --fatal-warnings rejects it, and flatpak does not
read /etc/flatpak/remotes.d on its own.

Assisted-by: Claude via Delta
Bump the moving pins to their current upstream heads ahead of CI testing:
silverblue:44 (ece43ccc), projectbluefin/actions v1 (b0fbcf6c), and
Homebrew/actions main (304fc86d).

Assisted-by: Claude via Delta
…er digest to 31849a8 (#21)

Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
…t to 2268101 (#19)

Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
Replace the stale examples with four that match the rewritten phase scripts
and current upstream practice.

- 30-tailscale: the third-party RPM repository pattern (add, install, remove),
  replacing the Chrome/1Password example.
- 40-gnome-extensions: install from extensions.gnome.org and from GitHub,
  compile the schemas they ship, enable them through the org.gnome.shell
  schema default.
- 50-nvidia: follow Bluefin's NVIDIA setup step for step.
- 60-desktop-swap: replace GNOME with COSMIC from the official Fedora repos,
  removing the GNOME session definitions too so the login screen stops offering
  sessions that can no longer start.

Assisted-by: Claude via Delta
The factory reusable enrolls the verified promotion PR with `gh pr merge
--auto` and no merge method. GitHub CLI rejects that outside a merge queue,
and a merge queue needs an organization-owned repository, so the step failed
on every run and the promotion PR was left to be merged by hand.

Set enqueue_promotion: false so the run ends green and a maintainer merges the
PR explicitly. The stable ruleset now allows squash merges only, so a manual
merge still produces the `chore: promote main to stable` commit that
execute-release.yml requires to trigger the release.

This is a stopgap: the promotion and release workflow is expected to be
rethought as part of #14.

Assisted-by: Claude via Delta
Make custom/ add to the inherited common/shared layer instead of duplicating or
shadowing it, and fix the seams that could not work as shipped.

- brew: drop fonts.Brewfile so the curated font set the shared layer ships
  survives the override, and install-fonts bundles it for real.
- flatpaks: replace the comment-only preinstall file with three real entries.
  The old file could not be parsed at all: ';' is not a GKeyFile comment and
  flatpak discards the whole file, logging the failure only at g_info. The
  validator now rejects malformed syntax and group names so it cannot return.
- ujust: reduce custom-apps to the two Brewfile shortcuts, and custom-system to
  configure-dev-groups plus a new install-config. Delete the recipes that
  duplicated common (benchmark, clean-containers, update-and-reboot) and the
  placeholder, dropping the last use of the removed /usr/lib/ujust/ujust.sh
  helper whose Choose() left toggle-example-feature doing nothing.
- configure-dev-groups: create the groups with groupadd --system instead of
  copying a /usr/lib/group entry Fedora does not ship, check membership first,
  and escalate only the mutating commands.
- config: add the custom/config seam (README and an inert environment.d
  example) and state the overlay order as the contract in 10-overlay.sh.
- files: custom/files is an override seam that carries payloads, so exclude its
  own README, which was landing at /README.md in the image.
- flathub: flatpak imports remotes from /etc/flatpak/remotes.d on first use, so
  ship the descriptor and delete our flatpak-add-flathub-repos.service, its
  marker file and its enable. Matches projectbluefin/utah.

custom/files/ ships no payload by design: it is a seam, not a feature.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
…icit

The reusable's non-queue enrollment path runs `gh pr merge --auto` with no
merge method, which GitHub CLI refuses without a TTY, and merge queues are
unavailable to personal-account repos. A local job now arms auto-merge with
`--squash` after the whole reusable call succeeds, so the release gate still
runs first. The `stable` ruleset keeps requiring one approval -- that is the
human step -- and auto-merge supplies the merge click after it.

execute-release.yml accepts the subject a promotion actually produces. A
squash merge takes its subject from `squash_merge_commit_title`
(COMMIT_OR_PR_TITLE here), and the promotion branch is a single commit, so
that subject is `chore: promote main to stable`. The rendered PR title is
accepted too, because it becomes the subject if the branch ever carries more
than one commit. PR #16 was merge-committed and matched neither.

A push to stable that is not a promotion now fails instead of reporting
success without promoting anything: stable receives no builds, so an image can
only reach :stable through the candidate digest this workflow promotes.

The release gate runs no e2e suites, by choice for a personal image.
The Justfile has always passed --build-arg SHA_HEAD_SHORT, but the Containerfile
never declared it, so podman dropped the value and 00-image-info.sh's BUILD_ID
branch could never fire. The commit now ships as
org.opencontainers.image.revision.

The ARG sits in the late metadata block with the other volatile values, because
declaring it before 00-image-info.sh would invalidate the package and overlay
layers on every commit. os-release therefore still carries no build commit, and
the unreachable BUILD_ID branch is removed rather than left as a no-op.

Tests assert the new contract, the label, and that the ARG stays behind the
metadata marker.
Four suites were failing because the phases they test were split and renamed,
not because the scripts were wrong. The suite was 155 tests with 26 failing;
it is now 172 with none.

- 10-build_test.bats tested build/10-build.sh, which no longer exists. Its
  assertions (package install, the no-overlay boundary, helper sourcing,
  fail-fast, nullglob) move to the package suite, so nothing is lost.
- 20-packages-and-services_test.bats asserted delegation to 10-build.sh and
  fabricated one to test against. The phase installs directly now: the tests
  cover the default package set in one call, the isolated COPR sequence, both
  timer enables, the overlay boundary, helper availability and fail-fast.
- clean-stage_test.bats tested build/clean-stage.sh, also gone, leaving
  90-cleanup.sh with no tests. Its replacement carries over every behaviour
  assertion (corrected: three systemctl calls, not two) and adds the
  third-party repository pass: each repo shape, Fedora's repo left alone, an
  already-disabled repo staying disabled, and the fail-loud path when a repo
  cannot be rewritten.
- 10-overlay_test.bats expected two rsync calls when the ladder makes four,
  an /etc/skel seed without its .config, and seven enables when nine run. The
  skel test also asserted filesystem effects a stubbed rsync cannot produce,
  so it now asserts the log. Adds the descriptor group, a test for the Flathub
  descriptor's URL and destination, and the seams' own-README exclusion.
- justfile-clean_test.bats no longer names a deleted script in its fixture.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The promotion trigger had no test, and its failure mode is silence: a subject
that stops matching promotes nothing while reporting success, which is how
PR #16 went unreleased.

The new suite reads the pattern out of execute-release.yml rather than
duplicating it, then asserts it accepts the subject a squash promotion
actually produces and the rendered PR title, and rejects the shapes a
promotion never produces, including PR #16's merge-commit subject. Setup
fails loudly if the workflow line is reformatted, so an empty pattern can
never match everything.

unit-tests.yml now runs when that workflow changes; without it, editing the
trigger would not run the test that guards it.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The Justfile resolves REPO_ORG from GITHUB_REPOSITORY_OWNER, which GitHub
Actions sets to the real repository owner, so three tests asserting the
template's default vendor (projectbluefin) failed only in CI — a developer
shell leaves the variable unset. They were already failing in the 05:25 run,
masked by the 26 stale suites repaired in eb9d7b4.

Setup unsets the variable, so those assertions test the documented default,
and a new test covers the owner-derived behaviour deliberately, since that
fallback is what lets a fork build without editing anything.

The GITHUB_TOKEN test gets the same treatment: it asserts the unset case but
inherited whatever the shell had exported.

Verified with GITHUB_REPOSITORY_OWNER, CI, GITHUB_ACTIONS and GITHUB_TOKEN all
set: 173 tests, 0 failures.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
GitHub holds every check run it creates for a bot-authored pull request in
action_required until someone approves it, and the scheduled approver is
throttled to hours here, so the promotion PR sat unvalidated for most of a
morning. The job that arms auto-merge now also approves those runs. They are
scoped to the bot-owned promotion branch and the bot actor, the same trust
boundary the scheduled workflow documents, which stays as the backstop. One
job instead of two, reusing the PR lookup it already does.

The job comment also records why a release does not follow the merge by
itself: the merge is performed as github-actions, and a push made with that
token creates no workflow runs, so execute-release.yml's push trigger never
fires for it. That is a property of auto-merge on a personal repository, not
of the trigger configuration — the identical trigger fires when a human
merges the promotion PR.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Auto-merge as the bot cannot trigger anything downstream: the merge is
performed as github-actions, and a push made with that token creates no
workflow runs, so execute-release.yml and sync-stable-to-main.yml never fired
and every release needed a manual dispatch. A merge performed by a person
fires both.

The job keeps the half that is still needed — approving the check runs GitHub
holds for a bot-authored pull request — and loses the arming, the
release/ready label gate it existed for, and its contents and pull-requests
write. The ruleset and the branch protection on stable now require zero
approvals while staying squash-only, so a single Squash and merge click is the
whole human step. A hand merge cannot produce any other commit shape, and a
squash of the single-commit promotion branch matches execute-release's trigger.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
validate-flatpaks.yml reimplemented the validation contract inline — its own
script's header calls the workflow a thin caller — and missed the GKeyFile
syntax rules, so a malformed preinstall file passed CI while failing
`just validate-flatpaks`. It now calls build/validate-flatpaks.sh, and its
paths include the script and its test so a change to the contract runs the
check that enforces it.

validate-justfiles.yml duplicated the Justfile's `check` recipe with an inline
just --unstable --fmt --check. It now runs `just check`, which covers the root
Justfile and every .just file from one definition.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Merging the promotion PR by hand leaves several things without a job:

- approve-trusted-promotion-runs.yml cleared held check runs on a schedule the
  promotion workflow now handles on every run, at roughly 96 jobs a day and
  one more scheduled workflow that dies after 60 days of inactivity. The
  promotion job comment no longer points at it.
- merge_group triggers in pr-validation.yml and unit-tests.yml can never fire:
  merge queues require an organization-owned repository.
- build-image.yml's PR scaffolding is dead while PR builds are disabled:
  detect-changes, the eight always-true pull_request guards, the
  REGISTRY_CACHE_WRITE expression, pr-number, and the commented-out
  pull_request trigger. The paths-ignore policy is unchanged, so a change to a
  workflow the build depends on still builds, while the four validate
  workflows stay ignored.
- sync-stable-to-main.yml had no manual trigger despite normally no-oping.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Four places stated one fact twice or three times, and .shellcheck-scope
existed to declare a rule git already knows.

- shell-sources stops parsing the manifest: the repository's shell scripts
  are the *.sh files git tracks. CI resolves the same command into
  validate-pr's shellcheck-glob, so the manifest and its drift tests retire
  with it.
- check and fix share _format-justfiles, so the justfile file set has one
  definition instead of a find loop each.
- format uses that same source set rather than walking every *.sh under the
  tree, which also stops it descending into nested checkouts.
- build resolves IMAGE_VENDOR/REPO_ORG once instead of three times.
- test-unit owns the suite invocation and its flags; CI calls it.
- clean drops previous.manifest.json, output.env and changelog.md, which
  nothing in the repo produces.
- build joins tag-images under the Image group, lint and format under Just.

Also: spawn-vm builds build-<type> instead of passing the rebuild flag where
target_image goes, the VM header comments say what the code does, and
.hadolint.yaml moves to .github/ because the workflow's hadolint-config
input is its only consumer.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
iso/iso.toml is rename location 7, and it still switched to
ghcr.io/projectbluefin/finpilot:stable, so an ISO built from this fork would
install the custom image and then immediately switch the machine to
upstream's.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The major-crossing guard matched quay.io/fedora-ostree-desktops/silverblue by
name, so swapping the base would silently drop it. Match the dockerfile
manager instead, and let a major crossing arrive as a PR a human merges rather
than suppressing it entirely.

The first-party image rule gains quay.io/hummingbird-community/* and loses its
schedule override: schedule only filters when Renovate may open a branch, and
"at any time" is already the default, so the line looked like a cadence without
being one. The cadence comes from renovate.yml's six-hourly cron.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Bootc Image Builder records the post-install `bootc switch` origin from the
image reference it is given, and the hand-written kickstart in iso/iso.toml
existed only to override the local build tag with the published GHCR ref.

Hand the builder the published ref instead: read image-info.json out of the
image, tag the local build under that name, and let BIB's own switch record
the origin. The ISO still deploys from local storage; only the recorded
origin changes.

This drops the last hardcoded owner from the ISO path, so a fork no longer
edits iso/iso.toml to rename, and it moves the builder pin off the frozen
quay.io/centos-bootc/bootc-image-builder container onto the current
ghcr.io/osbuild/bootc-image-builder.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
… digest to 536391d (#26)

Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
…hed ref

iso/iso.toml is now the source of truth for what the installer does, and
two things about it were wrong.

The kickstart table is load-bearing. bootc-image-builder only injects its
automatic partitioning -- clearpart --all, autopart --nohome, reboot --eject
-- on the branch where no kickstart content is supplied. Removing the table
therefore turns the ISO into an installer that erases the first disk without
asking. It stays, with command-free content, and the file now records why it
must not be deleted.

Bootc Image Builder records the installed system's bootc origin from the
image reference it is given, so the ISO is built against the reference in the
image's own image-info.json instead of the local build tag. That replaces the
hand-written %post switch and the hardcoded owner it carried, so a fork no
longer renames anything in iso/.

The module list drops the entries bootc-image-builder already enables for
every Anaconda ISO; the effective module set is unchanged, and the remaining
entries are the reason the partitioning and user screens exist.

iso/disk.toml keeps its size but documents the btrfs mountpoint rules.

The builder pin moves off the frozen pre-merge quay.io/centos-bootc container
onto the current ghcr.io/osbuild/bootc-image-builder.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Siddhj2206 and others added 16 commits September 18, 2026 11:36
The six co-located READMEs now state only what is local to their directory:
the format, how the build wires it, and the gotchas. Each defers the "where
does this belong" decision to the customize skill, so that decision has one
home.

Every stale reference is gone: build/10-build.sh and build/clean-stage.sh
never existed, the example catalogue was wrong in all four places that listed
it, and custom/ujust/README.md documented a recipe the image does not ship.

custom/flatpaks/README.md now states the real first-boot behaviour: the
preinstall service exits successfully when it cannot reach Flathub, so it does
not retry that boot.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Each skill now owns one procedure, with no format restated from a directory
README:

- overview: what finpilot is, the multi-stage assembly, the file map, and the
  one routing table.
- onboarding: every repository setting, each with a gh command and a
  GitHub-website route, ending in a verification audit.
- customize: where a package, app, or command belongs.
- build: the Containerfile, Justfile, pinning, and the example scripts.
- ci: the workflows, the release model, signing, and Renovate.
- troubleshooting: symptoms and the pre-commit checklist.

Every stale fact is gone: build/10-build.sh, the wrong example catalogue, the
FEDORA_MAJOR_VERSION ARG that does not exist, and the removed token-health and
label-enforcement machinery.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Comments keep explaining why, and lose the parts that cannot stay true:

- bare issue numbers, which point at a tracker a fork does not have
- an observed run id, which means nothing outside that run
- the "7 rename locations" framing in the Containerfile, replaced by the three
  real sites and the contract test that guards them
- "shared with Aurora" for projectbluefin/common
- tests/unit, replaced by tests/template

The default Brewfile header and the NVIDIA example no longer name the project,
which leaves the identity sites as the only place a rename has to reach.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Delete the material that existed to plan the rewrite rather than to ship the
template: CONTEXT.md, docs/ (the four assessments, the architecture map, the
actions catalog, and the Matt Pocock agent config), the setup checklist, the
copilot instructions, and the commit convention. Their durable content now
lives in the skills that own it.

The one surviving reference — 10-overlay.sh pointing at the common-overlay
assessment for why common/bluefin is imported but never overlaid — states that
reason in place instead.

No dead relative links remain, the suite is green, and actionlint, shellcheck,
yaml, and the Justfile checks all pass.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The skill named the template in its description and its opening line, which
made it another site a fork would have to grep for after a rename. It now
describes the repository it lives in.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
A first-time reader took the docs at face value and reported where they lie:

- README, overview, and build all called the FROM line "the single source for
  the Fedora major". It is not one read: `just build` parses the tag for the
  version string, and 00-image-info.sh reads the base's os-release for the
  metadata. All three now say what actually happens.
- AGENTS.md said pushes to `stable` publish `:stable`. `stable` never rebuilds;
  execute-release promotes the digest `main` already built.
- 60-desktop-swap.sh.example told you to set BASE_IMAGE_NAME in the identity
  block, which no longer exists.
- identity_test.bats described custom/ujust/README.md as restating the name,
  which it no longer does.
- The onboarding audit now says a documentation-only push builds nothing.
- build/README.md's RUN block used a real script name where a placeholder was
  meant, which read as if it were NVIDIA-specific.

The stale rewrite handoff moved out of the tree; it advertised files and skills
that no longer exist.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The cold-start reader flagged three facts stated in more than one place. Each
now lives where its reader is, and the skill points at it:

- the cosign verify command: README, not the ci skill
- the release-gate limitation: README, not the ci skill. The promote workflow's
  comment keeps the local reason run_e2e is false and drops the restatement.
- the first-boot failure modes: README, not the troubleshooting skill, which
  keeps the repo-side runtime symptom

The ci skill also repeated the "stable publishes :stable" error that AGENTS.md
had.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The by-hand routes were one-liners that assumed you already knew where GitHub
hides each setting. They are now walkthroughs: the menu path, the exact control,
what to type, and the trap.

- Steps 2-4 name the repository Settings page and the control on it.
- Step 5 walks the classic PAT from the profile menu to the copied value, and
  says why `repo` and `workflow` are both needed.
- Steps 7-9 say where the `validate` check comes from and why zero approvals on
  `stable` is deliberate.
- Step 10 admits that creating twelve labels by hand is tedious and says so.
- The failure modes now cover a check that has never run.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
…er digest to fac5b1d (#32)

Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
…t to b7e3487 (#35)

Co-authored-by: Renovate <renovate@whitesourcesoftware.com>
The ubuntu v26 bump made every build fail: setup-runner's "Add Ubuntu
resolute apt source" step runs `test "${IDV}" = "ubuntu-24.04"`, which
exits 1 on a 26.04 runner before any build step runs.

Revert runs-on to ubuntu-24.04 and hold Renovate below 26.04 so the
bump cannot immediately return. Drop the hold when upstream supports
26.04.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The promotion test used this fork's owner in its merge-commit example, and
execute-release.yml's comment pointed at a fork PR number. Neither means
anything once this tree is the template again.

Assisted-by: DeepSeek V4.1 Flash via OpenCode

@kubestellar-hive kubestellar-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at head bcee4638 (correctness perspective). The promotion chain holds together: main builds retain :testing (build-image.yml "Finalize branch tags"), execute-release.yml re-verifies the candidate digest's cosign signature and copies it with --preserve-digests, and the caller job's permissions match what the pinned reusable needs. Three findings, worst first:

  1. Fork residue in .github/renovate.json:41 — the "Keep first-party and base OCI images fresh immediately" rule matches quay.io/hummingbird-community/*, which is the author's personal fork's namespace, not anything this template uses. This repo's actual base (quay.io/fedora-ostree-desktops/silverblue, Containerfile:52) is not in the list. Digest bumps still automerge via the general rule at line 31, so the practical effect is small, but the shipped template config references a registry namespace that has nothing to do with it. Should be the base image ref or dropped.

  2. just build now hard-fails on the CentOS base the Containerfile still advertises — Justfile:149–150 derives the Fedora major from the base FROM line and requires a purely numeric tag; quay.io/centos-bootc/centos-bootc:stream10, still listed as a supported base option at Containerfile:30, doesn't match either sed pattern, so the recipe exits with "Could not read the base image". Before this PR, FEDORA_MAJOR_VERSION was an explicit ARG and that base built. Either handle non-numeric tags or drop the CentOS option from the comment.

  3. Low — set_os_release_value append path writes sed-escape artifacts (build/00-image-info.sh:61–69): the value is escaped for the sed replacement (&\&, |\|, \\\) before the branch, so when a key is absent and appended via printf, the literal backslashes land in /usr/lib/os-release. Only reachable with override values containing those characters (e.g. a HOME_URL with a query string), since all touched keys exist in Fedora's os-release; move the escaping inside the sed branch.

Not verified: behavior of the pinned projectbluefin/actions composite actions beyond reusable-execute-release.yml, and the squash-title dependency in execute-release's trigger check (documented and tested, but contingent on the repo's squash_merge_commit_title setting staying COMMIT_OR_PR_TITLE).

— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.78

The "keep first-party and base OCI images fresh immediately" rule matched
`quay.io/hummingbird-community/*` and nothing else. Hummingbird is one of the
three bases the template supports, but the rule never named the active base, so
the namespace it exists for was missing and the one it carried read as residue.

List the three base namespaces the Containerfile documents — Fedora, CentOS,
Hummingbird — alongside the context images, and add the Hummingbird option to
the Containerfile's base list so the namespace has a home.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
`just build` derived the version string's major from the base FROM line with a
sed that only matched `:([0-9]+)`. The Containerfile documents
`quay.io/centos-bootc/centos-bootc:stream10` as a base, so switching to it made
the recipe exit with "Could not read the base image" — a regression from
dropping the explicit FEDORA_MAJOR_VERSION ARG.

Take the tag verbatim instead and use it in the version string. The no-tag case
still aborts, which is now the only thing the guard is for.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
`set_os_release_value` escaped the value for the sed replacement before choosing
a branch, so when a key was absent the append path wrote the backslashes
literally. Move the escaping into the sed branch and append the value as it is.

Only reachable for a key the base os-release lacks, with a value containing `&`,
`|` or `\` — but the append path should not depend on that.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
`ubuntu-latest` currently resolves to 24.04, but the label moves underneath the
job and `build-image.yml` and `validate-renovate.yml` are already pinned. Pin
this one too, so the whole surface follows the same rule the Renovate config
holds.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
The runner image bakes its apt lists at image build time. `bubblewrap` has
rotated off the mirror since, so the baked list named `0.9.0-1ubuntu0.1` and
the install died on a 404 — three attempts, three different mirror IPs — before
the validator ever ran.

Refresh the lists first, which is what the error message asks for. The mirrors
still carry the package, just not that version.

Assisted-by: DeepSeek V4.1 Flash via OpenCode
@Siddhj2206

Copy link
Copy Markdown
Collaborator Author

All three findings are fixed at f6707c1.

  1. renovate.json:41 — the rule now names the three base namespaces the Containerfile documents (fedora-ostree-desktops, centos-bootc, hummingbird-community) rather than Hummingbird alone, and the Containerfile's base list gained the Hummingbird option so that namespace is not unexplained. The automerge policy is deliberately unchanged: everything below a major merges on green checks.
  2. Non-numeric base tagjust build takes the tag verbatim, so centos-bootc:stream10 builds and stamps VERSION=stream10.<date>. The no-tag case still aborts, which is all the guard does now.
  3. set_os_release_value append path — the sed escapes moved inside the replacement branch, so an appended value lands verbatim.

Findings 2 and 3 each have a test that fails without its fix.

Separately, validate-flatpaks.yml now refreshes the apt lists before installing flatpak. The runner image bakes them at image build time, and the baked list named a bubblewrap version the mirror has rotated away, so the install 404'd before the validator ever ran — on ubuntu-latest and on ubuntu-24.04 alike, since they resolve to the same image.

@Siddhj2206 Siddhj2206 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrite was done to both simplify unnecessary bulk as well as add useful features and fixes. Moves more stuff upstream such as renovate, removing the need for extra bash scripts like check-token-health.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The promotion workflow disables the reusable pre-merge gate and resolves a mutable candidate tag, so stable may publish a different image than the one reviewed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

This PR rewrites the template’s build phases, runtime customization, CI release flow, tests, and documentation.

Changes:

  • Splits image assembly into explicit overlay, package, metadata, and cleanup phases.
  • Promotes signed candidate digests from main to stable without rebuilding.
  • Reorganizes runtime seams, validation tests, ISO configuration, and agent documentation.
File Description
Containerfile Defines the rewritten build stages.
Justfile Updates build, test, and image recipes.
README.md Documents setup, customization, and releases.
.github/​workflows/​promote-main-to-stable.yml Configures promotion workflow.
.github/​workflows/​execute-release.yml Promotes candidate images and creates releases.
.github/​renovate.json Changes dependency automation policy.
build/​00-image-info.sh Writes image identity metadata.
build/​10-overlay.sh Applies runtime overlays and declarations.
build/​20-packages-and-services.sh Installs default packages and services.
build/​90-cleanup.sh Finalizes repositories and cleans artifacts.
iso/​iso.toml Simplifies interactive ISO installation.
iso/​disk.toml Documents disk image partitioning.
custom/​ujust/​custom-system.just Adds group and configuration commands.
custom/​ujust/​custom-apps.just Defines Homebrew shortcuts.
custom/​flatpaks/​default.preinstall Adds default Flatpak applications.
custom/​flatpaks/​README.md Documents Flatpak declarations.
custom/​brew/​default.Brewfile Updates default Brewfile.
custom/​brew/​README.md Documents Homebrew integration.
custom/​config/​README.md Documents per-user configuration.
custom/​config/​environment.d/​10-example.conf Adds an inert environment example.
custom/​files/​README.md Documents system file overlays.
build/​validate-flatpaks.sh Adds Flatpak syntax validation.
build/​README.md Documents build phases and examples.
build/​30-tailscale.sh.example Adds a Tailscale example.
build/​40-gnome-extensions.sh.example Adds GNOME extension setup.
build/​50-nvidia.sh.example Adds NVIDIA setup guidance.
build/​60-desktop-swap.sh.example Adds COSMIC desktop replacement.
tests/​contract/​identity_test.bats Tests identity consistency.
tests/​contract/​validate-flatpaks_test.bats Tests Flatpak validation.
tests/​template/​20-packages-and-services_test.bats Tests package phase behavior.
tests/​template/​execute-release_test.bats Tests release trigger matching.
tests/​template/​custom-apps-just_test.bats Tests application recipes.
tests/​template/​90-cleanup_test.bats Tests cleanup behavior.
.github/​workflows/​pr-validation.yml Expands shellcheck validation scope.
.github/​workflows/​unit-tests.yml Runs the reorganized test suite.
.github/​workflows/​validate-flatpaks.yml Uses centralized Flatpak validation.
.github/​workflows/​validate-brewfiles.yml Updates Brewfile test paths.
.github/​workflows/​validate-justfiles.yml Uses centralized Just validation.
.github/​workflows/​validate-renovate.yml Switches to reusable validation.
.github/​workflows/​renovate.yml Switches to reusable Renovate execution.
.github/​workflows/​sync-stable-to-main.yml Updates branch synchronization.
.github/​workflows/​clean.yml Updates image cleanup action pin.
.github/​hadolint.yaml Adds Containerfile lint configuration.
.pre-commit-config.yaml Updates validation test references.
AGENTS.md Replaces repository guidance with concise gates.
.agents/​skills/​README.md Simplifies the skill index.
.agents/​skills/​overview/​SKILL.md Adds repository architecture guidance.
.agents/​skills/​build/​SKILL.md Adds build procedures.
.agents/​skills/​ci/​SKILL.md Adds CI and release procedures.
.agents/​skills/​customize/​SKILL.md Adds customization routing.
.agents/​skills/​troubleshooting/​SKILL.md Adds troubleshooting guidance.
tests/​unit/​shellcheck-scope_test.bats Removes obsolete shellcheck-scope tests.
tests/​unit/​clean-stage_test.bats Removes obsolete cleanup tests.
build/​10-build.sh Removes monolithic build script.
build/​clean-stage.sh Removes superseded cleanup script.
build/​20-onepassword.sh.example Removes obsolete example.
build/​30-cosmic-desktop.sh.example Removes obsolete example.
build/​40-nvidia.sh.example Replaces the NVIDIA example.
custom/​brew/​fonts.Brewfile Removes the empty font declaration.
.shellcheck-scope Removes obsolete scope manifest.
.github/​workflows/​label-enforcement.yml Removes local label enforcement.
.github/​workflows/​approve-trusted-promotion-runs.yml Removes superseded approval workflow.
.github/​actions/​check-token-health/​action.yml Removes token health action.
.github/​actions/​check-token-health/​check_token_health.sh Removes token health implementation.
.github/​SETUP_CHECKLIST.md Replaces setup checklist with skill guidance.
.github/​copilot-instructions.md Removes superseded instructions.
.github/​commit-convention.md Removes superseded commit documentation.
.agents/​skills/​finpilot-templates/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-router/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-pr-checklist/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-packages/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-overview/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-onboarding/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-maintain/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-examples/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-custom/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-ci/​SKILL.md Removes superseded skill.
.agents/​skills/​finpilot-build/​SKILL.md Removes superseded skill.
.agents/​skills/​skill-improvement/​SKILL.md Removes superseded skill.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

# fire for it and the release becomes a manual dispatch. A merge performed
# by a person fires both, which is why this PR is merged in the UI rather
# than by a bot.
enqueue_promotion: false

@kubestellar-hive kubestellar-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at head f6707c1 — correctness perspective.

1. (medium) just rebuild-iso bakes a broken bootc origin containing localhost/.
rebuild-iso defaults $target_image to localhost/finpilot (Justfile:368) and _rebuild-bib passes that straight into build, which sets --build-arg IMAGE_NAME=${target_image} (Justfile:196). 00-image-info.sh then derives image-ref = ostree-image-signed:docker://ghcr.io/${IMAGE_VENDOR}/${IMAGE_NAME} (build/00-image-info.sh:43), i.e. ghcr.io/<vendor>/localhost/finpilot, and also writes NAME/VARIANT_ID/PRETTY_NAME as localhost/finpilot (build/00-image-info.sh:98-100). This identity pollution is pre-existing, but this PR makes it consequential: the new ISO path reads image-ref from image-info.json and hands it to BIB as the install/switch target (Justfile:316-317), so an ISO produced by just rebuild-iso installs a system whose bootc origin points at a registry path that cannot exist. The PR body sells this exact seam as the fix for "a renamed project does not install upstream's image". Fix: strip a localhost/ prefix before deriving IMAGE_NAME in the build recipe (or when composing image-ref).

2. (low) The promoted digest is not necessarily the gated digest.
execute-release.yml promotes whatever :testing resolves to at merge time (execute-release.yml:79) with run_release_gate: false (execute-release.yml:87); the reusable resolves the tag at run time. The promotion PR is merged by hand, possibly days after promote-main-to-stable.yml gated a candidate, and any main push in between moves :testing. Result: the comment's claim that "promote-main-to-stable.yml already gated the same digest" can be false, and the stable branch tree can diverge from the image published as :stable. Cosign re-verification still holds, so this is an edge case rather than a hole, but it contradicts the stated invariant; consider pinning the candidate digest in the promotion PR and passing it through.

3. (medium) Renovate now automerges GitHub Actions bumps into privileged workflows.
The previous config's last rule explicitly refused automerge for matchManagers: ["github-actions"] because those SHAs run with packages: write, id-token: write, secrets: inherit. The rewrite replaces it with automerge of everything below a major (.github/renovate.json:30-36), and PR builds are disabled, so third-party action bumps (e.g. nick-fields/retry) merge with only shellcheck/hadolint having run. The trade is disclosed in the rule description, but it reverses a documented supply-chain guard — worth an explicit maintainer decision rather than riding in with the refactor.

Not fully verified: the contents of projectbluefin/common:shared (whether every unit enabled in build/10-overlay.sh:118-131 exists there) and bootc-image-builder's kickstart-injection behavior assumed by iso/iso.toml — both taken on the author's research.

— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.78

…commit

The promotion PR lost its pre-merge release gate. `enqueue_promotion: false`
disables the factory reusable's `gate` job as well as its enrollment job — one
input, both jobs — and it was set false because a personal repository cannot
enroll for merge. Meanwhile `execute-release.yml` resolved the mutable `:testing`
tag at merge time with no guard, so a promotion that had fallen behind `main`
would publish a newer image than the PR was reviewed against.

- `promote-main-to-stable.yml` runs the gate itself, as a `gate` job calling
  `reusable-release-gate.yml`, so the promotion PR is verified and labelled
  before a human merges it. A new `locate-promotion-pr` job owns the PR lookup
  that both the approver and the gate need.
- `execute-release.yml` passes `source_branch: main`, which switches on the
  reusable's own guard: it refuses to resolve `:testing` unless `main`'s tree
  still matches the promoted commit. A manual dispatch is exempt, because that
  path is deliberate recovery.
- The `ci` skill and the README now describe the gate and the guard, and the
  workflow comments no longer claim a gate that was not running.

Assisted-by: DeepSeek V4.1 Flash via OpenCode

@kubestellar-hive kubestellar-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed (correctness) at head 899cac7. No blockers; two minor notes.

  1. low — ujust recipe discovery is no longer recursive. build/10-overlay.sh:84 collects recipes with the top-level glob /ctx/custom/ujust/*.just, where the deleted build/10-build.sh:34 used find /ctx/custom/ujust -iname '*.just' (recursive, case-insensitive). A fork that organizes recipes into subdirectories under custom/ujust/ now ships a 60-custom.just silently missing them — no build error, the recipes just don't appear. Worth either restoring recursion or stating the flat-directory contract in custom/ujust/README.md.

  2. info — a bare podman build . no longer works: Containerfile:61 sets ARG BASE_IMAGE_NAME="" and build/00-image-info.sh:21 hard-fails on the empty value. All docs route through just build (which derives the value from the FROM line), and the failure message is clear, so this looks intentional — flagging only because it's a behavioral regression from the previous ARG BASE_IMAGE_NAME="silverblue" default.

Verified against the diff: the promotion chain is internally consistent (main builds keep the bare :testing tag in build-image.yml's Finalize step, which execute-release.yml's source_tag: "testing" resolves), unit-tests.yml installs just before calling just test-unit, and the pinned projectbluefin/actions@ce1d0c6 reusable exposes the source_branch/fast_forward_sha/run_release_gate inputs the callers use. Not verified: runtime behavior of the reusable workflows and the bats suite were not executed here; the release-flow semantics are judged from the pinned sources only.

— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.78

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants