@omnitoolkit/releasebot 7.3.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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # releasebot
2
+
3
+ This program analyzes a git repository and, if necessary, performs a release according to the specifications of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and [semantic versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ It uses [semantic-release](https://github.com/semantic-release/semantic-release) (see [documentation](https://semantic-release.gitbook.io/semantic-release)) inside a Docker image with a custom-made module (see [index.js](src/index.js)). There, several plugins along with their configuration are defined and loaded on execution:
6
+
7
+ - [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer) reads each git commit added since the last release, determines if a new release is due, the type of release and the new version number. It analyzes the syntax according to the conventionalcommits preset in [parser.js](https://github.com/conventional-changelog/conventional-changelog/blob/conventional-changelog-conventionalcommits-v9.0.0/packages/conventional-changelog-conventionalcommits/src/parser.js) and checks for custom rules combined from the [default rules list](https://github.com/semantic-release/commit-analyzer/blob/v13.0.1/lib/default-release-rules.js) and the [@commitlint/config-conventional type list](https://github.com/conventional-changelog/commitlint/blob/v19.8.1/%40commitlint/config-conventional/README.md#type-enum). A commit should look like this:
8
+ ```
9
+ <type>(<scope>)!: <short summary>
10
+ │ │ │ │
11
+ │ │ │ └─> Summary in present tense. Not capitalized. No period at the end.
12
+ │ │ │
13
+ │ │ └─> Indicator for breaking change. Optional.
14
+ │ │
15
+ │ └─> Commit scope. Can be any string. Optional.
16
+
17
+ └─> Commit type: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test
18
+ No release: x
19
+ Patch release: x x x x x x x x x
20
+ Minor release: x
21
+ Major release: (any type when used with '!' indicator)
22
+ ```
23
+ (chart from [angular](https://github.com/angular/angular/blob/19.0.0/CONTRIBUTING.md#commit-message-header), changed)
24
+ - [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator) generates new release notes from the collected information according to the conventionalcommits preset in [writer.js](https://github.com/conventional-changelog/conventional-changelog/blob/conventional-changelog-conventionalcommits-v9.0.0/packages/conventional-changelog-conventionalcommits/src/writer.js) and the default commit types in [constants.js](https://github.com/conventional-changelog/conventional-changelog/blob/conventional-changelog-conventionalcommits-v9.0.0/packages/conventional-changelog-conventionalcommits/src/constants.js), which are changed to the combined custom rules from above.
25
+ - [@semantic-release/changelog](https://github.com/semantic-release/changelog) updates the changelog file `CHANGELOG.md` with the generated release notes.
26
+ - [@semantic-release/exec](https://github.com/semantic-release/exec) updates the file `VERSION` with the new version number (only if `VERSION` exists) and executes the command `./version.sh X.Y.Z` (only if `version.sh` exists). Both are optional, the latter can be used to set the new version number in other files in the repository.
27
+ - [@semantic-release/git](https://github.com/semantic-release/git) adds a new git commit with the above file changes, places a new git tag (new version number with prefixed `v`) and pushes the changes to the git remote.
28
+ - [@semantic-release/gitlab](https://github.com/semantic-release/gitlab) creates a GitLab release via API with the generated release notes.
29
+
30
+ ## Requirements
31
+
32
+ - Git repository hosted on GitLab SaaS or self-managed
33
+ - GitLab CI/CD enabled and variable `GITLAB_TOKEN` set (can either be a personal, project or group access token with the scopes `api` and `write_repository`)
34
+ - Willingness of all contributors to follow the commit syntax above
35
+
36
+ ## Usage
37
+
38
+ - In your `.gitlab-ci.yml`, define the stages explicitly and add a `release` stage at the end of the list. This ensures that the release runs only on a (to this point) successful pipeline.
39
+ ```yaml
40
+ stages:
41
+ - build
42
+ - test
43
+ - deploy
44
+ - release
45
+ ```
46
+ - Then add the following job and replace `main` with the desired version from the [releases](https://gitlab.com/omnitoolkit/omnitoolkit/-/releases) page:
47
+ ```yaml
48
+ release_version:
49
+ image: registry.gitlab.com/omnitoolkit/omnitoolkit/releasebot:main
50
+ stage: release
51
+ rules:
52
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
53
+ script:
54
+ - releasebot --branches ${CI_DEFAULT_BRANCH}
55
+ ```
56
+ - If `CI_DEFAULT_BRANCH` is not your release branch, replace it with the correct one.
57
+ - If you don't want to release on every push to the release branch, add `when: manual` to the job. You can now start the job on demand from the UI.
package/bin/releasebot ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("../src/cli.js").main().catch((error) => {
4
+ process.stderr.write(`${error.message}\n`);
5
+ process.exitCode = 1;
6
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "bin": {
3
+ "releasebot": "./bin/releasebot"
4
+ },
5
+ "dependencies": {
6
+ "@semantic-release/changelog": "6.0.3",
7
+ "@semantic-release/commit-analyzer": "13.0.1",
8
+ "@semantic-release/exec": "7.1.0",
9
+ "@semantic-release/git": "10.0.1",
10
+ "@semantic-release/gitlab": "13.3.2",
11
+ "@semantic-release/release-notes-generator": "14.1.0",
12
+ "conventional-changelog-conventionalcommits": "9.3.1",
13
+ "semantic-release": "25.0.3"
14
+ },
15
+ "description": "Opinionated GitLab semantic-release CLI.",
16
+ "files": [
17
+ "bin/releasebot",
18
+ "src/cli.js",
19
+ "src/constants.js",
20
+ "src/index.js"
21
+ ],
22
+ "homepage": "https://gitlab.com/omnitoolkit/omnitoolkit/-/tree/main/apps/releasebot#readme",
23
+ "license": "Apache-2.0",
24
+ "name": "@omnitoolkit/releasebot",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "directory": "apps/releasebot",
30
+ "type": "git",
31
+ "url": "git+https://gitlab.com/omnitoolkit/omnitoolkit.git"
32
+ },
33
+ "scripts": {
34
+ "test": "node --test"
35
+ },
36
+ "version": "7.3.1"
37
+ }
package/src/cli.js ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const fs = require("node:fs/promises");
6
+ const path = require("node:path");
7
+ const { execFile: execFileCallback, spawn: spawnCallback } = require("node:child_process");
8
+ const { promisify } = require("node:util");
9
+
10
+ const defaultExecFile = promisify(execFileCallback);
11
+ const defaultConfigPath = path.join(__dirname, "index.js");
12
+
13
+ function inferGitLabUrl({ env, remoteUrl }) {
14
+ if (env.CI_SERVER_URL) {
15
+ return env.CI_SERVER_URL;
16
+ }
17
+
18
+ const sshMatch = remoteUrl.match(/^[^@]+@([^:]+):/);
19
+
20
+ if (sshMatch) {
21
+ return `https://${sshMatch[1]}`;
22
+ }
23
+
24
+ return new URL(remoteUrl).origin;
25
+ }
26
+
27
+ async function gitOutput(execFile, args) {
28
+ const result = await execFile("git", args, { encoding: "utf8" });
29
+ return result.stdout.trim();
30
+ }
31
+
32
+ async function readGitMetadata(execFile) {
33
+ const remote = await gitOutput(execFile, ["remote", "get-url", "origin"]);
34
+ const branch = await gitOutput(execFile, [
35
+ "rev-parse",
36
+ "--symbolic-full-name",
37
+ "--abbrev-ref",
38
+ "HEAD"
39
+ ]);
40
+ const commit = await gitOutput(execFile, ["rev-parse", "--short=8", "HEAD"]);
41
+
42
+ if (!remote || !branch || !commit) {
43
+ throw new Error("missing git metadata");
44
+ }
45
+
46
+ return {
47
+ branch,
48
+ commit,
49
+ remote
50
+ };
51
+ }
52
+
53
+ async function fetchGitLabUser({ fetchImpl, gitlabUrl, token }) {
54
+ const response = await fetchImpl(`${gitlabUrl}/api/v4/user`, {
55
+ headers: {
56
+ "PRIVATE-TOKEN": token
57
+ }
58
+ });
59
+ const user = await response.json();
60
+ const email = user.commit_email || "";
61
+ const name = user.name || "";
62
+
63
+ if (!email || !name) {
64
+ throw new Error("missing GitLab user identity");
65
+ }
66
+
67
+ return {
68
+ email,
69
+ name
70
+ };
71
+ }
72
+
73
+ function resolveSemanticReleaseBin() {
74
+ return require.resolve("semantic-release/bin/semantic-release.js");
75
+ }
76
+
77
+ function nodePathEnv(existingNodePath) {
78
+ const paths = module.paths.join(path.delimiter);
79
+
80
+ if (!existingNodePath) {
81
+ return paths;
82
+ }
83
+
84
+ return `${paths}${path.delimiter}${existingNodePath}`;
85
+ }
86
+
87
+ function indent(text) {
88
+ return text
89
+ .trimEnd()
90
+ .split("\n")
91
+ .map((line) => ` ${line}`)
92
+ .join("\n");
93
+ }
94
+
95
+ function writeStartupSummary({ argv, configText, env, gitMetadata, stdout }) {
96
+ const extraParameters = argv.length > 0 ? argv.join(" ") : "NONE";
97
+
98
+ stdout.write("\n");
99
+ stdout.write("++++++++++++++++++++++++++++++++++++++++\n");
100
+ stdout.write("\n");
101
+ stdout.write("starting sematic-release on git repository\n");
102
+ stdout.write("\n");
103
+ stdout.write(` remote: ${gitMetadata.remote}\n`);
104
+ stdout.write(` branch: ${gitMetadata.branch}\n`);
105
+ stdout.write(` commit: ${gitMetadata.commit}\n`);
106
+ stdout.write("\n");
107
+ stdout.write("with env vars\n");
108
+ stdout.write("\n");
109
+ stdout.write(" GITLAB_TOKEN = [MASKED]\n");
110
+ stdout.write(` GITLAB_URL = ${env.GITLAB_URL}\n`);
111
+ stdout.write(` GIT_AUTHOR_EMAIL = ${env.GIT_AUTHOR_EMAIL}\n`);
112
+ stdout.write(` GIT_AUTHOR_NAME = ${env.GIT_AUTHOR_NAME}\n`);
113
+ stdout.write(` GIT_COMMITTER_EMAIL = ${env.GIT_COMMITTER_EMAIL}\n`);
114
+ stdout.write(` GIT_COMMITTER_NAME = ${env.GIT_COMMITTER_NAME}\n`);
115
+ stdout.write("\n");
116
+ stdout.write("with config\n");
117
+ stdout.write("\n");
118
+ stdout.write(`${indent(configText)}\n`);
119
+ stdout.write("\n");
120
+ stdout.write("with extra parameters\n");
121
+ stdout.write("\n");
122
+ stdout.write(`${indent(extraParameters)}\n`);
123
+ stdout.write("\n");
124
+ stdout.write("++++++++++++++++++++++++++++++++++++++++\n");
125
+ stdout.write("\n");
126
+ }
127
+
128
+ function spawnProcess(command, args, options) {
129
+ return new Promise((resolve, reject) => {
130
+ const child = spawnCallback(command, args, {
131
+ ...options,
132
+ stdio: "inherit"
133
+ });
134
+
135
+ child.on("error", reject);
136
+ child.on("close", (exitCode) => {
137
+ resolve({ exitCode: exitCode ?? 1 });
138
+ });
139
+ });
140
+ }
141
+
142
+ async function runReleasebot(options = {}) {
143
+ const argv = options.argv || [];
144
+ const configPath = options.configPath || defaultConfigPath;
145
+ const env = options.env || process.env;
146
+ const execFile = options.execFile || defaultExecFile;
147
+ const fetchImpl = options.fetch || globalThis.fetch;
148
+ const readFile = options.readFile || fs.readFile;
149
+ const spawn = options.spawn || spawnProcess;
150
+ const stderr = options.stderr || process.stderr;
151
+ const stdout = options.stdout || process.stdout;
152
+
153
+ if (!env.GITLAB_TOKEN) {
154
+ stderr.write("error - GITLAB_TOKEN not found in env vars\n");
155
+ return 1;
156
+ }
157
+
158
+ let gitMetadata;
159
+
160
+ try {
161
+ gitMetadata = await readGitMetadata(execFile);
162
+ } catch (_error) {
163
+ stderr.write("error - could not interact with git - are you not inside a git repository?\n");
164
+ return 1;
165
+ }
166
+
167
+ const gitlabUrl = inferGitLabUrl({
168
+ env,
169
+ remoteUrl: gitMetadata.remote
170
+ });
171
+
172
+ let gitLabUser;
173
+
174
+ try {
175
+ gitLabUser = await fetchGitLabUser({
176
+ fetchImpl,
177
+ gitlabUrl,
178
+ token: env.GITLAB_TOKEN
179
+ });
180
+ } catch (_error) {
181
+ stderr.write("error - could not get author email and name from gitlab - your GITLAB_TOKEN might be broken\n");
182
+ return 1;
183
+ }
184
+
185
+ const releaseEnv = {
186
+ ...env,
187
+ GITLAB_URL: gitlabUrl,
188
+ GIT_AUTHOR_EMAIL: gitLabUser.email,
189
+ GIT_AUTHOR_NAME: gitLabUser.name,
190
+ GIT_COMMITTER_EMAIL: gitLabUser.email,
191
+ GIT_COMMITTER_NAME: gitLabUser.name,
192
+ NODE_PATH: nodePathEnv(env.NODE_PATH)
193
+ };
194
+ const configText = await readFile(configPath, "utf8");
195
+
196
+ writeStartupSummary({
197
+ argv,
198
+ configText,
199
+ env: releaseEnv,
200
+ gitMetadata,
201
+ stdout
202
+ });
203
+
204
+ const result = await spawn(
205
+ process.execPath,
206
+ [options.semanticReleaseBin || resolveSemanticReleaseBin(), "--extends", configPath, ...argv],
207
+ {
208
+ env: releaseEnv
209
+ }
210
+ );
211
+
212
+ if (typeof result === "number") {
213
+ return result;
214
+ }
215
+
216
+ return result.exitCode ?? 1;
217
+ }
218
+
219
+ async function main() {
220
+ process.exitCode = await runReleasebot({
221
+ argv: process.argv.slice(2),
222
+ env: process.env,
223
+ stdout: process.stdout,
224
+ stderr: process.stderr
225
+ });
226
+ }
227
+
228
+ if (require.main === module) {
229
+ main().catch((error) => {
230
+ process.stderr.write(`${error.message}\n`);
231
+ process.exitCode = 1;
232
+ });
233
+ }
234
+
235
+ module.exports = {
236
+ fetchGitLabUser,
237
+ inferGitLabUrl,
238
+ main,
239
+ readGitMetadata,
240
+ resolveSemanticReleaseBin,
241
+ runReleasebot
242
+ };
@@ -0,0 +1,48 @@
1
+ // In order to match the custom releaseRules, the file constants.js is replaced.
2
+ export const DEFAULT_COMMIT_TYPES = Object.freeze([
3
+ {
4
+ type: 'feat',
5
+ section: 'Features'
6
+ },
7
+ {
8
+ type: 'fix',
9
+ section: 'Bug Fixes'
10
+ },
11
+ {
12
+ type: 'perf',
13
+ section: 'Performance Improvements'
14
+ },
15
+ {
16
+ type: 'revert',
17
+ section: 'Reverts'
18
+ },
19
+ {
20
+ type: 'docs',
21
+ section: 'Documentation',
22
+ },
23
+ {
24
+ type: 'style',
25
+ section: 'Styles',
26
+ },
27
+ {
28
+ type: 'chore',
29
+ section: 'Miscellaneous Chores',
30
+ hidden: true
31
+ },
32
+ {
33
+ type: 'refactor',
34
+ section: 'Code Refactoring',
35
+ },
36
+ {
37
+ type: 'test',
38
+ section: 'Tests',
39
+ },
40
+ {
41
+ type: 'build',
42
+ section: 'Build System',
43
+ },
44
+ {
45
+ type: 'ci',
46
+ section: 'Continuous Integration',
47
+ }
48
+ ].map(Object.freeze))
package/src/index.js ADDED
@@ -0,0 +1,64 @@
1
+ module.exports = {
2
+ branches: [
3
+ "main"
4
+ ],
5
+ ci: false,
6
+ plugins: [
7
+ [
8
+ "@semantic-release/commit-analyzer",
9
+ {
10
+ preset: "conventionalcommits",
11
+ releaseRules: [
12
+ { breaking: true, release: "major" },
13
+ { type: "feat", release: "minor" },
14
+ { revert: true, release: "patch" },
15
+ { type: "build", release: "patch" },
16
+ { type: "ci", release: "patch" },
17
+ { type: "docs", release: "patch" },
18
+ { type: "fix", release: "patch" },
19
+ { type: "perf", release: "patch" },
20
+ { type: "refactor", release: "patch" },
21
+ { type: "revert", release: "patch" },
22
+ { type: "style", release: "patch" },
23
+ { type: "test", release: "patch" },
24
+ { type: "chore", release: false }
25
+ ]
26
+ }
27
+ ],
28
+ [
29
+ // In order to match the custom releaseRules, the file constants.js is replaced.
30
+ "@semantic-release/release-notes-generator",
31
+ {
32
+ preset: "conventionalcommits"
33
+ }
34
+ ],
35
+ [
36
+ "@semantic-release/changelog",
37
+ {
38
+ changelogFile: "CHANGELOG.md"
39
+ }
40
+ ],
41
+ [
42
+ "@semantic-release/exec",
43
+ {
44
+ prepareCmd: "if [ -f VERSION ]; then echo ${nextRelease.version} > VERSION; fi; if [ -x version.sh ]; then ./version.sh ${nextRelease.version}; fi"
45
+ }
46
+ ],
47
+ [
48
+ "@semantic-release/git",
49
+ {
50
+ assets: "**",
51
+ message: "chore(release): ${nextRelease.version}"
52
+ }
53
+ ],
54
+ [
55
+ "@semantic-release/gitlab",
56
+ {
57
+ failCommentCondition: false,
58
+ failTitle: false,
59
+ labels: false,
60
+ successCommentCondition: false
61
+ }
62
+ ]
63
+ ]
64
+ }