From f77dc738199f0072e834d88be5d292405cb269ce Mon Sep 17 00:00:00 2001 From: Nam Rodriguez Date: Sat, 31 Oct 2026 14:21:42 -0400 Subject: [PATCH 1/2] Uninstall guide: give the stack its release's template again Changing KeepDataOnDelete with the current template ("Use existing template", or --use-previous-template) reports UPDATE_COMPLETE on a v0.8.4 or v0.8.5 install and keeps all the data: CloudFormation stores that parent template with every parameter already filled in. The guide's Console steps, its terminal command and the page summary now re-supply the release's own template (launch//coderhelm.json, from the stack's Version output) and check the stack's KeepDataOnDelete output before the delete. A test keeps any page from telling people to use the current template. The account page says the auto-update role carries its own copy of the boundary, kept with it. --- docs/components/uninstall/guide.tsx | 8 +++-- docs/content/self-host-site/aws-account.mdx | 4 +-- docs/e2e/uninstall.spec.ts | 6 ++++ docs/lib/uninstall/core.ts | 34 ++++++++++++++++++--- docs/lib/uninstall/page-info.ts | 2 +- docs/scripts/uninstall-core.test.mjs | 22 +++++++++++-- 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/docs/components/uninstall/guide.tsx b/docs/components/uninstall/guide.tsx index 9f3b1922..71b6d7b5 100644 --- a/docs/components/uninstall/guide.tsx +++ b/docs/components/uninstall/guide.tsx @@ -19,6 +19,7 @@ import { deleteCommand, fromInstallGuide, keepDataCommand, + releaseTemplateUrl, leftoversCommand, newState, queryToPartial, @@ -128,9 +129,10 @@ export function UninstallGuide() { {mode === 'everything' ? (
    -
  1. Open the stack {stackName(s)} and choose Update.
  2. -
  3. Choose Use existing template, then Next.
  4. -
  5. Set KeepDataOnDelete to false, leave everything else, then Next, tick the boxes, and Submit. It takes a minute and changes nothing that runs.
  6. +
  7. Open the stack {stackName(s)}, then Outputs, and note Version (for example v0.8.6).
  8. +
  9. Choose Update, then Replace existing template, Amazon S3 URL: {releaseTemplateUrl('')} with that version. It is the release you run. Don't choose Use existing template: on an install from v0.8.4 or v0.8.5 it reports success and keeps the data anyway.
  10. +
  11. Choose Next, set KeepDataOnDelete to false, leave everything else, then Next, tick the boxes, and Submit. It takes a minute and changes nothing that runs.
  12. +
  13. When it says UPDATE_COMPLETE, check Outputs again: KeepDataOnDelete must say false before you delete the stack.
