repokeeper 0.5.0 → 0.7.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.
Files changed (43) hide show
  1. package/.github/workflows/commitlint.yml +49 -0
  2. package/.github/workflows/release-please.yml +59 -0
  3. package/.github/workflows/repokeeper-check.yml +30 -0
  4. package/.github/workflows/stack-dart.yml +72 -0
  5. package/.github/workflows/stack-dotnet.yml +68 -0
  6. package/.github/workflows/stack-go.yml +59 -0
  7. package/.github/workflows/stack-java.yml +70 -0
  8. package/.github/workflows/stack-node.yml +85 -0
  9. package/.github/workflows/stack-php.yml +66 -0
  10. package/.github/workflows/stack-python.yml +70 -0
  11. package/.github/workflows/stack-ruby.yml +65 -0
  12. package/.github/workflows/stack-rust.yml +60 -0
  13. package/.github/workflows/stack-script.yml +87 -0
  14. package/README.md +60 -106
  15. package/dist/cli.js +52 -9
  16. package/dist/commands/bump.js +30 -0
  17. package/dist/commands/context.js +19 -2
  18. package/dist/commands/eject.js +74 -0
  19. package/dist/commands/gitlab.js +46 -0
  20. package/dist/commands/init.js +104 -10
  21. package/dist/commands/presets.js +114 -0
  22. package/dist/commands/report.js +7 -5
  23. package/dist/commands/update.js +3 -2
  24. package/dist/config/schema.js +40 -1
  25. package/dist/duplicates.js +2 -2
  26. package/dist/errors.js +1 -1
  27. package/dist/gitlab/api.js +50 -0
  28. package/dist/gitlab/settings.js +181 -0
  29. package/dist/model.js +8 -0
  30. package/dist/modules/commits.js +3 -2
  31. package/dist/modules/deps.js +1 -1
  32. package/dist/modules/health.js +3 -3
  33. package/dist/modules/hooks.js +7 -5
  34. package/dist/modules/release.js +18 -5
  35. package/dist/platforms/github.js +70 -12
  36. package/dist/platforms/gitlab-jobs.js +191 -0
  37. package/dist/platforms/gitlab.js +53 -13
  38. package/dist/release/bump.js +223 -0
  39. package/dist/stacks/index.js +56 -0
  40. package/dist/stacks/support.js +19 -0
  41. package/dist/templates.js +13 -1
  42. package/dist/version.js +3 -0
  43. package/package.json +6 -2
