Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions .github/workflows/release-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ name: 'Release Main'

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Run through beta/prod without pushing to main (verification only)'
type: boolean
default: false

permissions:
contents: write
Expand All @@ -18,23 +23,14 @@ jobs:
fetch-depth: 0
ref: develop

- name: Set up Node.js
uses: tomgrv/actions/setup-node@v2

- name: Install GitUtils
run: npm exec --yes --legacy-peer-deps --package github:tomgrv/devcontainer-features -- devcontainer-features -- add gitutils

- name: Verify GitUtils aliases
run: |
git config --global --get alias.beta >/dev/null || { echo 'Missing git beta alias after GitUtils install; check the "Install GitUtils" step logs.'; false; }
git config --global --get alias.prod >/dev/null || { echo 'Missing git prod alias after GitUtils install; check the "Install GitUtils" step logs.'; false; }

- name: Configure git identity
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'

- name: Run git beta then git prod
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: git beta && git prod
- name: Release promote
# TODO: pin to a released tag of tomgrv/actions once its
# multi-repo-consistency PR merges and tags (currently pointing at
# the working branch so this repo's own PR can be reviewed
# alongside it).
uses: tomgrv/actions/release-promote@claude/multi-repo-consistency-release-7se0jy
with:
# TODO: pin to a released tag of tomgrv/scripts once this repo's
# own PR merges and tags.
scripts-ref: claude/multi-repo-consistency-release-7se0jy
dry-run: ${{ inputs.dry_run == true }}
3 changes: 3 additions & 0 deletions .gitversion
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
major-version-bump-message: "^(build|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
123 changes: 86 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- @format -->

# scripts

Reusable shell scripts shared across
Expand Down Expand Up @@ -31,7 +33,7 @@ shell scripts instead of composite GitHub Actions.

## `setup.sh` — root bootstrap

A machine with nothing installed yet needs *something* fetchable with zero
A machine with nothing installed yet needs _something_ fetchable with zero
prerequisites. That's `setup.sh`, kept deliberately dumb and DRY: it
downloads a tarball of this repo to a temp dir, then hands off to the
`zz_use` it just downloaded to install the core `zz_*` bundle from
Expand Down Expand Up @@ -74,7 +76,7 @@ round-trip.
core `zz_*` scripts from it:

```sh
zz_update # or: zz_use --force <tool...>
zz_update # or: zz_use --force <tool...>
```

Any tool name accepts an optional `[org/repo/]` prefix and/or `@<ref>`
Expand Down Expand Up @@ -125,7 +127,7 @@ context" (a checkout, a warm cache, or a freshly downloaded tarball — all
three are just a directory of `zz_*/run.sh` siblings) and symlink every
script in it onto `PATH` under its real name, in a throwaway scratch dir.
From that point on, `command -v zz_bindir`, `zz_log ...`, even the
`. zz_colors` *inside* zz_bindir's and zz_log's own source, all just
`. zz_colors` _inside_ zz_bindir's and zz_log's own source, all just
resolve normally — zero reimplementation of what those scripts do. That's
what lets `zz_use` bootstrap the whole core `zz_*` bundle from nothing:
the very first `zz_use zz_colors ...` a freshly downloaded, standalone
Expand All @@ -141,11 +143,11 @@ For each `<tool>` requested, in order:
3. **Any other tool with a `zz_use/config/zz_use.json` entry** (override
with `ZZ_USE_CONFIG`) — an explicit mapping always wins if a name
happens to collide with a repo script:
- `{"apt": "<pkg>"}` → `apt-get install -y <pkg>` (via `sudo` if not root).
- `{"url": ..., "archive": "tar.gz"|"tar.xz"|"zip"|"raw", "binpath": ...}`
→ download, extract if needed, resolve a writable bin dir via
`zz_bindir`, and install the binary as `<tool>`. Templates support
`{VERSION}`, `{OS}` (`uname -s`, lowercased), `{ARCH}` (`amd64`/`arm64`).
- `{"apt": "<pkg>"}` → `apt-get install -y <pkg>` (via `sudo` if not root).
- `{"url": ..., "archive": "tar.gz"|"tar.xz"|"zip"|"raw", "binpath": ...}`
→ download, extract if needed, resolve a writable bin dir via
`zz_bindir`, and install the binary as `<tool>`. Templates support
`{VERSION}`, `{OS}` (`uname -s`, lowercased), `{ARCH}` (`amd64`/`arm64`).
4. **Any other script from this repo** (a functional script like
`load-json`, or a core one requested individually) — installed on its
own, not as part of the bundle: unlike the core set, functional
Expand All @@ -163,46 +165,93 @@ the tool isn't already available.

