@regardio/dev 1.13.8 → 1.14.3
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 +3 -3
- package/dist/bin/exec/clean.d.ts +3 -0
- package/dist/bin/exec/clean.d.ts.map +1 -0
- package/dist/bin/exec/clean.js +25 -0
- package/dist/bin/exec/clean.test.d.ts +2 -0
- package/dist/bin/exec/clean.test.d.ts.map +1 -0
- package/dist/bin/exec/clean.test.js +45 -0
- package/dist/bin/exec/husky.d.ts +3 -0
- package/dist/bin/exec/husky.d.ts.map +1 -0
- package/dist/bin/exec/p.d.ts +3 -0
- package/dist/bin/exec/p.d.ts.map +1 -0
- package/dist/bin/exec/s.d.ts +3 -0
- package/dist/bin/exec/s.d.ts.map +1 -0
- package/dist/bin/exec/ts.d.ts +3 -0
- package/dist/bin/exec/ts.d.ts.map +1 -0
- package/dist/bin/exec/ts.js +36 -0
- package/dist/bin/exec/ts.test.d.ts +2 -0
- package/dist/bin/exec/ts.test.d.ts.map +1 -0
- package/dist/bin/exec/ts.test.js +39 -0
- package/dist/bin/exec/tsc.d.ts +3 -0
- package/dist/bin/exec/tsc.d.ts.map +1 -0
- package/dist/bin/flow/hotfix.d.ts +3 -0
- package/dist/bin/flow/hotfix.d.ts.map +1 -0
- package/dist/bin/flow/hotfix.js +116 -0
- package/dist/bin/flow/release.d.ts +3 -0
- package/dist/bin/flow/release.d.ts.map +1 -0
- package/dist/bin/flow/release.js +68 -0
- package/dist/bin/flow/ship.d.ts +3 -0
- package/dist/bin/flow/ship.d.ts.map +1 -0
- package/dist/bin/flow/ship.js +104 -0
- package/dist/bin/flow/utils.d.ts +9 -0
- package/dist/bin/flow/utils.d.ts.map +1 -0
- package/dist/bin/flow/utils.js +63 -0
- package/dist/bin/flow/utils.test.d.ts +2 -0
- package/dist/bin/flow/utils.test.d.ts.map +1 -0
- package/dist/bin/flow/utils.test.js +127 -0
- package/dist/bin/lint/biome.d.ts +3 -0
- package/dist/bin/lint/biome.d.ts.map +1 -0
- package/dist/bin/lint/commit.d.ts +3 -0
- package/dist/bin/lint/commit.d.ts.map +1 -0
- package/dist/bin/lint/md.d.ts +3 -0
- package/dist/bin/lint/md.d.ts.map +1 -0
- package/dist/bin/lint/package.d.ts +4 -0
- package/dist/bin/lint/package.d.ts.map +1 -0
- package/dist/bin/lint/package.js +81 -0
- package/dist/bin/lint/package.test.d.ts +2 -0
- package/dist/bin/lint/package.test.d.ts.map +1 -0
- package/dist/bin/lint/package.test.js +65 -0
- package/package.json +21 -22
- package/src/bin/exec/clean.test.ts +63 -0
- package/src/bin/exec/clean.ts +36 -0
- package/src/bin/exec/ts.test.ts +54 -0
- package/src/bin/exec/ts.ts +52 -0
- package/src/bin/flow/hotfix.ts +210 -0
- package/src/bin/flow/release.ts +130 -0
- package/src/bin/flow/ship.ts +215 -0
- package/src/bin/flow/utils.test.ts +178 -0
- package/src/bin/flow/utils.ts +109 -0
- package/src/bin/lint/package.test.ts +83 -0
- package/src/bin/lint/package.ts +108 -0
- package/src/templates/release.yml +23 -17
- package/dist/bin/exec-clean.d.ts +0 -3
- package/dist/bin/exec-clean.d.ts.map +0 -1
- package/dist/bin/exec-clean.js +0 -18
- package/dist/bin/exec-husky.d.ts +0 -3
- package/dist/bin/exec-husky.d.ts.map +0 -1
- package/dist/bin/exec-p.d.ts +0 -3
- package/dist/bin/exec-p.d.ts.map +0 -1
- package/dist/bin/exec-s.d.ts +0 -3
- package/dist/bin/exec-s.d.ts.map +0 -1
- package/dist/bin/exec-ts.d.ts +0 -3
- package/dist/bin/exec-ts.d.ts.map +0 -1
- package/dist/bin/exec-ts.js +0 -28
- package/dist/bin/exec-tsc.d.ts +0 -3
- package/dist/bin/exec-tsc.d.ts.map +0 -1
- package/dist/bin/flow-changeset.d.ts +0 -3
- package/dist/bin/flow-changeset.d.ts.map +0 -1
- package/dist/bin/flow-changeset.js +0 -18
- package/dist/bin/flow-release.d.ts +0 -3
- package/dist/bin/flow-release.d.ts.map +0 -1
- package/dist/bin/flow-release.js +0 -115
- package/dist/bin/lint-biome.d.ts +0 -3
- package/dist/bin/lint-biome.d.ts.map +0 -1
- package/dist/bin/lint-commit.d.ts +0 -3
- package/dist/bin/lint-commit.d.ts.map +0 -1
- package/dist/bin/lint-md.d.ts +0 -3
- package/dist/bin/lint-md.d.ts.map +0 -1
- package/dist/bin/lint-package.d.ts +0 -3
- package/dist/bin/lint-package.d.ts.map +0 -1
- package/dist/bin/lint-package.js +0 -86
- package/dist/bin/lint-package.test.d.ts +0 -2
- package/dist/bin/lint-package.test.d.ts.map +0 -1
- package/dist/bin/lint-package.test.js +0 -111
- package/src/bin/exec-clean.ts +0 -24
- package/src/bin/exec-ts.ts +0 -39
- package/src/bin/flow-changeset.ts +0 -23
- package/src/bin/flow-release.ts +0 -185
- package/src/bin/lint-package.test.ts +0 -140
- package/src/bin/lint-package.ts +0 -114
- /package/dist/bin/{exec-husky.js → exec/husky.js} +0 -0
- /package/dist/bin/{exec-p.js → exec/p.js} +0 -0
- /package/dist/bin/{exec-s.js → exec/s.js} +0 -0
- /package/dist/bin/{exec-tsc.js → exec/tsc.js} +0 -0
- /package/dist/bin/{lint-biome.js → lint/biome.js} +0 -0
- /package/dist/bin/{lint-commit.js → lint/commit.js} +0 -0
- /package/dist/bin/{lint-md.js → lint/md.js} +0 -0
- /package/src/bin/{exec-husky.ts → exec/husky.ts} +0 -0
- /package/src/bin/{exec-p.ts → exec/p.ts} +0 -0
- /package/src/bin/{exec-s.ts → exec/s.ts} +0 -0
- /package/src/bin/{exec-tsc.ts → exec/tsc.ts} +0 -0
- /package/src/bin/{lint-biome.ts → lint/biome.ts} +0 -0
- /package/src/bin/{lint-commit.ts → lint/commit.ts} +0 -0
- /package/src/bin/{lint-md.ts → lint/md.ts} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flow-hotfix: Manage hotfix branches based on production code.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* flow-hotfix start <name> - Create hotfix/<name> from production
|
|
7
|
+
* flow-hotfix finish <patch|minor> "description" - Finish and propagate the hotfix
|
|
8
|
+
*
|
|
9
|
+
* GitLab workflow:
|
|
10
|
+
* production → hotfix/<name> → production → staging → main
|
|
11
|
+
*
|
|
12
|
+
* start:
|
|
13
|
+
* 1. Fetches origin, checks out production, creates hotfix/<name>
|
|
14
|
+
*
|
|
15
|
+
* finish:
|
|
16
|
+
* 1. Ensures you are on a hotfix/* branch with a clean working tree
|
|
17
|
+
* 2. Runs quality checks
|
|
18
|
+
* 3. Bumps version (patch or minor) and updates CHANGELOG.md
|
|
19
|
+
* 4. Commits, then merges into production
|
|
20
|
+
* 5. Merges production into staging
|
|
21
|
+
* 6. Merges staging into main
|
|
22
|
+
* 7. Pushes all three branches, deletes the hotfix branch
|
|
23
|
+
* 8. Checks out main
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
branchExists,
|
|
30
|
+
bumpVersion,
|
|
31
|
+
git,
|
|
32
|
+
gitRead,
|
|
33
|
+
insertChangelog,
|
|
34
|
+
runQualityChecks,
|
|
35
|
+
runScript,
|
|
36
|
+
} from './utils.js';
|
|
37
|
+
|
|
38
|
+
const subcommand = process.argv[2];
|
|
39
|
+
const subArgs = process.argv.slice(3);
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// flow-hotfix start <name>
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
if (subcommand === 'start') {
|
|
45
|
+
const name = subArgs[0];
|
|
46
|
+
if (!name) {
|
|
47
|
+
console.error('Usage: flow-hotfix start <name>');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hotfixBranch = `hotfix/${name}`;
|
|
52
|
+
|
|
53
|
+
const status = gitRead('status', '--porcelain');
|
|
54
|
+
if (status) {
|
|
55
|
+
console.error('Working directory has uncommitted changes. Commit or stash them first.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('\nFetching latest state from origin...');
|
|
60
|
+
git('fetch', 'origin');
|
|
61
|
+
|
|
62
|
+
if (!branchExists('production')) {
|
|
63
|
+
console.error(
|
|
64
|
+
'Branch "production" does not exist. Create it first:\n'
|
|
65
|
+
+ ' git checkout -b production && git push -u origin production',
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
git('checkout', 'production');
|
|
71
|
+
git('pull', '--ff-only', 'origin', 'production');
|
|
72
|
+
git('checkout', '-b', hotfixBranch);
|
|
73
|
+
|
|
74
|
+
console.log(`\n✅ Hotfix branch "${hotfixBranch}" created from production.`);
|
|
75
|
+
console.log('Apply your fix, then run: flow-hotfix finish <patch|minor> "description"');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// flow-hotfix finish <patch|minor> "description"
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
if (subcommand === 'finish') {
|
|
83
|
+
const bumpType = subArgs[0];
|
|
84
|
+
const message = subArgs.slice(1).join(' ');
|
|
85
|
+
|
|
86
|
+
if (!bumpType || !['patch', 'minor'].includes(bumpType)) {
|
|
87
|
+
console.error('Usage: flow-hotfix finish <patch|minor> "description"');
|
|
88
|
+
console.error('Hotfixes use patch or minor bumps only.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!message) {
|
|
93
|
+
console.error('A description is required.');
|
|
94
|
+
console.error('Example: flow-hotfix finish patch "Fix critical auth bug"');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Guard: must be on a hotfix/* branch
|
|
99
|
+
const currentBranch = gitRead('branch', '--show-current');
|
|
100
|
+
if (!currentBranch.startsWith('hotfix/')) {
|
|
101
|
+
console.error(`Must be on a hotfix/* branch. Currently on: ${currentBranch}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Guard: working tree must be clean
|
|
106
|
+
const status = gitRead('status', '--porcelain');
|
|
107
|
+
if (status) {
|
|
108
|
+
console.error('Working directory has uncommitted changes. Commit or stash them first.');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Quality checks
|
|
113
|
+
console.log('\nRunning quality checks...');
|
|
114
|
+
try {
|
|
115
|
+
runQualityChecks();
|
|
116
|
+
} catch {
|
|
117
|
+
console.error('\nQuality checks failed. Fix all issues before finishing the hotfix.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log('✅ Quality checks passed');
|
|
121
|
+
|
|
122
|
+
// Read package.json
|
|
123
|
+
const packageJsonPath = join(process.cwd(), 'package.json');
|
|
124
|
+
if (!existsSync(packageJsonPath)) {
|
|
125
|
+
console.error('No package.json found in current directory.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
|
|
130
|
+
name: string;
|
|
131
|
+
version: string;
|
|
132
|
+
};
|
|
133
|
+
const packageName = packageJson.name;
|
|
134
|
+
const oldVersion = packageJson.version;
|
|
135
|
+
const newVersion = bumpVersion(oldVersion, bumpType);
|
|
136
|
+
|
|
137
|
+
console.log(`\nBumping ${packageName}: ${oldVersion} → ${newVersion}`);
|
|
138
|
+
|
|
139
|
+
writeFileSync(
|
|
140
|
+
packageJsonPath,
|
|
141
|
+
`${JSON.stringify({ ...packageJson, version: newVersion }, null, 2)}\n`,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Update CHANGELOG.md
|
|
145
|
+
const changelogPath = join(process.cwd(), 'CHANGELOG.md');
|
|
146
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
147
|
+
insertChangelog(changelogPath, `## [${newVersion}] - ${today} (hotfix)\n\n${message}\n`);
|
|
148
|
+
|
|
149
|
+
// Fix formatting if available
|
|
150
|
+
try {
|
|
151
|
+
runScript('fix');
|
|
152
|
+
} catch {
|
|
153
|
+
// fix may not exist in all packages
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Commit
|
|
157
|
+
git('add', '-A');
|
|
158
|
+
git('commit', '-m', `chore(hotfix): ${packageName}@${newVersion}`, '-m', message);
|
|
159
|
+
|
|
160
|
+
// Fetch before merging
|
|
161
|
+
console.log('\nFetching latest state from origin...');
|
|
162
|
+
git('fetch', 'origin');
|
|
163
|
+
|
|
164
|
+
// Merge hotfix → production
|
|
165
|
+
console.log('\nMerging hotfix into production...');
|
|
166
|
+
git('checkout', 'production');
|
|
167
|
+
git('pull', '--ff-only', 'origin', 'production');
|
|
168
|
+
git(
|
|
169
|
+
'merge',
|
|
170
|
+
'--no-ff',
|
|
171
|
+
currentBranch,
|
|
172
|
+
'-m',
|
|
173
|
+
`chore(hotfix): merge ${currentBranch} into production`,
|
|
174
|
+
);
|
|
175
|
+
git('push', 'origin', 'production');
|
|
176
|
+
|
|
177
|
+
// Merge production → staging
|
|
178
|
+
console.log('\nPropagating hotfix to staging...');
|
|
179
|
+
git('checkout', 'staging');
|
|
180
|
+
git('pull', '--ff-only', 'origin', 'staging');
|
|
181
|
+
git('merge', '--no-ff', 'production', '-m', 'chore(hotfix): merge production into staging');
|
|
182
|
+
git('push', 'origin', 'staging');
|
|
183
|
+
|
|
184
|
+
// Merge staging → main
|
|
185
|
+
console.log('\nPropagating hotfix to main...');
|
|
186
|
+
git('checkout', 'main');
|
|
187
|
+
git('pull', '--ff-only', 'origin', 'main');
|
|
188
|
+
git('merge', '--no-ff', 'staging', '-m', 'chore(hotfix): merge staging into main');
|
|
189
|
+
git('push', 'origin', 'main');
|
|
190
|
+
|
|
191
|
+
// Delete hotfix branch
|
|
192
|
+
git('branch', '-d', currentBranch);
|
|
193
|
+
try {
|
|
194
|
+
git('push', 'origin', '--delete', currentBranch);
|
|
195
|
+
} catch {
|
|
196
|
+
// Remote branch may not exist if it was never pushed
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`\n✅ Hotfix ${packageName}@${newVersion} shipped to production → staging → main`);
|
|
200
|
+
console.log('You are on main and ready to keep working.');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Unknown subcommand
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
console.error('Usage:');
|
|
208
|
+
console.error(' flow-hotfix start <name>');
|
|
209
|
+
console.error(' flow-hotfix finish <patch|minor> "description"');
|
|
210
|
+
process.exit(1);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flow-release: Deploy changes to staging following the GitLab workflow.
|
|
4
|
+
*
|
|
5
|
+
* Usage: flow-release "message"
|
|
6
|
+
*
|
|
7
|
+
* GitLab workflow:
|
|
8
|
+
* main → staging (staging deploy, no version bump yet)
|
|
9
|
+
*
|
|
10
|
+
* Versioning happens at ship time (flow-ship), not here.
|
|
11
|
+
* This keeps version numbers reserved for production-verified code.
|
|
12
|
+
*
|
|
13
|
+
* This script:
|
|
14
|
+
* 1. Ensures the current branch is main and the working tree is clean
|
|
15
|
+
* 2. Pulls latest main from origin
|
|
16
|
+
* 3. Runs quality checks locally (build, typecheck, tests)
|
|
17
|
+
* 4. Runs fix — commits formatting output (if any) with a commitlint-compliant message
|
|
18
|
+
* 5. Merges main into staging (fast-forward) and pushes
|
|
19
|
+
* 6. Pushes main and returns so work can continue
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { branchExists, git, gitRead, runQualityChecks, runScript } from './utils.js';
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const message = args.join(' ');
|
|
28
|
+
|
|
29
|
+
if (!message) {
|
|
30
|
+
console.error('Usage: flow-release "message"');
|
|
31
|
+
console.error('Example: flow-release "Add new vitest configs"');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Guard: must be on main
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
const currentBranch = gitRead('branch', '--show-current');
|
|
39
|
+
if (currentBranch !== 'main') {
|
|
40
|
+
console.error(`Must be on the main branch to release. Currently on: ${currentBranch}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Guard: working tree must be clean
|
|
45
|
+
const status = gitRead('status', '--porcelain');
|
|
46
|
+
if (status) {
|
|
47
|
+
console.error('Working directory has uncommitted changes. Commit or stash them first.');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Sync with origin before doing anything
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
console.log('\nFetching latest state from origin...');
|
|
55
|
+
git('fetch', 'origin');
|
|
56
|
+
git('pull', '--ff-only', 'origin', 'main');
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Read package.json
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
const packageJsonPath = join(process.cwd(), 'package.json');
|
|
62
|
+
if (!existsSync(packageJsonPath)) {
|
|
63
|
+
console.error('No package.json found in current directory.');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { name: string };
|
|
68
|
+
const packageName = packageJson.name;
|
|
69
|
+
|
|
70
|
+
if (!packageName) {
|
|
71
|
+
console.error('No "name" field found in package.json.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Guard: staging branch must exist
|
|
76
|
+
if (!branchExists('staging')) {
|
|
77
|
+
console.error(
|
|
78
|
+
'Branch "staging" does not exist locally or on origin. Create it first:\n'
|
|
79
|
+
+ ' git checkout -b staging && git push -u origin staging',
|
|
80
|
+
);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Quality checks — no broken code may be committed
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
console.log('\nRunning quality checks...');
|
|
88
|
+
try {
|
|
89
|
+
runQualityChecks();
|
|
90
|
+
} catch {
|
|
91
|
+
console.error('\nQuality checks failed. Fix all issues before releasing.');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log('✅ Quality checks passed');
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Fix formatting if available
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
try {
|
|
100
|
+
runScript('fix');
|
|
101
|
+
} catch {
|
|
102
|
+
// fix may not exist in all packages
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Commit formatting fixes (if any)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
git('add', '-A');
|
|
109
|
+
const hasStagedChanges = gitRead('diff', '--cached', '--name-only') !== '';
|
|
110
|
+
if (hasStagedChanges) {
|
|
111
|
+
git('commit', '-m', `chore(staging): ${message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Merge into staging
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
console.log('\nMerging main into staging...');
|
|
118
|
+
git('checkout', 'staging');
|
|
119
|
+
git('merge', '--ff-only', 'main');
|
|
120
|
+
git('push', 'origin', 'staging');
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Return to main and sync
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
git('checkout', 'main');
|
|
126
|
+
git('push', 'origin', 'main');
|
|
127
|
+
|
|
128
|
+
console.log('\n✅ Changes deployed to staging');
|
|
129
|
+
console.log(` "${message}"`);
|
|
130
|
+
console.log('Run flow-ship <patch|minor|major> when ready to promote to production.');
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flow-ship: Version and promote staging to production following the GitLab workflow.
|
|
4
|
+
*
|
|
5
|
+
* Usage: flow-ship <patch|minor|major>
|
|
6
|
+
*
|
|
7
|
+
* GitLab workflow:
|
|
8
|
+
* staging → (version bump commit) → production
|
|
9
|
+
*
|
|
10
|
+
* Versioning is intentionally deferred to this step so that version numbers
|
|
11
|
+
* only ever correspond to production-verified code.
|
|
12
|
+
*
|
|
13
|
+
* This script:
|
|
14
|
+
* 1. Ensures the current branch is main and the working tree is clean
|
|
15
|
+
* 2. Verifies staging is ahead of production (there is something to ship)
|
|
16
|
+
* 3. Runs quality checks on the staging branch
|
|
17
|
+
* 4. Bumps the version in package.json
|
|
18
|
+
* 5. Collects change descriptions from git log between production and staging
|
|
19
|
+
* 6. Updates CHANGELOG.md
|
|
20
|
+
* 7. Commits the version bump on staging
|
|
21
|
+
* 8. Merges staging into production (fast-forward) and pushes
|
|
22
|
+
* 9. Merges production back into main to carry the version commit forward
|
|
23
|
+
* 10. Syncs staging with main so the next flow-release can ff-merge cleanly
|
|
24
|
+
* 11. Returns to main
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
branchExists,
|
|
31
|
+
bumpVersion,
|
|
32
|
+
confirm,
|
|
33
|
+
git,
|
|
34
|
+
gitRead,
|
|
35
|
+
insertChangelog,
|
|
36
|
+
runQualityChecks,
|
|
37
|
+
runScript,
|
|
38
|
+
} from './utils.js';
|
|
39
|
+
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const bumpType = args[0];
|
|
42
|
+
|
|
43
|
+
if (!bumpType || !['patch', 'minor', 'major'].includes(bumpType)) {
|
|
44
|
+
console.error('Usage: flow-ship <patch|minor|major>');
|
|
45
|
+
console.error('Example: flow-ship minor');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Guard: must be on main
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const currentBranch = gitRead('branch', '--show-current');
|
|
53
|
+
if (currentBranch !== 'main') {
|
|
54
|
+
console.error(`Must be on the main branch to ship. Currently on: ${currentBranch}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Guard: working tree must be clean
|
|
59
|
+
const status = gitRead('status', '--porcelain');
|
|
60
|
+
if (status) {
|
|
61
|
+
console.error('Working directory has uncommitted changes. Commit or stash them first.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Fetch and verify both branches exist
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
console.log('\nFetching latest state from origin...');
|
|
69
|
+
git('fetch', 'origin');
|
|
70
|
+
|
|
71
|
+
if (!branchExists('staging')) {
|
|
72
|
+
console.error(
|
|
73
|
+
'Branch "staging" does not exist. Create it first:\n'
|
|
74
|
+
+ ' git checkout -b staging && git push -u origin staging',
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!branchExists('production')) {
|
|
80
|
+
console.error(
|
|
81
|
+
'Branch "production" does not exist. Create it first:\n'
|
|
82
|
+
+ ' git checkout -b production && git push -u origin production',
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Verify staging has commits not yet in production
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
const ahead = gitRead('log', 'origin/production..origin/staging', '--oneline');
|
|
91
|
+
if (!ahead) {
|
|
92
|
+
console.error('staging is already in sync with production. Nothing to ship.');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('\nCommits to be shipped to production:');
|
|
97
|
+
console.log(ahead);
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Read package name for the confirmation prompt
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
const packageJsonPath = join(process.cwd(), 'package.json');
|
|
103
|
+
if (!existsSync(packageJsonPath)) {
|
|
104
|
+
console.error('No package.json found in current directory.');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const { name: packageName } = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
|
|
108
|
+
name: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (!confirm(`\nShip ${packageName} as a ${bumpType} release?`)) {
|
|
112
|
+
console.log('Aborted.');
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Quality checks on staging
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
console.log('\nChecking out staging for quality checks...');
|
|
120
|
+
git('checkout', 'staging');
|
|
121
|
+
git('pull', '--ff-only', 'origin', 'staging');
|
|
122
|
+
|
|
123
|
+
console.log('\nRunning quality checks on staging...');
|
|
124
|
+
try {
|
|
125
|
+
runQualityChecks();
|
|
126
|
+
} catch {
|
|
127
|
+
console.error('\nQuality checks failed on staging. Fix issues before shipping.');
|
|
128
|
+
git('checkout', 'main');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
console.log('✅ Quality checks passed');
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Read version from staging package.json and bump
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
|
|
137
|
+
name: string;
|
|
138
|
+
version: string;
|
|
139
|
+
};
|
|
140
|
+
const oldVersion = packageJson.version;
|
|
141
|
+
const newVersion = bumpVersion(oldVersion, bumpType);
|
|
142
|
+
|
|
143
|
+
console.log(`\nBumping ${packageName}: ${oldVersion} → ${newVersion}`);
|
|
144
|
+
|
|
145
|
+
writeFileSync(
|
|
146
|
+
packageJsonPath,
|
|
147
|
+
`${JSON.stringify({ ...packageJson, version: newVersion }, null, 2)}\n`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Derive change descriptions from git log between production and staging
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
const logOutput = gitRead('log', 'origin/production..HEAD', '--pretty=format:%s');
|
|
154
|
+
const changeLines = logOutput
|
|
155
|
+
? logOutput
|
|
156
|
+
.split('\n')
|
|
157
|
+
.map((s) => s.trim())
|
|
158
|
+
.filter((s) => s.length > 0)
|
|
159
|
+
: [];
|
|
160
|
+
|
|
161
|
+
const changeBody =
|
|
162
|
+
changeLines.length > 0
|
|
163
|
+
? changeLines.map((c) => `- ${c.length > 98 ? `${c.slice(0, 95)}...` : c}`).join('\n')
|
|
164
|
+
: `${bumpType} release`;
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Update CHANGELOG.md
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
const changelogPath = join(process.cwd(), 'CHANGELOG.md');
|
|
170
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
171
|
+
insertChangelog(changelogPath, `## [${newVersion}] - ${today}\n\n${changeBody}\n`);
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Fix formatting if available
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
try {
|
|
177
|
+
runScript('fix');
|
|
178
|
+
} catch {
|
|
179
|
+
// fix may not exist in all packages
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Commit version bump on staging
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
git('add', '-A');
|
|
186
|
+
git('commit', '-m', `chore(release): ${packageName}@${newVersion}`, '-m', changeBody);
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Merge staging → production
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
console.log('\nMerging staging into production...');
|
|
192
|
+
git('checkout', 'production');
|
|
193
|
+
git('pull', '--ff-only', 'origin', 'production');
|
|
194
|
+
git('merge', '--ff-only', 'staging');
|
|
195
|
+
git('push', 'origin', 'production');
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Bring version commit back to main
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
console.log('\nSyncing version commit back to main...');
|
|
201
|
+
git('checkout', 'main');
|
|
202
|
+
git('pull', '--ff-only', 'origin', 'main');
|
|
203
|
+
git('merge', '--ff-only', 'production');
|
|
204
|
+
git('push', 'origin', 'main');
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Sync staging with main so the next flow-release can ff-merge cleanly
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
git('checkout', 'staging');
|
|
210
|
+
git('merge', '--ff-only', 'main');
|
|
211
|
+
git('push', 'origin', 'staging');
|
|
212
|
+
git('checkout', 'main');
|
|
213
|
+
|
|
214
|
+
console.log(`\n✅ Shipped ${packageName}@${newVersion} to production`);
|
|
215
|
+
console.log('You are on main and ready to keep working.');
|