@@ -0,0 +1,65 @@
1
+ name: stack-ruby
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ ruby-versions:
7
+ description: JSON list of Ruby versions
8
+ type: string
9
+ default: '["3.3","3.4"]'
10
+ os:
11
+ description: JSON list of runner labels
12
+ type: string
13
+ default: '["ubuntu-latest"]'
14
+ install-command:
15
+ description: Command that installs dependencies
16
+ type: string
17
+ default: bundle install
18
+ commands:
19
+ description: JSON list of shell commands run in order
20
+ type: string
21
+ default: "[]"
22
+ working-directory:
23
+ description: Directory holding the Gemfile
24
+ type: string
25
+ default: .
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ ruby:
32
+ name: ruby ${{ matrix.ruby }} (${{ matrix.os }})
33
+ strategy:
34
+ fail-fast: false
35
+ matrix:
36
+ os: ${{ fromJSON(inputs.os) }}
37
+ ruby: ${{ fromJSON(inputs.ruby-versions) }}
38
+ runs-on: ${{ matrix.os }}
39
+ permissions:
40
+ contents: read
41
+ defaults:
42
+ run:
43
+ shell: bash
44
+ working-directory: ${{ inputs.working-directory }}
45
+ steps:
46
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
47
+ with:
48
+ persist-credentials: false
49
+ - uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
50
+ with:
51
+ ruby-version: ${{ matrix.ruby }}
52
+ working-directory: ${{ inputs.working-directory }}
53
+ - name: Install dependencies
54
+ env:
55
+ INSTALL: ${{ inputs.install-command }}
56
+ run: bash -c "$INSTALL"
57
+ - name: Run checks
58
+ env:
59
+ COMMANDS: ${{ inputs.commands }}
60
+ run: |
61
+ node -e 'for (const c of JSON.parse(process.env.COMMANDS)) console.log(c)' | while IFS= read -r command; do
62
+ echo "::group::$command"
63
+ bash -c "$command" </dev/null
64
+ echo "::endgroup::"
65
+ done
@@ -0,0 +1,60 @@
1
+ name: stack-rust
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ rust-versions:
7
+ description: JSON list of rustup toolchains ("stable", "1.85.0")
8
+ type: string
9
+ default: '["stable"]'
10
+ os:
11
+ description: JSON list of runner labels
12
+ type: string
13
+ default: '["ubuntu-latest"]'
14
+ commands:
15
+ description: JSON list of shell commands run in order
16
+ type: string
17
+ default: '["cargo test --all-targets"]'
18
+ working-directory:
19
+ description: Directory holding Cargo.toml
20
+ type: string
21
+ default: .
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ rust:
28
+ name: rust ${{ matrix.rust }} (${{ matrix.os }})
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ os: ${{ fromJSON(inputs.os) }}
33
+ rust: ${{ fromJSON(inputs.rust-versions) }}
34
+ runs-on: ${{ matrix.os }}
35
+ permissions:
36
+ contents: read
37
+ defaults:
38
+ run:
39
+ shell: bash
40
+ working-directory: ${{ inputs.working-directory }}
41
+ steps:
42
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
+ with:
44
+ persist-credentials: false
45
+ - name: Install the toolchain
46
+ env:
47
+ TOOLCHAIN: ${{ matrix.rust }}
48
+ # rustup ships on the hosted runners
49
+ run: |
50
+ rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt,clippy
51
+ rustup default "$TOOLCHAIN"
52
+ - name: Run checks
53
+ env:
54
+ COMMANDS: ${{ inputs.commands }}
55
+ run: |
56
+ node -e 'for (const c of JSON.parse(process.env.COMMANDS)) console.log(c)' | while IFS= read -r command; do
57
+ echo "::group::$command"
58
+ bash -c "$command" </dev/null
59
+ echo "::endgroup::"
60
+ done
@@ -0,0 +1,87 @@
1
+ name: stack-script
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ shell-scripts:
7
+ description: '"true" to run ShellCheck and shfmt on *.sh'
8
+ type: string
9
+ default: "true"
10
+ powershell-scripts:
11
+ description: '"true" to run PSScriptAnalyzer on *.ps1'
12
+ type: string
13
+ default: "false"
14
+ test-command:
15
+ description: Test command run on Linux and Windows; empty for none
16
+ type: string
17
+ default: ""
18
+ working-directory:
19
+ description: Directory the checks start from
20
+ type: string
21
+ default: .
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ shell:
28
+ if: inputs.shell-scripts == 'true'
29
+ runs-on: ubuntu-latest
30
+ permissions:
31
+ contents: read
32
+ defaults:
33
+ run:
34
+ working-directory: ${{ inputs.working-directory }}
35
+ steps:
36
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
37
+ with:
38
+ persist-credentials: false
39
+ - name: ShellCheck
40
+ run: git ls-files -z '*.sh' | xargs -0 --no-run-if-empty shellcheck
41
+ - name: shfmt
42
+ run: |
43
+ curl -fsSL -o "$RUNNER_TEMP/shfmt" https://github.com/mvdan/sh/releases/download/v3.14.1/shfmt_v3.14.1_linux_amd64
44
+ chmod +x "$RUNNER_TEMP/shfmt"
45
+ git ls-files -z '*.sh' | xargs -0 --no-run-if-empty "$RUNNER_TEMP/shfmt" -d
46
+ powershell:
47
+ if: inputs.powershell-scripts == 'true'
48
+ runs-on: windows-latest
49
+ permissions:
50
+ contents: read
51
+ defaults:
52
+ run:
53
+ working-directory: ${{ inputs.working-directory }}
54
+ steps:
55
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
56
+ with:
57
+ persist-credentials: false
58
+ - name: PSScriptAnalyzer
59
+ shell: pwsh
60
+ run: |
61
+ Install-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Force -Scope CurrentUser
62
+ # a repository settings file decides the rules; otherwise only errors and warnings fail the job
63
+ $issues = if (Test-Path PSScriptAnalyzerSettings.psd1) {
64
+ Invoke-ScriptAnalyzer -Path . -Recurse -Settings PSScriptAnalyzerSettings.psd1
65
+ } else {
66
+ Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error, Warning
67
+ }
68
+ $issues | Format-Table -AutoSize
69
+ if ($issues) { exit 1 }
70
+ test:
71
+ if: inputs.test-command != ''
72
+ strategy:
73
+ fail-fast: false
74
+ matrix:
75
+ os: [ubuntu-latest, windows-latest]
76
+ runs-on: ${{ matrix.os }}
77
+ permissions:
78
+ contents: read
79
+ defaults:
80
+ run:
81
+ shell: bash
82
+ working-directory: ${{ inputs.working-directory }}
83
+ steps:
84
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
85
+ with:
86
+ persist-credentials: false
87
+ - run: ${{ inputs.test-command }}
package/README.md CHANGED
@@ -6,13 +6,40 @@ 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`. GitLab is
10
- > supported for Node.js projects. See the
9
+ > Ruby, each with CI and releases, on GitHub and on GitLab, plus repository settings through
10
+ > `repokeeper github apply` and `repokeeper gitlab apply`. See the
11
11
  > [design](docs/superpowers/specs/2026-09-25-repokeeper-design.md).
12
12
 
13
+ ## How it works
14
+
15
+ ```mermaid
16
+ flowchart LR
17
+ config[".repokeeper.yml<br/>stacks, modules, owned"]
18
+ tool(["repokeeper<br/>init, update, check"])
19
+ files["Managed files<br/>.editorconfig, .gitignore, lefthook.yml,<br/>commitlint, CONTRIBUTING, LICENSE,<br/>dependabot.yml, ci.yml, release.yml"]
20
+ lock[".repokeeper/lock.json<br/>what repokeeper wrote"]
21
+ workflows["Reusable workflows<br/>vannt-dev/repokeeper at v0,<br/>or pinned, mirrored, local"]
22
+ config --> tool
23
+ tool -->|"init, update: write,<br/>keep your edits"| files
24
+ tool -.->|"check: compare,<br/>exit 1 on drift"| files
25
+ tool --> lock
26
+ files -->|"ci.yml, release.yml call"| workflows
27
+ ```
28
+
29
+ One file describes the repository (`.repokeeper.yml`); repokeeper turns it into the managed files and
30
+ remembers what it wrote, so it can tell your edits from its own. `check` only compares; `update`
31
+ moves the files to a newer standard and leaves alone what you changed or listed under `owned`.
32
+
13
33
  ## Usage
14
34
 
15
- repokeeper is not on npm yet. Until the first release, build it from source and link the command:
35
+ repokeeper is [on npm](https://www.npmjs.com/package/repokeeper). Install it once, or run it without
36
+ installing by putting `npx` in front of each command below:
37
+
38
+ ```bash
39
+ npm install --global repokeeper
40
+ ```
41
+
42
+ To work on repokeeper itself, build it from source and link the command instead:
16
43
 
17
44
  ```bash