## Core `zz_*` scripts

| Script | Purpose |
| ----------------------------- | --------------------------------------------------------------------- |
| `zz_use <tool>[@ref]` | the activator: resolve/install a dependency, if and only if missing (see below) |
| `zz_update` | force a fresh download of the zz_* bundle, bypassing the local cache |
| `zz_colors` | ANSI color vars (`$Red` `$Green` ... `$End`); source it: `. zz_colors` |
| `zz_log <lvl> <msg...>` | colored, leveled log line on stderr (`i`/`w`/`e`/`s`/`-`) |
| `zz_args <title> <caller> <<-help ...` | parse `$@` per a spec; `eval $(zz_args ...)` to bind the vars |
| `zz_prompt <question> [default]` | interactive free-form input |
| `zz_ask <options> <question...>` | interactive single-char confirm |
| `zz_input [file]` | read from arg (literal or file) or stdin |
| `zz_bindir [-t target]` | resolve/create a writable bin dir; `eval $(zz_bindir ...)` to bind `$dir` and extend `PATH` |
| `zz_dispatch <caller> <subcmd>` | dispatch an underscore-prefixed caller to a sibling `<name>-<subcmd>` script |
| `zz_npx [-s] <tool>` | run a local `node_modules/.bin` binary, falling back to `npx` |
| `zz_persist [-f\|-p] <key> <value>` | upsert a `KEY=VALUE` pair into an env file and/or `/etc/profile.d` |
| Script | Purpose |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `zz_use <tool>[@ref]` | the activator: resolve/install a dependency, if and only if missing (see below) |
| `zz_update` | force a fresh download of the zz_* bundle, bypassing the local cache |
| `zz_colors` | ANSI color vars (`$Red` `$Green` ... `$End`); source it: `. zz_colors` |
| `zz_log <lvl> <msg...>` | colored, leveled log line on stderr (`i`/`w`/`e`/`s`/`-`) |
| `zz_args <title> <caller> <<-help ...` | parse `$@` per a spec; `eval $(zz_args ...)` to bind the vars |
| `zz_prompt <question> [default]` | interactive free-form input |
| `zz_ask <options> <question...>` | interactive single-char confirm |
| `zz_input [file]` | read from arg (literal or file) or stdin |
| `zz_bindir [-t target]` | resolve/create a writable bin dir; `eval $(zz_bindir ...)` to bind `$dir` and extend `PATH` |
| `zz_dispatch <caller> <subcmd>` | dispatch an underscore-prefixed caller to a sibling `<name>-<subcmd>` script |
| `zz_npx [-s] <tool>` | run a local `node_modules/.bin` binary, falling back to `npx` |
| `zz_persist [-f\|-p] <key> <value>` | upsert a `KEY=VALUE` pair into an env file and/or `/etc/profile.d` |
| `zz_call [-p package.json] [command...]` | resolve a caller's declared env vars (`config.input`/`config.output` in `package.json`; ask + persist if missing), then run a command |

## Functional scripts

