From 484aef7e2a0b837c74475b502e3c712a5da85e43 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 2 Nov 2026 12:19:07 -0600 Subject: [PATCH] fix(upgrade): refuse a pre-#385 library Console unit before units stop (#385) --- BUILD-LOG.md | 1 + docs/SESSIONS.md | 1 + docs/TOOLS.md | 9 ++- docs/guides/host-run-sheet.md | 38 +++++++--- packages/cli/tests/stack-upgrade.test.mjs | 89 ++++++++++++++++++++++- scripts/stack-upgrade.sh | 30 +++++++- 6 files changed, 153 insertions(+), 15 deletions(-) diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 1969abc64..51b34733e 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -5820,3 +5820,4 @@ Before: agents can read the library; supporting-path and push-credential depende 2026-10-29T03:27:31Z | Codex implementation sub-agent | #385 OIDC-only writer preparation on exact base 09a847edaa25b609bac7728ff83f224708a29399 | Added fail-closed CLI, programmatic and service-render refusals for `--send` without `--sso`; retained read-only header preview/proof; migrated inventoried writer tests to real in-process OIDC login/callback sessions and updated docs. Focused regression 5/5 and selected service cases 4/4 pass; syntax and diff checks pass. The broader writer suite is blocked before execution because the clean checkout has no `node_modules` and exact pinned `oauth4webapi@3.8.8` is unavailable; no install, commit, push, PR, queue move or green/completion claim. 2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | CORRECTION and Mos2255 #385 review repair R2 | The 03:27:31Z dependency/no-commit statements are historical: exact pinned `oauth4webapi@3.8.8` is now materialized through the ignored `node_modules` fixture with no install, and original preparation is committed at `9936a1be1a37fdb30ba53cf2f14b7897225c3e4a`. On that unchanged HEAD, uncommitted R2 source/tests/docs make Case 2 library preview read-only until #412, drop inherited non-SSO `--send` during release planning, and refuse duplicate service `--send`. Focused RED was 0/2, 0/1, 0/1; focused GREEN and selected T53/T55/T57/T58 fixture blocks pass. No root/native/live/full-suite/CI action, commit, push, PR/API, merge or frozen/live-pin change. 2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on ab2bc23c7 | R2-F1: `scripts/stack-upgrade.sh` `read_args` now scans the serve argv after `--` and refuses `args-unreadable` before anything stops when the profile is library (no `--profile`, or the last one says library, as `case2-unit.sh` reads it) and `--send` is present; the message names the remediation, and the args file is never rewritten. `do_upgrade` and `do_rollback` both call `read_args` first, so rollback is covered; factory and tester paths are unchanged. RED on ab2bc23c7: new stack-upgrade case 0/1 (exit 2 rollback-failed, A4 and render-old failed, both units stopped), controls 1/1; GREEN 2/2. R2-I1: one component case runs the real `sso_for_target` then `send_for_target` on a legacy preview `--send` pointer, carried after `--sso` and before `--tester-socket`. R2-I2: TOOLS.md says a same-copy plain activate on a legacy pointer refuses already-active and needs `--no-send`. R2-I3: no code change; `uninstall`/`status` with `--send --send` exit 1, not 4, fail-closed either way. host-run-sheet section 7 gains the migration step. Whole files, one at a time: stack-upgrade 23/23 (69 s), stack-release 225/225 (608 s; a first run was cut by my 600 s background limit at 221 passing, its orphan fixture holder killed), case2-unit 48/48 (73 s). No root, systemd, container, network, push, PR or live action. +2026-11-02T18:19:04Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R4 on d0ddfa8de | R3-F1 fixed fail-closed, no migration: `scripts/stack-upgrade.sh` adds `legacy_console`, called right after `read_args` in `do_upgrade` and `do_rollback`. For the library profile (same default and last-wins rule as R3), an installed Console unit under the driver's UNIT_DIR whose ExecStart has a `--send` token after `serve` refuses new code `console-unit-legacy` (exit 3, nothing stopped). A missing unit is left to `read_old` (`units-unreadable`). The R3 args refusal stays. `case2-unit.sh`, factory, tester, stack-release.sh and OIDC/#412 untouched. Tests in stack-upgrade.test.mjs run the real base renderer (`git show 09a847eda` of case2-unit.sh and both templates) in the module fixture with fake getent/systemctl to make the legacy unit, prove the current case2-unit.sh refuses it, then drive upgrade and rollback with the real current renderer as A4's RENDER. Controls: read-only library unit, factory host, missing Console unit. RED on d0ddfa8de code 24/25 (the main case exits 2 rollback-failed, both units stopped), GREEN 25/25 (70.1 s). host-run-sheet section 7 now gives a read-only grep of the unit and says stop and report when it carries `--send`. TOOLS.md lists the code. No hash pins on changed files. stack-release.test.mjs and case2-unit.test.mjs read no changed file and were not rerun. One local commit, no push. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 64bbd8376..681d15662 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1882,3 +1882,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-29T03:27:31Z | Codex Daybreak Blue xhigh implementation sub-agent | #385 OIDC-only Console writes, branch work/vikunja-19-385-q284-complete at exact base 09a847edaa25b609bac7728ff83f224708a29399 | Source/test/docs preparation complete for review; focused refusal 5/5 and selected service 4/4 pass, broader real-OIDC writer tests blocked by absent pinned oauth4webapi@3.8.8; no install/commit/push/PR/queue move or overall-green claim. 2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | Mos2255 #385 review repair R2 on work/vikunja-19-385-q284-complete at unchanged HEAD 9936a1be1a37fdb30ba53cf2f14b7897225c3e4a/base 09a847edaa25b609bac7728ff83f224708a29399 | Corrects the 03:27:31Z historical blocker: pinned oauth4webapi3.8.8 fixture is now present without install and original prep is committed. F1/F2/F3 repaired in uncommitted source/tests/docs; F4/F7 disclosed only, F6 append conflicts preserved. Three focused test files only: intentional RED captured, GREEN controls plus T53 12/12, T55 8/8, T57 13/13 and T58 11/11 pass. No root/native/live/full-suite/CI, commit/push/PR/API/merge, frozen packet or live-pin action. 2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on work/vikunja-19-385-q284-complete at ab2bc23c7 | R2-F1 fixed: stack-upgrade refuses a library args file holding `--send` after `--` before any unit stops, for upgrade and rollback; R2-I1 component case and R2-I2 TOOLS.md precision added, R2-I3 noted. RED 0/1 then GREEN 2/2; stack-upgrade 23/23, stack-release 225/225, case2-unit 48/48. One local commit; no push, PR, Vikunja, Gitea or live action. +2026-11-02T18:19:04Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R4 on work/vikunja-19-385-q284-complete at d0ddfa8de | R3-F1 fixed: stack-upgrade refuses `console-unit-legacy` before any unit stops when a library host's installed Console unit runs serve with `--send`, for upgrade and rollback; host-run-sheet section 7 and TOOLS.md updated. RED 24/25 then GREEN 25/25 in stack-upgrade.test.mjs; other suites not rerun (no changed file read). One local commit; no push, PR, Vikunja, Gitea or live action. diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 1a79fe297..4dd4407cb 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -857,7 +857,14 @@ writes: one `case2-unit.sh` argument per line, the serve flags after `--`, owned by root with no group or other write, exactly one `--copy`, and that copy the one both installed units run. Otherwise the run refuses `args-unreadable` or `args-differ`. The driver writes the new copy id into -that file only after `confirm` passes. +that file only after `confirm` passes. For the `library` profile, an installed +Console unit whose `ExecStart` has a `--send` token after `serve` was rendered +before #385, and the current `case2-unit.sh` refuses it. `upgrade` and +`rollback` refuse `console-unit-legacy` right after reading the args file, +before any step runs, because no migration exists yet +(`docs/guides/host-run-sheet.md` section 7). A missing Console unit is not +this refusal; the later unit read refuses it `units-unreadable`, also before +anything stops. `upgrade`, in order. Every stack user step runs as `sudo -u USER -H env PATH=:/usr/local/bin:/usr/bin:/bin` from the diff --git a/docs/guides/host-run-sheet.md b/docs/guides/host-run-sheet.md index 14412bf05..3209cb5ca 100644 --- a/docs/guides/host-run-sheet.md +++ b/docs/guides/host-run-sheet.md @@ -385,21 +385,37 @@ but omits `--send`. That Console is read-only until Case 2 gains real OIDC in #4 explicitly refuses `--send`. Proxy preview and proof-protected reads remain available. The `factory` profile and its private tester socket are unchanged. -A library host installed before #385 has a `--send` line after `--`, because the earlier -`case2-unit.sh` required it. `stack-upgrade.sh upgrade` and `rollback` read the args file before -anything stops, and when the profile is `library` (no `--profile` line, or the last one says -`library`) and `--send` appears after `--`, they refuse `args-unreadable` with both units still -running. The script never rewrites the file. To migrate, as root: +A library host installed before #385 has `--send` in two places, because the earlier `case2-unit.sh` +required it: a `--send` line after `--` in the args file, and `--send` in the installed Console unit's +`ExecStart`. `stack-upgrade.sh upgrade` and `rollback` check both before anything stops, when the +profile is `library` (no `--profile` line, or the last one says `library`): -1. Open `/etc/mosaic-stack/dev.args` and delete the one line that reads `--send`. Change nothing else. -2. Check it: `stat -c '%U %a' /etc/mosaic-stack/dev.args` still gives `root` and a mode with no group or - other write, and `grep -cx -- --send /etc/mosaic-stack/dev.args` prints `0`. -3. Rerun the same `stack-upgrade.sh` command. +- `--send` after `--` in the args file refuses `args-unreadable`. +- A `--send` token after `serve` in `/etc/systemd/system/mosaic-webui.service`'s `ExecStart` refuses + `console-unit-legacy`. The current `case2-unit.sh` refuses that installed unit, so an upgrade or + rollback would stop both units and then fail both renders. + +Both refusals leave the units running, and the script rewrites neither file. Check the installed unit +read-only, as root: + +```sh +grep -n '^ExecStart=' /etc/systemd/system/mosaic-webui.service | grep -c -- ' --send' +``` + +- `0`, and no installed unit carries `--send`: removing the args line is enough. Open + `/etc/mosaic-stack/dev.args` and delete the one line that reads `--send`, changing nothing else. Check + that `stat -c '%U %a' /etc/mosaic-stack/dev.args` still gives `root` and a mode with no group or other + write, and that `grep -cx -- --send /etc/mosaic-stack/dev.args` prints `0`. Then rerun the same + `stack-upgrade.sh` command. +- `1` or more: the host needs a reviewed migration of the unit, the args file and the release pointer + together, and none exists yet. Stop. Leave the units running, change neither file and report to the + lead with the command's output. Removing only the args line does not help: the run then refuses + `console-unit-legacy`. The Console stays read-only after the upgrade, until #412 (#385). -Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` on any of these rules, before it stops -a unit. +Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` or `console-unit-legacy` on any of these +rules, before it stops a unit. ## 8. Enrollment, once per host diff --git a/packages/cli/tests/stack-upgrade.test.mjs b/packages/cli/tests/stack-upgrade.test.mjs index 66d7ff5c5..a0f1ef823 100644 --- a/packages/cli/tests/stack-upgrade.test.mjs +++ b/packages/cli/tests/stack-upgrade.test.mjs @@ -5,14 +5,15 @@ // stack-release-fixture.mjs's. Root's sudo, case2-unit.sh render and row 240 hostcheck are stand-ins // here; the stack's user's steps are the real stack-release.sh from the user's clone, except the // rehearsal, which the fixture's stub copies can't pass (its own tests are in stack-release.test.mjs). +// The #385 R3-F1 cases run the real base and current case2-unit.sh instead of the render stand-in. import test from 'node:test'; import assert from 'node:assert/strict'; -import {execFileSync,spawn} from 'node:child_process'; +import {execFileSync,spawn,spawnSync} from 'node:child_process'; import {mkdirSync,writeFileSync,readFileSync,existsSync,readdirSync,rmSync,chmodSync,copyFileSync,symlinkSync,statSync} from 'node:fs'; import {DatabaseSync} from 'node:sqlite'; import {join,dirname} from 'node:path'; import {once} from 'node:events'; -import {B,C,C1,U,COPIES,NODE,SBIN,base,fault,git,lines,logLines,mk,pointer,repo,sha,storeVersion,sysctl,units} from './stack-release-fixture.mjs'; +import {B,C,C1,U,COPIES,NODE,ROOT,SBIN,base,fault,git,lines,logLines,mk,pointer,repo,sha,storeVersion,sysctl,units} from './stack-release-fixture.mjs'; // --- Root's stand-ins. Each is serialized into its own file with K, its baked paths. --- @@ -479,6 +480,90 @@ test('#385 R2-F1 controls: library args without --send and a factory args file p assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]); }); +// R3-F1 (#385, PR 1836): a library host rendered by base case2-unit.sh has --send in its installed Console +// unit's ExecStart, not only in its args file. With the args line removed, A2 would stop both units and A4 +// and render-old would both refuse the installed unit (check_unit's ExecStart and the full-text match), so +// the driver refuses console-unit-legacy right after read_args. These cases run real renderers: base +// 09a847eda's case2-unit.sh, from git show into the module fixture, renders the legacy pair, and the +// checkout's own case2-unit.sh is A4's RENDER, both into an owned unit directory with fake getent and +// systemctl. +const BASE_REV='09a847eda'; +let baseRenderer; +const baseCase2=()=>{ + if(baseRenderer)return baseRenderer; + const d=join(base,'case2-base'); + for(const p of ['scripts/case2-unit.sh','packages/cli/systemd/mosaic-bus.service.in','packages/webui/systemd/mosaic-webui.service.in']){ + mkdirSync(dirname(join(d,p)),{recursive:true});writeFileSync(join(d,p),execFileSync('git',['-C',ROOT,'show',`${BASE_REV}:${p}`]),{mode:p.endsWith('.sh')?0o755:0o644}); + } + return baseRenderer=join(d,'scripts','case2-unit.sh'); +}; +// The stack's home gets a proof file; dir is a Case 2 unit directory that A4's real renderer and the driver +// read. args(copy, extra) is a library args file's lines without --send; legacy() renders base's pair into dir. +function case2Host(e,dir){ + const home=e.env.HOME,secrets=join(home,'.config','mosaic-dev','secrets');mkdirSync(secrets,{recursive:true,mode:0o700}); + const proof=join(secrets,'proxy-proof');writeFileSync(proof,'made-up-proof\n');chmodSync(proof,0o600); + const c2=join(e.root,'case2'),bin=join(c2,'bin'),log=join(c2,'systemctl.log');mkdirSync(bin,{recursive:true});mkdirSync(dir,{recursive:true}); + const uid=process.getuid(),gid=process.getgid(); + writeFileSync(join(bin,'getent'),`#!/bin/sh\ncase "$1 $2" in\n "passwd fixture") echo "fixture:x:${uid}:${gid}::${home}:/bin/bash" ;;\n "group ${gid}") echo "fixture:x:${gid}:" ;;\n *) exit 2 ;;\nesac\n`,{mode:0o755}); + writeFileSync(join(bin,'systemctl'),`#!/bin/sh\necho "$*" >> '${log}'\n[ "$1" != is-active ] || exit 3\n`,{mode:0o755}); + const env={CASE2_UNIT_DIR:dir,CASE2_GETENT:join(bin,'getent'),CASE2_SYSTEMCTL:join(bin,'systemctl')}; + const serve=['--port','443','--business','acme','--public-host','books.example','--auth-header','X-Auth-User','--allow-user','melanie','--proof-header','X-Proxy-Proof','--proof-file',proof]; + const args=(copy=B,extra=[])=>['--user','fixture','--home',home,'--business','acme','--copy',copy,'--node',NODE,...extra,'--',...serve]; + const render=(script,a)=>{const r=spawnSync('bash',[script,...a],{env:{PATH:e.env.PATH,...env},encoding:'utf8'});return {code:r.status,out:r.stdout,err:r.stderr}}; + const legacy=()=>{const a=args();a.splice(a.indexOf('melanie')+1,0,'--send');const r=render(baseCase2(),a);assert.equal(r.code,0,r.err);return r}; + const bytes=()=>Object.fromEntries(readdirSync(dir).sort().map((n)=>[n,readFileSync(join(dir,n),'utf8')])); + const syslog=()=>lines(log); + // The driver reads dir, and A4's RENDER is the checkout's real case2-unit.sh with the same seams. + const use=(text)=>{writeFileSync(e.args,text);chmodSync(e.args,0o640);e.argsText=text;e.denv.STACK_UPGRADE_UNIT_DIR=dir;delete e.denv.STACK_UPGRADE_RENDER;Object.assign(e.denv,env)}; + return {home,dir,args,render,legacy,bytes,syslog,use,con:join(dir,'mosaic-webui.service')}; +} +const legacyUnit=(f)=>`${f} runs serve with --send, so it was rendered before #385, and stack-upgrade.sh cannot migrate it to the read-only library Console: the units are left running; stop and report (docs/guides/host-run-sheet.md section 7) (nothing was stopped)`; +test('#385 R3-F1: an installed pre-#385 library Console unit with --send refuses console-unit-legacy before anything stops, for upgrade and rollback',async(t)=>{ + const e=await useed(t),h=case2Host(e,join(e.root,'case2','units')); + h.legacy(); + const c=readFileSync(h.con,'utf8'); + assert.ok(c.includes(`\nExecStart=${NODE} ${e.hc}/${B}/packages/webui/src/cli.mjs serve --port 443 --business acme --public-host books.example --auth-header X-Auth-User --allow-user melanie --send --proof-header X-Proxy-Proof --proof-file `),c); + // The operator removed the args file's --send line, as R3's section 7 said, so read_args passes. + h.use(h.args().join('\n')+'\n'); + assert.ok(!e.argsText.includes('--send')); + const before=h.bytes(),sys=h.syslog().length; + // The premise: the current renderer, as A4 runs it, refuses the installed legacy unit and writes nothing. + const p=h.render(join(checkout(C1),'scripts','case2-unit.sh'),h.args(C1)); + assert.equal(p.code,1,p.out+p.err); + assert.ok(p.err.startsWith(`case2-unit: installed ${h.con}: ExecStart= must be exactly once in [Service], as ExecStart=${NODE} ${e.hc}/${B}/packages/webui/src/cli.mjs serve --port 443 `),p.err); + assert.deepEqual(h.bytes(),before);assert.equal(h.syslog().length,sys); + for(const [why,run] of [['upgrade',()=>upgrade(e,C1)],['rollback',()=>drive(e,B,'rollback','--instance','dev','--to',B,...TO)]]){ + const n=existsSync(e.slog)?slog(e).length:0; + const r=await run();assert.equal(r.code,3,`${why}: ${r.all}`); + assert.equal(r.err.split('\n').at(-2),`stack-upgrade: console-unit-legacy: ${legacyUnit(h.con)}`,why); + assert.deepEqual(slog(e).slice(n).map((x)=>x.split(' ')[0]),['start','result'],why); + assert.equal(slog(e).at(-1),`result exit=3 console-unit-legacy ${legacyUnit(h.con)}`,why); + argsKept(e);assert.deepEqual(h.bytes(),before,why); + } + assert.ok(!existsSync(join(e.sd,'sudo.log')));assert.ok(!existsSync(join(e.sd,'render.log'))); + assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.equal(h.syslog().length,sys); + assert.deepEqual(live(e),['active','active',B,B]); +}); + +test('#385 R3-F1 controls: a read-only library Console unit, a factory host\'s unit and a missing Console unit pass the check',async(t)=>{ + const e=await useed(t); + const ro=case2Host(e,join(e.root,'case2','ro')),old=case2Host(e,join(e.root,'case2','legacy')),none=case2Host(e,join(e.root,'case2','none')); + const r0=ro.render(join(checkout(C1),'scripts','case2-unit.sh'),ro.args());assert.equal(r0.code,0,r0.err); + assert.ok(!readFileSync(ro.con,'utf8').includes('--send')); + old.legacy(); + none.legacy();rmSync(none.con); + // A bundle that does not exist stops the run at its first step after the check, with nothing stopped. + const gone=join(e.root,'no.bundle'); + for(const [why,h,extra] of [['a read-only library unit',ro,[]],['a factory args file over a unit holding --send',old,['--profile','factory']],['no installed Console unit',none,[]]]){ + h.use(h.args(B,extra).join('\n')+'\n'); + const n=existsSync(e.slog)?slog(e).length:0; + const r=await drive(e,C1,'upgrade','--instance','dev','--bundle',gone,'--sha',C1,...TO);assert.equal(r.code,3,`${why}: ${r.all}`); + assert.deepEqual(slog(e).slice(n).map((x)=>x.split(' ')[0]),['start','fetch','result'],why); + assert.match(slog(e).at(-1),/^result exit=3 fetch-failed /,why); + } + assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]); +}); + test('row 233: hostcheck (row 240): a nonzero --check or a missing file refuses profile-drift before the plan',async(t)=>{ const e=await useed(t); fault(e,'hostcheck-rc','1'); diff --git a/scripts/stack-upgrade.sh b/scripts/stack-upgrade.sh index f1705697c..c7172acb2 100755 --- a/scripts/stack-upgrade.sh +++ b/scripts/stack-upgrade.sh @@ -120,7 +120,8 @@ logl "start rc=0 argv:$(printf ' %q' "$0" "${ARGV[@]}")" # --- The args file and the stack's user --- # I2's file: one case2-unit.sh argument per line, owned by root with no group or other write. Sets -# ARGS (the lines), S_USER, S_HOME, BUSINESS, NODE and ARGS_COPY, and the paths derived from them. +# ARGS (the lines), S_USER, S_HOME, BUSINESS, NODE, ARGS_COPY and ARGS_PROFILE, and the paths derived +# from them. # #385 R2-F1: base case2-unit.sh required --send for the library profile (its default, and the last # --profile wins, as there), and the current one refuses it. A2 would stop both units, and A4 and # render-old would both fail on the same args, so a library file holding --send after -- refuses @@ -148,6 +149,7 @@ read_args() { esac done [ "$copies" = 1 ] || refuse args-unreadable "$f needs exactly one --copy before --, not $copies" + ARGS_PROFILE="$profile" if [ "$profile" = library ]; then for ((i = i + 1; i < ${#ARGS[@]}; i++)); do [ "${ARGS[i]}" != --send ] || @@ -165,6 +167,30 @@ read_args() { AS_USER=("$SUDO" -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin") } +# #385 R3-F1: base case2-unit.sh also put --send in the library Console unit's ExecStart. The current +# one checks an installed unit's ExecStart against its own read-only command, so A4 and render-old +# would both refuse that unit after A2 stopped both. No reviewed migration exists, so a library host +# whose installed Console unit has a --send token after serve refuses here, before anything runs. A +# missing unit is read_old's to refuse. +legacy_console() { + local f="$UNIT_DIR/$CON_UNIT" line t past + local -a lines words + [ "$ARGS_PROFILE" = library ] && [ -f "$f" ] && [ ! -L "$f" ] || return 0 + mapfile -t lines < "$f" || refuse units-unreadable "cannot read $f" + for line in "${lines[@]}"; do + [[ "$line" =~ ^[[:space:]]*ExecStart[[:space:]]*=(.*)$ ]] || continue + read -r -a words <<< "${BASH_REMATCH[1]}" + past="" + for t in "${words[@]}"; do + if [ -n "$past" ]; then + [ "$t" != --send ] || + refuse console-unit-legacy "$f runs serve with --send, so it was rendered before #385, and stack-upgrade.sh cannot migrate it to the read-only library Console: the units are left running; stop and report (docs/guides/host-run-sheet.md section 7)" + elif [ "$t" = serve ]; then past=1 + fi + done + done +} + # RENDER_ARGS: the args file's lines with copy $1 in place of --copy's value. args_with() { local i past="" @@ -296,6 +322,7 @@ run_steps() { do_upgrade() { read_args + legacy_console # Before A1: the bundle into the stack's user's clone, then the copy from it (I1). step fetch "${AS_USER[@]}" git -C "$CLONE" fetch -q "$BUNDLE" refs/heads/refactor:refs/remotes/origin/refactor || refuse fetch-failed "the stack's user could not fetch $BUNDLE into $CLONE" @@ -342,6 +369,7 @@ rehearse() { # check: the target is an older copy than this checkout, and no rehearsal. do_rollback() { read_args + legacy_console [ -x "$COPIES_DIR/$TO/scripts/mosaic" ] || refuse copy-missing "no $COPIES_DIR/$TO/scripts/mosaic" hostcheck read_old