Reported from a real runner: `sudo ./bin/dejima-ci-upgrade mark-node 2` answered "cannot check - dejima-ci-upgrade check-lan is not installed here" and recorded the assertion unverified. THE TOOL COULD NOT FIND ITSELF. verify-node commands were resolved on $PATH only, and the notes tell an operator to run this from a checkout because on the runner being upgraded it is not installed - it arrives WITH the version. So the lookup failed on the program doing the looking, mark-node recorded a weaker claim than it had evidence for, and status would have said UNVERIFIED forever. $PATH still wins where both exist, since the installed copy is what the machine runs; the fallback beside this program is what makes the documented workflow work. A CHECK THAT COULD NOT TELL WAS READ AS A CHECK THAT FAILED. Fixing the lookup exposed it immediately: check-lan exits 2 for "could not ask", and mark-node then refused to record, telling the operator their console work was missing on the strength of a check that never ran. Wrong in the alarming direction, about the one thing they cannot see for themselves. The contract is now written down - 0 in force, 2 could not determine, anything else not in force - and 2 records as unverified with its own message. AND THE NOTE'S ADVICE WAS INCOHERENT. It said not to run install.sh because that is a guest-side change a rollback reverts, while telling the operator to run set-tier and mark-node - which write /etc/dejima-ci/state, are equally guest-side, and are equally thrown away. Recording anything on a runner takes the snapshot cycle, so §2 now says that, and says to install the tool while you are in it rather than paying for the dance twice. It also says recording is OPTIONAL, which it is: the rules are on the hypervisor and in force either way. That is only safe advice because status no longer nags about it - for an upgrade with a verify-node command it runs the check before calling the work outstanding, and reports "NOT RECORDED, but its check passes" where it passes. Otherwise every rebuilt runner would report console work that is already done, and an operator who believed it would go and do it again. v2 is amended rather than superseded: it was deployed today and nobody else is on it. `bats test/` 133 passing, 5 new.
7.7 KiB
Upgrading a runner that already exists
A runner VM is pinned to the moment it was built. Half its controls were entered by hand in a hypervisor console, and the other half were installed from a checkout that has since moved on.
And unlike the sister project, it has a second way of going backwards: the scheduled rollback rebuilds it from a snapshot. That is this project's distinctive control and the reason a runner can be treated so casually, and it is also the reason an upgrade here is not simply "apply the change".
Why this project has one and dejima does not
dejima's containers are short-lived and only the tip of main is supported. A
gap opens, git pull closes it, and the next container starts from the closed
state. Nothing is pinned, so nothing drifts.
| dejima | a runner VM | |
|---|---|---|
| Lifetime | one session | months |
| Where the controls live | in the image, rebuilt each run | half in the guest, half typed into a console |
| How a gap closes | git pull |
a person, deliberately, on two machines |
| Can it move backwards? | no | yes, on a schedule |
A guest-side upgrade is not one step
This is the whole of it, and it is the step people skip:
The scheduled rollback rebuilds this VM from a snapshot. A guest-side change applied to the running machine and never baked into that snapshot is undone at the next rebuild, on a timer, with nothing to see.
So a guest-layer upgrade is a sequence. dejima-ci-upgrade status prints it
whenever one is outstanding, rather than leaving it to be remembered.
1. Do the node work first. Hypervisor rules live outside the VM and no rollback touches them. Doing them first also means that if you are interrupted, you are interrupted in the safe order: the boundary is tightened and the guest is merely stale.
2. Stop the runner and let jobs drain.
cd ~/dejima-ci && docker compose stop
3. Roll back to the clean snapshot first.
The counter-intuitive one. Upgrade from a known-clean base rather than from whatever the last few weeks of jobs left behind — otherwise you are about to promote that residue into your new rollback target and keep it forever.
4. Apply the guest change, from a current checkout:
git -C ~/dejima-ci-hardener pull
cd ~/dejima-ci-hardener
sudo ./install.sh
install.sh is idempotent; Enter at every prompt keeps what you have. It writes
guest_version into /etc/dejima-ci/state from the checkout's VERSION.
5. Verify, per the upgrade note. At minimum, that the runner comes back and takes a job.
6. Take a NEW snapshot.
7. Repoint the rollback job at the new snapshot, and confirm it. Skip this
and step 6 was decoration: the job still names the old snapshot and will restore
it on schedule, discarding everything above. proxmox/README.md §4 has the job
and the check.
Then delete the old snapshot, or name it so its date is unmissable. An old snapshot is a bootable copy of whatever the upgrade fixed, wired to a timer.
The one that fails safe
The state file lives in /etc/dejima-ci, inside the guest, so it is inside
the snapshot and rolls back with everything else.
That is correct rather than unfortunate. Forget step 6 and the version marker
reverts alongside the change it was recording, so dejima-ci-upgrade status
goes back to reporting the upgrade as outstanding. The system fails towards
saying there is work to do, which is the direction to fail in.
The two version numbers
/etc/dejima-ci/state:
tier=confined
guest_version=2
node_version=2
node_asserted=2026-08-25
node_verified=yes
guest_version is written by install.sh. The privileged tier has no
install.sh and no guest-side install at all — that is the grant the tier
exists for — so there it stays unset, and the tool reports n/a rather than
treating it as a gap.
node_version is written by dejima-ci-upgrade mark-node, by you, saying
you did the console work.
Both are guest state, so a rollback takes both. That is worth stating plainly because it makes recording anything on a runner a guest-side change with the same seven steps behind it. Two consequences follow, and neither is a bug:
- Recording a node-only upgrade is optional. The console work is on the hypervisor and is in force whether or not a file in the guest says so.
- After a rollback,
statuswill list a recorded upgrade as outstanding again. It does not simply believe that: where the upgrade declares averify-nodecommand it runs the check first, and where the check passes it says NOT RECORDED, but its check passes rather than console work outstanding. Nothing in the guest can read the hypervisor's rules, which is exactly what makes those rules the boundary, so it is an assertion and the tool treats it as one.
| Means | |
|---|---|
node behind guest |
Console work outstanding. The ordinary case after a pull. |
guest behind node |
On a runner, usually a guest upgrade that was applied and never re-snapshotted, since undone by the rollback. |
Tier is recorded, never detected
sudo dejima-ci-upgrade set-tier confined # or privileged
An absent /etc/dejima-ci/config identifies a privileged host correctly, and
also identifies a confined host whose install.sh has not run yet. Guessing
wrong selects a different set of upgrades — the same failure
test/ci-runner.bats takes DEJIMA_CI_TIER rather than sniffing for, on the
same grounds.
Where the assertion gets checked
An upgrade may declare a verify-node command that passes only if the console
work is in force. For v2 that is dejima-ci-upgrade check-lan, which asks
ff02::1 who is on the link and counts the replies that are not this VM's own.
mark-noderuns it first and refuses to record a claim the machine contradicts.--forceoverrides and records the result as unverified, sostatuskeeps saying so.- A check that cannot tell is not a check that failed. The contract is exit
0for in force,2for could-not-determine, anything else for not in force.check-lanexits2with nopingto probe with, andmark-noderecords the assertion as unverified rather than refusing it — telling an operator their console work is missing on the strength of a check that never ran would be wrong in the alarming direction. - The command is looked up on
$PATHfirst, then beside this program. The fallback is what makes the documented workflow work: the notes say to run from a checkout on a box where nothing is installed, and without it the tool could not find itself. statusre-runs the checks for everything already recorded. An assertion that was true in June and is false now is a rule somebody removed.
check-lan is a narrow test of one node rule, not an audit. This repository has
nothing else that runs inside the VM: test/ci-runner.bats runs inside a job,
which is the wrong vantage point, because a confined job's traffic is stopped by
dejima-ci-firewall long before it reaches the tap. The sister project has an
in-VM audit and checks more this way. That asymmetry is real and is not closed
here.
Adding an upgrade, if you are changing this repository
- Bump
VERSION. - Write
upgrades/000N-slug.mdwith the header documented here. - Set
layers:honestly. Anything includingguestis rollback-sensitive by construction and the tool will print the seven steps. - Give it a
verify-node:where the node-side change has an observable consequence from inside the guest. - Write it for a person standing at a console who has not read the diff.
Write one for every version, including versions that need nothing. A missing file and a file saying "nothing to do" are the same thing to a reader who has to go looking.