From 3f6b0d6eb78ca4a221787e25e0c3b50b622553c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Can=20G=C3=BCm=C3=BC=C5=9F?= Date: Fri, 14 Aug 2026 00:08:06 +0300 Subject: [PATCH] fix(cli): back up workflows during upgrade Fixes #1493 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/1493-upgrade-workflow-backup.md | 5 +++ index.cjs | 16 ++++++++++ packages/squad-cli/src/cli/core/upgrade.ts | 16 ++++++++++ test/workflows.test.cjs | 36 ++++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 .changeset/1493-upgrade-workflow-backup.md diff --git a/.changeset/1493-upgrade-workflow-backup.md b/.changeset/1493-upgrade-workflow-backup.md new file mode 100644 index 000000000..0054c6fc0 --- /dev/null +++ b/.changeset/1493-upgrade-workflow-backup.md @@ -0,0 +1,5 @@ +--- +"@bradygaster/squad-cli": patch +--- + +Back up locally modified workflow files before `squad upgrade` refreshes them. diff --git a/index.cjs b/index.cjs index 67f6aab2b..eb7355da6 100644 --- a/index.cjs +++ b/index.cjs @@ -735,13 +735,29 @@ function generateProjectWorkflowStub(workflowFile, projectType) { // Write a workflow file: verbatim copy for npm projects, stub for others function writeWorkflowFile(file, srcPath, destPath, projectType) { + const backupIfCustomized = (nextContent) => { + if (!fs.existsSync(destPath)) return; + + const existing = fs.readFileSync(destPath, 'utf8'); + const normalizedExisting = existing.replace(/\r\n/g, '\n'); + const normalizedNext = nextContent.replace(/\r\n/g, '\n'); + + if (normalizedExisting !== normalizedNext && normalizedExisting.trim().length > 0) { + const backupPath = destPath + '.local-backup'; + fs.writeFileSync(backupPath, existing); + console.log(`${YELLOW}⚠${RESET} ${file} has local customizations — backed up to ${path.basename(backupPath)}`); + } + }; + if (projectType !== 'npm' && PROJECT_TYPE_SENSITIVE_WORKFLOWS.has(file)) { const stub = generateProjectWorkflowStub(file, projectType); if (stub) { + backupIfCustomized(stub); fs.writeFileSync(destPath, stub); return; } } + backupIfCustomized(fs.readFileSync(srcPath, 'utf8')); fs.copyFileSync(srcPath, destPath); } diff --git a/packages/squad-cli/src/cli/core/upgrade.ts b/packages/squad-cli/src/cli/core/upgrade.ts index e8a9fbc28..4bd8c171e 100644 --- a/packages/squad-cli/src/cli/core/upgrade.ts +++ b/packages/squad-cli/src/cli/core/upgrade.ts @@ -461,13 +461,29 @@ function generateProjectWorkflowStub(workflowFile: string, projectType: string): * Write workflow file: verbatim copy for npm, stub for others */ function writeWorkflowFile(file: string, srcPath: string, destPath: string, projectType: string): void { + const backupIfCustomized = (nextContent: string): void => { + if (!storage.existsSync(destPath)) return; + + const existing = storage.readSync(destPath) ?? ''; + const normalizedExisting = existing.replace(/\r\n/g, '\n'); + const normalizedNext = nextContent.replace(/\r\n/g, '\n'); + + if (normalizedExisting !== normalizedNext && normalizedExisting.trim().length > 0) { + const backupPath = destPath + '.local-backup'; + storage.writeSync(backupPath, existing); + warn(`${file} has local customizations — backed up to ${path.basename(backupPath)}`); + } + }; + if (projectType !== 'npm' && PROJECT_TYPE_SENSITIVE_WORKFLOWS.has(file)) { const stub = generateProjectWorkflowStub(file, projectType); if (stub) { + backupIfCustomized(stub); storage.writeSync(destPath, stub); return; } } + backupIfCustomized(storage.readSync(srcPath) ?? ''); storage.copySync(srcPath, destPath); } diff --git a/test/workflows.test.cjs b/test/workflows.test.cjs index 8a40cd1ef..dc10fa6be 100644 --- a/test/workflows.test.cjs +++ b/test/workflows.test.cjs @@ -192,6 +192,42 @@ describe('CI/CD workflow templates (squad-ci, squad-preview, squad-release)', () assert.equal(after, template, 'upgraded file should match the template exactly'); }); + + it('upgrade backs up modified workflow content before overwriting', (t) => { + const firstTemplate = CI_CD_WORKFLOWS.find(f => + fs.existsSync(path.join(TEMPLATES_DIR, f)) + ); + if (!firstTemplate) { + t.skip('no CI/CD workflow templates created yet'); + return; + } + + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test","version":"1.0.0"}\n'); + + initSquad(tmpDir); + const dest = path.join(tmpDir, '.github', 'workflows', firstTemplate); + const backup = dest + '.local-backup'; + const customized = [ + 'name: Squad CI', + 'on: [push]', + 'jobs:', + ' custom:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - run: echo custom workflow edit', + '', + ].join('\n'); + + fs.writeFileSync(dest, customized); + + const result = runSquad(['upgrade'], tmpDir); + assert.equal(result.exitCode, 0, `upgrade should succeed: ${result.stdout}`); + + assert.equal(fs.readFileSync(backup, 'utf8'), customized, + 'upgrade should preserve the pre-upgrade workflow in a local backup'); + assert.match(result.stdout, /backed up .*\.local-backup/, + 'upgrade should log the workflow backup path'); + }); }); describe('workflow YAML validity after init', () => {