@naturalcycles/dev-lib 20.62.0 → 20.63.1

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.
File without changes
File without changes
@@ -467,6 +467,18 @@ export const sharedConfig = {
467
467
  ],
468
468
  'vitest/max-nested-describe': [0, { max: 4 }], // not good
469
469
  'vitest/no-alias-methods': 2,
470
+ 'vitest/no-restricted-vi-methods': [
471
+ 2,
472
+ {
473
+ mock: 'vi.mock() is never cleared (isolate: false) and leaks into other test files on the same worker. Spy on the module namespace instead: vi.spyOn(ns, "fn") or vi.spyOn(ns, "constName", "get").',
474
+ doMock:
475
+ 'vi.doMock() is never cleared (isolate: false) and leaks into other test files on the same worker. Use vi.spyOn() on the module namespace instead.',
476
+ hoisted:
477
+ 'vi.hoisted() only exists to feed vi.mock() factories, which are banned. Use vi.spyOn() instead.',
478
+ resetModules:
479
+ 'vi.resetModules() clears the shared module cache for every later test file on this worker (isolate: false).',
480
+ },
481
+ ],
470
482
  'vue/no-multiple-slot-args': 2,
471
483
  'vue/no-deprecated-destroyed-lifecycle': 2,
472
484
  'vue/define-emits-declaration': [2, 'type-based'],
