Compare commits
2
Commits
0.3.0
...
85fd37737a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85fd37737a | ||
|
|
bd0145642c |
No files matched your search
@@ -51,7 +51,7 @@ Never start a long-lived / blocking process such as `npm run watch`. It runs unt
|
|||||||
**Known problems:** <open issues, caveats, follow-ups>
|
**Known problems:** <open issues, caveats, follow-ups>
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the user has no further objections, merge back: `git checkout main && git merge --no-ff <branch>`. The branching model is documented in [CONTRIBUTING.md § Branching model](./CONTRIBUTING.md#branching-model).
|
Once the user has no further objections, merge back: `npm run create:finish` (on the branch — it merges `--no-ff`, runs `npm run verify`, and deletes the branch). The branching model is documented in [CONTRIBUTING.md § Branching model](./CONTRIBUTING.md#branching-model).
|
||||||
|
|
||||||
- **Leaf task** (no indented children): implement on the current branch and commit.
|
- **Leaf task** (no indented children): implement on the current branch and commit.
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -11,6 +11,7 @@ CI and review will bounce these even though `npm run check` and the linters don'
|
|||||||
- **`oxlint-disable` directives live in source, not `.oxlintrc.json`.** The trade-off must sit next to the code it silences. This is a _human_ last-resort convention; agents must not add these — see [AGENTS.md § Never do](./AGENTS.md#never-do). (rationale: README § Tooling decisions)
|
- **`oxlint-disable` directives live in source, not `.oxlintrc.json`.** The trade-off must sit next to the code it silences. This is a _human_ last-resort convention; agents must not add these — see [AGENTS.md § Never do](./AGENTS.md#never-do). (rationale: README § Tooling decisions)
|
||||||
- **Don't put slow / network / whole-project scans in `check` or pre-commit.** Advisory scans are not correctness gates; they belong under `maintain:`. (see [Feedback tiers](#feedback-tiers) and [Script prefix convention](#script-prefix-convention))
|
- **Don't put slow / network / whole-project scans in `check` or pre-commit.** Advisory scans are not correctness gates; they belong under `maintain:`. (see [Feedback tiers](#feedback-tiers) and [Script prefix convention](#script-prefix-convention))
|
||||||
- **New work starts with `npm run create:branch`, never a hand-written `git switch -c`/`git checkout -b`.** The command carries the branch precondition; branching around it skips the clean-tree, current-`main` and green-baseline checks, and the skip is invisible until a failure can no longer be attributed. (see [Branching model](#branching-model))
|
- **New work starts with `npm run create:branch`, never a hand-written `git switch -c`/`git checkout -b`.** The command carries the branch precondition; branching around it skips the clean-tree, current-`main` and green-baseline checks, and the skip is invisible until a failure can no longer be attributed. (see [Branching model](#branching-model))
|
||||||
|
- **Work is merged back with `npm run create:finish`, never a hand-written `git merge`.** The command carries the merge-side preconditions (clean tree, current `main`, a `feature/`/`fix/`/`chore/` branch) and runs `npm run verify` after the merge, so a merge cannot land unverified. (see [Branching model](#branching-model))
|
||||||
- **There is no local `npm run publish`, and `publish:publint` / `publish:attw` don't go in `check`.** (see [Publishing workflow](#publishing-workflow))
|
- **There is no local `npm run publish`, and `publish:publint` / `publish:attw` don't go in `check`.** (see [Publishing workflow](#publishing-workflow))
|
||||||
|
|
||||||
## Editor configuration
|
## Editor configuration
|
||||||
@@ -27,7 +28,7 @@ Examples from history: `:sparkles: Add watch tier with watch:test child`, `:recy
|
|||||||
|
|
||||||
Script names in `package.json` use a prefix that signals _when_ the script is intended to run. A `<prefix>:<name>` script is implicitly aggregated by a `<prefix>` script (if one exists) and run by the corresponding lefthook hook or CI step. Picking the right prefix documents the script's intended lifecycle:
|
Script names in `package.json` use a prefix that signals _when_ the script is intended to run. A `<prefix>:<name>` script is implicitly aggregated by a `<prefix>` script (if one exists) and run by the corresponding lefthook hook or CI step. Picking the right prefix documents the script's intended lifecycle:
|
||||||
|
|
||||||
- `create:*` — front doors of the repo's own workflow; these mutate git state rather than the source. `create:branch` opens a unit of work (asserts a clean tree, a current `main` and a green baseline before it branches), `create:release` closes one (maintainer-only). No bare `create` aggregator on purpose — see `publish:*` for the precedent.
|
- `create:*` — front doors of the repo's own workflow; these mutate git state rather than the source. `create:branch` opens a unit of work (asserts a clean tree, a current `main` and a green baseline before it branches), `create:finish` closes the branch half (merges the current unit of work into `main` and verifies the result), `create:release` closes the release half (maintainer-only). No bare `create` aggregator on purpose — see `publish:*` for the precedent.
|
||||||
- `check:*` — read-only verification; never modifies files. Aggregated by `npm run check`.
|
- `check:*` — read-only verification; never modifies files. Aggregated by `npm run check`.
|
||||||
- `fix:*` — mutating counterpart of a `check:*` script. Aggregated by `npm run fix`; the diff is the review surface.
|
- `fix:*` — mutating counterpart of a `check:*` script. Aggregated by `npm run fix`; the diff is the review surface.
|
||||||
- `test:*` — test scripts. `test` is the canonical entry point (`check:tsc` + unit tests); `test:unit` skips the typecheck for fast local iteration; `test:ci` adds c8 coverage.
|
- `test:*` — test scripts. `test` is the canonical entry point (`check:tsc` + unit tests); `test:unit` skips the typecheck for fast local iteration; `test:ci` adds c8 coverage.
|
||||||
@@ -85,7 +86,7 @@ This is why every test in the suite pairs an `expectTypeOf(...)` with an `assert
|
|||||||
- **Base branch:** `main`
|
- **Base branch:** `main`
|
||||||
- **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>`
|
- **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>`
|
||||||
- **Starting work:** `npm run create:branch -- <prefix>/<desc>`. It refuses, without changing anything, unless the working tree is clean (untracked files included), no merge/rebase/cherry-pick is in progress, `main` matches its upstream, and `npm run test` is green on `main` — so a later failure is always attributable to your edits. The prefix is still _your_ call, inferred from the task; the script validates it rather than guessing it.
|
- **Starting work:** `npm run create:branch -- <prefix>/<desc>`. It refuses, without changing anything, unless the working tree is clean (untracked files included), no merge/rebase/cherry-pick is in progress, `main` matches its upstream, and `npm run test` is green on `main` — so a later failure is always attributable to your edits. The prefix is still _your_ call, inferred from the task; the script validates it rather than guessing it.
|
||||||
- **Merging:** `git checkout main && git merge --no-ff <branch>` (local PR — review the diff yourself before closing the branch).
|
- **Merging:** `npm run create:finish` (on the branch). It asserts the same clean-tree / no-operation / current-`main` preconditions, fast-forwards a stale `main` (a true divergence is refused), merges the branch `--no-ff`, runs `npm run verify`, and deletes the branch only after the merge is green. The push is deliberately left to `create:release`, so the merge stays local and reviewable — read the diff yourself before finishing.
|
||||||
- CI runs `npm run check` + `npm run test:ci` on every push to `main` — this is the authoritative gate.
|
- CI runs `npm run check` + `npm run test:ci` on every push to `main` — this is the authoritative gate.
|
||||||
- **Releases are NOT triggered by pushes.** Only the maintainer triggers a release (see [Publishing workflow](#publishing-workflow)).
|
- **Releases are NOT triggered by pushes.** Only the maintainer triggers a release (see [Publishing workflow](#publishing-workflow)).
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}",
|
"fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}",
|
||||||
"fix:oxlint": "oxlint --fix src scripts",
|
"fix:oxlint": "oxlint --fix src scripts",
|
||||||
"create:branch": "./scripts/branch.sh",
|
"create:branch": "./scripts/branch.sh",
|
||||||
|
"create:finish": "./scripts/finish.sh",
|
||||||
"create:release": "./scripts/release.sh",
|
"create:release": "./scripts/release.sh",
|
||||||
"maintain": "npm run maintain:knip; npm run maintain:outdated",
|
"maintain": "npm run maintain:knip; npm run maintain:outdated",
|
||||||
"maintain:knip": "knip --include dependencies,exports,files",
|
"maintain:knip": "knip --include dependencies,exports,files",
|
||||||
|
|||||||
+6
-5
@@ -31,11 +31,12 @@ set -eu
|
|||||||
# describes nothing anyone wants, and `publish:*` already sets the precedent for
|
# describes nothing anyone wants, and `publish:*` already sets the precedent for
|
||||||
# a prefix without one.
|
# a prefix without one.
|
||||||
#
|
#
|
||||||
# Also rejected: a full git-flow CLI wrapping the merge too (merging ends in
|
# Also rejected here: reusing `pubv`'s preflight (release-shaped, third-party,
|
||||||
# "review the diff yourself", which is judgment, and only the start half carries
|
# and it would make branch start pay a build + pack it has no use for). The
|
||||||
# a verification burden); and reusing `pubv`'s preflight (release-shaped,
|
# merge half was originally rejected too ("review the diff yourself" is
|
||||||
# third-party, and it would make branch start pay a build + pack it has no use
|
# judgment), but it now has its own front door — `create:finish` — which owns
|
||||||
# for).
|
# the merge-side preconditions and the post-merge `verify`, so the start half
|
||||||
|
# does not have to carry that burden.
|
||||||
#
|
#
|
||||||
# Every refusal is non-mutating except the baseline test, which runs on `main`
|
# Every refusal is non-mutating except the baseline test, which runs on `main`
|
||||||
# after we switch there — so a red `main` restores the branch you started on
|
# after we switch there — so a red `main` restores the branch you started on
|
||||||
|
|||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Feature-finish front door. Run as `npm run create:finish`.
|
||||||
|
#
|
||||||
|
# Why this exists: `create:branch` opens a unit of work, but the close half
|
||||||
|
# (`git checkout main && git merge --no-ff <branch>`) stayed prose in the
|
||||||
|
# branching model, so it drifted per contributor and per session. This is the
|
||||||
|
# mirror image of `create:branch`: it asserts the same preconditions (clean
|
||||||
|
# tree, no in-progress operation, `main` matching its upstream), merges the
|
||||||
|
# current `feature/`/`fix/`/`chore/` branch into `main`, proves the result with
|
||||||
|
# `npm run verify`, and only then deletes the branch.
|
||||||
|
#
|
||||||
|
# `create:branch`'s comment argued against wrapping the merge as "judgment —
|
||||||
|
# review the diff yourself". That judgment still lives here, just moved: the
|
||||||
|
# maintainer reviews the handover *before* invoking this, and the script only
|
||||||
|
# commits the merge, never the push. The push is owned by `create:release`, so
|
||||||
|
# the release commit and its tag leave together and a local merge stays
|
||||||
|
# reviewable (and can be reverted with `git revert -m 1`) until then. `--no-ff`
|
||||||
|
# keeps the unit of work visible in `git log`.
|
||||||
|
#
|
||||||
|
# Unlike `create:branch` a stale `main` is fast-forwarded instead of refused:
|
||||||
|
# the tree is clean (checked above) and `main` is not the checked-out branch
|
||||||
|
# yet, so there is no local state to lose. True divergence (local commits *and*
|
||||||
|
# upstream commits) is still refused — that needs a human.
|
||||||
|
#
|
||||||
|
# On a merge conflict we abort and return to the feature branch, so a failed
|
||||||
|
# finish never strands you on a half-merged `main`.
|
||||||
|
|
||||||
|
BASE="main"
|
||||||
|
PREFIXES="feature fix chore"
|
||||||
|
|
||||||
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
|
||||||
|
echo "error: not inside a git work tree." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
STATE_ROOT=$(git rev-parse --absolute-git-dir)
|
||||||
|
for state in MERGE_HEAD rebase-merge rebase-apply CHERRY_PICK_HEAD BISECT_LOG; do
|
||||||
|
[ -e "${STATE_ROOT}/${state}" ] && {
|
||||||
|
echo "error: a '${state}' operation is in progress; finish or abort it first." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
# `--porcelain` is deliberately stricter than `git diff --quiet`: it also
|
||||||
|
# reports untracked files, which would otherwise not be part of the merge and
|
||||||
|
# silently outlive the branch deletion.
|
||||||
|
DIRTY=$(git status --porcelain)
|
||||||
|
if [ -n "${DIRTY}" ]; then
|
||||||
|
echo "error: working tree is not clean:" >&2
|
||||||
|
echo "${DIRTY}" | sed 's/^/ /' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
START_REF=$(git symbolic-ref --quiet --short HEAD || true)
|
||||||
|
if [ -z "${START_REF}" ]; then
|
||||||
|
echo "error: detached HEAD; switch to the branch you want to finish." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${START_REF}" = "${BASE}" ]; then
|
||||||
|
echo "error: already on '${BASE}'; switch to the branch to finish." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MATCH=0
|
||||||
|
for p in ${PREFIXES}; do
|
||||||
|
case "${START_REF}" in
|
||||||
|
"${p}/"*) MATCH=1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "${MATCH}" -ne 1 ]; then
|
||||||
|
echo "error: '${START_REF}' must start with one of: ${PREFIXES}." >&2
|
||||||
|
echo " refusing to merge a branch that is not a unit of work." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git show-ref --verify --quiet "refs/heads/${BASE}" || {
|
||||||
|
echo "error: no local '${BASE}' to merge into." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Derive the remote rather than hardcoding it: this repo has `origin` (ssh) and
|
||||||
|
# `origin_https`, and `main` tracks the latter — `git fetch origin main` would
|
||||||
|
# check currency against a ref that is never updated here.
|
||||||
|
UPSTREAM=$(git rev-parse --quiet --abbrev-ref --symbolic-full-name "${BASE}@{upstream}" 2>/dev/null || true)
|
||||||
|
BEHIND=0
|
||||||
|
if [ -n "${UPSTREAM}" ]; then
|
||||||
|
git fetch --quiet "${UPSTREAM%/*}" "${UPSTREAM#*/}" || {
|
||||||
|
echo "error: '${UPSTREAM}' check failed: could not reach '${UPSTREAM%/*}'." >&2
|
||||||
|
echo " refusing to merge onto a possibly stale '${BASE}'." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
BEHIND=$(git rev-list --count "${BASE}..${UPSTREAM}")
|
||||||
|
AHEAD=$(git rev-list --count "${UPSTREAM}..${BASE}")
|
||||||
|
if [ "${AHEAD}" -ne 0 ] && [ "${BEHIND}" -ne 0 ]; then
|
||||||
|
echo "error: '${BASE}' has diverged from '${UPSTREAM}' (ahead ${AHEAD}, behind ${BEHIND})." >&2
|
||||||
|
echo " reconcile '${BASE}' with '${UPSTREAM}' before finishing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "warning: '${BASE}' has no upstream; freshness against the remote is unchecked." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Finishing into ${BASE}:"
|
||||||
|
git --no-pager log --oneline --no-decorate "${BASE}..${START_REF}" | sed 's/^/ /'
|
||||||
|
|
||||||
|
git switch --quiet "${BASE}"
|
||||||
|
if [ "${BEHIND}" -ne 0 ]; then
|
||||||
|
echo "Fast-forwarding ${BASE} to ${UPSTREAM} (${BEHIND} commit(s))."
|
||||||
|
git merge --quiet --ff-only "${UPSTREAM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git merge --quiet --no-ff --no-edit "${START_REF}"; then
|
||||||
|
echo "error: merge of '${START_REF}' failed; aborting and returning to it." >&2
|
||||||
|
git merge --abort 2>/dev/null || true
|
||||||
|
git switch --quiet "${START_REF}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Verify: npm run verify"
|
||||||
|
if ! npm run --silent verify; then
|
||||||
|
echo "error: 'npm run verify' is red after the merge." >&2
|
||||||
|
echo " the merge is local and not yet pushed; fix it on '${BASE}' and commit," >&2
|
||||||
|
echo " then drop the now-merged branch with 'git branch -d ${START_REF}'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git branch --delete "${START_REF}" >/dev/null
|
||||||
|
echo "Merged ${START_REF} into ${BASE} and deleted the branch."
|
||||||
|
echo "Next: npm run create:release (or git push, for a merge with no release)."
|
||||||
@@ -23,12 +23,35 @@ set -eu
|
|||||||
# exactly what this ~30-line version replaces.
|
# exactly what this ~30-line version replaces.
|
||||||
|
|
||||||
CHANGELOG="CHANGELOG.md"
|
CHANGELOG="CHANGELOG.md"
|
||||||
|
BASE="main"
|
||||||
|
|
||||||
if ! command -v code >/dev/null 2>&1; then
|
if ! command -v code >/dev/null 2>&1; then
|
||||||
echo "Error: 'code' (VS Code CLI) not found; install it or remove the editor step." >&2
|
echo "Error: 'code' (VS Code CLI) not found; install it or remove the editor step." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Releases are cut from `main` (see CONTRIBUTING § Publishing workflow). Make
|
||||||
|
# that explicit rather than relying on pubv's default-branch check, so the
|
||||||
|
# error names `main` even when the remote's default is configured differently.
|
||||||
|
CURRENT=$(git symbolic-ref --quiet --short HEAD || true)
|
||||||
|
if [ "${CURRENT}" != "${BASE}" ]; then
|
||||||
|
echo "Error: releases are cut from '${BASE}', but HEAD is '${CURRENT:-detached}'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pubv decides the "default branch" by reading the *local*
|
||||||
|
# `refs/remotes/origin/HEAD`, not by asking the remote, and `git fetch` never
|
||||||
|
# updates that ref. After a default-branch change — or a clone from when the
|
||||||
|
# default was different — it goes stale and pubv warns/fails because the
|
||||||
|
# current branch (main) does not match it, even though main *is* the remote
|
||||||
|
# default. Refresh it from the remote first, so pubv's branch preflight
|
||||||
|
# compares against reality. (Without a network this fails, but so would the
|
||||||
|
# push pubv is about to do, so it is a real error rather than one to swallow.)
|
||||||
|
if ! git remote set-head origin --auto >/dev/null 2>&1; then
|
||||||
|
echo "Error: could not refresh origin/HEAD; check connectivity to origin." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Running pubv..."
|
echo "Running pubv..."
|
||||||
pubv --no-tag --no-push --tag-prefix=none
|
pubv --no-tag --no-push --tag-prefix=none
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user