repokeeper 0.1.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/LICENSE +21 -0
- package/README.md +54 -0
- package/dist/cli.js +106 -0
- package/dist/commands/check.js +45 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/init.js +89 -0
- package/dist/commands/report.js +27 -0
- package/dist/commands/update.js +42 -0
- package/dist/config/load.js +99 -0
- package/dist/config/schema.js +45 -0
- package/dist/config/types.js +21 -0
- package/dist/errors.js +26 -0
- package/dist/git.js +44 -0
- package/dist/model.js +21 -0
- package/dist/modules/ci.js +5 -0
- package/dist/modules/commits.js +35 -0
- package/dist/modules/deps.js +5 -0
- package/dist/modules/editorconfig.js +44 -0
- package/dist/modules/gitignore.js +20 -0
- package/dist/modules/health.js +58 -0
- package/dist/modules/hooks.js +41 -0
- package/dist/modules/index.js +18 -0
- package/dist/modules/release.js +10 -0
- package/dist/plan.js +19 -0
- package/dist/platforms/github.js +168 -0
- package/dist/stacks/dart.js +35 -0
- package/dist/stacks/dotnet.js +47 -0
- package/dist/stacks/index.js +23 -0
- package/dist/stacks/java.js +52 -0
- package/dist/stacks/node.js +81 -0
- package/dist/stacks/python.js +68 -0
- package/dist/stacks/script.js +29 -0
- package/dist/stacks/support.js +50 -0
- package/dist/stacks/types.js +1 -0
- package/dist/sync/apply.js +82 -0
- package/dist/sync/block.js +51 -0
- package/dist/sync/decide.js +21 -0
- package/dist/sync/hash.js +8 -0
- package/dist/sync/json.js +31 -0
- package/dist/sync/lock.js +40 -0
- package/dist/sync/state.js +50 -0
- package/dist/sync/sync.js +32 -0
- package/dist/sync/yaml.js +49 -0
- package/dist/templates.js +6 -0
- package/dist/version.js +23 -0
- package/package.json +56 -0
- package/templates/CODE_OF_CONDUCT.md +83 -0
- package/templates/gitignore/Dart.gitignore +29 -0
- package/templates/gitignore/Gradle.gitignore +21 -0
- package/templates/gitignore/Java.gitignore +24 -0
- package/templates/gitignore/Maven.gitignore +17 -0
- package/templates/gitignore/Node.gitignore +149 -0
- package/templates/gitignore/Python.gitignore +220 -0
- package/templates/gitignore/VisualStudio.gitignore +432 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MANAGED_HEADER } from "../model.js";
|
|
2
|
+
import { TOOL_VERSIONS } from "../version.js";
|
|
3
|
+
const COMMITLINT_CONFIG = `// ${MANAGED_HEADER}
|
|
4
|
+
export default {
|
|
5
|
+
extends: ["@commitlint/config-conventional"],
|
|
6
|
+
rules: {
|
|
7
|
+
"header-max-length": [2, "always", 100],
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
`;
|
|
11
|
+
export const commitsModule = {
|
|
12
|
+
id: "commits",
|
|
13
|
+
enabled: (config) => config.modules.commits,
|
|
14
|
+
outputs(ctx) {
|
|
15
|
+
const outputs = [
|
|
16
|
+
{ kind: "file", module: "commits", path: "commitlint.config.mjs", content: COMMITLINT_CONFIG },
|
|
17
|
+
];
|
|
18
|
+
if (ctx.stacks.some((s) => s.id === "node")) {
|
|
19
|
+
outputs.push({
|
|
20
|
+
kind: "json",
|
|
21
|
+
module: "commits",
|
|
22
|
+
path: "package.json",
|
|
23
|
+
keyPath: ["devDependencies", "@commitlint/cli"],
|
|
24
|
+
value: `^${TOOL_VERSIONS.commitlintCli}`,
|
|
25
|
+
}, {
|
|
26
|
+
kind: "json",
|
|
27
|
+
module: "commits",
|
|
28
|
+
path: "package.json",
|
|
29
|
+
keyPath: ["devDependencies", "@commitlint/config-conventional"],
|
|
30
|
+
value: `^${TOOL_VERSIONS.commitlintConventional}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return outputs;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { MANAGED_HEADER } from "../model.js";
|
|
2
|
+
const EDITORCONFIG = `# ${MANAGED_HEADER}
|
|
3
|
+
root = true
|
|
4
|
+
|
|
5
|
+
[*]
|
|
6
|
+
charset = utf-8
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
indent_style = space
|
|
11
|
+
indent_size = 2
|
|
12
|
+
|
|
13
|
+
[*.{cs,csx,vb,fs,fsx,fsi,py}]
|
|
14
|
+
indent_size = 4
|
|
15
|
+
|
|
16
|
+
[*.{ps1,psm1,bat,cmd}]
|
|
17
|
+
end_of_line = crlf
|
|
18
|
+
|
|
19
|
+
[*.md]
|
|
20
|
+
trim_trailing_whitespace = false
|
|
21
|
+
|
|
22
|
+
[Makefile]
|
|
23
|
+
indent_style = tab
|
|
24
|
+
`;
|
|
25
|
+
const GITATTRIBUTES = [
|
|
26
|
+
"* text=auto eol=lf",
|
|
27
|
+
"*.{ps1,psm1,bat,cmd} text eol=crlf",
|
|
28
|
+
"*.{png,jpg,jpeg,gif,ico,webp,pdf,zip,gz,woff,woff2} binary",
|
|
29
|
+
].join("\n");
|
|
30
|
+
export const editorconfigModule = {
|
|
31
|
+
id: "editorconfig",
|
|
32
|
+
enabled: (config) => config.modules.editorconfig,
|
|
33
|
+
outputs: () => [
|
|
34
|
+
{ kind: "file", module: "editorconfig", path: ".editorconfig", content: EDITORCONFIG },
|
|
35
|
+
{
|
|
36
|
+
kind: "block",
|
|
37
|
+
module: "editorconfig",
|
|
38
|
+
path: ".gitattributes",
|
|
39
|
+
id: "editorconfig",
|
|
40
|
+
comment: "hash",
|
|
41
|
+
body: GITATTRIBUTES,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readTemplate } from "../templates.js";
|
|
2
|
+
export const gitignoreModule = {
|
|
3
|
+
id: "gitignore",
|
|
4
|
+
enabled: (config) => config.modules.gitignore,
|
|
5
|
+
outputs(ctx) {
|
|
6
|
+
const names = [...new Set(ctx.stacks.flatMap((s) => s.gitignore))];
|
|
7
|
+
const sections = names.map((n) => `## ${n} (github/gitignore)\n${readTemplate(`gitignore/${n}.gitignore`).trim()}`);
|
|
8
|
+
sections.push("## repokeeper\n*.repokeeper-new");
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
kind: "block",
|
|
12
|
+
module: "gitignore",
|
|
13
|
+
path: ".gitignore",
|
|
14
|
+
id: "gitignore",
|
|
15
|
+
comment: "hash",
|
|
16
|
+
body: sections.join("\n\n"),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { UsageError } from "../errors.js";
|
|
2
|
+
import { MANAGED_HEADER } from "../model.js";
|
|
3
|
+
import { readTemplate } from "../templates.js";
|
|
4
|
+
import { TOOL_VERSIONS } from "../version.js";
|
|
5
|
+
const MIT = (holder) => `MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) ${holder}
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
`;
|
|
27
|
+
const md = (path, content) => ({ kind: "file", module: "health", path, content });
|
|
28
|
+
export const healthModule = {
|
|
29
|
+
id: "health",
|
|
30
|
+
enabled: (config) => config.modules.health !== false,
|
|
31
|
+
outputs(ctx) {
|
|
32
|
+
const health = ctx.config.modules.health;
|
|
33
|
+
if (!health)
|
|
34
|
+
return [];
|
|
35
|
+
const { owner, name } = ctx.repo;
|
|
36
|
+
const outputs = [];
|
|
37
|
+
if (health.license !== false) {
|
|
38
|
+
if (health.license !== "MIT") {
|
|
39
|
+
throw new UsageError(`license ${health.license} is not bundled with this version of repokeeper; use MIT, or set modules.health.license to false`);
|
|
40
|
+
}
|
|
41
|
+
outputs.push(md("LICENSE", MIT(health.copyright)));
|
|
42
|
+
}
|
|
43
|
+
const report = owner
|
|
44
|
+
? `through [GitHub security advisories](https://github.com/${owner}/${name}/security/advisories/new)`
|
|
45
|
+
: "through the repository's Security tab (Report a vulnerability)";
|
|
46
|
+
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`));
|
|
47
|
+
const install = ctx.stacks.map((s) => s.install).filter((c) => c !== null);
|
|
48
|
+
const run = install.length ? `Run ${install.map((c) => `\`${c}\``).join(" and ")}.` : "";
|
|
49
|
+
const hooks = ctx.stacks.some((s) => s.id === "node")
|
|
50
|
+
? "Installing the dependencies also installs the git hooks (lefthook)."
|
|
51
|
+
: `Then run \`npx --yes lefthook@${TOOL_VERSIONS.lefthook} install\` once to enable the git hooks (they need Node.js 22 or newer).`;
|
|
52
|
+
const setup = [run, hooks].filter(Boolean).join(" ");
|
|
53
|
+
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 \`main\`.\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 pull request. It is merged once the checks pass and it has been reviewed.\n\nPlease follow the [Code of Conduct](CODE_OF_CONDUCT.md).\n`));
|
|
54
|
+
outputs.push(md("CODE_OF_CONDUCT.md", readTemplate("CODE_OF_CONDUCT.md").replace("[INSERT CONTACT METHOD]", health.contact)));
|
|
55
|
+
outputs.push(...ctx.platform.communityFiles(ctx));
|
|
56
|
+
return outputs;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { stringify } from "yaml";
|
|
2
|
+
import { MANAGED_HEADER } from "../model.js";
|
|
3
|
+
import { TOOL_VERSIONS } from "../version.js";
|
|
4
|
+
export const hooksModule = {
|
|
5
|
+
id: "hooks",
|
|
6
|
+
enabled: (config) => config.modules.hooks,
|
|
7
|
+
outputs(ctx) {
|
|
8
|
+
const config = {};
|
|
9
|
+
const preCommit = ctx.stacks.flatMap((s) => s.staged).map((job) => ({ ...job, stage_fixed: true }));
|
|
10
|
+
if (preCommit.length > 0)
|
|
11
|
+
config["pre-commit"] = { parallel: true, jobs: preCommit };
|
|
12
|
+
if (ctx.config.modules.commits) {
|
|
13
|
+
const node = ctx.stacks.some((s) => s.id === "node");
|
|
14
|
+
const commitlint = node
|
|
15
|
+
? "npx --no-install commitlint --edit {1}"
|
|
16
|
+
: `npx --yes --package @commitlint/cli@${TOOL_VERSIONS.commitlintCli} --package @commitlint/config-conventional@${TOOL_VERSIONS.commitlintConventional} -- commitlint --edit {1}`;
|
|
17
|
+
config["commit-msg"] = { jobs: [{ name: "commitlint", run: commitlint }] };
|
|
18
|
+
}
|
|
19
|
+
const prePush = ctx.stacks.flatMap((s) => (s.test ? [{ name: `${s.id}:test`, run: s.test }] : []));
|
|
20
|
+
if (prePush.length > 0)
|
|
21
|
+
config["pre-push"] = { jobs: prePush };
|
|
22
|
+
const outputs = [
|
|
23
|
+
{
|
|
24
|
+
kind: "file",
|
|
25
|
+
module: "hooks",
|
|
26
|
+
path: "lefthook.yml",
|
|
27
|
+
content: `# ${MANAGED_HEADER}\n${stringify(config, { lineWidth: 0 })}`,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
if (ctx.stacks.some((s) => s.id === "node")) {
|
|
31
|
+
outputs.push({
|
|
32
|
+
kind: "json",
|
|
33
|
+
module: "hooks",
|
|
34
|
+
path: "package.json",
|
|
35
|
+
keyPath: ["devDependencies", "lefthook"],
|
|
36
|
+
value: `^${TOOL_VERSIONS.lefthook}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return outputs;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ciModule } from "./ci.js";
|
|
2
|
+
import { commitsModule } from "./commits.js";
|
|
3
|
+
import { depsModule } from "./deps.js";
|
|
4
|
+
import { editorconfigModule } from "./editorconfig.js";
|
|
5
|
+
import { gitignoreModule } from "./gitignore.js";
|
|
6
|
+
import { healthModule } from "./health.js";
|
|
7
|
+
import { hooksModule } from "./hooks.js";
|
|
8
|
+
import { releaseModule } from "./release.js";
|
|
9
|
+
export const MODULES = [
|
|
10
|
+
editorconfigModule,
|
|
11
|
+
gitignoreModule,
|
|
12
|
+
commitsModule,
|
|
13
|
+
hooksModule,
|
|
14
|
+
healthModule,
|
|
15
|
+
depsModule,
|
|
16
|
+
ciModule,
|
|
17
|
+
releaseModule,
|
|
18
|
+
];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** One release per repository: the first stack with a language release type wins, otherwise the first stack's `simple` release (which may name extra files). */
|
|
2
|
+
export function pickRelease(stacks) {
|
|
3
|
+
const language = stacks.find((stack) => stack.release.type !== "simple");
|
|
4
|
+
return language?.release ?? stacks[0]?.release ?? { type: "simple", version: null };
|
|
5
|
+
}
|
|
6
|
+
export const releaseModule = {
|
|
7
|
+
id: "release",
|
|
8
|
+
enabled: (config) => config.modules.release,
|
|
9
|
+
outputs: (ctx) => ctx.platform.releaseAutomation(ctx, pickRelease(ctx.stacks)),
|
|
10
|
+
};
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { outputId } from "./model.js";
|
|
2
|
+
import { MODULES } from "./modules/index.js";
|
|
3
|
+
const toPosix = (path) => path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4
|
+
/** Pure: the outputs the standard asks for, given the config and resolved stacks. */
|
|
5
|
+
export function planOutputs(ctx, modules = MODULES) {
|
|
6
|
+
const owned = new Set(ctx.config.owned.map(toPosix));
|
|
7
|
+
const outputs = modules
|
|
8
|
+
.filter((module) => module.enabled(ctx.config))
|
|
9
|
+
.flatMap((module) => module.outputs(ctx))
|
|
10
|
+
.filter((output) => !owned.has(output.path));
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
for (const output of outputs) {
|
|
13
|
+
const id = outputId(output);
|
|
14
|
+
if (seen.has(id))
|
|
15
|
+
throw new Error(`two modules produce ${id}`);
|
|
16
|
+
seen.add(id);
|
|
17
|
+
}
|
|
18
|
+
return outputs.sort((a, b) => outputId(a).localeCompare(outputId(b)));
|
|
19
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { stringify } from "yaml";
|
|
2
|
+
import { MANAGED_HEADER } from "../model.js";
|
|
3
|
+
import { REUSABLE_REPO, WORKFLOW_REF } from "../version.js";
|
|
4
|
+
const yamlFile = (module, path, data) => ({
|
|
5
|
+
kind: "file",
|
|
6
|
+
module,
|
|
7
|
+
path,
|
|
8
|
+
content: `# ${MANAGED_HEADER}\n${stringify(data)}`,
|
|
9
|
+
});
|
|
10
|
+
const WORKFLOW_KEYS = ["name", "on", "permissions", "concurrency", "env", "defaults", "jobs"];
|
|
11
|
+
/** Reference to a reusable workflow; local inside the repository that hosts them. */
|
|
12
|
+
function workflowRef(ctx, file) {
|
|
13
|
+
const self = ctx.repo.owner !== null && `${ctx.repo.owner}/${ctx.repo.name}` === REUSABLE_REPO;
|
|
14
|
+
return self ? `./.github/workflows/${file}` : `${REUSABLE_REPO}/.github/workflows/${file}@${WORKFLOW_REF}`;
|
|
15
|
+
}
|
|
16
|
+
const workflowKey = (module, path, keyPath, value) => ({
|
|
17
|
+
kind: "yaml",
|
|
18
|
+
module,
|
|
19
|
+
path,
|
|
20
|
+
keyPath,
|
|
21
|
+
value,
|
|
22
|
+
order: WORKFLOW_KEYS,
|
|
23
|
+
});
|
|
24
|
+
function defaultBranch(ctx) {
|
|
25
|
+
const branch = ctx.config.github?.default_branch;
|
|
26
|
+
return typeof branch === "string" && branch.length > 0 ? branch : "main";
|
|
27
|
+
}
|
|
28
|
+
const RELEASE_PLEASE_SCHEMA = "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json";
|
|
29
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
30
|
+
export const githubPlatform = {
|
|
31
|
+
id: "github",
|
|
32
|
+
communityFiles(ctx) {
|
|
33
|
+
const { owner, name } = ctx.repo;
|
|
34
|
+
const outputs = [
|
|
35
|
+
yamlFile("health", ".github/ISSUE_TEMPLATE/bug_report.yml", {
|
|
36
|
+
name: "Bug report",
|
|
37
|
+
description: "Report something that does not work as expected",
|
|
38
|
+
labels: ["bug"],
|
|
39
|
+
body: [
|
|
40
|
+
{
|
|
41
|
+
type: "textarea",
|
|
42
|
+
id: "what-happened",
|
|
43
|
+
attributes: { label: "What happened?", description: "Include the steps to reproduce it." },
|
|
44
|
+
validations: { required: true },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "textarea",
|
|
48
|
+
id: "expected",
|
|
49
|
+
attributes: { label: "What did you expect?" },
|
|
50
|
+
validations: { required: true },
|
|
51
|
+
},
|
|
52
|
+
{ type: "input", id: "version", attributes: { label: "Version" } },
|
|
53
|
+
],
|
|
54
|
+
}),
|
|
55
|
+
yamlFile("health", ".github/ISSUE_TEMPLATE/feature_request.yml", {
|
|
56
|
+
name: "Feature request",
|
|
57
|
+
description: "Suggest an improvement",
|
|
58
|
+
labels: ["enhancement"],
|
|
59
|
+
body: [
|
|
60
|
+
{
|
|
61
|
+
type: "textarea",
|
|
62
|
+
id: "problem",
|
|
63
|
+
attributes: { label: "What problem would this solve?" },
|
|
64
|
+
validations: { required: true },
|
|
65
|
+
},
|
|
66
|
+
{ type: "textarea", id: "proposal", attributes: { label: "What do you propose?" } },
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
yamlFile("health", ".github/ISSUE_TEMPLATE/config.yml", {
|
|
70
|
+
blank_issues_enabled: false,
|
|
71
|
+
contact_links: owner
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
name: "Report a security vulnerability",
|
|
75
|
+
url: `https://github.com/${owner}/${name}/security/advisories/new`,
|
|
76
|
+
about: "Please report vulnerabilities privately.",
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
: [],
|
|
80
|
+
}),
|
|
81
|
+
{
|
|
82
|
+
kind: "file",
|
|
83
|
+
module: "health",
|
|
84
|
+
path: ".github/pull_request_template.md",
|
|
85
|
+
content: `<!-- ${MANAGED_HEADER} -->\n\n## Summary\n\n## Testing\n\n- [ ] Tests added or updated\n- [ ] Commit messages follow Conventional Commits\n`,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
const health = ctx.config.modules.health;
|
|
89
|
+
if (health && health.codeowners.length > 0) {
|
|
90
|
+
outputs.push({
|
|
91
|
+
kind: "file",
|
|
92
|
+
module: "health",
|
|
93
|
+
path: ".github/CODEOWNERS",
|
|
94
|
+
content: `# ${MANAGED_HEADER}\n* ${health.codeowners.join(" ")}\n`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return outputs;
|
|
98
|
+
},
|
|
99
|
+
dependencyUpdates(ecosystems) {
|
|
100
|
+
const updates = [...new Set([...ecosystems, "github-actions"])].map((ecosystem) => ({
|
|
101
|
+
"package-ecosystem": ecosystem,
|
|
102
|
+
directory: "/",
|
|
103
|
+
schedule: { interval: "weekly" },
|
|
104
|
+
groups: { [`${ecosystem}-minor-and-patch`]: { "update-types": ["minor", "patch"] } },
|
|
105
|
+
}));
|
|
106
|
+
return [yamlFile("deps", ".github/dependabot.yml", { version: 2, updates })];
|
|
107
|
+
},
|
|
108
|
+
ciWorkflow(ctx) {
|
|
109
|
+
const path = ".github/workflows/ci.yml";
|
|
110
|
+
const jobs = [];
|
|
111
|
+
for (const stack of ctx.stacks) {
|
|
112
|
+
if (stack.ci) {
|
|
113
|
+
jobs.push(workflowKey("ci", path, ["jobs", stack.id], {
|
|
114
|
+
uses: workflowRef(ctx, stack.ci.workflow),
|
|
115
|
+
with: stack.ci.with,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (ctx.config.modules.commits) {
|
|
120
|
+
jobs.push(workflowKey("ci", path, ["jobs", "commits"], { uses: workflowRef(ctx, "commitlint.yml") }));
|
|
121
|
+
}
|
|
122
|
+
if (jobs.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
return [
|
|
125
|
+
workflowKey("ci", path, ["name"], "ci"),
|
|
126
|
+
workflowKey("ci", path, ["on"], { pull_request: null, push: { branches: [defaultBranch(ctx)] } }),
|
|
127
|
+
workflowKey("ci", path, ["permissions"], { contents: "read" }),
|
|
128
|
+
...jobs,
|
|
129
|
+
];
|
|
130
|
+
},
|
|
131
|
+
releaseAutomation(ctx, release) {
|
|
132
|
+
const path = ".github/workflows/release.yml";
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
kind: "file",
|
|
136
|
+
module: "release",
|
|
137
|
+
path: "release-please-config.json",
|
|
138
|
+
content: json({
|
|
139
|
+
$schema: RELEASE_PLEASE_SCHEMA,
|
|
140
|
+
packages: {
|
|
141
|
+
".": {
|
|
142
|
+
"release-type": release.type,
|
|
143
|
+
"changelog-path": "CHANGELOG.md",
|
|
144
|
+
"bump-minor-pre-major": true,
|
|
145
|
+
"include-component-in-tag": false,
|
|
146
|
+
...(release.extraFiles && release.extraFiles.length > 0 ? { "extra-files": release.extraFiles } : {}),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
kind: "seed",
|
|
153
|
+
module: "release",
|
|
154
|
+
path: ".release-please-manifest.json",
|
|
155
|
+
content: json({ ".": release.version ?? "0.0.0" }),
|
|
156
|
+
},
|
|
157
|
+
workflowKey("release", path, ["name"], "release"),
|
|
158
|
+
workflowKey("release", path, ["on"], { push: { branches: [defaultBranch(ctx)] } }),
|
|
159
|
+
workflowKey("release", path, ["permissions"], { contents: "read" }),
|
|
160
|
+
workflowKey("release", path, ["jobs", "release"], {
|
|
161
|
+
uses: workflowRef(ctx, "release-please.yml"),
|
|
162
|
+
permissions: { contents: "write", "pull-requests": "write", issues: "write" },
|
|
163
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: a GitHub Actions expression, not a JS template
|
|
164
|
+
secrets: { token: "${{ secrets.RELEASE_PLEASE_TOKEN }}" },
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { checkKeys, readText, stringList } from "./support.js";
|
|
4
|
+
const OPTION_KEYS = ["versions", "os"];
|
|
5
|
+
export const dartStack = {
|
|
6
|
+
id: "dart",
|
|
7
|
+
detect: (root) => existsSync(join(root, "pubspec.yaml")),
|
|
8
|
+
async resolve(root, options = {}) {
|
|
9
|
+
checkKeys("dart", options, OPTION_KEYS);
|
|
10
|
+
const pubspec = (await readText(root, "pubspec.yaml")) ?? "";
|
|
11
|
+
const flutter = /sdk:\s*flutter\b/.test(pubspec);
|
|
12
|
+
const tool = flutter ? "flutter" : "dart";
|
|
13
|
+
const test = existsSync(join(root, "test")) ? `${tool} test` : null;
|
|
14
|
+
const commands = ["dart format --output=none --set-exit-if-changed .", `${tool} analyze`, ...(test ? [test] : [])];
|
|
15
|
+
return {
|
|
16
|
+
id: "dart",
|
|
17
|
+
staged: [{ name: "dart:format", glob: "*.dart", run: "dart format {staged_files}" }],
|
|
18
|
+
test,
|
|
19
|
+
install: `${tool} pub get`,
|
|
20
|
+
gitignore: ["Dart"],
|
|
21
|
+
dependabot: ["pub"],
|
|
22
|
+
ci: {
|
|
23
|
+
workflow: "stack-dart.yml",
|
|
24
|
+
with: {
|
|
25
|
+
"sdk-versions": JSON.stringify(stringList("dart", options, "versions") ?? ["stable"]),
|
|
26
|
+
os: JSON.stringify(stringList("dart", options, "os") ?? ["ubuntu-latest"]),
|
|
27
|
+
flutter: String(flutter),
|
|
28
|
+
"install-command": `${tool} pub get`,
|
|
29
|
+
commands: JSON.stringify(commands),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
release: { type: "dart", version: /^version:\s*(\S+)/m.exec(pubspec)?.[1] ?? null },
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { checkKeys, filesMatching, readText, stringList } from "./support.js";
|
|
2
|
+
const OPTION_KEYS = ["versions", "os"];
|
|
3
|
+
const SOLUTIONS = /\.(sln|slnx)$/;
|
|
4
|
+
const PROJECTS = /\.(csproj|fsproj|vbproj)$/;
|
|
5
|
+
const VERSION_XPATH = "//Project/PropertyGroup/Version";
|
|
6
|
+
const versionOf = (xml) => (xml ? (/<Version>\s*([^<\s]+)\s*<\/Version>/.exec(xml)?.[1] ?? null) : null);
|
|
7
|
+
export const dotnetStack = {
|
|
8
|
+
id: "dotnet",
|
|
9
|
+
detect: (root) => filesMatching(root, ".", SOLUTIONS).length + filesMatching(root, ".", PROJECTS).length > 0,
|
|
10
|
+
async resolve(root, options = {}) {
|
|
11
|
+
checkKeys("dotnet", options, OPTION_KEYS);
|
|
12
|
+
const solutions = filesMatching(root, ".", SOLUTIONS);
|
|
13
|
+
const projects = filesMatching(root, ".", PROJECTS);
|
|
14
|
+
// with several entries at the root, dotnet stops with MSB1011 unless one is named
|
|
15
|
+
const target = solutions.length + projects.length > 1 ? ` ${solutions[0] ?? projects[0]}` : "";
|
|
16
|
+
let release = { type: "simple", version: null };
|
|
17
|
+
for (const file of ["Directory.Build.props", ...(projects.length === 1 ? projects : [])]) {
|
|
18
|
+
const version = versionOf(await readText(root, file));
|
|
19
|
+
if (version) {
|
|
20
|
+
release = { type: "simple", version, extraFiles: [{ type: "xml", path: file, xpath: VERSION_XPATH }] };
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
id: "dotnet",
|
|
26
|
+
staged: [{ name: "dotnet:format", glob: "*.{cs,vb,fs}", run: `dotnet format${target} --include {staged_files}` }],
|
|
27
|
+
test: `dotnet test${target}`,
|
|
28
|
+
install: `dotnet restore${target}`,
|
|
29
|
+
gitignore: ["VisualStudio"],
|
|
30
|
+
dependabot: ["nuget"],
|
|
31
|
+
ci: {
|
|
32
|
+
workflow: "stack-dotnet.yml",
|
|
33
|
+
with: {
|
|
34
|
+
"dotnet-versions": JSON.stringify(stringList("dotnet", options, "versions") ?? ["8.0", "9.0"]),
|
|
35
|
+
os: JSON.stringify(stringList("dotnet", options, "os") ?? ["ubuntu-latest"]),
|
|
36
|
+
commands: JSON.stringify([
|
|
37
|
+
`dotnet restore${target}`,
|
|
38
|
+
`dotnet format${target} --verify-no-changes --no-restore`,
|
|
39
|
+
`dotnet build${target} --no-restore`,
|
|
40
|
+
`dotnet test${target} --no-build`,
|
|
41
|
+
]),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
release,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { STACK_IDS } from "../config/types.js";
|
|
2
|
+
import { dartStack } from "./dart.js";
|
|
3
|
+
import { dotnetStack } from "./dotnet.js";
|
|
4
|
+
import { javaStack } from "./java.js";
|
|
5
|
+
import { nodeStack } from "./node.js";
|
|
6
|
+
import { pythonStack } from "./python.js";
|
|
7
|
+
import { scriptStack } from "./script.js";
|
|
8
|
+
const PACKS = {
|
|
9
|
+
node: nodeStack,
|
|
10
|
+
python: pythonStack,
|
|
11
|
+
dart: dartStack,
|
|
12
|
+
script: scriptStack,
|
|
13
|
+
java: javaStack,
|
|
14
|
+
dotnet: dotnetStack,
|
|
15
|
+
};
|
|
16
|
+
export function getStackPack(id) {
|
|
17
|
+
return PACKS[id];
|
|
18
|
+
}
|
|
19
|
+
export async function detectStacks(root) {
|
|
20
|
+
const found = STACK_IDS.filter((id) => PACKS[id].detect(root));
|
|
21
|
+
// scripts beside another stack belong to that stack; the script pack is for script-only repositories
|
|
22
|
+
return found.length > 1 ? found.filter((id) => id !== "script") : found;
|
|
23
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { checkKeys, readText, stringList } from "./support.js";
|
|
4
|
+
const OPTION_KEYS = ["versions", "os"];
|
|
5
|
+
/** The project's own <version>: parent, dependency and plugin versions are removed first. */
|
|
6
|
+
function mavenVersion(pom) {
|
|
7
|
+
const own = pom.replace(/<(parent|dependencies|dependencyManagement|build|profiles|reporting)>[\s\S]*?<\/\1>/g, "");
|
|
8
|
+
const version = /<version>\s*([^<\s]+)\s*<\/version>/.exec(own)?.[1];
|
|
9
|
+
return version && !version.includes("${") ? version : null;
|
|
10
|
+
}
|
|
11
|
+
export const javaStack = {
|
|
12
|
+
id: "java",
|
|
13
|
+
detect: (root) => ["pom.xml", "build.gradle", "build.gradle.kts"].some((f) => existsSync(join(root, f))),
|
|
14
|
+
async resolve(root, options = {}) {
|
|
15
|
+
checkKeys("java", options, OPTION_KEYS);
|
|
16
|
+
const has = (path) => existsSync(join(root, path));
|
|
17
|
+
const maven = has("pom.xml");
|
|
18
|
+
let command;
|
|
19
|
+
let release;
|
|
20
|
+
if (maven) {
|
|
21
|
+
command = has("mvnw") ? "./mvnw -B verify" : "mvn -B verify";
|
|
22
|
+
release = { type: "maven", version: mavenVersion((await readText(root, "pom.xml")) ?? "") };
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
command = has("gradlew") ? "./gradlew check" : "gradle check";
|
|
26
|
+
const properties = await readText(root, "gradle.properties");
|
|
27
|
+
const version = properties ? (/^version\s*=\s*(\S+)/m.exec(properties)?.[1] ?? null) : null;
|
|
28
|
+
// release-please's generic updater changes gradle.properties once it carries x-release-please markers
|
|
29
|
+
release = { type: "simple", version, ...(properties !== null ? { extraFiles: ["gradle.properties"] } : {}) };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
id: "java",
|
|
33
|
+
// formatting runs through the build (Spotless), not per staged file
|
|
34
|
+
staged: [],
|
|
35
|
+
test: command,
|
|
36
|
+
install: null,
|
|
37
|
+
gitignore: ["Java", maven ? "Maven" : "Gradle"],
|
|
38
|
+
dependabot: [maven ? "maven" : "gradle"],
|
|
39
|
+
ci: {
|
|
40
|
+
workflow: "stack-java.yml",
|
|
41
|
+
with: {
|
|
42
|
+
"java-versions": JSON.stringify(stringList("java", options, "versions") ?? ["17", "21"]),
|
|
43
|
+
os: JSON.stringify(stringList("java", options, "os") ?? ["ubuntu-latest"]),
|
|
44
|
+
"build-tool": maven ? "maven" : "gradle",
|
|
45
|
+
"gradle-version": !maven && !has("gradlew") ? "current" : "",
|
|
46
|
+
commands: JSON.stringify([command]),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
release,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|