@i-santos/create-package-starter 1.5.0-beta.11 → 1.5.0-beta.13

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.
Files changed (2) hide show
  1. package/lib/run.js +206 -52
  2. package/package.json +1 -1
package/lib/run.js CHANGED
@@ -45,7 +45,7 @@ function usage() {
45
45
  ' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
46
46
  ' create-package-starter setup-beta [--dir <directory>] [--repo <owner/repo>] [--beta-branch <branch>] [--default-branch <branch>] [--release-auth github-token|pat|app|manual-trigger] [--force] [--dry-run] [--yes]',
47
47
  ' create-package-starter open-pr [--repo <owner/repo>] [--base <branch>] [--head <branch>] [--title <text>] [--body <text>] [--body-file <path>] [--template <path>] [--draft] [--auto-merge] [--watch-checks] [--check-timeout <minutes>] [--yes] [--dry-run]',
48
- ' create-package-starter release-cycle [--repo <owner/repo>] [--mode auto|open-pr|publish] [--phase code|full] [--track auto|beta|stable] [--promote-stable] [--promote-type patch|minor|major] [--promote-summary <text>] [--head <branch>] [--base <branch>] [--title <text>] [--body-file <path>] [--update-pr-description] [--draft] [--auto-merge] [--watch-checks] [--check-timeout <minutes>] [--confirm-merges] [--merge-when-green] [--merge-method squash|merge|rebase] [--wait-release-pr] [--release-pr-timeout <minutes>] [--merge-release-pr] [--verify-npm] [--confirm-cleanup] [--sync-base auto|rebase|merge|off] [--no-resume] [--no-cleanup] [--yes] [--dry-run]',
48
+ ' create-package-starter release-cycle [--repo <owner/repo>] [--mode auto|open-pr|publish] [--phase code|full] [--track auto|beta|stable] [--promote-stable] [--promote-type patch|minor|major] [--promote-summary <text>] [--head <branch>] [--base <branch>] [--title <text>] [--body-file <path>] [--npm-package <name>] [--update-pr-description] [--draft] [--auto-merge] [--watch-checks] [--check-timeout <minutes>] [--confirm-merges] [--merge-when-green] [--merge-method squash|merge|rebase] [--wait-release-pr] [--release-pr-timeout <minutes>] [--merge-release-pr] [--verify-npm] [--confirm-cleanup] [--sync-base auto|rebase|merge|off] [--no-resume] [--no-cleanup] [--yes] [--dry-run]',
49
49
  ' create-package-starter promote-stable [--dir <directory>] [--type patch|minor|major] [--summary <text>] [--dry-run]',
50
50
  ' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
51
51
  '',
@@ -561,6 +561,7 @@ function parseReleaseCycleArgs(argv) {
561
561
  base: '',
562
562
  title: '',
563
563
  bodyFile: '',
564
+ npmPackages: [],
564
565
  updatePrDescription: false,
565
566
  draft: false,
566
567
  autoMerge: true,
@@ -645,6 +646,12 @@ function parseReleaseCycleArgs(argv) {
645
646
  continue;
646
647
  }
647
648
 
649
+ if (token === '--npm-package') {
650
+ args.npmPackages.push(parseValueFlag(argv, i, '--npm-package'));
651
+ i += 1;
652
+ continue;
653
+ }
654
+
648
655
  if (token === '--update-pr-description') {
649
656
  args.updatePrDescription = true;
650
657
  continue;
@@ -1903,6 +1910,9 @@ function findReleasePrs(repo, deps, options = {}) {
1903
1910
 
1904
1911
  function waitForReleasePr(repo, timeoutMinutes, deps, options = {}) {
1905
1912
  const expectedBase = options.expectedBase || '';
1913
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
1914
+ const progressIntervalMs = Number.isFinite(options.progressIntervalMs) ? options.progressIntervalMs : 30_000;
1915
+ let lastProgressAt = 0;
1906
1916
  const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
1907
1917
  while (nowMs(deps) <= timeoutAt) {
1908
1918
  const releasePrs = findReleasePrs(repo, deps, { expectedBase });
@@ -1913,6 +1923,12 @@ function waitForReleasePr(repo, timeoutMinutes, deps, options = {}) {
1913
1923
  throw new Error(`Multiple release PRs detected: ${releasePrs.map((item) => item.url).join(', ')}`);
1914
1924
  }
1915
1925
 
1926
+ const now = nowMs(deps);
1927
+ if (onProgress && (lastProgressAt === 0 || now - lastProgressAt >= progressIntervalMs)) {
1928
+ lastProgressAt = now;
1929
+ onProgress();
1930
+ }
1931
+
1916
1932
  waitForNextPoll(timeoutAt, 5000, deps);
1917
1933
  }
1918
1934
 
@@ -1983,51 +1999,145 @@ function waitForPromotionPr(repo, timeoutMinutes, deps) {
1983
1999
  throw new Error(`Timed out waiting for promotion PR after ${timeoutMinutes} minutes.`);
1984
2000
  }
1985
2001
 
1986
- function getRemotePackageVersion(repo, ref, deps) {
1987
- const endpoint = `/repos/${repo}/contents/package.json?ref=${encodeURIComponent(ref)}`;
2002
+ function encodePathForGitHubContent(pathValue) {
2003
+ return String(pathValue || '')
2004
+ .split('/')
2005
+ .filter(Boolean)
2006
+ .map((segment) => encodeURIComponent(segment))
2007
+ .join('/');
2008
+ }
2009
+
2010
+ function getRemotePackageVersionFromPath(repo, ref, packageJsonPath, deps) {
2011
+ const safePath = encodePathForGitHubContent(packageJsonPath);
2012
+ const endpoint = `/repos/${repo}/contents/${safePath}?ref=${encodeURIComponent(ref)}`;
1988
2013
  const contentResponse = ghApiJson(deps, 'GET', endpoint);
1989
2014
  if (!contentResponse.content) {
1990
- throw new Error(`Could not read package.json content from ${repo}@${ref}.`);
2015
+ throw new Error(`Could not read ${packageJsonPath} content from ${repo}@${ref}.`);
1991
2016
  }
1992
2017
 
1993
2018
  const decoded = Buffer.from(String(contentResponse.content).replace(/\n/g, ''), 'base64').toString('utf8');
1994
2019
  const parsed = parseJsonSafely(decoded, {});
1995
2020
  if (!parsed.name || !parsed.version) {
1996
- throw new Error(`package.json from ${repo}@${ref} must include name and version.`);
2021
+ throw new Error(`${packageJsonPath} from ${repo}@${ref} must include name and version.`);
1997
2022
  }
1998
2023
 
1999
2024
  return {
2000
2025
  name: parsed.name,
2001
- version: parsed.version
2026
+ version: parsed.version,
2027
+ packageJsonPath
2002
2028
  };
2003
2029
  }
2004
2030
 
2005
- function validateNpmPublishedVersionAndTag(packageName, expectedVersion, expectedTag, timeoutMinutes, deps) {
2031
+ function getRemotePackageVersion(repo, ref, deps) {
2032
+ return getRemotePackageVersionFromPath(repo, ref, 'package.json', deps);
2033
+ }
2034
+
2035
+ function listPullRequestFiles(repo, prNumber, deps) {
2036
+ const files = ghApiJson(deps, 'GET', `/repos/${repo}/pulls/${prNumber}/files?per_page=100`);
2037
+ return Array.isArray(files) ? files : [];
2038
+ }
2039
+
2040
+ function resolveExpectedNpmPackages(repo, releasePrNumber, targetRef, expectedTag, args, deps) {
2041
+ const explicitPackages = Array.isArray(args.npmPackages) ? args.npmPackages : [];
2042
+ const files = listPullRequestFiles(repo, releasePrNumber, deps);
2043
+ const packageJsonPaths = [...new Set(
2044
+ files
2045
+ .filter((file) => file && file.status !== 'removed' && typeof file.filename === 'string')
2046
+ .map((file) => file.filename)
2047
+ .filter((fileName) => fileName.endsWith('/package.json') || fileName === 'package.json')
2048
+ )];
2049
+
2050
+ const fallbackPaths = packageJsonPaths.length > 0 ? packageJsonPaths : ['package.json'];
2051
+ const resolved = fallbackPaths
2052
+ .map((filePath) => getRemotePackageVersionFromPath(repo, targetRef, filePath, deps))
2053
+ .filter((pkg) => pkg && pkg.name && pkg.version);
2054
+
2055
+ const byName = new Map();
2056
+ for (const pkg of resolved) {
2057
+ if (!byName.has(pkg.name)) {
2058
+ byName.set(pkg.name, pkg);
2059
+ }
2060
+ }
2061
+
2062
+ if (explicitPackages.length === 0) {
2063
+ return [...byName.values()];
2064
+ }
2065
+
2066
+ const filtered = [];
2067
+ const missing = [];
2068
+ for (const pkgName of explicitPackages) {
2069
+ if (byName.has(pkgName)) {
2070
+ filtered.push(byName.get(pkgName));
2071
+ } else {
2072
+ missing.push(pkgName);
2073
+ }
2074
+ }
2075
+
2076
+ if (missing.length > 0) {
2077
+ throw new Error(
2078
+ [
2079
+ `Could not resolve expected npm package(s) from release PR #${releasePrNumber}: ${missing.join(', ')}`,
2080
+ `Resolved package names: ${[...byName.keys()].join(', ') || 'none'}`,
2081
+ `Expected tag: ${expectedTag}`
2082
+ ].join('\n')
2083
+ );
2084
+ }
2085
+
2086
+ return filtered;
2087
+ }
2088
+
2089
+ function validateNpmPublishedPackages(packageTargets, expectedTag, timeoutMinutes, deps) {
2006
2090
  const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
2007
- let lastObservedVersion = '';
2008
- let lastObservedTagVersion = '';
2091
+ const observations = {};
2009
2092
  const isStableTrack = expectedTag === 'latest';
2093
+ const onProgress = typeof deps.onNpmValidationProgress === 'function' ? deps.onNpmValidationProgress : null;
2094
+ let lastProgressAt = 0;
2010
2095
 
2011
2096
  while (nowMs(deps) <= timeoutAt) {
2012
- const versionResult = deps.exec('npm', ['view', packageName, 'version', '--json']);
2013
- const tagsResult = deps.exec('npm', ['view', packageName, 'dist-tags', '--json']);
2014
- if (versionResult.status === 0 && tagsResult.status === 0) {
2015
- const observedVersion = String(parseJsonSafely(versionResult.stdout || '""', '') || '');
2016
- const tags = parseJsonSafely(tagsResult.stdout || '{}', {});
2017
- const observedTagVersion = tags && tags[expectedTag] ? String(tags[expectedTag]) : '';
2018
- lastObservedVersion = observedVersion;
2019
- lastObservedTagVersion = observedTagVersion;
2020
-
2021
- // npm "version" reflects the default dist-tag (usually latest), so beta verification
2022
- // must rely on the expected dist-tag value instead of global version.
2023
- const versionMatches = isStableTrack ? observedVersion === expectedVersion : true;
2024
- if (versionMatches && observedTagVersion === expectedVersion) {
2025
- return {
2026
- status: 'pass',
2027
- observedVersion,
2028
- observedTagVersion
2029
- };
2097
+ let allPass = true;
2098
+ for (const target of packageTargets) {
2099
+ const versionResult = deps.exec('npm', ['view', target.name, 'version', '--json']);
2100
+ const tagsResult = deps.exec('npm', ['view', target.name, 'dist-tags', '--json']);
2101
+ let observedVersion = '';
2102
+ let observedTagVersion = '';
2103
+ if (versionResult.status === 0) {
2104
+ observedVersion = String(parseJsonSafely(versionResult.stdout || '""', '') || '');
2105
+ }
2106
+ if (tagsResult.status === 0) {
2107
+ const tags = parseJsonSafely(tagsResult.stdout || '{}', {});
2108
+ observedTagVersion = tags && tags[expectedTag] ? String(tags[expectedTag]) : '';
2109
+ }
2110
+
2111
+ const versionMatches = isStableTrack ? observedVersion === target.version : true;
2112
+ const tagMatches = observedTagVersion === target.version;
2113
+ const passed = versionMatches && tagMatches;
2114
+ if (!passed) {
2115
+ allPass = false;
2030
2116
  }
2117
+
2118
+ observations[target.name] = {
2119
+ name: target.name,
2120
+ expectedVersion: target.version,
2121
+ observedVersion,
2122
+ observedTagVersion,
2123
+ passed
2124
+ };
2125
+ }
2126
+
2127
+ if (allPass) {
2128
+ return {
2129
+ status: 'pass',
2130
+ observations
2131
+ };
2132
+ }
2133
+
2134
+ const now = nowMs(deps);
2135
+ if (onProgress && (lastProgressAt === 0 || now - lastProgressAt >= 30_000)) {
2136
+ lastProgressAt = now;
2137
+ onProgress({
2138
+ expectedTag,
2139
+ observations
2140
+ });
2031
2141
  }
2032
2142
 
2033
2143
  waitForNextPoll(timeoutAt, 10000, deps);
@@ -2035,8 +2145,7 @@ function validateNpmPublishedVersionAndTag(packageName, expectedVersion, expecte
2035
2145
 
2036
2146
  return {
2037
2147
  status: 'timeout',
2038
- observedVersion: lastObservedVersion,
2039
- observedTagVersion: lastObservedTagVersion
2148
+ observations
2040
2149
  };
2041
2150
  }
2042
2151
 
@@ -3083,7 +3192,18 @@ async function runReleaseCycle(args, dependencies = {}) {
3083
3192
  } else {
3084
3193
  reporter.start('release-cycle-wait-release-pr', 'Waiting for release PR (changeset-release/*)...');
3085
3194
  const releasePr = waitForReleasePr(gitContext.repo, args.releasePrTimeout, deps, {
3086
- expectedBase: releaseBaseBranchForTrack(requestedTrack)
3195
+ expectedBase: releaseBaseBranchForTrack(requestedTrack),
3196
+ onProgress: () => {
3197
+ const run = getLatestWorkflowRunForBranch(gitContext.repo, DEFAULT_BETA_BRANCH, deps);
3198
+ if (!run) {
3199
+ logStep('run', `Still waiting release PR... no workflow runs found yet on ${DEFAULT_BETA_BRANCH}.`);
3200
+ return;
3201
+ }
3202
+ logStep(
3203
+ 'run',
3204
+ `Still waiting release PR... workflow "${run.workflowName || 'unknown'}" is ${run.status || 'unknown'}${run.conclusion ? ` (${run.conclusion})` : ''}.`
3205
+ );
3206
+ }
3087
3207
  });
3088
3208
  reporter.ok('release-cycle-wait-release-pr', `Release PR found: #${releasePr.number}`);
3089
3209
  summary.releasePr = `found (#${releasePr.number})`;
@@ -3131,28 +3251,45 @@ async function runReleaseCycle(args, dependencies = {}) {
3131
3251
  reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
3132
3252
  const targetRef = args.promoteStable ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
3133
3253
  const expectedTag = requestedTrack === 'stable' ? 'latest' : 'beta';
3134
- const remotePackage = getRemotePackageVersion(gitContext.repo, targetRef, deps);
3135
- const npmValidation = validateNpmPublishedVersionAndTag(
3136
- remotePackage.name,
3137
- remotePackage.version,
3254
+ const targetPackages = resolveExpectedNpmPackages(
3255
+ gitContext.repo,
3256
+ mergedReleasePr.number,
3257
+ targetRef,
3138
3258
  expectedTag,
3139
- args.releasePrTimeout,
3259
+ args,
3140
3260
  deps
3141
3261
  );
3262
+ const npmValidation = validateNpmPublishedPackages(
3263
+ targetPackages,
3264
+ expectedTag,
3265
+ args.releasePrTimeout,
3266
+ {
3267
+ ...deps,
3268
+ onNpmValidationProgress: ({ expectedTag: expectedTagValue, observations }) => {
3269
+ const statusLine = Object.values(observations)
3270
+ .map((entry) => `${entry.name}: expected ${expectedTagValue}=${entry.expectedVersion}, observed version=${entry.observedVersion || 'n/a'}, ${expectedTagValue}=${entry.observedTagVersion || 'n/a'}`)
3271
+ .join(' | ');
3272
+ logStep('run', `Waiting npm propagation... ${statusLine}`);
3273
+ }
3274
+ }
3275
+ );
3142
3276
  if (npmValidation.status !== 'pass') {
3143
3277
  summary.npmValidation = `failed (${expectedTag})`;
3278
+ const observedLines = Object.values(npmValidation.observations || {})
3279
+ .map((entry) => `${entry.name}: version=${entry.observedVersion || 'n/a'}, ${expectedTag}=${entry.observedTagVersion || 'n/a'}`);
3280
+ const expectedLines = targetPackages
3281
+ .map((pkg) => `${pkg.name}@${pkg.version}`);
3144
3282
  throw new Error(
3145
3283
  [
3146
3284
  'npm validation failed after release merge.',
3147
- `Expected: ${remotePackage.name}@${remotePackage.version} with dist-tag ${expectedTag}`,
3148
- `Observed version: ${npmValidation.observedVersion || 'n/a'}`,
3149
- `Observed tag (${expectedTag}): ${npmValidation.observedTagVersion || 'n/a'}`
3285
+ `Expected (${expectedTag}): ${expectedLines.join(', ')}`,
3286
+ ...observedLines
3150
3287
  ].join('\n')
3151
3288
  );
3152
3289
  }
3153
- reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
3154
- summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
3155
- summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
3290
+ reporter.ok('release-cycle-verify-npm', `${targetPackages.length} package(s) validated on tag ${expectedTag}.`);
3291
+ summary.actionsPerformed.push(`npm validation: ${targetPackages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ')} (${expectedTag})`);
3292
+ summary.npmValidation = `pass (${expectedTag} -> ${targetPackages.map((pkg) => pkg.version).join(', ')})`;
3156
3293
  npmValidationPassed = true;
3157
3294
  } else if (!args.verifyNpm) {
3158
3295
  summary.actionsSkipped.push('verify npm');
@@ -3262,28 +3399,45 @@ async function runReleaseCycle(args, dependencies = {}) {
3262
3399
  reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
3263
3400
  const targetRef = effectivePublishTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
3264
3401
  const expectedTag = effectivePublishTrack === 'stable' ? 'latest' : 'beta';
3265
- const remotePackage = getRemotePackageVersion(gitContext.repo, targetRef, deps);
3266
- const npmValidation = validateNpmPublishedVersionAndTag(
3267
- remotePackage.name,
3268
- remotePackage.version,
3402
+ const targetPackages = resolveExpectedNpmPackages(
3403
+ gitContext.repo,
3404
+ releasePr.number,
3405
+ targetRef,
3269
3406
  expectedTag,
3270
- args.releasePrTimeout,
3407
+ args,
3271
3408
  deps
3272
3409
  );
3410
+ const npmValidation = validateNpmPublishedPackages(
3411
+ targetPackages,
3412
+ expectedTag,
3413
+ args.releasePrTimeout,
3414
+ {
3415
+ ...deps,
3416
+ onNpmValidationProgress: ({ expectedTag: expectedTagValue, observations }) => {
3417
+ const statusLine = Object.values(observations)
3418
+ .map((entry) => `${entry.name}: expected ${expectedTagValue}=${entry.expectedVersion}, observed version=${entry.observedVersion || 'n/a'}, ${expectedTagValue}=${entry.observedTagVersion || 'n/a'}`)
3419
+ .join(' | ');
3420
+ logStep('run', `Waiting npm propagation... ${statusLine}`);
3421
+ }
3422
+ }
3423
+ );
3273
3424
  if (npmValidation.status !== 'pass') {
3274
3425
  summary.npmValidation = `failed (${expectedTag})`;
3426
+ const observedLines = Object.values(npmValidation.observations || {})
3427
+ .map((entry) => `${entry.name}: version=${entry.observedVersion || 'n/a'}, ${expectedTag}=${entry.observedTagVersion || 'n/a'}`);
3428
+ const expectedLines = targetPackages
3429
+ .map((pkg) => `${pkg.name}@${pkg.version}`);
3275
3430
  throw new Error(
3276
3431
  [
3277
3432
  'npm validation failed after release merge.',
3278
- `Expected: ${remotePackage.name}@${remotePackage.version} with dist-tag ${expectedTag}`,
3279
- `Observed version: ${npmValidation.observedVersion || 'n/a'}`,
3280
- `Observed tag (${expectedTag}): ${npmValidation.observedTagVersion || 'n/a'}`
3433
+ `Expected (${expectedTag}): ${expectedLines.join(', ')}`,
3434
+ ...observedLines
3281
3435
  ].join('\n')
3282
3436
  );
3283
3437
  }
3284
- reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
3285
- summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
3286
- summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
3438
+ reporter.ok('release-cycle-verify-npm', `${targetPackages.length} package(s) validated on tag ${expectedTag}.`);
3439
+ summary.actionsPerformed.push(`npm validation: ${targetPackages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ')} (${expectedTag})`);
3440
+ summary.npmValidation = `pass (${expectedTag} -> ${targetPackages.map((pkg) => pkg.version).join(', ')})`;
3287
3441
  npmValidationPassed = true;
3288
3442
  } else if (!args.verifyNpm) {
3289
3443
  summary.npmValidation = 'skipped';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i-santos/create-package-starter",
3
- "version": "1.5.0-beta.11",
3
+ "version": "1.5.0-beta.13",
4
4
  "description": "Scaffold new npm packages with a standardized Changesets release workflow",
5
5
  "license": "MIT",
6
6
  "author": "Igor Santos",