Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac9f784602 | ||
|
|
4565a71863 | ||
|
|
3b38bc8f38 | ||
|
|
a5ab22f426 | ||
|
|
54b54fdece | ||
|
|
60d0c27db7 | ||
|
|
b6528466e0 |
@@ -17,7 +17,7 @@ Audit and replacement review: [`docs/reviews/2026-03-21-repo-lib-audit.md`](/Use
|
|||||||
## 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.5.1#default' --refresh
|
+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.6.1#default' --refresh
|
||||||
```
|
```
|
||||||
|
|
||||||
The generated repo includes:
|
The generated repo includes:
|
||||||
@@ -34,7 +34,7 @@ The generated repo includes:
|
|||||||
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.5.1";
|
+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.6.1";
|
||||||
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -73,14 +73,6 @@ func parseReleaseCLIArgs(args []string) ([]string, release.ExecutionOptions, boo
|
|||||||
switch arg {
|
switch arg {
|
||||||
case "select":
|
case "select":
|
||||||
selectMode = true
|
selectMode = true
|
||||||
case "--dry-run":
|
|
||||||
execution.DryRun = true
|
|
||||||
case "--commit":
|
|
||||||
execution.Commit = true
|
|
||||||
case "--tag":
|
|
||||||
execution.Tag = true
|
|
||||||
case "--push":
|
|
||||||
execution.Push = true
|
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(arg, "--") {
|
if strings.HasPrefix(arg, "--") {
|
||||||
return nil, release.ExecutionOptions{}, false, fmt.Errorf("unknown flag %q", arg)
|
return nil, release.ExecutionOptions{}, false, fmt.Errorf("unknown flag %q", arg)
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ func translateReplacementBackrefs(raw string) string {
|
|||||||
|
|
||||||
for i := 0; i < len(raw); i++ {
|
for i := 0; i < len(raw); i++ {
|
||||||
if raw[i] == '\\' && i+1 < len(raw) && raw[i+1] >= '1' && raw[i+1] <= '9' {
|
if raw[i] == '\\' && i+1 < len(raw) && raw[i+1] >= '1' && raw[i+1] <= '9' {
|
||||||
b.WriteByte('$')
|
b.WriteString("${")
|
||||||
b.WriteByte(raw[i+1])
|
b.WriteByte(raw[i+1])
|
||||||
|
b.WriteByte('}')
|
||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package release
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTranslateReplacementBackrefsWrapsCaptureNumbers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := translateReplacementBackrefs(`\1git+https://example.test/ref\2`)
|
||||||
|
want := `${1}git+https://example.test/ref${2}`
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("translateReplacementBackrefs() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -256,13 +256,16 @@ func TestRunnerExecutesReleaseFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
func TestRunnerAlwaysCommitsTagsAndPushes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
remote := filepath.Join(t.TempDir(), "remote.git")
|
||||||
mustRun(t, root, "git", "init")
|
mustRun(t, root, "git", "init")
|
||||||
mustRun(t, root, "git", "config", "user.name", "Release Test")
|
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", "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 {
|
if err := os.WriteFile(filepath.Join(root, "flake.nix"), []byte("{ outputs = { self }: {}; }\n"), 0o644); err != nil {
|
||||||
t.Fatalf("WriteFile(flake.nix): %v", err)
|
t.Fatalf("WriteFile(flake.nix): %v", err)
|
||||||
}
|
}
|
||||||
@@ -271,6 +274,9 @@ func TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
mustRun(t, root, "git", "add", "-A")
|
mustRun(t, root, "git", "add", "-A")
|
||||||
mustRun(t, root, "git", "commit", "-m", "init")
|
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()
|
binDir := t.TempDir()
|
||||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||||
@@ -296,58 +302,20 @@ func TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
|||||||
|
|
||||||
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.1\nstable\n0\n")
|
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.1\nstable\n0\n")
|
||||||
status := strings.TrimSpace(mustOutput(t, root, "git", "status", "--short"))
|
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 != "" {
|
if status != "" {
|
||||||
t.Fatalf("git status --short = %q, want empty", 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())
|
tagList := strings.TrimSpace(mustOutput(t, root, "git", "tag", "--list", "v1.0.1"))
|
||||||
|
if tagList != "v1.0.1" {
|
||||||
|
t.Fatalf("git tag --list v1.0.1 = %q, want v1.0.1", tagList)
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := strings.TrimSpace(mustOutput(t, root, "git", "branch", "--show-current"))
|
||||||
|
remoteHead := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "origin/"+branch))
|
||||||
|
localHead := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "HEAD"))
|
||||||
|
if remoteHead != localHead {
|
||||||
|
t.Fatalf("origin/%s = %q, want %q", branch, remoteHead, localHead)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExecutionOptions struct {
|
type ExecutionOptions struct {
|
||||||
DryRun bool
|
|
||||||
Commit bool
|
Commit bool
|
||||||
Tag bool
|
Tag bool
|
||||||
Push bool
|
Push bool
|
||||||
@@ -31,13 +30,11 @@ type Runner struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o ExecutionOptions) Normalize() ExecutionOptions {
|
func (o ExecutionOptions) Normalize() ExecutionOptions {
|
||||||
if o.Push {
|
return ExecutionOptions{
|
||||||
o.Commit = true
|
Commit: true,
|
||||||
|
Tag: true,
|
||||||
|
Push: true,
|
||||||
}
|
}
|
||||||
if o.Tag {
|
|
||||||
o.Commit = true
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) Run(args []string) error {
|
func (r *Runner) Run(args []string) error {
|
||||||
@@ -60,11 +57,6 @@ func (r *Runner) Run(args []string) error {
|
|||||||
return err
|
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 {
|
if err := requireCleanGit(rootDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -114,10 +106,6 @@ func (r *Runner) finalizeRelease(rootDir string, version Version, execution Exec
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !execution.Commit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "add", "-A"); err != nil {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "add", "-A"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -127,34 +115,15 @@ func (r *Runner) finalizeRelease(rootDir string, version Version, execution Exec
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if execution.Tag {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "tag", version.Tag()); err != nil {
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "tag", version.Tag()); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !execution.Push {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push"); err != nil {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if execution.Tag {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push", "--tags"); err != nil {
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push", "--tags"); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func SelectCommand(config Config) ([]string, bool, error) {
|
|||||||
return nil, false, fmt.Errorf("no release commands available for current version %s", versionFile.Version.String())
|
return nil, false, fmt.Errorf("no release commands available for current version %s", versionFile.Version.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
model := newCommandPickerModel(versionFile.Version, options)
|
model := newCommandPickerModel(config, versionFile.Version)
|
||||||
finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -111,18 +111,6 @@ func formatReleaseCommand(args []string) string {
|
|||||||
func formatReleaseCommandWithExecution(args []string, execution ExecutionOptions) string {
|
func formatReleaseCommandWithExecution(args []string, execution ExecutionOptions) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
parts = append(parts, "release")
|
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 {
|
if len(args) == 0 {
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
@@ -199,7 +187,6 @@ func buildPreview(config Config, current Version, args []string, next Version) s
|
|||||||
" git commit: "+yesNo(execution.Commit),
|
" git commit: "+yesNo(execution.Commit),
|
||||||
" git tag: "+yesNo(execution.Tag),
|
" git tag: "+yesNo(execution.Tag),
|
||||||
" git push: "+yesNo(execution.Push),
|
" git push: "+yesNo(execution.Push),
|
||||||
" dry run: "+yesNo(execution.DryRun),
|
|
||||||
)
|
)
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
@@ -224,19 +211,32 @@ func capitalize(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type commandPickerModel struct {
|
type commandPickerModel struct {
|
||||||
current Version
|
config Config
|
||||||
options []CommandOption
|
current Version
|
||||||
cursor int
|
width int
|
||||||
width int
|
height int
|
||||||
height int
|
focusSection int
|
||||||
confirmed bool
|
focusIndex int
|
||||||
selected CommandOption
|
bumpOptions []selectionOption
|
||||||
|
channelOptions []selectionOption
|
||||||
|
bumpCursor int
|
||||||
|
channelCursor int
|
||||||
|
confirmed bool
|
||||||
|
selected CommandOption
|
||||||
|
err string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommandPickerModel(current Version, options []CommandOption) commandPickerModel {
|
type selectionOption struct {
|
||||||
|
Label string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommandPickerModel(config Config, current Version) commandPickerModel {
|
||||||
return commandPickerModel{
|
return commandPickerModel{
|
||||||
current: current,
|
config: config,
|
||||||
options: options,
|
current: current,
|
||||||
|
bumpOptions: buildBumpOptions(current),
|
||||||
|
channelOptions: buildChannelOptions(current, config.AllowedChannels),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,17 +253,20 @@ func (m commandPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q", "esc":
|
case "ctrl+c", "q", "esc":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "up", "k":
|
case "up", "k", "shift+tab":
|
||||||
if m.cursor > 0 {
|
m.moveFocus(-1)
|
||||||
m.cursor--
|
case "down", "j", "tab":
|
||||||
}
|
m.moveFocus(1)
|
||||||
case "down", "j":
|
case " ":
|
||||||
if m.cursor < len(m.options)-1 {
|
m.selectFocused()
|
||||||
m.cursor++
|
|
||||||
}
|
|
||||||
case "enter":
|
case "enter":
|
||||||
|
option, err := m.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
m.confirmed = true
|
m.confirmed = true
|
||||||
m.selected = m.options[m.cursor]
|
m.selected = option
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,28 +274,180 @@ func (m commandPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m commandPickerModel) View() string {
|
func (m commandPickerModel) View() string {
|
||||||
if len(m.options) == 0 {
|
if len(m.bumpOptions) == 0 || len(m.channelOptions) == 0 {
|
||||||
return "No release commands available.\n"
|
return "No release commands available.\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
preview := m.options[m.cursor].Preview
|
preview := m.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())
|
header := fmt.Sprintf("Release command picker\nCurrent version: %s\nUse up/down to move through options, Space to select, Enter to run, q to cancel.\n", m.current.String())
|
||||||
|
sections := strings.Join([]string{
|
||||||
listLines := make([]string, 0, len(m.options)+1)
|
m.renderSection("Bump type", m.bumpOptions, m.bumpCursor, m.focusSection == 0, m.focusedOptionIndex()),
|
||||||
listLines = append(listLines, "Commands")
|
"",
|
||||||
for i, option := range m.options {
|
m.renderSection("Channel", m.channelOptions, m.channelCursor, m.focusSection == 1, m.focusedOptionIndex()),
|
||||||
cursor := " "
|
}, "\n")
|
||||||
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 {
|
if m.width >= 100 {
|
||||||
return header + "\n" + renderColumns(list, preview, m.width)
|
return header + "\n" + renderColumns(sections, preview, m.width)
|
||||||
}
|
}
|
||||||
return header + "\n" + list + "\n\n" + preview + "\n"
|
return header + "\n" + sections + "\n\n" + preview + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBumpOptions(current Version) []selectionOption {
|
||||||
|
options := []selectionOption{
|
||||||
|
{Label: "Patch", Value: "patch"},
|
||||||
|
{Label: "Minor", Value: "minor"},
|
||||||
|
{Label: "Major", Value: "major"},
|
||||||
|
}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "None",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "None",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildChannelOptions(current Version, allowedChannels []string) []selectionOption {
|
||||||
|
options := []selectionOption{{
|
||||||
|
Label: "Current",
|
||||||
|
Value: current.Channel,
|
||||||
|
}}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "Stable",
|
||||||
|
Value: "stable",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, channel := range allowedChannels {
|
||||||
|
if channel == current.Channel {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: capitalize(channel),
|
||||||
|
Value: channel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *commandPickerModel) moveFocus(delta int) {
|
||||||
|
total := len(m.bumpOptions) + len(m.channelOptions)
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
index := wrapIndex(m.focusIndex+delta, total)
|
||||||
|
m.focusIndex = index
|
||||||
|
|
||||||
|
if index < len(m.bumpOptions) {
|
||||||
|
m.focusSection = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.focusSection = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *commandPickerModel) selectFocused() {
|
||||||
|
if m.focusSection == 0 {
|
||||||
|
m.bumpCursor = m.focusedOptionIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.channelCursor = m.focusedOptionIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapIndex(idx int, size int) int {
|
||||||
|
if size == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for idx < 0 {
|
||||||
|
idx += size
|
||||||
|
}
|
||||||
|
return idx % size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) focusedOptionIndex() int {
|
||||||
|
if m.focusSection == 0 {
|
||||||
|
return m.focusIndex
|
||||||
|
}
|
||||||
|
return m.focusIndex - len(m.bumpOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) renderSection(title string, options []selectionOption, cursor int, focused bool, focusedIndex int) string {
|
||||||
|
lines := []string{title}
|
||||||
|
for i, option := range options {
|
||||||
|
pointer := " "
|
||||||
|
if focused && i == focusedIndex {
|
||||||
|
pointer = ">"
|
||||||
|
}
|
||||||
|
radio := "( )"
|
||||||
|
if i == cursor {
|
||||||
|
radio = "(*)"
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%s %s %s", pointer, radio, option.Label))
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) selectedArgs() []string {
|
||||||
|
bump := m.bumpOptions[m.bumpCursor].Value
|
||||||
|
channel := m.channelOptions[m.channelCursor].Value
|
||||||
|
|
||||||
|
if bump == "" {
|
||||||
|
if channel == "stable" {
|
||||||
|
return []string{"stable"}
|
||||||
|
}
|
||||||
|
if channel == m.current.Channel {
|
||||||
|
if channel == "stable" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{channel}
|
||||||
|
}
|
||||||
|
return []string{channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == m.current.Channel || (channel == "stable" && m.current.Channel == "stable") {
|
||||||
|
return []string{bump}
|
||||||
|
}
|
||||||
|
return []string{bump, channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) selectedOption() (CommandOption, error) {
|
||||||
|
args := m.selectedArgs()
|
||||||
|
next, err := ResolveNextVersion(m.current, args, m.config.AllowedChannels)
|
||||||
|
if err != nil {
|
||||||
|
return CommandOption{}, err
|
||||||
|
}
|
||||||
|
return CommandOption{
|
||||||
|
Title: titleForArgs(args),
|
||||||
|
Description: descriptionForArgs(m.current, args, next),
|
||||||
|
Command: formatReleaseCommand(args),
|
||||||
|
Args: append([]string(nil), args...),
|
||||||
|
NextVersion: next,
|
||||||
|
Preview: buildPreview(m.config, m.current, args, next),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) preview() string {
|
||||||
|
option, err := m.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
lines := []string{
|
||||||
|
"Command",
|
||||||
|
" " + formatReleaseCommand(m.selectedArgs()),
|
||||||
|
"",
|
||||||
|
"Selection",
|
||||||
|
" " + err.Error(),
|
||||||
|
}
|
||||||
|
if m.err != "" {
|
||||||
|
lines = append(lines, "", "Error", " "+m.err)
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
return option.Preview
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderColumns(left string, right string, width int) string {
|
func renderColumns(left string, right string, width int) string {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package release
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildCommandOptionsForStableVersion(t *testing.T) {
|
func TestBuildCommandOptionsForStableVersion(t *testing.T) {
|
||||||
@@ -81,6 +83,125 @@ func TestBuildCommandOptionsForPrereleaseVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerSelectionForStableVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
}, MustParseVersion(t, "1.2.3"))
|
||||||
|
|
||||||
|
model.bumpCursor = 1
|
||||||
|
model.channelCursor = 0
|
||||||
|
|
||||||
|
option, err := model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "minor" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "minor")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.3.0" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.3.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bumpCursor = 3
|
||||||
|
model.channelCursor = 1
|
||||||
|
|
||||||
|
option, err = model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(channel only): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "alpha" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "alpha")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.4-alpha.1" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.4-alpha.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerSelectionForPrereleaseVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc"},
|
||||||
|
}, MustParseVersion(t, "1.2.3-beta.2"))
|
||||||
|
|
||||||
|
model.bumpCursor = 3
|
||||||
|
model.channelCursor = 0
|
||||||
|
|
||||||
|
option, err := model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(current prerelease): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "beta" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "beta")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.3-beta.3" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.3-beta.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.channelCursor = 1
|
||||||
|
|
||||||
|
option, err = model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(promote): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "stable" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "stable")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.3" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bumpCursor = 0
|
||||||
|
|
||||||
|
if _, err := model.selectedOption(); err == nil {
|
||||||
|
t.Fatalf("selectedOption(patch stable from prerelease) succeeded, want error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerFocusMovesAcrossSections(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
}, MustParseVersion(t, "1.2.3"))
|
||||||
|
|
||||||
|
next, _ := model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 0 || model.focusIndex != 1 || model.bumpCursor != 0 {
|
||||||
|
t.Fatalf("after first down: focusSection=%d focusIndex=%d bumpCursor=%d", model.focusSection, model.focusIndex, model.bumpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 1 || model.focusIndex != 4 || model.channelCursor != 0 {
|
||||||
|
t.Fatalf("after moving into channel section: focusSection=%d focusIndex=%d channelCursor=%d", model.focusSection, model.focusIndex, model.channelCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.channelCursor != 0 {
|
||||||
|
t.Fatalf("space changed selection unexpectedly: channelCursor=%d", model.channelCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyUp})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 0 || model.focusIndex != 3 || model.bumpCursor != 0 {
|
||||||
|
t.Fatalf("after moving back up: focusSection=%d focusIndex=%d bumpCursor=%d", model.focusSection, model.focusIndex, model.bumpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.bumpCursor != 3 {
|
||||||
|
t.Fatalf("space did not update bump selection: bumpCursor=%d", model.bumpCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func findOptionByCommand(options []CommandOption, command string) (CommandOption, bool) {
|
func findOptionByCommand(options []CommandOption, command string) (CommandOption, bool) {
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
if option.Command == command {
|
if option.Command == command {
|
||||||
|
|||||||
@@ -1,48 +1,54 @@
|
|||||||
---
|
---
|
||||||
name: repo-lib-consumer
|
name: repo-lib-consumer
|
||||||
description: Edit or extend repos that consume `repo-lib` through `repo-lib.lib.mkRepo`, `mkDevShell`, or `mkRelease`. Use when Codex needs to add or change tools, shell packages, checks or test phases, formatters, release steps, release channels, bootstrap hooks, or release automation in a Nix flake built on repo-lib.
|
description: Edit or extend repos that consume `repo-lib` through `repo-lib.lib.mkRepo`, `repo-lib.lib.mkRelease`, or a template generated from this library. Use when Codex needs to add or change tools, shell banner or bootstrap behavior, shell packages, checks, raw lefthook config, formatters, release steps, version metadata, release channels, or release automation in a Nix flake built on repo-lib.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Repo Lib Consumer
|
# Repo Lib Consumer
|
||||||
|
|
||||||
Use this skill to make idiomatic changes in a repo that already depends on `repo-lib`.
|
Use this skill when changing a repo that already depends on `repo-lib`.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Detect the integration style.
|
1. Detect the integration style.
|
||||||
Search for `repo-lib.lib.mkRepo`, `repo-lib.lib.mkDevShell`, `repo-lib.lib.mkRelease`, or `inputs.repo-lib`.
|
Search for `repo-lib.lib.mkRepo`, `repo-lib.lib.mkRelease`, or `inputs.repo-lib`.
|
||||||
|
|
||||||
2. Prefer the repo's current abstraction level.
|
2. Prefer the repo's current abstraction level.
|
||||||
If the repo already uses `mkRepo`, stay on `mkRepo`.
|
If the repo uses `mkRepo`, keep edits inside `config` and `perSystem`.
|
||||||
If the repo still uses `mkDevShell` or `mkRelease`, preserve that style unless the user asked to migrate.
|
If the repo uses `mkRelease` directly, preserve that style unless the user asked to migrate.
|
||||||
|
|
||||||
3. Load the right reference before editing.
|
3. Load the right reference before editing.
|
||||||
Read `references/api.md` for exact option names, defaults, generated outputs, and limitations.
|
Read `references/api.md` for exact option names, merge points, generated outputs, hook limitations, and release behavior.
|
||||||
Read `references/recipes.md` for common edits such as adding a tool, adding a test phase, wiring release file updates, or handling webhooks.
|
Read `references/recipes.md` for concrete change patterns such as adding a tool, adding a check, wiring `commit-msg`, changing the banner, or updating release-managed files.
|
||||||
|
|
||||||
4. Follow repo-lib conventions.
|
4. Follow repo-lib conventions.
|
||||||
Add bannered CLIs through `perSystem.tools`, not `shell.packages`.
|
Add bannered CLIs through `perSystem.tools`, not `shell.packages`.
|
||||||
Use `shell.packages` for packages that should be present in the shell but not shown in the banner.
|
Use `shell.packages` for packages that should exist in the shell but not in the banner.
|
||||||
Keep shells pure-first; only use `bootstrap` with `allowImpureBootstrap = true`.
|
Keep shells pure-first; only use `bootstrap` with `allowImpureBootstrap = true`.
|
||||||
Prefer structured `release.steps` over free-form shell when the task fits `writeFile` or `replace`.
|
Prefer `config.checks` for simple `pre-commit` and `pre-push` commands.
|
||||||
|
Use raw `config.lefthook` or `perSystem.lefthook` when the task needs `commit-msg` or extra lefthook fields.
|
||||||
|
Prefer structured `release.steps` over shell hooks; current step kinds are `writeFile`, `replace`, `versionMetaSet`, and `versionMetaUnset`.
|
||||||
|
|
||||||
5. Verify after edits.
|
5. Verify after edits.
|
||||||
Run `nix flake show --json`.
|
Run `nix flake show --json`.
|
||||||
Run `nix flake check` when feasible.
|
Run `nix flake check` when feasible.
|
||||||
If local flake evaluation cannot see newly created files because the repo is being loaded as a git flake, stage the new files before rerunning checks.
|
If local flake evaluation cannot see newly created files because the repo is loaded as a git flake, stage the new files before rerunning checks.
|
||||||
|
|
||||||
## Decision Rules
|
## Decision Rules
|
||||||
|
|
||||||
- Prefer `repo-lib.lib.tools.fromPackage` for tools with explicit metadata.
|
- Prefer `repo-lib.lib.tools.fromPackage` for packaged CLIs and `fromCommand` only when the tool should come from the host environment.
|
||||||
- Use `repo-lib.lib.tools.simple` only for very simple `--version` or `version` probes.
|
- Use `repo-lib.lib.tools.simple` only for very small package-backed probes that only need `version.args`.
|
||||||
- Put pre-commit and pre-push automation in `checks`, not shell hooks.
|
- Required tools fail shell startup if their probe fails. Do not mark a tool optional unless that is intentional.
|
||||||
- Treat `postVersion` as pre-tag and pre-push. It is not a true post-tag hook.
|
- `config.checks` supports only `pre-commit` and `pre-push`. `commit-msg` must go through raw lefthook config.
|
||||||
- For a webhook that must fire after the tag exists remotely, prefer CI triggered by tag push over local release command changes.
|
- Generated checks include `formatting-check`, `hook-check`, and `lefthook-check`.
|
||||||
|
- `config.shell.banner.style` must be `simple` or `pretty`.
|
||||||
|
- Treat `postVersion` as pre-format, pre-commit, pre-tag, and pre-push.
|
||||||
|
- Do not model a true post-tag webhook inside `repo-lib`; prefer CI triggered by tag push.
|
||||||
|
- The current generated `release` command is destructive and opinionated: it formats, stages, commits, tags, and pushes as part of the flow. Document that clearly when editing consumer release automation.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `references/api.md`
|
- `references/api.md`
|
||||||
Use for the exact consumer API, option matrix, generated outputs, release ordering, and legacy compatibility.
|
Use for the exact consumer API, generated outputs, hook and banner behavior, and current release semantics.
|
||||||
|
|
||||||
- `references/recipes.md`
|
- `references/recipes.md`
|
||||||
Use for concrete change patterns: add a tool, add a test phase, update release-managed files, or wire webhook behavior.
|
Use for common changes: add a tool, add a check, add a `commit-msg` hook, customize the shell banner, or update release-managed files.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "Repo Lib Consumer"
|
display_name: "Repo Lib Consumer"
|
||||||
short_description: "Edit repos that use repo-lib safely"
|
short_description: "Edit mkRepo or mkRelease consumers safely"
|
||||||
default_prompt: "Use $repo-lib-consumer to update a repo that consumes repo-lib."
|
default_prompt: "Use $repo-lib-consumer to update a repo that consumes repo-lib through mkRepo, mkRelease, or the repo-lib template."
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
Look for one of these patterns in the consuming repo:
|
Look for one of these patterns in the consuming repo:
|
||||||
|
|
||||||
- `repo-lib.lib.mkRepo`
|
- `repo-lib.lib.mkRepo`
|
||||||
- `repo-lib.lib.mkDevShell`
|
|
||||||
- `repo-lib.lib.mkRelease`
|
- `repo-lib.lib.mkRelease`
|
||||||
- `inputs.repo-lib`
|
- `inputs.repo-lib`
|
||||||
|
|
||||||
@@ -27,6 +26,7 @@ repo-lib.lib.mkRepo {
|
|||||||
extraShellText = "";
|
extraShellText = "";
|
||||||
allowImpureBootstrap = false;
|
allowImpureBootstrap = false;
|
||||||
bootstrap = "";
|
bootstrap = "";
|
||||||
|
banner = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
formatting = {
|
formatting = {
|
||||||
@@ -54,12 +54,35 @@ repo-lib.lib.mkRepo {
|
|||||||
Generated outputs:
|
Generated outputs:
|
||||||
|
|
||||||
- `devShells.${system}.default`
|
- `devShells.${system}.default`
|
||||||
|
- `checks.${system}.formatting-check`
|
||||||
- `checks.${system}.hook-check`
|
- `checks.${system}.hook-check`
|
||||||
- `checks.${system}.lefthook-check`
|
- `checks.${system}.lefthook-check`
|
||||||
- `formatter.${system}`
|
- `formatter.${system}`
|
||||||
- `packages.${system}.release` when `config.release != null`
|
- `packages.${system}.release` when `config.release != null`
|
||||||
- merged `packages` and `apps` from `perSystem`
|
- merged `packages` and `apps` from `perSystem`
|
||||||
|
|
||||||
|
Merge points:
|
||||||
|
|
||||||
|
- `config.checks` merges with `perSystem.checks`
|
||||||
|
- `config.lefthook` recursively merges with `perSystem.lefthook`
|
||||||
|
- `config.shell` recursively merges with `perSystem.shell`
|
||||||
|
- generated release packages merge with `perSystem.packages`
|
||||||
|
|
||||||
|
Conflicts on `checks`, `packages`, and `apps` names throw.
|
||||||
|
|
||||||
|
## `config.includeStandardPackages`
|
||||||
|
|
||||||
|
Default: `true`
|
||||||
|
|
||||||
|
When enabled, the shell includes:
|
||||||
|
|
||||||
|
- `nixfmt`
|
||||||
|
- `gitlint`
|
||||||
|
- `gitleaks`
|
||||||
|
- `shfmt`
|
||||||
|
|
||||||
|
Use `false` only when the consumer explicitly wants to own the full shell package list.
|
||||||
|
|
||||||
## `config.shell`
|
## `config.shell`
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
@@ -71,13 +94,38 @@ Fields:
|
|||||||
- `bootstrap`
|
- `bootstrap`
|
||||||
Shell snippet that runs before the banner.
|
Shell snippet that runs before the banner.
|
||||||
- `allowImpureBootstrap`
|
- `allowImpureBootstrap`
|
||||||
Must be `true` if `bootstrap` is non-empty.
|
Must be `true` when `bootstrap` is non-empty.
|
||||||
|
- `banner`
|
||||||
|
Shell banner configuration.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Default is pure-first.
|
- Default is pure-first.
|
||||||
- Do not add bootstrap work unless the user actually wants imperative setup.
|
- Do not add bootstrap work unless the user actually wants imperative local setup.
|
||||||
- Use `bootstrap` for unavoidable local setup only.
|
- The template uses bootstrap intentionally for Bun global install paths and Moon bootstrapping; do not generalize that into normal package setup unless the repo already wants that behavior.
|
||||||
|
|
||||||
|
### `config.shell.banner`
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
style = "simple";
|
||||||
|
icon = "🚀";
|
||||||
|
title = "Dev shell ready";
|
||||||
|
titleColor = "GREEN";
|
||||||
|
subtitle = "";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
|
borderColor = "BLUE";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `style` must be `simple` or `pretty`.
|
||||||
|
- `borderColor` matters only for `pretty`.
|
||||||
|
- Tool rows can also set `banner.color`, `banner.icon`, and `banner.iconColor`.
|
||||||
|
- Required tool probe failures abort shell startup.
|
||||||
|
|
||||||
## `config.formatting`
|
## `config.formatting`
|
||||||
|
|
||||||
@@ -91,7 +139,7 @@ Fields:
|
|||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- `nixfmt` is always enabled.
|
- `nixfmt` is always enabled.
|
||||||
- Use formatter settings instead of ad hoc shell formatting logic.
|
- Use formatter settings instead of shell hooks for formatting behavior.
|
||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
@@ -99,10 +147,10 @@ Rules:
|
|||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
command = "go test ./...";
|
command = "bun test";
|
||||||
stage = "pre-push"; # or "pre-commit"
|
stage = "pre-push"; # or "pre-commit"
|
||||||
passFilenames = false;
|
passFilenames = false;
|
||||||
runtimeInputs = [ pkgs.go ];
|
runtimeInputs = [ pkgs.bun ];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -114,37 +162,53 @@ Defaults:
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Only `pre-commit` and `pre-push` are supported.
|
- Only `pre-commit` and `pre-push` are supported here.
|
||||||
- The command is wrapped as a script and connected into `lefthook.nix`.
|
- The command is wrapped with `writeShellApplication`.
|
||||||
- `pre-commit` and `pre-push` commands are configured to run in parallel.
|
- `pre-commit` and `pre-push` stages are configured to run in parallel.
|
||||||
|
- `passFilenames = true` maps to `{staged_files}` for `pre-commit` and `{push_files}` for `pre-push`.
|
||||||
|
|
||||||
## Raw Lefthook config
|
## Raw Lefthook config
|
||||||
|
|
||||||
Use `config.lefthook` or `perSystem.lefthook` for advanced Lefthook features that the built-in `checks` abstraction does not carry.
|
Use `config.lefthook` or `perSystem.lefthook` when the task needs advanced Lefthook features or unsupported stages.
|
||||||
|
|
||||||
Example:
|
Pass-through attrset example:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
checks.tests = {
|
checks.tests = {
|
||||||
command = "go test ./...";
|
command = "bun test";
|
||||||
stage = "pre-push";
|
stage = "pre-push";
|
||||||
|
runtimeInputs = [ pkgs.bun ];
|
||||||
};
|
};
|
||||||
|
|
||||||
lefthook.pre-push.commands.tests.stage_fixed = true;
|
lefthook.pre-push.commands.tests.stage_fixed = true;
|
||||||
|
|
||||||
lefthook.commit-msg.commands.commitlint = {
|
lefthook.commit-msg.commands.commitlint = {
|
||||||
run = "pnpm commitlint --edit {1}";
|
run = "bun commitlint --edit {1}";
|
||||||
stage_fixed = true;
|
stage_fixed = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Structured hook-entry example in a raw hook list:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
perSystem = { pkgs, ... }: {
|
||||||
|
lefthook.biome = {
|
||||||
|
entry = "${pkgs.biome}/bin/biome check";
|
||||||
|
pass_filenames = true;
|
||||||
|
stages = [ "pre-commit" "pre-push" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- These attrsets are passed through to `lefthook.nix`.
|
- `config.lefthook` and `perSystem.lefthook` are recursive attrset passthroughs merged after generated checks.
|
||||||
- They are merged after generated checks, so they can extend generated commands.
|
- Structured hook entries support only:
|
||||||
- Prefer `checks` for the simple common case and `lefthook` for advanced fields such as `stage_fixed`, `files`, `glob`, `exclude`, `jobs`, or `scripts`.
|
`description`, `enable`, `entry`, `name`, `package`, `pass_filenames`, `stages`
|
||||||
|
- `stages` may include `pre-commit`, `pre-push`, or `commit-msg`.
|
||||||
|
- `pass_filenames = true` maps to `{1}` for `commit-msg`.
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
@@ -152,17 +216,19 @@ Preferred shape in `perSystem.tools`:
|
|||||||
|
|
||||||
```nix
|
```nix
|
||||||
(repo-lib.lib.tools.fromPackage {
|
(repo-lib.lib.tools.fromPackage {
|
||||||
name = "Go";
|
name = "Bun";
|
||||||
package = pkgs.go;
|
package = pkgs.bun;
|
||||||
exe = "go"; # optional
|
|
||||||
version = {
|
version = {
|
||||||
args = [ "version" ];
|
args = [ "--version" ];
|
||||||
|
match = null;
|
||||||
regex = null;
|
regex = null;
|
||||||
group = 0;
|
group = 0;
|
||||||
line = 1;
|
line = 1;
|
||||||
};
|
};
|
||||||
banner = {
|
banner = {
|
||||||
color = "CYAN";
|
color = "YELLOW";
|
||||||
|
icon = "";
|
||||||
|
iconColor = null;
|
||||||
};
|
};
|
||||||
required = true;
|
required = true;
|
||||||
})
|
})
|
||||||
@@ -174,7 +240,10 @@ For a tool that should come from the host `PATH` instead of `nixpkgs`:
|
|||||||
(repo-lib.lib.tools.fromCommand {
|
(repo-lib.lib.tools.fromCommand {
|
||||||
name = "Nix";
|
name = "Nix";
|
||||||
command = "nix";
|
command = "nix";
|
||||||
version.args = [ "--version" ];
|
version = {
|
||||||
|
args = [ "--version" ];
|
||||||
|
group = 1;
|
||||||
|
};
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -186,10 +255,11 @@ repo-lib.lib.tools.simple "Go" pkgs.go [ "version" ]
|
|||||||
|
|
||||||
Tool behavior:
|
Tool behavior:
|
||||||
|
|
||||||
- Tool packages are added to the shell automatically.
|
- Package-backed tools are added to the shell automatically.
|
||||||
- Command-backed tools are probed from the existing `PATH` and are not added to the shell automatically.
|
- Command-backed tools are probed from the existing `PATH` and are not added to the shell automatically.
|
||||||
- Banner probing uses absolute executable paths.
|
- Banner probing uses the resolved executable path.
|
||||||
- `required = true` by default.
|
- `required = true` by default.
|
||||||
|
- When `version.match` is set, the first matching output line is selected before `regex` extraction.
|
||||||
- Required tool probe failure aborts shell startup.
|
- Required tool probe failure aborts shell startup.
|
||||||
|
|
||||||
Use `shell.packages` instead of `tools` when:
|
Use `shell.packages` instead of `tools` when:
|
||||||
@@ -246,73 +316,66 @@ Set `release = null` to disable the generated release package.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `run`
|
### `versionMetaSet`
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
run = {
|
versionMetaSet = {
|
||||||
script = ''
|
key = "desktop_binary_version_max";
|
||||||
curl -fsS https://example.invalid/hook \
|
value = "$FULL_VERSION";
|
||||||
-H 'content-type: application/json' \
|
|
||||||
-d '{"tag":"'"$FULL_TAG"'"}'
|
|
||||||
'';
|
|
||||||
runtimeInputs = [ pkgs.curl ];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Also accepted for compatibility:
|
### `versionMetaUnset`
|
||||||
|
|
||||||
- `{ run = ''...''; }`
|
```nix
|
||||||
- legacy `mkRelease { release = [ { file = ...; content = ...; } ... ]; }`
|
{
|
||||||
|
versionMetaUnset = {
|
||||||
|
key = "desktop_unused";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Current supported step kinds are only `writeFile`, `replace`, `versionMetaSet`, and `versionMetaUnset`.
|
||||||
|
- Do not document or implement a `run` step in consumer repos unless the library itself gains that feature.
|
||||||
|
|
||||||
## Release ordering
|
## Release ordering
|
||||||
|
|
||||||
The generated `release` command does this:
|
The generated `release` command currently does this:
|
||||||
|
|
||||||
1. Update `VERSION`
|
1. Require a clean git worktree
|
||||||
2. Run `release.steps`
|
2. Update `VERSION`
|
||||||
3. Run `postVersion`
|
3. Run `release.steps`
|
||||||
4. Run `nix fmt`
|
4. Run `postVersion`
|
||||||
5. `git add -A`
|
5. Run `nix fmt`
|
||||||
6. Commit
|
6. `git add -A`
|
||||||
7. Tag
|
7. Commit with `chore(release): <tag>`
|
||||||
8. Push branch
|
8. Tag
|
||||||
9. Push tags
|
9. Push branch
|
||||||
|
10. Push tags
|
||||||
|
|
||||||
Important consequence:
|
Important consequences:
|
||||||
|
|
||||||
- `postVersion` is still before commit, tag, and push.
|
- `postVersion` is before formatting, commit, tag, and push.
|
||||||
- There is no true post-tag or post-push hook in current `repo-lib`.
|
- There is no true post-tag or post-push hook in current `repo-lib`.
|
||||||
|
- The current release runner is opinionated and performs commit, tag, and push as part of the flow.
|
||||||
|
|
||||||
## Post-tag webhook limitation
|
## `mkRelease`
|
||||||
|
|
||||||
If the user asks for a webhook after the tag exists remotely:
|
`repo-lib.lib.mkRelease` remains available when a repo wants only the release package:
|
||||||
|
|
||||||
- Prefer CI triggered by pushed tags in the consuming repo.
|
```nix
|
||||||
- Do not claim `postVersion` is post-tag; it is not.
|
repo-lib.lib.mkRelease {
|
||||||
- Only extend `repo-lib` itself if the user explicitly wants a new library capability.
|
system = system;
|
||||||
|
nixpkgsInput = nixpkgs; # optional
|
||||||
|
channels = [ "alpha" "beta" "rc" "internal" ];
|
||||||
|
steps = [ ];
|
||||||
|
postVersion = "";
|
||||||
|
runtimeInputs = [ ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Legacy API summary
|
Use the same release-step rules as `config.release`.
|
||||||
|
|
||||||
`mkDevShell` still supports:
|
|
||||||
|
|
||||||
- `extraPackages`
|
|
||||||
- `preToolHook`
|
|
||||||
- `extraShellHook`
|
|
||||||
- `lefthook`
|
|
||||||
- `additionalHooks`
|
|
||||||
- old `tools = [ { name; bin; versionCmd; color; } ]`
|
|
||||||
- `features.oxfmt`
|
|
||||||
- `formatters`
|
|
||||||
- `formatterSettings`
|
|
||||||
|
|
||||||
`mkRelease` still supports:
|
|
||||||
|
|
||||||
- `release = [ ... ]` as legacy alias for `steps`
|
|
||||||
- `extraRuntimeInputs` as legacy alias merged into `runtimeInputs`
|
|
||||||
|
|
||||||
When a repo already uses these APIs:
|
|
||||||
|
|
||||||
- preserve them unless the user asked to migrate
|
|
||||||
- do not mix old and new styles accidentally in the same call
|
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ Edit `perSystem.tools` in the consuming repo:
|
|||||||
```nix
|
```nix
|
||||||
tools = [
|
tools = [
|
||||||
(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 = "";
|
||||||
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Do not also add `pkgs.go` to `shell.packages`; `tools` already adds it.
|
- Do not also add the same package to `shell.packages`; `tools` already adds package-backed tools to the shell.
|
||||||
- Use `exe = "name"` only when the package exposes multiple binaries or the main program is not the desired one.
|
- Use `exe = "name"` only when the package exposes multiple binaries or the default binary is not the right one.
|
||||||
|
- Use `fromCommand` when the executable should come from the host environment instead of `nixpkgs`.
|
||||||
|
|
||||||
## Add a non-banner package to the shell
|
## Add a non-banner package to the shell
|
||||||
|
|
||||||
@@ -37,16 +41,38 @@ Use this for:
|
|||||||
- internal scripts
|
- internal scripts
|
||||||
- the generated `release` package itself
|
- the generated `release` package itself
|
||||||
|
|
||||||
## Add a test phase or lint hook
|
## Customize the shell banner
|
||||||
|
|
||||||
For a simple global check:
|
Use `config.shell.banner`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
config.checks.tests = {
|
config.shell.banner = {
|
||||||
command = "go test ./...";
|
style = "pretty";
|
||||||
|
icon = "☾";
|
||||||
|
title = "Moonrepo shell ready";
|
||||||
|
titleColor = "GREEN";
|
||||||
|
subtitle = "Bun + TypeScript + Varlock";
|
||||||
|
subtitleColor = "GRAY";
|
||||||
|
borderColor = "BLUE";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Guidance:
|
||||||
|
|
||||||
|
- Use `style = "pretty"` when the repo already has a styled shell banner.
|
||||||
|
- Keep icons and colors consistent with the repo's current shell UX.
|
||||||
|
- Remember that required tool probe failures will still abort shell startup.
|
||||||
|
|
||||||
|
## Add a test phase or lint hook
|
||||||
|
|
||||||
|
For a simple shared check:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.checks.typecheck = {
|
||||||
|
command = "bun run typecheck";
|
||||||
stage = "pre-push";
|
stage = "pre-push";
|
||||||
passFilenames = false;
|
passFilenames = false;
|
||||||
runtimeInputs = [ pkgs.go ];
|
runtimeInputs = [ pkgs.bun ];
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,20 +80,44 @@ For a system-specific check:
|
|||||||
|
|
||||||
```nix
|
```nix
|
||||||
perSystem = { pkgs, ... }: {
|
perSystem = { pkgs, ... }: {
|
||||||
checks.lint = {
|
checks.format = {
|
||||||
command = "bun test";
|
command = "oxfmt --check .";
|
||||||
stage = "pre-push";
|
stage = "pre-commit";
|
||||||
runtimeInputs = [ pkgs.bun ];
|
passFilenames = false;
|
||||||
|
runtimeInputs = [ pkgs.oxfmt ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Guidance:
|
Guidance:
|
||||||
|
|
||||||
- Use `pre-commit` for fast format/lint work.
|
- Use `pre-commit` for fast format or lint work.
|
||||||
- Use `pre-push` for slower test suites.
|
- Use `pre-push` for slower test suites.
|
||||||
- Prefer `runtimeInputs` over inline absolute paths when the command needs extra CLIs.
|
- Prefer `runtimeInputs` over inline absolute paths when the command needs extra CLIs.
|
||||||
|
|
||||||
|
## Add a `commit-msg` hook
|
||||||
|
|
||||||
|
`config.checks` cannot target `commit-msg`. Use raw Lefthook config:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.lefthook.commit-msg.commands.gitlint = {
|
||||||
|
run = "${pkgs.gitlint}/bin/gitlint --staged --msg-filename {1}";
|
||||||
|
stage_fixed = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a structured hook entry:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
perSystem = { pkgs, ... }: {
|
||||||
|
lefthook.commitlint = {
|
||||||
|
entry = "${pkgs.nodejs}/bin/node scripts/commitlint.mjs";
|
||||||
|
pass_filenames = true;
|
||||||
|
stages = [ "commit-msg" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Add or change formatters
|
## Add or change formatters
|
||||||
|
|
||||||
Use `config.formatting`:
|
Use `config.formatting`:
|
||||||
@@ -76,11 +126,12 @@ Use `config.formatting`:
|
|||||||
config.formatting = {
|
config.formatting = {
|
||||||
programs = {
|
programs = {
|
||||||
shfmt.enable = true;
|
shfmt.enable = true;
|
||||||
gofmt.enable = true;
|
oxfmt.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
shfmt.options = [ "-i" "2" "-s" "-w" ];
|
shfmt.options = [ "-i" "2" "-s" "-w" ];
|
||||||
|
oxfmt.excludes = [ "*.md" "*.yml" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -116,31 +167,27 @@ config.release.steps = [
|
|||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
## Add a webhook during release
|
Update metadata inside `VERSION`:
|
||||||
|
|
||||||
If the webhook may run before commit and tag creation, use a `run` step or `postVersion`.
|
|
||||||
|
|
||||||
Use a `run` step when it belongs with other release mutations:
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
config.release = {
|
config.release.steps = [
|
||||||
runtimeInputs = [ pkgs.curl ];
|
{
|
||||||
steps = [
|
versionMetaSet = {
|
||||||
{
|
key = "desktop_binary_version_max";
|
||||||
run = {
|
value = "$FULL_VERSION";
|
||||||
script = ''
|
};
|
||||||
curl -fsS https://example.invalid/release-hook \
|
}
|
||||||
-H 'content-type: application/json' \
|
{
|
||||||
-d '{"version":"'"$FULL_VERSION"'"}'
|
versionMetaUnset = {
|
||||||
'';
|
key = "desktop_unused";
|
||||||
runtimeInputs = [ pkgs.curl ];
|
};
|
||||||
};
|
}
|
||||||
}
|
];
|
||||||
];
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `postVersion` when the action should happen after all `steps`:
|
## Add a webhook during release
|
||||||
|
|
||||||
|
Current `repo-lib` does not expose a `run` release step. If the action must happen during local release execution, put it in `postVersion`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
config.release.postVersion = ''
|
config.release.postVersion = ''
|
||||||
@@ -153,8 +200,8 @@ config.release.runtimeInputs = [ pkgs.curl ];
|
|||||||
|
|
||||||
Important:
|
Important:
|
||||||
|
|
||||||
- Both of these still run before commit, tag, and push.
|
- `postVersion` still runs before `nix fmt`, commit, tag, and push.
|
||||||
- They are not true post-tag hooks.
|
- This is not a true post-tag hook.
|
||||||
|
|
||||||
## Add a true post-tag webhook
|
## Add a true post-tag webhook
|
||||||
|
|
||||||
@@ -162,11 +209,11 @@ Do not fake this with `postVersion`.
|
|||||||
|
|
||||||
Preferred approach in the consuming repo:
|
Preferred approach in the consuming repo:
|
||||||
|
|
||||||
1. Keep local release generation in `repo-lib`.
|
1. Keep local version generation in `repo-lib`.
|
||||||
2. Add CI triggered by tag push.
|
2. Trigger CI from tag push.
|
||||||
3. Put the webhook call in CI, where the tag is already created and pushed.
|
3. Put the webhook call in CI, where the tag already exists remotely.
|
||||||
|
|
||||||
Only change `repo-lib` itself if the user explicitly asks for a new local post-tag capability.
|
Only change `repo-lib` itself if the user explicitly asks for a new library capability.
|
||||||
|
|
||||||
## Add impure bootstrap work
|
## Add impure bootstrap work
|
||||||
|
|
||||||
@@ -175,8 +222,9 @@ Only do this when the user actually wants imperative shell setup:
|
|||||||
```nix
|
```nix
|
||||||
config.shell = {
|
config.shell = {
|
||||||
bootstrap = ''
|
bootstrap = ''
|
||||||
export GOBIN="$PWD/.tools/bin"
|
export BUN_INSTALL_GLOBAL_DIR="$PWD/.tools/bun/install/global"
|
||||||
export PATH="$GOBIN:$PATH"
|
export BUN_INSTALL_BIN="$PWD/.tools/bun/bin"
|
||||||
|
export PATH="$BUN_INSTALL_BIN:$PATH"
|
||||||
'';
|
'';
|
||||||
allowImpureBootstrap = true;
|
allowImpureBootstrap = true;
|
||||||
};
|
};
|
||||||
@@ -184,14 +232,14 @@ config.shell = {
|
|||||||
|
|
||||||
Do not add bootstrap work for normal Nix-packaged tools.
|
Do not add bootstrap work for normal Nix-packaged tools.
|
||||||
|
|
||||||
## Migrate a legacy consumer to `mkRepo`
|
## Move from direct `mkRelease` to `mkRepo`
|
||||||
|
|
||||||
Only do this if requested.
|
Only do this if requested.
|
||||||
|
|
||||||
Migration outline:
|
Migration outline:
|
||||||
|
|
||||||
1. Move repeated shell/check/formatter config into `config`.
|
1. Move release package config into `config.release`.
|
||||||
2. Move old banner tools into `perSystem.tools`.
|
2. Move shell setup into `config.shell` and `perSystem.shell.packages`.
|
||||||
3. Move extra shell packages into `perSystem.shell.packages`.
|
3. Move bannered CLIs into `perSystem.tools`.
|
||||||
4. Replace old `mkRelease { release = [ ... ]; }` with `config.release.steps`.
|
4. Move hook commands into `config.checks` or raw `lefthook`.
|
||||||
5. Keep behavior the same first; do not redesign the repo in the same change unless asked.
|
5. Keep behavior the same first; do not redesign the repo in the same change unless asked.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
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.5.1";
|
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v4.0.0";
|
||||||
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,46 +109,49 @@
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
shell.packages = [
|
shell = {
|
||||||
self.packages.${system}.release
|
packages = [
|
||||||
pkgs.bun
|
self.packages.${system}.release
|
||||||
pkgs.openbao
|
|
||||||
pkgs.oxfmt
|
|
||||||
pkgs.oxlint
|
|
||||||
];
|
|
||||||
|
|
||||||
checks.format = {
|
|
||||||
command = "oxfmt --check .";
|
|
||||||
stage = "pre-commit";
|
|
||||||
passFilenames = false;
|
|
||||||
runtimeInputs = [ pkgs.oxfmt ];
|
|
||||||
};
|
|
||||||
|
|
||||||
checks.typecheck = {
|
|
||||||
command = "bun run typecheck";
|
|
||||||
stage = "pre-push";
|
|
||||||
passFilenames = false;
|
|
||||||
runtimeInputs = [ pkgs.bun ];
|
|
||||||
};
|
|
||||||
|
|
||||||
checks.env-check = {
|
|
||||||
command = "bun run env:check";
|
|
||||||
stage = "pre-push";
|
|
||||||
passFilenames = false;
|
|
||||||
runtimeInputs = [
|
|
||||||
pkgs.bun
|
|
||||||
pkgs.openbao
|
pkgs.openbao
|
||||||
|
pkgs.oxfmt
|
||||||
|
pkgs.oxlint
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
checks.env-scan = {
|
checks = {
|
||||||
command = "bun run env:scan";
|
format = {
|
||||||
stage = "pre-commit";
|
command = "oxfmt --check .";
|
||||||
passFilenames = false;
|
stage = "pre-commit";
|
||||||
runtimeInputs = [
|
passFilenames = false;
|
||||||
pkgs.bun
|
runtimeInputs = [ pkgs.oxfmt ];
|
||||||
pkgs.openbao
|
};
|
||||||
];
|
|
||||||
|
typecheck = {
|
||||||
|
command = "bun run typecheck";
|
||||||
|
stage = "pre-push";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [ pkgs.bun ];
|
||||||
|
};
|
||||||
|
|
||||||
|
env-check = {
|
||||||
|
command = "bun run env:check";
|
||||||
|
stage = "pre-push";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.bun
|
||||||
|
pkgs.openbao
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
env-scan = {
|
||||||
|
command = "bun run env:scan";
|
||||||
|
stage = "pre-commit";
|
||||||
|
passFilenames = false;
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.bun
|
||||||
|
pkgs.openbao
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user