18
45
  git clone https://github.com/vannt-dev/repokeeper.git
@@ -34,6 +61,29 @@ yours. `owned` also takes a single key of a shared file, such as `.github/workfl
34
61
  `package.json#devDependencies.lefthook`; repokeeper then leaves that key, and everything under it,
35
62
  alone. Every write command accepts `--dry-run`.
36
63
 
64
+ `init` applies the whole standard unless you say otherwise:
65
+
66
+ - `repokeeper init --preset essential` writes only the editor settings, `.gitignore` and the CI
67
+ workflow. `standard` is the default. `strict` adds the drift check to CI and, on GitHub, writes
68
+ branch protection (pull requests with one approval, no force push, the commit check required) and
69
+ Dependabot security updates into `.repokeeper.yml`; `repokeeper github apply` then puts them in
70
+ place. There is no preset that promises code or secret scanning: repokeeper does not set those up.
71
+ - `repokeeper init --interactive` (or `-i`) asks about each part in turn, naming the files it would
72
+ write, shows the result, and asks once more before writing anything. Enter keeps the preset's
73
+ answer. Add `--dry-run` to go through the questions without the possibility of writing.
74
+
75
+ Either way the choice ends up as plain `modules:` switches in `.repokeeper.yml`, which you can change
76
+ later and apply with `repokeeper update`.
77
+
78
+ You can also start from the file instead of from detection: put a `.repokeeper.yml` you wrote
79
+ yourself in the repository and run `repokeeper init`. When the file is there and repokeeper has not
80
+ applied anything yet, `init` applies it as it is written. Nothing is detected, `--stack`,
81
+ `--platform` and `--preset` are refused, and the only line it changes is `standard:`, which it sets
82
+ to the standard it applied.
83
+
84
+ The [playground](https://vannt-dev.github.io/repokeeper/) writes that file for you: tick the stacks
85
+ and the parts you want, and it shows the `.repokeeper.yml` and the command to run.
86
+
37
87
  `init` reads the default branch from `origin/HEAD` and records it as `github.default_branch` when it
38
88
  isn't `main`. The release manifest starts from the latest `vX.Y.Z` tag when the stack has no version
39
89
  of its own.
@@ -49,110 +99,14 @@ projects, which the dotnet CLI can't build.
49
99
 
50
100
  Requires Node.js 22.12 or newer.
51
101
 
52
- ## CI and releases
53
-
54
- `ci.yml` and `release.yml` call reusable workflows from this repository (`stack-node.yml`,
55
- `commitlint.yml`, `release-please.yml`) at the moving major tag, so fixes reach every repository
56
- without a pull request. repokeeper owns the `name`, `on`, `permissions` and `concurrency` keys and
57
- the jobs it adds; jobs you add yourself are left alone, and so is the formatting of the rest of the
58
- file. A new push to a pull request cancels that pull request's earlier `ci` run; runs on the default
59
- branch always finish.
60
-
61
- The script stack runs ShellCheck and `shfmt -d` on `*.sh` (format with `shfmt -w` before pushing)
62
- and PSScriptAnalyzer on `*.ps1`, which fails on errors and warnings (not on information-level rules). To
63
- choose the rules yourself, add a `PSScriptAnalyzerSettings.psd1` at the repository root; the job then
64
- uses it instead of its own filter, so keep `Severity` in it unless you want information-level rules too:
65
-
66
- ```powershell
67
- @{
68
- Severity = @('Error', 'Warning')
69
- # installers print for the person running them
70
- ExcludeRules = @('PSAvoidUsingWriteHost')
71
- }
72
- ```
73
-
74
- `release.yml` runs [release-please](https://github.com/googleapis/release-please): it keeps a release
75
- pull request open, and merging it tags the release and updates `CHANGELOG.md`. Two settings make this
76
- work:
77
-
78
- - In the repository settings, under Actions → General, allow GitHub Actions to create and approve
79
- pull requests.
80
- - Optionally add a `RELEASE_PLEASE_TOKEN` secret (a fine-grained token with contents, pull requests
81
- and issues write access). Without it the release pull request is opened with `GITHUB_TOKEN`, and
82
- GitHub holds its `pull_request` runs until someone approves them; the `release-pr-ci` job approves
83
- them, so the pull request gets its checks and required checks in a ruleset can pass.
84
-
85
- A repository with no release yet (manifest at `0.0.0`) gets `initial-version: 0.1.0`, so its first
86
- release is 0.1.0 rather than release-please's default 1.0.0.
87
-
88
- Set `modules.drift: true` to add a `repokeeper` job to `ci.yml` that runs `repokeeper check` with
89
- the version that wrote the standard, so a pull request that edits a managed file fails until the edit
90
- is resolved.
91
-
92
- ## GitHub settings
93
-
94
- `repokeeper github apply` brings the repository's settings in line with the `github:` section of
95
- `.repokeeper.yml`. Only the keys you write are managed; anything left out stays as it is.
96
-
97
- ```yaml
98
- github:
99
- description: Keeps repositories on one standard
100
- topics: [cli, conventional-commits]
101
- merge: { squash: true, merge_commit: false, rebase: false, delete_branch_on_merge: true }
102
- security: { dependabot_alerts: true, dependabot_security_updates: true }
103
- protect: # a ruleset named "repokeeper" on the default branch; false removes it
104
- require_pull_request: true
105
- required_approvals: 0
106
- required_checks: ["commits / commitlint"] # check names exactly as pull requests show them
107
- allow_force_push: false
108
- ```
109
-
110
- It prints every change first and applies them with `--yes`, or after you confirm in a terminal;
111
- `--dry-run` only prints. The token comes from `GITHUB_TOKEN` or `gh auth token` and needs admin
112
- access to the repository. Legacy branch protection, visibility, secrets and collaborators are never
113
- touched.
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.
102
+ ## Guides
154
103
 
155
- `repokeeper github apply` has no GitLab counterpart yet.
104
+ | Guide | What it covers |
105
+ | --- | --- |
106
+ | [CI and releases](docs/ci-and-releases.md) | The workflows repokeeper writes, pinning or mirroring the reusable workflows, `repokeeper eject`, script linting, release-please, the drift check |
107
+ | [Monorepos](docs/monorepos.md) | Stacks in folders: `stack_options.<stack>.directory`, what runs where, the limits |
108
+ | [GitHub settings](docs/github-settings.md) | `repokeeper github apply`: description, topics, merge settings, security, branch protection |
109
+ | [GitLab](docs/gitlab.md) | What differs on GitLab, the image each stack's job runs in, how releases write the version, the two jobs that need a token, and `repokeeper gitlab apply`: merge settings, branch protection, the Renovate schedule |
156
110
 
157
111
  ## Pilots
158
112
 
package/dist/cli.js CHANGED
@@ -3,9 +3,13 @@ import { realpathSync } from "node:fs";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parseArgs } from "node:util";
6
+ import { bumpCommand } from "./commands/bump.js";
6
7
  import { checkCommand } from "./commands/check.js";
8
+ import { ejectCommand } from "./commands/eject.js";
7
9
  import { githubApplyCommand } from "./commands/github.js";
10
+ import { gitlabApplyCommand } from "./commands/gitlab.js";
8
11
  import { initCommand } from "./commands/init.js";
12
+ import { PRESET_SUMMARY, PRESETS } from "./commands/presets.js";
9
13
  import { updateCommand } from "./commands/update.js";
10
14
  import { STACK_IDS } from "./config/types.js";
11
15
  import { RepokeeperError, UsageError } from "./errors.js";
@@ -14,21 +18,27 @@ export const USAGE = [
14
18
  "usage: repokeeper <command> [options]",
15
19
  "",
16
20
  "commands:",
17
- " init detect stacks, write .repokeeper.yml and apply the standard",
21
+ " init detect stacks, write .repokeeper.yml and apply the standard (or apply a .repokeeper.yml you wrote)",
18
22
  " check report drift from the standard without writing (exit 1 on drift)",
19
23
  " update move to the standard of this repokeeper version and resync",
20
24
  " github apply diff the GitHub settings against .repokeeper.yml and apply them",
25
+ " gitlab apply the same for a GitLab project: settings, branch protection, the Renovate schedule",
26
+ " eject copy the reusable CI workflows into this repository and call them from there",
27
+ " bump <version> write a release's version into the stack's version files (the GitLab release job runs it)",
21
28
  "",
22
29
  "options:",
23
30
  " --dry-run show what would change without writing",
24
31
  " --force write even when target files have uncommitted changes",
25
32
  " --adopt <path> let repokeeper manage an existing file (repeatable); --adopt-all for every file",
26
33
  " --accept <path> take repokeeper's version of a locally edited file (update, repeatable)",
27
- " --stack <id> stack to use instead of detection (init, repeatable)",
34
+ " --stack <id> stack to use instead of detection (init, repeatable); id:folder for a stack in a folder",
28
35
  " --platform <id> github or gitlab, instead of detection from the origin remote (init)",
36
+ ` --preset <name> ${PRESETS.map((name) => `${name}: ${PRESET_SUMMARY[name]}`).join("; ")} (init)`,
37
+ " -i, --interactive ask which parts to apply, naming the files of each, and confirm before writing (init)",
29
38
  " --relock rebuild .repokeeper/lock.json from the current files (init)",
39
+ " --to <folder> write every reusable workflow into another repository's folder instead (eject)",
30
40
  " --json machine-readable output (check)",
31
- " -y, --yes apply without asking (github apply)",
41
+ " -y, --yes apply without asking (github apply, gitlab apply)",
32
42
  " -v, --version print the version",
33
43
  ].join("\n");
34
44
  const toPosix = (path) => path.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -46,7 +56,10 @@ export async function run(argv, io) {
46
56
  accept: { type: "string", multiple: true, default: [] },
47
57
  stack: { type: "string", multiple: true, default: [] },
48
58
  platform: { type: "string" },
59
+ preset: { type: "string" },
60
+ interactive: { type: "boolean", short: "i", default: false },
49
61
  relock: { type: "boolean", default: false },
62
+ to: { type: "string" },
50
63
  json: { type: "boolean", default: false },
51
64
  yes: { type: "boolean", short: "y", default: false },
52
65
  version: { type: "boolean", short: "v", default: false },
@@ -62,18 +75,34 @@ export async function run(argv, io) {
62
75
  io.out(USAGE);
63
76
  return command === undefined && !values.help ? 2 : 0;
64
77
  }
65
- const stacks = values.stack;
66
- for (const stack of stacks) {
67
- if (!STACK_IDS.includes(stack)) {
78
+ // `go:backend` names the folder of a monorepo the stack lives in
79
+ const stackDirectories = {};
80
+ const stacks = values.stack.map((value) => {
81
+ const [stack, ...rest] = value.split(":");
82
+ if (stack === undefined || !STACK_IDS.includes(stack)) {
68
83
  throw new UsageError(`unknown stack ${stack}; expected one of ${STACK_IDS.join(", ")}`);
69
84
  }
70
- }
85
+ const directory = toPosix(rest.join(":")).replace(/\/+$/, "");
86
+ if (directory !== "" && directory !== ".")
87
+ stackDirectories[stack] = directory;
88
+ return stack;
89
+ });
71
90
  const platform = values.platform;
72
91
  if (platform !== undefined && platform !== "github" && platform !== "gitlab") {
73
92
  throw new UsageError(`unknown platform ${platform}; expected github or gitlab`);
74
93
  }
75
94
  if (platform !== undefined && command !== "init")
76
95
  throw new UsageError("--platform is only for init");
96
+ const preset = values.preset;
97
+ if (preset !== undefined && !PRESETS.includes(preset)) {
98
+ throw new UsageError(`unknown preset ${preset}; expected one of ${PRESETS.join(", ")}`);
99
+ }
100
+ if ((preset !== undefined || values.interactive) && command !== "init") {
101
+ throw new UsageError("--preset and --interactive are only for init");
102
+ }
103
+ const to = values.to;
104
+ if (to !== undefined && command !== "eject")
105
+ throw new UsageError("--to is only for eject");
77
106
  const options = {
78
107
  dryRun: values["dry-run"],
79
108
  force: values.force,
@@ -81,10 +110,14 @@ export async function run(argv, io) {
81
110
  adoptAll: values["adopt-all"],
82
111
  accept: values.accept.map(toPosix),
83
112
  stacks: stacks,
113
+ stackDirectories,
84
114
  relock: values.relock,
85
115
  json: values.json,
86
116
  yes: values.yes,
87
117
  ...(platform !== undefined ? { platform } : {}),
118
+ ...(to !== undefined ? { to } : {}),
119
+ ...(preset !== undefined ? { preset: preset } : {}),
120
+ interactive: values.interactive,
88
121
  };
89
122
  if (command === "init")
90
123
  return await initCommand(io.cwd, options, io);
@@ -92,11 +125,20 @@ export async function run(argv, io) {
92
125
  return await checkCommand(io.cwd, options, io);
93
126
  if (command === "update")
94
127
  return await updateCommand(io.cwd, options, io);
128
+ if (command === "eject")
129
+ return await ejectCommand(io.cwd, options, io);
130
+ if (command === "bump")
131
+ return await bumpCommand(io.cwd, positionals[1], options, io);
95
132
  if (command === "github") {
96
133
  if (positionals[1] !== "apply")
97
134
  throw new UsageError("usage: repokeeper github apply [--dry-run] [--yes]");
98
135
  return await githubApplyCommand(io.cwd, options, io);
99
136
  }
137
+ if (command === "gitlab") {
138
+ if (positionals[1] !== "apply")
139
+ throw new UsageError("usage: repokeeper gitlab apply [--dry-run] [--yes]");
140
+ return await gitlabApplyCommand(io.cwd, options, io);
141
+ }
100
142
  throw new UsageError(`unknown command: ${command}`);
101
143
  }
102
144
  catch (error) {
@@ -114,10 +156,11 @@ export async function run(argv, io) {
114
156
  throw error;
115
157
  }
116
158
  }
117
- async function askYesNo(question) {
159
+ async function askYesNo(question, fallback = false) {
118
160
  const rl = createInterface({ input: process.stdin, output: process.stdout });
119
161
  try {
120
- return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
162
+ const answer = (await rl.question(`${question} ${fallback ? "[Y/n]" : "[y/N]"} `)).trim();
163
+ return answer === "" ? fallback : /^y(es)?$/i.test(answer);
121
164
  }
122
165
  finally {
123
166
  rl.close();
@@ -0,0 +1,30 @@
1
+ import { join } from "node:path";
2
+ import { loadConfig } from "../config/load.js";
3
+ import { UsageError } from "../errors.js";
4
+ import { pickReleaseStack } from "../modules/release.js";
5
+ import { bumpVersion } from "../release/bump.js";
6
+ import { buildContext } from "./context.js";
7
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
8
+ /**
9
+ * `repokeeper bump <version>`: writes a release's version into the version files of the stack the
10
+ * repository releases. The GitLab release job runs it, where no release tool knows the stack's files.
11
+ */
12
+ export async function bumpCommand(root, version, options, io) {
13
+ if (version === undefined || !VERSION.test(version)) {
14
+ throw new UsageError("usage: repokeeper bump <version> [--dry-run], with a version such as 1.4.0");
15
+ }
16
+ const ctx = await buildContext(root, await loadConfig(root));
17
+ const stack = pickReleaseStack(ctx.stacks);
18
+ if (stack === undefined)
19
+ throw new UsageError("no stack to release");
20
+ const folder = stack.directory ? join(root, stack.directory) : root;
21
+ const changed = await bumpVersion(folder, stack.release, version, !options.dryRun);
22
+ for (const path of changed)
23
+ io.out(`bumped ${stack.directory ? `${stack.directory}/${path}` : path}`);
24
+ if (changed.length === 0) {
25
+ io.out(`no file of the ${stack.id} stack holds a version to change; the release is its tag`);
26
+ }
27
+ if (options.dryRun)
28
+ io.out("dry run: nothing written");
29
+ return 0;
30
+ }
@@ -1,13 +1,30 @@
1
- import { UsageError } from "../errors.js";
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CONFIG_FILE } from "../config/load.js";
4
+ import { ConfigError, UsageError } from "../errors.js";
2
5
  import { repoInfo } from "../git.js";
3
6
  import { platformFor } from "../platforms/index.js";
4
7
  import { getStackPack } from "../stacks/index.js";
8
+ import { stackDirectory } from "../stacks/support.js";
5
9
  export async function buildContext(root, config, repo) {
6
10
  const platform = platformFor(config.platform);
7
11
  const unsupported = config.stacks.find((id) => !platform.stacks.includes(id));
8
12
  if (unsupported) {
9
13
  throw new UsageError(`stack "${unsupported}" is not supported on ${platform.id} yet (supported: ${platform.stacks.join(", ")})`);
10
14
  }
11
- const stacks = await Promise.all(config.stacks.map((id) => getStackPack(id).resolve(root, config.stack_options[id] ?? {})));
15
+ const stacks = await Promise.all(config.stacks.map(async (id) => {
16
+ // `directory` is read here, for every stack alike; the rest of the options are the stack's own
17
+ const { directory: _directory, ...options } = config.stack_options[id] ?? {};
18
+ const directory = stackDirectory(id, config.stack_options[id] ?? {});
19
+ if (directory === undefined)
20
+ return getStackPack(id).resolve(root, options);
21
+ if (platform.id !== "github") {
22
+ throw new UsageError(`a stack in a folder of its own (stack_options.${id}.directory) is not supported on ${platform.id} yet`);
23
+ }
24
+ if (!existsSync(join(root, directory))) {
25
+ throw new ConfigError(`${CONFIG_FILE}: stack_options.${id}.directory: the folder ${directory} does not exist`);
26
+ }
27
+ return { ...(await getStackPack(id).resolve(join(root, directory), options)), directory };
28
+ }));
12
29
  return { config, stacks, platform, repo: repo ?? (await repoInfo(root, config.platform)) };
13
30
  }