repokeeper 0.4.6 → 0.5.0
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 +44 -1
- package/dist/cli.js +9 -0
- package/dist/commands/context.js +8 -2
- package/dist/commands/github.js +2 -0
- package/dist/commands/init.js +7 -4
- package/dist/commands/report.js +21 -1
- package/dist/config/load.js +8 -1
- package/dist/config/schema.js +6 -1
- package/dist/config/types.js +3 -3
- package/dist/git.js +33 -4
- package/dist/model.js +4 -0
- package/dist/modules/health.js +2 -5
- package/dist/modules/release.js +7 -1
- package/dist/platforms/github.js +7 -1
- package/dist/platforms/gitlab.js +212 -0
- package/dist/platforms/index.js +5 -0
- package/dist/stacks/gradle.js +9 -2
- package/dist/sync/apply.js +4 -1
- package/dist/sync/decide.js +6 -2
- package/dist/sync/lock.js +2 -0
- package/dist/sync/marker.js +28 -0
- package/dist/sync/state.js +5 -1
- package/dist/version.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ the standard evolves.
|
|
|
6
6
|
|
|
7
7
|
> Status: early development. Supported stacks: Node.js (including NestJS), Python, Dart and
|
|
8
8
|
> Flutter, shell and PowerShell scripts, Java (Maven and Gradle), Kotlin, .NET, Go, Rust, PHP and
|
|
9
|
-
> Ruby, each with CI and releases, plus GitHub settings through `repokeeper github apply`.
|
|
9
|
+
> Ruby, each with CI and releases, plus GitHub settings through `repokeeper github apply`. GitLab is
|
|
10
|
+
> supported for Node.js projects. See the
|
|
10
11
|
> [design](docs/superpowers/specs/2026-09-25-repokeeper-design.md).
|
|
11
12
|
|
|
12
13
|
## Usage
|
|
@@ -111,6 +112,48 @@ It prints every change first and applies them with `--yes`, or after you confirm
|
|
|
111
112
|
access to the repository. Legacy branch protection, visibility, secrets and collaborators are never
|
|
112
113
|
touched.
|
|
113
114
|
|
|
115
|
+
## GitLab
|
|
116
|
+
|
|
117
|
+
`repokeeper init` selects GitLab when the `origin` remote's host contains `gitlab`; pass
|
|
118
|
+
`--platform gitlab` otherwise (a self-hosted instance under another name, or no remote yet). Only
|
|
119
|
+
the node stack is supported on GitLab for now.
|
|
120
|
+
|
|
121
|
+
What differs from GitHub:
|
|
122
|
+
|
|
123
|
+
| | GitHub | GitLab |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| Templates, CODEOWNERS | `.github/` | `.gitlab/` |
|
|
126
|
+
| CI | caller workflows of reusable workflows | every job generated into `.gitlab-ci.yml` |
|
|
127
|
+
| Dependency updates | Dependabot | Renovate (`renovate.json`) |
|
|
128
|
+
| Releases | release-please, through a release pull request | semantic-release, on every push to the default branch |
|
|
129
|
+
|
|
130
|
+
Jobs you add to `.gitlab-ci.yml` are kept; repokeeper manages only its own top-level keys
|
|
131
|
+
(`workflow`, `node`, `commits`, `repokeeper`, `renovate`, `release`). `stack_options.node.os` has
|
|
132
|
+
no effect: GitLab jobs run on Linux.
|
|
133
|
+
|
|
134
|
+
Two things to know when you add jobs of your own:
|
|
135
|
+
|
|
136
|
+
- The managed `workflow` runs pipelines for merge requests, the default branch, schedules and
|
|
137
|
+
tags. repokeeper's own jobs skip tags, so a tag pipeline holds only your jobs (publishing, for
|
|
138
|
+
example). A job meant for other branches never starts. To write the `workflow` rules yourself,
|
|
139
|
+
list the key under `owned` in `.repokeeper.yml`: `owned: [".gitlab-ci.yml#workflow"]`.
|
|
140
|
+
- repokeeper's jobs use GitLab's default stages `test` and `deploy`. If you declare `stages`,
|
|
141
|
+
include both; repokeeper warns when one is missing.
|
|
142
|
+
|
|
143
|
+
Two jobs stay inactive until you set them up in the project's CI/CD settings:
|
|
144
|
+
|
|
145
|
+
- **`release`** needs a CI/CD variable `GITLAB_TOKEN`: a project access token with the `api` and
|
|
146
|
+
`write_repository` scopes and a role that may push to the default branch. Every push to the
|
|
147
|
+
default branch with a `feat`, `fix` or breaking change then releases at once: version bump,
|
|
148
|
+
`CHANGELOG.md`, tag and GitLab release. There is no release merge request. A repository without
|
|
149
|
+
a `vX.Y.Z` tag starts at `1.0.0`; tag the current version first to continue from it. A variable
|
|
150
|
+
marked Protected is only visible on protected branches, so protect the default branch or leave
|
|
151
|
+
the variable unprotected; otherwise the job silently stays away.
|
|
152
|
+
- **`renovate`** needs a CI/CD variable `RENOVATE_TOKEN` (same scopes) and a pipeline schedule,
|
|
153
|
+
for example weekly. The schedule alone decides how often Renovate runs.
|
|
154
|
+
|
|
155
|
+
`repokeeper github apply` has no GitLab counterpart yet.
|
|
156
|
+
|
|
114
157
|
## Pilots
|
|
115
158
|
|
|
116
159
|
Each stack is tried on a real or demo repository before a release. The demo repositories carry the
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ export const USAGE = [
|
|
|
25
25
|
" --adopt <path> let repokeeper manage an existing file (repeatable); --adopt-all for every file",
|
|
26
26
|
" --accept <path> take repokeeper's version of a locally edited file (update, repeatable)",
|
|
27
27
|
" --stack <id> stack to use instead of detection (init, repeatable)",
|
|
28
|
+
" --platform <id> github or gitlab, instead of detection from the origin remote (init)",
|
|
28
29
|
" --relock rebuild .repokeeper/lock.json from the current files (init)",
|
|
29
30
|
" --json machine-readable output (check)",
|
|
30
31
|
" -y, --yes apply without asking (github apply)",
|
|
@@ -44,6 +45,7 @@ export async function run(argv, io) {
|
|
|
44
45
|
"adopt-all": { type: "boolean", default: false },
|
|
45
46
|
accept: { type: "string", multiple: true, default: [] },
|
|
46
47
|
stack: { type: "string", multiple: true, default: [] },
|
|
48
|
+
platform: { type: "string" },
|
|
47
49
|
relock: { type: "boolean", default: false },
|
|
48
50
|
json: { type: "boolean", default: false },
|
|
49
51
|
yes: { type: "boolean", short: "y", default: false },
|
|
@@ -66,6 +68,12 @@ export async function run(argv, io) {
|
|
|
66
68
|
throw new UsageError(`unknown stack ${stack}; expected one of ${STACK_IDS.join(", ")}`);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
const platform = values.platform;
|
|
72
|
+
if (platform !== undefined && platform !== "github" && platform !== "gitlab") {
|
|
73
|
+
throw new UsageError(`unknown platform ${platform}; expected github or gitlab`);
|
|
74
|
+
}
|
|
75
|
+
if (platform !== undefined && command !== "init")
|
|
76
|
+
throw new UsageError("--platform is only for init");
|
|
69
77
|
const options = {
|
|
70
78
|
dryRun: values["dry-run"],
|
|
71
79
|
force: values.force,
|
|
@@ -76,6 +84,7 @@ export async function run(argv, io) {
|
|
|
76
84
|
relock: values.relock,
|
|
77
85
|
json: values.json,
|
|
78
86
|
yes: values.yes,
|
|
87
|
+
...(platform !== undefined ? { platform } : {}),
|
|
79
88
|
};
|
|
80
89
|
if (command === "init")
|
|
81
90
|
return await initCommand(io.cwd, options, io);
|
package/dist/commands/context.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { UsageError } from "../errors.js";
|
|
1
2
|
import { repoInfo } from "../git.js";
|
|
2
|
-
import {
|
|
3
|
+
import { platformFor } from "../platforms/index.js";
|
|
3
4
|
import { getStackPack } from "../stacks/index.js";
|
|
4
5
|
export async function buildContext(root, config, repo) {
|
|
6
|
+
const platform = platformFor(config.platform);
|
|
7
|
+
const unsupported = config.stacks.find((id) => !platform.stacks.includes(id));
|
|
8
|
+
if (unsupported) {
|
|
9
|
+
throw new UsageError(`stack "${unsupported}" is not supported on ${platform.id} yet (supported: ${platform.stacks.join(", ")})`);
|
|
10
|
+
}
|
|
5
11
|
const stacks = await Promise.all(config.stacks.map((id) => getStackPack(id).resolve(root, config.stack_options[id] ?? {})));
|
|
6
|
-
return { config, stacks, platform
|
|
12
|
+
return { config, stacks, platform, repo: repo ?? (await repoInfo(root, config.platform)) };
|
|
7
13
|
}
|
package/dist/commands/github.js
CHANGED
|
@@ -6,6 +6,8 @@ import { planGithub } from "../github/settings.js";
|
|
|
6
6
|
/** `repokeeper github apply`: diff the repository's GitHub settings against `github:` and apply on confirmation. */
|
|
7
7
|
export async function githubApplyCommand(root, options, io) {
|
|
8
8
|
const config = await loadConfig(root);
|
|
9
|
+
if (config.platform !== "github")
|
|
10
|
+
throw new UsageError(`this repository uses the ${config.platform} platform`);
|
|
9
11
|
const repo = await repoInfo(root);
|
|
10
12
|
if (!repo.owner)
|
|
11
13
|
throw new UsageError("the origin remote is not a GitHub repository");
|
package/dist/commands/init.js
CHANGED
|
@@ -4,9 +4,10 @@ import { join } from "node:path";
|
|
|
4
4
|
import { CONFIG_FILE, loadConfig, renderConfig } from "../config/load.js";
|
|
5
5
|
import { defaultConfig } from "../config/types.js";
|
|
6
6
|
import { UsageError } from "../errors.js";
|
|
7
|
-
import { dirtyPaths, gitUserName, remoteDefaultBranch, repoInfo } from "../git.js";
|
|
7
|
+
import { dirtyPaths, gitUserName, remoteDefaultBranch, remoteHost, repoInfo } from "../git.js";
|
|
8
8
|
import { outputId } from "../model.js";
|
|
9
9
|
import { planOutputs } from "../plan.js";
|
|
10
|
+
import { platformFor } from "../platforms/index.js";
|
|
10
11
|
import { detectStacks } from "../stacks/index.js";
|
|
11
12
|
import { applySync } from "../sync/apply.js";
|
|
12
13
|
import { hashText } from "../sync/hash.js";
|
|
@@ -53,18 +54,20 @@ export async function initCommand(root, options, io) {
|
|
|
53
54
|
const stacks = options.stacks.length > 0 ? options.stacks : await detectStacks(root);
|
|
54
55
|
if (stacks.length === 0)
|
|
55
56
|
throw new UsageError("no supported stack detected; pass --stack node");
|
|
56
|
-
const
|
|
57
|
+
const platform = options.platform ?? ((await remoteHost(root))?.includes("gitlab") ? "gitlab" : "github");
|
|
58
|
+
const repo = await repoInfo(root, platform);
|
|
57
59
|
const holder = (await gitUserName(root)) ?? repo.owner ?? "the project authors";
|
|
58
60
|
const config = defaultConfig({
|
|
59
61
|
stacks,
|
|
60
62
|
standard: STANDARD_VERSION,
|
|
61
63
|
copyright: `${new Date().getFullYear()} ${holder}`,
|
|
62
|
-
contact:
|
|
64
|
+
contact: platformFor(platform).profileUrl(repo) ?? "the repository maintainers",
|
|
63
65
|
codeowners: repo.owner ? [`@${repo.owner}`] : [],
|
|
66
|
+
platform,
|
|
64
67
|
});
|
|
65
68
|
const branch = await remoteDefaultBranch(root);
|
|
66
69
|
if (branch && branch !== "main")
|
|
67
|
-
config
|
|
70
|
+
config[platform] = { default_branch: branch };
|
|
68
71
|
const ctx = await buildContext(root, config, repo);
|
|
69
72
|
const adopt = options.adoptAll ? "all" : new Set(options.adopt);
|
|
70
73
|
const result = await computeSync(root, planOutputs(ctx), null, { adopt, accept: new Set() });
|
package/dist/commands/report.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
3
4
|
import { duplicateTestRuns } from "../duplicates.js";
|
|
4
5
|
import { crlfTrackedFiles } from "../git.js";
|
|
5
6
|
import { describeOutput } from "../model.js";
|
|
@@ -61,11 +62,30 @@ export async function nextSteps(root, ctx, result) {
|
|
|
61
62
|
}
|
|
62
63
|
/** Problems repokeeper can't fix itself; printed, but not counted as drift. */
|
|
63
64
|
export async function printWarnings(root, ctx, io) {
|
|
64
|
-
if (ctx.config.modules.ci) {
|
|
65
|
+
if (ctx.config.modules.ci && ctx.platform.id === "github") {
|
|
65
66
|
for (const { file, command } of await duplicateTestRuns(root, ctx.stacks)) {
|
|
66
67
|
io.out(`note: ${file} also runs "${command}", which the repokeeper ci job now runs too; drop one of them to save CI time`);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
if (ctx.platform.id === "gitlab") {
|
|
71
|
+
const os = ctx.stacks.find((stack) => stack.id === "node")?.ci?.with.os;
|
|
72
|
+
if (os !== undefined && os !== '["ubuntu-latest"]') {
|
|
73
|
+
io.out("note: node.os is ignored on gitlab (Linux runners only)");
|
|
74
|
+
}
|
|
75
|
+
// repokeeper's jobs use GitLab's default stages; a stages list of the user's own replaces those
|
|
76
|
+
let ci = null;
|
|
77
|
+
try {
|
|
78
|
+
ci = parse(await readFile(join(root, ".gitlab-ci.yml"), "utf8"));
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
if (Array.isArray(ci?.stages)) {
|
|
82
|
+
const stages = ci.stages;
|
|
83
|
+
const missing = ["test", "deploy"].filter((stage) => !stages.includes(stage));
|
|
84
|
+
if (missing.length > 0) {
|
|
85
|
+
io.out(`warning: .gitlab-ci.yml: stages lacks ${missing.join(" and ")}, which repokeeper's jobs use; add it, or GitLab rejects the pipeline`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
69
89
|
if (!ctx.config.modules.editorconfig)
|
|
70
90
|
return;
|
|
71
91
|
const text = await readFile(join(root, ".gitattributes"), "utf8").catch(() => "");
|
package/dist/config/load.js
CHANGED
|
@@ -30,7 +30,12 @@ export function parseConfig(text) {
|
|
|
30
30
|
const path = pathOf(error);
|
|
31
31
|
throw new ConfigError(`${CONFIG_FILE}:${lineOf(doc, lineCounter, path)}: ${describe(error, path)}`);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
const typed = data;
|
|
34
|
+
const other = typed.platform === "gitlab" ? "github" : "gitlab";
|
|
35
|
+
if (typed[other] !== undefined) {
|
|
36
|
+
throw new ConfigError(`${CONFIG_FILE}:${lineOf(doc, lineCounter, [other])}: ${other} is not used on the ${typed.platform} platform`);
|
|
37
|
+
}
|
|
38
|
+
return withDefaults(typed);
|
|
34
39
|
}
|
|
35
40
|
export function renderConfig(config) {
|
|
36
41
|
return `# repokeeper configuration: https://github.com/vannt-dev/repokeeper\n${stringify(config)}`;
|
|
@@ -67,6 +72,8 @@ function withDefaults(data) {
|
|
|
67
72
|
};
|
|
68
73
|
if (data.github !== undefined)
|
|
69
74
|
config.github = data.github;
|
|
75
|
+
if (data.gitlab !== undefined)
|
|
76
|
+
config.gitlab = data.gitlab;
|
|
70
77
|
return config;
|
|
71
78
|
}
|
|
72
79
|
function pathOf(error) {
|
package/dist/config/schema.js
CHANGED
|
@@ -7,7 +7,7 @@ export const configSchema = {
|
|
|
7
7
|
properties: {
|
|
8
8
|
schema: { const: 1 },
|
|
9
9
|
standard: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
|
|
10
|
-
platform: { enum: ["github"] },
|
|
10
|
+
platform: { enum: ["github", "gitlab"] },
|
|
11
11
|
stacks: { type: "array", minItems: 1, uniqueItems: true, items: { enum: [...STACK_IDS] } },
|
|
12
12
|
modules: {
|
|
13
13
|
type: "object",
|
|
@@ -75,5 +75,10 @@ export const configSchema = {
|
|
|
75
75
|
},
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
|
+
gitlab: {
|
|
79
|
+
type: "object",
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: { default_branch: { type: "string", minLength: 1 } },
|
|
82
|
+
},
|
|
78
83
|
},
|
|
79
84
|
};
|
package/dist/config/types.js
CHANGED
|
@@ -11,16 +11,16 @@ export const STACK_IDS = [
|
|
|
11
11
|
"php",
|
|
12
12
|
"ruby",
|
|
13
13
|
];
|
|
14
|
-
/** The branch
|
|
14
|
+
/** The branch CI runs on and contributors branch from: `default_branch` of the configured platform, or `main`. */
|
|
15
15
|
export function defaultBranch(config) {
|
|
16
|
-
const branch = config.github?.default_branch;
|
|
16
|
+
const branch = (config.platform === "gitlab" ? config.gitlab : config.github)?.default_branch;
|
|
17
17
|
return typeof branch === "string" && branch.length > 0 ? branch : "main";
|
|
18
18
|
}
|
|
19
19
|
export function defaultConfig(input) {
|
|
20
20
|
return {
|
|
21
21
|
schema: 1,
|
|
22
22
|
standard: input.standard,
|
|
23
|
-
platform: "github",
|
|
23
|
+
platform: input.platform ?? "github",
|
|
24
24
|
stacks: input.stacks,
|
|
25
25
|
modules: {
|
|
26
26
|
editorconfig: true,
|
package/dist/git.js
CHANGED
|
@@ -18,10 +18,39 @@ export function parseRemoteUrl(url) {
|
|
|
18
18
|
const match = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url.trim());
|
|
19
19
|
return match ? { owner: match[1], name: match[2] } : null;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
/** Any hosted remote, HTTPS or SSH. The owner is the whole namespace path, because GitLab groups nest. */
|
|
22
|
+
export function parseRemote(url) {
|
|
23
|
+
const text = url.trim();
|
|
24
|
+
// the web address shares the port of an http(s) remote; an SSH port says nothing about it
|
|
25
|
+
const match = /^https?:\/\/(?:[^@/]+@)?([^/:]+(?::\d+)?)\/(.+)$/i.exec(text) ??
|
|
26
|
+
/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(text) ??
|
|
27
|
+
/^(?:[^@/]+@)?([^/:]+):(.+)$/.exec(text);
|
|
28
|
+
if (!match)
|
|
29
|
+
return null;
|
|
30
|
+
const host = match[1].toLowerCase();
|
|
31
|
+
const segments = match[2]
|
|
32
|
+
.replace(/\/+$/, "")
|
|
33
|
+
.replace(/\.git$/, "")
|
|
34
|
+
.split("/")
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
const name = segments.pop();
|
|
37
|
+
// a host has a dot, which rules out drive letters and file: URLs
|
|
38
|
+
if (!host.includes(".") || name === undefined || segments.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
return { host, owner: segments.join("/"), name };
|
|
41
|
+
}
|
|
42
|
+
async function originUrl(root) {
|
|
43
|
+
return git(root, ["remote", "get-url", "origin"]);
|
|
44
|
+
}
|
|
45
|
+
/** Host of the origin remote, or null without one. */
|
|
46
|
+
export async function remoteHost(root) {
|
|
47
|
+
const url = await originUrl(root);
|
|
48
|
+
return url ? (parseRemote(url)?.host ?? null) : null;
|
|
49
|
+
}
|
|
50
|
+
export async function repoInfo(root, platform = "github") {
|
|
51
|
+
const url = await originUrl(root);
|
|
52
|
+
const remote = url ? (platform === "gitlab" ? parseRemote(url) : parseRemoteUrl(url)) : null;
|
|
53
|
+
return { ...(remote ?? { owner: null, name: basename(root) }), releasedVersion: await latestReleaseVersion(root) };
|
|
25
54
|
}
|
|
26
55
|
/** The branch `origin/HEAD` points at, or null when the remote's default branch isn't known locally. */
|
|
27
56
|
export async function remoteDefaultBranch(root) {
|
package/dist/model.js
CHANGED
|
@@ -2,6 +2,8 @@ export const MANAGED_HEADER = "Managed by repokeeper (https://github.com/vannt-d
|
|
|
2
2
|
/** Header of YAML files repokeeper creates but owns only in part. */
|
|
3
3
|
export const YAML_HEADER = `${MANAGED_HEADER} Keys and jobs you add yourself are left alone.`;
|
|
4
4
|
export function outputId(output) {
|
|
5
|
+
if (output.kind === "marker")
|
|
6
|
+
return `marker:${output.path}`;
|
|
5
7
|
if (output.kind === "file")
|
|
6
8
|
return `file:${output.path}`;
|
|
7
9
|
if (output.kind === "seed")
|
|
@@ -13,6 +15,8 @@ export function outputId(output) {
|
|
|
13
15
|
return `json:${output.path}#${JSON.stringify(output.keyPath)}`;
|
|
14
16
|
}
|
|
15
17
|
export function describeOutput(output) {
|
|
18
|
+
if (output.kind === "marker")
|
|
19
|
+
return `${output.path} (release-please version markers)`;
|
|
16
20
|
if (output.kind === "file" || output.kind === "seed")
|
|
17
21
|
return output.path;
|
|
18
22
|
if (output.kind === "block")
|
package/dist/modules/health.js
CHANGED
|
@@ -33,7 +33,6 @@ export const healthModule = {
|
|
|
33
33
|
const health = ctx.config.modules.health;
|
|
34
34
|
if (!health)
|
|
35
35
|
return [];
|
|
36
|
-
const { owner, name } = ctx.repo;
|
|
37
36
|
const outputs = [];
|
|
38
37
|
if (health.license !== false) {
|
|
39
38
|
if (health.license !== "MIT") {
|
|
@@ -41,9 +40,7 @@ export const healthModule = {
|
|
|
41
40
|
}
|
|
42
41
|
outputs.push(md("LICENSE", MIT(health.copyright)));
|
|
43
42
|
}
|
|
44
|
-
const report =
|
|
45
|
-
? `through [GitHub security advisories](https://github.com/${owner}/${name}/security/advisories/new)`
|
|
46
|
-
: "through the repository's Security tab (Report a vulnerability)";
|
|
43
|
+
const report = ctx.platform.securityReport(ctx.repo);
|
|
47
44
|
outputs.push(md("SECURITY.md", `# Security Policy\n\n<!-- ${MANAGED_HEADER} -->\n\n## Supported versions\n\nSecurity fixes are released for the latest published version.\n\n## Reporting a vulnerability\n\nPlease do not open a public issue. Report the vulnerability privately ${report}.\nThe maintainers will acknowledge it as soon as possible and keep you informed until it is resolved.\n`));
|
|
48
45
|
const install = ctx.stacks.map((s) => s.install).filter((c) => c !== null);
|
|
49
46
|
const run = install.length ? `Run ${install.map((c) => `\`${c}\``).join(" and ")}.` : "";
|
|
@@ -51,7 +48,7 @@ export const healthModule = {
|
|
|
51
48
|
? "Installing the dependencies also installs the git hooks (lefthook)."
|
|
52
49
|
: `${run ? "Then run" : "Run"} \`npx --yes lefthook@${TOOL_VERSIONS.lefthook} install\` once to enable the git hooks (they need Node.js 22 or newer).`;
|
|
53
50
|
const setup = [run, hooks].filter(Boolean).join(" ");
|
|
54
|
-
outputs.push(md("CONTRIBUTING.md", `# Contributing\n\n<!-- ${MANAGED_HEADER} -->\n\nThanks for helping improve this project.\n\n## Local setup\n\n${setup}\n\n## Workflow\n\n1. Create a branch from \`${defaultBranch(ctx.config)}\`.\n2. Write commit messages in [Conventional Commits](https://www.conventionalcommits.org/) form, for example \`feat: add export button\` or \`fix(api): handle empty input\`. The \`commit-msg\` hook checks them.\n3. Open a
|
|
51
|
+
outputs.push(md("CONTRIBUTING.md", `# Contributing\n\n<!-- ${MANAGED_HEADER} -->\n\nThanks for helping improve this project.\n\n## Local setup\n\n${setup}\n\n## Workflow\n\n1. Create a branch from \`${defaultBranch(ctx.config)}\`.\n2. Write commit messages in [Conventional Commits](https://www.conventionalcommits.org/) form, for example \`feat: add export button\` or \`fix(api): handle empty input\`. The \`commit-msg\` hook checks them.\n3. Open a ${ctx.platform.changeRequest}. It is merged once the checks pass and it has been reviewed.\n\nPlease follow the [Code of Conduct](CODE_OF_CONDUCT.md).\n`));
|
|
55
52
|
outputs.push(md("CODE_OF_CONDUCT.md", readTemplate("CODE_OF_CONDUCT.md").replace("[INSERT CONTACT METHOD]", health.contact)));
|
|
56
53
|
outputs.push(...ctx.platform.communityFiles(ctx));
|
|
57
54
|
return outputs;
|
package/dist/modules/release.js
CHANGED
|
@@ -6,5 +6,11 @@ export function pickRelease(stacks) {
|
|
|
6
6
|
export const releaseModule = {
|
|
7
7
|
id: "release",
|
|
8
8
|
enabled: (config) => config.modules.release,
|
|
9
|
-
outputs: (ctx) =>
|
|
9
|
+
outputs: (ctx) => {
|
|
10
|
+
const release = pickRelease(ctx.stacks);
|
|
11
|
+
const markers = release.versionLine
|
|
12
|
+
? [{ kind: "marker", module: "release", path: release.versionLine.path, line: release.versionLine.line }]
|
|
13
|
+
: [];
|
|
14
|
+
return [...ctx.platform.releaseAutomation(ctx, release), ...markers];
|
|
15
|
+
},
|
|
10
16
|
};
|
package/dist/platforms/github.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stringify } from "yaml";
|
|
2
|
-
import { defaultBranch } from "../config/types.js";
|
|
2
|
+
import { defaultBranch, STACK_IDS } from "../config/types.js";
|
|
3
3
|
import { MANAGED_HEADER } from "../model.js";
|
|
4
4
|
import { PACKAGE_VERSION, REUSABLE_REPO, WORKFLOW_REF } from "../version.js";
|
|
5
5
|
const yamlFile = (module, path, data) => ({
|
|
@@ -49,6 +49,12 @@ const PHP_CHANGELOG_SECTIONS = [
|
|
|
49
49
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
50
50
|
export const githubPlatform = {
|
|
51
51
|
id: "github",
|
|
52
|
+
stacks: STACK_IDS,
|
|
53
|
+
changeRequest: "pull request",
|
|
54
|
+
profileUrl: (repo) => (repo.owner ? `https://github.com/${repo.owner}` : null),
|
|
55
|
+
securityReport: (repo) => repo.owner
|
|
56
|
+
? `through [GitHub security advisories](https://github.com/${repo.owner}/${repo.name}/security/advisories/new)`
|
|
57
|
+
: "through the repository's Security tab (Report a vulnerability)",
|
|
52
58
|
communityFiles(ctx) {
|
|
53
59
|
const { owner, name } = ctx.repo;
|
|
54
60
|
const outputs = [
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { defaultBranch } from "../config/types.js";
|
|
2
|
+
import { MANAGED_HEADER } from "../model.js";
|
|
3
|
+
import { PACKAGE_VERSION, TOOL_VERSIONS } from "../version.js";
|
|
4
|
+
const md = (path, body) => ({
|
|
5
|
+
kind: "file",
|
|
6
|
+
module: "health",
|
|
7
|
+
path,
|
|
8
|
+
content: `<!-- ${MANAGED_HEADER} -->\n\n${body}`,
|
|
9
|
+
});
|
|
10
|
+
/** Web address of the GitLab instance the repository lives on. */
|
|
11
|
+
const baseUrl = (repo) => `https://${repo.host ?? "gitlab.com"}`;
|
|
12
|
+
const CI_FILE = ".gitlab-ci.yml";
|
|
13
|
+
const CI_KEYS = ["workflow", "node", "commits", "repokeeper", "renovate", "release"];
|
|
14
|
+
/** One managed top-level key of .gitlab-ci.yml; keys the user adds are left alone. */
|
|
15
|
+
const ciKey = (module, key, value) => ({
|
|
16
|
+
kind: "yaml",
|
|
17
|
+
module,
|
|
18
|
+
path: CI_FILE,
|
|
19
|
+
keyPath: [key],
|
|
20
|
+
value,
|
|
21
|
+
order: CI_KEYS,
|
|
22
|
+
});
|
|
23
|
+
const NOT_SCHEDULED = '$CI_PIPELINE_SOURCE != "schedule"';
|
|
24
|
+
/** The checks of a change: merge requests and the default branch, not schedules and not tag pipelines. */
|
|
25
|
+
const FOR_CHANGES = `${NOT_SCHEDULED} && $CI_COMMIT_TAG == null`;
|
|
26
|
+
/** Image of the jobs that only run tools, whatever Node.js versions the project tests on. */
|
|
27
|
+
const TOOL_IMAGE = "node:24";
|
|
28
|
+
/** The node stack's CI inputs, as the GitHub reusable workflow receives them, as a GitLab job. */
|
|
29
|
+
function nodeJob(input) {
|
|
30
|
+
const pm = input["package-manager"] ?? "npm";
|
|
31
|
+
const cached = input.cache === "npm";
|
|
32
|
+
const scripts = JSON.parse(input.scripts ?? "[]");
|
|
33
|
+
return {
|
|
34
|
+
stage: "test",
|
|
35
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitLab CI variable
|
|
36
|
+
image: "node:${NODE_VERSION}",
|
|
37
|
+
parallel: { matrix: [{ NODE_VERSION: JSON.parse(input["node-versions"] ?? '["22","24"]') }] },
|
|
38
|
+
rules: [{ if: FOR_CHANGES }],
|
|
39
|
+
variables: {
|
|
40
|
+
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
|
41
|
+
...(cached ? { npm_config_cache: "$CI_PROJECT_DIR/.npm" } : {}),
|
|
42
|
+
},
|
|
43
|
+
...(cached ? { cache: { key: { files: ["package-lock.json"] }, paths: [".npm/"] } } : {}),
|
|
44
|
+
script: [
|
|
45
|
+
...(pm === "npm" ? [] : ["corepack enable"]),
|
|
46
|
+
input["install-command"] ?? "npm ci",
|
|
47
|
+
...scripts.map((script) => `${pm} run ${script}`),
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** commitlint from a scratch directory, so the project needs no commitlint of its own. */
|
|
52
|
+
const COMMITLINT = `dir="$(mktemp -d)"
|
|
53
|
+
(cd "$dir" && echo '{ "private": true }' > package.json && npm install --no-audit --no-fund @commitlint/cli@${TOOL_VERSIONS.commitlintCli} @commitlint/config-conventional@${TOOL_VERSIONS.commitlintConventional})
|
|
54
|
+
cat > "$dir/commitlint.config.mjs" <<'EOF'
|
|
55
|
+
export default { extends: ["@commitlint/config-conventional"], rules: { "header-max-length": [2, "always", 100] } };
|
|
56
|
+
EOF
|
|
57
|
+
lint() { "$dir/node_modules/.bin/commitlint" --config "$dir/commitlint.config.mjs" --verbose "$@"; }
|
|
58
|
+
# a merged results pipeline runs on a merge commit; the commits to lint end at the source branch
|
|
59
|
+
head="$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA"
|
|
60
|
+
[ -n "$head" ] || head="$CI_COMMIT_SHA"
|
|
61
|
+
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
|
|
62
|
+
lint --from "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --to "$head"
|
|
63
|
+
else
|
|
64
|
+
lint --last
|
|
65
|
+
fi
|
|
66
|
+
`;
|
|
67
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
68
|
+
/**
|
|
69
|
+
* semantic-release and the plugins it does not bundle, from a scratch directory. It resolves plugins next to
|
|
70
|
+
* itself before it looks in the project, so the project needs none of them installed.
|
|
71
|
+
*/
|
|
72
|
+
const SEMANTIC_RELEASE = `dir="$(mktemp -d)"
|
|
73
|
+
(cd "$dir" && echo '{ "private": true }' > package.json && npm install --no-audit --no-fund semantic-release@${TOOL_VERSIONS.semanticRelease} @semantic-release/changelog@${TOOL_VERSIONS.semanticReleaseChangelog} @semantic-release/git@${TOOL_VERSIONS.semanticReleaseGit} @semantic-release/gitlab@${TOOL_VERSIONS.semanticReleaseGitlab} conventional-changelog-conventionalcommits@${TOOL_VERSIONS.conventionalCommitsPreset})
|
|
74
|
+
"$dir/node_modules/.bin/semantic-release"
|
|
75
|
+
`;
|
|
76
|
+
export const gitlabPlatform = {
|
|
77
|
+
id: "gitlab",
|
|
78
|
+
stacks: ["node"],
|
|
79
|
+
changeRequest: "merge request",
|
|
80
|
+
profileUrl: (repo) => (repo.owner ? `${baseUrl(repo)}/${repo.owner}` : null),
|
|
81
|
+
securityReport: (repo) => repo.owner
|
|
82
|
+
? `by opening a [confidential issue](${baseUrl(repo)}/${repo.owner}/${repo.name}/-/issues/new?issue%5Bconfidential%5D=true)`
|
|
83
|
+
: "by opening a confidential issue in the repository",
|
|
84
|
+
communityFiles(ctx) {
|
|
85
|
+
const outputs = [
|
|
86
|
+
md(".gitlab/issue_templates/Bug.md", "## What happened?\n\n<!-- Include the steps to reproduce it. -->\n\n## What did you expect?\n\n## Version\n\n/label ~bug\n"),
|
|
87
|
+
md(".gitlab/issue_templates/Feature.md", "## What problem would this solve?\n\n## What do you propose?\n\n/label ~enhancement\n"),
|
|
88
|
+
md(".gitlab/merge_request_templates/Default.md", "## Summary\n\n## Testing\n\n- [ ] Tests added or updated\n- [ ] Commit messages follow Conventional Commits\n"),
|
|
89
|
+
];
|
|
90
|
+
const health = ctx.config.modules.health;
|
|
91
|
+
if (health && health.codeowners.length > 0) {
|
|
92
|
+
outputs.push({
|
|
93
|
+
kind: "file",
|
|
94
|
+
module: "health",
|
|
95
|
+
path: ".gitlab/CODEOWNERS",
|
|
96
|
+
content: `# ${MANAGED_HEADER}\n* ${health.codeowners.join(" ")}\n`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return outputs;
|
|
100
|
+
},
|
|
101
|
+
// Renovate finds the package managers itself, so the Dependabot ecosystem names are not needed
|
|
102
|
+
dependencyUpdates() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
kind: "file",
|
|
106
|
+
module: "deps",
|
|
107
|
+
path: "renovate.json",
|
|
108
|
+
content: json({
|
|
109
|
+
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
|
110
|
+
// chore commits pass commitlint, as the Dependabot prefix does on GitHub. No schedule preset: the
|
|
111
|
+
// pipeline schedule sets the cadence, and a Renovate schedule would skip runs outside its own window
|
|
112
|
+
extends: ["config:recommended", ":semanticCommits", ":semanticCommitTypeAll(chore)"],
|
|
113
|
+
packageRules: [
|
|
114
|
+
{ matchUpdateTypes: ["minor", "patch"], groupName: "minor and patch updates" },
|
|
115
|
+
// @types/node majors track the Node.js line a project runs on, which the project chooses
|
|
116
|
+
{ matchPackageNames: ["@types/node"], matchUpdateTypes: ["major"], enabled: false },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
// runs from a pipeline schedule of the repository; without the token or a schedule it never starts
|
|
121
|
+
ciKey("deps", "renovate", {
|
|
122
|
+
stage: "test",
|
|
123
|
+
image: `renovate/renovate:${TOOL_VERSIONS.renovate}`,
|
|
124
|
+
rules: [{ if: '$CI_PIPELINE_SOURCE == "schedule" && $RENOVATE_TOKEN' }],
|
|
125
|
+
variables: {
|
|
126
|
+
RENOVATE_PLATFORM: "gitlab",
|
|
127
|
+
RENOVATE_ENDPOINT: "$CI_API_V4_URL",
|
|
128
|
+
RENOVATE_AUTODISCOVER: "false",
|
|
129
|
+
RENOVATE_ONBOARDING: "false",
|
|
130
|
+
},
|
|
131
|
+
script: ['renovate "$CI_PROJECT_PATH"'],
|
|
132
|
+
}),
|
|
133
|
+
];
|
|
134
|
+
},
|
|
135
|
+
ciWorkflow(ctx) {
|
|
136
|
+
const jobs = [];
|
|
137
|
+
const node = ctx.stacks.find((stack) => stack.id === "node")?.ci;
|
|
138
|
+
if (node)
|
|
139
|
+
jobs.push(ciKey("ci", "node", nodeJob(node.with)));
|
|
140
|
+
if (ctx.config.modules.commits) {
|
|
141
|
+
jobs.push(ciKey("ci", "commits", {
|
|
142
|
+
stage: "test",
|
|
143
|
+
image: TOOL_IMAGE,
|
|
144
|
+
rules: [{ if: FOR_CHANGES }],
|
|
145
|
+
variables: { GIT_DEPTH: "0" },
|
|
146
|
+
script: [COMMITLINT],
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
if (ctx.config.modules.drift) {
|
|
150
|
+
jobs.push(ciKey("ci", "repokeeper", {
|
|
151
|
+
stage: "test",
|
|
152
|
+
image: TOOL_IMAGE,
|
|
153
|
+
rules: [{ if: FOR_CHANGES }],
|
|
154
|
+
script: [`npx --yes repokeeper@${PACKAGE_VERSION} check`],
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
if (jobs.length === 0)
|
|
158
|
+
return [];
|
|
159
|
+
return [
|
|
160
|
+
// one pipeline per merge request, per push to the default branch and per schedule; none for other branches.
|
|
161
|
+
// Tags are let through for jobs of the user's own, such as publishing: no managed job runs on one
|
|
162
|
+
ciKey("ci", "workflow", {
|
|
163
|
+
rules: [
|
|
164
|
+
{ if: '$CI_PIPELINE_SOURCE == "merge_request_event"' },
|
|
165
|
+
{ if: '$CI_PIPELINE_SOURCE == "schedule"' },
|
|
166
|
+
{ if: `$CI_COMMIT_BRANCH == "${defaultBranch(ctx.config)}"` },
|
|
167
|
+
{ if: "$CI_COMMIT_TAG" },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
...jobs,
|
|
171
|
+
];
|
|
172
|
+
},
|
|
173
|
+
releaseAutomation(ctx) {
|
|
174
|
+
const branch = defaultBranch(ctx.config);
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
kind: "file",
|
|
178
|
+
module: "release",
|
|
179
|
+
path: ".releaserc.json",
|
|
180
|
+
content: json({
|
|
181
|
+
branches: [branch],
|
|
182
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: a semantic-release template
|
|
183
|
+
tagFormat: "v${version}",
|
|
184
|
+
plugins: [
|
|
185
|
+
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],
|
|
186
|
+
["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }],
|
|
187
|
+
["@semantic-release/changelog", { changelogFile: "CHANGELOG.md" }],
|
|
188
|
+
// bumps package.json and the npm lock file; publishing stays the project's own business
|
|
189
|
+
["@semantic-release/npm", { npmPublish: false }],
|
|
190
|
+
[
|
|
191
|
+
"@semantic-release/git",
|
|
192
|
+
{
|
|
193
|
+
assets: ["CHANGELOG.md", "package.json", "package-lock.json", "npm-shrinkwrap.json"],
|
|
194
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: a semantic-release template
|
|
195
|
+
message: "chore(release): ${nextRelease.version} [skip ci]",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
"@semantic-release/gitlab",
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
// semantic-release pushes a commit and a tag, which the job token cannot do; without GITLAB_TOKEN the job is absent
|
|
203
|
+
ciKey("release", "release", {
|
|
204
|
+
stage: "deploy",
|
|
205
|
+
image: TOOL_IMAGE,
|
|
206
|
+
rules: [{ if: `${NOT_SCHEDULED} && $CI_COMMIT_BRANCH == "${branch}" && $GITLAB_TOKEN` }],
|
|
207
|
+
variables: { GIT_DEPTH: "0" },
|
|
208
|
+
script: [SEMANTIC_RELEASE],
|
|
209
|
+
}),
|
|
210
|
+
];
|
|
211
|
+
},
|
|
212
|
+
};
|
package/dist/stacks/gradle.js
CHANGED
|
@@ -10,7 +10,14 @@ export async function gradleBuild(root) {
|
|
|
10
10
|
return {
|
|
11
11
|
command: wrapper ? "./gradlew check" : "gradle check",
|
|
12
12
|
wrapper,
|
|
13
|
-
// release-please's generic updater
|
|
14
|
-
|
|
13
|
+
// release-please's generic updater only edits gradle.properties between x-release-please markers,
|
|
14
|
+
// which the release module adds around the version line
|
|
15
|
+
release: {
|
|
16
|
+
type: "simple",
|
|
17
|
+
version,
|
|
18
|
+
...(version !== null
|
|
19
|
+
? { extraFiles: ["gradle.properties"], versionLine: { path: "gradle.properties", line: "^version\\s*=" } }
|
|
20
|
+
: {}),
|
|
21
|
+
},
|
|
15
22
|
};
|
|
16
23
|
}
|
package/dist/sync/apply.js
CHANGED
|
@@ -5,6 +5,7 @@ import { removeBlock, upsertBlock } from "./block.js";
|
|
|
5
5
|
import { hashText } from "./hash.js";
|
|
6
6
|
import { deleteAtPath, formatJson, setAtPath } from "./json.js";
|
|
7
7
|
import { targetOf, writeLock } from "./lock.js";
|
|
8
|
+
import { addMarkers } from "./marker.js";
|
|
8
9
|
import { desiredText } from "./state.js";
|
|
9
10
|
import { deleteYamlKey, setYamlKey } from "./yaml.js";
|
|
10
11
|
async function readOrNull(path) {
|
|
@@ -26,6 +27,8 @@ async function write(root, output) {
|
|
|
26
27
|
if (output.kind === "file" || output.kind === "seed")
|
|
27
28
|
return put(path, output.content);
|
|
28
29
|
const existing = await readOrNull(path);
|
|
30
|
+
if (output.kind === "marker")
|
|
31
|
+
return existing === null ? undefined : put(path, addMarkers(existing, output.line));
|
|
29
32
|
if (output.kind === "block")
|
|
30
33
|
return put(path, upsertBlock(existing, output.id, output.body, output.comment));
|
|
31
34
|
if (output.kind === "yaml") {
|
|
@@ -37,7 +40,7 @@ async function write(root, output) {
|
|
|
37
40
|
}
|
|
38
41
|
async function remove(root, target) {
|
|
39
42
|
const path = join(root, target.path);
|
|
40
|
-
if (target.kind === "seed")
|
|
43
|
+
if (target.kind === "seed" || target.kind === "marker")
|
|
41
44
|
return;
|
|
42
45
|
if (target.kind === "file")
|
|
43
46
|
return rm(path, { force: true });
|
package/dist/sync/decide.js
CHANGED
|
@@ -7,6 +7,9 @@ export function decide(output, currentText, entry, adopt) {
|
|
|
7
7
|
const current = hashText(currentText);
|
|
8
8
|
if (current === hashText(desiredText(output)))
|
|
9
9
|
return "unchanged";
|
|
10
|
+
// adding the two marker lines is always safe, so they are written without --adopt or --accept
|
|
11
|
+
if (output.kind === "marker")
|
|
12
|
+
return "write";
|
|
10
13
|
if (entry)
|
|
11
14
|
return current === entry.hash ? "write" : "conflict";
|
|
12
15
|
return adopt ? "adopt" : "unmanaged";
|
|
@@ -15,7 +18,8 @@ export function decide(output, currentText, entry, adopt) {
|
|
|
15
18
|
export function decideRemoval(entry, currentText) {
|
|
16
19
|
if (currentText === null)
|
|
17
20
|
return "gone";
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
// other tools own a seed once it exists, and release-please uses the markers
|
|
22
|
+
if (entry.target.kind === "seed" || entry.target.kind === "marker")
|
|
23
|
+
return "left";
|
|
20
24
|
return hashText(currentText) === entry.hash ? "delete" : "orphan-edited";
|
|
21
25
|
}
|
package/dist/sync/lock.js
CHANGED
|
@@ -3,6 +3,8 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { LockError } from "../errors.js";
|
|
4
4
|
export const LOCK_FILE = ".repokeeper/lock.json";
|
|
5
5
|
export function targetOf(output) {
|
|
6
|
+
if (output.kind === "marker")
|
|
7
|
+
return { kind: "marker", path: output.path, line: output.line };
|
|
6
8
|
if (output.kind === "file")
|
|
7
9
|
return { kind: "file", path: output.path };
|
|
8
10
|
if (output.kind === "seed")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** release-please's block markers for a version in a file its generic updater edits. */
|
|
2
|
+
export const MARKER_START = "# x-release-please-start-version";
|
|
3
|
+
export const MARKER_END = "# x-release-please-end";
|
|
4
|
+
const HAS_MARKERS = /x-release-please-(start-version|version)\b/;
|
|
5
|
+
const UNMARKED = "unmarked";
|
|
6
|
+
function eolOf(text) {
|
|
7
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
8
|
+
}
|
|
9
|
+
function lineIndex(lines, line) {
|
|
10
|
+
const pattern = new RegExp(line);
|
|
11
|
+
return lines.findIndex((l) => pattern.test(l));
|
|
12
|
+
}
|
|
13
|
+
/** "" when there is nothing to add (no file, no matching line, or markers already present), else "unmarked". */
|
|
14
|
+
export function markerState(text, line) {
|
|
15
|
+
if (text === null || HAS_MARKERS.test(text))
|
|
16
|
+
return "";
|
|
17
|
+
return lineIndex(text.split(eolOf(text)), line) === -1 ? "" : UNMARKED;
|
|
18
|
+
}
|
|
19
|
+
/** The text with block markers around the first line matching `line`; unchanged when there is nothing to add. */
|
|
20
|
+
export function addMarkers(text, line) {
|
|
21
|
+
if (markerState(text, line) === "")
|
|
22
|
+
return text;
|
|
23
|
+
const eol = eolOf(text);
|
|
24
|
+
const lines = text.split(eol);
|
|
25
|
+
const index = lineIndex(lines, line);
|
|
26
|
+
lines.splice(index, 1, MARKER_START, lines[index], MARKER_END);
|
|
27
|
+
return lines.join(eol);
|
|
28
|
+
}
|
package/dist/sync/state.js
CHANGED
|
@@ -3,13 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import { UsageError } from "../errors.js";
|
|
4
4
|
import { readBlock } from "./block.js";
|
|
5
5
|
import { getAtPath } from "./json.js";
|
|
6
|
+
import { markerState } from "./marker.js";
|
|
6
7
|
import { readYamlKey } from "./yaml.js";
|
|
7
8
|
/** The text repokeeper compares and hashes for an output. */
|
|
8
9
|
export function desiredText(output) {
|
|
9
10
|
if (output.kind === "file")
|
|
10
11
|
return output.content;
|
|
11
12
|
// a seed only has to exist, so present content always compares equal
|
|
12
|
-
if (output.kind === "seed")
|
|
13
|
+
if (output.kind === "seed" || output.kind === "marker")
|
|
13
14
|
return "";
|
|
14
15
|
if (output.kind === "block")
|
|
15
16
|
return output.body;
|
|
@@ -28,6 +29,9 @@ async function readText(root, path) {
|
|
|
28
29
|
/** The same comparable text read from disk, or null when absent. */
|
|
29
30
|
export async function readCurrent(root, target) {
|
|
30
31
|
const text = await readText(root, target.path);
|
|
32
|
+
// markers are added to a file the user owns; a missing file or line means there is nothing to add
|
|
33
|
+
if (target.kind === "marker")
|
|
34
|
+
return markerState(text, target.line);
|
|
31
35
|
if (text === null)
|
|
32
36
|
return null;
|
|
33
37
|
if (target.kind === "file")
|
package/dist/version.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
export const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
3
3
|
/** The standard this build of repokeeper applies. Bump it whenever generated output changes. */
|
|
4
|
-
export const STANDARD_VERSION = "1.4.
|
|
4
|
+
export const STANDARD_VERSION = "1.4.3";
|
|
5
5
|
export const TOOL_VERSIONS = {
|
|
6
6
|
lefthook: "2.1.14",
|
|
7
7
|
commitlintCli: "21.2.3",
|
|
8
8
|
commitlintConventional: "21.2.3",
|
|
9
|
+
semanticRelease: "25.0.9",
|
|
10
|
+
semanticReleaseChangelog: "7.0.0",
|
|
11
|
+
semanticReleaseGit: "11.0.1",
|
|
12
|
+
semanticReleaseGitlab: "13.3.3",
|
|
13
|
+
// 10.x needs conventional-changelog-writer 9, and semantic-release 25's notes generator bundles 8
|
|
14
|
+
conventionalCommitsPreset: "9.3.1",
|
|
15
|
+
renovate: "44.128.1",
|
|
9
16
|
};
|
|
10
17
|
/** Repository hosting the reusable workflows that generated CI files call. */
|
|
11
18
|
export const REUSABLE_REPO = "vannt-dev/repokeeper";
|
package/package.json
CHANGED