Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
146b1e9501 | ||
|
|
8fec37023f | ||
|
|
e9d04c7f8d | ||
|
|
ece1dffb39 |
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
README.md
42
README.md
@@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
- `mkRepo` for `devShells`, `checks`, `formatter`, and optional `packages.release`
|
- `mkRepo` for `devShells`, `checks`, `formatter`, and optional `packages.release`
|
||||||
- structured tool banners driven from package-backed tool specs
|
- structured tool banners driven from package-backed tool specs
|
||||||
- structured release steps (`writeFile`, `replace`, `run`)
|
- structured release steps (`writeFile`, `replace`, `versionMetaSet`, `versionMetaUnset`)
|
||||||
- a minimal starter template in [`template/`](/Users/eric/Projects/repo-lib/template)
|
- 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
|
## Prerequisites
|
||||||
|
|
||||||
@@ -15,15 +17,24 @@
|
|||||||
## Use the template
|
## Use the template
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.4.0#default' --refresh
|
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.5.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
|
## Use the library
|
||||||
|
|
||||||
Add this flake input:
|
Add this flake input:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.4.0";
|
inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.5.1";
|
||||||
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -145,10 +156,9 @@ config.release = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
run = {
|
versionMetaSet = {
|
||||||
script = ''
|
key = "desktop_binary_version_max";
|
||||||
echo "Released $FULL_TAG"
|
value = "$FULL_VERSION";
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -159,16 +169,30 @@ The generated `release` command still supports:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
release
|
release
|
||||||
|
release select
|
||||||
|
release --dry-run patch
|
||||||
release patch
|
release patch
|
||||||
|
release patch --commit
|
||||||
|
release patch --commit --tag
|
||||||
|
release patch --commit --tag --push
|
||||||
release beta
|
release beta
|
||||||
release minor beta
|
release minor beta
|
||||||
release stable
|
release stable
|
||||||
release set 1.2.3
|
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
|
## Low-level APIs
|
||||||
|
|
||||||
`mkDevShell` and `mkRelease` remain available for repos that want lower-level control or a migration path from the older library shape.
|
`mkRelease` remains available for repos that want lower-level control over release automation.
|
||||||
|
|
||||||
## Common command
|
## Common command
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
34
flake.lock
generated
34
flake.lock
generated
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772408722,
|
||||||
|
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lefthook-nix": {
|
"lefthook-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -36,6 +54,21 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770107345,
|
"lastModified": 1770107345,
|
||||||
@@ -54,6 +87,7 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
"lefthook-nix": "lefthook-nix",
|
"lefthook-nix": "lefthook-nix",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"treefmt-nix": "treefmt-nix"
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
|||||||
20
flake.nix
20
flake.nix
@@ -3,6 +3,7 @@
|
|||||||
description = "Pure-first repo development platform for Nix flakes";
|
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";
|
||||||
lefthook-nix.url = "github:sudosubin/lefthook.nix";
|
lefthook-nix.url = "github:sudosubin/lefthook.nix";
|
||||||
lefthook-nix.inputs.nixpkgs.follows = "nixpkgs";
|
lefthook-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
|
flake-parts,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
treefmt-nix,
|
treefmt-nix,
|
||||||
lefthook-nix,
|
lefthook-nix,
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
let
|
let
|
||||||
lib = nixpkgs.lib;
|
lib = nixpkgs.lib;
|
||||||
repoLib = import ./packages/repo-lib/lib.nix {
|
repoLib = import ./packages/repo-lib/lib.nix {
|
||||||
inherit nixpkgs treefmt-nix;
|
inherit flake-parts nixpkgs treefmt-nix;
|
||||||
lefthookNix = lefthook-nix;
|
lefthookNix = lefthook-nix;
|
||||||
releaseScriptPath = ./packages/release/release.sh;
|
releaseScriptPath = ./packages/release/release.sh;
|
||||||
shellHookTemplatePath = ./packages/repo-lib/shell-hook.sh;
|
shellHookTemplatePath = ./packages/repo-lib/shell-hook.sh;
|
||||||
@@ -94,20 +96,16 @@
|
|||||||
pkgs.runCommand "release-tests"
|
pkgs.runCommand "release-tests"
|
||||||
{
|
{
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
bash
|
go
|
||||||
git
|
git
|
||||||
nix
|
|
||||||
gnused
|
|
||||||
coreutils
|
|
||||||
gnugrep
|
|
||||||
perl
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
export REPO_LIB_ROOT=${./.}
|
export HOME="$PWD/.home"
|
||||||
export NIXPKGS_FLAKE_PATH=${nixpkgs}
|
export GOCACHE="$PWD/.go-cache"
|
||||||
export HOME="$TMPDIR"
|
mkdir -p "$GOCACHE" "$HOME"
|
||||||
${pkgs.bash}/bin/bash ${./tests/release.sh}
|
cd ${./packages/release}
|
||||||
|
go test ./...
|
||||||
touch "$out"
|
touch "$out"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/release/.gitignore
vendored
Normal file
1
packages/release/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
vendor/
|
||||||
135
packages/release/cmd/release/main.go
Normal file
135
packages/release/cmd/release/main.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
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
|
||||||
|
case "--dry-run":
|
||||||
|
execution.DryRun = true
|
||||||
|
case "--commit":
|
||||||
|
execution.Commit = true
|
||||||
|
case "--tag":
|
||||||
|
execution.Tag = true
|
||||||
|
case "--push":
|
||||||
|
execution.Push = 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)
|
||||||
|
}
|
||||||
44
packages/release/internal/release/release_step_replace.go
Normal file
44
packages/release/internal/release/release_step_replace.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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.WriteByte('$')
|
||||||
|
b.WriteByte(raw[i+1])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteByte(raw[i])
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
404
packages/release/internal/release/release_test.go
Normal file
404
packages/release/internal/release/release_test.go
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
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 TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
|
||||||
|
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 != "M VERSION" {
|
||||||
|
t.Fatalf("git status --short = %q, want %q", status, "M VERSION")
|
||||||
|
}
|
||||||
|
|
||||||
|
tagList := strings.TrimSpace(mustOutput(t, root, "git", "tag", "--list"))
|
||||||
|
if tagList != "" {
|
||||||
|
t.Fatalf("git tag --list = %q, want empty", tagList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerDryRunDoesNotModifyRepo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
|
||||||
|
var stdout strings.Builder
|
||||||
|
r := &Runner{
|
||||||
|
Config: Config{
|
||||||
|
RootDir: root,
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc", "internal"},
|
||||||
|
Execution: ExecutionOptions{
|
||||||
|
DryRun: true,
|
||||||
|
Commit: true,
|
||||||
|
Tag: true,
|
||||||
|
Push: true,
|
||||||
|
},
|
||||||
|
Stdout: &stdout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Run([]string{"patch"}); err != nil {
|
||||||
|
t.Fatalf("Runner.Run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.0\nstable\n0\n")
|
||||||
|
status := strings.TrimSpace(mustOutput(t, root, "git", "status", "--short"))
|
||||||
|
if status != "" {
|
||||||
|
t.Fatalf("git status --short = %q, want empty", status)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), "Dry run: 1.0.1") {
|
||||||
|
t.Fatalf("dry-run output missing next version:\n%s", stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
160
packages/release/internal/release/runner.go
Normal file
160
packages/release/internal/release/runner.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
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 {
|
||||||
|
DryRun bool
|
||||||
|
Commit bool
|
||||||
|
Tag bool
|
||||||
|
Push bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
Config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o ExecutionOptions) Normalize() ExecutionOptions {
|
||||||
|
if o.Push {
|
||||||
|
o.Commit = true
|
||||||
|
}
|
||||||
|
if o.Tag {
|
||||||
|
o.Commit = true
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
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 execution.DryRun {
|
||||||
|
printReleasePlan(stdout, nextVersion, execution, strings.TrimSpace(r.Config.ReleaseStepsJSON) != "", strings.TrimSpace(r.Config.PostVersion) != "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 !execution.Commit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 execution.Tag {
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "tag", version.Tag()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !execution.Push {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if execution.Tag {
|
||||||
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push", "--tags"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printReleasePlan(stdout io.Writer, version Version, execution ExecutionOptions, hasReleaseSteps bool, hasPostVersion bool) {
|
||||||
|
fmt.Fprintf(stdout, "Dry run: %s\n", version.String())
|
||||||
|
fmt.Fprintf(stdout, "Tag: %s\n", version.Tag())
|
||||||
|
fmt.Fprintf(stdout, "Release steps: %s\n", yesNo(hasReleaseSteps))
|
||||||
|
fmt.Fprintf(stdout, "Post-version: %s\n", yesNo(hasPostVersion))
|
||||||
|
fmt.Fprintf(stdout, "nix fmt: yes\n")
|
||||||
|
fmt.Fprintf(stdout, "git commit: %s\n", yesNo(execution.Commit))
|
||||||
|
fmt.Fprintf(stdout, "git tag: %s\n", yesNo(execution.Tag))
|
||||||
|
fmt.Fprintf(stdout, "git push: %s\n", yesNo(execution.Push))
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
350
packages/release/internal/release/ui.go
Normal file
350
packages/release/internal/release/ui.go
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
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(versionFile.Version, options)
|
||||||
|
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 execution.DryRun {
|
||||||
|
parts = append(parts, "--dry-run")
|
||||||
|
}
|
||||||
|
if execution.Commit {
|
||||||
|
parts = append(parts, "--commit")
|
||||||
|
}
|
||||||
|
if execution.Tag {
|
||||||
|
parts = append(parts, "--tag")
|
||||||
|
}
|
||||||
|
if execution.Push {
|
||||||
|
parts = append(parts, "--push")
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
" dry run: "+yesNo(execution.DryRun),
|
||||||
|
)
|
||||||
|
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 {
|
||||||
|
current Version
|
||||||
|
options []CommandOption
|
||||||
|
cursor int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
confirmed bool
|
||||||
|
selected CommandOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommandPickerModel(current Version, options []CommandOption) commandPickerModel {
|
||||||
|
return commandPickerModel{
|
||||||
|
current: current,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
if m.cursor > 0 {
|
||||||
|
m.cursor--
|
||||||
|
}
|
||||||
|
case "down", "j":
|
||||||
|
if m.cursor < len(m.options)-1 {
|
||||||
|
m.cursor++
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
m.confirmed = true
|
||||||
|
m.selected = m.options[m.cursor]
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) View() string {
|
||||||
|
if len(m.options) == 0 {
|
||||||
|
return "No release commands available.\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
preview := m.options[m.cursor].Preview
|
||||||
|
header := fmt.Sprintf("Release command picker\nCurrent version: %s\nUse up/down or j/k to choose, Enter to run, q to cancel.\n", m.current.String())
|
||||||
|
|
||||||
|
listLines := make([]string, 0, len(m.options)+1)
|
||||||
|
listLines = append(listLines, "Commands")
|
||||||
|
for i, option := range m.options {
|
||||||
|
cursor := " "
|
||||||
|
if i == m.cursor {
|
||||||
|
cursor = "> "
|
||||||
|
}
|
||||||
|
listLines = append(listLines, fmt.Sprintf("%s%s\n %s", cursor, option.Command, option.Description))
|
||||||
|
}
|
||||||
|
list := strings.Join(listLines, "\n")
|
||||||
|
|
||||||
|
if m.width >= 100 {
|
||||||
|
return header + "\n" + renderColumns(list, preview, m.width)
|
||||||
|
}
|
||||||
|
return header + "\n" + list + "\n\n" + preview + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
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]) + "..."
|
||||||
|
}
|
||||||
91
packages/release/internal/release/ui_test.go
Normal file
91
packages/release/internal/release/ui_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
flake-parts,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
treefmt-nix,
|
treefmt-nix,
|
||||||
lefthookNix,
|
lefthookNix,
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
}:
|
}:
|
||||||
import ../repo-lib/lib.nix {
|
import ../repo-lib/lib.nix {
|
||||||
inherit
|
inherit
|
||||||
|
flake-parts
|
||||||
nixpkgs
|
nixpkgs
|
||||||
treefmt-nix
|
treefmt-nix
|
||||||
lefthookNix
|
lefthookNix
|
||||||
|
|||||||
@@ -2,539 +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'
|
||||||
VERSION_META_LINES=()
|
__RELEASE_STEPS_JSON__
|
||||||
VERSION_META_EXPORT_NAMES=()
|
EOF
|
||||||
|
)"
|
||||||
# ── logging ────────────────────────────────────────────────────────────────
|
export REPO_LIB_RELEASE_STEPS_JSON
|
||||||
|
REPO_LIB_RELEASE_POST_VERSION="$(cat <<'EOF'
|
||||||
log() { echo "[release] $*" >&2; }
|
|
||||||
|
|
||||||
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 (only opt-in path to promote prerelease -> stable)" \
|
|
||||||
" __CHANNEL_LIST__ switch channel (from stable, auto-bumps patch unless bump is specified)" \
|
|
||||||
"" \
|
|
||||||
"Safety rule:" \
|
|
||||||
" If current version is prerelease (e.g. x.y.z-beta.N), promotion to stable is allowed only via 'stable' or 'full'." \
|
|
||||||
" Commands like '${cmd} set x.y.z' or '${cmd} patch stable' are blocked from prerelease channels." \
|
|
||||||
"" \
|
|
||||||
"Examples:" \
|
|
||||||
" ${cmd} # patch bump on current channel" \
|
|
||||||
" ${cmd} minor # minor bump on current channel" \
|
|
||||||
" ${cmd} beta # from stable: patch bump + beta.1" \
|
|
||||||
" ${cmd} patch beta # patch bump, switch to beta channel" \
|
|
||||||
" ${cmd} rc # switch to rc channel" \
|
|
||||||
" ${cmd} stable # promote prerelease to stable (opt-in)" \
|
|
||||||
" ${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
|
|
||||||
}
|
|
||||||
|
|
||||||
meta_env_name() {
|
|
||||||
local key="$1"
|
|
||||||
key="${key//[^[:alnum:]]/_}"
|
|
||||||
key="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
|
||||||
printf 'VERSION_META_%s\n' "$key"
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_version_meta_exports() {
|
|
||||||
local export_name
|
|
||||||
for export_name in "${VERSION_META_EXPORT_NAMES[@]:-}"; do
|
|
||||||
unset "$export_name"
|
|
||||||
done
|
|
||||||
VERSION_META_EXPORT_NAMES=()
|
|
||||||
}
|
|
||||||
|
|
||||||
load_version_metadata() {
|
|
||||||
VERSION_META_LINES=()
|
|
||||||
[[ ! -f "$ROOT_DIR/VERSION" ]] && return 0
|
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n $line ]]; do
|
|
||||||
VERSION_META_LINES+=("$line")
|
|
||||||
done < <(tail -n +4 "$ROOT_DIR/VERSION" 2>/dev/null || true)
|
|
||||||
}
|
|
||||||
|
|
||||||
export_version_metadata() {
|
|
||||||
clear_version_meta_exports
|
|
||||||
|
|
||||||
local line key value export_name
|
|
||||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
|
||||||
[[ $line != *=* ]] && continue
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
[[ -z $key ]] && continue
|
|
||||||
export_name="$(meta_env_name "$key")"
|
|
||||||
printf -v "$export_name" '%s' "$value"
|
|
||||||
export "${export_name?}=$value"
|
|
||||||
VERSION_META_EXPORT_NAMES+=("$export_name")
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
write_version_file() {
|
|
||||||
local channel_to_write="$1"
|
|
||||||
local n_to_write="$2"
|
|
||||||
{
|
|
||||||
printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write"
|
|
||||||
local line
|
|
||||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
|
||||||
printf '%s\n' "$line"
|
|
||||||
done
|
|
||||||
} >"$ROOT_DIR/VERSION"
|
|
||||||
}
|
|
||||||
|
|
||||||
version_meta_get() {
|
|
||||||
local key="${1-}"
|
|
||||||
local line
|
|
||||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
|
||||||
if [[ $line == "$key="* ]]; then
|
|
||||||
printf '%s\n' "${line#*=}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
version_meta_set() {
|
|
||||||
local key="${1-}"
|
|
||||||
local value="${2-}"
|
|
||||||
[[ -z $key ]] && echo "Error: version_meta_set requires a key" >&2 && exit 1
|
|
||||||
|
|
||||||
local updated=0
|
|
||||||
local index
|
|
||||||
for index in "${!VERSION_META_LINES[@]}"; do
|
|
||||||
if [[ ${VERSION_META_LINES[index]} == "$key="* ]]; then
|
|
||||||
VERSION_META_LINES[index]="$key=$value"
|
|
||||||
updated=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $updated -eq 0 ]]; then
|
|
||||||
VERSION_META_LINES+=("$key=$value")
|
|
||||||
fi
|
|
||||||
|
|
||||||
export_version_metadata
|
|
||||||
version_meta_write
|
|
||||||
}
|
|
||||||
|
|
||||||
version_meta_unset() {
|
|
||||||
local key="${1-}"
|
|
||||||
[[ -z $key ]] && echo "Error: version_meta_unset requires a key" >&2 && exit 1
|
|
||||||
|
|
||||||
local filtered=()
|
|
||||||
local line
|
|
||||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
|
||||||
[[ $line == "$key="* ]] && continue
|
|
||||||
filtered+=("$line")
|
|
||||||
done
|
|
||||||
VERSION_META_LINES=("${filtered[@]}")
|
|
||||||
|
|
||||||
export_version_metadata
|
|
||||||
version_meta_write
|
|
||||||
}
|
|
||||||
|
|
||||||
version_meta_write() {
|
|
||||||
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
|
|
||||||
write_version_file "$channel_to_write" "$n_to_write"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
load_version_metadata
|
|
||||||
export_version_metadata
|
|
||||||
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
|
|
||||||
|
|
||||||
VERSION_META_LINES=()
|
|
||||||
write_version_file "$channel_to_write" "$n_to_write"
|
|
||||||
export_version_metadata
|
|
||||||
log "Initialized $ROOT_DIR/VERSION from highest tag: v$highest_tag"
|
|
||||||
}
|
|
||||||
|
|
||||||
do_read_version() {
|
|
||||||
load_version_metadata
|
|
||||||
export_version_metadata
|
|
||||||
|
|
||||||
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
|
|
||||||
write_version_file "$channel_to_write" "$n_to_write"
|
|
||||||
export_version_metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── 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"
|
|
||||||
compute_full_version
|
|
||||||
local current_full="$FULL_VERSION"
|
|
||||||
|
|
||||||
log "Current: base=$BASE_VERSION channel=$CHANNEL pre=${PRERELEASE_NUM:-}"
|
|
||||||
|
|
||||||
local action="${1-}"
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
if [[ $action == "set" ]]; then
|
|
||||||
local newv="${1-}"
|
|
||||||
local current_channel="$CHANNEL"
|
|
||||||
[[ -z $newv ]] && echo "Error: 'set' requires a version argument" >&2 && exit 1
|
|
||||||
parse_full_version "$newv"
|
|
||||||
validate_channel "$CHANNEL"
|
|
||||||
if [[ $current_channel != "stable" && $CHANNEL == "stable" ]]; then
|
|
||||||
echo "Error: from prerelease channel '$current_channel', promote using 'stable' or 'full' only" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
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="" was_channel_only=0
|
|
||||||
|
|
||||||
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"
|
|
||||||
was_channel_only=1
|
|
||||||
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"
|
|
||||||
if [[ $CHANNEL != "stable" && $target_channel == "stable" && $action != "stable" && $action != "full" ]]; then
|
|
||||||
echo "Error: from prerelease channel '$CHANNEL', promote using 'stable' or 'full' only" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z $part && $was_channel_only -eq 1 && $CHANNEL == "stable" && $target_channel != "stable" ]]; then
|
|
||||||
part="patch"
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
|
||||||
if [[ $FULL_VERSION == "$current_full" ]]; then
|
|
||||||
echo "Version $FULL_VERSION is already current; nothing to do." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
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 "$@"
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
flake-parts,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
treefmt-nix,
|
treefmt-nix,
|
||||||
lefthookNix,
|
lefthookNix,
|
||||||
@@ -6,58 +7,12 @@
|
|||||||
shellHookTemplatePath,
|
shellHookTemplatePath,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
lib = nixpkgs.lib;
|
defaults = import ./lib/defaults.nix { };
|
||||||
|
common = import ./lib/common.nix { inherit nixpkgs; };
|
||||||
supportedSystems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
|
|
||||||
defaultReleaseChannels = [
|
|
||||||
"alpha"
|
|
||||||
"beta"
|
|
||||||
"rc"
|
|
||||||
"internal"
|
|
||||||
];
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
defaultShellBanner = {
|
|
||||||
style = "simple";
|
|
||||||
icon = "🚀";
|
|
||||||
title = "Dev shell ready";
|
|
||||||
titleColor = "GREEN";
|
|
||||||
subtitle = "";
|
|
||||||
subtitleColor = "GRAY";
|
|
||||||
borderColor = "BLUE";
|
|
||||||
};
|
|
||||||
|
|
||||||
normalizeShellBanner =
|
normalizeShellBanner =
|
||||||
rawBanner:
|
rawBanner:
|
||||||
let
|
let
|
||||||
banner = defaultShellBanner // rawBanner;
|
banner = defaults.defaultShellBanner // rawBanner;
|
||||||
in
|
in
|
||||||
if
|
if
|
||||||
!(builtins.elem banner.style [
|
!(builtins.elem banner.style [
|
||||||
@@ -68,812 +23,78 @@ let
|
|||||||
throw "repo-lib: config.shell.banner.style must be one of simple or pretty"
|
throw "repo-lib: config.shell.banner.style must be one of simple or pretty"
|
||||||
else
|
else
|
||||||
banner;
|
banner;
|
||||||
|
toolsModule = import ./lib/tools.nix {
|
||||||
normalizeStrictTool =
|
lib = common.lib;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
hooksModule = import ./lib/hooks.nix {
|
||||||
normalizeLegacyTool =
|
inherit (common) lib sanitizeName;
|
||||||
pkgs: tool:
|
|
||||||
if tool ? package then
|
|
||||||
normalizeStrictTool pkgs tool
|
|
||||||
else
|
|
||||||
{
|
|
||||||
kind = "legacy";
|
|
||||||
name = tool.name;
|
|
||||||
command = tool.bin;
|
|
||||||
versionCommand = tool.versionCmd or "--version";
|
|
||||||
banner = {
|
|
||||||
color = tool.color or "YELLOW";
|
|
||||||
icon = tool.icon or null;
|
|
||||||
iconColor = tool.iconColor or null;
|
|
||||||
};
|
};
|
||||||
required = tool.required or false;
|
shellModule = import ./lib/shell.nix {
|
||||||
};
|
inherit (common)
|
||||||
|
lib
|
||||||
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";
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
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}'";
|
|
||||||
|
|
||||||
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
|
|
||||||
{ };
|
|
||||||
|
|
||||||
normalizeReleaseStep =
|
|
||||||
step:
|
|
||||||
if step ? writeFile then
|
|
||||||
{
|
|
||||||
kind = "writeFile";
|
|
||||||
path = step.writeFile.path;
|
|
||||||
text = step.writeFile.text;
|
|
||||||
runtimeInputs = [ ];
|
|
||||||
}
|
|
||||||
else if step ? replace then
|
|
||||||
{
|
|
||||||
kind = "replace";
|
|
||||||
path = step.replace.path;
|
|
||||||
regex = step.replace.regex;
|
|
||||||
replacement = step.replace.replacement;
|
|
||||||
runtimeInputs = [ ];
|
|
||||||
}
|
|
||||||
else if step ? run && builtins.isAttrs step.run then
|
|
||||||
{
|
|
||||||
kind = "run";
|
|
||||||
script = step.run.script;
|
|
||||||
runtimeInputs = step.run.runtimeInputs or [ ];
|
|
||||||
}
|
|
||||||
else if step ? run then
|
|
||||||
{
|
|
||||||
kind = "run";
|
|
||||||
script = step.run;
|
|
||||||
runtimeInputs = [ ];
|
|
||||||
}
|
|
||||||
else if step ? file then
|
|
||||||
{
|
|
||||||
kind = "writeFile";
|
|
||||||
path = step.file;
|
|
||||||
text = step.content;
|
|
||||||
runtimeInputs = [ ];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw "repo-lib: release step must contain one of writeFile, replace, or run";
|
|
||||||
|
|
||||||
releaseStepScript =
|
|
||||||
step:
|
|
||||||
if step.kind == "writeFile" then
|
|
||||||
''
|
|
||||||
target_path="$ROOT_DIR/${step.path}"
|
|
||||||
mkdir -p "$(dirname "$target_path")"
|
|
||||||
cat >"$target_path" << NIXEOF
|
|
||||||
${step.text}
|
|
||||||
NIXEOF
|
|
||||||
log "Generated version file: ${step.path}"
|
|
||||||
''
|
|
||||||
else if step.kind == "replace" then
|
|
||||||
''
|
|
||||||
target_path="$ROOT_DIR/${step.path}"
|
|
||||||
REPO_LIB_STEP_REGEX=$(cat <<'NIXEOF'
|
|
||||||
${step.regex}
|
|
||||||
NIXEOF
|
|
||||||
)
|
|
||||||
REPO_LIB_STEP_REPLACEMENT=$(cat <<NIXEOF
|
|
||||||
${step.replacement}
|
|
||||||
NIXEOF
|
|
||||||
)
|
|
||||||
export REPO_LIB_STEP_REGEX REPO_LIB_STEP_REPLACEMENT
|
|
||||||
perl - "$target_path" <<'REPO_LIB_PERL_REPLACE'
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
|
|
||||||
my $path = shift @ARGV;
|
|
||||||
my $regex_src = $ENV{"REPO_LIB_STEP_REGEX"} // q{};
|
|
||||||
my $template = $ENV{"REPO_LIB_STEP_REPLACEMENT"} // q{};
|
|
||||||
|
|
||||||
open my $in, q{<}, $path or die "failed to open $path: $!";
|
|
||||||
local $/ = undef;
|
|
||||||
my $content = <$in>;
|
|
||||||
close $in;
|
|
||||||
|
|
||||||
my $regex = qr/$regex_src/ms;
|
|
||||||
$content =~ s{$regex}{
|
|
||||||
my @cap = map { defined $_ ? $_ : q{} } ($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
|
||||||
my $result = $template;
|
|
||||||
$result =~ s{\\([1-9])}{$cap[$1 - 1]}ge;
|
|
||||||
$result;
|
|
||||||
}gems;
|
|
||||||
|
|
||||||
open my $out, q{>}, $path or die "failed to open $path for write: $!";
|
|
||||||
print {$out} $content;
|
|
||||||
close $out;
|
|
||||||
REPO_LIB_PERL_REPLACE
|
|
||||||
log "Updated ${step.path}"
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
${step.script}
|
|
||||||
'';
|
|
||||||
|
|
||||||
normalizeReleaseConfig =
|
|
||||||
raw:
|
|
||||||
let
|
|
||||||
hasLegacySteps = raw ? release;
|
|
||||||
hasStructuredSteps = raw ? steps;
|
|
||||||
steps =
|
|
||||||
if hasLegacySteps && hasStructuredSteps then
|
|
||||||
throw "repo-lib: pass either 'release' or 'steps' to mkRelease, not both"
|
|
||||||
else if hasStructuredSteps then
|
|
||||||
builtins.map normalizeReleaseStep raw.steps
|
|
||||||
else if hasLegacySteps then
|
|
||||||
builtins.map normalizeReleaseStep raw.release
|
|
||||||
else
|
|
||||||
[ ];
|
|
||||||
in
|
|
||||||
{
|
|
||||||
postVersion = raw.postVersion or "";
|
|
||||||
channels = raw.channels or defaultReleaseChannels;
|
|
||||||
runtimeInputs = (raw.runtimeInputs or [ ]) ++ (raw.extraRuntimeInputs or [ ]);
|
|
||||||
steps = steps;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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:
|
|
||||||
if tool.kind == "strict" then
|
|
||||||
''
|
|
||||||
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}
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
repo_lib_print_pretty_legacy_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 tool.command} \
|
|
||||||
${lib.escapeShellArg tool.versionCommand}
|
|
||||||
''
|
|
||||||
) 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:
|
|
||||||
if tool.kind == "strict" then
|
|
||||||
''
|
|
||||||
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}
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
repo_lib_print_simple_legacy_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 tool.command} \
|
|
||||||
${lib.escapeShellArg tool.versionCommand}
|
|
||||||
''
|
|
||||||
) 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;
|
|
||||||
in
|
|
||||||
rec {
|
|
||||||
systems = {
|
|
||||||
default = supportedSystems;
|
|
||||||
};
|
|
||||||
|
|
||||||
tools = rec {
|
|
||||||
fromPackage =
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
package,
|
|
||||||
exe ? null,
|
|
||||||
version ? { },
|
|
||||||
banner ? { },
|
|
||||||
required ? true,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
inherit
|
inherit
|
||||||
name
|
treefmt-nix
|
||||||
package
|
lefthookNix
|
||||||
exe
|
shellHookTemplatePath
|
||||||
version
|
;
|
||||||
banner
|
inherit (defaults)
|
||||||
required
|
defaultShellBanner
|
||||||
|
;
|
||||||
|
inherit normalizeShellBanner;
|
||||||
|
inherit (hooksModule)
|
||||||
|
normalizeLefthookConfig
|
||||||
|
parallelHookStageConfig
|
||||||
|
checkToLefthookConfig
|
||||||
|
hookToLefthookConfig
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
releaseModule = import ./lib/release.nix {
|
||||||
fromCommand =
|
inherit (common)
|
||||||
{
|
lib
|
||||||
name,
|
importPkgs
|
||||||
command,
|
;
|
||||||
version ? { },
|
|
||||||
banner ? { },
|
|
||||||
required ? true,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
inherit
|
inherit
|
||||||
name
|
nixpkgs
|
||||||
command
|
releaseScriptPath
|
||||||
version
|
;
|
||||||
banner
|
inherit (defaults)
|
||||||
required
|
defaultReleaseChannels
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
repoModule = import ./lib/repo.nix {
|
||||||
simple =
|
|
||||||
name: package: args:
|
|
||||||
fromPackage {
|
|
||||||
inherit name package;
|
|
||||||
version.args = args;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkDevShell =
|
|
||||||
{
|
|
||||||
system,
|
|
||||||
src ? ./.,
|
|
||||||
nixpkgsInput ? nixpkgs,
|
|
||||||
extraPackages ? [ ],
|
|
||||||
preToolHook ? "",
|
|
||||||
extraShellHook ? "",
|
|
||||||
additionalHooks ? { },
|
|
||||||
lefthook ? { },
|
|
||||||
tools ? [ ],
|
|
||||||
includeStandardPackages ? true,
|
|
||||||
formatters ? { },
|
|
||||||
formatterSettings ? { },
|
|
||||||
features ? { },
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
pkgs = importPkgs nixpkgsInput system;
|
|
||||||
oxfmtEnabled = features.oxfmt or false;
|
|
||||||
legacyTools = builtins.map (tool: normalizeLegacyTool pkgs tool) tools;
|
|
||||||
duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) legacyTools);
|
|
||||||
normalizedFormatting = {
|
|
||||||
programs =
|
|
||||||
(lib.optionalAttrs oxfmtEnabled {
|
|
||||||
oxfmt.enable = true;
|
|
||||||
})
|
|
||||||
// formatters;
|
|
||||||
settings = formatterSettings;
|
|
||||||
};
|
|
||||||
shellConfig = {
|
|
||||||
env = { };
|
|
||||||
extraShellText = extraShellHook;
|
|
||||||
allowImpureBootstrap = true;
|
|
||||||
bootstrap = preToolHook;
|
|
||||||
banner = defaultShellBanner;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
if duplicateToolNames != [ ] then
|
|
||||||
throw "repo-lib: duplicate tool names: ${lib.concatStringsSep ", " duplicateToolNames}"
|
|
||||||
else
|
|
||||||
buildShellArtifacts {
|
|
||||||
inherit
|
inherit
|
||||||
pkgs
|
flake-parts
|
||||||
system
|
nixpkgs
|
||||||
src
|
|
||||||
includeStandardPackages
|
|
||||||
;
|
;
|
||||||
formatting = normalizedFormatting;
|
inherit (common)
|
||||||
rawHookEntries = additionalHooks;
|
lib
|
||||||
lefthookConfig = lefthook;
|
importPkgs
|
||||||
shellConfig = shellConfig;
|
duplicateStrings
|
||||||
tools = legacyTools;
|
mergeUniqueAttrs
|
||||||
extraPackages =
|
;
|
||||||
extraPackages
|
inherit (defaults)
|
||||||
++ lib.optionals oxfmtEnabled [
|
supportedSystems
|
||||||
pkgs.oxfmt
|
defaultReleaseChannels
|
||||||
pkgs.oxlint
|
;
|
||||||
];
|
inherit (toolsModule)
|
||||||
};
|
normalizeStrictTool
|
||||||
|
;
|
||||||
mkRelease =
|
inherit (hooksModule)
|
||||||
{
|
normalizeLefthookConfig
|
||||||
system,
|
;
|
||||||
nixpkgsInput ? nixpkgs,
|
inherit normalizeShellBanner;
|
||||||
...
|
inherit (shellModule)
|
||||||
}@rawArgs:
|
buildShellArtifacts
|
||||||
let
|
;
|
||||||
pkgs = importPkgs nixpkgsInput system;
|
inherit (releaseModule)
|
||||||
release = normalizeReleaseConfig rawArgs;
|
mkRelease
|
||||||
channelList = lib.concatStringsSep " " release.channels;
|
|
||||||
releaseStepsScript = lib.concatMapStrings releaseStepScript release.steps;
|
|
||||||
script =
|
|
||||||
builtins.replaceStrings
|
|
||||||
[
|
|
||||||
"__CHANNEL_LIST__"
|
|
||||||
"__RELEASE_STEPS__"
|
|
||||||
"__POST_VERSION__"
|
|
||||||
]
|
|
||||||
[
|
|
||||||
channelList
|
|
||||||
releaseStepsScript
|
|
||||||
release.postVersion
|
|
||||||
]
|
|
||||||
(builtins.readFile releaseScriptPath);
|
|
||||||
in
|
|
||||||
pkgs.writeShellApplication {
|
|
||||||
name = "release";
|
|
||||||
runtimeInputs =
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
git
|
|
||||||
gnugrep
|
|
||||||
gawk
|
|
||||||
gnused
|
|
||||||
coreutils
|
|
||||||
perl
|
|
||||||
]
|
|
||||||
++ release.runtimeInputs
|
|
||||||
++ lib.concatMap (step: step.runtimeInputs or [ ]) release.steps;
|
|
||||||
text = script;
|
|
||||||
};
|
|
||||||
|
|
||||||
mkRepo =
|
|
||||||
{
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
src ? ./.,
|
|
||||||
systems ? supportedSystems,
|
|
||||||
config ? { },
|
|
||||||
perSystem ? (
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
system,
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
}:
|
|
||||||
{ }
|
|
||||||
),
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
normalizedConfig = normalizeRepoConfig config;
|
|
||||||
systemResults = lib.genAttrs systems (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = importPkgs nixpkgs system;
|
|
||||||
perSystemResult = {
|
|
||||||
tools = [ ];
|
|
||||||
shell = { };
|
|
||||||
checks = { };
|
|
||||||
lefthook = { };
|
|
||||||
packages = { };
|
|
||||||
apps = { };
|
|
||||||
}
|
|
||||||
// perSystem {
|
|
||||||
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 = nixpkgs;
|
|
||||||
channels = normalizedConfig.release.channels;
|
|
||||||
steps = normalizedConfig.release.steps;
|
|
||||||
postVersion = normalizedConfig.release.postVersion;
|
|
||||||
runtimeInputs = normalizedConfig.release.runtimeInputs;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit env;
|
systems.default = defaults.supportedSystems;
|
||||||
packages = mergeUniqueAttrs "package" releasePackages perSystemResult.packages;
|
inherit (toolsModule) tools;
|
||||||
apps = perSystemResult.apps;
|
inherit (repoModule) normalizeRepoConfig mkRepo;
|
||||||
}
|
inherit (releaseModule) mkRelease;
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells = lib.genAttrs systems (system: {
|
|
||||||
default = systemResults.${system}.env.shell;
|
|
||||||
});
|
|
||||||
|
|
||||||
checks = lib.genAttrs systems (system: systemResults.${system}.env.checks);
|
|
||||||
|
|
||||||
formatter = lib.genAttrs systems (system: systemResults.${system}.env.formatter);
|
|
||||||
packages = lib.genAttrs systems (system: systemResults.${system}.packages);
|
|
||||||
apps = lib.genAttrs systems (system: systemResults.${system}.apps);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -70,39 +70,6 @@ repo_lib_capture_tool() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
repo_lib_capture_legacy_tool() {
|
|
||||||
local required="$1"
|
|
||||||
local command_name="$2"
|
|
||||||
local version_command="$3"
|
|
||||||
|
|
||||||
local output=""
|
|
||||||
local version=""
|
|
||||||
|
|
||||||
REPO_LIB_TOOL_VERSION=""
|
|
||||||
REPO_LIB_TOOL_ERROR=""
|
|
||||||
|
|
||||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
|
||||||
REPO_LIB_TOOL_ERROR="missing command"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! output="$(sh -c "$command_name $version_command" 2>&1)"; then
|
|
||||||
REPO_LIB_TOOL_ERROR="probe failed"
|
|
||||||
printf "%s\n" "$output" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
version="$(printf '%s\n' "$output" | head -n 1 | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
|
||||||
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() {
|
repo_lib_print_simple_header() {
|
||||||
local title_color_name="$1"
|
local title_color_name="$1"
|
||||||
local icon="$2"
|
local icon="$2"
|
||||||
@@ -164,42 +131,6 @@ repo_lib_print_simple_tool() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
repo_lib_print_simple_legacy_tool() {
|
|
||||||
local name="$1"
|
|
||||||
local color_name="$2"
|
|
||||||
local icon="$3"
|
|
||||||
local icon_color_name="$4"
|
|
||||||
local required="$5"
|
|
||||||
local command_name="$6"
|
|
||||||
local version_command="$7"
|
|
||||||
|
|
||||||
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_legacy_tool "$required" "$command_name" "$version_command"; 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() {
|
repo_lib_print_pretty_header() {
|
||||||
local border_color_name="$1"
|
local border_color_name="$1"
|
||||||
local title_color_name="$2"
|
local title_color_name="$2"
|
||||||
@@ -286,45 +217,6 @@ repo_lib_print_pretty_tool() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
repo_lib_print_pretty_legacy_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 command_name="$7"
|
|
||||||
local version_command="$8"
|
|
||||||
|
|
||||||
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_legacy_tool "$required" "$command_name" "$version_command"; 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() {
|
repo_lib_print_pretty_footer() {
|
||||||
local border_color_name="$1"
|
local border_color_name="$1"
|
||||||
local border_color="${!border_color_name:-$BLUE}"
|
local border_color="${!border_color_name:-$BLUE}"
|
||||||
|
|||||||
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')
|
||||||
3
template/.gitignore
vendored
3
template/.gitignore
vendored
@@ -1,9 +1,12 @@
|
|||||||
.direnv/
|
.direnv/
|
||||||
|
.moon/cache/
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
lefthook.yml
|
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"
|
||||||
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,10 +1,9 @@
|
|||||||
# 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";
|
||||||
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.4.0";
|
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.5.1";
|
||||||
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,102 +19,64 @@
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
# includeStandardPackages = false;
|
|
||||||
|
|
||||||
shell = {
|
shell = {
|
||||||
env = {
|
banner = {
|
||||||
# FOO = "bar";
|
style = "pretty";
|
||||||
|
icon = "☾";
|
||||||
|
title = "Moonrepo shell ready";
|
||||||
|
titleColor = "GREEN";
|
||||||
|
subtitle = "Bun + TypeScript + Varlock";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
|
borderColor = "BLUE";
|
||||||
};
|
};
|
||||||
|
|
||||||
extraShellText = ''
|
extraShellText = ''
|
||||||
# any repo-specific shell setup here
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Impure bootstrap is available as an explicit escape hatch.
|
bootstrap = ''
|
||||||
# bootstrap = ''
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
# export GOBIN="$PWD/.tools/bin"
|
|
||||||
# export PATH="$GOBIN:$PATH"
|
export BUN_INSTALL_GLOBAL_DIR="$repo_root/.tools/bun/install/global"
|
||||||
# '';
|
export BUN_INSTALL_BIN="$repo_root/.tools/bun/bin"
|
||||||
# allowImpureBootstrap = true;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
formatting = {
|
formatting = {
|
||||||
# nixfmt is enabled by default and wired into lefthook.
|
|
||||||
programs = {
|
programs = {
|
||||||
# shfmt.enable = true;
|
oxfmt.enable = true;
|
||||||
# gofmt.enable = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
# shfmt.options = [ "-i" "2" "-s" "-w" ];
|
oxfmt.excludes = [
|
||||||
};
|
"*.css"
|
||||||
};
|
"*.graphql"
|
||||||
|
"*.hbs"
|
||||||
# These checks become lefthook commands in the generated `lefthook.yml`.
|
"*.html"
|
||||||
# repo-lib runs `pre-commit` and `pre-push` hook commands in parallel.
|
"*.md"
|
||||||
# It also sets `output = [ "failure" "summary" ]` by default.
|
"*.mdx"
|
||||||
checks = {
|
"*.mustache"
|
||||||
tests = {
|
"*.scss"
|
||||||
command = "echo 'No tests defined yet.'";
|
"*.vue"
|
||||||
stage = "pre-push";
|
"*.yaml"
|
||||||
passFilenames = false;
|
"*.yml"
|
||||||
};
|
|
||||||
|
|
||||||
# fmt = {
|
|
||||||
# command = "nix fmt";
|
|
||||||
# stage = "pre-commit";
|
|
||||||
# passFilenames = false;
|
|
||||||
# };
|
|
||||||
};
|
|
||||||
|
|
||||||
# For advanced Lefthook fields like `stage_fixed`, use raw passthrough.
|
|
||||||
# repo-lib merges this after generated checks.
|
|
||||||
# lefthook.pre-push.commands.tests.stage_fixed = true;
|
|
||||||
# lefthook.commit-msg.commands.commitlint = {
|
|
||||||
# run = "pnpm commitlint --edit {1}";
|
|
||||||
# stage_fixed = true;
|
|
||||||
# };
|
|
||||||
|
|
||||||
# repo-lib also installs built-in hooks for:
|
|
||||||
# - treefmt / nixfmt on `pre-commit`
|
|
||||||
# - gitleaks on `pre-commit`
|
|
||||||
# - gitlint on `commit-msg`
|
|
||||||
|
|
||||||
# release = null;
|
|
||||||
release = {
|
|
||||||
steps = [
|
|
||||||
# Write a generated version file during release.
|
|
||||||
# {
|
|
||||||
# writeFile = {
|
|
||||||
# path = "src/version.ts";
|
|
||||||
# text = ''
|
|
||||||
# export const APP_VERSION = "$FULL_VERSION" as const;
|
|
||||||
# '';
|
|
||||||
# };
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Replace a version string while preserving surrounding captures.
|
|
||||||
# {
|
|
||||||
# replace = {
|
|
||||||
# path = "README.md";
|
|
||||||
# regex = ''^(version = ")[^"]*(")$'';
|
|
||||||
# replacement = ''\1$FULL_VERSION\2'';
|
|
||||||
# };
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Run any extra release step with declared runtime inputs.
|
|
||||||
# {
|
|
||||||
# run = {
|
|
||||||
# runtimeInputs = [ pkgs.git ];
|
|
||||||
# script = ''
|
|
||||||
# git status --short
|
|
||||||
# '';
|
|
||||||
# };
|
|
||||||
# }
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
release = {
|
||||||
|
steps = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
@@ -137,37 +98,58 @@
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
# (repo-lib.lib.tools.fromPackage {
|
(repo-lib.lib.tools.fromPackage {
|
||||||
# name = "Go";
|
name = "Bun";
|
||||||
# package = pkgs.go;
|
package = pkgs.bun;
|
||||||
# version.args = [ "version" ];
|
version.args = [ "--version" ];
|
||||||
# banner.color = "CYAN";
|
banner = {
|
||||||
# })
|
color = "YELLOW";
|
||||||
|
icon = "";
|
||||||
|
};
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
shell.packages = [
|
shell.packages = [
|
||||||
self.packages.${system}.release
|
self.packages.${system}.release
|
||||||
# pkgs.go
|
pkgs.bun
|
||||||
# pkgs.bun
|
pkgs.openbao
|
||||||
|
pkgs.oxfmt
|
||||||
|
pkgs.oxlint
|
||||||
];
|
];
|
||||||
|
|
||||||
# checks.lint = {
|
checks.format = {
|
||||||
# command = "bun test";
|
command = "oxfmt --check .";
|
||||||
# stage = "pre-push";
|
stage = "pre-commit";
|
||||||
# passFilenames = false;
|
passFilenames = false;
|
||||||
# runtimeInputs = [ pkgs.bun ];
|
runtimeInputs = [ pkgs.oxfmt ];
|
||||||
# };
|
};
|
||||||
|
|
||||||
# checks.generated = {
|
checks.typecheck = {
|
||||||
# command = "git diff --exit-code";
|
command = "bun run typecheck";
|
||||||
# stage = "pre-commit";
|
stage = "pre-push";
|
||||||
# passFilenames = false;
|
passFilenames = false;
|
||||||
# };
|
runtimeInputs = [ pkgs.bun ];
|
||||||
|
};
|
||||||
|
|
||||||
# packages.my-tool = pkgs.writeShellApplication {
|
checks.env-check = {
|
||||||
# name = "my-tool";
|
command = "bun run env:check";
|
||||||
# text = ''echo hello'';
|
stage = "pre-push";
|
||||||
# };
|
passFilenames = false;
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.bun
|
||||||
|
pkgs.openbao
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
checks.env-scan = {
|
||||||
|
command = "bun run env:scan";
|
||||||
|
stage = "pre-commit";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
1431
tests/release.sh
1431
tests/release.sh
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user