Open the stack
diff --git a/docs/content/self-host-site/aws-account.mdx b/docs/content/self-host-site/aws-account.mdx index 123789ed..3d426d4f 100644 --- a/docs/content/self-host-site/aws-account.mdx +++ b/docs/content/self-host-site/aws-account.mdx @@ -23,7 +23,7 @@ A CloudFormation stack named `coderhelm-` (the name in your AWS account, ` ### The permissions boundary -A managed policy, `coderhelm--boundary-stack`. It is the most any CoderHelm role can do, whatever its own policy says. Every role the stack creates carries it. Even within the AWS services CoderHelm uses, it refuses: +A managed policy, `coderhelm--boundary-stack`. It is the most any CoderHelm role can do, whatever its own policy says. Every role the stack creates carries it; the auto-update role (only with auto-update on) carries an identical copy, `coderhelm--deploy-boundary-stack`, which is kept with that role when you delete the stack. Even within the AWS services CoderHelm uses, it refuses: - creating IAM users, access keys, sign in profiles, groups or identity providers; - anything in Organizations, account settings, CloudTrail or GuardDuty; @@ -46,7 +46,7 @@ A Secrets Manager secret, `coderhelm//secrets`. CloudFormation makes it wi ### The auto-update role (only with auto-update on) -With **Install new releases by themselves** on, the stack adds `coderhelm--deploy-stack`, a role only CloudFormation can use, limited to resources named or tagged for this install, with the boundary on it. The updater may hand it only to CloudFormation, and only for a change set on this stack from a published CoderHelm release template. With auto-update off (the default) there is no such role: an admin reviews each update's change set and applies it with their own permissions. +With **Install new releases by themselves** on, the stack adds `coderhelm--deploy-stack`, a role only CloudFormation can use, limited to resources named or tagged for this install, with its own copy of the boundary on it. The role and its copy are kept when you delete the stack (CloudFormation keeps using a stack's role, and a role can't delete itself); `leftovers` lists them and `--delete` removes them. The updater may hand it only to CloudFormation, and only for a change set on this stack from a published CoderHelm release template. With auto-update off (the default) there is no such role: an admin reviews each update's change set and applies it with their own permissions. ### The progress bucket diff --git a/docs/e2e/uninstall.spec.ts b/docs/e2e/uninstall.spec.ts index 65a5305c..e2a7168f 100644 --- a/docs/e2e/uninstall.spec.ts +++ b/docs/e2e/uninstall.spec.ts @@ -50,6 +50,12 @@ test('delete everything: set KeepDataOnDelete to false first', async ({ page }) await expect(page.getByTestId('goes')).toContainText('7-day wait'); await page.getByText('Or from a terminal').click(); await expect(page.getByTestId('keep-false-command')).toContainText('ParameterKey=KeepDataOnDelete,ParameterValue=false'); + // The release's own template again, never the current one (it kept every old value on v0.8.4 and v0.8.5). + await expect(page.getByTestId('keep-false-command')).toContainText('--template-url "https://coderhelm-public.s3.amazonaws.com/launch/$V/coderhelm.json"'); + await expect(page.getByTestId('keep-false-command')).not.toContainText('use-previous-template'); + await expect(page.getByTestId('keep-false-command')).toContainText("OutputKey=='KeepDataOnDelete'"); + await expect(page.getByTestId('set-keep-false')).toContainText('Replace existing template'); + await expect(page.getByTestId('set-keep-false')).toContainText('KeepDataOnDelete must say false'); await shot(page, 'uninstall-everything'); await page.getByTestId('next-data').click(); await expect(page.getByTestId('step-delete')).toContainText('deletes its tables itself'); diff --git a/docs/lib/uninstall/core.ts b/docs/lib/uninstall/core.ts index cf0617bf..de272562 100644 --- a/docs/lib/uninstall/core.ts +++ b/docs/lib/uninstall/core.ts @@ -81,14 +81,38 @@ export function stacksConsoleUrl(region: string): string { return `https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}#/stacks`; } -/** The CLI form of Update stack that only changes KeepDataOnDelete (every other value kept). */ +/** Where a release's parent template is; the stack's Version output says which one it runs. */ +export function releaseTemplateUrl(version: string): string { + return `https://coderhelm-public.s3.amazonaws.com/launch/${version}/coderhelm.json`; +} + +/** + * The CLI form of Update stack that only changes KeepDataOnDelete (every other + * value kept), then waits and prints the stack's KeepDataOnDelete output. + * It gives the stack its own release's template again, never "use the + * previous template": CloudFormation keeps a v0.8.4 or v0.8.5 template + * with every parameter already filled in, so that update reports + * UPDATE_COMPLETE and changes nothing. + */ export function keepDataCommand(s: Pick, keep: boolean): string { const others = ['Stage', 'AdminEmail', 'ModelProvider', 'BedrockInferencePrefix', 'EmailTransport', 'EmailFrom', 'Hostname', 'SignupDomain', 'RequireTwoFactor', 'AutoUpdate', 'UpdateDay', 'UpdateHour', 'UpdateTimezone', 'SetupToken', 'StatusId', 'DomainCertificateArn', 'DomainZoneId']; + const at = `--region ${s.region} --stack-name ${stackName(s)}`; return [ - `aws cloudformation update-stack --region ${s.region} --stack-name ${stackName(s)} --use-previous-template`, - '--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND', - `--parameters ParameterKey=KeepDataOnDelete,ParameterValue=${keep ? 'true' : 'false'} ${others.map((k) => `ParameterKey=${k},UsePreviousValue=true`).join(' ')}`, - ].join(' \\\n '); + `V=$(aws cloudformation describe-stacks ${at} --query "Stacks[0].Outputs[?OutputKey=='Version'].OutputValue" --output text)`, + [ + `aws cloudformation update-stack ${at}`, + `--template-url "${releaseTemplateUrl('$V')}"`, + '--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND', + `--parameters ParameterKey=KeepDataOnDelete,ParameterValue=${keep ? 'true' : 'false'} ${others.map((k) => `ParameterKey=${k},UsePreviousValue=true`).join(' ')}`, + ].join(' \\\n '), + `aws cloudformation wait stack-update-complete ${at}`, + keepDataCheckCommand(s), + ].join('\n'); +} + +/** Prints the stack's KeepDataOnDelete output: it must say what was chosen before the stack is deleted. */ +export function keepDataCheckCommand(s: Pick): string { + return `aws cloudformation describe-stacks --region ${s.region} --stack-name ${stackName(s)} --query "Stacks[0].Outputs[?OutputKey=='KeepDataOnDelete'].OutputValue" --output text`; } export function deleteCommand(s: Pick): string { diff --git a/docs/lib/uninstall/page-info.ts b/docs/lib/uninstall/page-info.ts index 2d8d0473..914ab8bd 100644 --- a/docs/lib/uninstall/page-info.ts +++ b/docs/lib/uninstall/page-info.ts @@ -20,7 +20,7 @@ ${UNINSTALL_GUIDE.description} ## Steps 1. **Your install.** Its name and region. The stack is \`coderhelm-\`. -2. **Keep the data or not.** The stack parameter \`KeepDataOnDelete\` is \`true\` unless you changed it: deleting the stack keeps the tables, the data bucket, the KMS keys, the user pool and the secret. To delete those too, first run **Update stack**, **Use current template**, set \`KeepDataOnDelete\` to \`false\`, and submit. KMS keys are then deleted after AWS's shortest wait of 7 days. +2. **Keep the data or not.** The stack parameter \`KeepDataOnDelete\` is \`true\` unless you changed it: deleting the stack keeps the tables, the data bucket, the KMS keys, the user pool and the secret. To delete those too, first run **Update stack** with **Replace existing template** and your release's own template, \`https://coderhelm-public.s3.amazonaws.com/launch//coderhelm.json\` (the stack's **Version** output), set \`KeepDataOnDelete\` to \`false\`, submit, and check that the stack's **KeepDataOnDelete** output says \`false\`. Don't choose **Use current template**: on an install from v0.8.4 or v0.8.5 it reports success and keeps every old value. KMS keys are then deleted after AWS's shortest wait of 7 days. 3. **Delete the stack.** In CloudFormation, select \`coderhelm-\` and choose **Delete**. CloudFormation removes every part in order. With \`KeepDataOnDelete=false\` the stack empties its buckets and deletes its tables itself, retrying while DynamoDB settles. A hybrid install releases its CoderHelm address first. 4. **Check nothing is left.** \`npx -y github:CoderHelm/coderhelm-platform leftovers --stage --region \` lists, read-only, anything still named or tagged for the install: data you kept, pieces from an earlier failed install, the auto-update role if auto-update was ever on. \`--delete\` removes them after you type the install name. diff --git a/docs/scripts/uninstall-core.test.mjs b/docs/scripts/uninstall-core.test.mjs index 5b4aa2da..11538526 100644 --- a/docs/scripts/uninstall-core.test.mjs +++ b/docs/scripts/uninstall-core.test.mjs @@ -26,9 +26,15 @@ test('the stack, its console page, and the commands name exactly this install', assert.match(c.leftoversCommand(S, true), / --delete$/); }); -test('changing KeepDataOnDelete keeps the template and every other value', () => { +test('changing KeepDataOnDelete gives the stack its own release template again, keeps every other value, and checks the output', () => { const cmd = c.keepDataCommand(S, false); - assert.match(cmd, /--use-previous-template/); + // --use-previous-template silently kept every old value on v0.8.4 and v0.8.5 (CloudFormation stores that template resolved). + assert.doesNotMatch(cmd, /use-previous-template/); + assert.match(cmd, /^V=\$\(aws cloudformation describe-stacks --region eu-west-1 --stack-name coderhelm-prod --query "Stacks\[0\]\.Outputs\[\?OutputKey=='Version'\]\.OutputValue" --output text\)/); + assert.match(cmd, /--template-url "https:\/\/coderhelm-public\.s3\.amazonaws\.com\/launch\/\$V\/coderhelm\.json"/); + assert.match(cmd, /wait stack-update-complete --region eu-west-1 --stack-name coderhelm-prod/); + assert.ok(cmd.trim().endsWith(c.keepDataCheckCommand(S)), 'it ends by printing the KeepDataOnDelete output'); + assert.match(c.keepDataCheckCommand(S), /OutputKey=='KeepDataOnDelete'/); assert.match(cmd, /ParameterKey=KeepDataOnDelete,ParameterValue=false/); assert.match(cmd, /CAPABILITY_AUTO_EXPAND/); for (const k of ['Stage', 'AdminEmail', 'AutoUpdate', 'DomainCertificateArn']) assert.match(cmd, new RegExp(`ParameterKey=${k},UsePreviousValue=true`)); @@ -47,3 +53,15 @@ test('validation names what is wrong', () => { assert.deepEqual(c.validate({ stage: 'prod', region: 'us-east-1' }), {}); assert.ok(c.validate({ stage: 'Prod', region: 'x' }).stage); }); + +test('no page tells anyone to change a parameter with the current template', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const root = path.resolve(import.meta.dirname, '..'); + const files = ['lib/uninstall/core.ts', 'lib/uninstall/page-info.ts', 'components/uninstall/guide.tsx', 'lib/install/core.ts', 'lib/install/page-info.ts', 'components/install/wizard.tsx']; + for (const f of files) { + const text = fs.readFileSync(path.join(root, f), 'utf8'); + assert.doesNotMatch(text, /--use-previous-template/, f); + for (const m of text.matchAll(/[^.\n]*Use (current|existing) template[^.\n]*/g)) assert.match(m[0], /Don('|')t/, `${f}: ${m[0].trim()}`); + } +}); From 21f2dd6b198e784691523cf2711cab0320b1bb60 Mon Sep 17 00:00:00 2001 From: Nam Rodriguez Date: Sat, 31 Oct 2026 15:07:48 -0400 Subject: [PATCH 2/2] Docs: platform lock to 13cd0f5 (the one-stack install fixes) --- docs/platform.lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform.lock.json b/docs/platform.lock.json index 079608ee..a2ae493e 100644 --- a/docs/platform.lock.json +++ b/docs/platform.lock.json @@ -1,4 +1,4 @@ { "repo": "CoderHelm/coderhelm-platform", - "ref": "61b7387c736cd8ac6a2e1d23f71541f9d3e5b5d0" + "ref": "13cd0f513985e8e72da8d615e03a4a934527def0" }