@curia-sh/cli 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -0
- package/bin/curia.mjs +15 -0
- package/package.json +36 -0
- package/src/acquire.mjs +188 -0
- package/src/archive.mjs +154 -0
- package/src/atomic.mjs +38 -0
- package/src/bundle.mjs +119 -0
- package/src/cli.mjs +73 -0
- package/src/commands.mjs +45 -0
- package/src/compose.mjs +137 -0
- package/src/config.mjs +289 -0
- package/src/doctor.mjs +392 -0
- package/src/exit.mjs +32 -0
- package/src/install.mjs +194 -0
- package/src/launcher.mjs +53 -0
- package/src/layout.mjs +95 -0
- package/src/lock.mjs +74 -0
- package/src/manifest.mjs +571 -0
- package/src/preflight.mjs +593 -0
- package/src/purge.mjs +230 -0
- package/src/resources.mjs +146 -0
- package/src/rollback.mjs +131 -0
- package/src/root.mjs +196 -0
- package/src/secrets.mjs +149 -0
- package/src/stable.mjs +314 -0
- package/src/stage.mjs +141 -0
- package/src/steps.mjs +25 -0
- package/src/switch.mjs +199 -0
- package/src/tailscale.mjs +156 -0
- package/src/uninstall.mjs +190 -0
- package/src/update.mjs +174 -0
- package/stable-index.pub +3 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, rmSync, unlinkSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { BOOTSTRAP_COMMAND } from './acquire.mjs'
|
|
5
|
+
import { EXIT, Refusal } from './exit.mjs'
|
|
6
|
+
import { dockerRunner } from './compose.mjs'
|
|
7
|
+
import { launcherPath } from './launcher.mjs'
|
|
8
|
+
import { lockPath, withLifecycleLock } from './lock.mjs'
|
|
9
|
+
import { removeInstallationResources } from './resources.mjs'
|
|
10
|
+
import { installationRoot, openRoot } from './root.mjs'
|
|
11
|
+
import { readSecret } from './secrets.mjs'
|
|
12
|
+
import { namedSteps } from './steps.mjs'
|
|
13
|
+
import { tailscaleRunner, withdrawServeRoutes } from './tailscale.mjs'
|
|
14
|
+
|
|
15
|
+
// `curia uninstall` (#886, implementing #855): the ordinary uninstall. It
|
|
16
|
+
// removes Curia's runnable footprint from the host and keeps the
|
|
17
|
+
// installation, as four named steps.
|
|
18
|
+
//
|
|
19
|
+
// preflight the root boundary (`openRoot`, which must find an
|
|
20
|
+
// installation). Nothing changes.
|
|
21
|
+
// docker under the lifecycle lock from here to the end: every
|
|
22
|
+
// container, network, and volume that carries this
|
|
23
|
+
// installation's label is stopped and removed. That is the
|
|
24
|
+
// five services of the Compose project, its network and tmux
|
|
25
|
+
// socket volume, and every agent container and agent cache
|
|
26
|
+
// volume the service created. Nothing without the label is
|
|
27
|
+
// read or touched, and the release images stay for purge.
|
|
28
|
+
// routes the Tailscale Serve routes Curia created, as
|
|
29
|
+
// `state/tailscale.json` records them, are turned off. No
|
|
30
|
+
// other route, and nothing else of the node.
|
|
31
|
+
// files the contents of versions/, cache/, and run/ are removed, and
|
|
32
|
+
// then the launcher, when it is this installation's.
|
|
33
|
+
//
|
|
34
|
+
// What stays is the installation: `config/`, `secrets/`, `state/`, and
|
|
35
|
+
// `work/`, with the installation record and its ID. Nothing here reads a
|
|
36
|
+
// session, drains one, changes a claim, or judges what under `work/` is
|
|
37
|
+
// worth keeping: the preserved directories are the recovery mechanism, and
|
|
38
|
+
// the bootstrap's `curia install` over the root is the reinstall.
|
|
39
|
+
//
|
|
40
|
+
// Every step reads what is there before it removes it, so a rerun over a
|
|
41
|
+
// partial cleanup does the rest and a rerun over a finished one does
|
|
42
|
+
// nothing. There is no persisted operation record and no repair mode. A
|
|
43
|
+
// failed step names itself and the command that reruns it.
|
|
44
|
+
|
|
45
|
+
export const UNINSTALL_STEPS = Object.freeze(['preflight', 'docker', 'routes', 'files'])
|
|
46
|
+
|
|
47
|
+
// The directories whose contents go, in the order they are removed. The
|
|
48
|
+
// directories themselves stay, so the root keeps its seven boundaries and the
|
|
49
|
+
// lock has a place to live on a rerun.
|
|
50
|
+
export const REMOVED_BOUNDARIES = Object.freeze(['versions', 'cache', 'run'])
|
|
51
|
+
export const PRESERVED_BOUNDARIES = Object.freeze(['config', 'secrets', 'state', 'work'])
|
|
52
|
+
|
|
53
|
+
// The command's seam. `context` is what `runCli` hands a command. `deps` are
|
|
54
|
+
// the boundaries a test replaces: the Docker runner and the `tailscale`
|
|
55
|
+
// runner.
|
|
56
|
+
export async function runUninstall(
|
|
57
|
+
{ env, stdout, uid, root },
|
|
58
|
+
{ docker = dockerRunner, tailscale = tailscaleRunner } = {},
|
|
59
|
+
) {
|
|
60
|
+
const launcher = launcherPath(env)
|
|
61
|
+
const steps = namedSteps({
|
|
62
|
+
steps: UNINSTALL_STEPS,
|
|
63
|
+
stdout,
|
|
64
|
+
rerun: (step) => (existsSync(launcher)
|
|
65
|
+
? `Run '${launcher} uninstall' to run ${step} again; the completed steps are kept.`
|
|
66
|
+
: `Fix the cause, then run 'curia uninstall' again from the bootstrap's reinstall (${reinstallCommand({ env, root })}), or remove the rest by hand: ${REMOVED_BOUNDARIES.map((b) => join(root, b)).join(', ')}.`),
|
|
67
|
+
})
|
|
68
|
+
const say = (text) => stdout.write(`${text}\n`)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// 1. preflight
|
|
72
|
+
steps.begin('preflight')
|
|
73
|
+
const opened = openRoot(root, { uid })
|
|
74
|
+
if (opened.status !== 'installed') {
|
|
75
|
+
throw new Refusal(`${root} holds no installation, so there is nothing to uninstall. Nothing changed.`)
|
|
76
|
+
}
|
|
77
|
+
const { installationId, activeVersion } = opened.record
|
|
78
|
+
say(`uninstalling Curia ${activeVersion} from ${root} (installation ${installationId}); config/, secrets/, state/, and work/ are kept`)
|
|
79
|
+
|
|
80
|
+
return await withLifecycleLock(root, async () => {
|
|
81
|
+
// 2. docker
|
|
82
|
+
steps.begin('docker')
|
|
83
|
+
const removed = await removeInstallationResources(installationId, { docker, stdout })
|
|
84
|
+
const count = removed.containers.length + removed.networks.length + removed.volumes.length
|
|
85
|
+
say(count === 0
|
|
86
|
+
? `no container, network, or volume carries the label of installation ${installationId}`
|
|
87
|
+
: `removed every container, network, and volume of installation ${installationId}; the release images are kept for 'curia purge'`)
|
|
88
|
+
|
|
89
|
+
// 3. routes
|
|
90
|
+
steps.begin('routes')
|
|
91
|
+
const routes = await withdrawServeRoutes({ stateDir: join(root, 'state'), stdout }, { tailscale })
|
|
92
|
+
if (routes.recorded.length === 0) say('no Serve route is recorded for this installation')
|
|
93
|
+
else if (routes.withdrawn.length === 0) say(`no recorded Serve route is standing; nothing to withdraw`)
|
|
94
|
+
|
|
95
|
+
// 4. files
|
|
96
|
+
steps.begin('files')
|
|
97
|
+
const lock = lockPath(root)
|
|
98
|
+
for (const name of REMOVED_BOUNDARIES) {
|
|
99
|
+
const dir = join(root, name)
|
|
100
|
+
if (!existsSync(dir)) continue
|
|
101
|
+
for (const entry of readdirSync(dir)) {
|
|
102
|
+
const file = join(dir, entry)
|
|
103
|
+
if (file === lock) continue
|
|
104
|
+
rmSync(file, { recursive: true, force: true })
|
|
105
|
+
}
|
|
106
|
+
say(`emptied ${dir}`)
|
|
107
|
+
}
|
|
108
|
+
const launcherFate = removeLauncher(launcher, root)
|
|
109
|
+
say(launcherFate === 'removed' ? `removed the launcher ${launcher}`
|
|
110
|
+
: launcherFate === 'foreign' ? `kept the launcher ${launcher}: it belongs to another installation root`
|
|
111
|
+
: `no launcher at ${launcher}`)
|
|
112
|
+
|
|
113
|
+
say('')
|
|
114
|
+
say(`Curia is uninstalled. The installation at ${root} is preserved.`)
|
|
115
|
+
say(` kept: ${PRESERVED_BOUNDARIES.map((b) => `${b}/`).join(', ')} (installation ${installationId}: configuration, secrets, history, and resumable work)`)
|
|
116
|
+
say(` removed: the launcher, ${REMOVED_BOUNDARIES.map((b) => `${b}/`).join(', ')}, and the installation's containers, networks, volumes, and Serve routes`)
|
|
117
|
+
say(` images: kept; 'curia purge' removes them`)
|
|
118
|
+
say(` reinstall: ${reinstallCommand({ env, root })}`)
|
|
119
|
+
say(` purge: ${purgeCommand({ env, root })}`)
|
|
120
|
+
const external = externalChecklist(root, { uid })
|
|
121
|
+
if (external.length > 0) {
|
|
122
|
+
say('')
|
|
123
|
+
say("External resources Curia never deletes. Remove them yourself if you won't reinstall:")
|
|
124
|
+
for (const line of external) say(` ${line}`)
|
|
125
|
+
}
|
|
126
|
+
return EXIT.ok
|
|
127
|
+
})
|
|
128
|
+
} catch (e) {
|
|
129
|
+
throw steps.wrap(e)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The bootstrap installs over a preserved root and recognizes the record, so
|
|
134
|
+
// the reinstall command is the install command, with the root named when it
|
|
135
|
+
// is not the default one.
|
|
136
|
+
export function reinstallCommand({ env, root }) {
|
|
137
|
+
return `${BOOTSTRAP_COMMAND}${rootOption({ env, root })}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function purgeCommand({ env, root }) {
|
|
141
|
+
return `${BOOTSTRAP_COMMAND} --purge${rootOption({ env, root })}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function rootOption({ env, root }) {
|
|
145
|
+
const byDefault = installationRoot({ HOME: env.HOME, XDG_DATA_HOME: env.XDG_DATA_HOME })
|
|
146
|
+
return root === byDefault ? '' : ` --root ${root}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Removes the launcher when it names this root. A launcher of another root
|
|
150
|
+
// is another installation's and stays.
|
|
151
|
+
function removeLauncher(launcher, root) {
|
|
152
|
+
let text
|
|
153
|
+
try {
|
|
154
|
+
text = readFileSync(launcher, 'utf8')
|
|
155
|
+
} catch (e) {
|
|
156
|
+
if (e.code === 'ENOENT') return 'absent'
|
|
157
|
+
throw e
|
|
158
|
+
}
|
|
159
|
+
if (!text.includes(`CURIA_ROOT='${root}'`)) return 'foreign'
|
|
160
|
+
unlinkSync(launcher)
|
|
161
|
+
return 'removed'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The external resources Curia knows the identifiers of, from the files it
|
|
165
|
+
// keeps. Identifiers only, never a token or a key, and a file that cannot be
|
|
166
|
+
// read contributes nothing: the checklist is a courtesy, not a check.
|
|
167
|
+
export function externalChecklist(root, { uid }) {
|
|
168
|
+
const lines = []
|
|
169
|
+
const github = parse(() => readSecret(root, 'github-app.json', { uid }))
|
|
170
|
+
if (github?.id) lines.push(`GitHub App ${github.id} and its installations: https://github.com/settings/apps`)
|
|
171
|
+
const discord = parse(() => readFileSync(join(root, 'state', 'discord.json'), 'utf8'))
|
|
172
|
+
if (discord?.guild_id) lines.push(`Discord bot, server ${discord.guild_id}${discord.channel ? `, channel ${discord.channel}` : ''}: https://discord.com/developers/applications`)
|
|
173
|
+
const tailscale = parse(() => readFileSync(join(root, 'state', 'tailscale.json'), 'utf8'))
|
|
174
|
+
if (tailscale?.machine_name) lines.push(`Tailscale node ${tailscale.machine_name}: https://login.tailscale.com/admin/machines`)
|
|
175
|
+
return lines
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parse(read) {
|
|
179
|
+
try {
|
|
180
|
+
const text = read()
|
|
181
|
+
return text ? JSON.parse(text) : null
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Used by the command table.
|
|
188
|
+
export function uninstallCommand(context, deps) {
|
|
189
|
+
return runUninstall(context, deps)
|
|
190
|
+
}
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { EXIT, Refusal, UsageError } from './exit.mjs'
|
|
5
|
+
import { acquireProbes, acquireRelease } from './acquire.mjs'
|
|
6
|
+
import { operatorConfigPath } from './config.mjs'
|
|
7
|
+
import { launcherPath } from './launcher.mjs'
|
|
8
|
+
import { withLifecycleLock } from './lock.mjs'
|
|
9
|
+
import { releaseProbes } from './manifest.mjs'
|
|
10
|
+
import { hostProbes, preflight } from './preflight.mjs'
|
|
11
|
+
import { openRoot, versionPaths } from './root.mjs'
|
|
12
|
+
import { StableIndexError, fetchStableIndex, pinnedPublicKey, releaseNotesUrl, renderSelection, selectRelease, selectionFromArgs, stableProbes } from './stable.mjs'
|
|
13
|
+
import { IncompatibleRelease, isCompleteStage, placeVersion, validateWithRelease, verifyRetained } from './stage.mjs'
|
|
14
|
+
import { namedSteps } from './steps.mjs'
|
|
15
|
+
import { switchRelease } from './switch.mjs'
|
|
16
|
+
import { dockerRunner } from './compose.mjs'
|
|
17
|
+
|
|
18
|
+
// `curia update` (#883, implementing #854): from the signed stable-release
|
|
19
|
+
// index to a verified, validated target release staged beside the active
|
|
20
|
+
// one, as one linear sequence of named steps.
|
|
21
|
+
//
|
|
22
|
+
// preflight the root boundary (`openRoot`, which must find an installation)
|
|
23
|
+
// and the host preflight. Nothing on disk changes.
|
|
24
|
+
// select the stable-release index is downloaded and proven against the
|
|
25
|
+
// pinned key, and one version is selected from it: the stable
|
|
26
|
+
// release by default, an exact version when asked, an exact
|
|
27
|
+
// prerelease only with `--prerelease`. A withdrawn version is
|
|
28
|
+
// never selected. When the selected version is the active one,
|
|
29
|
+
// there is nothing to do and the command stops here, ok.
|
|
30
|
+
// acquire under the lifecycle lock from here to the end: every artifact
|
|
31
|
+
// of the target (package, pinned runtime, bundle, checksum,
|
|
32
|
+
// manifest) is downloaded into cache/update/ and proven, the
|
|
33
|
+
// bootstrap's own steps in this package's code (acquire.mjs). A
|
|
34
|
+
// target already complete under versions/<target>/ is verified
|
|
35
|
+
// and reused instead, so a rerun downloads nothing.
|
|
36
|
+
// stage the target passes the release door (`verifyStagedRelease`)
|
|
37
|
+
// and lands read-only as versions/<target>/ through one rename,
|
|
38
|
+
// beside the active version, which does not change.
|
|
39
|
+
// validate the target release validates the current operator
|
|
40
|
+
// configuration with its own reader (`readOperatorConfig` of
|
|
41
|
+
// the staged package), so a configuration the target refuses
|
|
42
|
+
// stops the update before anything switches.
|
|
43
|
+
// switch the core services (service, app, overseer) are recreated
|
|
44
|
+
// from the target's bundle while tmux, ttyd, and the live agent
|
|
45
|
+
// containers keep running; every service reports healthy, the
|
|
46
|
+
// service and the app report the target version, and the live
|
|
47
|
+
// sessions are re-adopted; then the record names the target
|
|
48
|
+
// and the release that was active is kept as the one rollback
|
|
49
|
+
// release, every other version removed. A failure switches
|
|
50
|
+
// the core services back once and leaves the record alone.
|
|
51
|
+
// The sequence is `switchRelease` in switch.mjs, which `curia
|
|
52
|
+
// rollback` shares.
|
|
53
|
+
//
|
|
54
|
+
// Discovery is the same `fetchStableIndex` and `selectRelease` the service's
|
|
55
|
+
// daily check and the Curia app use. A failed discovery refuses before the
|
|
56
|
+
// lock, so it never touches the running installation. The command never
|
|
57
|
+
// rewrites the launcher: it reads the record, and the record is what the
|
|
58
|
+
// switch writes.
|
|
59
|
+
|
|
60
|
+
export const UPDATE_STEPS = Object.freeze(['preflight', 'select', 'acquire', 'stage', 'validate', 'switch'])
|
|
61
|
+
|
|
62
|
+
const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
63
|
+
|
|
64
|
+
// The command's seam. `context` is what `runCli` hands a command. `deps` are
|
|
65
|
+
// the boundaries a test replaces: the host probes, the stable-index probe
|
|
66
|
+
// and the pinned key, the acquisition probes, the release probes, the
|
|
67
|
+
// target's configuration validator, the Docker runner, the loopback `fetch`,
|
|
68
|
+
// and the clock the waits use.
|
|
69
|
+
export async function runUpdate(
|
|
70
|
+
{ env, args = [], stdout, uid, gid, root },
|
|
71
|
+
{ hostProbes: host = hostProbes, stableProbes: stable = stableProbes, publicKey = pinnedPublicKey(), acquireProbes: acquire = acquireProbes, releaseProbes: release = releaseProbes, validateTarget = validateWithRelease, docker = dockerRunner, fetch: fetchImpl = globalThis.fetch, sleep, now } = {},
|
|
72
|
+
) {
|
|
73
|
+
let selection
|
|
74
|
+
try {
|
|
75
|
+
selection = selectionFromArgs(args)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e instanceof StableIndexError) throw new UsageError(e.message)
|
|
78
|
+
throw e
|
|
79
|
+
}
|
|
80
|
+
const command = [launcherPath(env), 'update', ...args].join(' ')
|
|
81
|
+
const steps = namedSteps({ steps: UPDATE_STEPS, stdout, rerun: (step) => `Run '${command}' to run ${step} again; the completed steps are kept.` })
|
|
82
|
+
const say = (text) => stdout.write(`${text}\n`)
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// 1. preflight
|
|
86
|
+
steps.begin('preflight')
|
|
87
|
+
const opened = openRoot(root, { uid })
|
|
88
|
+
if (opened.status !== 'installed') {
|
|
89
|
+
throw new Refusal(`${root} holds no installation, so there is nothing to update. Run the bootstrap to install Curia there.`)
|
|
90
|
+
}
|
|
91
|
+
const record = opened.record
|
|
92
|
+
const active = record.activeVersion
|
|
93
|
+
const hostReport = await preflight({ uid, root, stdout }, host)
|
|
94
|
+
if (!hostReport.ok) throw hostReport.refusal
|
|
95
|
+
const dockerGid = hostReport.facts.docker.group.gid
|
|
96
|
+
|
|
97
|
+
// 2. select
|
|
98
|
+
steps.begin('select')
|
|
99
|
+
const fetched = await fetchStableIndex({ stdout, publicKey }, stable)
|
|
100
|
+
if (!fetched.ok) throw new Refusal(`${fetched.error} The running installation is not affected.`)
|
|
101
|
+
const { index } = fetched
|
|
102
|
+
if (index.withdrawn.includes(active)) {
|
|
103
|
+
say(`warning: the active version ${active} is withdrawn. The release notes at ${releaseNotesUrl(active)} say why.`)
|
|
104
|
+
}
|
|
105
|
+
const { version: target, selection: how } = selectRelease(index, selection)
|
|
106
|
+
stdout.write(renderSelection({ version: target, selection: how }))
|
|
107
|
+
if (target === active) {
|
|
108
|
+
say(`${active} is the active version. Nothing to update.`)
|
|
109
|
+
return EXIT.ok
|
|
110
|
+
}
|
|
111
|
+
say(`updating ${active} to ${target} (release notes: ${releaseNotesUrl(target)})`)
|
|
112
|
+
|
|
113
|
+
return await withLifecycleLock(root, async () => {
|
|
114
|
+
// 3. acquire
|
|
115
|
+
steps.begin('acquire')
|
|
116
|
+
const paths = versionPaths(root, target)
|
|
117
|
+
let stage = null
|
|
118
|
+
if (isCompleteStage(paths.dir)) {
|
|
119
|
+
say(`${target} is already staged under ${paths.dir}; verifying the retained artifacts`)
|
|
120
|
+
await verifyRetained({ version: target, dir: paths.dir, stdout }, release)
|
|
121
|
+
} else {
|
|
122
|
+
stage = join(root, 'cache', 'update', `${target}.${process.pid}`)
|
|
123
|
+
rmSync(stage, { recursive: true, force: true })
|
|
124
|
+
mkdirSync(stage, { recursive: true, mode: 0o700 })
|
|
125
|
+
try {
|
|
126
|
+
await acquireRelease({ version: target, stage, stdout }, acquire)
|
|
127
|
+
} catch (e) {
|
|
128
|
+
rmSync(stage, { recursive: true, force: true })
|
|
129
|
+
throw e
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 4. stage
|
|
134
|
+
steps.begin('stage')
|
|
135
|
+
if (stage) {
|
|
136
|
+
try {
|
|
137
|
+
await placeVersion({ root, version: target, stage, stdout }, release)
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(stage, { recursive: true, force: true })
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
say(`${target} is staged under ${paths.dir}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 5. validate
|
|
146
|
+
steps.begin('validate')
|
|
147
|
+
try {
|
|
148
|
+
await validateTarget({ root, version: target, dir: paths.dir })
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (!(e instanceof IncompatibleRelease)) throw e
|
|
151
|
+
throw new Error(e.reason === 'configuration' ? `${e.message} Fix the file, or choose another version. The active version is unchanged.` : `${e.message} Choose a version that does.`)
|
|
152
|
+
}
|
|
153
|
+
say(`${target} accepts the current operator configuration at ${operatorConfigPath(root)}`)
|
|
154
|
+
|
|
155
|
+
// 6. switch
|
|
156
|
+
steps.begin('switch')
|
|
157
|
+
await switchRelease(
|
|
158
|
+
{ root, from: active, to: target, record, environment: { uid, gid, dockerGid, installationId: record.installationId }, stdout },
|
|
159
|
+
{ docker, fetch: fetchImpl, sleep, now },
|
|
160
|
+
)
|
|
161
|
+
say('')
|
|
162
|
+
say(`Curia ${target} is running. Open the Curia app as before; nothing in integration setup has to be repeated.`)
|
|
163
|
+
say(`If ${target} misbehaves, 'curia rollback' switches back to ${active}.`)
|
|
164
|
+
return EXIT.ok
|
|
165
|
+
})
|
|
166
|
+
} catch (e) {
|
|
167
|
+
throw steps.wrap(e)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Used by the command table.
|
|
172
|
+
export function updateCommand(context, deps) {
|
|
173
|
+
return runUpdate(context, deps)
|
|
174
|
+
}
|
package/stable-index.pub
ADDED