rman 1.0.10 → 1.0.12

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.
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import semver from 'semver';
3
4
  import { detectChangeHash, expandTag } from '../utils/change-hash.js';
@@ -6,6 +7,7 @@ import { exec } from '../utils/exec.js';
6
7
  import { GitHelper } from '../utils/git.js';
7
8
  import { filterPackages } from '../utils/package-filter.js';
8
9
  import { expandReleaseTag, findLastReleaseVersion, formatCalendarVersion, isCalendarVersion, usesCalendarVersion, } from '../utils/release-version.js';
10
+ import { stampVersionConstant, stampVersionLabel } from '../utils/version-stamp.js';
9
11
  import { parseWorkspaceRange } from '../utils/workspace-range.js';
10
12
  import { ChangelogService } from './changelog.service.js';
11
13
  export var VersionService;
@@ -133,9 +135,13 @@ export var VersionService;
133
135
  const isRealEntry = (e) => !(repository.monorepo && e.package === repository.rootPackage);
134
136
  const bumped = plan.filter(e => e.status === 'bump' && isRealEntry(e));
135
137
  const bumpedByName = new Map(bumped.map(e => [e.package.name, e]));
138
+ /** Repo-relative paths of every file stamped with the new version below (a Dockerfile label, a
139
+ * source constant), so each lands in the same commit as the bump that made it stale - keyed by
140
+ * package, the way `changelogFileByPackage` is. */
141
+ const stampedByPackage = new Map();
136
142
  for (const entry of bumped) {
137
143
  const pkg = entry.package;
138
- await runVersionScript(pkg, 'preScript', 'preversion');
144
+ await runVersionScript(pkg, 'before', 'preversion');
139
145
  pkg.json.version = entry.to;
140
146
  for (const depKey of DEPENDENCY_KEYS) {
141
147
  const deps = pkg.json[depKey];
@@ -157,9 +163,14 @@ export var VersionService;
157
163
  deps[depName] = '^' + depEntry.to;
158
164
  }
159
165
  }
160
- await runVersionScript(pkg, 'script', 'version');
166
+ await runVersionScript(pkg, 'exec', 'version');
161
167
  pkg.writeJson();
162
- await runVersionScript(pkg, 'postScript', 'postversion');
168
+ // Before `postversion`, so a script that reacts to the bump sees the whole new state.
169
+ const stamped = [stampDockerfile(pkg, entry.to), ...stampSourceFiles(pkg, entry.to)].filter((f) => !!f);
170
+ if (stamped.length) {
171
+ stampedByPackage.set(pkg.name, stamped.map(f => path.relative(repository.dirname, f)));
172
+ }
173
+ await runVersionScript(pkg, 'after', 'postversion');
163
174
  }
164
175
  const rootEntry = repository.monorepo ? plan.find(e => e.package === repository.rootPackage) : undefined;
165
176
  if (rootEntry?.status === 'bump') {
@@ -218,6 +229,7 @@ export var VersionService;
218
229
  const changelogFile = changelogFileByPackage.get(e.package.name);
219
230
  if (changelogFile)
220
231
  files.push(changelogFile);
232
+ files.push(...(stampedByPackage.get(e.package.name) ?? []));
221
233
  }
222
234
  await git.commit(files, buildCommitMessage(repository, groupEntries, options.message));
223
235
  const tags = new Set(groupEntries.map(e => expandTag(e.package, e.to)));
@@ -490,3 +502,55 @@ async function runVersionScript(pkg, cfgKey, npmScriptName) {
490
502
  if (command)
491
503
  await exec(command, { cwd: pkg.dirname, stdio: 'inherit' });
492
504
  }
