Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac9f784602 | ||
|
|
4565a71863 | ||
|
|
3b38bc8f38 | ||
|
|
a5ab22f426 | ||
|
|
54b54fdece | ||
|
|
60d0c27db7 | ||
|
|
b6528466e0 | ||
|
|
146b1e9501 | ||
|
|
8fec37023f | ||
|
|
e9d04c7f8d | ||
|
|
ece1dffb39 | ||
|
|
71c7fe09cd | ||
|
|
45f3830794 | ||
|
|
b8d0a69d4d | ||
|
|
c5f8ee6005 | ||
|
|
9983f0b8e9 | ||
|
|
0d339e2de0 | ||
|
|
7dcb0d1b3a | ||
|
|
f8658265ae | ||
|
|
c42899c89e | ||
|
|
00fb6ef297 | ||
|
|
dc475afcd1 | ||
|
|
96d2d19046 | ||
|
|
976fc8c1a7 | ||
|
|
30029e5954 | ||
|
|
9edb042e69 | ||
|
|
198b0bb1b0 | ||
|
|
00a9ab240a | ||
|
|
53e498ca45 | ||
|
|
80cc529de7 | ||
|
|
4d2579ae1e | ||
|
|
1399862dec | ||
|
|
ba4a992474 | ||
|
|
aa4a050390 | ||
|
|
b7558a4218 | ||
|
|
f7dce637d5 | ||
|
|
250882da1f | ||
|
|
e445e49baf | ||
|
|
ef3cf30a34 | ||
|
|
86a0792b6e | ||
|
|
d1aea76dd9 | ||
|
|
cdc9e18035 | ||
|
|
374ba596ab | ||
|
|
ffeede1dca | ||
|
|
a7c17bc738 | ||
|
|
7e93c5267a | ||
|
|
fd7a2ca07d | ||
|
|
d7d6a42d48 | ||
|
|
3a5cdb5900 | ||
|
|
d8f92486c3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
lefthook.yml
|
||||||
.direnv
|
.direnv
|
||||||
result
|
result
|
||||||
template/flake.lock
|
template/flake.lock
|
||||||
1
.tools/bun/bin/moon
Symbolic link
1
.tools/bun/bin/moon
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../install/global/node_modules/@moonrepo/cli/moon.js
|
||||||
1
.tools/bun/bin/moonx
Symbolic link
1
.tools/bun/bin/moonx
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../install/global/node_modules/@moonrepo/cli/moonx.js
|
||||||
28
.tools/bun/install/global/bun.lock
Normal file
28
.tools/bun/install/global/bun.lock
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@moonrepo/cli": "^2.0.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@moonrepo/cli": ["@moonrepo/cli@2.0.4", "", { "dependencies": { "detect-libc": "^2.1.2" }, "optionalDependencies": { "@moonrepo/core-linux-arm64-gnu": "2.0.4", "@moonrepo/core-linux-arm64-musl": "2.0.4", "@moonrepo/core-linux-x64-gnu": "2.0.4", "@moonrepo/core-linux-x64-musl": "2.0.4", "@moonrepo/core-macos-arm64": "2.0.4", "@moonrepo/core-windows-x64-msvc": "2.0.4" }, "bin": { "moon": "moon.js", "moonx": "moonx.js" } }, "sha512-cP62Fa7hzToEi0I2i3Gx7zhPPdimVCeW8WqbSlE5y6fhjH38g1N77vZg4A6rcauDEUl/cpA5+vWQyxm4KCsqUA=="],
|
||||||
|
|
||||||
|
"@moonrepo/core-linux-arm64-gnu": ["@moonrepo/core-linux-arm64-gnu@2.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvMgTfYUN8ARvhOgVumR1j1XQY0V+59qz+Bi9BBmgOJsaB/QosGGWkCfdV2dChpzfE9AELnjIBgrazUGcOX9KA=="],
|
||||||
|
|
||||||
|
"@moonrepo/core-linux-arm64-musl": ["@moonrepo/core-linux-arm64-musl@2.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lF+KuN8ymTR+kynI7OU+CN//V6hSGL9eq8+7VUVFY3cRobyzjEulB7xkPgVYH2C81E8TUK2tp1R79Y8nOgEHaA=="],
|
||||||
|
|
||||||
|
"@moonrepo/core-linux-x64-gnu": ["@moonrepo/core-linux-x64-gnu@2.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-0auEy/jyMm5vjIZy/dLdrIoOJ0hnoxcEk+veBGwZatS65dSKEvXdUYrYSGWTRvykbsXDBTAaXFUtj1enGzIXmg=="],
|
||||||
|
|
||||||
|
"@moonrepo/core-linux-x64-musl": ["@moonrepo/core-linux-x64-musl@2.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h7b7uw8GdhTyZg2R7rHA04mqBIvYvcHtgtVBUXwwZQlh6jprFFtxuvehAdK32PN8sxygwRa+AdTfgq3vZHXFmw=="],
|
||||||
|
|
||||||
|
"@moonrepo/core-macos-arm64": ["@moonrepo/core-macos-arm64@2.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gz2FO0xRHUOXQjzBAz97S4pShL0YbTeNWLdCuRyjX8SNU1l82TX+l20e6OKfyt6jThWvCZeSiolx7W02xDT+iA=="],
|
||||||
|
|
||||||
|
"@moonrepo/core-windows-x64-msvc": ["@moonrepo/core-windows-x64-msvc@2.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-LjwpvjWTeusQjpqLNK7N7tVr/IQSd0W8v0L7fUIKBXNGmuONhDCHwgDqSs3yVzwcQMluiHM2qwu9YSKIEVfTIw=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
18
.tools/bun/install/global/node_modules/@moonrepo/cli/LICENSE
generated
vendored
Normal file
18
.tools/bun/install/global/node_modules/@moonrepo/cli/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 moonrepo, Inc., Miles Johnson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||||
|
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||||
|
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
104
.tools/bun/install/global/node_modules/@moonrepo/cli/README.md
generated
vendored
Normal file
104
.tools/bun/install/global/node_modules/@moonrepo/cli/README.md
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# @moonrepo/cli
|
||||||
|
|
||||||
|
The official CLI for [moon](https://moonrepo.dev), a build system and repo management tool for the
|
||||||
|
web ecosystem, written in Rust! Supports JavaScript, TypeScript, Bash, Rust, Go, and much more!
|
||||||
|
|
||||||
|
- [Documentation](https://moonrepo.dev/docs)
|
||||||
|
- [Getting started](https://moonrepo.dev/docs/install)
|
||||||
|
- [Feature comparison](https://moonrepo.dev/docs/comparison)
|
||||||
|
- [FAQ](https://moonrepo.dev/docs/faq)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
moon can be installed with bash:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -fsSL https://moonrepo.dev/install/moon.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with npm, pnpm, or yarn.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn add --dev @moonrepo/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Once installed, initialize moon in your repository.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moon init
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once [projects](https://moonrepo.dev/docs/create-project) and
|
||||||
|
[tasks](https://moonrepo.dev/docs/create-task) have been configured, tasks can be ran with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run `lint` in project `app`
|
||||||
|
moon run app:lint
|
||||||
|
|
||||||
|
# Run `lint` in all projects
|
||||||
|
moon run :lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why use moon?
|
||||||
|
|
||||||
|
Working in the JavaScript ecosystem can be very involved, especially when it comes to managing a
|
||||||
|
repository effectively. Which package manager to use? Which Node.js version to use? How to import
|
||||||
|
node modules? How to build packages? So on and so forth. moon aims to streamline this entire process
|
||||||
|
and provide a first-class developer experience.
|
||||||
|
|
||||||
|
- **Increased productivity** - With [Rust](https://www.rust-lang.org/) as our foundation, we can
|
||||||
|
ensure robust speeds, high performance, and low memory usage. Instead of long builds blocking you,
|
||||||
|
focus on your work.
|
||||||
|
- **Exceptional developer experience** - As veterans of the JavaScript ecosystem, we're well aware
|
||||||
|
of the pain points and frustrations. Our goal is to mitigate and overcome these obstacles.
|
||||||
|
- **Incremental adoption** - At its core, moon has been designed to be adopted incrementally and is
|
||||||
|
_not_ an "all at once adoption". Migrate project-by-project, or task-by-task, it's up to you!
|
||||||
|
- **Reduced scripts confusion** - `package.json` scripts can become unwieldy, very quickly. No more
|
||||||
|
duplicating the same script into every package, or reverse-engineering which root scripts to use.
|
||||||
|
With moon, all you need to know is the project name, and a task name.
|
||||||
|
- **Ensure correct versions** - Whether it's Node.js or npm, ensure the same version of each tool is
|
||||||
|
the same across _every_ developer's environment. No more wasted hours of debugging.
|
||||||
|
- **Automation built-in** - When applicable, moon will automatically install `node_modules`, or sync
|
||||||
|
package dependencies, or even sync TypeScript project references.
|
||||||
|
- And of course, the amazing list of features below!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
> Not all features are currently supported, view the documentation for an accurate list!
|
||||||
|
|
||||||
|
#### Management
|
||||||
|
|
||||||
|
- **Smart hashing** - Collects inputs from multiple sources to ensure builds are deterministic and
|
||||||
|
reproducible.
|
||||||
|
- **Remote caching** - Persists builds, hashes, and caches between teammates and CI/CD environments.
|
||||||
|
- **Integrated toolchain** - Automatically downloads and installs explicit versions of Node.js and
|
||||||
|
other tools for consistency across the entire workspace or per project.
|
||||||
|
- **Multi-platform** - Runs on common development platforms: Linux, macOS, and Windows.
|
||||||
|
|
||||||
|
#### Organization
|
||||||
|
|
||||||
|
- **Project graph** - Generates a project graph for dependency and dependent relationships.
|
||||||
|
- **Code generation** - Easily scaffold new applications, libraries, tooling, and more!
|
||||||
|
- **Dependency workspaces** - Works alongside package manager workspaces so that projects have
|
||||||
|
distinct dependency trees.
|
||||||
|
- **Code ownership** - Declare owners, maintainers, support channels, and more. Generate CODEOWNERS.
|
||||||
|
|
||||||
|
#### Orchestration
|
||||||
|
|
||||||
|
- **Dependency graph** - Generates a dependency graph to increase performance and reduce workloads.
|
||||||
|
- **Action pipeline** - Executes actions in parallel and in order using a thread pool and our
|
||||||
|
dependency graph.
|
||||||
|
- **Action distribution** - Distributes actions across multiple machines to increase throughput.
|
||||||
|
- **Incremental builds** - With our smart hashing, only rebuild projects that have been changed
|
||||||
|
since the last build.
|
||||||
|
|
||||||
|
#### Notification
|
||||||
|
|
||||||
|
- **Flakiness detection** - Reduce flaky builds with automatic retries and passthrough settings.
|
||||||
|
- **Webhook events** - Receive a webhook for every event in the pipeline. Useful for metrics
|
||||||
|
gathering and insights.
|
||||||
|
- **Terminal notifications** - Receives notifications in your chosen terminal when builds are
|
||||||
|
successful... or are not.
|
||||||
|
- **Git hooks** - Manage Git hooks to enforce workflows and requirements for contributors.
|
||||||
15
.tools/bun/install/global/node_modules/@moonrepo/cli/moon.js
generated
vendored
Executable file
15
.tools/bun/install/global/node_modules/@moonrepo/cli/moon.js
generated
vendored
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const cp = require('child_process');
|
||||||
|
const { findMoonExe } = require('./utils');
|
||||||
|
|
||||||
|
const result = cp.spawnSync(findMoonExe(), process.argv.slice(2), {
|
||||||
|
shell: false,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exitCode = result.status;
|
||||||
15
.tools/bun/install/global/node_modules/@moonrepo/cli/moonx.js
generated
vendored
Executable file
15
.tools/bun/install/global/node_modules/@moonrepo/cli/moonx.js
generated
vendored
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const cp = require('child_process');
|
||||||
|
const { findMoonxExe } = require('./utils');
|
||||||
|
|
||||||
|
const result = cp.spawnSync(findMoonxExe(), process.argv.slice(2), {
|
||||||
|
shell: false,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exitCode = result.status;
|
||||||
39
.tools/bun/install/global/node_modules/@moonrepo/cli/package.json
generated
vendored
Normal file
39
.tools/bun/install/global/node_modules/@moonrepo/cli/package.json
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@moonrepo/cli",
|
||||||
|
"version": "2.0.4",
|
||||||
|
"type": "commonjs",
|
||||||
|
"description": "moon command line and core system.",
|
||||||
|
"keywords": [
|
||||||
|
"moon",
|
||||||
|
"repo",
|
||||||
|
"cli",
|
||||||
|
"core"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"moon.js",
|
||||||
|
"moonx.js",
|
||||||
|
"utils.js"
|
||||||
|
],
|
||||||
|
"author": "Miles Johnson",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"moon": "moon.js",
|
||||||
|
"moonx": "moonx.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/moonrepo/moon",
|
||||||
|
"directory": "packages/cli"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.1.2"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@moonrepo/core-linux-arm64-gnu": "2.0.4",
|
||||||
|
"@moonrepo/core-linux-arm64-musl": "2.0.4",
|
||||||
|
"@moonrepo/core-linux-x64-gnu": "2.0.4",
|
||||||
|
"@moonrepo/core-linux-x64-musl": "2.0.4",
|
||||||
|
"@moonrepo/core-macos-arm64": "2.0.4",
|
||||||
|
"@moonrepo/core-windows-x64-msvc": "2.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
51
.tools/bun/install/global/node_modules/@moonrepo/cli/utils.js
generated
vendored
Normal file
51
.tools/bun/install/global/node_modules/@moonrepo/cli/utils.js
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const isLinux = process.platform === 'linux';
|
||||||
|
const isMacos = process.platform === 'darwin';
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
const platform = isWindows ? 'windows' : isMacos ? 'macos' : process.platform;
|
||||||
|
const arch =
|
||||||
|
process.env['npm_config_user_agent'] && process.env['npm_config_user_agent'].match(/^bun.*arm64$/)
|
||||||
|
? 'arm64'
|
||||||
|
: process.arch; // https://github.com/moonrepo/moon/issues/1103
|
||||||
|
const parts = [platform, arch];
|
||||||
|
|
||||||
|
if (isLinux) {
|
||||||
|
const { familySync } = require('detect-libc');
|
||||||
|
|
||||||
|
if (familySync() === 'musl') {
|
||||||
|
parts.push('musl');
|
||||||
|
// } else if (process.arch === 'arm') {
|
||||||
|
// parts.push('gnueabihf');
|
||||||
|
} else {
|
||||||
|
parts.push('gnu');
|
||||||
|
}
|
||||||
|
} else if (isWindows) {
|
||||||
|
parts.push('msvc');
|
||||||
|
}
|
||||||
|
|
||||||
|
const triple = parts.join('-');
|
||||||
|
|
||||||
|
function findExe(name) {
|
||||||
|
const pkgPath = require.resolve(`@moonrepo/core-${triple}/package.json`);
|
||||||
|
const exePath = path.join(path.dirname(pkgPath), isWindows ? `${name}.exe` : name);
|
||||||
|
|
||||||
|
if (fs.existsSync(exePath)) {
|
||||||
|
return exePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`moon executable "${exePath}" not found!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMoonExe() {
|
||||||
|
return findExe('moon');
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMoonxExe() {
|
||||||
|
return findExe('moonx');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.findMoonExe = findMoonExe;
|
||||||
|
exports.findMoonxExe = findMoonxExe;
|
||||||
3
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/README.md
generated
vendored
Normal file
3
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/README.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# @moonrepo/core-macos-arm64
|
||||||
|
|
||||||
|
This is the `aarch64-apple-darwin` binary for `@moonrepo/cli`.
|
||||||
BIN
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/moon
generated
vendored
Executable file
BIN
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/moon
generated
vendored
Executable file
Binary file not shown.
BIN
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/moonx
generated
vendored
Executable file
BIN
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/moonx
generated
vendored
Executable file
Binary file not shown.
32
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/package.json
generated
vendored
Normal file
32
.tools/bun/install/global/node_modules/@moonrepo/core-macos-arm64/package.json
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@moonrepo/core-macos-arm64",
|
||||||
|
"version": "2.0.4",
|
||||||
|
"description": "macOS ARM64 (Silicon) binary for moon.",
|
||||||
|
"keywords": [
|
||||||
|
"moon",
|
||||||
|
"repo",
|
||||||
|
"macos",
|
||||||
|
"darwin",
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"author": "Miles Johnson",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/moonrepo/moon",
|
||||||
|
"directory": "packages/core-macos-arm64"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"executableFiles": [
|
||||||
|
"moon",
|
||||||
|
"moonx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
201
.tools/bun/install/global/node_modules/detect-libc/LICENSE
generated
vendored
Normal file
201
.tools/bun/install/global/node_modules/detect-libc/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
163
.tools/bun/install/global/node_modules/detect-libc/README.md
generated
vendored
Normal file
163
.tools/bun/install/global/node_modules/detect-libc/README.md
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# detect-libc
|
||||||
|
|
||||||
|
Node.js module to detect details of the C standard library (libc)
|
||||||
|
implementation provided by a given Linux system.
|
||||||
|
|
||||||
|
Currently supports detection of GNU glibc and MUSL libc.
|
||||||
|
|
||||||
|
Provides asychronous and synchronous functions for the
|
||||||
|
family (e.g. `glibc`, `musl`) and version (e.g. `1.23`, `1.2.3`).
|
||||||
|
|
||||||
|
The version numbers of libc implementations
|
||||||
|
are not guaranteed to be semver-compliant.
|
||||||
|
|
||||||
|
For previous v1.x releases, please see the
|
||||||
|
[v1](https://github.com/lovell/detect-libc/tree/v1) branch.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install detect-libc
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### GLIBC
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const GLIBC: string = 'glibc';
|
||||||
|
```
|
||||||
|
|
||||||
|
A String constant containing the value `glibc`.
|
||||||
|
|
||||||
|
### MUSL
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const MUSL: string = 'musl';
|
||||||
|
```
|
||||||
|
|
||||||
|
A String constant containing the value `musl`.
|
||||||
|
|
||||||
|
### family
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function family(): Promise<string | null>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolves asychronously with:
|
||||||
|
|
||||||
|
* `glibc` or `musl` when the libc family can be determined
|
||||||
|
* `null` when the libc family cannot be determined
|
||||||
|
* `null` when run on a non-Linux platform
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { family, GLIBC, MUSL } = require('detect-libc');
|
||||||
|
|
||||||
|
switch (await family()) {
|
||||||
|
case GLIBC: ...
|
||||||
|
case MUSL: ...
|
||||||
|
case null: ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### familySync
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function familySync(): string | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Synchronous version of `family()`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { familySync, GLIBC, MUSL } = require('detect-libc');
|
||||||
|
|
||||||
|
switch (familySync()) {
|
||||||
|
case GLIBC: ...
|
||||||
|
case MUSL: ...
|
||||||
|
case null: ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### version
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function version(): Promise<string | null>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolves asychronously with:
|
||||||
|
|
||||||
|
* The version when it can be determined
|
||||||
|
* `null` when the libc family cannot be determined
|
||||||
|
* `null` when run on a non-Linux platform
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { version } = require('detect-libc');
|
||||||
|
|
||||||
|
const v = await version();
|
||||||
|
if (v) {
|
||||||
|
const [major, minor, patch] = v.split('.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### versionSync
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function versionSync(): string | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Synchronous version of `version()`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { versionSync } = require('detect-libc');
|
||||||
|
|
||||||
|
const v = versionSync();
|
||||||
|
if (v) {
|
||||||
|
const [major, minor, patch] = v.split('.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### isNonGlibcLinux
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function isNonGlibcLinux(): Promise<boolean>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolves asychronously with:
|
||||||
|
|
||||||
|
* `false` when the libc family is `glibc`
|
||||||
|
* `true` when the libc family is not `glibc`
|
||||||
|
* `false` when run on a non-Linux platform
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { isNonGlibcLinux } = require('detect-libc');
|
||||||
|
|
||||||
|
if (await isNonGlibcLinux()) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### isNonGlibcLinuxSync
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function isNonGlibcLinuxSync(): boolean;
|
||||||
|
```
|
||||||
|
|
||||||
|
Synchronous version of `isNonGlibcLinux()`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { isNonGlibcLinuxSync } = require('detect-libc');
|
||||||
|
|
||||||
|
if (isNonGlibcLinuxSync()) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
Copyright 2017 Lovell Fuller and others.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
14
.tools/bun/install/global/node_modules/detect-libc/index.d.ts
generated
vendored
Normal file
14
.tools/bun/install/global/node_modules/detect-libc/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2017 Lovell Fuller and others.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
export const GLIBC: 'glibc';
|
||||||
|
export const MUSL: 'musl';
|
||||||
|
|
||||||
|
export function family(): Promise<string | null>;
|
||||||
|
export function familySync(): string | null;
|
||||||
|
|
||||||
|
export function isNonGlibcLinux(): Promise<boolean>;
|
||||||
|
export function isNonGlibcLinuxSync(): boolean;
|
||||||
|
|
||||||
|
export function version(): Promise<string | null>;
|
||||||
|
export function versionSync(): string | null;
|
||||||
313
.tools/bun/install/global/node_modules/detect-libc/lib/detect-libc.js
generated
vendored
Normal file
313
.tools/bun/install/global/node_modules/detect-libc/lib/detect-libc.js
generated
vendored
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
// Copyright 2017 Lovell Fuller and others.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const childProcess = require('child_process');
|
||||||
|
const { isLinux, getReport } = require('./process');
|
||||||
|
const { LDD_PATH, SELF_PATH, readFile, readFileSync } = require('./filesystem');
|
||||||
|
const { interpreterPath } = require('./elf');
|
||||||
|
|
||||||
|
let cachedFamilyInterpreter;
|
||||||
|
let cachedFamilyFilesystem;
|
||||||
|
let cachedVersionFilesystem;
|
||||||
|
|
||||||
|
const command = 'getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true';
|
||||||
|
let commandOut = '';
|
||||||
|
|
||||||
|
const safeCommand = () => {
|
||||||
|
if (!commandOut) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
childProcess.exec(command, (err, out) => {
|
||||||
|
commandOut = err ? ' ' : out;
|
||||||
|
resolve(commandOut);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return commandOut;
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeCommandSync = () => {
|
||||||
|
if (!commandOut) {
|
||||||
|
try {
|
||||||
|
commandOut = childProcess.execSync(command, { encoding: 'utf8' });
|
||||||
|
} catch (_err) {
|
||||||
|
commandOut = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commandOut;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A String constant containing the value `glibc`.
|
||||||
|
* @type {string}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
const GLIBC = 'glibc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Regexp constant to get the GLIBC Version.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const RE_GLIBC_VERSION = /LIBC[a-z0-9 \-).]*?(\d+\.\d+)/i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A String constant containing the value `musl`.
|
||||||
|
* @type {string}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
const MUSL = 'musl';
|
||||||
|
|
||||||
|
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-');
|
||||||
|
|
||||||
|
const familyFromReport = () => {
|
||||||
|
const report = getReport();
|
||||||
|
if (report.header && report.header.glibcVersionRuntime) {
|
||||||
|
return GLIBC;
|
||||||
|
}
|
||||||
|
if (Array.isArray(report.sharedObjects)) {
|
||||||
|
if (report.sharedObjects.some(isFileMusl)) {
|
||||||
|
return MUSL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const familyFromCommand = (out) => {
|
||||||
|
const [getconf, ldd1] = out.split(/[\r\n]+/);
|
||||||
|
if (getconf && getconf.includes(GLIBC)) {
|
||||||
|
return GLIBC;
|
||||||
|
}
|
||||||
|
if (ldd1 && ldd1.includes(MUSL)) {
|
||||||
|
return MUSL;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const familyFromInterpreterPath = (path) => {
|
||||||
|
if (path) {
|
||||||
|
if (path.includes('/ld-musl-')) {
|
||||||
|
return MUSL;
|
||||||
|
} else if (path.includes('/ld-linux-')) {
|
||||||
|
return GLIBC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFamilyFromLddContent = (content) => {
|
||||||
|
content = content.toString();
|
||||||
|
if (content.includes('musl')) {
|
||||||
|
return MUSL;
|
||||||
|
}
|
||||||
|
if (content.includes('GNU C Library')) {
|
||||||
|
return GLIBC;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const familyFromFilesystem = async () => {
|
||||||
|
if (cachedFamilyFilesystem !== undefined) {
|
||||||
|
return cachedFamilyFilesystem;
|
||||||
|
}
|
||||||
|
cachedFamilyFilesystem = null;
|
||||||
|
try {
|
||||||
|
const lddContent = await readFile(LDD_PATH);
|
||||||
|
cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
|
||||||
|
} catch (e) {}
|
||||||
|
return cachedFamilyFilesystem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const familyFromFilesystemSync = () => {
|
||||||
|
if (cachedFamilyFilesystem !== undefined) {
|
||||||
|
return cachedFamilyFilesystem;
|
||||||
|
}
|
||||||
|
cachedFamilyFilesystem = null;
|
||||||
|
try {
|
||||||
|
const lddContent = readFileSync(LDD_PATH);
|
||||||
|
cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
|
||||||
|
} catch (e) {}
|
||||||
|
return cachedFamilyFilesystem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const familyFromInterpreter = async () => {
|
||||||
|
if (cachedFamilyInterpreter !== undefined) {
|
||||||
|
return cachedFamilyInterpreter;
|
||||||
|
}
|
||||||
|
cachedFamilyInterpreter = null;
|
||||||
|
try {
|
||||||
|
const selfContent = await readFile(SELF_PATH);
|
||||||
|
const path = interpreterPath(selfContent);
|
||||||
|
cachedFamilyInterpreter = familyFromInterpreterPath(path);
|
||||||
|
} catch (e) {}
|
||||||
|
return cachedFamilyInterpreter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const familyFromInterpreterSync = () => {
|
||||||
|
if (cachedFamilyInterpreter !== undefined) {
|
||||||
|
return cachedFamilyInterpreter;
|
||||||
|
}
|
||||||
|
cachedFamilyInterpreter = null;
|
||||||
|
try {
|
||||||
|
const selfContent = readFileSync(SELF_PATH);
|
||||||
|
const path = interpreterPath(selfContent);
|
||||||
|
cachedFamilyInterpreter = familyFromInterpreterPath(path);
|
||||||
|
} catch (e) {}
|
||||||
|
return cachedFamilyInterpreter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves with the libc family when it can be determined, `null` otherwise.
|
||||||
|
* @returns {Promise<?string>}
|
||||||
|
*/
|
||||||
|
const family = async () => {
|
||||||
|
let family = null;
|
||||||
|
if (isLinux()) {
|
||||||
|
family = await familyFromInterpreter();
|
||||||
|
if (!family) {
|
||||||
|
family = await familyFromFilesystem();
|
||||||
|
if (!family) {
|
||||||
|
family = familyFromReport();
|
||||||
|
}
|
||||||
|
if (!family) {
|
||||||
|
const out = await safeCommand();
|
||||||
|
family = familyFromCommand(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return family;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the libc family when it can be determined, `null` otherwise.
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
|
const familySync = () => {
|
||||||
|
let family = null;
|
||||||
|
if (isLinux()) {
|
||||||
|
family = familyFromInterpreterSync();
|
||||||
|
if (!family) {
|
||||||
|
family = familyFromFilesystemSync();
|
||||||
|
if (!family) {
|
||||||
|
family = familyFromReport();
|
||||||
|
}
|
||||||
|
if (!family) {
|
||||||
|
const out = safeCommandSync();
|
||||||
|
family = familyFromCommand(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return family;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves `true` only when the platform is Linux and the libc family is not `glibc`.
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
const isNonGlibcLinux = async () => isLinux() && await family() !== GLIBC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` only when the platform is Linux and the libc family is not `glibc`.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isNonGlibcLinuxSync = () => isLinux() && familySync() !== GLIBC;
|
||||||
|
|
||||||
|
const versionFromFilesystem = async () => {
|
||||||
|
if (cachedVersionFilesystem !== undefined) {
|
||||||
|
return cachedVersionFilesystem;
|
||||||
|
}
|
||||||
|
cachedVersionFilesystem = null;
|
||||||
|
try {
|
||||||
|
const lddContent = await readFile(LDD_PATH);
|
||||||
|
const versionMatch = lddContent.match(RE_GLIBC_VERSION);
|
||||||
|
if (versionMatch) {
|
||||||
|
cachedVersionFilesystem = versionMatch[1];
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return cachedVersionFilesystem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionFromFilesystemSync = () => {
|
||||||
|
if (cachedVersionFilesystem !== undefined) {
|
||||||
|
return cachedVersionFilesystem;
|
||||||
|
}
|
||||||
|
cachedVersionFilesystem = null;
|
||||||
|
try {
|
||||||
|
const lddContent = readFileSync(LDD_PATH);
|
||||||
|
const versionMatch = lddContent.match(RE_GLIBC_VERSION);
|
||||||
|
if (versionMatch) {
|
||||||
|
cachedVersionFilesystem = versionMatch[1];
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return cachedVersionFilesystem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionFromReport = () => {
|
||||||
|
const report = getReport();
|
||||||
|
if (report.header && report.header.glibcVersionRuntime) {
|
||||||
|
return report.header.glibcVersionRuntime;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionSuffix = (s) => s.trim().split(/\s+/)[1];
|
||||||
|
|
||||||
|
const versionFromCommand = (out) => {
|
||||||
|
const [getconf, ldd1, ldd2] = out.split(/[\r\n]+/);
|
||||||
|
if (getconf && getconf.includes(GLIBC)) {
|
||||||
|
return versionSuffix(getconf);
|
||||||
|
}
|
||||||
|
if (ldd1 && ldd2 && ldd1.includes(MUSL)) {
|
||||||
|
return versionSuffix(ldd2);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves with the libc version when it can be determined, `null` otherwise.
|
||||||
|
* @returns {Promise<?string>}
|
||||||
|
*/
|
||||||
|
const version = async () => {
|
||||||
|
let version = null;
|
||||||
|
if (isLinux()) {
|
||||||
|
version = await versionFromFilesystem();
|
||||||
|
if (!version) {
|
||||||
|
version = versionFromReport();
|
||||||
|
}
|
||||||
|
if (!version) {
|
||||||
|
const out = await safeCommand();
|
||||||
|
version = versionFromCommand(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the libc version when it can be determined, `null` otherwise.
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
|
const versionSync = () => {
|
||||||
|
let version = null;
|
||||||
|
if (isLinux()) {
|
||||||
|
version = versionFromFilesystemSync();
|
||||||
|
if (!version) {
|
||||||
|
version = versionFromReport();
|
||||||
|
}
|
||||||
|
if (!version) {
|
||||||
|
const out = safeCommandSync();
|
||||||
|
version = versionFromCommand(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GLIBC,
|
||||||
|
MUSL,
|
||||||
|
family,
|
||||||
|
familySync,
|
||||||
|
isNonGlibcLinux,
|
||||||
|
isNonGlibcLinuxSync,
|
||||||
|
version,
|
||||||
|
versionSync
|
||||||
|
};
|
||||||
39
.tools/bun/install/global/node_modules/detect-libc/lib/elf.js
generated
vendored
Normal file
39
.tools/bun/install/global/node_modules/detect-libc/lib/elf.js
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2017 Lovell Fuller and others.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const interpreterPath = (elf) => {
|
||||||
|
if (elf.length < 64) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (elf.readUInt32BE(0) !== 0x7F454C46) {
|
||||||
|
// Unexpected magic bytes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (elf.readUInt8(4) !== 2) {
|
||||||
|
// Not a 64-bit ELF
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (elf.readUInt8(5) !== 1) {
|
||||||
|
// Not little-endian
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const offset = elf.readUInt32LE(32);
|
||||||
|
const size = elf.readUInt16LE(54);
|
||||||
|
const count = elf.readUInt16LE(56);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const headerOffset = offset + (i * size);
|
||||||
|
const type = elf.readUInt32LE(headerOffset);
|
||||||
|
if (type === 3) {
|
||||||
|
const fileOffset = elf.readUInt32LE(headerOffset + 8);
|
||||||
|
const fileSize = elf.readUInt32LE(headerOffset + 32);
|
||||||
|
return elf.subarray(fileOffset, fileOffset + fileSize).toString().replace(/\0.*$/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
interpreterPath
|
||||||
|
};
|
||||||
51
.tools/bun/install/global/node_modules/detect-libc/lib/filesystem.js
generated
vendored
Normal file
51
.tools/bun/install/global/node_modules/detect-libc/lib/filesystem.js
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2017 Lovell Fuller and others.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const LDD_PATH = '/usr/bin/ldd';
|
||||||
|
const SELF_PATH = '/proc/self/exe';
|
||||||
|
const MAX_LENGTH = 2048;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the content of a file synchronous
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
const readFileSync = (path) => {
|
||||||
|
const fd = fs.openSync(path, 'r');
|
||||||
|
const buffer = Buffer.alloc(MAX_LENGTH);
|
||||||
|
const bytesRead = fs.readSync(fd, buffer, 0, MAX_LENGTH, 0);
|
||||||
|
fs.close(fd, () => {});
|
||||||
|
return buffer.subarray(0, bytesRead);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the content of a file
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
const readFile = (path) => new Promise((resolve, reject) => {
|
||||||
|
fs.open(path, 'r', (err, fd) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
const buffer = Buffer.alloc(MAX_LENGTH);
|
||||||
|
fs.read(fd, buffer, 0, MAX_LENGTH, 0, (_, bytesRead) => {
|
||||||
|
resolve(buffer.subarray(0, bytesRead));
|
||||||
|
fs.close(fd, () => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LDD_PATH,
|
||||||
|
SELF_PATH,
|
||||||
|
readFileSync,
|
||||||
|
readFile
|
||||||
|
};
|
||||||
24
.tools/bun/install/global/node_modules/detect-libc/lib/process.js
generated
vendored
Normal file
24
.tools/bun/install/global/node_modules/detect-libc/lib/process.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2017 Lovell Fuller and others.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const isLinux = () => process.platform === 'linux';
|
||||||
|
|
||||||
|
let report = null;
|
||||||
|
const getReport = () => {
|
||||||
|
if (!report) {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (isLinux() && process.report) {
|
||||||
|
const orig = process.report.excludeNetwork;
|
||||||
|
process.report.excludeNetwork = true;
|
||||||
|
report = process.report.getReport();
|
||||||
|
process.report.excludeNetwork = orig;
|
||||||
|
} else {
|
||||||
|
report = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return report;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { isLinux, getReport };
|
||||||
44
.tools/bun/install/global/node_modules/detect-libc/package.json
generated
vendored
Normal file
44
.tools/bun/install/global/node_modules/detect-libc/package.json
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "detect-libc",
|
||||||
|
"version": "2.1.2",
|
||||||
|
"description": "Node.js module to detect the C standard library (libc) implementation family and version",
|
||||||
|
"main": "lib/detect-libc.js",
|
||||||
|
"files": [
|
||||||
|
"lib/",
|
||||||
|
"index.d.ts"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "semistandard && nyc --reporter=text --check-coverage --branches=100 ava test/unit.js",
|
||||||
|
"changelog": "conventional-changelog -i CHANGELOG.md -s",
|
||||||
|
"bench": "node benchmark/detect-libc",
|
||||||
|
"bench:calls": "node benchmark/call-familySync.js && sleep 1 && node benchmark/call-isNonGlibcLinuxSync.js && sleep 1 && node benchmark/call-versionSync.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/lovell/detect-libc.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"libc",
|
||||||
|
"glibc",
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"author": "Lovell Fuller <npm@lovell.info>",
|
||||||
|
"contributors": [
|
||||||
|
"Niklas Salmoukas <niklas@salmoukas.com>",
|
||||||
|
"Vinícius Lourenço <vinyygamerlol@gmail.com>"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"ava": "^2.4.0",
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"conventional-changelog-cli": "^5.0.0",
|
||||||
|
"eslint-config-standard": "^13.0.1",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
|
"proxyquire": "^2.1.3",
|
||||||
|
"semistandard": "^14.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"types": "index.d.ts"
|
||||||
|
}
|
||||||
5
.tools/bun/install/global/package.json
Normal file
5
.tools/bun/install/global/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@moonrepo/cli": "^2.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
201
README.md
Normal file
201
README.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# repo-lib
|
||||||
|
|
||||||
|
`repo-lib` is a pure-first Nix flake library for repo-level developer workflows:
|
||||||
|
|
||||||
|
- `mkRepo` for `devShells`, `checks`, `formatter`, and optional `packages.release`
|
||||||
|
- structured tool banners driven from package-backed tool specs
|
||||||
|
- structured release steps (`writeFile`, `replace`, `versionMetaSet`, `versionMetaUnset`)
|
||||||
|
- a Bun-only Moonrepo + TypeScript + Varlock template in [`template/`](/Users/eric/Projects/repo-lib/template)
|
||||||
|
|
||||||
|
Audit and replacement review: [`docs/reviews/2026-03-21-repo-lib-audit.md`](/Users/eric/Projects/repo-lib/docs/reviews/2026-03-21-repo-lib-audit.md)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Nix](https://nixos.org/download/) with flakes enabled
|
||||||
|
- [`direnv`](https://direnv.net/) (recommended)
|
||||||
|
|
||||||
|
## Use the template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.6.1#default' --refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated repo includes:
|
||||||
|
|
||||||
|
- a `repo-lib`-managed Nix flake
|
||||||
|
- Bun as the only JS runtime and package manager
|
||||||
|
- Moonrepo root tasks
|
||||||
|
- shared TypeScript configs adapted from `../moon`
|
||||||
|
- Varlock with a committed `.env.schema`
|
||||||
|
- empty `apps/` and `packages/` directories for new projects
|
||||||
|
|
||||||
|
## Use the library
|
||||||
|
|
||||||
|
Add this flake input:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.6.1";
|
||||||
|
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
```
|
||||||
|
|
||||||
|
Build your repo outputs from `mkRepo`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
outputs = { self, nixpkgs, repo-lib, ... }:
|
||||||
|
repo-lib.lib.mkRepo {
|
||||||
|
inherit self nixpkgs;
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
config = {
|
||||||
|
checks.tests = {
|
||||||
|
command = "echo 'No tests defined yet.'";
|
||||||
|
stage = "pre-push";
|
||||||
|
passFilenames = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
release = {
|
||||||
|
steps = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
perSystem = { pkgs, system, ... }: {
|
||||||
|
tools = [
|
||||||
|
(repo-lib.lib.tools.fromCommand {
|
||||||
|
name = "Nix";
|
||||||
|
version.args = [ "--version" ];
|
||||||
|
command = "nix";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
shell.packages = [
|
||||||
|
self.packages.${system}.release
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`mkRepo` generates:
|
||||||
|
|
||||||
|
- `devShells.${system}.default`
|
||||||
|
- `checks.${system}.hook-check`
|
||||||
|
- `checks.${system}.lefthook-check`
|
||||||
|
- `formatter.${system}`
|
||||||
|
- `packages.${system}.release` when `config.release != null`
|
||||||
|
- merged `packages` and `apps` from `perSystem`
|
||||||
|
|
||||||
|
Checks are installed through `lefthook`, with `pre-commit` and `pre-push` commands configured to run in parallel.
|
||||||
|
repo-lib also sets Lefthook `output = [ "failure" "summary" ]` by default.
|
||||||
|
|
||||||
|
For advanced Lefthook features, use raw `config.lefthook` or `perSystem.lefthook`. Those attrsets are merged after generated checks, so you can augment a generated command with fields that the simple `checks` abstraction does not carry, such as `stage_fixed`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.lefthook.pre-push.commands.tests.stage_fixed = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool banners
|
||||||
|
|
||||||
|
Tools are declared once. Package-backed tools are added to the shell automatically, and both package-backed and command-backed tools are rendered in the startup banner.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
(repo-lib.lib.tools.fromPackage {
|
||||||
|
name = "Go";
|
||||||
|
package = pkgs.go;
|
||||||
|
version.args = [ "version" ];
|
||||||
|
banner.color = "CYAN";
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Required tools fail shell startup if their version probe fails. This keeps banner output honest instead of silently hiding misconfiguration.
|
||||||
|
|
||||||
|
When a tool should come from the host environment instead of `nixpkgs`, use `fromCommand`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
(repo-lib.lib.tools.fromCommand {
|
||||||
|
name = "Nix";
|
||||||
|
command = "nix";
|
||||||
|
version.args = [ "--version" ];
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Purity model
|
||||||
|
|
||||||
|
The default path is pure: declare tools and packages in Nix, then let `mkRepo` assemble the shell.
|
||||||
|
|
||||||
|
Impure bootstrap work is still possible, but it must be explicit:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.shell = {
|
||||||
|
bootstrap = ''
|
||||||
|
export GOBIN="$PWD/.tools/bin"
|
||||||
|
export PATH="$GOBIN:$PATH"
|
||||||
|
'';
|
||||||
|
allowImpureBootstrap = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release steps
|
||||||
|
|
||||||
|
Structured release steps are preferred over raw `sed` snippets:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.release = {
|
||||||
|
steps = [
|
||||||
|
{
|
||||||
|
writeFile = {
|
||||||
|
path = "src/version.ts";
|
||||||
|
text = ''
|
||||||
|
export const APP_VERSION = "$FULL_VERSION" as const;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
replace = {
|
||||||
|
path = "README.md";
|
||||||
|
regex = ''^(version = ")[^"]*(")$'';
|
||||||
|
replacement = ''\1$FULL_VERSION\2'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
versionMetaSet = {
|
||||||
|
key = "desktop_binary_version_max";
|
||||||
|
value = "$FULL_VERSION";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated `release` command still supports:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
release
|
||||||
|
release select
|
||||||
|
release --dry-run patch
|
||||||
|
release patch
|
||||||
|
release patch --commit
|
||||||
|
release patch --commit --tag
|
||||||
|
release patch --commit --tag --push
|
||||||
|
release beta
|
||||||
|
release minor beta
|
||||||
|
release stable
|
||||||
|
release set 1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, `release` updates repo files, runs structured release steps, executes `postVersion`, and runs `nix fmt`, but it does not commit, tag, or push unless you opt in with flags.
|
||||||
|
|
||||||
|
- `--commit` stages all changes and creates `chore(release): <tag>`
|
||||||
|
- `--tag` creates the Git tag after commit
|
||||||
|
- `--push` pushes the current branch and, when tagging is enabled, pushes tags too
|
||||||
|
- `--dry-run` resolves and prints the plan without mutating the repo
|
||||||
|
|
||||||
|
When `release` runs with no args in an interactive terminal, it opens a Bubble Tea picker so you can preview the exact command, flags, and resolved next version before executing it. Use `release select` to force that picker explicitly.
|
||||||
|
|
||||||
|
## Low-level APIs
|
||||||
|
|
||||||
|
`mkRelease` remains available for repos that want lower-level control over release automation.
|
||||||
|
|
||||||
|
## Common command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix fmt
|
||||||
|
```
|
||||||
292
docs/reviews/2026-03-21-repo-lib-audit.md
Normal file
292
docs/reviews/2026-03-21-repo-lib-audit.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
# Repo-Lib Audit
|
||||||
|
|
||||||
|
Date: 2026-03-21
|
||||||
|
|
||||||
|
## Direct Answers
|
||||||
|
|
||||||
|
### 1. Does it work today?
|
||||||
|
|
||||||
|
Partially.
|
||||||
|
|
||||||
|
- `nix flake show --all-systems` succeeds from the repo root.
|
||||||
|
- Before this audit, `nix flake check` failed because the old shell-based release test harness relied on a brittle regex over `nix derivation show` internals instead of asserting against stable JSON structure.
|
||||||
|
- `mkRepo`, the template flake, the dev shell, and the formatter/check outputs all evaluate. The primary failure was test-harness fragility, not a clear functional break in the library itself.
|
||||||
|
- The release path still carries real operational risk because [`packages/release/release.sh`](../../../packages/release/release.sh) combines mutation, formatting, commit, tag, and push in one command and uses destructive rollback.
|
||||||
|
|
||||||
|
### 2. Is the code organization maintainable?
|
||||||
|
|
||||||
|
Not in its current shape.
|
||||||
|
|
||||||
|
- [`packages/repo-lib/lib.nix`](../../../packages/repo-lib/lib.nix) is a single 879-line module that owns unrelated concerns:
|
||||||
|
- tool schema normalization
|
||||||
|
- hook/check config synthesis
|
||||||
|
- shell banner generation
|
||||||
|
- release step normalization
|
||||||
|
- public API assembly
|
||||||
|
- [`packages/repo-lib/shell-hook.sh`](../../../packages/repo-lib/shell-hook.sh) is not just presentation. It contains tool probing, parsing, error handling, and shell-failure behavior.
|
||||||
|
- `mkDevShell` and `mkRepo` overlap conceptually and preserve legacy paths that make the main implementation harder to reason about.
|
||||||
|
|
||||||
|
### 3. Is the public API readable and usable for consumers?
|
||||||
|
|
||||||
|
Usable, but underspecified and harder to learn than the README suggests.
|
||||||
|
|
||||||
|
- [`README.md`](../../../README.md) presents `mkRepo` as a compact abstraction, but the real behavior is distributed across `lib.nix`, `shell-hook.sh`, generated Lefthook config, and the release script.
|
||||||
|
- The boundaries between `config`, `perSystem`, `checks`, `lefthook`, `shell`, `tools`, and `release` are not obvious from the docs alone.
|
||||||
|
- Raw `config.lefthook` / `perSystem.lefthook` passthrough is effectively required for advanced use, which means the higher-level `checks` abstraction is incomplete.
|
||||||
|
|
||||||
|
### 4. Which parts should be kept, split, or replaced?
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
|
||||||
|
- the high-level `mkRepo` consumer entrypoint, if compatibility matters
|
||||||
|
- the template
|
||||||
|
- the structured release-step idea
|
||||||
|
|
||||||
|
Split:
|
||||||
|
|
||||||
|
- release tooling from repo shell/hook wiring
|
||||||
|
- shell banner rendering from shell/package assembly
|
||||||
|
- hook/check generation from the rest of `mkRepo`
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
- custom flake output composition with `flake-parts`
|
||||||
|
- custom hook glue with `lefthook.nix`
|
||||||
|
- keep `treefmt-nix` as the formatting layer rather than wrapping it deeper
|
||||||
|
|
||||||
|
### 5. What is the lowest-complexity target architecture?
|
||||||
|
|
||||||
|
Option A: keep `repo-lib.lib.mkRepo` as a thin compatibility wrapper, but rebase its internals on established components:
|
||||||
|
|
||||||
|
- `flake-parts` for flake structure and `perSystem`
|
||||||
|
- `treefmt-nix` for formatting
|
||||||
|
- `lefthook.nix` for hooks
|
||||||
|
- a separate `mkRelease` package for release automation, with explicit opt-ins for commit/tag/push
|
||||||
|
|
||||||
|
That preserves migration cost for consumers while removing most of the custom orchestration burden from this repo.
|
||||||
|
|
||||||
|
## Correctness Findings
|
||||||
|
|
||||||
|
### High: self-checks were failing because the test harness depended on unstable derivation internals
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- the removed shell-based release test harness
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- The repo did not pass `nix flake check` on the host system before this audit because the tests around `lefthook-check` assumed `nix derivation show` would expose `"/nix/store/...-lefthook.yml.drv"` as a quoted string.
|
||||||
|
- Current Nix emits input derivations in a different JSON shape, so the regex broke even though the underlying derivation still existed.
|
||||||
|
- This is a release blocker because the repo’s own baseline was red.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- Fixed in this audit by replacing the ad hoc scrape with a helper that locates the relevant input derivation from the JSON more defensibly.
|
||||||
|
|
||||||
|
### High: release rollback is destructive
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`packages/release/release.sh`](../../../packages/release/release.sh)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- `revert_on_failure` runs `git reset --hard "$START_HEAD"` after any trapped error.
|
||||||
|
- That will discard all working tree changes created during the release flow, including user-visible file changes that might be useful for debugging or manual recovery.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- This is too aggressive for a library-provided command.
|
||||||
|
- Rollback should be opt-in, staged to a temp branch/worktree, or replaced with a safer failure mode that leaves artifacts visible.
|
||||||
|
|
||||||
|
### Medium: release performs too many side effects in one irreversible flow
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`packages/release/release.sh`](../../../packages/release/release.sh)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- The default flow updates version state, runs release steps, formats, stages, commits, tags, and pushes.
|
||||||
|
- There is no dry-run mode.
|
||||||
|
- There is no `--no-push`, `--no-tag`, or `--no-commit` mode.
|
||||||
|
- The command is framed as a package generated by the library, so consumers inherit a strong opinionated workflow whether they want it or not.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- Release should be separated from repo shell wiring and broken into explicit phases or flags.
|
||||||
|
|
||||||
|
## Organization And Readability Findings
|
||||||
|
|
||||||
|
### High: `lib.nix` is a monolith
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`packages/repo-lib/lib.nix`](../../../packages/repo-lib/lib.nix)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- One file owns normalization helpers, shell assembly, banner formatting inputs, Lefthook synthesis, release templating, compatibility APIs, and top-level outputs.
|
||||||
|
- The public API is therefore not separable from its implementation detail.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- This is the main maintainability problem in the repo.
|
||||||
|
- Even if behavior is mostly correct, the cost of safely changing it is too high.
|
||||||
|
|
||||||
|
### Medium: shell UX logic is coupled to operational behavior
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`packages/repo-lib/shell-hook.sh`](../../../packages/repo-lib/shell-hook.sh)
|
||||||
|
- [`packages/repo-lib/lib.nix`](../../../packages/repo-lib/lib.nix)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- Tool banners do more than render text. They probe commands, parse versions, print failures, and may exit the shell startup for required tools.
|
||||||
|
- That behavior is not obvious from the README example and is spread across generated shell script fragments.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- The banner feature is nice, but it is expensive in complexity and debugging surface relative to the value it adds.
|
||||||
|
- If retained, it should be optional and isolated behind a smaller interface.
|
||||||
|
|
||||||
|
### Medium: legacy compatibility paths dominate the core implementation
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`packages/repo-lib/lib.nix`](../../../packages/repo-lib/lib.nix)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- `mkDevShell` uses legacy tool normalization and its own feature toggles.
|
||||||
|
- `mkRepo` carries a newer strict tool shape.
|
||||||
|
- Both flows feed the same shell-artifact builder, which means the common implementation has to keep both mental models alive.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- Deprecate `mkDevShell` once a thin `mkRepo` wrapper exists over standard components.
|
||||||
|
|
||||||
|
## Public API And Usability Findings
|
||||||
|
|
||||||
|
### High: README underspecifies the real API
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`README.md`](../../../README.md)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- The README explains the happy-path shape of `mkRepo`, but not the actual behavioral contract.
|
||||||
|
- It does not provide a reference for:
|
||||||
|
- tool spec fields
|
||||||
|
- shell banner behavior
|
||||||
|
- exact merge order between `config` and `perSystem`
|
||||||
|
- what the `checks` abstraction cannot express
|
||||||
|
- what `release` is allowed to mutate by default
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- Consumers can start quickly, but they cannot predict behavior well without reading the implementation.
|
||||||
|
|
||||||
|
### Medium: abstraction boundaries are blurry
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [`README.md`](../../../README.md)
|
||||||
|
- [`template/flake.nix`](../../../template/flake.nix)
|
||||||
|
- [`packages/repo-lib/lib.nix`](../../../packages/repo-lib/lib.nix)
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- `checks` looks like the high-level hook API, but advanced usage requires raw Lefthook passthrough.
|
||||||
|
- `shell.bootstrap` is documented as the purity escape hatch, but the template uses it for tool bootstrapping and operational setup.
|
||||||
|
- `release` is presented as optional packaging, but it is operational automation with repo mutation and remote side effects.
|
||||||
|
|
||||||
|
Assessment:
|
||||||
|
|
||||||
|
- These concepts should be separate modules with narrower contracts.
|
||||||
|
|
||||||
|
## Replacement Options
|
||||||
|
|
||||||
|
### Option A: thin compatibility layer
|
||||||
|
|
||||||
|
Keep `repo-lib.lib.mkRepo`, but make it a wrapper over standard components.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
- `flake-parts` for top-level flake assembly and `perSystem`
|
||||||
|
- `treefmt-nix` for formatting
|
||||||
|
- `lefthook.nix` for Git hooks
|
||||||
|
- a standalone `mkRelease` output for release automation
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
|
||||||
|
- lower migration cost
|
||||||
|
- preserves existing entrypoint
|
||||||
|
- reduces bespoke glue
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
|
||||||
|
- some compatibility debt remains
|
||||||
|
- requires a staged migration plan
|
||||||
|
|
||||||
|
### Option B: full replacement
|
||||||
|
|
||||||
|
Stop positioning this as a general-purpose Nix library and keep only:
|
||||||
|
|
||||||
|
- the template
|
||||||
|
- any repo-specific release helper
|
||||||
|
- migration docs to standard tools
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
|
||||||
|
- lowest long-term maintenance burden
|
||||||
|
- clearest product boundary
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
|
||||||
|
- highest consumer migration cost
|
||||||
|
- discards the existing `mkRepo` API
|
||||||
|
|
||||||
|
## Final Recommendation
|
||||||
|
|
||||||
|
Choose **Option A**.
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- `mkRepo` has enough consumer value to keep as a compatibility surface.
|
||||||
|
- Most of the complexity is not unique value. It is custom orchestration around capabilities already provided by better-maintained ecosystem tools.
|
||||||
|
- The release flow should be split out regardless of which option is chosen.
|
||||||
|
|
||||||
|
Concrete target:
|
||||||
|
|
||||||
|
1. Rebase flake structure on `flake-parts`.
|
||||||
|
2. Replace custom hook synthesis with `lefthook.nix`.
|
||||||
|
3. Keep `treefmt-nix` directly exposed instead of deeply wrapped.
|
||||||
|
4. Make shell banners optional or move them behind a very small isolated module.
|
||||||
|
5. Move release automation into a separate package with explicit side-effect flags.
|
||||||
|
6. Mark `mkDevShell` deprecated once `mkRepo` is stable on the new internals.
|
||||||
|
|
||||||
|
## Migration Cost And Compatibility Notes
|
||||||
|
|
||||||
|
- A thin compatibility wrapper keeps consumer migration reasonable.
|
||||||
|
- The biggest compatibility risk is release behavior, because some consumers may depend on the current commit/tag/push flow.
|
||||||
|
- Introduce safer release behavior behind new flags first, then deprecate the old all-in-one default.
|
||||||
|
- Keep template output working during the transition; it is currently the clearest example of intended usage.
|
||||||
|
|
||||||
|
## Required Validation For Follow-Up Work
|
||||||
|
|
||||||
|
- `nix flake show --all-systems`
|
||||||
|
- `nix flake check`
|
||||||
|
- minimal consumer repo using `mkRepo`
|
||||||
|
- template repo evaluation
|
||||||
|
- release smoke test in a temporary git repo
|
||||||
|
- hook assertions that do not depend on private derivation naming/layout
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- `flake-parts`: https://flake.parts/
|
||||||
|
- `treefmt-nix`: https://github.com/numtide/treefmt-nix
|
||||||
|
- `lefthook.nix`: https://github.com/cachix/lefthook.nix
|
||||||
|
- `devenv`: https://github.com/cachix/devenv
|
||||||
93
flake.lock
generated
93
flake.lock
generated
@@ -1,79 +1,44 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-compat": {
|
"flake-parts": {
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767039857,
|
|
||||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git-hooks": {
|
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
"gitignore": "gitignore",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772024342,
|
"lastModified": 1772408722,
|
||||||
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
|
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||||
"owner": "cachix",
|
"owner": "hercules-ci",
|
||||||
"repo": "git-hooks.nix",
|
"repo": "flake-parts",
|
||||||
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
|
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "cachix",
|
"owner": "hercules-ci",
|
||||||
"repo": "git-hooks.nix",
|
"repo": "flake-parts",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitignore": {
|
"lefthook-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"git-hooks",
|
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709087332,
|
"lastModified": 1770377107,
|
||||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
"narHash": "sha256-/QEXSDeAo5RK81PtM0yDhmt9k3v1/pse/jsrT1yXNhU=",
|
||||||
"owner": "hercules-ci",
|
"owner": "sudosubin",
|
||||||
"repo": "gitignore.nix",
|
"repo": "lefthook.nix",
|
||||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
"rev": "9cdaf7ce95ae77cbabc5b556bdd35d3cf0b849f5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "hercules-ci",
|
"owner": "sudosubin",
|
||||||
"repo": "gitignore.nix",
|
"repo": "lefthook.nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
|
||||||
"lastModified": 1770073757,
|
|
||||||
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772542754,
|
"lastModified": 1772542754,
|
||||||
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
||||||
@@ -89,7 +54,22 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_3": {
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772328832,
|
||||||
|
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770107345,
|
"lastModified": 1770107345,
|
||||||
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
|
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
|
||||||
@@ -107,14 +87,15 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"git-hooks": "git-hooks",
|
"flake-parts": "flake-parts",
|
||||||
"nixpkgs": "nixpkgs_2",
|
"lefthook-nix": "lefthook-nix",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
"treefmt-nix": "treefmt-nix"
|
"treefmt-nix": "treefmt-nix"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"treefmt-nix": {
|
"treefmt-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs_3"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770228511,
|
"lastModified": 1770228511,
|
||||||
|
|||||||
359
flake.nix
359
flake.nix
@@ -1,306 +1,129 @@
|
|||||||
# flake.nix — devshell-lib
|
# flake.nix — repo-lib
|
||||||
{
|
{
|
||||||
description = "Shared devshell boilerplate library";
|
description = "Pure-first repo development platform for Nix flakes";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
git-hooks.url = "github:cachix/git-hooks.nix";
|
lefthook-nix.url = "github:sudosubin/lefthook.nix";
|
||||||
|
lefthook-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
|
flake-parts,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
treefmt-nix,
|
treefmt-nix,
|
||||||
git-hooks,
|
lefthook-nix,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
supportedSystems = [
|
lib = nixpkgs.lib;
|
||||||
"x86_64-linux"
|
repoLib = import ./packages/repo-lib/lib.nix {
|
||||||
"aarch64-linux"
|
inherit flake-parts nixpkgs treefmt-nix;
|
||||||
"x86_64-darwin"
|
lefthookNix = lefthook-nix;
|
||||||
"aarch64-darwin"
|
releaseScriptPath = ./packages/release/release.sh;
|
||||||
];
|
shellHookTemplatePath = ./packages/repo-lib/shell-hook.sh;
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
lib = {
|
|
||||||
|
|
||||||
# ── mkDevShell ───────────────────────────────────────────────────────
|
|
||||||
mkDevShell =
|
|
||||||
{
|
|
||||||
system,
|
|
||||||
extraPackages ? [ ],
|
|
||||||
extraShellHook ? "",
|
|
||||||
additionalHooks ? { },
|
|
||||||
tools ? [ ],
|
|
||||||
includeStandardPackages ? true,
|
|
||||||
# tools = list of { name, bin, versionCmd, color? }
|
|
||||||
# e.g. { name = "Bun"; bin = "${pkgs.bun}/bin/bun"; versionCmd = "--version"; color = "YELLOW"; }
|
|
||||||
formatters ? { },
|
|
||||||
# formatters = treefmt-nix programs attrset, merged over { nixfmt.enable = true; }
|
|
||||||
# e.g. { gofmt.enable = true; shfmt.enable = true; }
|
|
||||||
formatterSettings ? { },
|
|
||||||
# formatterSettings = treefmt-nix settings.formatter attrset
|
|
||||||
# e.g. { shfmt.options = [ "-i" "2" "-s" "-w" ]; }
|
|
||||||
features ? { },
|
|
||||||
# features.oxfmt = true → adds pkgs.oxfmt + pkgs.oxlint, enables oxfmt in treefmt
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { inherit system; };
|
|
||||||
standardPackages = with pkgs; [
|
|
||||||
nixfmt
|
|
||||||
gitlint
|
|
||||||
gitleaks
|
|
||||||
shfmt
|
|
||||||
];
|
|
||||||
selectedStandardPackages = pkgs.lib.optionals includeStandardPackages standardPackages;
|
|
||||||
|
|
||||||
oxfmtEnabled = features.oxfmt or false;
|
|
||||||
oxfmtPackages = pkgs.lib.optionals oxfmtEnabled [
|
|
||||||
pkgs.oxfmt
|
|
||||||
pkgs.oxlint
|
|
||||||
];
|
|
||||||
oxfmtFormatters = pkgs.lib.optionalAttrs oxfmtEnabled {
|
|
||||||
oxfmt.enable = true;
|
|
||||||
};
|
};
|
||||||
|
supportedSystems = repoLib.systems.default;
|
||||||
|
importPkgs = nixpkgsInput: system: import nixpkgsInput { inherit system; };
|
||||||
|
|
||||||
treefmtEval = treefmt-nix.lib.evalModule pkgs {
|
projectOutputs = repoLib.mkRepo {
|
||||||
projectRootFile = "flake.nix";
|
inherit self nixpkgs;
|
||||||
programs = {
|
|
||||||
nixfmt.enable = true; # always on — every repo has a flake.nix
|
|
||||||
}
|
|
||||||
// oxfmtFormatters
|
|
||||||
// formatters;
|
|
||||||
settings.formatter = { } // formatterSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
pre-commit-check = git-hooks.lib.${system}.run {
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
hooks = {
|
config = {
|
||||||
treefmt = {
|
release = {
|
||||||
enable = true;
|
steps = [
|
||||||
entry = "${treefmtEval.config.build.wrapper}/bin/treefmt";
|
{
|
||||||
pass_filenames = true;
|
replace = {
|
||||||
};
|
path = "template/flake.nix";
|
||||||
gitlint.enable = true;
|
regex = ''^([[:space:]]*repo-lib\.url = ")git\+https://git\.dgren\.dev/eric/nix-flake-lib[^"]*(";)'';
|
||||||
gitleaks = {
|
replacement = ''\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/$FULL_TAG\2'';
|
||||||
enable = true;
|
|
||||||
entry = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
|
|
||||||
pass_filenames = false;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// additionalHooks;
|
|
||||||
};
|
|
||||||
|
|
||||||
toolBannerScript = pkgs.lib.concatMapStrings (
|
|
||||||
t:
|
|
||||||
let
|
|
||||||
colorVar = "$" + (t.color or "YELLOW");
|
|
||||||
in
|
|
||||||
''
|
|
||||||
if command -v ${t.bin} >/dev/null 2>&1; then
|
|
||||||
printf " $CYAN ${t.name}:$RESET\t${colorVar}%s$RESET\n" "$(${t.bin} ${t.versionCmd})"
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
) tools;
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
inherit pre-commit-check;
|
replace = {
|
||||||
|
path = "README.md";
|
||||||
formatter = treefmtEval.config.build.wrapper;
|
regex = ''(nix flake new myapp -t ')git\+https://git\.dgren\.dev/eric/nix-flake-lib[^']*(#default' --refresh)'';
|
||||||
|
replacement = ''\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/$FULL_TAG\2'';
|
||||||
shell = pkgs.mkShell {
|
|
||||||
packages = selectedStandardPackages ++ extraPackages ++ oxfmtPackages;
|
|
||||||
|
|
||||||
buildInputs = pre-commit-check.enabledPackages;
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
${pre-commit-check.shellHook}
|
|
||||||
|
|
||||||
if [ -t 1 ]; then
|
|
||||||
command -v tput >/dev/null 2>&1 && tput clear || printf '\033c'
|
|
||||||
fi
|
|
||||||
|
|
||||||
GREEN='\033[1;32m'
|
|
||||||
CYAN='\033[1;36m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[1;34m'
|
|
||||||
RESET='\033[0m'
|
|
||||||
|
|
||||||
printf "\n$GREEN 🚀 Dev shell ready$RESET\n\n"
|
|
||||||
${toolBannerScript}
|
|
||||||
printf "\n"
|
|
||||||
|
|
||||||
${extraShellHook}
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
# ── mkRelease ────────────────────────────────────────────────────────
|
|
||||||
mkRelease =
|
|
||||||
{
|
{
|
||||||
|
replace = {
|
||||||
|
path = "README.md";
|
||||||
|
regex = ''^([[:space:]]*inputs\.repo-lib\.url = ")git\+https://git\.dgren\.dev/eric/nix-flake-lib[^"]*(";)'';
|
||||||
|
replacement = ''\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/$FULL_TAG\2'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
perSystem =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
system,
|
system,
|
||||||
# Source of truth is always $ROOT_DIR/VERSION.
|
...
|
||||||
# Format:
|
|
||||||
# line 1: X.Y.Z
|
|
||||||
# line 2: CHANNEL (stable|alpha|beta|rc|internal|...)
|
|
||||||
# line 3: N (prerelease number, 0 for stable)
|
|
||||||
postVersion ? "",
|
|
||||||
# Shell string — runs after VERSION + release steps are written/run, before git add.
|
|
||||||
# Same env vars available.
|
|
||||||
release ? [ ],
|
|
||||||
# Unified list processed in declaration order:
|
|
||||||
# { file = "path/to/file"; content = ''...$FULL_VERSION...''; } # write file
|
|
||||||
# { run = ''...shell snippet...''; } # run script
|
|
||||||
# Example:
|
|
||||||
# release = [
|
|
||||||
# {
|
|
||||||
# file = "src/version.ts";
|
|
||||||
# content = ''export const APP_VERSION = "$FULL_VERSION" as const;'';
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# file = "internal/version/version.go";
|
|
||||||
# content = ''
|
|
||||||
# package version
|
|
||||||
#
|
|
||||||
# const Version = "$FULL_VERSION"
|
|
||||||
# '';
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# run = ''
|
|
||||||
# sed -E -i "s#^([[:space:]]*my-lib\\.url = \")github:org/my-lib[^"]*(\";)#\\1github:org/my-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/flake.nix"
|
|
||||||
# '';
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# Runtime env includes: BASE_VERSION, CHANNEL, PRERELEASE_NUM, FULL_VERSION, FULL_TAG.
|
|
||||||
channels ? [
|
|
||||||
"alpha"
|
|
||||||
"beta"
|
|
||||||
"rc"
|
|
||||||
"internal"
|
|
||||||
],
|
|
||||||
# Valid release channels beyond "stable". Validated at runtime.
|
|
||||||
extraRuntimeInputs ? [ ],
|
|
||||||
# Extra packages available in the release script's PATH.
|
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { inherit system; };
|
|
||||||
channelList = pkgs.lib.concatStringsSep " " channels;
|
|
||||||
|
|
||||||
releaseStepsScript = pkgs.lib.concatMapStrings (
|
|
||||||
entry:
|
|
||||||
if entry ? file then
|
|
||||||
''
|
|
||||||
mkdir -p "$(dirname "${entry.file}")"
|
|
||||||
cat > "${entry.file}" << NIXEOF
|
|
||||||
${entry.content}
|
|
||||||
NIXEOF
|
|
||||||
log "Generated version file: ${entry.file}"
|
|
||||||
''
|
|
||||||
else if entry ? run then
|
|
||||||
''
|
|
||||||
${entry.run}
|
|
||||||
''
|
|
||||||
else
|
|
||||||
builtins.throw "release entry must have either 'file' or 'run'"
|
|
||||||
) release;
|
|
||||||
|
|
||||||
script =
|
|
||||||
builtins.replaceStrings
|
|
||||||
[
|
|
||||||
"__CHANNEL_LIST__"
|
|
||||||
"__RELEASE_STEPS__"
|
|
||||||
"__POST_VERSION__"
|
|
||||||
]
|
|
||||||
[
|
|
||||||
channelList
|
|
||||||
releaseStepsScript
|
|
||||||
postVersion
|
|
||||||
]
|
|
||||||
(builtins.readFile ./packages/release/release.sh);
|
|
||||||
in
|
|
||||||
pkgs.writeShellApplication {
|
|
||||||
name = "release";
|
|
||||||
runtimeInputs =
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
git
|
|
||||||
gnugrep
|
|
||||||
gawk
|
|
||||||
gnused
|
|
||||||
coreutils
|
|
||||||
]
|
|
||||||
++ extraRuntimeInputs;
|
|
||||||
text = script;
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
# ── packages ────────────────────────────────────────────────────────────
|
|
||||||
packages = forAllSystems (system: {
|
|
||||||
# Expose a no-op release package for the lib repo itself (dogfood)
|
|
||||||
release = self.lib.mkRelease {
|
|
||||||
inherit system;
|
|
||||||
release = [
|
|
||||||
{
|
{
|
||||||
run = ''
|
tools = [
|
||||||
sed -E -i "s#^([[:space:]]*devshell-lib\\.url = \")git\\+https://git\\.dgren\\.dev/eric/nix-flake-lib[^\"]*(\";)#\\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/template/flake.nix"
|
(repoLib.tools.fromCommand {
|
||||||
log "Updated template/flake.nix devshell-lib ref to $FULL_TAG"
|
name = "Nix";
|
||||||
|
command = "nix";
|
||||||
|
version = {
|
||||||
|
args = [ "--version" ];
|
||||||
|
group = 1;
|
||||||
|
};
|
||||||
|
banner = {
|
||||||
|
color = "BLUE";
|
||||||
|
icon = "";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
shell.packages = [ self.packages.${system}.release ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testChecks = lib.genAttrs supportedSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = importPkgs nixpkgs system;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
release-tests =
|
||||||
|
pkgs.runCommand "release-tests"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
git
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
export HOME="$PWD/.home"
|
||||||
|
export GOCACHE="$PWD/.go-cache"
|
||||||
|
mkdir -p "$GOCACHE" "$HOME"
|
||||||
|
cd ${./packages/release}
|
||||||
|
go test ./...
|
||||||
|
touch "$out"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
# ── devShells ───────────────────────────────────────────────────────────
|
|
||||||
devShells = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { inherit system; };
|
|
||||||
env = self.lib.mkDevShell {
|
|
||||||
inherit system;
|
|
||||||
extraPackages = with pkgs; [
|
|
||||||
self.packages.${system}.release
|
|
||||||
];
|
|
||||||
tools = [
|
|
||||||
{
|
|
||||||
name = "Nix";
|
|
||||||
bin = "${pkgs.nix}/bin/nix";
|
|
||||||
versionCmd = "--version";
|
|
||||||
color = "YELLOW";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
default = env.shell;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
# ── checks ──────────────────────────────────────────────────────────────
|
|
||||||
checks = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
env = self.lib.mkDevShell { inherit system; };
|
|
||||||
in
|
in
|
||||||
{
|
projectOutputs
|
||||||
inherit (env) pre-commit-check;
|
// {
|
||||||
}
|
lib = repoLib;
|
||||||
);
|
|
||||||
|
|
||||||
# ── formatter ───────────────────────────────────────────────────────────
|
|
||||||
formatter = forAllSystems (system: (self.lib.mkDevShell { inherit system; }).formatter);
|
|
||||||
|
|
||||||
# ── templates ───────────────────────────────────────────────────────────
|
|
||||||
templates = {
|
templates = {
|
||||||
default = {
|
default = {
|
||||||
path = ./template;
|
path = ./template;
|
||||||
description = "Product repo using devshell-lib";
|
description = "Product repo using repo-lib";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
checks = lib.genAttrs supportedSystems (
|
||||||
|
system: projectOutputs.checks.${system} // testChecks.${system}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/release/.gitignore
vendored
Normal file
1
packages/release/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
vendor/
|
||||||
127
packages/release/cmd/release/main.go
Normal file
127
packages/release/cmd/release/main.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
release "repo-lib/packages/release/internal/release"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args []string) error {
|
||||||
|
if len(args) > 0 && args[0] == "version-meta" {
|
||||||
|
return runVersionMeta(args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseArgs, execution, selectMode, err := parseReleaseCLIArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := release.Config{
|
||||||
|
RootDir: os.Getenv("REPO_LIB_RELEASE_ROOT_DIR"),
|
||||||
|
AllowedChannels: splitEnvList("REPO_LIB_RELEASE_CHANNELS"),
|
||||||
|
ReleaseStepsJSON: os.Getenv("REPO_LIB_RELEASE_STEPS_JSON"),
|
||||||
|
PostVersion: os.Getenv("REPO_LIB_RELEASE_POST_VERSION"),
|
||||||
|
Execution: execution,
|
||||||
|
Env: os.Environ(),
|
||||||
|
Stdout: os.Stdout,
|
||||||
|
Stderr: os.Stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRunInteractiveSelector(releaseArgs, selectMode) {
|
||||||
|
if !release.IsInteractiveTerminal(os.Stdin, os.Stdout) {
|
||||||
|
return fmt.Errorf("interactive release selector requires a terminal")
|
||||||
|
}
|
||||||
|
selectedArgs, confirmed, err := release.SelectCommand(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
releaseArgs = selectedArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &release.Runner{Config: config}
|
||||||
|
return r.Run(releaseArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRunInteractiveSelector(args []string, selectMode bool) bool {
|
||||||
|
if selectMode {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(args) == 0 {
|
||||||
|
return release.IsInteractiveTerminal(os.Stdin, os.Stdout)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseReleaseCLIArgs(args []string) ([]string, release.ExecutionOptions, bool, error) {
|
||||||
|
var releaseArgs []string
|
||||||
|
execution := release.ExecutionOptions{}
|
||||||
|
selectMode := false
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "select":
|
||||||
|
selectMode = true
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(arg, "--") {
|
||||||
|
return nil, release.ExecutionOptions{}, false, fmt.Errorf("unknown flag %q", arg)
|
||||||
|
}
|
||||||
|
releaseArgs = append(releaseArgs, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectMode && len(releaseArgs) > 0 {
|
||||||
|
return nil, release.ExecutionOptions{}, false, fmt.Errorf("select does not take a release argument")
|
||||||
|
}
|
||||||
|
return releaseArgs, execution.Normalize(), selectMode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVersionMeta(args []string) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return fmt.Errorf("version-meta requires an action and key")
|
||||||
|
}
|
||||||
|
rootDir := os.Getenv("ROOT_DIR")
|
||||||
|
if rootDir == "" {
|
||||||
|
return fmt.Errorf("ROOT_DIR is required")
|
||||||
|
}
|
||||||
|
versionPath := rootDir + "/VERSION"
|
||||||
|
file, err := release.ReadVersionFile(versionPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "set":
|
||||||
|
if len(args) != 3 {
|
||||||
|
return fmt.Errorf("version-meta set requires key and value")
|
||||||
|
}
|
||||||
|
file.Metadata.Set(args[1], args[2])
|
||||||
|
case "unset":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("version-meta unset requires key")
|
||||||
|
}
|
||||||
|
file.Metadata.Unset(args[1])
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown version-meta action %q", args[0])
|
||||||
|
}
|
||||||
|
return file.Write(versionPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitEnvList(name string) []string {
|
||||||
|
raw := strings.Fields(os.Getenv(name))
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
29
packages/release/go.mod
Normal file
29
packages/release/go.mod
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
module repo-lib/packages/release
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
golang.org/x/term v0.41.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
45
packages/release/go.sum
Normal file
45
packages/release/go.sum
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
91
packages/release/internal/release/exec.go
Normal file
91
packages/release/internal/release/exec.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func requireCleanGit(rootDir string) error {
|
||||||
|
if _, err := runCommand(rootDir, nil, io.Discard, io.Discard, "git", "diff", "--quiet"); err != nil {
|
||||||
|
return errors.New("git working tree is not clean. Commit or stash changes first")
|
||||||
|
}
|
||||||
|
if _, err := runCommand(rootDir, nil, io.Discard, io.Discard, "git", "diff", "--cached", "--quiet"); err != nil {
|
||||||
|
return errors.New("git working tree is not clean. Commit or stash changes first")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitOutput(dir string, args ...string) (string, error) {
|
||||||
|
return runCommand(dir, nil, nil, nil, "git", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(dir string, env []string, stdout io.Writer, stderr io.Writer, name string, args ...string) (string, error) {
|
||||||
|
resolvedName, err := resolveExecutable(name, env)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(resolvedName, args...)
|
||||||
|
if dir != "" {
|
||||||
|
cmd.Dir = dir
|
||||||
|
}
|
||||||
|
if env != nil {
|
||||||
|
cmd.Env = env
|
||||||
|
} else {
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = io.MultiWriter(&out, writerOrDiscard(stdout))
|
||||||
|
cmd.Stderr = io.MultiWriter(&out, writerOrDiscard(stderr))
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
output := strings.TrimSpace(out.String())
|
||||||
|
if output == "" {
|
||||||
|
return out.String(), fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err)
|
||||||
|
}
|
||||||
|
return out.String(), fmt.Errorf("%s %s: %w\n%s", name, strings.Join(args, " "), err, output)
|
||||||
|
}
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveExecutable(name string, env []string) (string, error) {
|
||||||
|
if strings.ContainsRune(name, os.PathSeparator) {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pathValue := os.Getenv("PATH")
|
||||||
|
for _, entry := range env {
|
||||||
|
if strings.HasPrefix(entry, "PATH=") {
|
||||||
|
pathValue = strings.TrimPrefix(entry, "PATH=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range filepath.SplitList(pathValue) {
|
||||||
|
if dir == "" {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
candidate := filepath.Join(dir, name)
|
||||||
|
info, err := os.Stat(candidate)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.Mode()&0o111 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return candidate, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("executable %q not found in PATH", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writerOrDiscard(w io.Writer) io.Writer {
|
||||||
|
if w == nil {
|
||||||
|
return io.Discard
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
64
packages/release/internal/release/release_step.go
Normal file
64
packages/release/internal/release/release_step.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleaseStep struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Regex string `json:"regex,omitempty"`
|
||||||
|
Replacement string `json:"replacement,omitempty"`
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeReleaseSteps(raw string) ([]ReleaseStep, error) {
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var steps []ReleaseStep
|
||||||
|
if err := json.Unmarshal([]byte(raw), &steps); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode release steps: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, step := range steps {
|
||||||
|
if err := validateReleaseStep(step); err != nil {
|
||||||
|
return nil, fmt.Errorf("release step %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateReleaseStep(step ReleaseStep) error {
|
||||||
|
switch step.Kind {
|
||||||
|
case "writeFile":
|
||||||
|
if step.Path == "" {
|
||||||
|
return fmt.Errorf("writeFile.path is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "replace":
|
||||||
|
if step.Path == "" {
|
||||||
|
return fmt.Errorf("replace.path is required")
|
||||||
|
}
|
||||||
|
if step.Regex == "" {
|
||||||
|
return fmt.Errorf("replace.regex is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "versionMetaSet":
|
||||||
|
if step.Key == "" {
|
||||||
|
return fmt.Errorf("versionMetaSet.key is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "versionMetaUnset":
|
||||||
|
if step.Key == "" {
|
||||||
|
return fmt.Errorf("versionMetaUnset.key is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported release step kind %q", step.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/release/internal/release/release_step_apply.go
Normal file
39
packages/release/internal/release/release_step_apply.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Runner) runReleaseSteps(rootDir string, versionPath string, versionFile *VersionFile, version Version, stdout io.Writer, stderr io.Writer) error {
|
||||||
|
steps, err := decodeReleaseSteps(r.Config.ReleaseStepsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(steps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := newReleaseStepContext(rootDir, versionPath, versionFile, version, r.Config.Env)
|
||||||
|
for i, step := range steps {
|
||||||
|
if err := applyReleaseStep(ctx, step); err != nil {
|
||||||
|
return fmt.Errorf("release step %d (%s): %w", i+1, step.Kind, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyReleaseStep(ctx *ReleaseStepContext, step ReleaseStep) error {
|
||||||
|
switch step.Kind {
|
||||||
|
case "writeFile":
|
||||||
|
return applyWriteFileStep(ctx, step)
|
||||||
|
case "replace":
|
||||||
|
return applyReplaceStep(ctx, step)
|
||||||
|
case "versionMetaSet":
|
||||||
|
return applyVersionMetaSetStep(ctx, step)
|
||||||
|
case "versionMetaUnset":
|
||||||
|
return applyVersionMetaUnsetStep(ctx, step)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported release step kind %q", step.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
73
packages/release/internal/release/release_step_context.go
Normal file
73
packages/release/internal/release/release_step_context.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleaseStepContext struct {
|
||||||
|
RootDir string
|
||||||
|
VersionPath string
|
||||||
|
Version Version
|
||||||
|
VersionFile *VersionFile
|
||||||
|
Env map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReleaseStepContext(rootDir string, versionPath string, versionFile *VersionFile, version Version, env []string) *ReleaseStepContext {
|
||||||
|
return &ReleaseStepContext{
|
||||||
|
RootDir: rootDir,
|
||||||
|
VersionPath: versionPath,
|
||||||
|
Version: version,
|
||||||
|
VersionFile: versionFile,
|
||||||
|
Env: buildReleaseEnv(rootDir, versionFile, version, env),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReleaseEnv(rootDir string, versionFile *VersionFile, version Version, baseEnv []string) map[string]string {
|
||||||
|
env := make(map[string]string, len(baseEnv)+8+len(versionFile.Metadata.lines))
|
||||||
|
if len(baseEnv) == 0 {
|
||||||
|
baseEnv = os.Environ()
|
||||||
|
}
|
||||||
|
for _, entry := range baseEnv {
|
||||||
|
key, value, ok := strings.Cut(entry, "=")
|
||||||
|
if ok {
|
||||||
|
env[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env["ROOT_DIR"] = rootDir
|
||||||
|
env["BASE_VERSION"] = version.BaseString()
|
||||||
|
env["CHANNEL"] = version.Channel
|
||||||
|
env["FULL_VERSION"] = version.String()
|
||||||
|
env["FULL_TAG"] = version.Tag()
|
||||||
|
if version.Channel == "stable" {
|
||||||
|
env["PRERELEASE_NUM"] = ""
|
||||||
|
} else {
|
||||||
|
env["PRERELEASE_NUM"] = strconv.Itoa(version.Prerelease)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range versionFile.Metadata.lines {
|
||||||
|
key, value, ok := strings.Cut(line, "=")
|
||||||
|
if !ok || key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
env[sanitizeMetaEnvName(key)] = value
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReleaseStepContext) expand(raw string) string {
|
||||||
|
return os.Expand(raw, func(name string) string {
|
||||||
|
return c.Env[name]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReleaseStepContext) resolvePath(path string) string {
|
||||||
|
expanded := c.expand(path)
|
||||||
|
if filepath.IsAbs(expanded) {
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
return filepath.Join(c.RootDir, expanded)
|
||||||
|
}
|
||||||
45
packages/release/internal/release/release_step_replace.go
Normal file
45
packages/release/internal/release/release_step_replace.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyReplaceStep(ctx *ReleaseStepContext, step ReleaseStep) error {
|
||||||
|
targetPath := ctx.resolvePath(step.Path)
|
||||||
|
content, err := os.ReadFile(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern, err := regexp.Compile("(?m)" + ctx.expand(step.Regex))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compile regex for %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
replacement := translateReplacementBackrefs(ctx.expand(step.Replacement))
|
||||||
|
updated := pattern.ReplaceAllString(string(content), replacement)
|
||||||
|
if err := os.WriteFile(targetPath, []byte(updated), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateReplacementBackrefs(raw string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(raw))
|
||||||
|
|
||||||
|
for i := 0; i < len(raw); i++ {
|
||||||
|
if raw[i] == '\\' && i+1 < len(raw) && raw[i+1] >= '1' && raw[i+1] <= '9' {
|
||||||
|
b.WriteString("${")
|
||||||
|
b.WriteByte(raw[i+1])
|
||||||
|
b.WriteByte('}')
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteByte(raw[i])
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTranslateReplacementBackrefsWrapsCaptureNumbers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := translateReplacementBackrefs(`\1git+https://example.test/ref\2`)
|
||||||
|
want := `${1}git+https://example.test/ref${2}`
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("translateReplacementBackrefs() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func applyVersionMetaSetStep(ctx *ReleaseStepContext, step ReleaseStep) error {
|
||||||
|
ctx.VersionFile.Metadata.Set(step.Key, ctx.expand(step.Value))
|
||||||
|
ctx.Env[sanitizeMetaEnvName(step.Key)] = ctx.VersionFile.Metadata.Get(step.Key)
|
||||||
|
if err := ctx.VersionFile.Write(ctx.VersionPath); err != nil {
|
||||||
|
return fmt.Errorf("write VERSION: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyVersionMetaUnsetStep(ctx *ReleaseStepContext, step ReleaseStep) error {
|
||||||
|
ctx.VersionFile.Metadata.Unset(step.Key)
|
||||||
|
delete(ctx.Env, sanitizeMetaEnvName(step.Key))
|
||||||
|
if err := ctx.VersionFile.Write(ctx.VersionPath); err != nil {
|
||||||
|
return fmt.Errorf("write VERSION: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
18
packages/release/internal/release/release_step_write_file.go
Normal file
18
packages/release/internal/release/release_step_write_file.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyWriteFileStep(ctx *ReleaseStepContext, step ReleaseStep) error {
|
||||||
|
targetPath := ctx.resolvePath(step.Path)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir %s: %w", filepath.Dir(targetPath), err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(targetPath, []byte(ctx.expand(step.Text)), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
372
packages/release/internal/release/release_test.go
Normal file
372
packages/release/internal/release/release_test.go
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveNextVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
allowed := []string{"alpha", "beta", "rc", "internal"}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
current string
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "channel only from stable bumps patch",
|
||||||
|
current: "1.0.0",
|
||||||
|
args: []string{"beta"},
|
||||||
|
want: "1.0.1-beta.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit minor bump keeps requested bump",
|
||||||
|
current: "1.0.0",
|
||||||
|
args: []string{"minor", "beta"},
|
||||||
|
want: "1.1.0-beta.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full promotes prerelease to stable",
|
||||||
|
current: "1.1.5-beta.1",
|
||||||
|
args: []string{"full"},
|
||||||
|
want: "1.1.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set stable from prerelease requires full",
|
||||||
|
current: "1.1.5-beta.1",
|
||||||
|
args: []string{"set", "1.1.5"},
|
||||||
|
wantErr: "promote using 'stable' or 'full' only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "patch stable from prerelease requires full",
|
||||||
|
current: "1.1.5-beta.1",
|
||||||
|
args: []string{"patch", "stable"},
|
||||||
|
wantErr: "promote using 'stable' or 'full' only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full no-op fails",
|
||||||
|
current: "1.1.5",
|
||||||
|
args: []string{"full"},
|
||||||
|
wantErr: "Version 1.1.5 is already current; nothing to do.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
current, err := ParseVersion(tc.current)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseVersion(%q): %v", tc.current, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ResolveNextVersion(current, tc.args, allowed)
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("ResolveNextVersion(%q, %v) succeeded, want error", tc.current, tc.args)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||||
|
t.Fatalf("ResolveNextVersion(%q, %v) error = %q, want substring %q", tc.current, tc.args, err.Error(), tc.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveNextVersion(%q, %v): %v", tc.current, tc.args, err)
|
||||||
|
}
|
||||||
|
if got.String() != tc.want {
|
||||||
|
t.Fatalf("ResolveNextVersion(%q, %v) = %q, want %q", tc.current, tc.args, got.String(), tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionFileMetadataRoundTrip(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "VERSION")
|
||||||
|
content := strings.Join([]string{
|
||||||
|
"1.0.0",
|
||||||
|
"stable",
|
||||||
|
"0",
|
||||||
|
"desktop_backend_change_scope=bindings",
|
||||||
|
"desktop_release_mode=binary",
|
||||||
|
"desktop_unused=temporary",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(VERSION): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := ReadVersionFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadVersionFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := file.Current().String(); got != "1.0.0" {
|
||||||
|
t.Fatalf("Current() = %q, want 1.0.0", got)
|
||||||
|
}
|
||||||
|
if got := file.Metadata.Get("desktop_backend_change_scope"); got != "bindings" {
|
||||||
|
t.Fatalf("Metadata.Get(scope) = %q, want bindings", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Version = MustParseVersion(t, "1.0.1")
|
||||||
|
file.Metadata.Set("desktop_release_mode", "codepush")
|
||||||
|
file.Metadata.Set("desktop_binary_version_min", "1.0.0")
|
||||||
|
file.Metadata.Set("desktop_binary_version_max", "1.0.1")
|
||||||
|
file.Metadata.Set("desktop_backend_compat_id", "compat-123")
|
||||||
|
file.Metadata.Unset("desktop_unused")
|
||||||
|
|
||||||
|
if err := file.Write(path); err != nil {
|
||||||
|
t.Fatalf("Write(VERSION): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotBytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(VERSION): %v", err)
|
||||||
|
}
|
||||||
|
got := string(gotBytes)
|
||||||
|
for _, needle := range []string{
|
||||||
|
"1.0.1\nstable\n0\n",
|
||||||
|
"desktop_backend_change_scope=bindings",
|
||||||
|
"desktop_release_mode=codepush",
|
||||||
|
"desktop_binary_version_min=1.0.0",
|
||||||
|
"desktop_binary_version_max=1.0.1",
|
||||||
|
"desktop_backend_compat_id=compat-123",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, needle) {
|
||||||
|
t.Fatalf("VERSION missing %q:\n%s", needle, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "desktop_unused=temporary") {
|
||||||
|
t.Fatalf("VERSION still contains removed metadata:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerExecutesReleaseFlow(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
remote := filepath.Join(t.TempDir(), "remote.git")
|
||||||
|
mustRun(t, root, "git", "init")
|
||||||
|
mustRun(t, root, "git", "config", "user.name", "Release Test")
|
||||||
|
mustRun(t, root, "git", "config", "user.email", "release-test@example.com")
|
||||||
|
mustRun(t, root, "git", "config", "commit.gpgsign", "false")
|
||||||
|
mustRun(t, root, "git", "config", "tag.gpgsign", "false")
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "flake.nix"), []byte("{ outputs = { self }: {}; }\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(flake.nix): %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.0.0\nstable\n0\ndesktop_backend_change_scope=bindings\ndesktop_release_mode=binary\ndesktop_unused=temporary\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(VERSION): %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "notes.txt"), []byte("version=old\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(notes.txt): %v", err)
|
||||||
|
}
|
||||||
|
mustRun(t, root, "git", "add", "-A")
|
||||||
|
mustRun(t, root, "git", "commit", "-m", "init")
|
||||||
|
mustRun(t, root, "git", "init", "--bare", remote)
|
||||||
|
mustRun(t, root, "git", "remote", "add", "origin", remote)
|
||||||
|
mustRun(t, root, "git", "push", "-u", "origin", "HEAD")
|
||||||
|
|
||||||
|
binDir := t.TempDir()
|
||||||
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll(bin): %v", err)
|
||||||
|
}
|
||||||
|
nixPath := filepath.Join(binDir, "nix")
|
||||||
|
nixScript := "#!/usr/bin/env bash\nif [[ \"${1-}\" == \"fmt\" ]]; then\n exit 0\nfi\necho \"unexpected nix invocation: $*\" >&2\nexit 1\n"
|
||||||
|
if err := os.WriteFile(nixPath, []byte(nixScript), 0o755); err != nil {
|
||||||
|
t.Fatalf("WriteFile(bin/nix): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &Runner{
|
||||||
|
Config: Config{
|
||||||
|
RootDir: root,
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc", "internal"},
|
||||||
|
ReleaseStepsJSON: mustJSON(t, []ReleaseStep{
|
||||||
|
{Kind: "writeFile", Path: "generated/version.txt", Text: "$FULL_VERSION\n"},
|
||||||
|
{Kind: "replace", Path: "notes.txt", Regex: "^version=.*$", Replacement: "version=$FULL_VERSION"},
|
||||||
|
{Kind: "writeFile", Path: "release.tag", Text: "$FULL_TAG\n"},
|
||||||
|
{Kind: "writeFile", Path: "metadata/scope.txt", Text: "$VERSION_META_DESKTOP_BACKEND_CHANGE_SCOPE\n"},
|
||||||
|
{Kind: "writeFile", Path: "metadata/mode-before.txt", Text: "$VERSION_META_DESKTOP_RELEASE_MODE\n"},
|
||||||
|
{Kind: "versionMetaSet", Key: "desktop_release_mode", Value: "codepush"},
|
||||||
|
{Kind: "versionMetaSet", Key: "desktop_binary_version_min", Value: "1.0.0"},
|
||||||
|
{Kind: "versionMetaSet", Key: "desktop_binary_version_max", Value: "$FULL_VERSION"},
|
||||||
|
{Kind: "versionMetaSet", Key: "desktop_backend_compat_id", Value: "compat-123"},
|
||||||
|
{Kind: "versionMetaUnset", Key: "desktop_unused"},
|
||||||
|
}),
|
||||||
|
PostVersion: "printf '%s\\n' \"$FULL_VERSION\" >\"$ROOT_DIR/post-version.txt\"",
|
||||||
|
Execution: ExecutionOptions{
|
||||||
|
Commit: true,
|
||||||
|
Tag: true,
|
||||||
|
Push: true,
|
||||||
|
},
|
||||||
|
Env: append(os.Environ(), "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Run([]string{"patch"}); err != nil {
|
||||||
|
t.Fatalf("Runner.Run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionFile, err := ReadVersionFile(filepath.Join(root, "VERSION"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadVersionFile(after): %v", err)
|
||||||
|
}
|
||||||
|
if got := versionFile.Current().String(); got != "1.0.1" {
|
||||||
|
t.Fatalf("Current() after release = %q, want 1.0.1", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFileEquals(t, filepath.Join(root, "generated/version.txt"), "1.0.1\n")
|
||||||
|
assertFileEquals(t, filepath.Join(root, "notes.txt"), "version=1.0.1\n")
|
||||||
|
assertFileEquals(t, filepath.Join(root, "release.tag"), "v1.0.1\n")
|
||||||
|
assertFileEquals(t, filepath.Join(root, "metadata/scope.txt"), "bindings\n")
|
||||||
|
assertFileEquals(t, filepath.Join(root, "metadata/mode-before.txt"), "binary\n")
|
||||||
|
assertFileEquals(t, filepath.Join(root, "post-version.txt"), "1.0.1\n")
|
||||||
|
|
||||||
|
versionBytes, err := os.ReadFile(filepath.Join(root, "VERSION"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(VERSION after): %v", err)
|
||||||
|
}
|
||||||
|
versionText := string(versionBytes)
|
||||||
|
for _, needle := range []string{
|
||||||
|
"desktop_backend_change_scope=bindings",
|
||||||
|
"desktop_release_mode=codepush",
|
||||||
|
"desktop_binary_version_min=1.0.0",
|
||||||
|
"desktop_binary_version_max=1.0.1",
|
||||||
|
"desktop_backend_compat_id=compat-123",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(versionText, needle) {
|
||||||
|
t.Fatalf("VERSION missing %q:\n%s", needle, versionText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(versionText, "desktop_unused=temporary") {
|
||||||
|
t.Fatalf("VERSION still contains removed metadata:\n%s", versionText)
|
||||||
|
}
|
||||||
|
|
||||||
|
tagList := strings.TrimSpace(mustOutput(t, root, "git", "tag", "--list", "v1.0.1"))
|
||||||
|
if tagList != "v1.0.1" {
|
||||||
|
t.Fatalf("git tag --list v1.0.1 = %q, want v1.0.1", tagList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerAlwaysCommitsTagsAndPushes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
remote := filepath.Join(t.TempDir(), "remote.git")
|
||||||
|
mustRun(t, root, "git", "init")
|
||||||
|
mustRun(t, root, "git", "config", "user.name", "Release Test")
|
||||||
|
mustRun(t, root, "git", "config", "user.email", "release-test@example.com")
|
||||||
|
mustRun(t, root, "git", "config", "commit.gpgsign", "false")
|
||||||
|
mustRun(t, root, "git", "config", "tag.gpgsign", "false")
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "flake.nix"), []byte("{ outputs = { self }: {}; }\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(flake.nix): %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.0.0\nstable\n0\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(VERSION): %v", err)
|
||||||
|
}
|
||||||
|
mustRun(t, root, "git", "add", "-A")
|
||||||
|
mustRun(t, root, "git", "commit", "-m", "init")
|
||||||
|
mustRun(t, root, "git", "init", "--bare", remote)
|
||||||
|
mustRun(t, root, "git", "remote", "add", "origin", remote)
|
||||||
|
mustRun(t, root, "git", "push", "-u", "origin", "HEAD")
|
||||||
|
|
||||||
|
binDir := t.TempDir()
|
||||||
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll(bin): %v", err)
|
||||||
|
}
|
||||||
|
nixPath := filepath.Join(binDir, "nix")
|
||||||
|
nixScript := "#!/usr/bin/env bash\nif [[ \"${1-}\" == \"fmt\" ]]; then\n exit 0\nfi\necho \"unexpected nix invocation: $*\" >&2\nexit 1\n"
|
||||||
|
if err := os.WriteFile(nixPath, []byte(nixScript), 0o755); err != nil {
|
||||||
|
t.Fatalf("WriteFile(bin/nix): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &Runner{
|
||||||
|
Config: Config{
|
||||||
|
RootDir: root,
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc", "internal"},
|
||||||
|
Env: append(os.Environ(), "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Run([]string{"patch"}); err != nil {
|
||||||
|
t.Fatalf("Runner.Run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.1\nstable\n0\n")
|
||||||
|
status := strings.TrimSpace(mustOutput(t, root, "git", "status", "--short"))
|
||||||
|
if status != "" {
|
||||||
|
t.Fatalf("git status --short = %q, want empty", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
tagList := strings.TrimSpace(mustOutput(t, root, "git", "tag", "--list", "v1.0.1"))
|
||||||
|
if tagList != "v1.0.1" {
|
||||||
|
t.Fatalf("git tag --list v1.0.1 = %q, want v1.0.1", tagList)
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := strings.TrimSpace(mustOutput(t, root, "git", "branch", "--show-current"))
|
||||||
|
remoteHead := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "origin/"+branch))
|
||||||
|
localHead := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "HEAD"))
|
||||||
|
if remoteHead != localHead {
|
||||||
|
t.Fatalf("origin/%s = %q, want %q", branch, remoteHead, localHead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustParseVersion(t *testing.T, raw string) Version {
|
||||||
|
t.Helper()
|
||||||
|
v, err := ParseVersion(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseVersion(%q): %v", raw, err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustJSON(t *testing.T, value any) string {
|
||||||
|
t.Helper()
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal: %v", err)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRun(t *testing.T, dir string, name string, args ...string) {
|
||||||
|
t.Helper()
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s %s failed: %v\n%s", name, strings.Join(args, " "), err, string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustOutput(t *testing.T, dir string, name string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s %s failed: %v\n%s", name, strings.Join(args, " "), err, string(out))
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFileEquals(t *testing.T, path string, want string) {
|
||||||
|
t.Helper()
|
||||||
|
gotBytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%s): %v", path, err)
|
||||||
|
}
|
||||||
|
if got := string(gotBytes); got != want {
|
||||||
|
t.Fatalf("%s = %q, want %q", path, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
129
packages/release/internal/release/runner.go
Normal file
129
packages/release/internal/release/runner.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
RootDir string
|
||||||
|
AllowedChannels []string
|
||||||
|
ReleaseStepsJSON string
|
||||||
|
PostVersion string
|
||||||
|
Execution ExecutionOptions
|
||||||
|
Env []string
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecutionOptions struct {
|
||||||
|
Commit bool
|
||||||
|
Tag bool
|
||||||
|
Push bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
Config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ExecutionOptions) Normalize() ExecutionOptions {
|
||||||
|
return ExecutionOptions{
|
||||||
|
Commit: true,
|
||||||
|
Tag: true,
|
||||||
|
Push: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(args []string) error {
|
||||||
|
rootDir, err := r.rootDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout := writerOrDiscard(r.Config.Stdout)
|
||||||
|
stderr := writerOrDiscard(r.Config.Stderr)
|
||||||
|
execution := r.Config.Execution.Normalize()
|
||||||
|
|
||||||
|
versionFile, versionPath, err := r.loadVersionFile(rootDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVersion, err := ResolveNextVersion(versionFile.Version, args, r.Config.AllowedChannels)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := requireCleanGit(rootDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
versionFile.Version = nextVersion
|
||||||
|
if err := versionFile.Write(versionPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.runReleaseSteps(rootDir, versionPath, versionFile, nextVersion, stdout, stderr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.runShell(rootDir, versionFile, nextVersion, r.Config.PostVersion, stdout, stderr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.finalizeRelease(rootDir, nextVersion, execution, stdout, stderr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) rootDir() (string, error) {
|
||||||
|
if r.Config.RootDir != "" {
|
||||||
|
return r.Config.RootDir, nil
|
||||||
|
}
|
||||||
|
rootDir, err := gitOutput("", "rev-parse", "--show-toplevel")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(rootDir), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) loadVersionFile(rootDir string) (*VersionFile, string, error) {
|
||||||
|
versionPath := filepath.Join(rootDir, "VERSION")
|
||||||
|
if _, err := os.Stat(versionPath); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("VERSION file not found at %s", versionPath)
|
||||||
|
}
|
||||||
|
versionFile, err := ReadVersionFile(versionPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return versionFile, versionPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) finalizeRelease(rootDir string, version Version, execution ExecutionOptions, stdout io.Writer, stderr io.Writer) error {
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "nix", "fmt"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "add", "-A"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
commitMsg := "chore(release): " + version.Tag()
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "commit", "-m", commitMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "tag", version.Tag()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push", "--tags"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
90
packages/release/internal/release/shell.go
Normal file
90
packages/release/internal/release/shell.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const shellPrelude = `
|
||||||
|
log() { echo "[release] $*" >&2; }
|
||||||
|
version_meta_get() {
|
||||||
|
local key="${1-}"
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ $line == "$key="* ]] && printf '%s\n' "${line#*=}" && return 0
|
||||||
|
done < <(tail -n +4 "$ROOT_DIR/VERSION" 2>/dev/null || true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
version_meta_set() {
|
||||||
|
local key="${1-}"
|
||||||
|
local value="${2-}"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
awk -v key="$key" -v value="$value" '
|
||||||
|
NR <= 3 { print; next }
|
||||||
|
$0 ~ ("^" key "=") { print key "=" value; updated=1; next }
|
||||||
|
{ print }
|
||||||
|
END { if (!updated) print key "=" value }
|
||||||
|
' "$ROOT_DIR/VERSION" >"$tmp"
|
||||||
|
mv "$tmp" "$ROOT_DIR/VERSION"
|
||||||
|
export_version_meta_env
|
||||||
|
}
|
||||||
|
version_meta_unset() {
|
||||||
|
local key="${1-}"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
awk -v key="$key" '
|
||||||
|
NR <= 3 { print; next }
|
||||||
|
$0 ~ ("^" key "=") { next }
|
||||||
|
{ print }
|
||||||
|
' "$ROOT_DIR/VERSION" >"$tmp"
|
||||||
|
mv "$tmp" "$ROOT_DIR/VERSION"
|
||||||
|
export_version_meta_env
|
||||||
|
}
|
||||||
|
export_version_meta_env() {
|
||||||
|
local line key value env_key
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ $line == *=* ]] || continue
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
env_key="$(printf '%s' "$key" | tr -c '[:alnum:]' '_' | tr '[:lower:]' '[:upper:]')"
|
||||||
|
export "VERSION_META_${env_key}=$value"
|
||||||
|
done < <(tail -n +4 "$ROOT_DIR/VERSION" 2>/dev/null || true)
|
||||||
|
}
|
||||||
|
export_version_meta_env
|
||||||
|
`
|
||||||
|
|
||||||
|
func (r *Runner) runShell(rootDir string, versionFile *VersionFile, version Version, script string, stdout io.Writer, stderr io.Writer) error {
|
||||||
|
if strings.TrimSpace(script) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
env := r.shellEnv(rootDir, versionFile, version)
|
||||||
|
_, err := runCommand(rootDir, env, stdout, stderr, "bash", "-euo", "pipefail", "-c", shellPrelude+"\n"+script)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) shellEnv(rootDir string, versionFile *VersionFile, version Version) []string {
|
||||||
|
envMap := buildReleaseEnv(rootDir, versionFile, version, r.Config.Env)
|
||||||
|
env := make([]string, 0, len(envMap))
|
||||||
|
for key, value := range envMap {
|
||||||
|
env = append(env, key+"="+value)
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeMetaEnvName(key string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("VERSION_META_")
|
||||||
|
for _, r := range key {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z':
|
||||||
|
b.WriteRune(r - 32)
|
||||||
|
case r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
|
||||||
|
b.WriteRune(r)
|
||||||
|
default:
|
||||||
|
b.WriteByte('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
10
packages/release/internal/release/slices.go
Normal file
10
packages/release/internal/release/slices.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
func contains(values []string, target string) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
505
packages/release/internal/release/ui.go
Normal file
505
packages/release/internal/release/ui.go
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandOption struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Command string
|
||||||
|
Args []string
|
||||||
|
NextVersion Version
|
||||||
|
Preview string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsInteractiveTerminal(stdin io.Reader, stdout io.Writer) bool {
|
||||||
|
in, inOK := stdin.(*os.File)
|
||||||
|
out, outOK := stdout.(*os.File)
|
||||||
|
if !inOK || !outOK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return term.IsTerminal(int(in.Fd())) && term.IsTerminal(int(out.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectCommand(config Config) ([]string, bool, error) {
|
||||||
|
r := &Runner{Config: config}
|
||||||
|
rootDir, err := r.rootDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
versionFile, _, err := r.loadVersionFile(rootDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := BuildCommandOptions(config, versionFile.Version)
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, false, fmt.Errorf("no release commands available for current version %s", versionFile.Version.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
model := newCommandPickerModel(config, versionFile.Version)
|
||||||
|
finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := finalModel.(commandPickerModel)
|
||||||
|
if !result.confirmed {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return append([]string(nil), result.selected.Args...), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildCommandOptions(config Config, current Version) []CommandOption {
|
||||||
|
var options []CommandOption
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, args := range candidateCommandArgs(current, config.AllowedChannels) {
|
||||||
|
command := formatReleaseCommand(args)
|
||||||
|
if _, exists := seen[command]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
next, err := ResolveNextVersion(current, args, config.AllowedChannels)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, CommandOption{
|
||||||
|
Title: titleForArgs(args),
|
||||||
|
Description: descriptionForArgs(current, args, next),
|
||||||
|
Command: command,
|
||||||
|
Args: append([]string(nil), args...),
|
||||||
|
NextVersion: next,
|
||||||
|
Preview: buildPreview(config, current, args, next),
|
||||||
|
})
|
||||||
|
seen[command] = struct{}{}
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func candidateCommandArgs(current Version, allowedChannels []string) [][]string {
|
||||||
|
candidates := [][]string{
|
||||||
|
{"patch"},
|
||||||
|
{"minor"},
|
||||||
|
{"major"},
|
||||||
|
}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
candidates = append([][]string{{"stable"}}, candidates...)
|
||||||
|
}
|
||||||
|
for _, channel := range allowedChannels {
|
||||||
|
candidates = append(candidates,
|
||||||
|
[]string{channel},
|
||||||
|
[]string{"minor", channel},
|
||||||
|
[]string{"major", channel},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatReleaseCommand(args []string) string {
|
||||||
|
return formatReleaseCommandWithExecution(args, ExecutionOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatReleaseCommandWithExecution(args []string, execution ExecutionOptions) string {
|
||||||
|
var parts []string
|
||||||
|
parts = append(parts, "release")
|
||||||
|
if len(args) == 0 {
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
return strings.Join(append(parts, args...), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func titleForArgs(args []string) string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "Patch release"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(args) {
|
||||||
|
case 1:
|
||||||
|
switch args[0] {
|
||||||
|
case "patch":
|
||||||
|
return "Patch release"
|
||||||
|
case "minor":
|
||||||
|
return "Minor release"
|
||||||
|
case "major":
|
||||||
|
return "Major release"
|
||||||
|
case "stable":
|
||||||
|
return "Promote to stable"
|
||||||
|
default:
|
||||||
|
return strings.ToUpper(args[0][:1]) + args[0][1:] + " prerelease"
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return capitalize(args[0]) + " " + args[1]
|
||||||
|
default:
|
||||||
|
return strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func descriptionForArgs(current Version, args []string, next Version) string {
|
||||||
|
switch len(args) {
|
||||||
|
case 1:
|
||||||
|
switch args[0] {
|
||||||
|
case "patch":
|
||||||
|
return "Bump patch and keep the current channel."
|
||||||
|
case "minor":
|
||||||
|
return "Bump minor and keep the current channel."
|
||||||
|
case "major":
|
||||||
|
return "Bump major and keep the current channel."
|
||||||
|
case "stable":
|
||||||
|
return "Promote the current prerelease to a stable release."
|
||||||
|
default:
|
||||||
|
if current.Channel == args[0] && current.Channel != "stable" {
|
||||||
|
return "Advance the current prerelease number."
|
||||||
|
}
|
||||||
|
return "Switch to the " + args[0] + " channel."
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return fmt.Sprintf("Bump %s and publish to %s.", args[0], args[1])
|
||||||
|
default:
|
||||||
|
return "Release " + next.String() + "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPreview(config Config, current Version, args []string, next Version) string {
|
||||||
|
execution := config.Execution.Normalize()
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines,
|
||||||
|
"Command",
|
||||||
|
" "+formatReleaseCommandWithExecution(args, execution),
|
||||||
|
"",
|
||||||
|
"Version",
|
||||||
|
" Current: "+current.String(),
|
||||||
|
" Next: "+next.String(),
|
||||||
|
" Tag: "+next.Tag(),
|
||||||
|
"",
|
||||||
|
"Flow",
|
||||||
|
" Release steps: "+yesNo(strings.TrimSpace(config.ReleaseStepsJSON) != ""),
|
||||||
|
" Post-version: "+yesNo(strings.TrimSpace(config.PostVersion) != ""),
|
||||||
|
" nix fmt: yes",
|
||||||
|
" git commit: "+yesNo(execution.Commit),
|
||||||
|
" git tag: "+yesNo(execution.Tag),
|
||||||
|
" git push: "+yesNo(execution.Push),
|
||||||
|
)
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func yesNo(v bool) string {
|
||||||
|
if v {
|
||||||
|
return "yes"
|
||||||
|
}
|
||||||
|
return "no"
|
||||||
|
}
|
||||||
|
|
||||||
|
func capitalize(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
runes := []rune(s)
|
||||||
|
first := runes[0]
|
||||||
|
if first >= 'a' && first <= 'z' {
|
||||||
|
runes[0] = first - 32
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandPickerModel struct {
|
||||||
|
config Config
|
||||||
|
current Version
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
focusSection int
|
||||||
|
focusIndex int
|
||||||
|
bumpOptions []selectionOption
|
||||||
|
channelOptions []selectionOption
|
||||||
|
bumpCursor int
|
||||||
|
channelCursor int
|
||||||
|
confirmed bool
|
||||||
|
selected CommandOption
|
||||||
|
err string
|
||||||
|
}
|
||||||
|
|
||||||
|
type selectionOption struct {
|
||||||
|
Label string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommandPickerModel(config Config, current Version) commandPickerModel {
|
||||||
|
return commandPickerModel{
|
||||||
|
config: config,
|
||||||
|
current: current,
|
||||||
|
bumpOptions: buildBumpOptions(current),
|
||||||
|
channelOptions: buildChannelOptions(current, config.AllowedChannels),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q", "esc":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "up", "k", "shift+tab":
|
||||||
|
m.moveFocus(-1)
|
||||||
|
case "down", "j", "tab":
|
||||||
|
m.moveFocus(1)
|
||||||
|
case " ":
|
||||||
|
m.selectFocused()
|
||||||
|
case "enter":
|
||||||
|
option, err := m.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.confirmed = true
|
||||||
|
m.selected = option
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) View() string {
|
||||||
|
if len(m.bumpOptions) == 0 || len(m.channelOptions) == 0 {
|
||||||
|
return "No release commands available.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
preview := m.preview()
|
||||||
|
header := fmt.Sprintf("Release command picker\nCurrent version: %s\nUse up/down to move through options, Space to select, Enter to run, q to cancel.\n", m.current.String())
|
||||||
|
sections := strings.Join([]string{
|
||||||
|
m.renderSection("Bump type", m.bumpOptions, m.bumpCursor, m.focusSection == 0, m.focusedOptionIndex()),
|
||||||
|
"",
|
||||||
|
m.renderSection("Channel", m.channelOptions, m.channelCursor, m.focusSection == 1, m.focusedOptionIndex()),
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
if m.width >= 100 {
|
||||||
|
return header + "\n" + renderColumns(sections, preview, m.width)
|
||||||
|
}
|
||||||
|
return header + "\n" + sections + "\n\n" + preview + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBumpOptions(current Version) []selectionOption {
|
||||||
|
options := []selectionOption{
|
||||||
|
{Label: "Patch", Value: "patch"},
|
||||||
|
{Label: "Minor", Value: "minor"},
|
||||||
|
{Label: "Major", Value: "major"},
|
||||||
|
}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "None",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "None",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildChannelOptions(current Version, allowedChannels []string) []selectionOption {
|
||||||
|
options := []selectionOption{{
|
||||||
|
Label: "Current",
|
||||||
|
Value: current.Channel,
|
||||||
|
}}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "Stable",
|
||||||
|
Value: "stable",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, channel := range allowedChannels {
|
||||||
|
if channel == current.Channel {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: capitalize(channel),
|
||||||
|
Value: channel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *commandPickerModel) moveFocus(delta int) {
|
||||||
|
total := len(m.bumpOptions) + len(m.channelOptions)
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
index := wrapIndex(m.focusIndex+delta, total)
|
||||||
|
m.focusIndex = index
|
||||||
|
|
||||||
|
if index < len(m.bumpOptions) {
|
||||||
|
m.focusSection = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.focusSection = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *commandPickerModel) selectFocused() {
|
||||||
|
if m.focusSection == 0 {
|
||||||
|
m.bumpCursor = m.focusedOptionIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.channelCursor = m.focusedOptionIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapIndex(idx int, size int) int {
|
||||||
|
if size == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for idx < 0 {
|
||||||
|
idx += size
|
||||||
|
}
|
||||||
|
return idx % size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) focusedOptionIndex() int {
|
||||||
|
if m.focusSection == 0 {
|
||||||
|
return m.focusIndex
|
||||||
|
}
|
||||||
|
return m.focusIndex - len(m.bumpOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) renderSection(title string, options []selectionOption, cursor int, focused bool, focusedIndex int) string {
|
||||||
|
lines := []string{title}
|
||||||
|
for i, option := range options {
|
||||||
|
pointer := " "
|
||||||
|
if focused && i == focusedIndex {
|
||||||
|
pointer = ">"
|
||||||
|
}
|
||||||
|
radio := "( )"
|
||||||
|
if i == cursor {
|
||||||
|
radio = "(*)"
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%s %s %s", pointer, radio, option.Label))
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) selectedArgs() []string {
|
||||||
|
bump := m.bumpOptions[m.bumpCursor].Value
|
||||||
|
channel := m.channelOptions[m.channelCursor].Value
|
||||||
|
|
||||||
|
if bump == "" {
|
||||||
|
if channel == "stable" {
|
||||||
|
return []string{"stable"}
|
||||||
|
}
|
||||||
|
if channel == m.current.Channel {
|
||||||
|
if channel == "stable" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{channel}
|
||||||
|
}
|
||||||
|
return []string{channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == m.current.Channel || (channel == "stable" && m.current.Channel == "stable") {
|
||||||
|
return []string{bump}
|
||||||
|
}
|
||||||
|
return []string{bump, channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) selectedOption() (CommandOption, error) {
|
||||||
|
args := m.selectedArgs()
|
||||||
|
next, err := ResolveNextVersion(m.current, args, m.config.AllowedChannels)
|
||||||
|
if err != nil {
|
||||||
|
return CommandOption{}, err
|
||||||
|
}
|
||||||
|
return CommandOption{
|
||||||
|
Title: titleForArgs(args),
|
||||||
|
Description: descriptionForArgs(m.current, args, next),
|
||||||
|
Command: formatReleaseCommand(args),
|
||||||
|
Args: append([]string(nil), args...),
|
||||||
|
NextVersion: next,
|
||||||
|
Preview: buildPreview(m.config, m.current, args, next),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) preview() string {
|
||||||
|
option, err := m.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
lines := []string{
|
||||||
|
"Command",
|
||||||
|
" " + formatReleaseCommand(m.selectedArgs()),
|
||||||
|
"",
|
||||||
|
"Selection",
|
||||||
|
" " + err.Error(),
|
||||||
|
}
|
||||||
|
if m.err != "" {
|
||||||
|
lines = append(lines, "", "Error", " "+m.err)
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
return option.Preview
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderColumns(left string, right string, width int) string {
|
||||||
|
if width < 40 {
|
||||||
|
return left + "\n\n" + right
|
||||||
|
}
|
||||||
|
|
||||||
|
leftWidth := width / 2
|
||||||
|
rightWidth := width - leftWidth - 3
|
||||||
|
leftLines := strings.Split(left, "\n")
|
||||||
|
rightLines := strings.Split(right, "\n")
|
||||||
|
maxLines := len(leftLines)
|
||||||
|
if len(rightLines) > maxLines {
|
||||||
|
maxLines = len(rightLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for i := 0; i < maxLines; i++ {
|
||||||
|
leftLine := ""
|
||||||
|
if i < len(leftLines) {
|
||||||
|
leftLine = leftLines[i]
|
||||||
|
}
|
||||||
|
rightLine := ""
|
||||||
|
if i < len(rightLines) {
|
||||||
|
rightLine = rightLines[i]
|
||||||
|
}
|
||||||
|
b.WriteString(padRight(trimRunes(leftLine, leftWidth), leftWidth))
|
||||||
|
b.WriteString(" | ")
|
||||||
|
b.WriteString(trimRunes(rightLine, rightWidth))
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func padRight(s string, width int) string {
|
||||||
|
missing := width - len([]rune(s))
|
||||||
|
if missing <= 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s + strings.Repeat(" ", missing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimRunes(s string, width int) string {
|
||||||
|
runes := []rune(s)
|
||||||
|
if len(runes) <= width {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if width <= 1 {
|
||||||
|
return string(runes[:width])
|
||||||
|
}
|
||||||
|
if width <= 3 {
|
||||||
|
return string(runes[:width])
|
||||||
|
}
|
||||||
|
return string(runes[:width-3]) + "..."
|
||||||
|
}
|
||||||
212
packages/release/internal/release/ui_test.go
Normal file
212
packages/release/internal/release/ui_test.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildCommandOptionsForStableVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
current := MustParseVersion(t, "1.0.0")
|
||||||
|
options := BuildCommandOptions(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
ReleaseStepsJSON: `[{"kind":"writeFile","path":"VERSION.txt","text":"$FULL_VERSION\n"}]`,
|
||||||
|
PostVersion: "echo post",
|
||||||
|
Execution: ExecutionOptions{
|
||||||
|
Commit: true,
|
||||||
|
Tag: true,
|
||||||
|
Push: true,
|
||||||
|
},
|
||||||
|
}, current)
|
||||||
|
|
||||||
|
want := map[string]string{
|
||||||
|
"release patch": "1.0.1",
|
||||||
|
"release minor": "1.1.0",
|
||||||
|
"release major": "2.0.0",
|
||||||
|
"release alpha": "1.0.1-alpha.1",
|
||||||
|
"release minor beta": "1.1.0-beta.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for command, nextVersion := range want {
|
||||||
|
option, ok := findOptionByCommand(options, command)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected command %q in options", command)
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != nextVersion {
|
||||||
|
t.Fatalf("%s next version = %q, want %q", command, option.NextVersion.String(), nextVersion)
|
||||||
|
}
|
||||||
|
if !strings.Contains(option.Preview, "Release steps: yes") {
|
||||||
|
t.Fatalf("%s preview missing release steps marker:\n%s", command, option.Preview)
|
||||||
|
}
|
||||||
|
if !strings.Contains(option.Preview, "Post-version: yes") {
|
||||||
|
t.Fatalf("%s preview missing post-version marker:\n%s", command, option.Preview)
|
||||||
|
}
|
||||||
|
if !strings.Contains(option.Preview, "git push: yes") {
|
||||||
|
t.Fatalf("%s preview missing git push marker:\n%s", command, option.Preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCommandOptionsForPrereleaseVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
current := MustParseVersion(t, "1.2.3-beta.2")
|
||||||
|
options := BuildCommandOptions(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc"},
|
||||||
|
}, current)
|
||||||
|
|
||||||
|
stableOption, ok := findOptionByCommand(options, "release stable")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected release stable option")
|
||||||
|
}
|
||||||
|
if stableOption.NextVersion.String() != "1.2.3" {
|
||||||
|
t.Fatalf("release stable next version = %q, want 1.2.3", stableOption.NextVersion.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
betaOption, ok := findOptionByCommand(options, "release beta")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected release beta option")
|
||||||
|
}
|
||||||
|
if betaOption.NextVersion.String() != "1.2.3-beta.3" {
|
||||||
|
t.Fatalf("release beta next version = %q, want 1.2.3-beta.3", betaOption.NextVersion.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
patchOption, ok := findOptionByCommand(options, "release patch")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected release patch option")
|
||||||
|
}
|
||||||
|
if patchOption.NextVersion.String() != "1.2.4-beta.1" {
|
||||||
|
t.Fatalf("release patch next version = %q, want 1.2.4-beta.1", patchOption.NextVersion.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerSelectionForStableVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
}, MustParseVersion(t, "1.2.3"))
|
||||||
|
|
||||||
|
model.bumpCursor = 1
|
||||||
|
model.channelCursor = 0
|
||||||
|
|
||||||
|
option, err := model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "minor" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "minor")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.3.0" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.3.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bumpCursor = 3
|
||||||
|
model.channelCursor = 1
|
||||||
|
|
||||||
|
option, err = model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(channel only): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "alpha" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "alpha")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.4-alpha.1" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.4-alpha.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerSelectionForPrereleaseVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc"},
|
||||||
|
}, MustParseVersion(t, "1.2.3-beta.2"))
|
||||||
|
|
||||||
|
model.bumpCursor = 3
|
||||||
|
model.channelCursor = 0
|
||||||
|
|
||||||
|
option, err := model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(current prerelease): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "beta" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "beta")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.3-beta.3" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.3-beta.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.channelCursor = 1
|
||||||
|
|
||||||
|
option, err = model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(promote): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "stable" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "stable")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.3" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bumpCursor = 0
|
||||||
|
|
||||||
|
if _, err := model.selectedOption(); err == nil {
|
||||||
|
t.Fatalf("selectedOption(patch stable from prerelease) succeeded, want error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerFocusMovesAcrossSections(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
}, MustParseVersion(t, "1.2.3"))
|
||||||
|
|
||||||
|
next, _ := model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 0 || model.focusIndex != 1 || model.bumpCursor != 0 {
|
||||||
|
t.Fatalf("after first down: focusSection=%d focusIndex=%d bumpCursor=%d", model.focusSection, model.focusIndex, model.bumpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 1 || model.focusIndex != 4 || model.channelCursor != 0 {
|
||||||
|
t.Fatalf("after moving into channel section: focusSection=%d focusIndex=%d channelCursor=%d", model.focusSection, model.focusIndex, model.channelCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.channelCursor != 0 {
|
||||||
|
t.Fatalf("space changed selection unexpectedly: channelCursor=%d", model.channelCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyUp})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 0 || model.focusIndex != 3 || model.bumpCursor != 0 {
|
||||||
|
t.Fatalf("after moving back up: focusSection=%d focusIndex=%d bumpCursor=%d", model.focusSection, model.focusIndex, model.bumpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.bumpCursor != 3 {
|
||||||
|
t.Fatalf("space did not update bump selection: bumpCursor=%d", model.bumpCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOptionByCommand(options []CommandOption, command string) (CommandOption, bool) {
|
||||||
|
for _, option := range options {
|
||||||
|
if option.Command == command {
|
||||||
|
return option, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CommandOption{}, false
|
||||||
|
}
|
||||||
261
packages/release/internal/release/version.go
Normal file
261
packages/release/internal/release/version.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionPattern = regexp.MustCompile(`^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([A-Za-z]+)\.([0-9]+))?$`)
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
Channel string
|
||||||
|
Prerelease int
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVersion(raw string) (Version, error) {
|
||||||
|
match := versionPattern.FindStringSubmatch(raw)
|
||||||
|
if match == nil {
|
||||||
|
return Version{}, fmt.Errorf("invalid version %q (expected x.y.z or x.y.z-channel.N)", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
major, _ := strconv.Atoi(match[1])
|
||||||
|
minor, _ := strconv.Atoi(match[2])
|
||||||
|
patch, _ := strconv.Atoi(match[3])
|
||||||
|
v := Version{
|
||||||
|
Major: major,
|
||||||
|
Minor: minor,
|
||||||
|
Patch: patch,
|
||||||
|
Channel: "stable",
|
||||||
|
}
|
||||||
|
if match[4] != "" {
|
||||||
|
pre, _ := strconv.Atoi(match[5])
|
||||||
|
v.Channel = match[4]
|
||||||
|
v.Prerelease = pre
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
if v.Channel == "" || v.Channel == "stable" {
|
||||||
|
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d.%d.%d-%s.%d", v.Major, v.Minor, v.Patch, v.Channel, v.Prerelease)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) BaseString() string {
|
||||||
|
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) Tag() string {
|
||||||
|
return "v" + v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) cmp(other Version) int {
|
||||||
|
if v.Major != other.Major {
|
||||||
|
return compareInt(v.Major, other.Major)
|
||||||
|
}
|
||||||
|
if v.Minor != other.Minor {
|
||||||
|
return compareInt(v.Minor, other.Minor)
|
||||||
|
}
|
||||||
|
if v.Patch != other.Patch {
|
||||||
|
return compareInt(v.Patch, other.Patch)
|
||||||
|
}
|
||||||
|
if v.Channel == "stable" && other.Channel != "stable" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if v.Channel != "stable" && other.Channel == "stable" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if v.Channel == "stable" && other.Channel == "stable" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v.Channel != other.Channel {
|
||||||
|
return comparePrerelease(v.Channel, other.Channel)
|
||||||
|
}
|
||||||
|
return compareInt(v.Prerelease, other.Prerelease)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveNextVersion(current Version, args []string, allowedChannels []string) (Version, error) {
|
||||||
|
currentFull := current.String()
|
||||||
|
action := ""
|
||||||
|
rest := args
|
||||||
|
if len(rest) > 0 {
|
||||||
|
action = rest[0]
|
||||||
|
rest = rest[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "set" {
|
||||||
|
return resolveSetVersion(current, currentFull, rest, allowedChannels)
|
||||||
|
}
|
||||||
|
|
||||||
|
part := ""
|
||||||
|
targetChannel := ""
|
||||||
|
wasChannelOnly := false
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "":
|
||||||
|
part = "patch"
|
||||||
|
case "major", "minor", "patch":
|
||||||
|
part = action
|
||||||
|
if len(rest) > 0 {
|
||||||
|
targetChannel = rest[0]
|
||||||
|
rest = rest[1:]
|
||||||
|
}
|
||||||
|
case "stable", "full":
|
||||||
|
if len(rest) > 0 {
|
||||||
|
return Version{}, fmt.Errorf("%q takes no second argument", action)
|
||||||
|
}
|
||||||
|
targetChannel = "stable"
|
||||||
|
default:
|
||||||
|
if contains(allowedChannels, action) {
|
||||||
|
if len(rest) > 0 {
|
||||||
|
return Version{}, errors.New("channel-only bump takes no second argument")
|
||||||
|
}
|
||||||
|
targetChannel = action
|
||||||
|
wasChannelOnly = true
|
||||||
|
} else {
|
||||||
|
return Version{}, fmt.Errorf("unknown argument %q", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetChannel == "" {
|
||||||
|
targetChannel = current.Channel
|
||||||
|
}
|
||||||
|
if err := validateChannel(targetChannel, allowedChannels); err != nil {
|
||||||
|
return Version{}, err
|
||||||
|
}
|
||||||
|
if current.Channel != "stable" && targetChannel == "stable" && action != "stable" && action != "full" {
|
||||||
|
return Version{}, fmt.Errorf("from prerelease channel %q, promote using 'stable' or 'full' only", current.Channel)
|
||||||
|
}
|
||||||
|
if part == "" && wasChannelOnly && current.Channel == "stable" && targetChannel != "stable" {
|
||||||
|
part = "patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
next := current
|
||||||
|
oldBase := next.BaseString()
|
||||||
|
oldChannel := next.Channel
|
||||||
|
oldPre := next.Prerelease
|
||||||
|
if part != "" {
|
||||||
|
bumpVersion(&next, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetChannel == "stable" {
|
||||||
|
next.Channel = "stable"
|
||||||
|
next.Prerelease = 0
|
||||||
|
} else {
|
||||||
|
if next.BaseString() == oldBase && targetChannel == oldChannel && oldPre > 0 {
|
||||||
|
next.Prerelease = oldPre + 1
|
||||||
|
} else {
|
||||||
|
next.Prerelease = 1
|
||||||
|
}
|
||||||
|
next.Channel = targetChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
if next.String() == currentFull {
|
||||||
|
return Version{}, fmt.Errorf("Version %s is already current; nothing to do.", next.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSetVersion(current Version, currentFull string, args []string, allowedChannels []string) (Version, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return Version{}, errors.New("'set' requires a version argument")
|
||||||
|
}
|
||||||
|
next, err := ParseVersion(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return Version{}, err
|
||||||
|
}
|
||||||
|
if err := validateChannel(next.Channel, allowedChannels); err != nil {
|
||||||
|
return Version{}, err
|
||||||
|
}
|
||||||
|
if current.Channel != "stable" && next.Channel == "stable" {
|
||||||
|
return Version{}, fmt.Errorf("from prerelease channel %q, promote using 'stable' or 'full' only", current.Channel)
|
||||||
|
}
|
||||||
|
switch next.cmp(current) {
|
||||||
|
case 0:
|
||||||
|
return Version{}, fmt.Errorf("Version %s is already current; nothing to do.", next.String())
|
||||||
|
case -1:
|
||||||
|
return Version{}, fmt.Errorf("%s is lower than current %s", next.String(), currentFull)
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateChannel(channel string, allowedChannels []string) error {
|
||||||
|
if channel == "" || channel == "stable" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if contains(allowedChannels, channel) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unknown channel %q", channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bumpVersion(v *Version, part string) {
|
||||||
|
switch part {
|
||||||
|
case "major":
|
||||||
|
v.Major++
|
||||||
|
v.Minor = 0
|
||||||
|
v.Patch = 0
|
||||||
|
case "minor":
|
||||||
|
v.Minor++
|
||||||
|
v.Patch = 0
|
||||||
|
case "patch":
|
||||||
|
v.Patch++
|
||||||
|
default:
|
||||||
|
panic("unknown bump part: " + part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareInt(left int, right int) int {
|
||||||
|
switch {
|
||||||
|
case left > right:
|
||||||
|
return 1
|
||||||
|
case left < right:
|
||||||
|
return -1
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparePrerelease(left string, right string) int {
|
||||||
|
values := []string{left, right}
|
||||||
|
sort.Slice(values, func(i int, j int) bool {
|
||||||
|
return semverLikeLess(values[i], values[j])
|
||||||
|
})
|
||||||
|
switch {
|
||||||
|
case left == right:
|
||||||
|
return 0
|
||||||
|
case values[len(values)-1] == left:
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverLikeLess(left string, right string) bool {
|
||||||
|
leftParts := strings.FieldsFunc(left, func(r rune) bool { return r == '.' || r == '-' })
|
||||||
|
rightParts := strings.FieldsFunc(right, func(r rune) bool { return r == '.' || r == '-' })
|
||||||
|
for i := 0; i < len(leftParts) && i < len(rightParts); i++ {
|
||||||
|
li, lerr := strconv.Atoi(leftParts[i])
|
||||||
|
ri, rerr := strconv.Atoi(rightParts[i])
|
||||||
|
switch {
|
||||||
|
case lerr == nil && rerr == nil:
|
||||||
|
if li != ri {
|
||||||
|
return li < ri
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if leftParts[i] != rightParts[i] {
|
||||||
|
return leftParts[i] < rightParts[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(leftParts) < len(rightParts)
|
||||||
|
}
|
||||||
112
packages/release/internal/release/version_file.go
Normal file
112
packages/release/internal/release/version_file.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
lines []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Metadata) Lines() []string {
|
||||||
|
return append([]string(nil), m.lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Metadata) Get(key string) string {
|
||||||
|
for _, line := range m.lines {
|
||||||
|
if strings.HasPrefix(line, key+"=") {
|
||||||
|
return strings.TrimPrefix(line, key+"=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) Set(key string, value string) {
|
||||||
|
for i, line := range m.lines {
|
||||||
|
if strings.HasPrefix(line, key+"=") {
|
||||||
|
m.lines[i] = key + "=" + value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.lines = append(m.lines, key+"="+value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) Unset(key string) {
|
||||||
|
filtered := make([]string, 0, len(m.lines))
|
||||||
|
for _, line := range m.lines {
|
||||||
|
if strings.HasPrefix(line, key+"=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, line)
|
||||||
|
}
|
||||||
|
m.lines = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionFile struct {
|
||||||
|
Version Version
|
||||||
|
Metadata Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f VersionFile) Current() Version {
|
||||||
|
return f.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadVersionFile(path string) (*VersionFile, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
text := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
if len(lines) < 3 {
|
||||||
|
return nil, fmt.Errorf("invalid VERSION file %q", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
base := strings.TrimSpace(lines[0])
|
||||||
|
channel := strings.TrimSpace(lines[1])
|
||||||
|
preRaw := strings.TrimSpace(lines[2])
|
||||||
|
if channel == "" {
|
||||||
|
channel = "stable"
|
||||||
|
}
|
||||||
|
|
||||||
|
rawVersion := base
|
||||||
|
if channel != "stable" {
|
||||||
|
rawVersion = fmt.Sprintf("%s-%s.%s", base, channel, preRaw)
|
||||||
|
}
|
||||||
|
version, err := ParseVersion(rawVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metaLines := make([]string, 0, len(lines)-3)
|
||||||
|
for _, line := range lines[3:] {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metaLines = append(metaLines, line)
|
||||||
|
}
|
||||||
|
return &VersionFile{
|
||||||
|
Version: version,
|
||||||
|
Metadata: Metadata{lines: metaLines},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *VersionFile) Write(path string) error {
|
||||||
|
channel := f.Version.Channel
|
||||||
|
pre := strconv.Itoa(f.Version.Prerelease)
|
||||||
|
if channel == "" || channel == "stable" {
|
||||||
|
channel = "stable"
|
||||||
|
pre = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%s\n%s\n%s\n", f.Version.BaseString(), channel, pre)
|
||||||
|
for _, line := range f.Metadata.lines {
|
||||||
|
fmt.Fprintln(&buf, line)
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, buf.Bytes(), 0o644)
|
||||||
|
}
|
||||||
@@ -1,57 +1,18 @@
|
|||||||
# release.nix
|
|
||||||
{
|
{
|
||||||
pkgs,
|
flake-parts,
|
||||||
postVersion ? "",
|
nixpkgs,
|
||||||
release ? [ ],
|
treefmt-nix,
|
||||||
# Unified list, processed in declaration order:
|
lefthookNix,
|
||||||
# { file = "path/to/file"; content = "..."; } — write file
|
releaseScriptPath ? ./release.sh,
|
||||||
# { run = "shell snippet..."; } — run script
|
shellHookTemplatePath ? ../repo-lib/shell-hook.sh,
|
||||||
channels ? [
|
|
||||||
"alpha"
|
|
||||||
"beta"
|
|
||||||
"rc"
|
|
||||||
"internal"
|
|
||||||
],
|
|
||||||
extraRuntimeInputs ? [ ],
|
|
||||||
}:
|
}:
|
||||||
let
|
import ../repo-lib/lib.nix {
|
||||||
channelList = pkgs.lib.concatStringsSep " " channels;
|
inherit
|
||||||
|
flake-parts
|
||||||
releaseScript = pkgs.lib.concatMapStrings (
|
nixpkgs
|
||||||
entry:
|
treefmt-nix
|
||||||
if entry ? file then
|
lefthookNix
|
||||||
''
|
releaseScriptPath
|
||||||
mkdir -p "$(dirname "${entry.file}")"
|
shellHookTemplatePath
|
||||||
cat > "${entry.file}" << NIXEOF
|
;
|
||||||
${entry.content}
|
|
||||||
NIXEOF
|
|
||||||
log "Generated version file: ${entry.file}"
|
|
||||||
''
|
|
||||||
else if entry ? run then
|
|
||||||
''
|
|
||||||
${entry.run}
|
|
||||||
''
|
|
||||||
else
|
|
||||||
builtins.throw "release entry must have either 'file' or 'run'"
|
|
||||||
) release;
|
|
||||||
|
|
||||||
script =
|
|
||||||
builtins.replaceStrings
|
|
||||||
[ "__CHANNEL_LIST__" "__RELEASE_STEPS__" "__POST_VERSION__" ]
|
|
||||||
[ channelList releaseScript postVersion ]
|
|
||||||
(builtins.readFile ./release.sh);
|
|
||||||
in
|
|
||||||
pkgs.writeShellApplication {
|
|
||||||
name = "release";
|
|
||||||
runtimeInputs =
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
git
|
|
||||||
gnugrep
|
|
||||||
gawk
|
|
||||||
gnused
|
|
||||||
coreutils
|
|
||||||
]
|
|
||||||
++ extraRuntimeInputs;
|
|
||||||
text = script;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,393 +2,18 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
REPO_LIB_RELEASE_ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||||
GITLINT_FILE="$ROOT_DIR/.gitlint"
|
export REPO_LIB_RELEASE_ROOT_DIR
|
||||||
START_HEAD=""
|
export REPO_LIB_RELEASE_CHANNELS='__CHANNEL_LIST__'
|
||||||
CREATED_TAG=""
|
REPO_LIB_RELEASE_STEPS_JSON="$(cat <<'EOF'
|
||||||
|
__RELEASE_STEPS_JSON__
|
||||||
# ── logging ────────────────────────────────────────────────────────────────
|
EOF
|
||||||
|
)"
|
||||||
log() { echo "[release] $*" >&2; }
|
export REPO_LIB_RELEASE_STEPS_JSON
|
||||||
|
REPO_LIB_RELEASE_POST_VERSION="$(cat <<'EOF'
|
||||||
usage() {
|
|
||||||
local cmd
|
|
||||||
cmd="$(basename "$0")"
|
|
||||||
printf '%s\n' \
|
|
||||||
"Usage:" \
|
|
||||||
" ${cmd} [major|minor|patch] [stable|__CHANNEL_LIST__]" \
|
|
||||||
" ${cmd} set <version>" \
|
|
||||||
"" \
|
|
||||||
"Bump types:" \
|
|
||||||
" (none) bump patch, keep current channel" \
|
|
||||||
" major/minor/patch bump the given part, keep current channel" \
|
|
||||||
" stable / full remove prerelease suffix" \
|
|
||||||
" __CHANNEL_LIST__ switch channel (bumps prerelease number if same base+channel)" \
|
|
||||||
"" \
|
|
||||||
"Examples:" \
|
|
||||||
" ${cmd} # patch bump on current channel" \
|
|
||||||
" ${cmd} minor # minor bump on current channel" \
|
|
||||||
" ${cmd} patch beta # patch bump, switch to beta channel" \
|
|
||||||
" ${cmd} rc # switch to rc channel" \
|
|
||||||
" ${cmd} stable # promote to stable release" \
|
|
||||||
" ${cmd} set 1.2.3" \
|
|
||||||
" ${cmd} set 1.2.3-beta.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── git ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_clean_git() {
|
|
||||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
||||||
echo "Error: git working tree is not clean. Commit or stash changes first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
revert_on_failure() {
|
|
||||||
local status=$?
|
|
||||||
if [[ -n $START_HEAD ]]; then
|
|
||||||
log "Release failed — reverting to $START_HEAD"
|
|
||||||
git reset --hard "$START_HEAD"
|
|
||||||
fi
|
|
||||||
if [[ -n $CREATED_TAG ]]; then
|
|
||||||
git tag -d "$CREATED_TAG" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
exit $status
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── version parsing ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
parse_base_version() {
|
|
||||||
local v="$1"
|
|
||||||
if [[ ! $v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
|
||||||
echo "Error: invalid base version '$v' (expected x.y.z)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
MAJOR="${BASH_REMATCH[1]}"
|
|
||||||
MINOR="${BASH_REMATCH[2]}"
|
|
||||||
PATCH="${BASH_REMATCH[3]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_full_version() {
|
|
||||||
local v="$1"
|
|
||||||
CHANNEL="stable"
|
|
||||||
PRERELEASE_NUM=""
|
|
||||||
|
|
||||||
if [[ $v =~ ^([0-9]+\.[0-9]+\.[0-9]+)-([a-zA-Z]+)\.([0-9]+)$ ]]; then
|
|
||||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
|
||||||
CHANNEL="${BASH_REMATCH[2]}"
|
|
||||||
PRERELEASE_NUM="${BASH_REMATCH[3]}"
|
|
||||||
elif [[ $v =~ ^([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
|
||||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
|
||||||
else
|
|
||||||
echo "Error: invalid version '$v' (expected x.y.z or x.y.z-channel.N)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
parse_base_version "$BASE_VERSION"
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_channel() {
|
|
||||||
local ch="$1"
|
|
||||||
[[ $ch == "stable" ]] && return 0
|
|
||||||
local valid_channels="__CHANNEL_LIST__"
|
|
||||||
for c in $valid_channels; do
|
|
||||||
[[ $ch == "$c" ]] && return 0
|
|
||||||
done
|
|
||||||
echo "Error: unknown channel '$ch'. Valid channels: stable $valid_channels" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
version_cmp() {
|
|
||||||
# Returns: 0 if equal, 1 if v1 > v2, 2 if v1 < v2
|
|
||||||
# Stable > prerelease for same base version
|
|
||||||
local v1="$1" v2="$2"
|
|
||||||
[[ $v1 == "$v2" ]] && return 0
|
|
||||||
|
|
||||||
local base1="" pre1="" base2="" pre2=""
|
|
||||||
if [[ $v1 =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(.+)$ ]]; then
|
|
||||||
base1="${BASH_REMATCH[1]}"
|
|
||||||
pre1="${BASH_REMATCH[2]}"
|
|
||||||
else
|
|
||||||
base1="$v1"
|
|
||||||
fi
|
|
||||||
if [[ $v2 =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(.+)$ ]]; then
|
|
||||||
base2="${BASH_REMATCH[1]}"
|
|
||||||
pre2="${BASH_REMATCH[2]}"
|
|
||||||
else
|
|
||||||
base2="$v2"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $base1 != "$base2" ]]; then
|
|
||||||
local highest_base
|
|
||||||
highest_base=$(printf '%s\n%s\n' "$base1" "$base2" | sort -V | tail -n1)
|
|
||||||
[[ $highest_base == "$base1" ]] && return 1 || return 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -z $pre1 && -n $pre2 ]] && return 1 # stable > prerelease
|
|
||||||
[[ -n $pre1 && -z $pre2 ]] && return 2 # prerelease < stable
|
|
||||||
[[ -z $pre1 && -z $pre2 ]] && return 0 # both stable
|
|
||||||
|
|
||||||
local highest_pre
|
|
||||||
highest_pre=$(printf '%s\n%s\n' "$pre1" "$pre2" | sort -V | tail -n1)
|
|
||||||
[[ $highest_pre == "$pre1" ]] && return 1 || return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
bump_base_version() {
|
|
||||||
case "$1" in
|
|
||||||
major)
|
|
||||||
MAJOR=$((MAJOR + 1))
|
|
||||||
MINOR=0
|
|
||||||
PATCH=0
|
|
||||||
;;
|
|
||||||
minor)
|
|
||||||
MINOR=$((MINOR + 1))
|
|
||||||
PATCH=0
|
|
||||||
;;
|
|
||||||
patch) PATCH=$((PATCH + 1)) ;;
|
|
||||||
*)
|
|
||||||
echo "Error: unknown bump part '$1'" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
BASE_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compute_full_version() {
|
|
||||||
if [[ $CHANNEL == "stable" || -z $CHANNEL ]]; then
|
|
||||||
FULL_VERSION="$BASE_VERSION"
|
|
||||||
else
|
|
||||||
FULL_VERSION="${BASE_VERSION}-${CHANNEL}.${PRERELEASE_NUM:-1}"
|
|
||||||
fi
|
|
||||||
FULL_TAG="v$FULL_VERSION"
|
|
||||||
export BASE_VERSION CHANNEL PRERELEASE_NUM FULL_VERSION FULL_TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── gitlint ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
get_gitlint_title_regex() {
|
|
||||||
[[ ! -f $GITLINT_FILE ]] && return 0
|
|
||||||
awk '
|
|
||||||
/^\[title-match-regex\]$/ { in_section=1; next }
|
|
||||||
/^\[/ { in_section=0 }
|
|
||||||
in_section && /^regex=/ { sub(/^regex=/, ""); print; exit }
|
|
||||||
' "$GITLINT_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_commit_message() {
|
|
||||||
local msg="$1"
|
|
||||||
local regex
|
|
||||||
regex="$(get_gitlint_title_regex)"
|
|
||||||
if [[ -n $regex && ! $msg =~ $regex ]]; then
|
|
||||||
echo "Error: commit message does not match .gitlint title-match-regex" >&2
|
|
||||||
echo "Regex: $regex" >&2
|
|
||||||
echo "Message: $msg" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── version file generation ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
run_release_steps() {
|
|
||||||
:
|
|
||||||
__RELEASE_STEPS__
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── version source (built-in) ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Initializes $ROOT_DIR/VERSION from git tags if it doesn't exist.
|
|
||||||
# Must be called outside of any subshell so log output stays on stderr
|
|
||||||
# and never contaminates the stdout of do_read_version.
|
|
||||||
init_version_file() {
|
|
||||||
if [[ -f "$ROOT_DIR/VERSION" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local highest_tag=""
|
|
||||||
while IFS= read -r raw_tag; do
|
|
||||||
local tag="${raw_tag#v}"
|
|
||||||
[[ $tag =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$ ]] || continue
|
|
||||||
|
|
||||||
if [[ -z $highest_tag ]]; then
|
|
||||||
highest_tag="$tag"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local cmp_status=0
|
|
||||||
version_cmp "$tag" "$highest_tag" || cmp_status=$?
|
|
||||||
[[ $cmp_status -eq 1 ]] && highest_tag="$tag"
|
|
||||||
done < <(git tag --list)
|
|
||||||
|
|
||||||
[[ -z $highest_tag ]] && highest_tag="0.0.1"
|
|
||||||
|
|
||||||
parse_full_version "$highest_tag"
|
|
||||||
local channel_to_write="$CHANNEL"
|
|
||||||
local n_to_write="${PRERELEASE_NUM:-1}"
|
|
||||||
if [[ $channel_to_write == "stable" || -z $channel_to_write ]]; then
|
|
||||||
channel_to_write="stable"
|
|
||||||
n_to_write="0"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write" >"$ROOT_DIR/VERSION"
|
|
||||||
log "Initialized $ROOT_DIR/VERSION from highest tag: v$highest_tag"
|
|
||||||
}
|
|
||||||
|
|
||||||
do_read_version() {
|
|
||||||
local base_line channel_line n_line
|
|
||||||
base_line="$(sed -n '1p' "$ROOT_DIR/VERSION" | tr -d '\r')"
|
|
||||||
channel_line="$(sed -n '2p' "$ROOT_DIR/VERSION" | tr -d '\r')"
|
|
||||||
n_line="$(sed -n '3p' "$ROOT_DIR/VERSION" | tr -d '\r')"
|
|
||||||
|
|
||||||
if [[ -z $channel_line || $channel_line == "stable" ]]; then
|
|
||||||
printf '%s\n' "$base_line"
|
|
||||||
else
|
|
||||||
printf '%s-%s.%s\n' "$base_line" "$channel_line" "$n_line"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
do_write_version() {
|
|
||||||
local channel_to_write="$CHANNEL"
|
|
||||||
local n_to_write="${PRERELEASE_NUM:-1}"
|
|
||||||
if [[ $channel_to_write == "stable" || -z $channel_to_write ]]; then
|
|
||||||
channel_to_write="stable"
|
|
||||||
n_to_write="0"
|
|
||||||
fi
|
|
||||||
printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write" >"$ROOT_DIR/VERSION"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── user-provided hook ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
do_post_version() {
|
|
||||||
:
|
|
||||||
__POST_VERSION__
|
__POST_VERSION__
|
||||||
}
|
EOF
|
||||||
|
)"
|
||||||
|
export REPO_LIB_RELEASE_POST_VERSION
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
exec __RELEASE_RUNNER__ "$@"
|
||||||
|
|
||||||
main() {
|
|
||||||
[[ ${1-} == "-h" || ${1-} == "--help" ]] && usage && exit 0
|
|
||||||
|
|
||||||
require_clean_git
|
|
||||||
START_HEAD="$(git rev-parse HEAD)"
|
|
||||||
trap revert_on_failure ERR
|
|
||||||
|
|
||||||
# Initialize VERSION file outside any subshell so log lines never
|
|
||||||
# bleed into the stdout capture below.
|
|
||||||
init_version_file
|
|
||||||
|
|
||||||
local raw_version
|
|
||||||
raw_version="$(do_read_version | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' | tail -n1)"
|
|
||||||
if [[ -z $raw_version ]]; then
|
|
||||||
echo "Error: could not determine current version from VERSION source" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
parse_full_version "$raw_version"
|
|
||||||
|
|
||||||
log "Current: base=$BASE_VERSION channel=$CHANNEL pre=${PRERELEASE_NUM:-}"
|
|
||||||
|
|
||||||
local action="${1-}"
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
if [[ $action == "set" ]]; then
|
|
||||||
local newv="${1-}"
|
|
||||||
[[ -z $newv ]] && echo "Error: 'set' requires a version argument" >&2 && exit 1
|
|
||||||
compute_full_version
|
|
||||||
local current_full="$FULL_VERSION"
|
|
||||||
parse_full_version "$newv"
|
|
||||||
validate_channel "$CHANNEL"
|
|
||||||
compute_full_version
|
|
||||||
local cmp_status=0
|
|
||||||
version_cmp "$FULL_VERSION" "$current_full" || cmp_status=$?
|
|
||||||
case $cmp_status in
|
|
||||||
0)
|
|
||||||
echo "Version $FULL_VERSION is already current; nothing to do." >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
echo "Error: $FULL_VERSION is lower than current $current_full" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
else
|
|
||||||
local part="" target_channel=""
|
|
||||||
|
|
||||||
case "$action" in
|
|
||||||
"") part="patch" ;;
|
|
||||||
major | minor | patch)
|
|
||||||
part="$action"
|
|
||||||
target_channel="${1-}"
|
|
||||||
;;
|
|
||||||
stable | full)
|
|
||||||
[[ -n ${1-} ]] && echo "Error: '$action' takes no second argument" >&2 && usage && exit 1
|
|
||||||
target_channel="stable"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# check if action is a valid channel
|
|
||||||
local is_channel=0
|
|
||||||
for c in __CHANNEL_LIST__; do
|
|
||||||
[[ $action == "$c" ]] && is_channel=1 && break
|
|
||||||
done
|
|
||||||
if [[ $is_channel == 1 ]]; then
|
|
||||||
[[ -n ${1-} ]] && echo "Error: channel-only bump takes no second argument" >&2 && usage && exit 1
|
|
||||||
target_channel="$action"
|
|
||||||
else
|
|
||||||
echo "Error: unknown argument '$action'" >&2
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[[ -z $target_channel ]] && target_channel="$CHANNEL"
|
|
||||||
[[ $target_channel == "full" ]] && target_channel="stable"
|
|
||||||
validate_channel "$target_channel"
|
|
||||||
|
|
||||||
local old_base="$BASE_VERSION" old_channel="$CHANNEL" old_pre="$PRERELEASE_NUM"
|
|
||||||
[[ -n $part ]] && bump_base_version "$part"
|
|
||||||
|
|
||||||
if [[ $target_channel == "stable" ]]; then
|
|
||||||
CHANNEL="stable"
|
|
||||||
PRERELEASE_NUM=""
|
|
||||||
else
|
|
||||||
if [[ $BASE_VERSION == "$old_base" && $target_channel == "$old_channel" && -n $old_pre ]]; then
|
|
||||||
PRERELEASE_NUM=$((old_pre + 1))
|
|
||||||
else
|
|
||||||
PRERELEASE_NUM=1
|
|
||||||
fi
|
|
||||||
CHANNEL="$target_channel"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
compute_full_version
|
|
||||||
log "Releasing $FULL_VERSION"
|
|
||||||
|
|
||||||
do_write_version
|
|
||||||
log "Updated version source"
|
|
||||||
|
|
||||||
run_release_steps
|
|
||||||
log "Release steps done"
|
|
||||||
|
|
||||||
do_post_version
|
|
||||||
log "Post-version hook done"
|
|
||||||
|
|
||||||
(cd "$ROOT_DIR" && nix fmt)
|
|
||||||
log "Formatted files"
|
|
||||||
|
|
||||||
git add -A
|
|
||||||
local commit_msg="chore(release): v$FULL_VERSION"
|
|
||||||
validate_commit_message "$commit_msg"
|
|
||||||
git commit -m "$commit_msg"
|
|
||||||
log "Created commit"
|
|
||||||
|
|
||||||
git tag "$FULL_TAG"
|
|
||||||
CREATED_TAG="$FULL_TAG"
|
|
||||||
log "Tagged $FULL_TAG"
|
|
||||||
|
|
||||||
git push
|
|
||||||
git push --tags
|
|
||||||
log "Done — released $FULL_TAG"
|
|
||||||
|
|
||||||
trap - ERR
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
|
|||||||
100
packages/repo-lib/lib.nix
Normal file
100
packages/repo-lib/lib.nix
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
flake-parts,
|
||||||
|
nixpkgs,
|
||||||
|
treefmt-nix,
|
||||||
|
lefthookNix,
|
||||||
|
releaseScriptPath,
|
||||||
|
shellHookTemplatePath,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
defaults = import ./lib/defaults.nix { };
|
||||||
|
common = import ./lib/common.nix { inherit nixpkgs; };
|
||||||
|
normalizeShellBanner =
|
||||||
|
rawBanner:
|
||||||
|
let
|
||||||
|
banner = defaults.defaultShellBanner // rawBanner;
|
||||||
|
in
|
||||||
|
if
|
||||||
|
!(builtins.elem banner.style [
|
||||||
|
"simple"
|
||||||
|
"pretty"
|
||||||
|
])
|
||||||
|
then
|
||||||
|
throw "repo-lib: config.shell.banner.style must be one of simple or pretty"
|
||||||
|
else
|
||||||
|
banner;
|
||||||
|
toolsModule = import ./lib/tools.nix {
|
||||||
|
lib = common.lib;
|
||||||
|
};
|
||||||
|
hooksModule = import ./lib/hooks.nix {
|
||||||
|
inherit (common) lib sanitizeName;
|
||||||
|
};
|
||||||
|
shellModule = import ./lib/shell.nix {
|
||||||
|
inherit (common)
|
||||||
|
lib
|
||||||
|
;
|
||||||
|
inherit
|
||||||
|
treefmt-nix
|
||||||
|
lefthookNix
|
||||||
|
shellHookTemplatePath
|
||||||
|
;
|
||||||
|
inherit (defaults)
|
||||||
|
defaultShellBanner
|
||||||
|
;
|
||||||
|
inherit normalizeShellBanner;
|
||||||
|
inherit (hooksModule)
|
||||||
|
normalizeLefthookConfig
|
||||||
|
parallelHookStageConfig
|
||||||
|
checkToLefthookConfig
|
||||||
|
hookToLefthookConfig
|
||||||
|
;
|
||||||
|
};
|
||||||
|
releaseModule = import ./lib/release.nix {
|
||||||
|
inherit (common)
|
||||||
|
lib
|
||||||
|
importPkgs
|
||||||
|
;
|
||||||
|
inherit
|
||||||
|
nixpkgs
|
||||||
|
releaseScriptPath
|
||||||
|
;
|
||||||
|
inherit (defaults)
|
||||||
|
defaultReleaseChannels
|
||||||
|
;
|
||||||
|
};
|
||||||
|
repoModule = import ./lib/repo.nix {
|
||||||
|
inherit
|
||||||
|
flake-parts
|
||||||
|
nixpkgs
|
||||||
|
;
|
||||||
|
inherit (common)
|
||||||
|
lib
|
||||||
|
importPkgs
|
||||||
|
duplicateStrings
|
||||||
|
mergeUniqueAttrs
|
||||||
|
;
|
||||||
|
inherit (defaults)
|
||||||
|
supportedSystems
|
||||||
|
defaultReleaseChannels
|
||||||
|
;
|
||||||
|
inherit (toolsModule)
|
||||||
|
normalizeStrictTool
|
||||||
|
;
|
||||||
|
inherit (hooksModule)
|
||||||
|
normalizeLefthookConfig
|
||||||
|
;
|
||||||
|
inherit normalizeShellBanner;
|
||||||
|
inherit (shellModule)
|
||||||
|
buildShellArtifacts
|
||||||
|
;
|
||||||
|
inherit (releaseModule)
|
||||||
|
mkRelease
|
||||||
|
;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
systems.default = defaults.supportedSystems;
|
||||||
|
inherit (toolsModule) tools;
|
||||||
|
inherit (repoModule) normalizeRepoConfig mkRepo;
|
||||||
|
inherit (releaseModule) mkRelease;
|
||||||
|
}
|
||||||
29
packages/repo-lib/lib/common.nix
Normal file
29
packages/repo-lib/lib/common.nix
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{ nixpkgs }:
|
||||||
|
let
|
||||||
|
lib = nixpkgs.lib;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit lib;
|
||||||
|
|
||||||
|
importPkgs = nixpkgsInput: system: import nixpkgsInput { inherit system; };
|
||||||
|
|
||||||
|
duplicateStrings =
|
||||||
|
names:
|
||||||
|
lib.unique (
|
||||||
|
builtins.filter (
|
||||||
|
name: builtins.length (builtins.filter (candidate: candidate == name) names) > 1
|
||||||
|
) names
|
||||||
|
);
|
||||||
|
|
||||||
|
mergeUniqueAttrs =
|
||||||
|
label: left: right:
|
||||||
|
let
|
||||||
|
overlap = builtins.attrNames (lib.intersectAttrs left right);
|
||||||
|
in
|
||||||
|
if overlap != [ ] then
|
||||||
|
throw "repo-lib: duplicate ${label}: ${lib.concatStringsSep ", " overlap}"
|
||||||
|
else
|
||||||
|
left // right;
|
||||||
|
|
||||||
|
sanitizeName = name: lib.strings.sanitizeDerivationName name;
|
||||||
|
}
|
||||||
26
packages/repo-lib/lib/defaults.nix
Normal file
26
packages/repo-lib/lib/defaults.nix
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{ }:
|
||||||
|
{
|
||||||
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultReleaseChannels = [
|
||||||
|
"alpha"
|
||||||
|
"beta"
|
||||||
|
"rc"
|
||||||
|
"internal"
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultShellBanner = {
|
||||||
|
style = "simple";
|
||||||
|
icon = "🚀";
|
||||||
|
title = "Dev shell ready";
|
||||||
|
titleColor = "GREEN";
|
||||||
|
subtitle = "";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
|
borderColor = "BLUE";
|
||||||
|
};
|
||||||
|
}
|
||||||
116
packages/repo-lib/lib/hooks.nix
Normal file
116
packages/repo-lib/lib/hooks.nix
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
sanitizeName,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
hookStageFileArgs =
|
||||||
|
stage: passFilenames:
|
||||||
|
if !passFilenames then
|
||||||
|
""
|
||||||
|
else if stage == "pre-commit" then
|
||||||
|
" {staged_files}"
|
||||||
|
else if stage == "pre-push" then
|
||||||
|
" {push_files}"
|
||||||
|
else if stage == "commit-msg" then
|
||||||
|
" {1}"
|
||||||
|
else
|
||||||
|
throw "repo-lib: unsupported lefthook stage '${stage}'";
|
||||||
|
|
||||||
|
normalizeHookStage =
|
||||||
|
hookName: stage:
|
||||||
|
if
|
||||||
|
builtins.elem stage [
|
||||||
|
"pre-commit"
|
||||||
|
"pre-push"
|
||||||
|
"commit-msg"
|
||||||
|
]
|
||||||
|
then
|
||||||
|
stage
|
||||||
|
else
|
||||||
|
throw "repo-lib: hook '${hookName}' has unsupported stage '${stage}' for lefthook";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit hookStageFileArgs normalizeHookStage;
|
||||||
|
|
||||||
|
checkToLefthookConfig =
|
||||||
|
pkgs: name: rawCheck:
|
||||||
|
let
|
||||||
|
check = {
|
||||||
|
stage = "pre-commit";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [ ];
|
||||||
|
}
|
||||||
|
// rawCheck;
|
||||||
|
wrapperName = "repo-lib-check-${sanitizeName name}";
|
||||||
|
wrapper = pkgs.writeShellApplication {
|
||||||
|
name = wrapperName;
|
||||||
|
runtimeInputs = check.runtimeInputs;
|
||||||
|
text = ''
|
||||||
|
set -euo pipefail
|
||||||
|
${check.command}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
if !(check ? command) then
|
||||||
|
throw "repo-lib: check '${name}' is missing 'command'"
|
||||||
|
else if
|
||||||
|
!(builtins.elem check.stage [
|
||||||
|
"pre-commit"
|
||||||
|
"pre-push"
|
||||||
|
])
|
||||||
|
then
|
||||||
|
throw "repo-lib: check '${name}' has unsupported stage '${check.stage}'"
|
||||||
|
else
|
||||||
|
lib.setAttrByPath [ check.stage "commands" name ] {
|
||||||
|
run = "${wrapper}/bin/${wrapperName}${hookStageFileArgs check.stage check.passFilenames}";
|
||||||
|
};
|
||||||
|
|
||||||
|
normalizeLefthookConfig =
|
||||||
|
label: raw: if builtins.isAttrs raw then raw else throw "repo-lib: ${label} must be an attrset";
|
||||||
|
|
||||||
|
hookToLefthookConfig =
|
||||||
|
name: hook:
|
||||||
|
let
|
||||||
|
supportedFields = [
|
||||||
|
"description"
|
||||||
|
"enable"
|
||||||
|
"entry"
|
||||||
|
"name"
|
||||||
|
"package"
|
||||||
|
"pass_filenames"
|
||||||
|
"stages"
|
||||||
|
];
|
||||||
|
unsupportedFields = builtins.filter (field: !(builtins.elem field supportedFields)) (
|
||||||
|
builtins.attrNames hook
|
||||||
|
);
|
||||||
|
stages = builtins.map (stage: normalizeHookStage name stage) (hook.stages or [ "pre-commit" ]);
|
||||||
|
passFilenames = hook.pass_filenames or false;
|
||||||
|
in
|
||||||
|
if unsupportedFields != [ ] then
|
||||||
|
throw ''
|
||||||
|
repo-lib: hook '${name}' uses unsupported fields for lefthook: ${lib.concatStringsSep ", " unsupportedFields}
|
||||||
|
''
|
||||||
|
else if !(hook ? entry) then
|
||||||
|
throw "repo-lib: hook '${name}' is missing 'entry'"
|
||||||
|
else
|
||||||
|
lib.foldl' lib.recursiveUpdate { } (
|
||||||
|
builtins.map (
|
||||||
|
stage:
|
||||||
|
lib.setAttrByPath [ stage "commands" name ] {
|
||||||
|
run = "${hook.entry}${hookStageFileArgs stage passFilenames}";
|
||||||
|
}
|
||||||
|
) stages
|
||||||
|
);
|
||||||
|
|
||||||
|
parallelHookStageConfig =
|
||||||
|
stage:
|
||||||
|
if
|
||||||
|
builtins.elem stage [
|
||||||
|
"pre-commit"
|
||||||
|
"pre-push"
|
||||||
|
]
|
||||||
|
then
|
||||||
|
lib.setAttrByPath [ stage "parallel" ] true
|
||||||
|
else
|
||||||
|
{ };
|
||||||
|
}
|
||||||
105
packages/repo-lib/lib/release.nix
Normal file
105
packages/repo-lib/lib/release.nix
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
nixpkgs,
|
||||||
|
releaseScriptPath,
|
||||||
|
defaultReleaseChannels,
|
||||||
|
importPkgs,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
normalizeReleaseStep =
|
||||||
|
step:
|
||||||
|
if step ? writeFile then
|
||||||
|
{
|
||||||
|
kind = "writeFile";
|
||||||
|
path = step.writeFile.path;
|
||||||
|
text = step.writeFile.text;
|
||||||
|
}
|
||||||
|
else if step ? replace then
|
||||||
|
{
|
||||||
|
kind = "replace";
|
||||||
|
path = step.replace.path;
|
||||||
|
regex = step.replace.regex;
|
||||||
|
replacement = step.replace.replacement;
|
||||||
|
}
|
||||||
|
else if step ? versionMetaSet then
|
||||||
|
{
|
||||||
|
kind = "versionMetaSet";
|
||||||
|
key = step.versionMetaSet.key;
|
||||||
|
value = step.versionMetaSet.value;
|
||||||
|
}
|
||||||
|
else if step ? versionMetaUnset then
|
||||||
|
{
|
||||||
|
kind = "versionMetaUnset";
|
||||||
|
key = step.versionMetaUnset.key;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw "repo-lib: release step must contain one of writeFile, replace, versionMetaSet, or versionMetaUnset";
|
||||||
|
|
||||||
|
normalizeReleaseConfig =
|
||||||
|
raw:
|
||||||
|
let
|
||||||
|
steps = if raw ? steps then builtins.map normalizeReleaseStep raw.steps else [ ];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
postVersion = raw.postVersion or "";
|
||||||
|
channels = raw.channels or defaultReleaseChannels;
|
||||||
|
runtimeInputs = raw.runtimeInputs or [ ];
|
||||||
|
steps = steps;
|
||||||
|
};
|
||||||
|
|
||||||
|
mkRelease =
|
||||||
|
{
|
||||||
|
system,
|
||||||
|
nixpkgsInput ? nixpkgs,
|
||||||
|
...
|
||||||
|
}@rawArgs:
|
||||||
|
let
|
||||||
|
pkgs = importPkgs nixpkgsInput system;
|
||||||
|
release = normalizeReleaseConfig rawArgs;
|
||||||
|
channelList = lib.concatStringsSep " " release.channels;
|
||||||
|
releaseStepsJson = builtins.toJSON release.steps;
|
||||||
|
releaseRunner = pkgs.buildGoModule {
|
||||||
|
pname = "repo-lib-release-runner";
|
||||||
|
version = "0.0.0";
|
||||||
|
src = ../../release;
|
||||||
|
vendorHash = "sha256-fGFteYruAda2MBHkKgbTeCpIgO30tKCa+tzF6HcUvWM=";
|
||||||
|
subPackages = [ "cmd/release" ];
|
||||||
|
};
|
||||||
|
script =
|
||||||
|
builtins.replaceStrings
|
||||||
|
[
|
||||||
|
"__CHANNEL_LIST__"
|
||||||
|
"__RELEASE_STEPS_JSON__"
|
||||||
|
"__POST_VERSION__"
|
||||||
|
"__RELEASE_RUNNER__"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
channelList
|
||||||
|
releaseStepsJson
|
||||||
|
release.postVersion
|
||||||
|
(lib.getExe' releaseRunner "release")
|
||||||
|
]
|
||||||
|
(builtins.readFile releaseScriptPath);
|
||||||
|
in
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "release";
|
||||||
|
runtimeInputs =
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
git
|
||||||
|
gnugrep
|
||||||
|
gawk
|
||||||
|
gnused
|
||||||
|
coreutils
|
||||||
|
]
|
||||||
|
++ release.runtimeInputs;
|
||||||
|
text = script;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
normalizeReleaseStep
|
||||||
|
normalizeReleaseConfig
|
||||||
|
mkRelease
|
||||||
|
;
|
||||||
|
}
|
||||||
195
packages/repo-lib/lib/repo.nix
Normal file
195
packages/repo-lib/lib/repo.nix
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
{
|
||||||
|
flake-parts,
|
||||||
|
nixpkgs,
|
||||||
|
lib,
|
||||||
|
importPkgs,
|
||||||
|
duplicateStrings,
|
||||||
|
mergeUniqueAttrs,
|
||||||
|
supportedSystems,
|
||||||
|
defaultReleaseChannels,
|
||||||
|
normalizeStrictTool,
|
||||||
|
normalizeLefthookConfig,
|
||||||
|
normalizeShellBanner,
|
||||||
|
buildShellArtifacts,
|
||||||
|
mkRelease,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
normalizeRepoConfig =
|
||||||
|
rawConfig:
|
||||||
|
let
|
||||||
|
merged = lib.recursiveUpdate {
|
||||||
|
includeStandardPackages = true;
|
||||||
|
shell = {
|
||||||
|
env = { };
|
||||||
|
extraShellText = "";
|
||||||
|
allowImpureBootstrap = false;
|
||||||
|
bootstrap = "";
|
||||||
|
banner = { };
|
||||||
|
};
|
||||||
|
formatting = {
|
||||||
|
programs = { };
|
||||||
|
settings = { };
|
||||||
|
};
|
||||||
|
checks = { };
|
||||||
|
lefthook = { };
|
||||||
|
release = null;
|
||||||
|
} rawConfig;
|
||||||
|
release =
|
||||||
|
if merged.release == null then
|
||||||
|
null
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channels = defaultReleaseChannels;
|
||||||
|
steps = [ ];
|
||||||
|
postVersion = "";
|
||||||
|
runtimeInputs = [ ];
|
||||||
|
}
|
||||||
|
// merged.release;
|
||||||
|
in
|
||||||
|
if merged.shell.bootstrap != "" && !merged.shell.allowImpureBootstrap then
|
||||||
|
throw "repo-lib: config.shell.bootstrap requires config.shell.allowImpureBootstrap = true"
|
||||||
|
else
|
||||||
|
merged
|
||||||
|
// {
|
||||||
|
inherit release;
|
||||||
|
shell = merged.shell // {
|
||||||
|
banner = normalizeShellBanner merged.shell.banner;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
buildRepoSystemOutputs =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
system,
|
||||||
|
src,
|
||||||
|
nixpkgsInput,
|
||||||
|
normalizedConfig,
|
||||||
|
userPerSystem,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
perSystemResult = {
|
||||||
|
tools = [ ];
|
||||||
|
shell = { };
|
||||||
|
checks = { };
|
||||||
|
lefthook = { };
|
||||||
|
packages = { };
|
||||||
|
apps = { };
|
||||||
|
}
|
||||||
|
// userPerSystem {
|
||||||
|
inherit pkgs system;
|
||||||
|
lib = nixpkgs.lib;
|
||||||
|
config = normalizedConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
strictTools = builtins.map (tool: normalizeStrictTool pkgs tool) perSystemResult.tools;
|
||||||
|
duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) strictTools);
|
||||||
|
mergedChecks = mergeUniqueAttrs "check" normalizedConfig.checks perSystemResult.checks;
|
||||||
|
mergedLefthookConfig =
|
||||||
|
lib.recursiveUpdate (normalizeLefthookConfig "config.lefthook" normalizedConfig.lefthook)
|
||||||
|
(normalizeLefthookConfig "perSystem.lefthook" (perSystemResult.lefthook or { }));
|
||||||
|
shellConfig = lib.recursiveUpdate normalizedConfig.shell (perSystemResult.shell or { });
|
||||||
|
env =
|
||||||
|
if duplicateToolNames != [ ] then
|
||||||
|
throw "repo-lib: duplicate tool names: ${lib.concatStringsSep ", " duplicateToolNames}"
|
||||||
|
else
|
||||||
|
buildShellArtifacts {
|
||||||
|
inherit
|
||||||
|
pkgs
|
||||||
|
system
|
||||||
|
src
|
||||||
|
;
|
||||||
|
includeStandardPackages = normalizedConfig.includeStandardPackages;
|
||||||
|
formatting = normalizedConfig.formatting;
|
||||||
|
tools = strictTools;
|
||||||
|
checkSpecs = mergedChecks;
|
||||||
|
lefthookConfig = mergedLefthookConfig;
|
||||||
|
shellConfig = shellConfig;
|
||||||
|
extraPackages = perSystemResult.shell.packages or [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
releasePackages =
|
||||||
|
if normalizedConfig.release == null then
|
||||||
|
{ }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release = mkRelease {
|
||||||
|
inherit system;
|
||||||
|
nixpkgsInput = nixpkgsInput;
|
||||||
|
channels = normalizedConfig.release.channels;
|
||||||
|
steps = normalizedConfig.release.steps;
|
||||||
|
postVersion = normalizedConfig.release.postVersion;
|
||||||
|
runtimeInputs = normalizedConfig.release.runtimeInputs;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = env.checks;
|
||||||
|
formatter = env.formatter;
|
||||||
|
shell = env.shell;
|
||||||
|
packages = mergeUniqueAttrs "package" releasePackages perSystemResult.packages;
|
||||||
|
apps = perSystemResult.apps;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit normalizeRepoConfig;
|
||||||
|
|
||||||
|
mkRepo =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
src ? ./.,
|
||||||
|
systems ? supportedSystems,
|
||||||
|
config ? { },
|
||||||
|
perSystem ? (
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
system,
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
}:
|
||||||
|
{ }
|
||||||
|
),
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
normalizedConfig = normalizeRepoConfig config;
|
||||||
|
userPerSystem = perSystem;
|
||||||
|
in
|
||||||
|
flake-parts.lib.mkFlake
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
inherit self nixpkgs;
|
||||||
|
flake-parts = flake-parts;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
inherit systems;
|
||||||
|
|
||||||
|
perSystem =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
system,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
systemOutputs = buildRepoSystemOutputs {
|
||||||
|
inherit
|
||||||
|
pkgs
|
||||||
|
system
|
||||||
|
src
|
||||||
|
normalizedConfig
|
||||||
|
;
|
||||||
|
nixpkgsInput = nixpkgs;
|
||||||
|
userPerSystem = userPerSystem;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = systemOutputs.shell;
|
||||||
|
inherit (systemOutputs)
|
||||||
|
apps
|
||||||
|
checks
|
||||||
|
formatter
|
||||||
|
packages
|
||||||
|
;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
221
packages/repo-lib/lib/shell.nix
Normal file
221
packages/repo-lib/lib/shell.nix
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
treefmt-nix,
|
||||||
|
lefthookNix,
|
||||||
|
shellHookTemplatePath,
|
||||||
|
defaultShellBanner,
|
||||||
|
normalizeShellBanner,
|
||||||
|
normalizeLefthookConfig,
|
||||||
|
parallelHookStageConfig,
|
||||||
|
checkToLefthookConfig,
|
||||||
|
hookToLefthookConfig,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
buildShellHook =
|
||||||
|
{
|
||||||
|
hooksShellHook,
|
||||||
|
shellEnvScript,
|
||||||
|
bootstrap,
|
||||||
|
shellBannerScript,
|
||||||
|
extraShellText,
|
||||||
|
toolLabelWidth,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
template = builtins.readFile shellHookTemplatePath;
|
||||||
|
in
|
||||||
|
builtins.replaceStrings
|
||||||
|
[
|
||||||
|
"@HOOKS_SHELL_HOOK@"
|
||||||
|
"@TOOL_LABEL_WIDTH@"
|
||||||
|
"@SHELL_ENV_SCRIPT@"
|
||||||
|
"@BOOTSTRAP@"
|
||||||
|
"@SHELL_BANNER_SCRIPT@"
|
||||||
|
"@EXTRA_SHELL_TEXT@"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
hooksShellHook
|
||||||
|
(toString toolLabelWidth)
|
||||||
|
shellEnvScript
|
||||||
|
bootstrap
|
||||||
|
shellBannerScript
|
||||||
|
extraShellText
|
||||||
|
]
|
||||||
|
template;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit buildShellHook;
|
||||||
|
|
||||||
|
buildShellArtifacts =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
system,
|
||||||
|
src,
|
||||||
|
includeStandardPackages ? true,
|
||||||
|
formatting,
|
||||||
|
tools ? [ ],
|
||||||
|
shellConfig ? {
|
||||||
|
env = { };
|
||||||
|
extraShellText = "";
|
||||||
|
bootstrap = "";
|
||||||
|
banner = defaultShellBanner;
|
||||||
|
},
|
||||||
|
checkSpecs ? { },
|
||||||
|
rawHookEntries ? { },
|
||||||
|
lefthookConfig ? { },
|
||||||
|
extraPackages ? [ ],
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
standardPackages = with pkgs; [
|
||||||
|
nixfmt
|
||||||
|
gitlint
|
||||||
|
gitleaks
|
||||||
|
shfmt
|
||||||
|
];
|
||||||
|
toolPackages = lib.filter (pkg: pkg != null) (builtins.map (tool: tool.package or null) tools);
|
||||||
|
selectedStandardPackages = lib.optionals includeStandardPackages standardPackages;
|
||||||
|
|
||||||
|
treefmtEval = treefmt-nix.lib.evalModule pkgs {
|
||||||
|
projectRootFile = "flake.nix";
|
||||||
|
programs = {
|
||||||
|
nixfmt.enable = true;
|
||||||
|
}
|
||||||
|
// formatting.programs;
|
||||||
|
settings.formatter = { } // formatting.settings;
|
||||||
|
};
|
||||||
|
treefmtWrapper = treefmtEval.config.build.wrapper;
|
||||||
|
lefthookBinWrapper = pkgs.writeShellScript "lefthook-dumb-term" ''
|
||||||
|
exec env TERM=dumb ${lib.getExe pkgs.lefthook} "$@"
|
||||||
|
'';
|
||||||
|
|
||||||
|
normalizedLefthookConfig = normalizeLefthookConfig "lefthook config" lefthookConfig;
|
||||||
|
lefthookCheck = lefthookNix.lib.${system}.run {
|
||||||
|
inherit src;
|
||||||
|
config = lib.foldl' lib.recursiveUpdate { } (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
output = [
|
||||||
|
"failure"
|
||||||
|
"summary"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
(parallelHookStageConfig "pre-commit")
|
||||||
|
(parallelHookStageConfig "pre-push")
|
||||||
|
(lib.setAttrByPath [ "pre-commit" "commands" "treefmt" ] {
|
||||||
|
run = "${treefmtWrapper}/bin/treefmt --no-cache {staged_files}";
|
||||||
|
stage_fixed = true;
|
||||||
|
})
|
||||||
|
(lib.setAttrByPath [ "pre-commit" "commands" "gitleaks" ] {
|
||||||
|
run = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
|
||||||
|
})
|
||||||
|
(lib.setAttrByPath [ "commit-msg" "commands" "gitlint" ] {
|
||||||
|
run = "${pkgs.gitlint}/bin/gitlint --staged --msg-filename {1}";
|
||||||
|
})
|
||||||
|
]
|
||||||
|
++ lib.mapAttrsToList (name: check: checkToLefthookConfig pkgs name check) checkSpecs
|
||||||
|
++ lib.mapAttrsToList hookToLefthookConfig rawHookEntries
|
||||||
|
++ [ normalizedLefthookConfig ]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
selectedCheckOutputs = {
|
||||||
|
formatting-check = treefmtEval.config.build.check src;
|
||||||
|
hook-check = lefthookCheck;
|
||||||
|
lefthook-check = lefthookCheck;
|
||||||
|
};
|
||||||
|
|
||||||
|
toolNames = builtins.map (tool: tool.name) tools;
|
||||||
|
toolNameWidth =
|
||||||
|
if toolNames == [ ] then
|
||||||
|
0
|
||||||
|
else
|
||||||
|
builtins.foldl' (maxWidth: name: lib.max maxWidth (builtins.stringLength name)) 0 toolNames;
|
||||||
|
toolLabelWidth = toolNameWidth + 1;
|
||||||
|
|
||||||
|
shellEnvScript = lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
name: value: "export ${name}=${lib.escapeShellArg (toString value)}"
|
||||||
|
) shellConfig.env
|
||||||
|
);
|
||||||
|
|
||||||
|
banner = normalizeShellBanner (shellConfig.banner or { });
|
||||||
|
|
||||||
|
shellBannerScript =
|
||||||
|
if banner.style == "pretty" then
|
||||||
|
''
|
||||||
|
repo_lib_print_pretty_header \
|
||||||
|
${lib.escapeShellArg banner.borderColor} \
|
||||||
|
${lib.escapeShellArg banner.titleColor} \
|
||||||
|
${lib.escapeShellArg banner.icon} \
|
||||||
|
${lib.escapeShellArg banner.title} \
|
||||||
|
${lib.escapeShellArg banner.subtitleColor} \
|
||||||
|
${lib.escapeShellArg banner.subtitle}
|
||||||
|
''
|
||||||
|
+ lib.concatMapStrings (tool: ''
|
||||||
|
repo_lib_print_pretty_tool \
|
||||||
|
${lib.escapeShellArg banner.borderColor} \
|
||||||
|
${lib.escapeShellArg tool.name} \
|
||||||
|
${lib.escapeShellArg tool.banner.color} \
|
||||||
|
${lib.escapeShellArg (if tool.banner.icon == null then "" else tool.banner.icon)} \
|
||||||
|
${lib.escapeShellArg (if tool.banner.iconColor == null then "" else tool.banner.iconColor)} \
|
||||||
|
${lib.escapeShellArg (if tool.required then "1" else "0")} \
|
||||||
|
${lib.escapeShellArg (toString tool.version.line)} \
|
||||||
|
${lib.escapeShellArg (toString tool.version.group)} \
|
||||||
|
${lib.escapeShellArg (if tool.version.regex == null then "" else tool.version.regex)} \
|
||||||
|
${lib.escapeShellArg (if tool.version.match == null then "" else tool.version.match)} \
|
||||||
|
${lib.escapeShellArg tool.executable} \
|
||||||
|
${lib.escapeShellArgs tool.version.args}
|
||||||
|
'') tools
|
||||||
|
+ ''
|
||||||
|
repo_lib_print_pretty_footer \
|
||||||
|
${lib.escapeShellArg banner.borderColor}
|
||||||
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
|
repo_lib_print_simple_header \
|
||||||
|
${lib.escapeShellArg banner.titleColor} \
|
||||||
|
${lib.escapeShellArg banner.icon} \
|
||||||
|
${lib.escapeShellArg banner.title} \
|
||||||
|
${lib.escapeShellArg banner.subtitleColor} \
|
||||||
|
${lib.escapeShellArg banner.subtitle}
|
||||||
|
''
|
||||||
|
+ lib.concatMapStrings (tool: ''
|
||||||
|
repo_lib_print_simple_tool \
|
||||||
|
${lib.escapeShellArg tool.name} \
|
||||||
|
${lib.escapeShellArg tool.banner.color} \
|
||||||
|
${lib.escapeShellArg (if tool.banner.icon == null then "" else tool.banner.icon)} \
|
||||||
|
${lib.escapeShellArg (if tool.banner.iconColor == null then "" else tool.banner.iconColor)} \
|
||||||
|
${lib.escapeShellArg (if tool.required then "1" else "0")} \
|
||||||
|
${lib.escapeShellArg (toString tool.version.line)} \
|
||||||
|
${lib.escapeShellArg (toString tool.version.group)} \
|
||||||
|
${lib.escapeShellArg (if tool.version.regex == null then "" else tool.version.regex)} \
|
||||||
|
${lib.escapeShellArg (if tool.version.match == null then "" else tool.version.match)} \
|
||||||
|
${lib.escapeShellArg tool.executable} \
|
||||||
|
${lib.escapeShellArgs tool.version.args}
|
||||||
|
'') tools
|
||||||
|
+ ''
|
||||||
|
printf "\n"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = selectedCheckOutputs;
|
||||||
|
formatter = treefmtWrapper;
|
||||||
|
shell = pkgs.mkShell {
|
||||||
|
LEFTHOOK_BIN = builtins.toString lefthookBinWrapper;
|
||||||
|
packages = lib.unique (
|
||||||
|
selectedStandardPackages
|
||||||
|
++ extraPackages
|
||||||
|
++ toolPackages
|
||||||
|
++ [
|
||||||
|
pkgs.lefthook
|
||||||
|
treefmtWrapper
|
||||||
|
]
|
||||||
|
);
|
||||||
|
shellHook = buildShellHook {
|
||||||
|
hooksShellHook = lefthookCheck.shellHook;
|
||||||
|
inherit toolLabelWidth shellEnvScript shellBannerScript;
|
||||||
|
bootstrap = shellConfig.bootstrap;
|
||||||
|
extraShellText = shellConfig.extraShellText;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// selectedCheckOutputs;
|
||||||
|
}
|
||||||
90
packages/repo-lib/lib/tools.nix
Normal file
90
packages/repo-lib/lib/tools.nix
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
normalizeStrictTool =
|
||||||
|
pkgs: tool:
|
||||||
|
let
|
||||||
|
version = {
|
||||||
|
args = [ "--version" ];
|
||||||
|
match = null;
|
||||||
|
regex = null;
|
||||||
|
group = 0;
|
||||||
|
line = 1;
|
||||||
|
}
|
||||||
|
// (tool.version or { });
|
||||||
|
banner = {
|
||||||
|
color = "YELLOW";
|
||||||
|
icon = null;
|
||||||
|
iconColor = null;
|
||||||
|
}
|
||||||
|
// (tool.banner or { });
|
||||||
|
executable =
|
||||||
|
if tool ? command && tool.command != null then
|
||||||
|
tool.command
|
||||||
|
else if tool ? exe && tool.exe != null then
|
||||||
|
"${lib.getExe' tool.package tool.exe}"
|
||||||
|
else
|
||||||
|
"${lib.getExe tool.package}";
|
||||||
|
in
|
||||||
|
if !(tool ? command && tool.command != null) && !(tool ? package) then
|
||||||
|
throw "repo-lib: tool '${tool.name or "<unnamed>"}' is missing 'package' or 'command'"
|
||||||
|
else
|
||||||
|
{
|
||||||
|
kind = "strict";
|
||||||
|
inherit executable version banner;
|
||||||
|
name = tool.name;
|
||||||
|
package = tool.package or null;
|
||||||
|
required = tool.required or true;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit normalizeStrictTool;
|
||||||
|
|
||||||
|
tools = rec {
|
||||||
|
fromPackage =
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
package,
|
||||||
|
exe ? null,
|
||||||
|
version ? { },
|
||||||
|
banner ? { },
|
||||||
|
required ? true,
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
name
|
||||||
|
package
|
||||||
|
exe
|
||||||
|
version
|
||||||
|
banner
|
||||||
|
required
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
fromCommand =
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
version ? { },
|
||||||
|
banner ? { },
|
||||||
|
required ? true,
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
name
|
||||||
|
command
|
||||||
|
version
|
||||||
|
banner
|
||||||
|
required
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
simple =
|
||||||
|
name: package: args:
|
||||||
|
fromPackage {
|
||||||
|
inherit name package;
|
||||||
|
version.args = args;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
233
packages/repo-lib/shell-hook.sh
Normal file
233
packages/repo-lib/shell-hook.sh
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
@HOOKS_SHELL_HOOK@
|
||||||
|
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
command -v tput >/dev/null 2>&1 && tput clear || printf '\033c'
|
||||||
|
fi
|
||||||
|
|
||||||
|
GREEN=$'\033[1;32m'
|
||||||
|
CYAN=$'\033[1;36m'
|
||||||
|
YELLOW=$'\033[1;33m'
|
||||||
|
BLUE=$'\033[1;34m'
|
||||||
|
RED=$'\033[1;31m'
|
||||||
|
MAGENTA=$'\033[1;35m'
|
||||||
|
WHITE=$'\033[1;37m'
|
||||||
|
GRAY=$'\033[0;90m'
|
||||||
|
BOLD=$'\033[1m'
|
||||||
|
UNDERLINE=$'\033[4m'
|
||||||
|
RESET=$'\033[0m'
|
||||||
|
|
||||||
|
REPO_LIB_TOOL_VERSION=""
|
||||||
|
REPO_LIB_TOOL_ERROR=""
|
||||||
|
|
||||||
|
repo_lib_capture_tool() {
|
||||||
|
local required="$1"
|
||||||
|
local line_no="$2"
|
||||||
|
local group_no="$3"
|
||||||
|
local regex="$4"
|
||||||
|
local match_regex="$5"
|
||||||
|
local executable="$6"
|
||||||
|
shift 6
|
||||||
|
|
||||||
|
local output=""
|
||||||
|
local selected=""
|
||||||
|
local version=""
|
||||||
|
|
||||||
|
REPO_LIB_TOOL_VERSION=""
|
||||||
|
REPO_LIB_TOOL_ERROR=""
|
||||||
|
|
||||||
|
if ! output="$("$executable" "$@" 2>&1)"; then
|
||||||
|
REPO_LIB_TOOL_ERROR="probe failed"
|
||||||
|
printf "%s\n" "$output" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$match_regex" ]; then
|
||||||
|
selected="$(printf '%s\n' "$output" | grep -E -m 1 "$match_regex" || true)"
|
||||||
|
else
|
||||||
|
selected="$(printf '%s\n' "$output" | sed -n "${line_no}p")"
|
||||||
|
fi
|
||||||
|
selected="$(printf '%s' "$selected" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
|
||||||
|
if [ -n "$regex" ]; then
|
||||||
|
if [[ "$selected" =~ $regex ]]; then
|
||||||
|
version="${BASH_REMATCH[$group_no]}"
|
||||||
|
else
|
||||||
|
REPO_LIB_TOOL_ERROR="version parse failed"
|
||||||
|
printf "%s\n" "$output" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
version="$selected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
REPO_LIB_TOOL_ERROR="empty version"
|
||||||
|
printf "%s\n" "$output" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_LIB_TOOL_VERSION="$version"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_lib_print_simple_header() {
|
||||||
|
local title_color_name="$1"
|
||||||
|
local icon="$2"
|
||||||
|
local title="$3"
|
||||||
|
local subtitle_color_name="$4"
|
||||||
|
local subtitle="$5"
|
||||||
|
|
||||||
|
local title_color="${!title_color_name:-$GREEN}"
|
||||||
|
local subtitle_color="${!subtitle_color_name:-$GRAY}"
|
||||||
|
|
||||||
|
printf "\n%s" "$title_color"
|
||||||
|
if [ -n "$icon" ]; then
|
||||||
|
printf "%s " "$icon"
|
||||||
|
fi
|
||||||
|
printf "%s%s" "$title" "$RESET"
|
||||||
|
if [ -n "$subtitle" ]; then
|
||||||
|
printf " %s%s%s" "$subtitle_color" "$subtitle" "$RESET"
|
||||||
|
fi
|
||||||
|
printf "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_lib_print_simple_tool() {
|
||||||
|
local name="$1"
|
||||||
|
local color_name="$2"
|
||||||
|
local icon="$3"
|
||||||
|
local icon_color_name="$4"
|
||||||
|
local required="$5"
|
||||||
|
local line_no="$6"
|
||||||
|
local group_no="$7"
|
||||||
|
local regex="$8"
|
||||||
|
local match_regex="$9"
|
||||||
|
local executable="${10}"
|
||||||
|
shift 10
|
||||||
|
|
||||||
|
local color="${!color_name:-$YELLOW}"
|
||||||
|
local effective_icon_color_name="$icon_color_name"
|
||||||
|
local icon_color=""
|
||||||
|
|
||||||
|
if [ -z "$effective_icon_color_name" ]; then
|
||||||
|
effective_icon_color_name="$color_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if repo_lib_capture_tool "$required" "$line_no" "$group_no" "$regex" "$match_regex" "$executable" "$@"; then
|
||||||
|
icon_color="${!effective_icon_color_name:-$color}"
|
||||||
|
printf " "
|
||||||
|
if [ -n "$icon" ]; then
|
||||||
|
printf "%s%s%s " "$icon_color" "$icon" "$RESET"
|
||||||
|
fi
|
||||||
|
printf "$CYAN %-@TOOL_LABEL_WIDTH@s$RESET %s%s$RESET\n" "${name}:" "$color" "$REPO_LIB_TOOL_VERSION"
|
||||||
|
else
|
||||||
|
printf " "
|
||||||
|
if [ -n "$icon" ]; then
|
||||||
|
printf "%s%s%s " "$RED" "$icon" "$RESET"
|
||||||
|
fi
|
||||||
|
printf "$CYAN %-@TOOL_LABEL_WIDTH@s$RESET $RED%s$RESET\n" "${name}:" "$REPO_LIB_TOOL_ERROR"
|
||||||
|
if [ "$required" = "1" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_lib_print_pretty_header() {
|
||||||
|
local border_color_name="$1"
|
||||||
|
local title_color_name="$2"
|
||||||
|
local icon="$3"
|
||||||
|
local title="$4"
|
||||||
|
local subtitle_color_name="$5"
|
||||||
|
local subtitle="$6"
|
||||||
|
|
||||||
|
local border_color="${!border_color_name:-$BLUE}"
|
||||||
|
local title_color="${!title_color_name:-$GREEN}"
|
||||||
|
local subtitle_color="${!subtitle_color_name:-$GRAY}"
|
||||||
|
|
||||||
|
printf "\n%s╭─%s %s" "$border_color" "$RESET" "$title_color"
|
||||||
|
if [ -n "$icon" ]; then
|
||||||
|
printf "%s " "$icon"
|
||||||
|
fi
|
||||||
|
printf "%s%s" "$title" "$RESET"
|
||||||
|
if [ -n "$subtitle" ]; then
|
||||||
|
printf " %s%s%s" "$subtitle_color" "$subtitle" "$RESET"
|
||||||
|
fi
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_lib_print_pretty_row() {
|
||||||
|
local border_color_name="$1"
|
||||||
|
local icon="$2"
|
||||||
|
local icon_color_name="$3"
|
||||||
|
local label="$4"
|
||||||
|
local value="$5"
|
||||||
|
local value_color_name="$6"
|
||||||
|
|
||||||
|
local border_color="${!border_color_name:-$BLUE}"
|
||||||
|
local icon_color="${!icon_color_name:-$WHITE}"
|
||||||
|
local value_color="${!value_color_name:-$YELLOW}"
|
||||||
|
|
||||||
|
if [ -z "$icon" ]; then
|
||||||
|
icon="•"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%s│%s %s%s%s ${WHITE}%-@TOOL_LABEL_WIDTH@s${RESET} %s%s${RESET}\n" \
|
||||||
|
"$border_color" "$RESET" "$icon_color" "$icon" "$RESET" "$label" "$value_color" "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_lib_print_pretty_tool() {
|
||||||
|
local border_color_name="$1"
|
||||||
|
local name="$2"
|
||||||
|
local color_name="$3"
|
||||||
|
local icon="$4"
|
||||||
|
local icon_color_name="$5"
|
||||||
|
local required="$6"
|
||||||
|
local line_no="$7"
|
||||||
|
local group_no="$8"
|
||||||
|
local regex="$9"
|
||||||
|
local match_regex="${10}"
|
||||||
|
local executable="${11}"
|
||||||
|
shift 11
|
||||||
|
|
||||||
|
local effective_icon_color_name="$icon_color_name"
|
||||||
|
local value_color_name="$color_name"
|
||||||
|
local value=""
|
||||||
|
|
||||||
|
if [ -z "$effective_icon_color_name" ]; then
|
||||||
|
effective_icon_color_name="$color_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if repo_lib_capture_tool "$required" "$line_no" "$group_no" "$regex" "$match_regex" "$executable" "$@"; then
|
||||||
|
value="$REPO_LIB_TOOL_VERSION"
|
||||||
|
else
|
||||||
|
value="$REPO_LIB_TOOL_ERROR"
|
||||||
|
effective_icon_color_name="RED"
|
||||||
|
value_color_name="RED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_lib_print_pretty_row \
|
||||||
|
"$border_color_name" \
|
||||||
|
"$icon" \
|
||||||
|
"$effective_icon_color_name" \
|
||||||
|
"$name" \
|
||||||
|
"$value" \
|
||||||
|
"$value_color_name"
|
||||||
|
|
||||||
|
if [ "$value_color_name" = "RED" ] && [ "$required" = "1" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_lib_print_pretty_footer() {
|
||||||
|
local border_color_name="$1"
|
||||||
|
local border_color="${!border_color_name:-$BLUE}"
|
||||||
|
|
||||||
|
printf "%s╰─%s\n\n" "$border_color" "$RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
@SHELL_ENV_SCRIPT@
|
||||||
|
|
||||||
|
@BOOTSTRAP@
|
||||||
|
|
||||||
|
@SHELL_BANNER_SCRIPT@
|
||||||
|
|
||||||
|
@EXTRA_SHELL_TEXT@
|
||||||
54
skills/repo-lib-consumer/SKILL.md
Normal file
54
skills/repo-lib-consumer/SKILL.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: repo-lib-consumer
|
||||||
|
description: Edit or extend repos that consume `repo-lib` through `repo-lib.lib.mkRepo`, `repo-lib.lib.mkRelease`, or a template generated from this library. Use when Codex needs to add or change tools, shell banner or bootstrap behavior, shell packages, checks, raw lefthook config, formatters, release steps, version metadata, release channels, or release automation in a Nix flake built on repo-lib.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Repo Lib Consumer
|
||||||
|
|
||||||
|
Use this skill when changing a repo that already depends on `repo-lib`.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Detect the integration style.
|
||||||
|
Search for `repo-lib.lib.mkRepo`, `repo-lib.lib.mkRelease`, or `inputs.repo-lib`.
|
||||||
|
|
||||||
|
2. Prefer the repo's current abstraction level.
|
||||||
|
If the repo uses `mkRepo`, keep edits inside `config` and `perSystem`.
|
||||||
|
If the repo uses `mkRelease` directly, preserve that style unless the user asked to migrate.
|
||||||
|
|
||||||
|
3. Load the right reference before editing.
|
||||||
|
Read `references/api.md` for exact option names, merge points, generated outputs, hook limitations, and release behavior.
|
||||||
|
Read `references/recipes.md` for concrete change patterns such as adding a tool, adding a check, wiring `commit-msg`, changing the banner, or updating release-managed files.
|
||||||
|
|
||||||
|
4. Follow repo-lib conventions.
|
||||||
|
Add bannered CLIs through `perSystem.tools`, not `shell.packages`.
|
||||||
|
Use `shell.packages` for packages that should exist in the shell but not in the banner.
|
||||||
|
Keep shells pure-first; only use `bootstrap` with `allowImpureBootstrap = true`.
|
||||||
|
Prefer `config.checks` for simple `pre-commit` and `pre-push` commands.
|
||||||
|
Use raw `config.lefthook` or `perSystem.lefthook` when the task needs `commit-msg` or extra lefthook fields.
|
||||||
|
Prefer structured `release.steps` over shell hooks; current step kinds are `writeFile`, `replace`, `versionMetaSet`, and `versionMetaUnset`.
|
||||||
|
|
||||||
|
5. Verify after edits.
|
||||||
|
Run `nix flake show --json`.
|
||||||
|
Run `nix flake check` when feasible.
|
||||||
|
If local flake evaluation cannot see newly created files because the repo is loaded as a git flake, stage the new files before rerunning checks.
|
||||||
|
|
||||||
|
## Decision Rules
|
||||||
|
|
||||||
|
- Prefer `repo-lib.lib.tools.fromPackage` for packaged CLIs and `fromCommand` only when the tool should come from the host environment.
|
||||||
|
- Use `repo-lib.lib.tools.simple` only for very small package-backed probes that only need `version.args`.
|
||||||
|
- Required tools fail shell startup if their probe fails. Do not mark a tool optional unless that is intentional.
|
||||||
|
- `config.checks` supports only `pre-commit` and `pre-push`. `commit-msg` must go through raw lefthook config.
|
||||||
|
- Generated checks include `formatting-check`, `hook-check`, and `lefthook-check`.
|
||||||
|
- `config.shell.banner.style` must be `simple` or `pretty`.
|
||||||
|
- Treat `postVersion` as pre-format, pre-commit, pre-tag, and pre-push.
|
||||||
|
- Do not model a true post-tag webhook inside `repo-lib`; prefer CI triggered by tag push.
|
||||||
|
- The current generated `release` command is destructive and opinionated: it formats, stages, commits, tags, and pushes as part of the flow. Document that clearly when editing consumer release automation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/api.md`
|
||||||
|
Use for the exact consumer API, generated outputs, hook and banner behavior, and current release semantics.
|
||||||
|
|
||||||
|
- `references/recipes.md`
|
||||||
|
Use for common changes: add a tool, add a check, add a `commit-msg` hook, customize the shell banner, or update release-managed files.
|
||||||
4
skills/repo-lib-consumer/agents/openai.yaml
Normal file
4
skills/repo-lib-consumer/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Repo Lib Consumer"
|
||||||
|
short_description: "Edit mkRepo or mkRelease consumers safely"
|
||||||
|
default_prompt: "Use $repo-lib-consumer to update a repo that consumes repo-lib through mkRepo, mkRelease, or the repo-lib template."
|
||||||
381
skills/repo-lib-consumer/references/api.md
Normal file
381
skills/repo-lib-consumer/references/api.md
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# repo-lib Consumer API
|
||||||
|
|
||||||
|
## Detect the repo shape
|
||||||
|
|
||||||
|
Look for one of these patterns in the consuming repo:
|
||||||
|
|
||||||
|
- `repo-lib.lib.mkRepo`
|
||||||
|
- `repo-lib.lib.mkRelease`
|
||||||
|
- `inputs.repo-lib`
|
||||||
|
|
||||||
|
Prefer editing the existing style instead of migrating incidentally.
|
||||||
|
|
||||||
|
## Preferred `mkRepo` shape
|
||||||
|
|
||||||
|
```nix
|
||||||
|
repo-lib.lib.mkRepo {
|
||||||
|
inherit self nixpkgs;
|
||||||
|
src = ./.;
|
||||||
|
systems = repo-lib.lib.systems.default; # optional
|
||||||
|
|
||||||
|
config = {
|
||||||
|
includeStandardPackages = true;
|
||||||
|
|
||||||
|
shell = {
|
||||||
|
env = { };
|
||||||
|
extraShellText = "";
|
||||||
|
allowImpureBootstrap = false;
|
||||||
|
bootstrap = "";
|
||||||
|
banner = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
formatting = {
|
||||||
|
programs = { };
|
||||||
|
settings = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
checks = { };
|
||||||
|
lefthook = { };
|
||||||
|
|
||||||
|
release = null; # or attrset below
|
||||||
|
};
|
||||||
|
|
||||||
|
perSystem = { pkgs, system, lib, config }: {
|
||||||
|
tools = [ ];
|
||||||
|
shell.packages = [ ];
|
||||||
|
checks = { };
|
||||||
|
lefthook = { };
|
||||||
|
packages = { };
|
||||||
|
apps = { };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated outputs:
|
||||||
|
|
||||||
|
- `devShells.${system}.default`
|
||||||
|
- `checks.${system}.formatting-check`
|
||||||
|
- `checks.${system}.hook-check`
|
||||||
|
- `checks.${system}.lefthook-check`
|
||||||
|
- `formatter.${system}`
|
||||||
|
- `packages.${system}.release` when `config.release != null`
|
||||||
|
- merged `packages` and `apps` from `perSystem`
|
||||||
|
|
||||||
|
Merge points:
|
||||||
|
|
||||||
|
- `config.checks` merges with `perSystem.checks`
|
||||||
|
- `config.lefthook` recursively merges with `perSystem.lefthook`
|
||||||
|
- `config.shell` recursively merges with `perSystem.shell`
|
||||||
|
- generated release packages merge with `perSystem.packages`
|
||||||
|
|
||||||
|
Conflicts on `checks`, `packages`, and `apps` names throw.
|
||||||
|
|
||||||
|
## `config.includeStandardPackages`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
When enabled, the shell includes:
|
||||||
|
|
||||||
|
- `nixfmt`
|
||||||
|
- `gitlint`
|
||||||
|
- `gitleaks`
|
||||||
|
- `shfmt`
|
||||||
|
|
||||||
|
Use `false` only when the consumer explicitly wants to own the full shell package list.
|
||||||
|
|
||||||
|
## `config.shell`
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
- `env`
|
||||||
|
Attrset of environment variables exported in the shell.
|
||||||
|
- `extraShellText`
|
||||||
|
Extra shell snippet appended after the banner.
|
||||||
|
- `bootstrap`
|
||||||
|
Shell snippet that runs before the banner.
|
||||||
|
- `allowImpureBootstrap`
|
||||||
|
Must be `true` when `bootstrap` is non-empty.
|
||||||
|
- `banner`
|
||||||
|
Shell banner configuration.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Default is pure-first.
|
||||||
|
- Do not add bootstrap work unless the user actually wants imperative local setup.
|
||||||
|
- The template uses bootstrap intentionally for Bun global install paths and Moon bootstrapping; do not generalize that into normal package setup unless the repo already wants that behavior.
|
||||||
|
|
||||||
|
### `config.shell.banner`
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
style = "simple";
|
||||||
|
icon = "🚀";
|
||||||
|
title = "Dev shell ready";
|
||||||
|
titleColor = "GREEN";
|
||||||
|
subtitle = "";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
|
borderColor = "BLUE";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `style` must be `simple` or `pretty`.
|
||||||
|
- `borderColor` matters only for `pretty`.
|
||||||
|
- Tool rows can also set `banner.color`, `banner.icon`, and `banner.iconColor`.
|
||||||
|
- Required tool probe failures abort shell startup.
|
||||||
|
|
||||||
|
## `config.formatting`
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
- `programs`
|
||||||
|
Passed to `treefmt-nix.lib.evalModule`.
|
||||||
|
- `settings`
|
||||||
|
Passed to `settings.formatter`.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `nixfmt` is always enabled.
|
||||||
|
- Use formatter settings instead of shell hooks for formatting behavior.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
`config.checks.<name>` and `perSystem.checks.<name>` use this shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
command = "bun test";
|
||||||
|
stage = "pre-push"; # or "pre-commit"
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [ pkgs.bun ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
- `stage = "pre-commit"`
|
||||||
|
- `passFilenames = false`
|
||||||
|
- `runtimeInputs = [ ]`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Only `pre-commit` and `pre-push` are supported here.
|
||||||
|
- The command is wrapped with `writeShellApplication`.
|
||||||
|
- `pre-commit` and `pre-push` stages are configured to run in parallel.
|
||||||
|
- `passFilenames = true` maps to `{staged_files}` for `pre-commit` and `{push_files}` for `pre-push`.
|
||||||
|
|
||||||
|
## Raw Lefthook config
|
||||||
|
|
||||||
|
Use `config.lefthook` or `perSystem.lefthook` when the task needs advanced Lefthook features or unsupported stages.
|
||||||
|
|
||||||
|
Pass-through attrset example:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
checks.tests = {
|
||||||
|
command = "bun test";
|
||||||
|
stage = "pre-push";
|
||||||
|
runtimeInputs = [ pkgs.bun ];
|
||||||
|
};
|
||||||
|
|
||||||
|
lefthook.pre-push.commands.tests.stage_fixed = true;
|
||||||
|
|
||||||
|
lefthook.commit-msg.commands.commitlint = {
|
||||||
|
run = "bun commitlint --edit {1}";
|
||||||
|
stage_fixed = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Structured hook-entry example in a raw hook list:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
perSystem = { pkgs, ... }: {
|
||||||
|
lefthook.biome = {
|
||||||
|
entry = "${pkgs.biome}/bin/biome check";
|
||||||
|
pass_filenames = true;
|
||||||
|
stages = [ "pre-commit" "pre-push" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `config.lefthook` and `perSystem.lefthook` are recursive attrset passthroughs merged after generated checks.
|
||||||
|
- Structured hook entries support only:
|
||||||
|
`description`, `enable`, `entry`, `name`, `package`, `pass_filenames`, `stages`
|
||||||
|
- `stages` may include `pre-commit`, `pre-push`, or `commit-msg`.
|
||||||
|
- `pass_filenames = true` maps to `{1}` for `commit-msg`.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
Preferred shape in `perSystem.tools`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
(repo-lib.lib.tools.fromPackage {
|
||||||
|
name = "Bun";
|
||||||
|
package = pkgs.bun;
|
||||||
|
version = {
|
||||||
|
args = [ "--version" ];
|
||||||
|
match = null;
|
||||||
|
regex = null;
|
||||||
|
group = 0;
|
||||||
|
line = 1;
|
||||||
|
};
|
||||||
|
banner = {
|
||||||
|
color = "YELLOW";
|
||||||
|
icon = "";
|
||||||
|
iconColor = null;
|
||||||
|
};
|
||||||
|
required = true;
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
For a tool that should come from the host `PATH` instead of `nixpkgs`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
(repo-lib.lib.tools.fromCommand {
|
||||||
|
name = "Nix";
|
||||||
|
command = "nix";
|
||||||
|
version = {
|
||||||
|
args = [ "--version" ];
|
||||||
|
group = 1;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Helper:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
repo-lib.lib.tools.simple "Go" pkgs.go [ "version" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Tool behavior:
|
||||||
|
|
||||||
|
- Package-backed tools are added to the shell automatically.
|
||||||
|
- Command-backed tools are probed from the existing `PATH` and are not added to the shell automatically.
|
||||||
|
- Banner probing uses the resolved executable path.
|
||||||
|
- `required = true` by default.
|
||||||
|
- When `version.match` is set, the first matching output line is selected before `regex` extraction.
|
||||||
|
- Required tool probe failure aborts shell startup.
|
||||||
|
|
||||||
|
Use `shell.packages` instead of `tools` when:
|
||||||
|
|
||||||
|
- the package should be in the shell but not in the banner
|
||||||
|
- the package is not a CLI tool with a stable version probe
|
||||||
|
|
||||||
|
## `config.release`
|
||||||
|
|
||||||
|
Shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
channels = [ "alpha" "beta" "rc" "internal" ];
|
||||||
|
steps = [ ];
|
||||||
|
postVersion = "";
|
||||||
|
runtimeInputs = [ ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
- `channels = [ "alpha" "beta" "rc" "internal" ]`
|
||||||
|
- `steps = [ ]`
|
||||||
|
- `postVersion = ""`
|
||||||
|
- `runtimeInputs = [ ]`
|
||||||
|
|
||||||
|
Set `release = null` to disable the generated release package.
|
||||||
|
|
||||||
|
## Release step shapes
|
||||||
|
|
||||||
|
### `writeFile`
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
writeFile = {
|
||||||
|
path = "src/version.ts";
|
||||||
|
text = ''
|
||||||
|
export const APP_VERSION = "$FULL_VERSION" as const;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `replace`
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
replace = {
|
||||||
|
path = "README.md";
|
||||||
|
regex = ''^(version = ")[^"]*(")$'';
|
||||||
|
replacement = ''\1$FULL_VERSION\2'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `versionMetaSet`
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
versionMetaSet = {
|
||||||
|
key = "desktop_binary_version_max";
|
||||||
|
value = "$FULL_VERSION";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `versionMetaUnset`
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
versionMetaUnset = {
|
||||||
|
key = "desktop_unused";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Current supported step kinds are only `writeFile`, `replace`, `versionMetaSet`, and `versionMetaUnset`.
|
||||||
|
- Do not document or implement a `run` step in consumer repos unless the library itself gains that feature.
|
||||||
|
|
||||||
|
## Release ordering
|
||||||
|
|
||||||
|
The generated `release` command currently does this:
|
||||||
|
|
||||||
|
1. Require a clean git worktree
|
||||||
|
2. Update `VERSION`
|
||||||
|
3. Run `release.steps`
|
||||||
|
4. Run `postVersion`
|
||||||
|
5. Run `nix fmt`
|
||||||
|
6. `git add -A`
|
||||||
|
7. Commit with `chore(release): <tag>`
|
||||||
|
8. Tag
|
||||||
|
9. Push branch
|
||||||
|
10. Push tags
|
||||||
|
|
||||||
|
Important consequences:
|
||||||
|
|
||||||
|
- `postVersion` is before formatting, commit, tag, and push.
|
||||||
|
- There is no true post-tag or post-push hook in current `repo-lib`.
|
||||||
|
- The current release runner is opinionated and performs commit, tag, and push as part of the flow.
|
||||||
|
|
||||||
|
## `mkRelease`
|
||||||
|
|
||||||
|
`repo-lib.lib.mkRelease` remains available when a repo wants only the release package:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
repo-lib.lib.mkRelease {
|
||||||
|
system = system;
|
||||||
|
nixpkgsInput = nixpkgs; # optional
|
||||||
|
channels = [ "alpha" "beta" "rc" "internal" ];
|
||||||
|
steps = [ ];
|
||||||
|
postVersion = "";
|
||||||
|
runtimeInputs = [ ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the same release-step rules as `config.release`.
|
||||||
245
skills/repo-lib-consumer/references/recipes.md
Normal file
245
skills/repo-lib-consumer/references/recipes.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
# repo-lib Change Recipes
|
||||||
|
|
||||||
|
## Add a new bannered tool
|
||||||
|
|
||||||
|
Edit `perSystem.tools` in the consuming repo:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
tools = [
|
||||||
|
(repo-lib.lib.tools.fromPackage {
|
||||||
|
name = "Bun";
|
||||||
|
package = pkgs.bun;
|
||||||
|
version.args = [ "--version" ];
|
||||||
|
banner = {
|
||||||
|
color = "YELLOW";
|
||||||
|
icon = "";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Do not also add the same package to `shell.packages`; `tools` already adds package-backed tools to the shell.
|
||||||
|
- Use `exe = "name"` only when the package exposes multiple binaries or the default binary is not the right one.
|
||||||
|
- Use `fromCommand` when the executable should come from the host environment instead of `nixpkgs`.
|
||||||
|
|
||||||
|
## Add a non-banner package to the shell
|
||||||
|
|
||||||
|
Use `shell.packages`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shell.packages = [
|
||||||
|
self.packages.${system}.release
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this for:
|
||||||
|
|
||||||
|
- helper CLIs that do not need a banner entry
|
||||||
|
- internal scripts
|
||||||
|
- the generated `release` package itself
|
||||||
|
|
||||||
|
## Customize the shell banner
|
||||||
|
|
||||||
|
Use `config.shell.banner`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.shell.banner = {
|
||||||
|
style = "pretty";
|
||||||
|
icon = "☾";
|
||||||
|
title = "Moonrepo shell ready";
|
||||||
|
titleColor = "GREEN";
|
||||||
|
subtitle = "Bun + TypeScript + Varlock";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
|
borderColor = "BLUE";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Guidance:
|
||||||
|
|
||||||
|
- Use `style = "pretty"` when the repo already has a styled shell banner.
|
||||||
|
- Keep icons and colors consistent with the repo's current shell UX.
|
||||||
|
- Remember that required tool probe failures will still abort shell startup.
|
||||||
|
|
||||||
|
## Add a test phase or lint hook
|
||||||
|
|
||||||
|
For a simple shared check:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.checks.typecheck = {
|
||||||
|
command = "bun run typecheck";
|
||||||
|
stage = "pre-push";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [ pkgs.bun ];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
For a system-specific check:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
perSystem = { pkgs, ... }: {
|
||||||
|
checks.format = {
|
||||||
|
command = "oxfmt --check .";
|
||||||
|
stage = "pre-commit";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [ pkgs.oxfmt ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Guidance:
|
||||||
|
|
||||||
|
- Use `pre-commit` for fast format or lint work.
|
||||||
|
- Use `pre-push` for slower test suites.
|
||||||
|
- Prefer `runtimeInputs` over inline absolute paths when the command needs extra CLIs.
|
||||||
|
|
||||||
|
## Add a `commit-msg` hook
|
||||||
|
|
||||||
|
`config.checks` cannot target `commit-msg`. Use raw Lefthook config:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.lefthook.commit-msg.commands.gitlint = {
|
||||||
|
run = "${pkgs.gitlint}/bin/gitlint --staged --msg-filename {1}";
|
||||||
|
stage_fixed = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a structured hook entry:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
perSystem = { pkgs, ... }: {
|
||||||
|
lefthook.commitlint = {
|
||||||
|
entry = "${pkgs.nodejs}/bin/node scripts/commitlint.mjs";
|
||||||
|
pass_filenames = true;
|
||||||
|
stages = [ "commit-msg" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add or change formatters
|
||||||
|
|
||||||
|
Use `config.formatting`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.formatting = {
|
||||||
|
programs = {
|
||||||
|
shfmt.enable = true;
|
||||||
|
oxfmt.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
shfmt.options = [ "-i" "2" "-s" "-w" ];
|
||||||
|
oxfmt.excludes = [ "*.md" "*.yml" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add release-managed files
|
||||||
|
|
||||||
|
Generate a file from the release version:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.release.steps = [
|
||||||
|
{
|
||||||
|
writeFile = {
|
||||||
|
path = "src/version.ts";
|
||||||
|
text = ''
|
||||||
|
export const APP_VERSION = "$FULL_VERSION" as const;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Update an existing file with a regex:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.release.steps = [
|
||||||
|
{
|
||||||
|
replace = {
|
||||||
|
path = "README.md";
|
||||||
|
regex = ''^(version = ")[^"]*(")$'';
|
||||||
|
replacement = ''\1$FULL_VERSION\2'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Update metadata inside `VERSION`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.release.steps = [
|
||||||
|
{
|
||||||
|
versionMetaSet = {
|
||||||
|
key = "desktop_binary_version_max";
|
||||||
|
value = "$FULL_VERSION";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
versionMetaUnset = {
|
||||||
|
key = "desktop_unused";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add a webhook during release
|
||||||
|
|
||||||
|
Current `repo-lib` does not expose a `run` release step. If the action must happen during local release execution, put it in `postVersion`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.release.postVersion = ''
|
||||||
|
curl -fsS https://example.invalid/release-hook \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-d '{"version":"'"$FULL_VERSION"'","tag":"'"$FULL_TAG"'"}'
|
||||||
|
'';
|
||||||
|
config.release.runtimeInputs = [ pkgs.curl ];
|
||||||
|
```
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- `postVersion` still runs before `nix fmt`, commit, tag, and push.
|
||||||
|
- This is not a true post-tag hook.
|
||||||
|
|
||||||
|
## Add a true post-tag webhook
|
||||||
|
|
||||||
|
Do not fake this with `postVersion`.
|
||||||
|
|
||||||
|
Preferred approach in the consuming repo:
|
||||||
|
|
||||||
|
1. Keep local version generation in `repo-lib`.
|
||||||
|
2. Trigger CI from tag push.
|
||||||
|
3. Put the webhook call in CI, where the tag already exists remotely.
|
||||||
|
|
||||||
|
Only change `repo-lib` itself if the user explicitly asks for a new library capability.
|
||||||
|
|
||||||
|
## Add impure bootstrap work
|
||||||
|
|
||||||
|
Only do this when the user actually wants imperative shell setup:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.shell = {
|
||||||
|
bootstrap = ''
|
||||||
|
export BUN_INSTALL_GLOBAL_DIR="$PWD/.tools/bun/install/global"
|
||||||
|
export BUN_INSTALL_BIN="$PWD/.tools/bun/bin"
|
||||||
|
export PATH="$BUN_INSTALL_BIN:$PATH"
|
||||||
|
'';
|
||||||
|
allowImpureBootstrap = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not add bootstrap work for normal Nix-packaged tools.
|
||||||
|
|
||||||
|
## Move from direct `mkRelease` to `mkRepo`
|
||||||
|
|
||||||
|
Only do this if requested.
|
||||||
|
|
||||||
|
Migration outline:
|
||||||
|
|
||||||
|
1. Move release package config into `config.release`.
|
||||||
|
2. Move shell setup into `config.shell` and `perSystem.shell.packages`.
|
||||||
|
3. Move bannered CLIs into `perSystem.tools`.
|
||||||
|
4. Move hook commands into `config.checks` or raw `lefthook`.
|
||||||
|
5. Keep behavior the same first; do not redesign the repo in the same change unless asked.
|
||||||
18
template/.env.schema
Normal file
18
template/.env.schema
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# @currentEnv=$REPO_ENVIRONMENT
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# Canonical repo environment used by Varlock.
|
||||||
|
# @type=enum(development,ci,production)
|
||||||
|
REPO_ENVIRONMENT=development
|
||||||
|
|
||||||
|
# Safe starter values for local development.
|
||||||
|
# @type=string
|
||||||
|
APP_NAME=typescript-monorepo
|
||||||
|
|
||||||
|
# @type=port
|
||||||
|
APP_PORT=3000
|
||||||
|
|
||||||
|
# Optional example secret resolved from OpenBao.
|
||||||
|
# Replace the namespace and secret path with values for your repo before use.
|
||||||
|
# @optional @sensitive
|
||||||
|
EXAMPLE_API_TOKEN=exec('bao kv get -mount=kv -namespace="${BAO_NAMESPACE:+$BAO_NAMESPACE/}template" -field=EXAMPLE_API_TOKEN "$REPO_ENVIRONMENT/shared" 2>/dev/null || true')
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
watch_file flake.nix
|
||||||
use flake
|
use flake
|
||||||
|
|||||||
4
template/.gitignore
vendored
4
template/.gitignore
vendored
@@ -1,8 +1,12 @@
|
|||||||
.direnv/
|
.direnv/
|
||||||
|
.moon/cache/
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
lefthook.yml
|
||||||
|
.tools/
|
||||||
|
|
||||||
bazel-*
|
bazel-*
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.env.sh
|
||||||
|
|||||||
11
template/.moon/workspace.yml
Normal file
11
template/.moon/workspace.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
$schema: "./cache/schemas/workspace.json"
|
||||||
|
|
||||||
|
projects:
|
||||||
|
globs:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
|
sources:
|
||||||
|
root: "."
|
||||||
|
|
||||||
|
vcs:
|
||||||
|
defaultBranch: "main"
|
||||||
10
template/.vscode/settings.json
vendored
Normal file
10
template/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/Thumbs.db": true,
|
||||||
|
"VERSION": true
|
||||||
|
}
|
||||||
|
}
|
||||||
32
template/README.md
Normal file
32
template/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# TypeScript Monorepo Template
|
||||||
|
|
||||||
|
This template gives you a Bun-only monorepo with:
|
||||||
|
|
||||||
|
- Moonrepo at the workspace root
|
||||||
|
- strict shared TypeScript configs
|
||||||
|
- Varlock with a committed `.env.schema`
|
||||||
|
- a Nix flake shell built through `repo-lib`
|
||||||
|
|
||||||
|
## First Run
|
||||||
|
|
||||||
|
1. Enter the shell with `direnv allow` or `nix develop`.
|
||||||
|
2. Install workspace dependencies with `bun install`.
|
||||||
|
3. Review and customize `.env.schema` for your repo.
|
||||||
|
4. Run `bun run env:check`.
|
||||||
|
5. Run `moon run :typecheck`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `apps/` for applications
|
||||||
|
- `packages/` for shared libraries
|
||||||
|
- `tsconfig/` for shared TypeScript profiles
|
||||||
|
- `moon.yml` for root Moonrepo tasks
|
||||||
|
|
||||||
|
## Varlock
|
||||||
|
|
||||||
|
`bunfig.toml` preloads `varlock/auto-load`, and the root scripts expose:
|
||||||
|
|
||||||
|
- `bun run env:check`
|
||||||
|
- `bun run env:scan`
|
||||||
|
|
||||||
|
If you use OpenBao locally, set `OPENBAO_ADDR`, `OPENBAO_NAMESPACE`, and `OPENBAO_CACERT` in your shell or an ignored `.env.sh` file before running those commands.
|
||||||
1
template/apps/.gitkeep
Normal file
1
template/apps/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
template/bunfig.toml
Normal file
2
template/bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
env = false
|
||||||
|
preload = ["varlock/auto-load"]
|
||||||
159
template/flake.lock
generated
159
template/flake.lock
generated
@@ -1,159 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"devshell-lib": {
|
|
||||||
"inputs": {
|
|
||||||
"git-hooks": "git-hooks",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"treefmt-nix": "treefmt-nix"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1772603902,
|
|
||||||
"narHash": "sha256-GN5EC9m0flWDuc6qaB6QoIBD73yFnhl2PBIYXzSTGeQ=",
|
|
||||||
"ref": "v0.0.2",
|
|
||||||
"rev": "db4ed150e01e2f9245e668077245447d0089163f",
|
|
||||||
"revCount": 15,
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.dgren.dev/eric/nix-flake-lib"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"ref": "v0.0.2",
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.dgren.dev/eric/nix-flake-lib"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767039857,
|
|
||||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git-hooks": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"gitignore": "gitignore",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1772024342,
|
|
||||||
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitignore": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"devshell-lib",
|
|
||||||
"git-hooks",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709087332,
|
|
||||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1770073757,
|
|
||||||
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1770107345,
|
|
||||||
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_3": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1772542754,
|
|
||||||
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"devshell-lib": "devshell-lib",
|
|
||||||
"nixpkgs": "nixpkgs_3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"treefmt-nix": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1770228511,
|
|
||||||
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "treefmt-nix",
|
|
||||||
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "treefmt-nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,145 +1,158 @@
|
|||||||
# flake.nix — product repo template
|
|
||||||
{
|
{
|
||||||
description = "my-product";
|
description = "typescript-monorepo";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
devshell-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=v0.0.5";
|
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v4.0.0";
|
||||||
devshell-lib.inputs.nixpkgs.follows = "nixpkgs";
|
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
devshell-lib,
|
repo-lib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
repo-lib.lib.mkRepo {
|
||||||
supportedSystems = [
|
inherit self nixpkgs;
|
||||||
"x86_64-linux"
|
src = ./.;
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { inherit system; };
|
|
||||||
env = devshell-lib.lib.mkDevShell {
|
|
||||||
inherit system;
|
|
||||||
|
|
||||||
# includeStandardPackages = false; # opt out of nixfmt/gitlint/gitleaks/shfmt defaults
|
config = {
|
||||||
|
shell = {
|
||||||
extraPackages = with pkgs; [
|
banner = {
|
||||||
# add your tools here, e.g.:
|
style = "pretty";
|
||||||
# go
|
icon = "☾";
|
||||||
# bun
|
title = "Moonrepo shell ready";
|
||||||
# rustc
|
titleColor = "GREEN";
|
||||||
];
|
subtitle = "Bun + TypeScript + Varlock";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
features = {
|
borderColor = "BLUE";
|
||||||
# oxfmt = true; # enables oxfmt + oxlint from nixpkgs
|
|
||||||
};
|
};
|
||||||
|
|
||||||
formatters = {
|
extraShellText = ''
|
||||||
# shfmt.enable = true;
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
# gofmt.enable = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
formatterSettings = {
|
|
||||||
# shfmt.options = [ "-i" "2" "-s" "-w" ];
|
|
||||||
# oxfmt.includes = [ "*.ts" "*.tsx" "*.js" "*.json" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
additionalHooks = {
|
|
||||||
tests = {
|
|
||||||
enable = true;
|
|
||||||
entry = "echo 'No tests defined yet.'"; # replace with your test command
|
|
||||||
pass_filenames = false;
|
|
||||||
stages = [ "pre-push" ];
|
|
||||||
};
|
|
||||||
# my-hook = {
|
|
||||||
# enable = true;
|
|
||||||
# entry = "${pkgs.some-tool}/bin/some-tool";
|
|
||||||
# pass_filenames = false;
|
|
||||||
# };
|
|
||||||
};
|
|
||||||
|
|
||||||
tools = [
|
|
||||||
# { name = "Bun"; bin = "${pkgs.bun}/bin/bun"; versionCmd = "--version"; color = "YELLOW"; }
|
|
||||||
# { name = "Go"; bin = "${pkgs.go}/bin/go"; versionCmd = "version"; color = "CYAN"; }
|
|
||||||
# { name = "Rust"; bin = "${pkgs.rustc}/bin/rustc"; versionCmd = "--version"; color = "YELLOW"; }
|
|
||||||
];
|
|
||||||
|
|
||||||
extraShellHook = ''
|
|
||||||
# any repo-specific shell setup here
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
bootstrap = ''
|
||||||
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
|
|
||||||
|
export BUN_INSTALL_GLOBAL_DIR="$repo_root/.tools/bun/install/global"
|
||||||
|
export BUN_INSTALL_BIN="$repo_root/.tools/bun/bin"
|
||||||
|
export PATH="$BUN_INSTALL_BIN:$PATH"
|
||||||
|
|
||||||
|
mkdir -p "$BUN_INSTALL_GLOBAL_DIR" "$BUN_INSTALL_BIN"
|
||||||
|
|
||||||
|
if [ ! -x "$BUN_INSTALL_BIN/moon" ]; then
|
||||||
|
bun add -g @moonrepo/cli
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
allowImpureBootstrap = true;
|
||||||
};
|
};
|
||||||
in
|
|
||||||
|
formatting = {
|
||||||
|
programs = {
|
||||||
|
oxfmt.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
oxfmt.excludes = [
|
||||||
|
"*.css"
|
||||||
|
"*.graphql"
|
||||||
|
"*.hbs"
|
||||||
|
"*.html"
|
||||||
|
"*.md"
|
||||||
|
"*.mdx"
|
||||||
|
"*.mustache"
|
||||||
|
"*.scss"
|
||||||
|
"*.vue"
|
||||||
|
"*.yaml"
|
||||||
|
"*.yml"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
release = {
|
||||||
|
steps = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
perSystem =
|
||||||
{
|
{
|
||||||
default = env.shell;
|
pkgs,
|
||||||
}
|
system,
|
||||||
);
|
...
|
||||||
|
}:
|
||||||
checks = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
env = devshell-lib.lib.mkDevShell { inherit system; };
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
inherit (env) pre-commit-check;
|
tools = [
|
||||||
}
|
(repo-lib.lib.tools.fromCommand {
|
||||||
);
|
name = "Nix";
|
||||||
|
command = "nix";
|
||||||
|
version = {
|
||||||
|
args = [ "--version" ];
|
||||||
|
group = 1;
|
||||||
|
};
|
||||||
|
banner = {
|
||||||
|
color = "BLUE";
|
||||||
|
icon = "";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
formatter = forAllSystems (system: (devshell-lib.lib.mkDevShell { inherit system; }).formatter);
|
(repo-lib.lib.tools.fromPackage {
|
||||||
|
name = "Bun";
|
||||||
|
package = pkgs.bun;
|
||||||
|
version.args = [ "--version" ];
|
||||||
|
banner = {
|
||||||
|
color = "YELLOW";
|
||||||
|
icon = "";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
# Optional: release command (`release`)
|
shell = {
|
||||||
#
|
packages = [
|
||||||
# The release script always updates VERSION first, then:
|
self.packages.${system}.release
|
||||||
# 1) runs release steps in order (file writes and scripts)
|
pkgs.openbao
|
||||||
# 2) runs postVersion hook
|
pkgs.oxfmt
|
||||||
# 3) formats, stages, commits, tags, and pushes
|
pkgs.oxlint
|
||||||
#
|
];
|
||||||
# Runtime env vars available in release.run/postVersion:
|
};
|
||||||
# BASE_VERSION, CHANNEL, PRERELEASE_NUM, FULL_VERSION, FULL_TAG
|
|
||||||
#
|
checks = {
|
||||||
# packages = forAllSystems (
|
format = {
|
||||||
# system:
|
command = "oxfmt --check .";
|
||||||
# {
|
stage = "pre-commit";
|
||||||
# release = devshell-lib.lib.mkRelease {
|
passFilenames = false;
|
||||||
# inherit system;
|
runtimeInputs = [ pkgs.oxfmt ];
|
||||||
#
|
};
|
||||||
# release = [
|
|
||||||
# {
|
typecheck = {
|
||||||
# file = "src/version.ts";
|
command = "bun run typecheck";
|
||||||
# content = ''
|
stage = "pre-push";
|
||||||
# export const APP_VERSION = "$FULL_VERSION" as const;
|
passFilenames = false;
|
||||||
# '';
|
runtimeInputs = [ pkgs.bun ];
|
||||||
# }
|
};
|
||||||
# {
|
|
||||||
# file = "internal/version/version.go";
|
env-check = {
|
||||||
# content = ''
|
command = "bun run env:check";
|
||||||
# package version
|
stage = "pre-push";
|
||||||
#
|
passFilenames = false;
|
||||||
# const Version = "$FULL_VERSION"
|
runtimeInputs = [
|
||||||
# '';
|
pkgs.bun
|
||||||
# }
|
pkgs.openbao
|
||||||
# {
|
];
|
||||||
# run = ''
|
};
|
||||||
# sed -E -i "s#^([[:space:]]*my-lib\\.url = \")github:org/my-lib[^"]*(\";)#\\1github:org/my-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/flake.nix"
|
|
||||||
# '';
|
env-scan = {
|
||||||
# }
|
command = "bun run env:scan";
|
||||||
# ];
|
stage = "pre-commit";
|
||||||
#
|
passFilenames = false;
|
||||||
# postVersion = ''
|
runtimeInputs = [
|
||||||
# echo "Released $FULL_TAG"
|
pkgs.bun
|
||||||
# '';
|
pkgs.openbao
|
||||||
# };
|
];
|
||||||
# }
|
};
|
||||||
# );
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
45
template/moon.yml
Normal file
45
template/moon.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
$schema: "./.moon/cache/schemas/project.json"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
typecheck:
|
||||||
|
command: "bun"
|
||||||
|
args:
|
||||||
|
- "run"
|
||||||
|
- "typecheck"
|
||||||
|
inputs:
|
||||||
|
- "package.json"
|
||||||
|
- "tsconfig.json"
|
||||||
|
- "tsconfig.options.json"
|
||||||
|
- "tsconfig/**/*"
|
||||||
|
options:
|
||||||
|
cache: false
|
||||||
|
runFromWorkspaceRoot: true
|
||||||
|
|
||||||
|
env-check:
|
||||||
|
command: "bun"
|
||||||
|
args:
|
||||||
|
- "run"
|
||||||
|
- "env:check"
|
||||||
|
inputs:
|
||||||
|
- ".env.schema"
|
||||||
|
- "package.json"
|
||||||
|
- "bunfig.toml"
|
||||||
|
toolchains: "system"
|
||||||
|
options:
|
||||||
|
cache: false
|
||||||
|
runFromWorkspaceRoot: true
|
||||||
|
|
||||||
|
env-scan:
|
||||||
|
command: "bun"
|
||||||
|
args:
|
||||||
|
- "run"
|
||||||
|
- "env:scan"
|
||||||
|
inputs:
|
||||||
|
- ".env.schema"
|
||||||
|
- "package.json"
|
||||||
|
- "bunfig.toml"
|
||||||
|
toolchains: "system"
|
||||||
|
options:
|
||||||
|
cache: false
|
||||||
|
runFromWorkspaceRoot: true
|
||||||
|
runInCI: "skip"
|
||||||
23
template/package.json
Normal file
23
template/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "typescript-monorepo",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"apps/*",
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "bunx tsc -p tsconfig.json --pretty false",
|
||||||
|
"env:check": "sh -ec 'export BAO_ADDR=\"${BAO_ADDR:-${OPENBAO_ADDR:-}}\"; export BAO_NAMESPACE=\"${BAO_NAMESPACE:-${OPENBAO_NAMESPACE:-}}\"; export BAO_CACERT=\"${BAO_CACERT:-${OPENBAO_CACERT:-}}\"; exec varlock load --show-all'",
|
||||||
|
"env:scan": "sh -ec 'export BAO_ADDR=\"${BAO_ADDR:-${OPENBAO_ADDR:-}}\"; export BAO_NAMESPACE=\"${BAO_NAMESPACE:-${OPENBAO_NAMESPACE:-}}\"; export BAO_CACERT=\"${BAO_CACERT:-${OPENBAO_CACERT:-}}\"; exec varlock scan --staged'",
|
||||||
|
"check": "bun run typecheck && bun run env:check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@moonrepo/cli": "^2.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"typescript": "^6.0.0-beta",
|
||||||
|
"varlock": "0.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
template/packages/.gitkeep
Normal file
1
template/packages/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
4
template/tsconfig.json
Normal file
4
template/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.options.json",
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
19
template/tsconfig.options.json
Normal file
19
template/tsconfig.options.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
6
template/tsconfig/browser.json
Normal file
6
template/tsconfig/browser.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./runtime.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["vite/client"]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
template/tsconfig/bun.json
Normal file
6
template/tsconfig/bun.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./runtime.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["@types/bun"]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
template/tsconfig/package.json
Normal file
6
template/tsconfig/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"extends": "../tsconfig.options.json"
|
||||||
|
}
|
||||||
6
template/tsconfig/runtime.json
Normal file
6
template/tsconfig/runtime.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.options.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user