build-strap 5.2.1 → 6.0.0-main.134

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 CHANGED
@@ -3,7 +3,7 @@ A suite of functions to help with writing build scripts using TypeScript (or jav
3
3
 
4
4
  This project builds itself, so take a look in the `targets/` folder for an example. Run `./bs` to build.
5
5
 
6
- [![CI Build](https://github.com/murrayju/build-strap/workflows/CI%20Build/badge.svg?branch=master)](https://github.com/murrayju/build-strap/actions?query=workflow%3A%22CI+Build%22)
6
+ [![CI Build](https://github.com/murrayju/build-strap/workflows/CI%20Build/badge.svg?branch=main)](https://github.com/murrayju/build-strap/actions?query=workflow%3A%22CI+Build%22)
7
7
  [![version](https://img.shields.io/github/v/tag/murrayju/build-strap.svg?label=version&sort=semver)](https://github.com/murrayju/build-strap/releases/latest)
8
8
  [![npm](https://img.shields.io/npm/v/build-strap)](https://npmjs.org/package/build-strap)
9
9
  [![dependencies](https://img.shields.io/david/murrayju/build-strap.svg)](https://david-dm.org/murrayju/build-strap)
@@ -36,8 +36,7 @@ By default, the build tools read project-specific configuration from your `packa
36
36
  "yarnVersion": "1.22.4",
37
37
  "repoType": "git",
38
38
  "copyright": "your company",
39
- "releaseBranch": "master",
40
- "devBranch": "dev",
39
+ "mainBranch": "main",
41
40
  "npm": {
42
41
  "publish": true,
43
42
  },
@@ -50,11 +49,34 @@ By default, the build tools read project-specific configuration from your `packa
50
49
  }
51
50
  ```
52
51
 
52
+ ### mainBranch
53
+ The name of the single long-lived branch of the repository (defaults to `main`). All work happens on short-lived branches that are merged into it via pull request.
54
+
53
55
  ### docker
54
56
  * **registry**: base URL for the docker registry, as needed by `docker push`.
55
57
  * **repository**: the name of the (organization's) docker repository, in which to put this project
56
58
  * **name**: the name of the project, used as the docker image name.
57
59
 
60
+ ## Branching and Release Workflow
61
+ This project (and projects built with it) use a single long-lived branch (`main`) with short-lived work branches merged into it via pull request. There is no separate development or release branch.
62
+
63
+ Releases are triggered by pushing a [semver](https://semver.org/) tag (with an optional leading `v`) to a commit on `main`:
64
+
65
+ ```
66
+ git tag v1.2.3 && git push origin v1.2.3
67
+ ```
68
+
69
+ The version being built is determined as follows:
70
+
71
+ | Build | npm version | npm dist-tag | docker tags |
72
+ | --- | --- | --- | --- |
73
+ | Tag `v1.2.3` on `main` | `1.2.3` | `latest` | `latest`, `1`, `1.2`, `1.2.3` |
74
+ | Tag `v1.2.3-rc.1` on `main` | `1.2.3-rc.1` | `rc` | `1.2.3-rc.1`, `latest-rc` |
75
+ | Untagged commit on `main` | `1.2.3-main.42` | `next` | `latest-main` |
76
+ | Work branch / pull request | `1.2.3-my-branch.42` | `branch` | (none) |
77
+
78
+ For untagged builds, the base version comes from the `version` field in `package.json` and the build number comes from `--buildNum=` (or the `BUILD_NUMBER` environment variable). For release builds, the version comes from the git tag, and the build refuses to publish if the tagged commit is not contained in `main`.
79
+
58
80
  ## Write Your Build Script
59
81
  See the reference implementations (below) for a complete example of a robust build environment. The library exports many useful functions. Here are some of the most important (see source for more):
60
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "build-strap",
3
- "version": "5.2.1",
3
+ "version": "6.0.0-main.134",
4
4
  "description": "Node scripts for building things",
5
5
  "license": "MIT",
6
6
  "author": "Justin Murray <justin@murrayju.com>",
package/src/docker.d.ts CHANGED
@@ -9,12 +9,11 @@ export declare function dockerBuild(tags?: string[], buildArgs?: string[], targe
9
9
  export declare function dockerTag(imageId: string, tags?: string[], repo?: string): Promise<void>;
10
10
  /**
11
11
  * Apply the standard convention for tagging the docker image for the project,
12
- * based on the source control branch being built.
13
- * For releases, applies `latest`, `M.m.p`, `M.m`, `M`
14
- * For release candidates, applies `latest-rc`
15
- * For feature branches, applies `latest-feature`
16
- * For development branch, applies `latest-dev`
17
- * All other branches, no tag is applies
12
+ * based on the semver git tag (or branch) being built.
13
+ * For releases (a semver tag), applies `latest`, `M`, `M.m`, `M.m.p`
14
+ * For pre-releases (e.g. tag `v1.2.3-rc.1`), applies `1.2.3-rc.1` and `latest-rc`
15
+ * For untagged builds of the main branch, applies `latest-main`
16
+ * For all other builds, no tag is applied
18
17
  * @param {string} imageId id of the docker image to which to apply the tags
19
18
  * @param {string} repo optional override of the docker repo url to use (derived from package.json by default)
20
19
  */
package/src/docker.js CHANGED
@@ -12,7 +12,7 @@ import { downloadFile } from './fetch.js';
12
12
  import { mountDmg, unmountDmg } from './macos.js';
13
13
  import { getCfg, getPkgName, getPkgScope } from './pkg.js';
14
14
  import { buildLog } from './run.js';
15
- import { getDevBranch, getVersion } from './version.js';
15
+ import { getMainBranch, getVersion } from './version.js';
16
16
  export function getDockerConfig() {
17
17
  return getCfg().docker || {};
18
18
  }
@@ -37,29 +37,29 @@ export async function dockerTag(imageId, tags = ['latest'], repo = getDockerRepo
37
37
  }
38
38
  /**
39
39
  * Apply the standard convention for tagging the docker image for the project,
40
- * based on the source control branch being built.
41
- * For releases, applies `latest`, `M.m.p`, `M.m`, `M`
42
- * For release candidates, applies `latest-rc`
43
- * For feature branches, applies `latest-feature`
44
- * For development branch, applies `latest-dev`
45
- * All other branches, no tag is applies
40
+ * based on the semver git tag (or branch) being built.
41
+ * For releases (a semver tag), applies `latest`, `M`, `M.m`, `M.m.p`
42
+ * For pre-releases (e.g. tag `v1.2.3-rc.1`), applies `1.2.3-rc.1` and `latest-rc`
43
+ * For untagged builds of the main branch, applies `latest-main`
44
+ * For all other builds, no tag is applied
46
45
  * @param {string} imageId id of the docker image to which to apply the tags
47
46
  * @param {string} repo optional override of the docker repo url to use (derived from package.json by default)
48
47
  */
49
48
  export async function dockerTagVersion(imageId, repo = getDockerRepo()) {
50
- const { branch, isRelease, major, minor, patch } = await getVersion();
49
+ const { branch, isRelease, major, minor, npm, patch, prerelease } = await getVersion();
51
50
  // determine what tags to apply
52
51
  if (isRelease) {
53
- await dockerTag(imageId, ['latest', `${major}`, `${major}.${minor}`, `${major}.${minor}.${patch}`], repo);
52
+ await dockerTag(imageId, prerelease
53
+ ? [npm, `latest-${prerelease.split('.')[0]}`]
54
+ : [
55
+ 'latest',
56
+ `${major}`,
57
+ `${major}.${minor}`,
58
+ `${major}.${minor}.${patch}`,
59
+ ], repo);
54
60
  }
55
- else if (branch === (await getDevBranch())) {
56
- await dockerTag(imageId, ['latest-dev'], repo);
57
- }
58
- else if (branch.match(/^(release|patch)-/)) {
59
- await dockerTag(imageId, ['latest-rc'], repo);
60
- }
61
- else if (branch.match(/^feature-/)) {
62
- await dockerTag(imageId, ['latest-feature'], repo);
61
+ else if (branch === getMainBranch()) {
62
+ await dockerTag(imageId, [`latest-${branch}`], repo);
63
63
  }
64
64
  }
65
65
  export async function dockerImages(repo = getDockerRepo(), filter = null) {
package/src/git.d.ts CHANGED
@@ -1,10 +1,28 @@
1
1
  import { SpawnOptions } from './cp.js';
2
2
  export declare const git: (args: string[], opts?: SpawnOptions) => Promise<import("./cp.js").SpawnResult>;
3
3
  export declare const gitOutput: (args: string[], opts?: SpawnOptions) => Promise<string>;
4
- export declare function gitBranch(): Promise<string>;
4
+ /**
5
+ * Convert a git ref name into a string that is safe to embed in a version.
6
+ */
7
+ export declare function sanitizeGitRefName(refName: string): string;
8
+ /**
9
+ * Name of the branch that is currently checked out, or `null` when the working
10
+ * copy is in a detached HEAD state (as is typical for tag and pull request
11
+ * builds on CI) and no branch name can be determined from the environment.
12
+ */
13
+ export declare function gitBranch(): Promise<null | string>;
5
14
  export declare function gitRevId(): Promise<string>;
15
+ /**
16
+ * All tags that point directly at the given commit (HEAD by default).
17
+ */
18
+ export declare function gitTagsAtCommit(commitish?: string): Promise<string[]>;
19
+ /**
20
+ * True when `ancestorCommitish` is reachable from `descendantCommitish`.
21
+ * Useful for verifying that a release tag lives on the main branch.
22
+ */
23
+ export declare function gitIsAncestor(ancestorCommitish: string, descendantCommitish: string): Promise<boolean>;
6
24
  type GitInfo = {
7
- branch: string;
25
+ branch: null | string;
8
26
  revision: string;
9
27
  };
10
28
  export declare function gitInfo(noCache?: boolean): Promise<GitInfo>;
package/src/git.js CHANGED
@@ -3,16 +3,51 @@ import { userQuestion } from './prompt.js';
3
3
  import { buildLog } from './run.js';
4
4
  export const git = async (args, opts) => spawn('git', args, opts);
5
5
  export const gitOutput = async (args, opts) => (await git(args, { captureOutput: true, ...opts })).stdout.trim();
6
+ /**
7
+ * Convert a git ref name into a string that is safe to embed in a version.
8
+ */
9
+ export function sanitizeGitRefName(refName) {
10
+ return refName.replace(/[_+/]/g, '-').trim();
11
+ }
12
+ /**
13
+ * Name of the branch that is currently checked out, or `null` when the working
14
+ * copy is in a detached HEAD state (as is typical for tag and pull request
15
+ * builds on CI) and no branch name can be determined from the environment.
16
+ */
6
17
  export async function gitBranch() {
7
- return (await gitOutput(['symbolic-ref', '--short', 'HEAD']))
8
- .replace(/[_+/]/g, '-')
9
- .trim();
18
+ const checkedOutBranch = await gitOutput(['symbolic-ref', '--short', 'HEAD'], {
19
+ rejectOnErrorCode: false,
20
+ });
21
+ if (checkedOutBranch) {
22
+ return sanitizeGitRefName(checkedOutBranch);
23
+ }
24
+ // Detached HEAD: fall back to the branch reported by the CI environment.
25
+ // For pull requests, GITHUB_HEAD_REF holds the source branch of the PR.
26
+ const { GITHUB_HEAD_REF, GITHUB_REF } = process.env;
27
+ const envBranch = GITHUB_HEAD_REF || GITHUB_REF?.match(/^refs\/heads\/(.+)$/)?.[1];
28
+ return envBranch ? sanitizeGitRefName(envBranch) : null;
10
29
  }
11
30
  export async function gitRevId() {
12
31
  return (await gitOutput(['rev-parse', '--short=12', 'HEAD']))
13
32
  .replace(/[+]/g, '')
14
33
  .trim();
15
34
  }
35
+ /**
36
+ * All tags that point directly at the given commit (HEAD by default).
37
+ */
38
+ export async function gitTagsAtCommit(commitish = 'HEAD') {
39
+ const output = await gitOutput(['tag', '--points-at', commitish], {
40
+ rejectOnErrorCode: false,
41
+ });
42
+ return output.split('\n').filter(Boolean);
43
+ }
44
+ /**
45
+ * True when `ancestorCommitish` is reachable from `descendantCommitish`.
46
+ * Useful for verifying that a release tag lives on the main branch.
47
+ */
48
+ export async function gitIsAncestor(ancestorCommitish, descendantCommitish) {
49
+ return ((await git(['merge-base', '--is-ancestor', ancestorCommitish, descendantCommitish], { rejectOnErrorCode: false })).code === 0);
50
+ }
16
51
  let info = null;
17
52
  export async function gitInfo(noCache = false) {
18
53
  if (!info || noCache) {
package/src/npm.js CHANGED
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import { spawn } from './cp.js';
5
5
  import { getCfg, getPkgName } from './pkg.js';
6
6
  import { buildLog } from './run.js';
7
- import { getDevBranch, getVersion } from './version.js';
7
+ import { getDefaultNpmDistTag, getVersion } from './version.js';
8
8
  export function getNpmConfig() {
9
9
  return getCfg().npm || {};
10
10
  }
@@ -86,7 +86,7 @@ export async function npmPublish({ npmAuthToken, npmConfig, npmCreds, npmPath, p
86
86
  });
87
87
  }
88
88
  const existing = await npmGetVersions(name, npmPath);
89
- const { branch, isRelease, npm: npmVersion } = await getVersion();
89
+ const { npm: npmVersion } = await getVersion();
90
90
  if (existing.includes(npmVersion)) {
91
91
  if (skipExisting) {
92
92
  buildLog('npm package with this same version already exists. Skipping publish...');
@@ -102,12 +102,7 @@ export async function npmPublish({ npmAuthToken, npmConfig, npmCreds, npmPath, p
102
102
  'publish',
103
103
  resolvedPath,
104
104
  '--tag',
105
- tag ||
106
- (isRelease
107
- ? 'latest'
108
- : branch === (await getDevBranch())
109
- ? 'next'
110
- : 'branch'),
105
+ tag || (await getDefaultNpmDistTag()),
111
106
  ...(access ? ['--access', access] : []),
112
107
  ...(dryRun ? ['--dry-run'] : []),
113
108
  ...(provenance ? ['--provenance'] : []),
package/src/pkg.d.ts CHANGED
@@ -4,10 +4,9 @@ import { NpmConfig } from './npm.js';
4
4
  export interface BuildStrapConfig {
5
5
  cert?: CertConfig;
6
6
  copyright?: string;
7
- devBranch?: string;
8
7
  docker?: DockerConfig;
8
+ mainBranch?: string;
9
9
  npm?: NpmConfig;
10
- releaseBranch?: string;
11
10
  repoType?: 'git' | 'hg';
12
11
  }
13
12
  export interface Person {
package/src/version.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  export declare function getBuild(): string;
2
- export declare function getReleaseBranch(): string;
2
+ /**
3
+ * The one long-lived branch of the repository. All work happens on short-lived
4
+ * branches that are merged into it via pull request, and releases are cut by
5
+ * pushing a semver tag to a commit on it.
6
+ */
7
+ export declare function getMainBranch(): string;
3
8
  declare enum RepoType {
4
9
  git = "git",
5
10
  hg = "hg",
@@ -7,15 +12,44 @@ declare enum RepoType {
7
12
  }
8
13
  export declare function getRepoType(): Promise<RepoType>;
9
14
  type RepoInfo = {
10
- branch: string;
15
+ branch: null | string;
11
16
  revision: string;
12
17
  };
13
18
  export declare function getRepoInfo(): Promise<RepoInfo>;
14
- export declare function getDevBranch(): Promise<string>;
15
- export declare function getIsRelease(passedBranch?: null | string, releaseOverride?: null | boolean): Promise<boolean>;
19
+ export interface SemverTag {
20
+ major: string;
21
+ minor: string;
22
+ patch: string;
23
+ prerelease: null | string;
24
+ tag: string;
25
+ version: string;
26
+ }
27
+ /**
28
+ * Parse a git tag as a semver release tag. Returns null if the tag is not a
29
+ * semver version tag (an optional leading `v` is allowed).
30
+ */
31
+ export declare function parseSemverTag(tag: string): null | SemverTag;
32
+ /**
33
+ * The semver tag pointing at the commit being built, if any. This is what makes
34
+ * a build a release build: releases are triggered by pushing a semver tag.
35
+ * When multiple semver tags point at the same commit, the highest one wins.
36
+ */
37
+ export declare function getReleaseTag(): Promise<null | SemverTag>;
38
+ /**
39
+ * Verify that the semver release tag agrees with the version in package.json, so
40
+ * that the published artifact always matches its source. Pre-release identifiers
41
+ * on the tag (e.g. `v1.2.3-rc.1` for package version `1.2.3`) are allowed.
42
+ */
43
+ export declare function assertReleaseTagMatchesPackageVersion(): Promise<void>;
44
+ /**
45
+ * Verify that the commit being released is contained in the main branch, so that
46
+ * a release can never be cut from an unmerged work branch.
47
+ */
48
+ export declare function assertReleaseCommitIsOnMainBranch(mainBranch?: string): Promise<void>;
49
+ export declare function getIsRelease(releaseOverride?: null | boolean): Promise<boolean>;
16
50
  export declare function useVersionCache(useCache?: boolean): void;
17
51
  export interface Version {
18
- branch: string;
52
+ branch: null | string;
19
53
  build: string;
20
54
  info: string;
21
55
  isRelease: boolean;
@@ -24,10 +58,20 @@ export interface Version {
24
58
  name: string;
25
59
  npm: string;
26
60
  patch: string;
61
+ prerelease: null | string;
62
+ releaseTag: null | string;
27
63
  revision: string;
28
64
  short: string;
29
65
  }
30
66
  export declare function getVersion(logIt?: boolean, release?: null | boolean): Promise<Version>;
67
+ /**
68
+ * The npm dist-tag to publish under, following the branch/tag conventions:
69
+ * - release tag without pre-release identifiers => `latest`
70
+ * - release tag with pre-release identifiers => the first identifier (`rc`, `beta`, ...)
71
+ * - untagged build of the main branch => `next`
72
+ * - anything else => `branch`
73
+ */
74
+ export declare function getDefaultNpmDistTag(): Promise<string>;
31
75
  export declare function getBanner(): Promise<string>;
32
76
  export declare function getVersionCode(): Promise<string>;
33
77
  export declare function getUniqueBuildTag(): Promise<string>;
package/src/version.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import fs from 'fs';
2
- import { gitInfo } from './git.js';
2
+ import { gitInfo, gitIsAncestor, gitTagsAtCommit } from './git.js';
3
3
  import { getCfg, getPkg } from './pkg.js';
4
4
  import { buildLog } from './run.js';
5
5
  export function getBuild() {
6
6
  const arg = process.argv.find((el) => /^--buildNum=\d+$/.test(el));
7
7
  return (arg && arg.substring(11)) || process.env.BUILD_NUMBER || '0';
8
8
  }
9
- export function getReleaseBranch() {
10
- const { releaseBranch } = getCfg();
11
- return releaseBranch || 'master';
9
+ /**
10
+ * The one long-lived branch of the repository. All work happens on short-lived
11
+ * branches that are merged into it via pull request, and releases are cut by
12
+ * pushing a semver tag to a commit on it.
13
+ */
14
+ export function getMainBranch() {
15
+ const { mainBranch } = getCfg();
16
+ return mainBranch || 'main';
12
17
  }
13
18
  var RepoType;
14
19
  (function (RepoType) {
@@ -31,41 +36,131 @@ export async function getRepoInfo() {
31
36
  throw new Error(`Unsupported repo type: ${repoType}`);
32
37
  }
33
38
  const { branch, revision } = await gitInfo();
34
- if (!branch || !revision) {
35
- throw new Error('Failed to parse branch and revision from source repo.');
39
+ if (!revision) {
40
+ throw new Error('Failed to parse revision from source repo.');
36
41
  }
37
42
  return { branch, revision };
38
43
  }
39
- export async function getDevBranch() {
40
- const { devBranch } = getCfg();
41
- return devBranch || ((await getRepoType()) === 'hg' ? 'default' : 'dev');
44
+ const semverTagRegex = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
45
+ /**
46
+ * Parse a git tag as a semver release tag. Returns null if the tag is not a
47
+ * semver version tag (an optional leading `v` is allowed).
48
+ */
49
+ export function parseSemverTag(tag) {
50
+ const [, major, minor, patch, prerelease] = tag.match(semverTagRegex) || [];
51
+ if (!major || !minor || !patch) {
52
+ return null;
53
+ }
54
+ return {
55
+ major,
56
+ minor,
57
+ patch,
58
+ prerelease: prerelease || null,
59
+ tag,
60
+ version: `${major}.${minor}.${patch}${prerelease ? `-${prerelease}` : ''}`,
61
+ };
62
+ }
63
+ /**
64
+ * The semver tag pointing at the commit being built, if any. This is what makes
65
+ * a build a release build: releases are triggered by pushing a semver tag.
66
+ * When multiple semver tags point at the same commit, the highest one wins.
67
+ */
68
+ export async function getReleaseTag() {
69
+ const overrideTag = process.argv
70
+ .find((el) => el.startsWith('--releaseTag='))
71
+ ?.substring('--releaseTag='.length) ||
72
+ process.env.RELEASE_TAG ||
73
+ // GitHub Actions sets this for tag builds
74
+ process.env.GITHUB_REF?.match(/^refs\/tags\/(.+)$/)?.[1];
75
+ if (overrideTag) {
76
+ const parsed = parseSemverTag(overrideTag);
77
+ if (!parsed) {
78
+ throw new Error(`\`${overrideTag}\` is not a valid semver release tag.`);
79
+ }
80
+ return parsed;
81
+ }
82
+ if ((await getRepoType()) !== RepoType.git) {
83
+ return null;
84
+ }
85
+ const semverTags = (await gitTagsAtCommit())
86
+ .map(parseSemverTag)
87
+ .filter((t) => t != null);
88
+ return (semverTags.sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true }))[semverTags.length - 1] || null);
89
+ }
90
+ /**
91
+ * Verify that the semver release tag agrees with the version in package.json, so
92
+ * that the published artifact always matches its source. Pre-release identifiers
93
+ * on the tag (e.g. `v1.2.3-rc.1` for package version `1.2.3`) are allowed.
94
+ */
95
+ export async function assertReleaseTagMatchesPackageVersion() {
96
+ const releaseTag = await getReleaseTag();
97
+ if (!releaseTag) {
98
+ return;
99
+ }
100
+ const { version: pkgVersion } = getPkg();
101
+ const pkgSemver = parseSemverTag(pkgVersion);
102
+ const tagBaseVersion = `${releaseTag.major}.${releaseTag.minor}.${releaseTag.patch}`;
103
+ const pkgBaseVersion = pkgSemver && `${pkgSemver.major}.${pkgSemver.minor}.${pkgSemver.patch}`;
104
+ if (tagBaseVersion !== pkgBaseVersion) {
105
+ throw new Error(`Refusing to release: tag \`${releaseTag.tag}\` does not match the version in package.json (\`${pkgVersion}\`).`);
106
+ }
42
107
  }
43
- export async function getIsRelease(passedBranch = null, releaseOverride = null) {
44
- const branch = passedBranch || (await getRepoInfo()).branch;
45
- return releaseOverride != null
46
- ? releaseOverride
47
- : process.argv.includes('--force-release-version')
48
- ? true
49
- : branch === getReleaseBranch();
108
+ /**
109
+ * Verify that the commit being released is contained in the main branch, so that
110
+ * a release can never be cut from an unmerged work branch.
111
+ */
112
+ export async function assertReleaseCommitIsOnMainBranch(mainBranch = getMainBranch()) {
113
+ const { revision } = await getRepoInfo();
114
+ const candidateRefs = [mainBranch, `origin/${mainBranch}`];
115
+ for (const ref of candidateRefs) {
116
+ // eslint-disable-next-line no-await-in-loop
117
+ if (await gitIsAncestor(revision, ref)) {
118
+ return;
119
+ }
120
+ }
121
+ throw new Error(`Refusing to release ${revision}: it is not contained in the \`${mainBranch}\` branch.`);
122
+ }
123
+ export async function getIsRelease(releaseOverride = null) {
124
+ if (releaseOverride != null) {
125
+ return releaseOverride;
126
+ }
127
+ if (process.argv.includes('--force-release-version')) {
128
+ return true;
129
+ }
130
+ return (await getReleaseTag()) != null;
50
131
  }
51
132
  let cacheVersion = true;
52
133
  export function useVersionCache(useCache = true) {
53
134
  cacheVersion = useCache;
54
135
  }
136
+ /**
137
+ * Label used to identify a non-release build in its version string. Falls back
138
+ * to the revision when no branch name is available.
139
+ */
140
+ function getBuildSourceLabel({ branch, revision }) {
141
+ return branch || revision;
142
+ }
55
143
  let version = null;
56
144
  export async function getVersion(logIt = true, release = null) {
57
145
  if (!cacheVersion || version == null) {
58
- const { name, version: pkgVers } = getPkg();
59
- const { branch, revision } = await getRepoInfo();
60
- const [, major, minor, patch] = pkgVers.match(/^(\d+)\.(\d+)\.(\d+)$/) || [];
61
- if (!major || !minor || !patch) {
62
- throw new Error('Invalid version format in package.json');
146
+ const { name, version: pkgVersion } = getPkg();
147
+ const repoInfo = await getRepoInfo();
148
+ const { branch, revision } = repoInfo;
149
+ const releaseTag = await getReleaseTag();
150
+ const isRelease = await getIsRelease(release);
151
+ // A release build takes its version from the git tag that triggered it;
152
+ // everything else is a pre-release of the version in package.json.
153
+ const semver = (isRelease && releaseTag) || parseSemverTag(pkgVersion.split('-')[0]);
154
+ if (!semver) {
155
+ throw new Error(`Invalid version format in package.json: \`${pkgVersion}\``);
63
156
  }
157
+ const { major, minor, patch, prerelease } = semver;
64
158
  const build = getBuild();
65
- const isRelease = await getIsRelease(branch, release);
66
159
  const short = `${major}.${minor}.${patch}`;
67
- const npm = `${short}${isRelease ? '' : `-${branch}.${build}`}`;
68
- const info = `${short}${isRelease ? `+${build}.` : `-${branch}.${build}+`}${revision}`;
160
+ const npm = isRelease
161
+ ? semver.version
162
+ : `${short}-${getBuildSourceLabel(repoInfo)}.${build}`;
163
+ const info = `${npm}+${build}.${revision}`;
69
164
  version = {
70
165
  branch,
71
166
  build,
@@ -76,6 +171,8 @@ export async function getVersion(logIt = true, release = null) {
76
171
  name,
77
172
  npm,
78
173
  patch,
174
+ prerelease,
175
+ releaseTag: releaseTag?.tag || null,
79
176
  revision,
80
177
  short,
81
178
  };
@@ -85,6 +182,20 @@ export async function getVersion(logIt = true, release = null) {
85
182
  }
86
183
  return version;
87
184
  }
185
+ /**
186
+ * The npm dist-tag to publish under, following the branch/tag conventions:
187
+ * - release tag without pre-release identifiers => `latest`
188
+ * - release tag with pre-release identifiers => the first identifier (`rc`, `beta`, ...)
189
+ * - untagged build of the main branch => `next`
190
+ * - anything else => `branch`
191
+ */
192
+ export async function getDefaultNpmDistTag() {
193
+ const { branch, isRelease, prerelease } = await getVersion();
194
+ if (isRelease) {
195
+ return prerelease ? prerelease.split('.')[0] : 'latest';
196
+ }
197
+ return branch === getMainBranch() ? 'next' : 'branch';
198
+ }
88
199
  export async function getBanner() {
89
200
  const { copyright = '' } = getCfg();
90
201
  const { info, name } = await getVersion();
@@ -96,6 +207,6 @@ export async function getVersionCode() {
96
207
  export const version = ${JSON.stringify(await getVersion(), null, 2)}`;
97
208
  }
98
209
  export async function getUniqueBuildTag() {
99
- const { branch, build, revision } = await getVersion();
100
- return `${branch}-${revision}-${build}`;
210
+ const { build, revision, ...repoInfo } = await getVersion();
211
+ return `${getBuildSourceLabel({ ...repoInfo, revision })}-${revision}-${build}`;
101
212
  }
package/src/yarn.js CHANGED
@@ -6,7 +6,7 @@ import { envNpmCreds, getNpmConfig, npmGetVersions, npmWriteRc, } from './npm.js
6
6
  import { getPkg, getPkgName, getPkgSafeName } from './pkg.js';
7
7
  import { buildLog } from './run.js';
8
8
  import { generateFileHash } from './tgz.js';
9
- import { getDevBranch, getVersion } from './version.js';
9
+ import { getDefaultNpmDistTag, getVersion } from './version.js';
10
10
  export async function yarn({ args = [], spawnOptions, } = {}) {
11
11
  const localYarn = path.join(path.dirname(process.execPath), '/node_modules/yarn/bin/yarn.js');
12
12
  const spawnOpts = {
@@ -102,7 +102,7 @@ export async function yarnPublish({ npmAuthToken, npmConfig, npmCreds, npmPath,
102
102
  });
103
103
  }
104
104
  const existing = await npmGetVersions(getPkgName(true), npmPath);
105
- const { branch, isRelease, npm: npmVersion } = await getVersion();
105
+ const { npm: npmVersion } = await getVersion();
106
106
  if (existing.includes(npmVersion)) {
107
107
  if (skipExisting) {
108
108
  buildLog('npm package with this same version already exists. Skipping publish...');
@@ -117,12 +117,7 @@ export async function yarnPublish({ npmAuthToken, npmConfig, npmCreds, npmPath,
117
117
  '--new-version',
118
118
  npmVersion,
119
119
  '--tag',
120
- tag ||
121
- (isRelease
122
- ? 'latest'
123
- : branch === (await getDevBranch())
124
- ? 'next'
125
- : 'branch'),
120
+ tag || (await getDefaultNpmDistTag()),
126
121
  ...(access ? ['--access', access] : []),
127
122
  ],
128
123
  spawnOptions: {