505
+ /**
506
+ * Keeps a package's Dockerfile `org.opencontainers.image.version` label in step with the version
507
+ * just written, returning the absolute path when it actually changed (so the caller can fold it
508
+ * into the same commit) and `undefined` otherwise.
509
+ *
510
+ * Here rather than in a build script: the label is a *statement of the package's version*, so it
511
+ * belongs to whatever writes that version - which keeps it in the bump commit, leaves the tree
512
+ * clean, and makes it right for anyone building the Dockerfile by hand. A build-time rewrite is
513
+ * both later than it needs to be and invisible to git.
514
+ *
515
+ * The same path `publish --target docker` builds from (`publish.docker.dockerfile`, default
516
+ * `Dockerfile`), so the two can never disagree about which file this is. Opt out with `.rmanrc
517
+ * "version": { "stampDockerfile": false }`; a package with no Dockerfile, or one that doesn't
518
+ * declare the label, is a no-op either way.
519
+ */
520
+ function stampDockerfile(pkg, version) {
521
+ if (pkg.config?.version?.stampDockerfile === false)
522
+ return undefined;
523
+ const file = path.resolve(pkg.dirname, pkg.config?.publish?.docker?.dockerfile || 'Dockerfile');
524
+ if (!fs.existsSync(file))
525
+ return undefined;
526
+ const stamped = stampVersionLabel(fs.readFileSync(file, 'utf-8'), version);
527
+ if (stamped === undefined)
528
+ return undefined;
529
+ fs.writeFileSync(file, stamped, 'utf-8');
530
+ return file;
531
+ }
532
+ /**
533
+ * Keeps every `.rmanrc "version.stamp"` source file's `version` constant in step with the version
534
+ * just written, returning the absolute paths of the ones that actually changed.
535
+ *
536
+ * Explicitly listed rather than discovered: unlike the OCI Dockerfile label there is no standard
537
+ * saying "this file holds the version", and the assignment this matches is deliberately broad. A
538
+ * listed file a package doesn't have is a silent no-op, which is what lets one `"[*]"` declaration
539
+ * cover a repo where only some packages carry one.
540
+ */
541
+ function stampSourceFiles(pkg, version) {
542
+ const configured = pkg.config?.version?.stamp;
543
+ const patterns = typeof configured === 'string' ? [configured] : (configured ?? []);
544
+ const stamped = [];
545
+ for (const rel of patterns) {
546
+ const file = path.resolve(pkg.dirname, rel);
547
+ if (!fs.existsSync(file))
548
+ continue;
549
+ const next = stampVersionConstant(fs.readFileSync(file, 'utf-8'), version);
550
+ if (next === undefined)
551
+ continue;
552
+ fs.writeFileSync(file, next, 'utf-8');
553
+ stamped.push(file);
554
+ }
555
+ return stamped;
556
+ }
@@ -63,5 +63,5 @@ export declare function npmRunPath(options?: RunPathOptions): string;
63
63
  ```
64
64
  */
65
65
  export declare function npmRunPathEnv(options?: EnvOptions): {
66
- [x: string]: string | undefined;
66
+ [key: string]: string | undefined;
67
67
  };
@@ -0,0 +1,40 @@
1
+ /** Keeping a package's version truthful wherever the package itself declares it - its Dockerfile's
2
+ * OCI label, and any source file that hard-codes it as a constant. Both are `version`'s job rather
3
+ * than a build step's: a version is being written, these are places it is written, and doing it
4
+ * here puts them in the bump commit instead of leaving the repository disagreeing with itself. */
5
+ /** The OCI label whose value is, by specification, "version of the packaged software" - so a
6
+ * package's own `package.json` version is the only correct value it can hold, which is what makes
7
+ * stamping it automatic rather than something to configure. */
8
+ export declare const OCI_VERSION_LABEL = "org.opencontainers.image.version";
9
+ /**
10
+ * Rewrites every `org.opencontainers.image.version` value in a Dockerfile's `LABEL` instructions to
11
+ * `version`, returning the new content - or `undefined` when there was nothing to change (no such
12
+ * label, or it already holds this version), so a caller can skip writing the file at all.
13
+ *
14
+ * Only ever *rewrites*: a Dockerfile that doesn't declare the label is left exactly as it is rather
15
+ * than having one inserted. Which labels an image carries is the author's decision; keeping one
16
+ * they already declared truthful is not.
17
+ *
18
+ * The existing quoting style is preserved, so the diff is the version and nothing else.
19
+ */
20
+ export declare function stampVersionLabel(content: string, version: string): string | undefined;
21
+ /**
22
+ * Rewrites the string assigned to a `name` constant (default `version`) in a source file to
23
+ * `version`, returning the new content - or `undefined` when there was nothing to change, so a
24
+ * caller can skip writing the file at all. Both an assignment and an object property are matched:
25
+ *
26
+ * ```ts
27
+ * export const version = '1'; -> export const version = '6.0.10';
28
+ * { name: 'app', version: '1' } -> { name: 'app', version: '6.0.10' }
29
+ * ```
30
+ *
31
+ * Unlike the Dockerfile label there is no standard naming a file as holding the version, so this
32
+ * only ever runs against files `.rmanrc "version.stamp"` explicitly lists - which is also what
33
+ * keeps a match this broad safe.
34
+ *
35
+ * Stamping the *source* rather than the build output is the point: a build-time rewrite leaves the
36
+ * checked-in file claiming some placeholder, so anything running from source (tests, ts-node, the
37
+ * dev loop) reports that placeholder, git never records the released version, and the rewrite has
38
+ * to be redone on every build.
39
+ */
40
+ export declare function stampVersionConstant(content: string, version: string, name?: string): string | undefined;
@@ -0,0 +1,76 @@
1
+ /** Keeping a package's version truthful wherever the package itself declares it - its Dockerfile's
2
+ * OCI label, and any source file that hard-codes it as a constant. Both are `version`'s job rather
3
+ * than a build step's: a version is being written, these are places it is written, and doing it
4
+ * here puts them in the bump commit instead of leaving the repository disagreeing with itself. */
5
+ /** The OCI label whose value is, by specification, "version of the packaged software" - so a
6
+ * package's own `package.json` version is the only correct value it can hold, which is what makes
7
+ * stamping it automatic rather than something to configure. */
8
+ export const OCI_VERSION_LABEL = 'org.opencontainers.image.version';
9
+ /**
10
+ * Rewrites every `org.opencontainers.image.version` value in a Dockerfile's `LABEL` instructions to
11
+ * `version`, returning the new content - or `undefined` when there was nothing to change (no such
12
+ * label, or it already holds this version), so a caller can skip writing the file at all.
13
+ *
14
+ * Only ever *rewrites*: a Dockerfile that doesn't declare the label is left exactly as it is rather
15
+ * than having one inserted. Which labels an image carries is the author's decision; keeping one
16
+ * they already declared truthful is not.
17
+ *
18
+ * The existing quoting style is preserved, so the diff is the version and nothing else.
19
+ */
20
+ export function stampVersionLabel(content, version) {
21
+ const lines = content.split('\n');
22
+ let changed = false;
23
+ // A LABEL's key=value pairs can spill over several lines via trailing backslashes, so the
24
+ // instruction a line belongs to is tracked rather than assumed from the line itself. Anything
25
+ // outside a LABEL is left alone - the same key in an ARG, an ENV or a comment is not a label.
26
+ let continuing = false;
27
+ for (let i = 0; i < lines.length; i++) {
28
+ const line = lines[i];
29
+ const inLabel = continuing || /^\s*LABEL\b/i.test(line);
30
+ continuing = inLabel && /\\\s*$/.test(line);
31
+ if (!inLabel)
32
+ continue;
33
+ lines[i] = line.replace(LABEL_VALUE, (whole, key, value) => {
34
+ const quote = value[0] === '"' || value[0] === "'" ? value[0] : '';
35
+ const next = `${key}${quote}${version}${quote}`;
36
+ if (next !== whole)
37
+ changed = true;
38
+ return next;
39
+ });
40
+ }
41
+ return changed ? lines.join('\n') : undefined;
42
+ }
43
+ /**
44
+ * Rewrites the string assigned to a `name` constant (default `version`) in a source file to
45
+ * `version`, returning the new content - or `undefined` when there was nothing to change, so a
46
+ * caller can skip writing the file at all. Both an assignment and an object property are matched:
47
+ *
48
+ * ```ts
49
+ * export const version = '1'; -> export const version = '6.0.10';
50
+ * { name: 'app', version: '1' } -> { name: 'app', version: '6.0.10' }
51
+ * ```
52
+ *
53
+ * Unlike the Dockerfile label there is no standard naming a file as holding the version, so this
54
+ * only ever runs against files `.rmanrc "version.stamp"` explicitly lists - which is also what
55
+ * keeps a match this broad safe.
56
+ *
57
+ * Stamping the *source* rather than the build output is the point: a build-time rewrite leaves the
58
+ * checked-in file claiming some placeholder, so anything running from source (tests, ts-node, the
59
+ * dev loop) reports that placeholder, git never records the released version, and the rewrite has
60
+ * to be redone on every build.
61
+ */
62
+ export function stampVersionConstant(content, version, name = 'version') {
63
+ let changed = false;
64
+ const pattern = new RegExp(`(\\b${escapeRegExp(name)}\\s*[:=]\\s*)(['"\`])([^'"\`\\n]*)\\2`, 'g');
65
+ const result = content.replace(pattern, (whole, prefix, quote) => {
66
+ const next = `${prefix}${quote}${version}${quote}`;
67
+ if (next !== whole)
68
+ changed = true;
69
+ return next;
70
+ });
71
+ return changed ? result : undefined;
72
+ }
73
+ const LABEL_VALUE = new RegExp(`(${OCI_VERSION_LABEL.replace(/\./g, '\\.')}\\s*=\\s*)("[^"\\n]*"|'[^'\\n]*'|[^\\s\\\\]+)`, 'g');
74
+ function escapeRegExp(value) {
75
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
76
+ }