aberlaas-release 2.20.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.
- package/lib/ensureNpmLogin.js +170 -0
- package/lib/ensureValidSetup.js +137 -0
- package/lib/getReleaseData.js +111 -0
- package/lib/main.js +52 -0
- package/lib/updateChangelog.js +150 -0
- package/lib/updateGitRepo.js +62 -0
- package/package.json +45 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { _ } from 'golgoth';
|
|
2
|
+
import {
|
|
3
|
+
consoleInfo,
|
|
4
|
+
consoleWarn,
|
|
5
|
+
env,
|
|
6
|
+
firostError,
|
|
7
|
+
prompt,
|
|
8
|
+
readJson,
|
|
9
|
+
run,
|
|
10
|
+
sleep,
|
|
11
|
+
wrap,
|
|
12
|
+
write,
|
|
13
|
+
} from 'firost';
|
|
14
|
+
import { hostGitPath, hostPackagePath } from 'aberlaas-helper';
|
|
15
|
+
|
|
16
|
+
export const __ = {
|
|
17
|
+
/**
|
|
18
|
+
* Ensures the user is logged in to npm by checking authentication status and prompting for login if needed.
|
|
19
|
+
* @returns {boolean|undefined} Returns true if already logged in, undefined if login was required and completed
|
|
20
|
+
* @throws {Error} Throws error if npm authentication fails for reasons other than E401 unauthorized
|
|
21
|
+
*/
|
|
22
|
+
async ensureNpmLogin() {
|
|
23
|
+
try {
|
|
24
|
+
await __.npmRun('whoami');
|
|
25
|
+
return true;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code == 'ABERLAAS_RELEASE_NPM_ERROR_E401') {
|
|
28
|
+
await __.waitForNpmLogin();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Executes an npm command and returns its output
|
|
37
|
+
* @param {string} command - The npm command to execute (without 'npm' prefix)
|
|
38
|
+
* @returns {string} The stdout output from the npm command
|
|
39
|
+
* @throws {Error} Throws a formatted error if the npm command fails
|
|
40
|
+
*/
|
|
41
|
+
async npmRun(command) {
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await __.run(`npm ${command}`, {
|
|
44
|
+
stdout: false,
|
|
45
|
+
stderr: false,
|
|
46
|
+
});
|
|
47
|
+
return stdout;
|
|
48
|
+
} catch ({ stderr }) {
|
|
49
|
+
const errorLines = _.split(stderr, '\n');
|
|
50
|
+
|
|
51
|
+
// Identify known npm errors
|
|
52
|
+
if (_.startsWith(errorLines[0], 'npm error code')) {
|
|
53
|
+
const npmErrorCode = _.replace(errorLines[0], 'npm error code ', '');
|
|
54
|
+
const npmErrorMessage = _.chain(errorLines).slice(1).join('\n').value();
|
|
55
|
+
throw firostError(
|
|
56
|
+
`ABERLAAS_RELEASE_NPM_ERROR_${npmErrorCode}`,
|
|
57
|
+
npmErrorMessage,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Throw unknown errors up
|
|
62
|
+
throw firostError('ABERLAAS_RELEASE_NPM_UNKNOWN_ERROR', stderr);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Prompts the user to create and configure an npm authentication token when not logged in.
|
|
68
|
+
* Opens the npm token creation page in browser, guides user through token setup,
|
|
69
|
+
* collects the token, and saves it to .npmrc file.
|
|
70
|
+
*/
|
|
71
|
+
async waitForNpmLogin() {
|
|
72
|
+
__.consoleWarn('You are not currently authentified to npm.');
|
|
73
|
+
|
|
74
|
+
await __.displayLoginInstructions();
|
|
75
|
+
await __.openBrowserForToken();
|
|
76
|
+
await __.saveNpmToken();
|
|
77
|
+
|
|
78
|
+
// Try again
|
|
79
|
+
await __.ensureNpmLogin();
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Displays login instructions to the user
|
|
84
|
+
*/
|
|
85
|
+
async displayLoginInstructions() {
|
|
86
|
+
const rootPackage = await readJson(hostPackagePath('package.json'));
|
|
87
|
+
const packageName = rootPackage.name;
|
|
88
|
+
const tokenName = __.generateTokenName(packageName);
|
|
89
|
+
|
|
90
|
+
__.consoleInfo('');
|
|
91
|
+
__.consoleInfo('Your npm token page will open.');
|
|
92
|
+
__.consoleInfo('We suggest you fill in the following informations:');
|
|
93
|
+
__.consoleInfo('');
|
|
94
|
+
__.consoleInfo('GENERAL');
|
|
95
|
+
__.consoleInfo(`Token name*: ${tokenName}`);
|
|
96
|
+
__.consoleInfo('☑️ Bypass two-factor authentication (2FA)');
|
|
97
|
+
__.consoleInfo('');
|
|
98
|
+
__.consoleInfo('PACKAGE AND SCOPES');
|
|
99
|
+
__.consoleInfo('Permissions: Read and write');
|
|
100
|
+
__.consoleInfo(`🔘 Only select packages and scopes: ${packageName}`);
|
|
101
|
+
__.consoleInfo('');
|
|
102
|
+
__.consoleInfo('EXPIRATION');
|
|
103
|
+
__.consoleInfo('Expiration date: 90 days');
|
|
104
|
+
__.consoleInfo('');
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generates a suggested token name from package name
|
|
109
|
+
* @param {string} packageName - The package name
|
|
110
|
+
* @returns {string} The suggested token name
|
|
111
|
+
*/
|
|
112
|
+
generateTokenName(packageName) {
|
|
113
|
+
const cleanPackageName = _.chain(packageName)
|
|
114
|
+
.replace('-', '_')
|
|
115
|
+
.replace('@', '')
|
|
116
|
+
.replace('/', '_')
|
|
117
|
+
.toUpper()
|
|
118
|
+
.value();
|
|
119
|
+
return `ABERLAAS_RELEASE_${cleanPackageName}`;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Opens browser for token creation
|
|
124
|
+
*/
|
|
125
|
+
async openBrowserForToken() {
|
|
126
|
+
const npmUsername = await __.getNpmUsername();
|
|
127
|
+
const tokenUrl = __.buildTokenUrl(npmUsername);
|
|
128
|
+
|
|
129
|
+
await __.prompt('Press Enter to open the webpage');
|
|
130
|
+
__.run(`$BROWSER ${tokenUrl}`, { shell: true, stderr: false });
|
|
131
|
+
await __.sleep(2000);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Gets the NPM username from environment variable or prompts user for input
|
|
136
|
+
* @returns {string} The NPM username
|
|
137
|
+
*/
|
|
138
|
+
async getNpmUsername() {
|
|
139
|
+
return (
|
|
140
|
+
__.env('ABERLAAS_NPM_USERNAME') || (await __.prompt('NPM Username: '))
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds the npm token creation URL for a given username
|
|
146
|
+
* @param {string} npmUsername - The npm username
|
|
147
|
+
* @returns {string} The token creation URL
|
|
148
|
+
*/
|
|
149
|
+
buildTokenUrl(npmUsername) {
|
|
150
|
+
return `https://www.npmjs.com/settings/${npmUsername}/tokens/granular-access-tokens/new`;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Saves the npm token to .npmrc file
|
|
155
|
+
*/
|
|
156
|
+
async saveNpmToken() {
|
|
157
|
+
const npmToken = await __.prompt('Enter you new token here:');
|
|
158
|
+
const npmrcContent = `//registry.npmjs.org/:_authToken=${npmToken}`;
|
|
159
|
+
const npmrcPath = hostGitPath('.npmrc');
|
|
160
|
+
await write(npmrcContent, npmrcPath);
|
|
161
|
+
},
|
|
162
|
+
run,
|
|
163
|
+
env,
|
|
164
|
+
prompt,
|
|
165
|
+
consoleWarn,
|
|
166
|
+
consoleInfo,
|
|
167
|
+
sleep,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const ensureNpmLogin = wrap(__, 'ensureNpmLogin');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { _ } from 'golgoth';
|
|
2
|
+
import { consoleInfo, firostError, run as firostRun } from 'firost';
|
|
3
|
+
import { hostGitRoot, yarnRun } from 'aberlaas-helper';
|
|
4
|
+
import Gilmore from 'gilmore';
|
|
5
|
+
import { ensureNpmLogin } from './ensureNpmLogin.js';
|
|
6
|
+
|
|
7
|
+
export const __ = {
|
|
8
|
+
/**
|
|
9
|
+
* Validates that the provided bump type is one of the accepted semantic
|
|
10
|
+
* versioning types, or empty for auto-detection.
|
|
11
|
+
* @param {object} cliArgs Release options
|
|
12
|
+
* @returns {void} - Returns nothing if valid, throws error if invalid
|
|
13
|
+
* @throws {Error} Throws ABERLAAS_RELEASE_UNKNOWN_BUMP_TYPE error if bumpType is provided but not 'patch', 'minor', or 'major'
|
|
14
|
+
*/
|
|
15
|
+
ensureCorrectBumpType(cliArgs) {
|
|
16
|
+
const bumpType = cliArgs._[0];
|
|
17
|
+
|
|
18
|
+
// If no bump type provided, allow it (will be auto-detected)
|
|
19
|
+
if (_.isEmpty(bumpType)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If bump type is provided, validate it
|
|
24
|
+
if (_.includes(['patch', 'minor', 'major'], bumpType)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw firostError(
|
|
29
|
+
'ABERLAAS_RELEASE_UNKNOWN_BUMP_TYPE',
|
|
30
|
+
'Bump type should be either major, minor or patch (or omitted for auto-detection)',
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ensures the repository is on the main branch before allowing a release
|
|
36
|
+
* @param {object} repo - The repository object with branch operations
|
|
37
|
+
* @returns {Promise<boolean>} True if on main branch
|
|
38
|
+
* @throws {Error} Throws ABERLAAS_RELEASE_NOT_ON_MAIN_BRANCH error if not on main branch
|
|
39
|
+
*/
|
|
40
|
+
async ensureCorrectBranch(repo) {
|
|
41
|
+
const currentBranch = await repo.currentBranchName();
|
|
42
|
+
if (currentBranch == 'main') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
throw firostError(
|
|
46
|
+
'ABERLAAS_RELEASE_NOT_ON_MAIN_BRANCH',
|
|
47
|
+
'You must be on branch main to release.',
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ensures the repository has no uncommitted changes before proceeding with operations
|
|
53
|
+
* @param {object} repo - The repository object to check status on
|
|
54
|
+
* @returns {Promise<boolean>} Returns true if repository is clean
|
|
55
|
+
* @throws {Error} Throws ABERLAAS_RELEASE_NOT_CLEAN_DIRECTORY error if uncommitted changes exist
|
|
56
|
+
*/
|
|
57
|
+
async ensureCleanRepository(repo) {
|
|
58
|
+
const changes = await repo.status();
|
|
59
|
+
if (_.isEmpty(changes)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
throw firostError(
|
|
63
|
+
'ABERLAAS_RELEASE_NOT_CLEAN_DIRECTORY',
|
|
64
|
+
'Your working directory must be clean, with no uncommitted changes.',
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ensures that all tests are passing before proceeding with a release
|
|
70
|
+
* @param {object} cliArgs Release options
|
|
71
|
+
* @returns {Promise<void>} A promise that resolves if tests pass, rejects with ABERLAAS_RELEASE_TESTS_FAILING error if tests fail
|
|
72
|
+
*/
|
|
73
|
+
async ensureTestsArePassing(cliArgs = {}) {
|
|
74
|
+
if (cliArgs['skip-test']) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
__.consoleInfo('Running tests...');
|
|
78
|
+
try {
|
|
79
|
+
await __.yarnRun('test --failFast');
|
|
80
|
+
return true;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw firostError('ABERLAAS_RELEASE_TESTS_FAILING', err.message);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ensures that linting passes by running the lint process and throwing an error if it fails
|
|
88
|
+
* @param {object} cliArgs Release options
|
|
89
|
+
* @returns {Promise<void>} A promise that resolves if linting passes
|
|
90
|
+
* @throws {Error} Throws ABERLAAS_RELEASE_LINT_FAILING error if linting fails
|
|
91
|
+
*/
|
|
92
|
+
async ensureLintIsPassing(cliArgs = {}) {
|
|
93
|
+
if (cliArgs['skip-lint']) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
__.consoleInfo('Running lint...');
|
|
97
|
+
try {
|
|
98
|
+
await __.yarnRun('lint');
|
|
99
|
+
return true;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw firostError('ABERLAAS_RELEASE_LINT_FAILING', err.message);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
consoleInfo,
|
|
106
|
+
ensureNpmLogin,
|
|
107
|
+
firostRun,
|
|
108
|
+
yarnRun,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validate all pre-conditions before starting the release
|
|
113
|
+
* @param {object} cliArgs Release options
|
|
114
|
+
* @param {boolean} cliArgs.skipTest Skip test execution
|
|
115
|
+
* @param {boolean} cliArgs.skipLint Skip lint execution
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
export async function ensureValidSetup(cliArgs = {}) {
|
|
119
|
+
__.ensureCorrectBumpType(cliArgs);
|
|
120
|
+
|
|
121
|
+
const repo = new Gilmore(hostGitRoot());
|
|
122
|
+
|
|
123
|
+
// Need to be on branch main
|
|
124
|
+
await __.ensureCorrectBranch(repo);
|
|
125
|
+
|
|
126
|
+
// Need to have a clean directory
|
|
127
|
+
await __.ensureCleanRepository(repo);
|
|
128
|
+
|
|
129
|
+
// Check npm login
|
|
130
|
+
await __.ensureNpmLogin();
|
|
131
|
+
|
|
132
|
+
// Check tests are passing
|
|
133
|
+
await __.ensureTestsArePassing(cliArgs);
|
|
134
|
+
|
|
135
|
+
// Check lint is passing
|
|
136
|
+
await __.ensureLintIsPassing(cliArgs);
|
|
137
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { _, pMap } from 'golgoth';
|
|
2
|
+
import { glob, readJson } from 'firost';
|
|
3
|
+
import { hostGitPath, hostGitRoot } from 'aberlaas-helper';
|
|
4
|
+
import { getGitDiff, parseCommits } from 'changelogen';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
|
|
7
|
+
export const __ = {
|
|
8
|
+
/**
|
|
9
|
+
* Gets all packages that need to be released
|
|
10
|
+
* @returns {Array<{filepath: string, content: object}>} Array of packages with their filepath and content
|
|
11
|
+
*/
|
|
12
|
+
async getAllPackagesToRelease() {
|
|
13
|
+
const rootPackagePath = hostGitPath('package.json');
|
|
14
|
+
const rootPackageContent = await readJson(rootPackagePath);
|
|
15
|
+
const workspaces = rootPackageContent.workspaces;
|
|
16
|
+
|
|
17
|
+
// If no workspaces, this is the package to publish
|
|
18
|
+
if (!workspaces) {
|
|
19
|
+
if (rootPackageContent.private) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
filepath: rootPackagePath,
|
|
25
|
+
content: rootPackageContent,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If workspaces, we get the packages of all those workspaces
|
|
31
|
+
const rootPath = hostGitRoot();
|
|
32
|
+
const rawList = await pMap(workspaces, async (workspacePattern) => {
|
|
33
|
+
const packagesPath = await glob(
|
|
34
|
+
`${rootPath}/${workspacePattern}/package.json`,
|
|
35
|
+
);
|
|
36
|
+
const packagesData = await pMap(packagesPath, async (filepath) => {
|
|
37
|
+
const content = await readJson(filepath);
|
|
38
|
+
if (content.private) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
filepath,
|
|
43
|
+
content,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
return _.compact(packagesData);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return _.flatten(rawList);
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Determines the appropriate semantic version bump type based on CLI arguments or git commit analysis
|
|
53
|
+
* @param {object} [cliArgs={}] - Command line arguments object containing potential bump type
|
|
54
|
+
* @param {string} currentVersion - The current version to compare commits against
|
|
55
|
+
* @returns {Promise<string>} The bump type: 'major', 'minor', or 'patch'
|
|
56
|
+
*/
|
|
57
|
+
async getBumpType(cliArgs = {}, currentVersion) {
|
|
58
|
+
const argFromCli = cliArgs._[0];
|
|
59
|
+
if (argFromCli) {
|
|
60
|
+
return argFromCli;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Getting all commits since last version tag
|
|
64
|
+
const rawCommits = await getGitDiff(
|
|
65
|
+
`v${currentVersion}`,
|
|
66
|
+
'HEAD',
|
|
67
|
+
hostGitRoot(),
|
|
68
|
+
);
|
|
69
|
+
// This is the minimal object required by changelogen
|
|
70
|
+
const minimalConfig = { scopeMap: {} };
|
|
71
|
+
const commits = parseCommits(rawCommits, minimalConfig);
|
|
72
|
+
|
|
73
|
+
// If any commit has breaking changes: major
|
|
74
|
+
const hasBreakingChanges = _.some(commits, { isBreaking: true });
|
|
75
|
+
if (hasBreakingChanges) {
|
|
76
|
+
return 'major';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If any commit adds a feature: minor
|
|
80
|
+
const hasFeature = _.some(commits, { type: 'feat' });
|
|
81
|
+
if (hasFeature) {
|
|
82
|
+
return 'minor';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Anything else: patch
|
|
86
|
+
return 'patch';
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gathers all release information from CLI arguments
|
|
92
|
+
* @param {object} cliArgs - CLI arguments from minimist
|
|
93
|
+
* @returns {object} Release data containing bumpType, allPackages, currentVersion, newVersion, skipChangelog
|
|
94
|
+
*/
|
|
95
|
+
export async function getReleaseData(cliArgs) {
|
|
96
|
+
const allPackages = await __.getAllPackagesToRelease();
|
|
97
|
+
const currentVersion = allPackages[0].content.version;
|
|
98
|
+
|
|
99
|
+
const bumpType = await __.getBumpType(cliArgs, currentVersion);
|
|
100
|
+
|
|
101
|
+
const newVersion = semver.inc(currentVersion, bumpType);
|
|
102
|
+
const skipChangelog = !!cliArgs['skip-changelog'];
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
bumpType,
|
|
106
|
+
allPackages,
|
|
107
|
+
currentVersion,
|
|
108
|
+
newVersion,
|
|
109
|
+
skipChangelog,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/lib/main.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pMap } from 'golgoth';
|
|
3
|
+
import { consoleInfo, run as firostRun } from 'firost';
|
|
4
|
+
import { ensureValidSetup } from './ensureValidSetup.js';
|
|
5
|
+
import { getReleaseData } from './getReleaseData.js';
|
|
6
|
+
import { updateGitRepo } from './updateGitRepo.js';
|
|
7
|
+
|
|
8
|
+
export let __;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wrapper to release the current module(s)
|
|
12
|
+
* @param {object} cliArgs CLI Argument object, as created by minimist
|
|
13
|
+
* @returns {boolean} True on success
|
|
14
|
+
*/
|
|
15
|
+
export async function run(cliArgs = {}) {
|
|
16
|
+
await __.ensureValidSetup(cliArgs);
|
|
17
|
+
|
|
18
|
+
const releaseData = await __.getReleaseData(cliArgs);
|
|
19
|
+
__.consoleInfo(`Release new version ${releaseData.newVersion}`);
|
|
20
|
+
|
|
21
|
+
await __.updateGitRepo(releaseData);
|
|
22
|
+
|
|
23
|
+
await __.publishAllPackagesToNpm(releaseData);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
__ = {
|
|
27
|
+
/**
|
|
28
|
+
* Publishes all packages to npm
|
|
29
|
+
* @param {object} releaseData - Release data containing allPackages
|
|
30
|
+
*/
|
|
31
|
+
async publishAllPackagesToNpm(releaseData) {
|
|
32
|
+
await pMap(
|
|
33
|
+
releaseData.allPackages,
|
|
34
|
+
async ({ filepath, content }) => {
|
|
35
|
+
const packageName = content.name;
|
|
36
|
+
__.consoleInfo(`Publishing ${packageName} to npm`);
|
|
37
|
+
|
|
38
|
+
const packageDir = path.dirname(filepath);
|
|
39
|
+
await __.firostRun('npm publish --access public', { cwd: packageDir });
|
|
40
|
+
},
|
|
41
|
+
{ concurrency: 1 },
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
ensureValidSetup,
|
|
46
|
+
updateGitRepo,
|
|
47
|
+
getReleaseData,
|
|
48
|
+
consoleInfo,
|
|
49
|
+
firostRun,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default { run };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { _ } from 'golgoth';
|
|
2
|
+
import {
|
|
3
|
+
consoleInfo,
|
|
4
|
+
exists,
|
|
5
|
+
firostError,
|
|
6
|
+
read,
|
|
7
|
+
run,
|
|
8
|
+
select,
|
|
9
|
+
write,
|
|
10
|
+
} from 'firost';
|
|
11
|
+
import { hostGitPath, hostGitRoot } from 'aberlaas-helper';
|
|
12
|
+
import { generateMarkDown, getGitDiff, parseCommits } from 'changelogen';
|
|
13
|
+
import cliMarkdown from 'cli-markdown';
|
|
14
|
+
|
|
15
|
+
export const __ = {
|
|
16
|
+
/**
|
|
17
|
+
* Generate changelog markdown from git commits between two versions
|
|
18
|
+
* @param {object} releaseData - Release data containing currentVersion, newVersion, and skipChangelog
|
|
19
|
+
* @returns {string} Generated changelog markdown
|
|
20
|
+
*/
|
|
21
|
+
async generateChangelogFromGit(releaseData) {
|
|
22
|
+
const { currentVersion, newVersion } = releaseData;
|
|
23
|
+
const gitRoot = hostGitRoot();
|
|
24
|
+
const currentVersionTag = `v${currentVersion}`;
|
|
25
|
+
|
|
26
|
+
// Get config
|
|
27
|
+
const config = {
|
|
28
|
+
from: currentVersionTag,
|
|
29
|
+
to: 'HEAD',
|
|
30
|
+
newVersion,
|
|
31
|
+
noAuthors: true,
|
|
32
|
+
types: {
|
|
33
|
+
feat: { title: 'Features', semver: 'minor' },
|
|
34
|
+
fix: { title: 'Bug Fixes', semver: 'patch' },
|
|
35
|
+
perf: { title: 'Performance', semver: 'patch' },
|
|
36
|
+
},
|
|
37
|
+
templates: {
|
|
38
|
+
tagMessage: 'v{{newVersion}}',
|
|
39
|
+
tagBody: 'v{{newVersion}}',
|
|
40
|
+
},
|
|
41
|
+
// Note: This scopeMap key is required by changelogen
|
|
42
|
+
scopeMap: {},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const rawCommits = await getGitDiff(currentVersionTag, 'HEAD', gitRoot);
|
|
46
|
+
const commits = parseCommits(rawCommits, config);
|
|
47
|
+
|
|
48
|
+
// Filter commits to only keep user-facing types
|
|
49
|
+
const allowedTypes = _.keys(config.types);
|
|
50
|
+
const filteredCommits = _.filter(commits, (commit) => {
|
|
51
|
+
return _.includes(allowedTypes, commit.type);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Sort commits by type order
|
|
55
|
+
const typeOrder = { feat: 0, fix: 1, perf: 2 };
|
|
56
|
+
const sortedCommits = _.sortBy(filteredCommits, (commit) => {
|
|
57
|
+
return typeOrder[commit.type];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Generate markdown
|
|
61
|
+
return await generateMarkDown(sortedCommits, config);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Display the changelog and ask the user to accept/edit/cancel.
|
|
66
|
+
* @param {string} changelog - The changelog content to display and confirm
|
|
67
|
+
* @param {object} selectOptions - Options object with input stream for testing
|
|
68
|
+
* @returns {string} The approved (and possibly edited) changelog content
|
|
69
|
+
*/
|
|
70
|
+
async confirmOrEditChangelog(changelog, selectOptions = {}) {
|
|
71
|
+
__.consoleInfo('CHANGELOG:');
|
|
72
|
+
|
|
73
|
+
__.consoleLog(_.repeat('━', 60));
|
|
74
|
+
__.consoleLog(__.cliMarkdown(changelog));
|
|
75
|
+
__.consoleLog(_.repeat('━', 60));
|
|
76
|
+
|
|
77
|
+
const nextStep = await __.select(
|
|
78
|
+
'What to do?',
|
|
79
|
+
[
|
|
80
|
+
{ name: '✅ Approve', value: 'approve' },
|
|
81
|
+
{ name: '📝 Edit', value: 'edit' },
|
|
82
|
+
{ name: '⛔️ Cancel', value: 'cancel' },
|
|
83
|
+
],
|
|
84
|
+
selectOptions,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (nextStep == 'cancel') {
|
|
88
|
+
throw firostError(
|
|
89
|
+
'ABERLAAS_RELEASE_CHANGELOG_CANCELLED',
|
|
90
|
+
'Release cancelled by user',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (nextStep === 'approve') {
|
|
95
|
+
return changelog;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Save the changelog in temp file and edit it
|
|
99
|
+
const changelogFilepath = hostGitPath('./tmp/CHANGELOG.md');
|
|
100
|
+
await write(changelog, changelogFilepath);
|
|
101
|
+
|
|
102
|
+
await __.run(`$EDITOR ${changelogFilepath}`, {
|
|
103
|
+
stdin: true,
|
|
104
|
+
shell: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const newChangelog = await read(changelogFilepath);
|
|
108
|
+
return await __.confirmOrEditChangelog(newChangelog, selectOptions);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Prepends new changelog content to existing CHANGELOG.md file
|
|
113
|
+
* @param {string} newChangelog - The new changelog content to prepend
|
|
114
|
+
*/
|
|
115
|
+
async addToExistingChangelogFile(newChangelog) {
|
|
116
|
+
const changelogPath = hostGitPath('CHANGELOG.md');
|
|
117
|
+
|
|
118
|
+
if (!(await exists(changelogPath))) {
|
|
119
|
+
await write(newChangelog, changelogPath);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const existingChangelog = await read(changelogPath);
|
|
124
|
+
const updatedChangelog = `${newChangelog}\n\n${existingChangelog}`;
|
|
125
|
+
await write(updatedChangelog, changelogPath);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
consoleInfo,
|
|
129
|
+
consoleLog(input) {
|
|
130
|
+
console.log(input);
|
|
131
|
+
},
|
|
132
|
+
select,
|
|
133
|
+
cliMarkdown,
|
|
134
|
+
run,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Update the CHANGELOG.md file with new additions
|
|
139
|
+
* @param {object} releaseData - Release data containing currentVersion, newVersion, and skipChangelog
|
|
140
|
+
*/
|
|
141
|
+
export async function updateChangelog(releaseData) {
|
|
142
|
+
if (releaseData.skipChangelog) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const suggestedChangelog = await __.generateChangelogFromGit(releaseData);
|
|
147
|
+
const approvedChangelog = await __.confirmOrEditChangelog(suggestedChangelog);
|
|
148
|
+
|
|
149
|
+
await __.addToExistingChangelogFile(approvedChangelog);
|
|
150
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { pMap } from 'golgoth';
|
|
2
|
+
import { consoleInfo, writeJson } from 'firost';
|
|
3
|
+
import { hostGitRoot } from 'aberlaas-helper';
|
|
4
|
+
import Gilmore from 'gilmore';
|
|
5
|
+
import { updateChangelog } from './updateChangelog.js';
|
|
6
|
+
|
|
7
|
+
export const __ = {
|
|
8
|
+
/**
|
|
9
|
+
* Bumps the version of all packages to the new version
|
|
10
|
+
* @param {object} releaseData - Release data containing allPackages and newVersion
|
|
11
|
+
*/
|
|
12
|
+
async bumpAllPackageVersions(releaseData) {
|
|
13
|
+
await pMap(releaseData.allPackages, async ({ filepath, content }) => {
|
|
14
|
+
const packageName = content.name;
|
|
15
|
+
__.consoleInfo(`Updating ${packageName} to ${releaseData.newVersion}`);
|
|
16
|
+
const newContent = { ...content, version: releaseData.newVersion };
|
|
17
|
+
await writeJson(newContent, filepath, {
|
|
18
|
+
sort: false,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a commit, tag, and pushes to remote repository
|
|
25
|
+
* @param {object} releaseData - Release data containing newVersion
|
|
26
|
+
*/
|
|
27
|
+
async commitTagAndPush(releaseData) {
|
|
28
|
+
const gitRoot = hostGitRoot();
|
|
29
|
+
const repo = new Gilmore(gitRoot);
|
|
30
|
+
|
|
31
|
+
// Create commit
|
|
32
|
+
__.consoleInfo(
|
|
33
|
+
`Creating new commit for version v${releaseData.newVersion}`,
|
|
34
|
+
);
|
|
35
|
+
await repo.commitAll(`v${releaseData.newVersion}`, { skipHook: true });
|
|
36
|
+
|
|
37
|
+
// Create tag
|
|
38
|
+
await repo.createTag(`v${releaseData.newVersion}`);
|
|
39
|
+
|
|
40
|
+
// Push to remote
|
|
41
|
+
__.consoleInfo('Pushing to remote repository');
|
|
42
|
+
await repo.push();
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
consoleInfo,
|
|
46
|
+
updateChangelog,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update git repository with all changes for the release
|
|
51
|
+
* @param {object} releaseData - Release data containing currentVersion, newVersion, skipChangelog, and allPackages
|
|
52
|
+
*/
|
|
53
|
+
export async function updateGitRepo(releaseData) {
|
|
54
|
+
// Update CHANGELOG.md
|
|
55
|
+
await __.updateChangelog(releaseData);
|
|
56
|
+
|
|
57
|
+
// Update all .version keys in packages
|
|
58
|
+
await __.bumpAllPackageVersions(releaseData);
|
|
59
|
+
|
|
60
|
+
// Commit and push to remote
|
|
61
|
+
await __.commitTagAndPush(releaseData);
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aberlaas-release",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"description": "aberlaas release command: Release and publish new versions",
|
|
6
|
+
"version": "2.20.1",
|
|
7
|
+
"repository": "pixelastic/aberlaas",
|
|
8
|
+
"homepage": "https://projects.pixelastic.com/aberlaas/",
|
|
9
|
+
"author": "Tim Carry (@pixelastic)",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"lib/*.js"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./lib/main.js"
|
|
16
|
+
},
|
|
17
|
+
"main": "./lib/main.js",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.18.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"aberlaas-helper": "workspace:*",
|
|
23
|
+
"aberlaas-lint": "workspace:*",
|
|
24
|
+
"aberlaas-test": "workspace:*",
|
|
25
|
+
"changelogen": "0.6.2",
|
|
26
|
+
"cli-markdown": "3.5.1",
|
|
27
|
+
"firost": "5.5.1",
|
|
28
|
+
"gilmore": "1.2.0",
|
|
29
|
+
"golgoth": "3.0.0",
|
|
30
|
+
"semver": "7.7.3"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "cd ../docs && yarn run build",
|
|
34
|
+
"build:prod": "cd ../docs && yarn run build:prod",
|
|
35
|
+
"cms": "cd ../docs && yarn run cms",
|
|
36
|
+
"serve": "cd ../docs && yarn run serve",
|
|
37
|
+
"release": "cd ../.. && ./scripts/release",
|
|
38
|
+
"test:meta": "cd ../.. && ./scripts/test-meta",
|
|
39
|
+
"test": "cd ../.. && ./scripts/test",
|
|
40
|
+
"test:watch": "cd ../.. && ./scripts/test-watch",
|
|
41
|
+
"compress": "cd ../.. && ./scripts/compress",
|
|
42
|
+
"lint": "cd ../.. && ./scripts/lint",
|
|
43
|
+
"lint:fix": "cd ../.. && ./scripts/lint-fix"
|
|
44
|
+
}
|
|
45
|
+
}
|