@proteinjs/build 2.0.2 → 2.2.0
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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/dist/src/buildWorkspace.d.ts +6 -1
- package/dist/src/buildWorkspace.d.ts.map +1 -1
- package/dist/src/buildWorkspace.js +12 -3
- package/dist/src/buildWorkspace.js.map +1 -1
- package/dist/src/mergeToMain.d.ts +32 -0
- package/dist/src/mergeToMain.d.ts.map +1 -0
- package/dist/src/mergeToMain.js +274 -0
- package/dist/src/mergeToMain.js.map +1 -0
- package/dist/src/versionWorkspace.d.ts.map +1 -1
- package/dist/src/versionWorkspace.js +117 -50
- package/dist/src/versionWorkspace.js.map +1 -1
- package/dist/test/parseMergeToMainSpec.test.d.ts +2 -0
- package/dist/test/parseMergeToMainSpec.test.d.ts.map +1 -0
- package/dist/test/parseMergeToMainSpec.test.js +40 -0
- package/dist/test/parseMergeToMainSpec.test.js.map +1 -0
- package/package.json +3 -2
- package/src/buildWorkspace.ts +12 -2
- package/src/mergeToMain.ts +187 -0
- package/src/versionWorkspace.ts +96 -16
- package/test/parseMergeToMainSpec.test.ts +43 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { PackageUtil, cmd, LogColorWrapper } from '@proteinjs/util-node';
|
|
4
|
+
import { Logger } from '@proteinjs/logger';
|
|
5
|
+
import { primaryLogColor, secondaryLogColor } from './logColors';
|
|
6
|
+
|
|
7
|
+
const cw = new LogColorWrapper();
|
|
8
|
+
const logger = new Logger({ name: cw.color('workspace:', primaryLogColor) + cw.color('merge', secondaryLogColor) });
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Opt-in pre-phase for `version-workspace`: merge feature-branch work into `main` in each leaf git
|
|
12
|
+
* repo BEFORE versioning, so the release runs on main. Without the flag, behavior is unchanged —
|
|
13
|
+
* the workspace versions in place on whatever branch each repo is on (the "stay on feature
|
|
14
|
+
* branches and just bump deps" mode).
|
|
15
|
+
*
|
|
16
|
+
* version-workspace --merge-to-main
|
|
17
|
+
* Every leaf repo whose current branch != main merges its current branch HEAD into main.
|
|
18
|
+
*
|
|
19
|
+
* version-workspace --merge-to-main=chat:17dda73,thought:ffdc105
|
|
20
|
+
* Only the named repos (matched by git-repo directory name), each merged at the PINNED sha —
|
|
21
|
+
* work pushed to the feature branch after the pin simply isn't in this release. A bare name
|
|
22
|
+
* (no `:sha`) merges that repo's current branch HEAD.
|
|
23
|
+
*
|
|
24
|
+
* Also accepted via env: VERSION_WORKSPACE_MERGE_TO_MAIN (same syntax; `1`/`true` = bare mode).
|
|
25
|
+
*
|
|
26
|
+
* Semantics per repo: fetch; checkout main; sync with origin/main FAST-FORWARD-ONLY (never rebase
|
|
27
|
+
* — local main legitimately holds an unpushed merge commit on resume, and a rebase would
|
|
28
|
+
* linearize it; true local/origin divergence stops loudly); merge the sha with a merge commit
|
|
29
|
+
* (`--no-ff`). Already-merged shas are skipped (idempotent — safe to re-run after fixing a
|
|
30
|
+
* conflict). A conflict stops the run with the repo + conflicted files named, leaving the merge in
|
|
31
|
+
* progress in that repo for resolution; re-running continues past it. Repos are LEFT ON MAIN
|
|
32
|
+
* afterward (the feature branch itself is never modified); the workspace root/metarepos are never
|
|
33
|
+
* touched by this phase.
|
|
34
|
+
*/
|
|
35
|
+
export type MergeToMainSpec = { enabled: boolean; pins: Map<string, string | undefined> };
|
|
36
|
+
|
|
37
|
+
export function parseMergeToMainSpec(args: string[], envValue?: string): MergeToMainSpec {
|
|
38
|
+
const pins = new Map<string, string | undefined>();
|
|
39
|
+
let enabled = false;
|
|
40
|
+
|
|
41
|
+
const consume = (value: string) => {
|
|
42
|
+
enabled = true;
|
|
43
|
+
if (value === '' || value === 'true' || value === '1') {
|
|
44
|
+
return; // bare mode: all repos at branch HEAD
|
|
45
|
+
}
|
|
46
|
+
for (const entry of value.split(',')) {
|
|
47
|
+
const trimmed = entry.trim();
|
|
48
|
+
if (!trimmed) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const [repo, sha] = trimmed.split(':').map((s) => s.trim());
|
|
52
|
+
pins.set(repo, sha || undefined);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (const arg of args) {
|
|
57
|
+
if (arg === '--merge-to-main') {
|
|
58
|
+
consume('');
|
|
59
|
+
} else {
|
|
60
|
+
const match = arg.match(/^--merge-to-main=(.*)$/);
|
|
61
|
+
if (match) {
|
|
62
|
+
consume(match[1]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!enabled && envValue) {
|
|
67
|
+
consume(envValue);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { enabled, pins };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function mergeToMain(workspacePath: string, spec: MergeToMainSpec, planOnly: boolean): Promise<void> {
|
|
74
|
+
if (!spec.enabled) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const repoRoots = await leafRepoRoots(workspacePath);
|
|
79
|
+
for (const repoRoot of repoRoots) {
|
|
80
|
+
const repoName = path.basename(repoRoot);
|
|
81
|
+
const pinned = spec.pins.size > 0;
|
|
82
|
+
if (pinned && !spec.pins.has(repoName)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const branch = await git(repoRoot, 'rev-parse --abbrev-ref HEAD');
|
|
86
|
+
const sha = spec.pins.get(repoName) ?? (branch === 'main' ? undefined : await git(repoRoot, 'rev-parse HEAD'));
|
|
87
|
+
if (!sha) {
|
|
88
|
+
// Bare mode and already on main with no pin: nothing to merge.
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (planOnly) {
|
|
93
|
+
logger.info({
|
|
94
|
+
message: `(${cw.color(repoName)}) would merge ${sha.slice(0, 9)} (${branch}) into main. NOTE: plan-only bump levels below reflect PRE-merge state; run with --dry-run after merging for post-merge planning.`,
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await cmd('git', ['fetch', '-q'], { cwd: repoRoot }, { logPrefix: `[${cw.color(repoName)}] ` });
|
|
100
|
+
try {
|
|
101
|
+
await git(repoRoot, `cat-file -e ${sha}^{commit}`);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(`(${repoName}) --merge-to-main sha ${sha} is not a commit in this repo`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (branch !== 'main') {
|
|
107
|
+
await cmd('git', ['checkout', 'main'], { cwd: repoRoot }, { logPrefix: `[${cw.color(repoName)}] ` });
|
|
108
|
+
}
|
|
109
|
+
// Sync local main with origin FAST-FORWARD-ONLY — never rebase: after a prior run of this
|
|
110
|
+
// phase, local main legitimately holds an UNPUSHED MERGE COMMIT, and a rebase would linearize
|
|
111
|
+
// it (replaying every branch commit, usually into conflicts). Ahead-of-origin is the normal
|
|
112
|
+
// resume state and is fine; true divergence (local merge AND origin moved) is abnormal —
|
|
113
|
+
// stop loudly rather than guess.
|
|
114
|
+
try {
|
|
115
|
+
await cmd(
|
|
116
|
+
'git',
|
|
117
|
+
['merge', '--ff-only', 'origin/main'],
|
|
118
|
+
{ cwd: repoRoot },
|
|
119
|
+
{ logPrefix: `[${cw.color(repoName)}] ` }
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
const originIsAncestor = await git(repoRoot, 'merge-base --is-ancestor origin/main HEAD')
|
|
123
|
+
.then(() => true)
|
|
124
|
+
.catch(() => false);
|
|
125
|
+
if (!originIsAncestor) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`(${repoName}) local main and origin/main have DIVERGED (local holds unpushed commits AND origin moved). ` +
|
|
128
|
+
`Reconcile manually (e.g. merge origin/main into main), then re-run.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
logger.info({
|
|
132
|
+
message: `(${cw.color(repoName)}) local main is ahead of origin (unpushed merge from a prior run) — continuing`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const alreadyMerged = await git(repoRoot, `merge-base --is-ancestor ${sha} HEAD`)
|
|
137
|
+
.then(() => true)
|
|
138
|
+
.catch(() => false);
|
|
139
|
+
if (alreadyMerged) {
|
|
140
|
+
logger.info({ message: `(${cw.color(repoName)}) ${sha.slice(0, 9)} already on main — skipping merge` });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
logger.info({ message: `(${cw.color(repoName)}) merging ${sha.slice(0, 9)} (${branch}) into main` });
|
|
145
|
+
try {
|
|
146
|
+
await cmd(
|
|
147
|
+
'git',
|
|
148
|
+
[
|
|
149
|
+
'merge',
|
|
150
|
+
'--no-ff',
|
|
151
|
+
'-m',
|
|
152
|
+
`merge: ${branch} @ ${sha.slice(0, 9)} -> main (version-workspace --merge-to-main)`,
|
|
153
|
+
sha,
|
|
154
|
+
],
|
|
155
|
+
{ cwd: repoRoot },
|
|
156
|
+
{ logPrefix: `[${cw.color(repoName)}] ` }
|
|
157
|
+
);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const conflicted = await git(repoRoot, 'diff --name-only --diff-filter=U').catch(() => '');
|
|
160
|
+
throw new Error(
|
|
161
|
+
`(${repoName}) merge of ${sha.slice(0, 9)} into main has conflicts: [${conflicted.split('\n').filter(Boolean).join(', ')}]. ` +
|
|
162
|
+
`Resolve + commit the merge in ${repoRoot}, then re-run — already-merged repos are skipped.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Unique git-repo roots that directly contain workspace packages — never the workspace root. */
|
|
169
|
+
async function leafRepoRoots(workspacePath: string): Promise<string[]> {
|
|
170
|
+
const { packageMap, sortedPackageNames } = await PackageUtil.getWorkspaceMetadata(workspacePath);
|
|
171
|
+
const roots = new Set<string>();
|
|
172
|
+
for (const packageName of sortedPackageNames) {
|
|
173
|
+
const localPackage = packageMap[packageName];
|
|
174
|
+
const packageDir = path.dirname(localPackage.filePath);
|
|
175
|
+
const repoRoot = await git(packageDir, 'rev-parse --show-toplevel').catch(() => undefined);
|
|
176
|
+
if (repoRoot && path.resolve(repoRoot) !== path.resolve(workspacePath)) {
|
|
177
|
+
roots.add(repoRoot);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return Array.from(roots).sort();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function git(cwd: string, args: string): Promise<string> {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
exec(`git ${args}`, { cwd }, (error, stdout) => (error ? reject(error) : resolve(stdout.trim())));
|
|
186
|
+
});
|
|
187
|
+
}
|
package/src/versionWorkspace.ts
CHANGED
|
@@ -6,6 +6,7 @@ import semver from 'semver';
|
|
|
6
6
|
import { Commit } from './Github';
|
|
7
7
|
import { primaryLogColor, secondaryLogColor } from './logColors';
|
|
8
8
|
import { hasLintConfig } from './lintWorkspace';
|
|
9
|
+
import { mergeToMain, parseMergeToMainSpec } from './mergeToMain';
|
|
9
10
|
|
|
10
11
|
const cw = new LogColorWrapper();
|
|
11
12
|
const logger = new Logger({ name: cw.color('workspace:', primaryLogColor) + cw.color('version', secondaryLogColor) });
|
|
@@ -25,6 +26,13 @@ export async function versionWorkspace() {
|
|
|
25
26
|
}
|
|
26
27
|
const workspacePath = process.cwd();
|
|
27
28
|
await evictGitLocks(workspacePath);
|
|
29
|
+
|
|
30
|
+
// Opt-in pre-phase: merge feature-branch work into main per leaf repo before versioning (see
|
|
31
|
+
// mergeToMain.ts). Default (no flag) is unchanged: version in place on each repo's current
|
|
32
|
+
// branch. Repos this phase touches are left ON MAIN; feature branches are never modified.
|
|
33
|
+
const mergeSpec = parseMergeToMainSpec(process.argv.slice(2), process.env.VERSION_WORKSPACE_MERGE_TO_MAIN);
|
|
34
|
+
await mergeToMain(workspacePath, mergeSpec, planOnly);
|
|
35
|
+
|
|
28
36
|
const workspaceRootDirty = await isRepoDirty(workspacePath);
|
|
29
37
|
if (workspaceRootDirty) {
|
|
30
38
|
logger.info({ message: `> Workspace root is dirty, will skip pull/push for root repo` });
|
|
@@ -37,7 +45,17 @@ export async function versionWorkspace() {
|
|
|
37
45
|
|
|
38
46
|
const { packageMap, packageGraph, sortedPackageNames, workspaceToPackageMap } =
|
|
39
47
|
await PackageUtil.getWorkspaceMetadata(workspacePath);
|
|
40
|
-
const
|
|
48
|
+
const userSkippedPackages = getUserSkippedPackages();
|
|
49
|
+
if (userSkippedPackages.size > 0) {
|
|
50
|
+
logger.info({
|
|
51
|
+
message: `> Skipping packages this run (no version/build/publish; dependents keep their current dep versions): ${Array.from(
|
|
52
|
+
userSkippedPackages
|
|
53
|
+
)
|
|
54
|
+
.map((n) => cw.color(n))
|
|
55
|
+
.join(', ')}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const skippedPackages = ['root', 'typescript-parser', ...Array.from(userSkippedPackages)];
|
|
41
59
|
const filteredPackageNames = sortedPackageNames.filter((packageName) => {
|
|
42
60
|
const localPackage = packageMap[packageName];
|
|
43
61
|
return (
|
|
@@ -85,7 +103,12 @@ export async function versionWorkspace() {
|
|
|
85
103
|
const localPackage = packageMap[packageName];
|
|
86
104
|
const skipBumpingPackageVersion = isInFixedVersionWorkspace(localPackage);
|
|
87
105
|
const ownBump = skipBumpingPackageVersion ? undefined : commitBumps.get(packageName);
|
|
88
|
-
const dependenciesChanged = await applyDependencyVersionRewrites(
|
|
106
|
+
const dependenciesChanged = await applyDependencyVersionRewrites(
|
|
107
|
+
localPackage,
|
|
108
|
+
packageMap,
|
|
109
|
+
packageGraph,
|
|
110
|
+
userSkippedPackages
|
|
111
|
+
);
|
|
89
112
|
const cascadeBump: CommitBump | undefined = dependenciesChanged ? 'patch' : undefined;
|
|
90
113
|
const effectiveBump = maxBump(ownBump, cascadeBump);
|
|
91
114
|
|
|
@@ -98,11 +121,7 @@ export async function versionWorkspace() {
|
|
|
98
121
|
// In-memory version mutation runs in plan-only too so downstream
|
|
99
122
|
// packages see the right dep versions when we simulate cascades.
|
|
100
123
|
localPackage.packageJson.version = semver.inc(currentVersion, effectiveBump);
|
|
101
|
-
const sourceNote = ownBump
|
|
102
|
-
? cascadeBump
|
|
103
|
-
? `own+cascade, own=${ownBump}`
|
|
104
|
-
: `own=${ownBump}`
|
|
105
|
-
: 'dep cascade';
|
|
124
|
+
const sourceNote = ownBump ? (cascadeBump ? `own+cascade, own=${ownBump}` : `own=${ownBump}`) : 'dep cascade';
|
|
106
125
|
const planPrefix = planOnly ? 'would bump' : 'bumping';
|
|
107
126
|
logger.info({
|
|
108
127
|
message: `(${cw.color(packageName)}) ${planPrefix} version (${effectiveBump}; ${sourceNote}) from ${currentVersion} -> ${localPackage.packageJson.version}`,
|
|
@@ -208,6 +227,46 @@ function isPlanOnly() {
|
|
|
208
227
|
return false;
|
|
209
228
|
}
|
|
210
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Packages to exclude from this run entirely: not versioned, built, or
|
|
232
|
+
* published, and — critically — dependents' references to them are NOT
|
|
233
|
+
* rewritten to the on-disk version (see `applyDependencyVersionRewrites`).
|
|
234
|
+
*
|
|
235
|
+
* Use case: a package in the workspace has shipped a breaking new major
|
|
236
|
+
* (already published and checked out locally) but its consumers haven't been
|
|
237
|
+
* migrated yet. Skipping it lets the rest of the workspace version and deploy
|
|
238
|
+
* while consumers stay pinned to the version they were built against.
|
|
239
|
+
*
|
|
240
|
+
* Accepts `--skip=<name>[,<name>...]` (repeatable) or the
|
|
241
|
+
* VERSION_WORKSPACE_SKIP env var, e.g.:
|
|
242
|
+
* version-workspace --skip=@proteinjs/conversation
|
|
243
|
+
*/
|
|
244
|
+
function getUserSkippedPackages(): Set<string> {
|
|
245
|
+
const skipped = new Set<string>();
|
|
246
|
+
const args = process.argv.slice(2);
|
|
247
|
+
for (const arg of args) {
|
|
248
|
+
const match = arg.match(/^--skip=(.+)$/);
|
|
249
|
+
if (match) {
|
|
250
|
+
match[1]
|
|
251
|
+
.split(',')
|
|
252
|
+
.map((name) => name.trim())
|
|
253
|
+
.filter((name) => name.length > 0)
|
|
254
|
+
.forEach((name) => skipped.add(name));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const envFlag = process.env.VERSION_WORKSPACE_SKIP;
|
|
259
|
+
if (envFlag) {
|
|
260
|
+
envFlag
|
|
261
|
+
.split(',')
|
|
262
|
+
.map((name) => name.trim())
|
|
263
|
+
.filter((name) => name.length > 0)
|
|
264
|
+
.forEach((name) => skipped.add(name));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return skipped;
|
|
268
|
+
}
|
|
269
|
+
|
|
211
270
|
function isInFixedVersionWorkspace(localPackage: LocalPackage) {
|
|
212
271
|
return (
|
|
213
272
|
localPackage.workspace &&
|
|
@@ -280,7 +339,8 @@ async function pullWorkspace(workspacePath: string, skipRootRepo = false) {
|
|
|
280
339
|
async function applyDependencyVersionRewrites(
|
|
281
340
|
localPackage: LocalPackage,
|
|
282
341
|
packageMap: LocalPackageMap,
|
|
283
|
-
packageGraph: any
|
|
342
|
+
packageGraph: any,
|
|
343
|
+
userSkippedPackages: Set<string> = new Set()
|
|
284
344
|
): Promise<boolean> {
|
|
285
345
|
const localDependencies = packageGraph.successors(localPackage.name);
|
|
286
346
|
if (!localDependencies || localDependencies.length == 0) {
|
|
@@ -306,6 +366,16 @@ async function applyDependencyVersionRewrites(
|
|
|
306
366
|
continue;
|
|
307
367
|
}
|
|
308
368
|
|
|
369
|
+
if (userSkippedPackages.has(localDependency)) {
|
|
370
|
+
// The dep was skipped via --skip: leave this package's reference at its
|
|
371
|
+
// current (published, known-compatible) version instead of rewriting to
|
|
372
|
+
// the on-disk version of the skipped package.
|
|
373
|
+
logger.info({
|
|
374
|
+
message: `(${cw.color(localPackage.name)}) keeping dependency version of ${cw.color(localDependency)} at ${currentDependencyVersion.prefix ?? ''}${currentDependencyVersion.version} (skipped via --skip; on-disk is ${localDependencyVersion})`,
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
309
379
|
const newDependencyVersion: DependencyVersion = {
|
|
310
380
|
prefix: currentDependencyVersion.prefix,
|
|
311
381
|
version: localDependencyVersion,
|
|
@@ -326,10 +396,7 @@ async function applyDependencyVersionRewrites(
|
|
|
326
396
|
* This is the input to `computeCommitLeaves` and to the per-package bump-level
|
|
327
397
|
* combine in the main loop.
|
|
328
398
|
*/
|
|
329
|
-
async function scanCommitBumps(
|
|
330
|
-
packageNames: string[],
|
|
331
|
-
packageMap: LocalPackageMap
|
|
332
|
-
): Promise<Map<string, CommitBump>> {
|
|
399
|
+
async function scanCommitBumps(packageNames: string[], packageMap: LocalPackageMap): Promise<Map<string, CommitBump>> {
|
|
333
400
|
const result = new Map<string, CommitBump>();
|
|
334
401
|
for (const packageName of packageNames) {
|
|
335
402
|
const localPackage = packageMap[packageName];
|
|
@@ -845,7 +912,13 @@ async function publish(localPackage: LocalPackage) {
|
|
|
845
912
|
}
|
|
846
913
|
|
|
847
914
|
await retryOnNetworkError(
|
|
848
|
-
() =>
|
|
915
|
+
() =>
|
|
916
|
+
cmd(
|
|
917
|
+
'npm',
|
|
918
|
+
args,
|
|
919
|
+
{ cwd: packageDir, env: { ...process.env } },
|
|
920
|
+
{ logPrefix: `[${cw.color(localPackage.name)}] ` }
|
|
921
|
+
),
|
|
849
922
|
localPackage.name,
|
|
850
923
|
3,
|
|
851
924
|
15_000
|
|
@@ -991,7 +1064,10 @@ export async function classifyUnpushedCommits(dir: string): Promise<CommitBump |
|
|
|
991
1064
|
resolve(undefined);
|
|
992
1065
|
return;
|
|
993
1066
|
}
|
|
994
|
-
const commits = stdout
|
|
1067
|
+
const commits = stdout
|
|
1068
|
+
.split('\x00')
|
|
1069
|
+
.map((s) => s.trim())
|
|
1070
|
+
.filter((s) => s.length > 0);
|
|
995
1071
|
let result: CommitBump | undefined;
|
|
996
1072
|
for (const commit of commits) {
|
|
997
1073
|
result = maxBump(result, classifyCommitMessage(commit));
|
|
@@ -1048,8 +1124,12 @@ export async function evictGitLocks(workspacePath: string) {
|
|
|
1048
1124
|
function isNetworkError(error: any): boolean {
|
|
1049
1125
|
const output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
|
|
1050
1126
|
return (
|
|
1051
|
-
/ECONNRESET/i.test(output) ||
|
|
1052
|
-
/
|
|
1127
|
+
/ECONNRESET/i.test(output) ||
|
|
1128
|
+
/ETIMEDOUT/i.test(output) ||
|
|
1129
|
+
/ENOTFOUND/i.test(output) ||
|
|
1130
|
+
/EAI_AGAIN/i.test(output) ||
|
|
1131
|
+
/ECONNREFUSED/i.test(output) ||
|
|
1132
|
+
/socket hang up/i.test(output) ||
|
|
1053
1133
|
/network/i.test(output)
|
|
1054
1134
|
);
|
|
1055
1135
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseMergeToMainSpec } from '../src/mergeToMain';
|
|
2
|
+
|
|
3
|
+
describe('parseMergeToMainSpec', () => {
|
|
4
|
+
it('is disabled with no flag and no env', () => {
|
|
5
|
+
expect(parseMergeToMainSpec([]).enabled).toBe(false);
|
|
6
|
+
expect(parseMergeToMainSpec(['--dry-run']).enabled).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('bare flag enables merge for all repos at branch HEAD', () => {
|
|
10
|
+
const spec = parseMergeToMainSpec(['--merge-to-main']);
|
|
11
|
+
expect(spec.enabled).toBe(true);
|
|
12
|
+
expect(spec.pins.size).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('pinned form names repos with shas', () => {
|
|
16
|
+
const spec = parseMergeToMainSpec(['--merge-to-main=chat:17dda73,thought:ffdc105']);
|
|
17
|
+
expect(spec.enabled).toBe(true);
|
|
18
|
+
expect(spec.pins.get('chat')).toBe('17dda73');
|
|
19
|
+
expect(spec.pins.get('thought')).toBe('ffdc105');
|
|
20
|
+
expect(spec.pins.has('flow')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('bare repo name (no sha) pins the repo at its branch HEAD', () => {
|
|
24
|
+
const spec = parseMergeToMainSpec(['--merge-to-main=chat,thought:abc123']);
|
|
25
|
+
expect(spec.pins.has('chat')).toBe(true);
|
|
26
|
+
expect(spec.pins.get('chat')).toBeUndefined();
|
|
27
|
+
expect(spec.pins.get('thought')).toBe('abc123');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('repeatable flags accumulate pins', () => {
|
|
31
|
+
const spec = parseMergeToMainSpec(['--merge-to-main=chat:a', '--merge-to-main=flow:b']);
|
|
32
|
+
expect(spec.pins.get('chat')).toBe('a');
|
|
33
|
+
expect(spec.pins.get('flow')).toBe('b');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('env var enables when no flag is present; flag wins over env', () => {
|
|
37
|
+
expect(parseMergeToMainSpec([], '1').enabled).toBe(true);
|
|
38
|
+
expect(parseMergeToMainSpec([], 'chat:abc').pins.get('chat')).toBe('abc');
|
|
39
|
+
const flagWins = parseMergeToMainSpec(['--merge-to-main=flow:x'], 'chat:abc');
|
|
40
|
+
expect(flagWins.pins.has('chat')).toBe(false);
|
|
41
|
+
expect(flagWins.pins.get('flow')).toBe('x');
|
|
42
|
+
});
|
|
43
|
+
});
|