| Script | Purpose |
| --------------------- | ------------------------------------------------------------------------ |
| `load-json` | load JSON from a file/URL, tag it with `$id` |
| `validate-json` | validate JSON against a (local/inferred/remote) JSON Schema |
| `normalize-json` | sort JSON keys per schema + alphabetically, optional in-place write |
| `merge-json` | recursively merge one JSON file into another (arrays deduped, unioned) |
| `resolve-context` | resolve a feature's source/target dirs from the calling script |
| `edit-script` | copy an installed script locally and open it for editing |
| `distribute-utils` | copy `zz_*`/utility scripts into a project's local scripts directory |
| `install-feature` | copy a feature's stubs/config/bin into a target, run `install-*.sh` |
| `configure-feature` | deploy a feature's stubs into the cwd (merging), run `configure-*.sh` |
| Script | Purpose |
| ------------------- | ---------------------------------------------------------------------- |
| `load-json` | load JSON from a file/URL, tag it with `$id` |
| `validate-json` | validate JSON against a (local/inferred/remote) JSON Schema |
| `normalize-json` | sort JSON keys per schema + alphabetically, optional in-place write |
| `merge-json` | recursively merge one JSON file into another (arrays deduped, unioned) |
| `resolve-context` | resolve a feature's source/target dirs from the calling script |
| `edit-script` | copy an installed script locally and open it for editing |
| `distribute-utils` | copy `zz_*`/utility scripts into a project's local scripts directory |
| `install-feature` | copy a feature's stubs/config/bin into a target, run `install-*.sh` |
| `configure-feature` | deploy a feature's stubs into the cwd (merging), run `configure-*.sh` |

See each folder's own `README.md` for its usage line.

## Git utilities

Migrated from `tomgrv/devcontainer-features`'s `gitutils` feature (which
used to ship them directly under `src/gitutils/bin/`), mirroring the same
move `common-utils`'s functional scripts made earlier — one source of
truth here, fetched on demand via `zz_use` instead of duplicated per
consumer. Installed as `git-<name>` on `PATH`, so git resolves them as
`git <name>` subcommands (e.g. `git-release-beta` → `git release-beta`).
The `gitutils` feature still owns the config (which aliases like `git
beta`/`git prod` point at which of these) and the git-flow install/config
lifecycle — only the script implementations moved.

| Script | Purpose |
| -------------------- | ----------------------------------------------------------- |
| `git-align` | align the current branch with its remote counterpart |
| `git-autorebase` | non-interactive rebasing with conflict resolution |
| `git-co` | enhanced commit |
| `git-degit` | clone and degit a repository |
| `git-fix` | dispatch to `git-fix-<subcommand>` |
| `git-fix-author` | set `user.name`/`user.email` to a specified commit's author |
| `git-fix-base` | rebase commits from one branch onto another |
| `git-fix-blanks` | discard whitespace/blank/quote-slash-only changes |
| `git-fix-children` | delete all descendant tags and branches of a commit |
| `git-fix-date` | fix commit dates/times in history |
| `git-fix-del` | delete a specified commit and rebase subsequent history |
| `git-fix-emoji` | fix git emoji |
| `git-fix-last` | edit the last commit's message and content |
| `git-fix-lock` | resolve conflicts and regenerate lock files |
| `git-fix-message` | rewrite an arbitrary commit message |
| `git-fix-mode` | fix file mode changes from diff |
| `git-fix-privacy` | fix privacy in history |
| `git-fix-prune` | prune stale remote-tracking references |
| `git-fix-rights` | set appropriate file/directory permissions |
| `git-fix-secrets` | redact a secret across git history |
| `git-fix-up` | amend a commit with current changes and rebase |
| `git-forall` | execute a command for all files in the repository |
| `git-getcommit` | list history and ask for a commit to fix up |
| `git-integrate` | integrate modifications from the remote repository |
| `git-pick` | pick files from a specific commit |
| `git-release` | dispatch to `git-release-<subcommand>` |
| `git-release-alpha` | squash-merge the current feature branch into develop |
| `git-release-beta` | start a release branch via Git Flow |
| `git-release-hotfix` | start a hotfix branch via Git Flow |
| `git-release-prod` | finish a release/hotfix branch via Git Flow |
| `git-unset` | unset all git config keys starting with a given prefix |
| `git-workspaces` | list workspace directories and affected workspaces |

## Usage

