autonomous-sdlc-harness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/user_reviews/README.md +9 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# push-branch.sh — push the current non-protected branch to its upstream, and
|
|
3
|
+
# never abort the run that called it.
|
|
4
|
+
#
|
|
5
|
+
# WHAT IT IS FOR. This is the single mechanism every unattended commit point
|
|
6
|
+
# calls right after a commit lands, so the remote branch reflects the local
|
|
7
|
+
# commit chain no matter where the run stops — the watcher after its task-prompt
|
|
8
|
+
# commit, the `committer` agent's opt-in push, and the task, user-review-fix and
|
|
9
|
+
# planning orchestrators. It is invoked as a SEPARATE statement from the commit
|
|
10
|
+
# (never `if commit-on-branch.sh …; then push-branch.sh; fi`: that compound is
|
|
11
|
+
# not covered by the guards and stalls an unattended run), which works because a
|
|
12
|
+
# push with nothing new to send is a harmless no-op — so callers may run it
|
|
13
|
+
# unconditionally, including after a commit wrapper that made no commit.
|
|
14
|
+
#
|
|
15
|
+
# WHY A WRAPPER EXISTS AT ALL. Same reason as its sibling: an invocation of a
|
|
16
|
+
# script path under the configured scripts directory is allow-listed literally
|
|
17
|
+
# by the generated permission profile and auto-allowed by the script-allowlist
|
|
18
|
+
# guard (when its basename is not on that guard's deny list, in a command
|
|
19
|
+
# carrying none of `$(…)`, a backtick, `|`, `<`, a braced form other than a bare
|
|
20
|
+
# `${IDENT}`, or a `>` that is neither a descriptor duplication (`2>&1`, `>&2`,
|
|
21
|
+
# `2>&-`) nor a redirection to the literal `/dev/null`), where a `git push`
|
|
22
|
+
# command line assembled with a shell expansion is matched statically by
|
|
23
|
+
# neither and draws a prompt an unattended run cannot answer.
|
|
24
|
+
#
|
|
25
|
+
# EVERY FAILURE PATH IS NON-FATAL (exit 0). A non-git directory, a detached
|
|
26
|
+
# HEAD, a protected branch, a configuration that cannot be resolved, or a failed
|
|
27
|
+
# push (a transient network, say) is made VISIBLE with a one-line message but
|
|
28
|
+
# must NEVER abort the calling run — a stale remote is recoverable and a halted
|
|
29
|
+
# run is not. That is why this script uses `set -uo pipefail` deliberately
|
|
30
|
+
# WITHOUT `set -e`, and why even the fail-closed arm below exits 0: it refuses
|
|
31
|
+
# to push, which is the closed outcome here, and says so.
|
|
32
|
+
#
|
|
33
|
+
# DEFENCE IN DEPTH, NOT THE SOLE GUARD. The caller-agnostic backstop is the
|
|
34
|
+
# committed pre-push hook `init` writes into the configured `githooksDir`, which
|
|
35
|
+
# git runs on every push GIT PERFORMS, however it was started. That backstop is
|
|
36
|
+
# caller-agnostic WITHIN GIT: a tool that implements push against the git backend
|
|
37
|
+
# itself, as `jj git push` does, performs no git push and runs no git hook. The
|
|
38
|
+
# plugin's protected-branch guard refuses an agent-submitted push only when it
|
|
39
|
+
# can PROVE the target is protected, and is silent on the rest — and it is the
|
|
40
|
+
# layer that sees a `jj git push` and refuses one that NAMES a protected target,
|
|
41
|
+
# so the two cover different callers. Neither covers an argument-less
|
|
42
|
+
# `jj git push`, which runs no git hook and gives that guard nothing to prove;
|
|
43
|
+
# only a forge-side ruleset does. This
|
|
44
|
+
# script refuses too, so it is safe when called from outside an unattended
|
|
45
|
+
# profile — and it takes its protected set from
|
|
46
|
+
# `<repo_root>/harness.config.json` at run time through
|
|
47
|
+
# `lib/harness-run-lib.sh`, which is where that set is kept in step across all
|
|
48
|
+
# of them. There is no second list to keep in sync with: a branch name written
|
|
49
|
+
# into this file would enforce the wrong set the moment `protectedBranches`
|
|
50
|
+
# changed, and nothing at all in a repository whose integration branch is named
|
|
51
|
+
# something else.
|
|
52
|
+
#
|
|
53
|
+
# WHAT IT NEVER DOES. It performs only a fast-forward push of already-committed
|
|
54
|
+
# work: no `--force`, no `--force-with-lease`, no history-rewriting flag, no
|
|
55
|
+
# commit of its own, and no non-zero exit.
|
|
56
|
+
#
|
|
57
|
+
# Usage: push-branch.sh [<repo-dir>]
|
|
58
|
+
# <repo-dir> worktree/repository to operate in (default: $PWD)
|
|
59
|
+
#
|
|
60
|
+
# REPRO — reproduce any decision by hand, against a throwaway fixture:
|
|
61
|
+
#
|
|
62
|
+
# b=$(mktemp -d)/origin.git; git init -q --bare "$b"
|
|
63
|
+
# d=$(mktemp -d); git -C "$d" init -q -b feat_x; git -C "$d" remote add origin "$b"
|
|
64
|
+
# printf '%s' '{"version":1,"defaultBranch":"trunk","protectedBranches":["trunk","release/*"],"stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
|
|
65
|
+
# git -C "$d" add -A && git -C "$d" commit -qm "seed"
|
|
66
|
+
#
|
|
67
|
+
# no upstream yet push-branch.sh "$d" -> exit 0; `git -C "$b" branch` lists
|
|
68
|
+
# feat_x and the branch now has an upstream
|
|
69
|
+
# nothing new push-branch.sh "$d" -> exit 0 ("everything up to date")
|
|
70
|
+
# protected branch git -C "$d" checkout -q -b trunk; push-branch.sh "$d"
|
|
71
|
+
# -> exit 0, refusal message, "$b" unchanged
|
|
72
|
+
# unresolvable printf 'x' > "$d/harness.config.json"; push-branch.sh "$d"
|
|
73
|
+
# -> exit 0, refusal message, nothing pushed
|
|
74
|
+
# push fails git -C "$d" remote set-url origin /nonexistent.git
|
|
75
|
+
# -> exit 0 with a visible failure line
|
|
76
|
+
|
|
77
|
+
set -uo pipefail
|
|
78
|
+
|
|
79
|
+
# The library is reached by a path computed from this script's own location — no
|
|
80
|
+
# session root, and no runtime-substituted token, is assumed. Not finding it is
|
|
81
|
+
# non-fatal like everything else here.
|
|
82
|
+
hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
|
|
83
|
+
if [ ! -r "$hr_lib" ]; then
|
|
84
|
+
echo "push-branch.sh: cannot read '$hr_lib' — refusing to push"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
# shellcheck source=lib/harness-run-lib.sh
|
|
88
|
+
. "$hr_lib"
|
|
89
|
+
|
|
90
|
+
repo_dir="${1:-$PWD}"
|
|
91
|
+
|
|
92
|
+
# Resolve the repository top. Not a git repository => warn and skip (non-fatal).
|
|
93
|
+
top="$(hr_repo_root "$repo_dir")"
|
|
94
|
+
if [ -z "$top" ]; then
|
|
95
|
+
echo "push-branch.sh: '$repo_dir' is not a git repository — nothing to push"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Current branch. Empty => detached HEAD => refuse (non-fatal skip).
|
|
100
|
+
branch="$(hr_current_branch "$top")"
|
|
101
|
+
if [ -z "$branch" ]; then
|
|
102
|
+
echo "push-branch.sh: detached HEAD — refusing to push"
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# The protected-branch decision, from this repository's configuration. Called
|
|
107
|
+
# unsubstituted so the library's per-process cache is warmed here.
|
|
108
|
+
hr_branch_is_protected "$top" "$branch"
|
|
109
|
+
protected=$?
|
|
110
|
+
case "$protected" in
|
|
111
|
+
0)
|
|
112
|
+
echo "push-branch.sh: branch '$branch' is protected — refusing to push"
|
|
113
|
+
exit 0
|
|
114
|
+
;;
|
|
115
|
+
2)
|
|
116
|
+
# Fail closed, and stay non-fatal: a configuration that cannot be read is a
|
|
117
|
+
# branch that cannot be judged, so the push does not happen — but the run
|
|
118
|
+
# that called this is not stopped over it.
|
|
119
|
+
echo "push-branch.sh: cannot resolve '$top/harness.config.json' — refusing to push"
|
|
120
|
+
exit 0
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
|
|
124
|
+
# Push the branch. Use the existing upstream when one is configured; otherwise
|
|
125
|
+
# set it on the fly, so a branch with no upstream is still pushed. Fast-forward
|
|
126
|
+
# only.
|
|
127
|
+
if git -C "$top" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
|
128
|
+
git -C "$top" push
|
|
129
|
+
else
|
|
130
|
+
git -C "$top" push --set-upstream origin "$branch"
|
|
131
|
+
fi
|
|
132
|
+
push_status=$?
|
|
133
|
+
|
|
134
|
+
if [ "$push_status" -eq 0 ]; then
|
|
135
|
+
echo "push-branch.sh: pushed $branch to origin"
|
|
136
|
+
else
|
|
137
|
+
# Visible but non-blocking: a push problem must never abort the calling run.
|
|
138
|
+
echo "push-branch.sh: push failed for $branch (see output above)"
|
|
139
|
+
fi
|
|
140
|
+
exit 0
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# refresh-branch.sh — merge the configured default branch INTO the branch this
|
|
3
|
+
# checkout has out, so a run whose base went stale can catch up. One direction
|
|
4
|
+
# only: into the working branch, never into the integration line.
|
|
5
|
+
#
|
|
6
|
+
# WHY IT EXISTS. The plugin's protected-branch guard denies a `git merge` or
|
|
7
|
+
# `git rebase` whose target argument names a protected branch, and the generated
|
|
8
|
+
# permission profile puts `Bash(git merge:*)` on `ask` — which an unattended run
|
|
9
|
+
# cannot answer. Both fire on the SAFE direction too, because the safe direction
|
|
10
|
+
# still spells the protected branch as its argument. So a run cut from a stale
|
|
11
|
+
# base had no permitted way to refresh it, and the only route left was evasion.
|
|
12
|
+
# This wrapper is the sanctioned route: `bash <scripts_dir>/refresh-branch.sh` is
|
|
13
|
+
# allow-listed in three literal forms by the generated permission profile (its
|
|
14
|
+
# row in `cli/src/generators/outerLoopScripts.ts` carries `agentInvocable: true`)
|
|
15
|
+
# and auto-allowed by the script-allowlist guard, and it carries no `merge` token
|
|
16
|
+
# on the caller's command line for the protected-branch guard to judge.
|
|
17
|
+
#
|
|
18
|
+
# IT IS NOT A WAY AROUND THAT GUARD — IT RE-IMPLEMENTS ITS INTENT. The guard's
|
|
19
|
+
# dangerous case is a merge or rebase issued while HEAD is ON a protected branch,
|
|
20
|
+
# which would put unreviewed work on the integration line. This script makes that
|
|
21
|
+
# decision itself, from `<repo_root>/harness.config.json` at run time through
|
|
22
|
+
# `lib/harness-run-lib.sh` rather than from a branch name written into this file,
|
|
23
|
+
# and refuses (exit 2) before anything is fetched or merged. The library's
|
|
24
|
+
# unresolvable answer is a refusal here too, never a permit.
|
|
25
|
+
#
|
|
26
|
+
# WHAT IT NEVER DOES. It never pushes. It never checks out, resets, updates or
|
|
27
|
+
# deletes the base branch — the only ref it writes is the remote-tracking
|
|
28
|
+
# `refs/remotes/origin/<base>` a fetch updates. It never rebases and never
|
|
29
|
+
# rewrites history. It leaves a conflicted merge aborted rather than half-done,
|
|
30
|
+
# so a failed run's working copy is exactly what it was — with one exception it
|
|
31
|
+
# reports as its own status rather than hiding: exit 5 below, where the abort
|
|
32
|
+
# itself failed and the tree is left mid-merge.
|
|
33
|
+
#
|
|
34
|
+
# `--local` MERGES THE LOCAL `<base>` REF AND MAKES NO NETWORK CALL. Default mode
|
|
35
|
+
# fetches and merges `origin/<base>`. `--local` is for the state where the local
|
|
36
|
+
# base is AHEAD of the remote one — commits landed in the main checkout and were
|
|
37
|
+
# not pushed — which is exactly the state a worktree cut from `origin/<base>` is
|
|
38
|
+
# stale against while `origin/<base>` looks current.
|
|
39
|
+
#
|
|
40
|
+
# Usage: refresh-branch.sh [--local]
|
|
41
|
+
# --local merge the local `<base>` branch instead of `origin/<base>`, and
|
|
42
|
+
# skip the fetch
|
|
43
|
+
# No repository argument: the checkout is THIS FILE's own location, never $PWD,
|
|
44
|
+
# because the caller runs it from wherever it happens to be.
|
|
45
|
+
#
|
|
46
|
+
# Exit map a caller can switch on:
|
|
47
|
+
#
|
|
48
|
+
# 0 the current branch now contains the base (the merge landed)
|
|
49
|
+
# 1 usage error / not a git repository / detached HEAD / the configuration
|
|
50
|
+
# could not be read / the working tree has uncommitted tracked changes
|
|
51
|
+
# 2 protected-branch refusal — HEAD is ON a protected branch, so this would be
|
|
52
|
+
# a merge INTO the integration line; nothing was attempted
|
|
53
|
+
# 3 nothing to do — the current branch already contains the base
|
|
54
|
+
# 4 the merge conflicted and was aborted; the working copy is exactly as it was
|
|
55
|
+
# 5 the merge conflicted and `git merge --abort` ALSO failed, so the working
|
|
56
|
+
# copy is left mid-merge and is NOT what it was — the one row of this map on
|
|
57
|
+
# which this script's "leaves a conflicted merge aborted rather than
|
|
58
|
+
# half-done" guarantee does not hold, and the one that needs a human before
|
|
59
|
+
# anything else runs in this checkout
|
|
60
|
+
#
|
|
61
|
+
# Two conditions outside that list land on 1 as well, for the same reason every
|
|
62
|
+
# other row of it does — the script cannot proceed and changed nothing: the base
|
|
63
|
+
# ref named by the mode is not present, and a merge that failed for a reason
|
|
64
|
+
# other than a conflict.
|
|
65
|
+
#
|
|
66
|
+
# REPRO — reproduce every row by hand, against a throwaway fixture:
|
|
67
|
+
#
|
|
68
|
+
# b=$(mktemp -d)/origin.git; git init -q --bare "$b"
|
|
69
|
+
# d=$(mktemp -d); git -C "$d" init -q -b trunk; git -C "$d" remote add origin "$b"
|
|
70
|
+
# printf '%s' '{"version":1,"defaultBranch":"trunk","protectedBranches":["trunk","release/*"],"stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
|
|
71
|
+
# printf 'a\n' > "$d/f.txt"; git -C "$d" add -A; git -C "$d" commit -qm seed
|
|
72
|
+
# git -C "$d" push -q -u origin trunk
|
|
73
|
+
# git -C "$d" checkout -q -b feat_x
|
|
74
|
+
# mkdir -p "$d/scripts/lib" # copy this script + lib/ there
|
|
75
|
+
# # move trunk on and publish it, so feat_x is behind:
|
|
76
|
+
# git -C "$d" checkout -q trunk; printf 'b\n' >> "$d/f.txt"
|
|
77
|
+
# git -C "$d" commit -qam base; git -C "$d" push -q; git -C "$d" checkout -q feat_x
|
|
78
|
+
#
|
|
79
|
+
# behind the base bash "$d/scripts/refresh-branch.sh" -> exit 0, and
|
|
80
|
+
# `git -C "$d" log --oneline` shows the base commit
|
|
81
|
+
# already current the same call again -> exit 3
|
|
82
|
+
# protected HEAD git -C "$d" checkout -q trunk; same call -> exit 2, nothing
|
|
83
|
+
# merged; `git -C "$d" checkout -q feat_x` -> exit 0 again
|
|
84
|
+
# detached HEAD git -C "$d" checkout -q --detach; same call -> exit 1
|
|
85
|
+
# dirty tree printf 'x\n' >> "$d/f.txt"; same call -> exit 1, and
|
|
86
|
+
# `git -C "$d" status --porcelain` is byte-identical after
|
|
87
|
+
# conflict edit the same line on trunk and on feat_x, commit both
|
|
88
|
+
# -> exit 4, and `git -C "$d" status` shows no MERGING state
|
|
89
|
+
# failed abort (5) no fixture row: the state that makes `git merge --abort`
|
|
90
|
+
# fail has to arise between this script's own merge and its
|
|
91
|
+
# abort, and nothing outside the script runs in that window
|
|
92
|
+
# unresolvable printf 'x' > "$d/harness.config.json"; same call -> exit 1,
|
|
93
|
+
# nothing merged; restore -> exit 0
|
|
94
|
+
# --local commit on trunk WITHOUT pushing, then from feat_x:
|
|
95
|
+
# bash "$d/scripts/refresh-branch.sh" --local -> exit 0
|
|
96
|
+
# the same call without --local -> exit 3
|
|
97
|
+
|
|
98
|
+
set -uo pipefail
|
|
99
|
+
|
|
100
|
+
# The library is reached by a path computed from this script's own location, so a
|
|
101
|
+
# copy that was moved with its `lib/` still finds it, and no session root or
|
|
102
|
+
# runtime-substituted token is assumed.
|
|
103
|
+
hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
|
|
104
|
+
if [ ! -r "$hr_lib" ]; then
|
|
105
|
+
echo "refresh-branch.sh: cannot read '$hr_lib' — refusing to merge" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
# shellcheck source=lib/harness-run-lib.sh
|
|
109
|
+
. "$hr_lib"
|
|
110
|
+
|
|
111
|
+
use_local=0
|
|
112
|
+
while [ "$#" -gt 0 ]; do
|
|
113
|
+
case "$1" in
|
|
114
|
+
--local)
|
|
115
|
+
use_local=1
|
|
116
|
+
shift
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
echo "refresh-branch.sh: unknown argument '$1'" >&2
|
|
120
|
+
echo " usage: refresh-branch.sh [--local]" >&2
|
|
121
|
+
exit 1
|
|
122
|
+
;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
# The checkout being refreshed is where THIS FILE lives — never $PWD, which
|
|
127
|
+
# belongs to whoever invoked it.
|
|
128
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
129
|
+
top="$(hr_repo_root "$script_dir")" || top=""
|
|
130
|
+
if [ -z "$top" ]; then
|
|
131
|
+
echo "refresh-branch.sh: '$script_dir' is not inside a git repository — refusing to merge" >&2
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Warm the library's cache once, unsubstituted, and fail closed here rather than
|
|
136
|
+
# letting each reader below fail separately: a configuration that cannot be read
|
|
137
|
+
# is a protected set that cannot be judged.
|
|
138
|
+
if ! hr_config_load "$top"; then
|
|
139
|
+
echo "refresh-branch.sh: cannot resolve '$top/harness.config.json' — refusing to merge" >&2
|
|
140
|
+
echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
base="$(hr_default_branch "$top")" || base=""
|
|
145
|
+
if [ -z "$base" ]; then
|
|
146
|
+
echo "refresh-branch.sh: no defaultBranch in '$top/harness.config.json' — refusing to merge" >&2
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
branch="$(hr_current_branch "$top")"
|
|
151
|
+
if [ -z "$branch" ]; then
|
|
152
|
+
echo "refresh-branch.sh: detached HEAD — refusing to merge" >&2
|
|
153
|
+
exit 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# The direction check, and the reason this wrapper is safe to allow-list. It
|
|
157
|
+
# refuses on the branch the guard's dangerous case is about, and it takes the
|
|
158
|
+
# library's unresolvable answer as a refusal rather than as a permit.
|
|
159
|
+
hr_branch_is_protected "$top" "$branch"
|
|
160
|
+
protected=$?
|
|
161
|
+
case "$protected" in
|
|
162
|
+
0)
|
|
163
|
+
patterns="$(hr_protected_patterns "$top")" || patterns=""
|
|
164
|
+
protected_set=""
|
|
165
|
+
while IFS= read -r pattern; do
|
|
166
|
+
[ -n "$pattern" ] || continue
|
|
167
|
+
if [ -z "$protected_set" ]; then
|
|
168
|
+
protected_set="$pattern"
|
|
169
|
+
else
|
|
170
|
+
protected_set="$protected_set $pattern"
|
|
171
|
+
fi
|
|
172
|
+
done <<EOF
|
|
173
|
+
$patterns
|
|
174
|
+
EOF
|
|
175
|
+
echo "refresh-branch.sh: HEAD is on protected branch '$branch' — refusing to merge into it" >&2
|
|
176
|
+
echo " protected set (from harness.config.json): $protected_set" >&2
|
|
177
|
+
echo " this wrapper only merges '$base' INTO a working branch; check one out and re-run" >&2
|
|
178
|
+
exit 2
|
|
179
|
+
;;
|
|
180
|
+
2)
|
|
181
|
+
echo "refresh-branch.sh: cannot resolve '$top/harness.config.json' — refusing to merge" >&2
|
|
182
|
+
exit 1
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
|
|
186
|
+
# A merge writes the working tree, so an uncommitted tracked change is refused
|
|
187
|
+
# before anything is fetched: the caller's own edit must not be carried into a
|
|
188
|
+
# merge commit, and an aborted merge must be able to restore what was there.
|
|
189
|
+
if ! git -C "$top" diff --quiet || ! git -C "$top" diff --cached --quiet; then
|
|
190
|
+
echo "refresh-branch.sh: uncommitted tracked changes in '$top' — refusing to merge" >&2
|
|
191
|
+
echo " commit them (bash \"$script_dir/commit-on-branch.sh\" …) and re-run" >&2
|
|
192
|
+
exit 1
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
if [ "$use_local" -eq 1 ]; then
|
|
196
|
+
base_ref="$base"
|
|
197
|
+
ref_path="refs/heads/$base"
|
|
198
|
+
else
|
|
199
|
+
base_ref="origin/$base"
|
|
200
|
+
ref_path="refs/remotes/origin/$base"
|
|
201
|
+
# Tolerate a failed fetch the way create-worktree.sh does — offline with the
|
|
202
|
+
# ref already present is still a refresh this can do — and judge the ref the
|
|
203
|
+
# next block names rather than the fetch. git's own stderr still reaches the
|
|
204
|
+
# caller's log whichever outcome follows.
|
|
205
|
+
git -C "$top" fetch origin "$base" || true
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if ! git -C "$top" show-ref --verify --quiet "$ref_path"; then
|
|
209
|
+
echo "refresh-branch.sh: no '$base_ref' in '$top' — nothing to merge from" >&2
|
|
210
|
+
if [ "$use_local" -eq 1 ]; then
|
|
211
|
+
echo " --local merges the local '$base' branch; drop the flag to merge origin/$base" >&2
|
|
212
|
+
else
|
|
213
|
+
echo " push the default branch (git push -u origin $base), or pass --local to merge the local one" >&2
|
|
214
|
+
fi
|
|
215
|
+
exit 1
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# Already contained => a distinct, dedicated status, so a caller can tell "the
|
|
219
|
+
# base is already here" apart from a refusal.
|
|
220
|
+
if git -C "$top" merge-base --is-ancestor "$base_ref" HEAD; then
|
|
221
|
+
echo "refresh-branch.sh: '$branch' already contains $base_ref — nothing to do"
|
|
222
|
+
exit 3
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if git -C "$top" merge --no-edit "$base_ref"; then
|
|
226
|
+
echo "refresh-branch.sh: merged $base_ref into $branch"
|
|
227
|
+
exit 0
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# A failed merge is one of two things, and they are distinguished by whether a
|
|
231
|
+
# merge is in progress: a conflict leaves MERGE_HEAD behind, anything else (a
|
|
232
|
+
# refused fast-forward, a hook, an unmergeable ref) never started one.
|
|
233
|
+
if git -C "$top" rev-parse --verify --quiet MERGE_HEAD >/dev/null; then
|
|
234
|
+
if git -C "$top" merge --abort; then
|
|
235
|
+
echo "refresh-branch.sh: $base_ref conflicts with $branch — merge aborted, working copy unchanged" >&2
|
|
236
|
+
exit 4
|
|
237
|
+
fi
|
|
238
|
+
echo "refresh-branch.sh: $base_ref conflicts with $branch and 'git merge --abort' failed — the working copy is LEFT MID-MERGE and is NOT what it was" >&2
|
|
239
|
+
echo " resolve or abandon it yourself (git status, git merge --abort), then re-run" >&2
|
|
240
|
+
exit 5
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
echo "refresh-branch.sh: 'git merge $base_ref' failed (see output above) — nothing was merged" >&2
|
|
244
|
+
exit 1
|