@@ -77,6 +77,9 @@ export function getSharedConfig(cwd) {
77
77
  watch: false,
78
78
  // dir: 'src',
79
79
  restoreMocks: true,
80
+ // restoreMocks does not cover vi.stubEnv/vi.stubGlobal
81
+ unstubEnvs: true,
82
+ unstubGlobals: true,
80
83
  silent,
81
84
  setupFiles: getSetupFiles(testType, cwd),
82
85
  logHeapUsage: true,
@@ -16,10 +16,18 @@ export declare function getCurrentBranch(): string;
16
16
  */
17
17
  export declare function ensureFullGitHistory(repo: RepoInfo): void;
18
18
  /**
19
- * Abort if someone pushed to the branch since this checkout - releasing from a stale HEAD
20
- * would compute wrong versions/notes. No-op if the branch has no remote counterpart.
19
+ * Abort if the branch was rewritten since this checkout (e.g. force-pushed) and no longer
20
+ * contains HEAD: the release tag would land on an orphaned commit, invisible to the next release,
21
+ * which would then try to publish the same version again.
22
+ *
23
+ * A remote that merely moved forward is fine: the plan (version, notes) comes from `lastTag..HEAD`
24
+ * and the tag goes on HEAD, so commits ahead of HEAD can't affect it - they are released by their
25
+ * own run. Aborting on them would silently skip releases in repos whose CI runs the release only
26
+ * when package files change (the newer commits may not trigger a run at all).
27
+ *
28
+ * No-op if the branch has no remote counterpart.
21
29
  */
22
- export declare function checkHeadIsUpToDateWithRemote(branch: string, repo: RepoInfo): void;
30
+ export declare function checkHeadIsOnRemoteBranch(branch: string, repo: RepoInfo): void;
23
31
  /**
24
32
  * All git tags reachable from HEAD.
25
33
  * Reachability matters: on a prerelease branch this naturally excludes tags of other
@@ -56,10 +56,18 @@ export function ensureFullGitHistory(repo) {
56
56
  }
57
57
  }
58
58
  /**
59
- * Abort if someone pushed to the branch since this checkout - releasing from a stale HEAD
60
- * would compute wrong versions/notes. No-op if the branch has no remote counterpart.
59
+ * Abort if the branch was rewritten since this checkout (e.g. force-pushed) and no longer
60
+ * contains HEAD: the release tag would land on an orphaned commit, invisible to the next release,
61
+ * which would then try to publish the same version again.
62
+ *
63
+ * A remote that merely moved forward is fine: the plan (version, notes) comes from `lastTag..HEAD`
64
+ * and the tag goes on HEAD, so commits ahead of HEAD can't affect it - they are released by their
65
+ * own run. Aborting on them would silently skip releases in repos whose CI runs the release only
66
+ * when package files change (the newer commits may not trigger a run at all).
67
+ *
68
+ * No-op if the branch has no remote counterpart.
61
69
  */
62
- export function checkHeadIsUpToDateWithRemote(branch, repo) {
70
+ export function checkHeadIsOnRemoteBranch(branch, repo) {
63
71
  const remote = getAuthenticatedRemote(repo);
64
72
  let remoteSha;
65
73
  try {
@@ -69,13 +77,26 @@ export function checkHeadIsUpToDateWithRemote(branch, repo) {
69
77
  ?.trim();
70
78
  }
71
79
  catch {
72
- console.log(dimGrey('git ls-remote failed, skipping up-to-date check'));
80
+ console.log(dimGrey('git ls-remote failed, skipping remote branch check'));
73
81
  return;
74
82
  }
75
83
  if (!remoteSha)
76
84
  return;
77
85
  const headSha = exec2.exec('git rev-parse HEAD').trim();
78
- _assert(remoteSha === headSha, `Local HEAD (${headSha.slice(0, 7)}) is not up to date with remote ${branch} (${remoteSha.slice(0, 7)}) - aborting release`);
86
+ if (remoteSha === headSha)
87
+ return;
88
+ // The branch moved since checkout: fetch its tip to tell a fast-forward from a rewrite
89
+ try {
90
+ exec2.exec(`git fetch --quiet "${remote}" "refs/heads/${branch}"`);
91
+ }
92
+ catch {
93
+ // Sanitized: the original error contains the failed command incl. the token
94
+ throw new Error(`git fetch of remote ${branch} failed - cannot verify that it still contains HEAD, aborting release`);
95
+ }
96
+ const localOnlyCommits = Number(exec2.exec('git rev-list --count FETCH_HEAD..HEAD').trim());
97
+ _assert(localOnlyCommits === 0, `Remote ${branch} (${remoteSha.slice(0, 7)}) was rewritten and no longer contains HEAD (${headSha.slice(0, 7)}) - aborting release`);
98
+ const ahead = exec2.exec('git rev-list --count HEAD..FETCH_HEAD').trim();
99
+ console.log(dimGrey(`Remote ${branch} is ${ahead} commit(s) ahead of HEAD (${headSha.slice(0, 7)}) - releasing from HEAD, the newer commits get their own run`));
79
100
  }
80
101
  /**
81
102
  * All git tags reachable from HEAD.
@@ -7,7 +7,7 @@ import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
7
7
  import picomatch from 'picomatch';
8
8
  import { readDevLibConfigIfPresent } from '../config.js';
9
9
  import { analyzeCommits, parseCommit } from './commit.util.js';
10
- import { checkHeadIsUpToDateWithRemote, createAndPushTag, ensureFullGitHistory, getCommitsSince, getCurrentBranch, getReachableTags, getRepoInfo, } from './git.util.js';
10
+ import { checkHeadIsOnRemoteBranch, createAndPushTag, ensureFullGitHistory, getCommitsSince, getCurrentBranch, getReachableTags, getRepoInfo, } from './git.util.js';
11
11
  import { GithubApi } from './github.util.js';
12
12
  import { generateReleaseNotes } from './notes.util.js';
13
13
  import { getLastRelease, getNextVersion, getVersionsFromTags } from './version.util.js';
@@ -51,7 +51,7 @@ export async function releaseCommand() {
51
51
  }
52
52
  ensureFullGitHistory(repo);
53
53
  if (!dryRun) {
54
- checkHeadIsUpToDateWithRemote(branch, repo);
54
+ checkHeadIsOnRemoteBranch(branch, repo);
55
55
  }
56
56
  const mode = resolveReleaseMode(cfg.mode);
57
57
  const packages = discoverReleasePackages(mode, argv.filter);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/dev-lib",
3
3
  "type": "module",
4
- "version": "20.62.0",
4
+ "version": "20.63.1",
5
5
  "dependencies": {
6
6
  "prompts": "^2",
7
7
  "@naturalcycles/js-lib": "^15",