Install the whole workspace, or a single script's own package:

```sh
npm install --save-dev @tomgrv/scripts # everything
npm install --save-dev @tomgrv/scripts # everything
# or, e.g.:
npm install --save-dev ./validate-json # just this one, standalone
npm install --save-dev ./validate-json # just this one, standalone
```

Every functional script is self-contained: `zz_use zz_colors zz_args ...`
Expand All @@ -213,6 +262,6 @@ Any single folder can be copied out and still work standalone.
## Tests

```sh
npm test # bats --recursive . (every */test.bats)
bats validate-json/test.bats # a single script's tests
npm test # bats --recursive . (every */test.bats)
bats validate-json/test.bats # a single script's tests
```
20 changes: 20 additions & 0 deletions git-align/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- @format -->

# git-align

Align the current branch with its remote counterpart.

Part of [`tomgrv/scripts`](https://github.com/tomgrv/scripts) — installed
and linked onto `PATH` as `git-align` (invoke via `git align`, since
git resolves any `git-*` executable on `PATH` as a subcommand).

## Dependencies

Declared via `zz_use` at the top of `run.sh` and resolved on demand
(installed if and only if missing) — see `run.sh` for the exact list.

## Tests

```sh
bats test.bats
```
12 changes: 12 additions & 0 deletions git-align/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "git-align",
"version": "1.0.0",
"description": "Align the current branch with its remote counterpart",
"private": true,
"bin": {
"git-align": "run.sh"
},
"scripts": {
"test": "bats test.bats"
}
}
30 changes: 30 additions & 0 deletions git-align/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh

# get the branch name whatever the form is
branch=$(git rev-parse --abbrev-ref HEAD)

# get the remote branch name
origin=$(git config --get branch.$branch.remote)

# Stash all changes, including untracked files, with a message
# Record the stash ref before/after so we only pop when something was stashed
# (a clean tree stashes nothing, and an unconditional pop would consume an
# unrelated, older stash entry).
before=$(git rev-parse -q --verify refs/stash 2>/dev/null)
git stash push -u -m "Stashing changes before aligning branch"
after=$(git rev-parse -q --verify refs/stash 2>/dev/null)

# Fetch the latest changes from the remote and align the branch
git fetch $origin
git branch -m $branch $branch-to-delete
if git checkout -b $branch $origin/$branch; then
git branch -D $branch-to-delete
else
zz_log e "Failed to checkout branch $branch from $origin/$branch"
git branch -m $branch-to-delete $branch
fi
# Apply the stashed changes only if a new stash entry was created
[ "$before" != "$after" ] && git stash pop

# Print the current branch name
zz_log i "Current branch: $(git rev-parse --abbrev-ref HEAD)"
17 changes: 17 additions & 0 deletions git-align/test.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bats

load ../tests/helpers.bash

setup() {
setup_scripts_path
}

teardown() {
teardown_scripts_path
}

@test "git-align is installed on PATH and syntactically valid" {
command -v git-align
run sh -n "$BATS_TEST_DIRNAME/run.sh"
[ "$status" -eq 0 ]
}
20 changes: 20 additions & 0 deletions git-autorebase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- @format -->

# git-autorebase

Automatically handle non-interactive rebasing with conflict resolution.

Part of [`tomgrv/scripts`](https://github.com/tomgrv/scripts) — installed
and linked onto `PATH` as `git-autorebase` (invoke via `git autorebase`, since
git resolves any `git-*` executable on `PATH` as a subcommand).

## Dependencies

Declared via `zz_use` at the top of `run.sh` and resolved on demand
(installed if and only if missing) — see `run.sh` for the exact list.

## Tests

```sh
bats test.bats
```
12 changes: 12 additions & 0 deletions git-autorebase/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "git-autorebase",
"version": "1.0.0",
"description": "Automatically handle non-interactive rebasing with conflict resolution",
"private": true,
"bin": {
"git-autorebase": "run.sh"
},
"scripts": {
"test": "bats test.bats"
}
}
Loading
Loading