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,302 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scratch-run.sh — run ONE file that lives under `<state_dir>/scratch/`, in the
|
|
3
|
+
# interpreter that file's own extension names, and refuse every other path.
|
|
4
|
+
#
|
|
5
|
+
# WHAT IT IS FOR. The two things a dispatched agent otherwise has no permitted
|
|
6
|
+
# route to do: a LANGUAGE PROBE — run a few lines in the project's own language
|
|
7
|
+
# before the code that would answer the question exists — and a MUTATION CHECK —
|
|
8
|
+
# break an implementation on purpose to prove a new test fails. Without a route
|
|
9
|
+
# the agent either reaches for an interpreter and is refused, or silently
|
|
10
|
+
# downgrades a claim it meant to verify from *executed* to *reasoned*.
|
|
11
|
+
#
|
|
12
|
+
# WHY A WRAPPER EXISTS AT ALL. Same reason as its siblings: an invocation of a
|
|
13
|
+
# script path under the configured scripts directory is allow-listed literally in
|
|
14
|
+
# three forms by the generated permission profile (this file's row in
|
|
15
|
+
# `cli/src/generators/outerLoopScripts.ts` carries `agentInvocable: true`) and is
|
|
16
|
+
# auto-allowed by the script-allowlist guard — a script under that directory
|
|
17
|
+
# whose basename is not on the guard's deny list, in a command carrying none of
|
|
18
|
+
# `$(…)`, a backtick, `|`, `<`, a braced form other than a bare `${IDENT}`, or a
|
|
19
|
+
# `>` that is neither a descriptor duplication (`2>&1`, `>&2`, `2>&-`) nor a
|
|
20
|
+
# redirection to the literal `/dev/null`. A bare `python3 …` command line is
|
|
21
|
+
# matched statically by neither and draws a prompt an unattended run cannot
|
|
22
|
+
# answer. This basename is deliberately NOT on that deny list, and its absence
|
|
23
|
+
# there is the decision that makes the guard route cover this script from the
|
|
24
|
+
# first run, whether or not the profile has been regenerated.
|
|
25
|
+
#
|
|
26
|
+
# WHAT THIS DOES NOT CONTAIN. The first argument is a path and every remaining
|
|
27
|
+
# argument is forwarded to the file, so nothing here takes an inline program — no
|
|
28
|
+
# `-c`, no `-e`, no `bash -c`. That fence is over WHICH FILE RUNS and over nothing
|
|
29
|
+
# the file does. A probe is a program the agent composed one tool call earlier and
|
|
30
|
+
# it runs with the session's own privileges, so everything the generated profile's
|
|
31
|
+
# `deny` floor and its `ask` list, and the script-allowlist guard's
|
|
32
|
+
# `DENY_SCRIPT_BASENAMES`, withhold from a COMMAND STRING is reachable from inside
|
|
33
|
+
# one — directly, or one `subprocess` / `child_process` call further on. Do not
|
|
34
|
+
# read the file/string distinction as a capability boundary: it costs an extra
|
|
35
|
+
# tool call. The layer that still holds is the `pre-push` git hook, because git
|
|
36
|
+
# runs it however git was invoked, subprocess included — the ranking
|
|
37
|
+
# `plugin/docs/AUTONOMOUS_FLOW_WHITEBOARD.md`'s protected-branch bullet states.
|
|
38
|
+
# The two mitigations that do apply are narrower than containment and are named as
|
|
39
|
+
# what they are: `<state_dir>/scratch/` is gitignored by its contents, so nothing
|
|
40
|
+
# run from here reaches a commit, and the agent instructions require a MUTATION
|
|
41
|
+
# CHECK to be reverted before the task's own verification runs.
|
|
42
|
+
#
|
|
43
|
+
# THE REFUSAL IS THE WHOLE OF THE SAFETY — over WHICH FILE, and over nothing
|
|
44
|
+
# else, per the paragraph above. This script executes what it is given, so the
|
|
45
|
+
# only thing standing between it and an arbitrary FILE is the path test below: the argument is rejected outright if it carries `..` or any
|
|
46
|
+
# character outside `A-Za-z0-9._/-`, and what is left must resolve strictly
|
|
47
|
+
# inside `<repo_root>/<state_dir>/scratch/`. A relative argument resolves against
|
|
48
|
+
# `<repo_root>`; an absolute one is compared as given. Both sides of the
|
|
49
|
+
# comparison are resolved PHYSICALLY (`cd … && pwd -P`), so a symlinked directory
|
|
50
|
+
# planted inside the scratch tree cannot name a target outside it, and a target
|
|
51
|
+
# that is itself a symlink is refused rather than followed. The `..` test rejects
|
|
52
|
+
# the two characters anywhere in the argument rather than only a whole segment —
|
|
53
|
+
# strictly stronger, and free, because nothing in that directory depends on a
|
|
54
|
+
# file's name.
|
|
55
|
+
#
|
|
56
|
+
# THE INTERPRETER COMES FROM THE FILE'S OWN EXTENSION. The table is below, and it
|
|
57
|
+
# is the only place it is stated. NOT from the detected preset, on two grounds
|
|
58
|
+
# that both survive reading `DETECTION_PRESET_NAMES` (`cli/src/config/model.ts`).
|
|
59
|
+
# A preset covers the repository's LAYOUT rather than its toolchain: some names in
|
|
60
|
+
# `DETECTION_PRESET_NAMES` imply a language and others imply none, and which
|
|
61
|
+
# language a repository is actually probed in is settled by `commandFamilies`
|
|
62
|
+
# (`cli/src/detect/presets.ts`), not by the preset. So a preset-derived entry
|
|
63
|
+
# reads the wrong field — silent wherever the preset names no toolchain, and
|
|
64
|
+
# wrong wherever a probe's language is not the one the preset implies. And it
|
|
65
|
+
# would be the wrong SHAPE regardless: a preset-derived profile entry
|
|
66
|
+
# is a `Bash(<interpreter>:*)` grant over every command line that interpreter can
|
|
67
|
+
# be handed, which is the breadth the profile `_README`'s package-manager
|
|
68
|
+
# paragraph refuses — while this table grants nothing at all, the reachability
|
|
69
|
+
# coming from this script's own row. NOT from a configuration key either: the
|
|
70
|
+
# family that answered IS recorded, as `detection.commandFamily`, and this table
|
|
71
|
+
# is still not derived from it — a family id answers which toolchain answered for
|
|
72
|
+
# the command keys, not what interpreter a scratch file needs, and a
|
|
73
|
+
# family-derived entry would be the same over-broad `Bash(<interpreter>:*)` grant
|
|
74
|
+
# that paragraph refuses. An extension the table
|
|
75
|
+
# does not carry is refused by name, listing the ones that are — never guessed,
|
|
76
|
+
# and never run under `sh`.
|
|
77
|
+
#
|
|
78
|
+
# WHAT THAT COSTS, STATED RATHER THAN LEFT TO BE DISCOVERED. The table is a CLOSED
|
|
79
|
+
# seven-extension set with NO ADOPTER EXTENSION POINT: no configuration key, no
|
|
80
|
+
# flag, and the file ships fixed. Its `<extension>:<interpreter>` shape is a SINGLE
|
|
81
|
+
# token run as `exec "$interpreter" "$target" "$@"`, so it cannot express a
|
|
82
|
+
# two-word toolchain invocation — `go run`, `cargo`, `dotnet script`. THAT is what
|
|
83
|
+
# bounds the set, and it bounds it by TOOLCHAIN rather than by adoption: the
|
|
84
|
+
# stacks with no probe route here are the ones whose file is not run by a single
|
|
85
|
+
# token — Go, Rust, .NET, Swift, the JVM and Android, C++. Every language a
|
|
86
|
+
# `commandFamilies` function (`cli/src/detect/presets.ts`) covers that DOES run a
|
|
87
|
+
# source file as `<token> <file>` is carried: Node, Python, Ruby, Dart and PHP.
|
|
88
|
+
# Where an adoption's language is not one this table carries there is no probe
|
|
89
|
+
# route here at all, and what an agent has instead is the evidence downgrade
|
|
90
|
+
# `plugin/agents/layer-implementer.md` requires of it (its *An evidence downgrade
|
|
91
|
+
# is recorded* paragraph) — the claim recorded as reasoned rather than executed.
|
|
92
|
+
# Widening the set to a two-word toolchain is a decision for a later round,
|
|
93
|
+
# carried as a residual in `docs/outer-loop-verification.md` §4.
|
|
94
|
+
#
|
|
95
|
+
# THE RUN MUST NEVER SEE A FILE FROM HERE IN A COMMIT. `<state_dir>/scratch/` is
|
|
96
|
+
# ignored by its CONTENTS — the managed block in the generated `.gitignore`, with
|
|
97
|
+
# a negation for that directory's own README — so a probe cannot be committed by
|
|
98
|
+
# accident. The other half of that duty belongs to the agent rather than to this
|
|
99
|
+
# script: a MUTATION CHECK is REVERTED before the task's own verification runs,
|
|
100
|
+
# because the committer will see that suite and it has to be clean.
|
|
101
|
+
#
|
|
102
|
+
# IT IS DELIBERATELY NOT A WRAPPER. It carries no adopter command line — it is
|
|
103
|
+
# not a `WRAPPER_SCRIPTS` row, answers to no `commands.*` key, and has no
|
|
104
|
+
# `{{command}}` for `init` to inline. Like every outer-loop script it ships fixed
|
|
105
|
+
# and reads `harness.config.json` at run time.
|
|
106
|
+
#
|
|
107
|
+
# IT PRINTS NOTHING OF ITS OWN ON THE RUN PATH. The file's stdout and stderr are
|
|
108
|
+
# the answer, and a caller reading a probe's output must not have to filter a
|
|
109
|
+
# wrapper's verdict line out of it — the opposite of the project-command
|
|
110
|
+
# wrappers, whose one verdict line is their contract. Every refusal, and only a
|
|
111
|
+
# refusal, prints `scratch-run.sh: …` on stderr.
|
|
112
|
+
#
|
|
113
|
+
# Usage: scratch-run.sh <file-under-state-dir-scratch> [<arg>...]
|
|
114
|
+
# <file> the file to run; relative to <repo_root>, or absolute
|
|
115
|
+
# <arg>... forwarded to that file, unchanged
|
|
116
|
+
#
|
|
117
|
+
# Exit map a caller can switch on. A run passes the file's OWN status through
|
|
118
|
+
# untouched, so the refusal codes cannot be exclusive — a probe is free to exit
|
|
119
|
+
# 65. The reliable signal that NOTHING ran is the `scratch-run.sh:` line on
|
|
120
|
+
# stderr; the codes are for telling one refusal from another:
|
|
121
|
+
#
|
|
122
|
+
# 0-N the file ran; this is its own exit status, unmodified
|
|
123
|
+
# 64 usage error — no file argument
|
|
124
|
+
# 65 path refused — a `..`, a character outside `A-Za-z0-9._/-`, a symlink,
|
|
125
|
+
# or a target that does not resolve inside <state_dir>/scratch/
|
|
126
|
+
# 66 the extension has no interpreter in the table below
|
|
127
|
+
# 67 the file is not there, or is not a readable regular file
|
|
128
|
+
# 68 the run-time context could not be established — the shared library is
|
|
129
|
+
# unreadable, this is not a git repository, `harness.config.json` could
|
|
130
|
+
# not be resolved, or the scratch directory has never been materialized
|
|
131
|
+
# 69 the interpreter the extension names is not on PATH
|
|
132
|
+
#
|
|
133
|
+
# REPRO — reproduce any decision by hand, against a throwaway fixture:
|
|
134
|
+
#
|
|
135
|
+
# d=$(mktemp -d); git -C "$d" init -q -b feat_x
|
|
136
|
+
# printf '%s' '{"version":1,"defaultBranch":"main","stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
|
|
137
|
+
# mkdir -p "$d/sdlc-harness/scratch"
|
|
138
|
+
# printf 'import sys\nprint("ok", sys.argv[1:])\n' > "$d/sdlc-harness/scratch/probe.py"
|
|
139
|
+
# cp -R <scripts_dir>/. "$d/scripts/" # this file and its lib/
|
|
140
|
+
#
|
|
141
|
+
# the probe (cd "$d" && scripts/scratch-run.sh sdlc-harness/scratch/probe.py a b)
|
|
142
|
+
# -> prints `ok ['a', 'b']`, exit 0
|
|
143
|
+
# its own status a probe ending `sys.exit(7)` -> exit 7
|
|
144
|
+
# absolute form scripts/scratch-run.sh "$d/sdlc-harness/scratch/probe.py" -> runs
|
|
145
|
+
# outside scripts/scratch-run.sh harness.config.json -> exit 65
|
|
146
|
+
# traversal scripts/scratch-run.sh sdlc-harness/scratch/../../harness.config.json
|
|
147
|
+
# -> exit 65
|
|
148
|
+
# unknown ext mv probe.py probe.pl; same call -> exit 66
|
|
149
|
+
# shell probe mv probe.py probe.sh; same call -> exit 66
|
|
150
|
+
# absent file scripts/scratch-run.sh sdlc-harness/scratch/nope.py -> exit 67
|
|
151
|
+
# unresolvable printf 'x' > "$d/harness.config.json"; same call -> exit 68
|
|
152
|
+
|
|
153
|
+
# Deliberately no `-e`: every refusal below is an explicit `exit`, and an
|
|
154
|
+
# implicit one would skip the message that says what was refused and why.
|
|
155
|
+
set -uo pipefail
|
|
156
|
+
|
|
157
|
+
# --- THE INTERPRETER TABLE, stated once: `<extension>:<interpreter>`, space
|
|
158
|
+
# separated. Both readers below derive from this one string — the lookup, and
|
|
159
|
+
# the refusal message that lists what is supported — so there is no second list
|
|
160
|
+
# to keep in step with it. Matching is exact and case-sensitive: `.PY` is an
|
|
161
|
+
# extension this table does not carry, and a guess is worse than a refusal.
|
|
162
|
+
#
|
|
163
|
+
# THE ONE EXTENSION THIS TABLE OMITS ON PURPOSE IS `.sh`. Not for containment —
|
|
164
|
+
# WHAT THIS DOES NOT CONTAIN above says why a `.py` probe reaches a shell one
|
|
165
|
+
# `subprocess` call further on — but because it is the row reachable by fewer
|
|
166
|
+
# routes than the rest, buying nothing the others do not. The script-allowlist
|
|
167
|
+
# guard requires EVERY `.sh` token in a command to resolve under the scripts
|
|
168
|
+
# directory, so `bash <scripts_dir>/scratch-run.sh <state_dir>/scratch/probe.sh`
|
|
169
|
+
# carries a second one that does not, and falls through to the permission system:
|
|
170
|
+
# it would run on the profile entry alone, drawing a prompt on an adoption whose
|
|
171
|
+
# guard route covers every other row. A shell probe is refused by name here, with
|
|
172
|
+
# the table's supported extensions in the message.
|
|
173
|
+
SCRATCH_INTERPRETERS='py:python3 js:node mjs:node cjs:node rb:ruby dart:dart php:php'
|
|
174
|
+
|
|
175
|
+
# The one directory under `<state_dir>` a file may be run out of. Spelled once
|
|
176
|
+
# here, mirroring the `scratch` row of `STATE_DIR_ENTRIES` in
|
|
177
|
+
# `cli/src/generators/stateDir.ts`.
|
|
178
|
+
SCRATCH_SUBDIR='scratch'
|
|
179
|
+
|
|
180
|
+
# The library is reached by a path computed from this script's own location — no
|
|
181
|
+
# session root and no runtime-substituted token is assumed.
|
|
182
|
+
hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
|
|
183
|
+
if [ ! -r "$hr_lib" ]; then
|
|
184
|
+
echo "scratch-run.sh: cannot read '$hr_lib' — refusing to run anything" >&2
|
|
185
|
+
exit 68
|
|
186
|
+
fi
|
|
187
|
+
# shellcheck source=lib/harness-run-lib.sh
|
|
188
|
+
. "$hr_lib"
|
|
189
|
+
|
|
190
|
+
if [ "$#" -lt 1 ] || [ -z "${1-}" ]; then
|
|
191
|
+
echo "scratch-run.sh: no file argument" >&2
|
|
192
|
+
echo " usage: scratch-run.sh <file-under-state-dir-scratch> [<arg>...]" >&2
|
|
193
|
+
exit 64
|
|
194
|
+
fi
|
|
195
|
+
requested="$1"
|
|
196
|
+
|
|
197
|
+
# --- The path tests, on the argument AS GIVEN, before anything is resolved.
|
|
198
|
+
case "$requested" in
|
|
199
|
+
*..*)
|
|
200
|
+
echo "scratch-run.sh: '$requested' carries '..' — refusing, a scratch path names no parent" >&2
|
|
201
|
+
exit 65
|
|
202
|
+
;;
|
|
203
|
+
esac
|
|
204
|
+
case "$requested" in
|
|
205
|
+
*[!A-Za-z0-9._/-]*)
|
|
206
|
+
echo "scratch-run.sh: '$requested' carries a character outside A-Za-z0-9._/- — refusing" >&2
|
|
207
|
+
exit 65
|
|
208
|
+
;;
|
|
209
|
+
esac
|
|
210
|
+
|
|
211
|
+
# The repository is THIS FILE's own checkout, never the caller's directory: the
|
|
212
|
+
# three forms the permission profile emits include a sibling worktree's own copy,
|
|
213
|
+
# and each copy must answer for the checkout it belongs to whatever directory the
|
|
214
|
+
# caller stood in.
|
|
215
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
216
|
+
repo_root="$(hr_repo_root "$script_dir")"
|
|
217
|
+
if [ -z "$repo_root" ]; then
|
|
218
|
+
echo "scratch-run.sh: '$script_dir' is not inside a git repository — refusing to run anything" >&2
|
|
219
|
+
exit 68
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# `<repo_root>/<state_dir>/scratch`, from this repository's configuration at run
|
|
223
|
+
# time. The library folds "not adopted" and "unreadable" into one non-zero
|
|
224
|
+
# answer, and both are closed here: a configuration that cannot be read is a
|
|
225
|
+
# scratch directory that cannot be located.
|
|
226
|
+
scratch_dir="$(hr_state_path "$repo_root" "$SCRATCH_SUBDIR")"
|
|
227
|
+
if [ -z "$scratch_dir" ]; then
|
|
228
|
+
echo "scratch-run.sh: cannot resolve '$repo_root/harness.config.json' — refusing to run anything" >&2
|
|
229
|
+
echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
|
|
230
|
+
exit 68
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
scratch_real="$(cd "$scratch_dir" 2>/dev/null && pwd -P)"
|
|
234
|
+
if [ -z "$scratch_real" ]; then
|
|
235
|
+
echo "scratch-run.sh: '$scratch_dir' does not exist — re-run 'init' to materialize the state tree" >&2
|
|
236
|
+
exit 68
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# A relative argument resolves against <repo_root>; an absolute one is taken as
|
|
240
|
+
# given. Neither can carry `..` by the test above.
|
|
241
|
+
case "$requested" in
|
|
242
|
+
/*) candidate="$requested" ;;
|
|
243
|
+
*) candidate="${repo_root%/}/$requested" ;;
|
|
244
|
+
esac
|
|
245
|
+
|
|
246
|
+
candidate_dir="$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)"
|
|
247
|
+
if [ -z "$candidate_dir" ]; then
|
|
248
|
+
echo "scratch-run.sh: '$requested' names no existing directory under '$repo_root'" >&2
|
|
249
|
+
exit 67
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# STRICTLY INSIDE: the scratch directory itself or a directory beneath it, on the
|
|
253
|
+
# physical path of both sides, so a symlinked directory in the tree cannot widen
|
|
254
|
+
# the fence.
|
|
255
|
+
case "$candidate_dir" in
|
|
256
|
+
"$scratch_real" | "$scratch_real"/*) ;;
|
|
257
|
+
*)
|
|
258
|
+
echo "scratch-run.sh: '$requested' resolves to '$candidate_dir', outside '$scratch_real' — refusing" >&2
|
|
259
|
+
echo " a file is runnable here only from <state_dir>/$SCRATCH_SUBDIR/ (see that directory's README.md)" >&2
|
|
260
|
+
exit 65
|
|
261
|
+
;;
|
|
262
|
+
esac
|
|
263
|
+
|
|
264
|
+
target="$candidate_dir/$(basename "$candidate")"
|
|
265
|
+
if [ -L "$target" ]; then
|
|
266
|
+
echo "scratch-run.sh: '$requested' is a symlink — refusing, its target is outside this script's judgement" >&2
|
|
267
|
+
exit 65
|
|
268
|
+
fi
|
|
269
|
+
if [ ! -f "$target" ] || [ ! -r "$target" ]; then
|
|
270
|
+
echo "scratch-run.sh: '$target' is not a readable regular file" >&2
|
|
271
|
+
exit 67
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# --- The interpreter, from the extension and from nothing else.
|
|
275
|
+
base="$(basename "$target")"
|
|
276
|
+
case "$base" in
|
|
277
|
+
?*.*) extension="${base##*.}" ;;
|
|
278
|
+
*) extension="" ;;
|
|
279
|
+
esac
|
|
280
|
+
|
|
281
|
+
interpreter=""
|
|
282
|
+
supported=""
|
|
283
|
+
for entry in $SCRATCH_INTERPRETERS; do
|
|
284
|
+
if [ "${entry%%:*}" = "$extension" ]; then interpreter="${entry#*:}"; fi
|
|
285
|
+
if [ -z "$supported" ]; then supported=".${entry%%:*}"; else supported="$supported .${entry%%:*}"; fi
|
|
286
|
+
done
|
|
287
|
+
|
|
288
|
+
if [ -z "$interpreter" ]; then
|
|
289
|
+
echo "scratch-run.sh: '$base' has no interpreter for extension '${extension:-<none>}'" >&2
|
|
290
|
+
echo " the table carries: $supported" >&2
|
|
291
|
+
exit 66
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if ! command -v "$interpreter" >/dev/null 2>&1; then
|
|
295
|
+
echo "scratch-run.sh: '$interpreter' (for .$extension) is not on PATH — nothing was run" >&2
|
|
296
|
+
exit 69
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# The file, then the caller's remaining arguments, and nothing else. `exec` is
|
|
300
|
+
# what makes this script's exit status the file's own, with nothing added.
|
|
301
|
+
shift
|
|
302
|
+
exec "$interpreter" "$target" "$@"
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# setup-worktree.sh — bootstrap the checkout this script sits in, so a run can
|
|
3
|
+
# start in it: link the machine-local files a fresh working copy cannot carry in
|
|
4
|
+
# git, install the committed pre-push backstop for THIS working copy, install
|
|
5
|
+
# dependencies and build.
|
|
6
|
+
#
|
|
7
|
+
# WHO RUNS IT. create-worktree.sh, immediately after it adds a working copy —
|
|
8
|
+
# and a person, by hand, on a checkout that predates a configuration change.
|
|
9
|
+
# Every step is idempotent, so re-running it is the supported repair. It is not
|
|
10
|
+
# meant to be run by a dispatched agent: it is not agent-invocable, it has no
|
|
11
|
+
# entry in the generated permission profile, and it should not get one — it
|
|
12
|
+
# installs dependencies and writes git configuration, which are not decisions an
|
|
13
|
+
# agent takes. That is a CALLING CONVENTION, NOT A GATE. Its basename is not on
|
|
14
|
+
# the script-allowlist guard's `DENY_SCRIPT_BASENAMES`, and that guard grants
|
|
15
|
+
# independently of the profile, so an agent that invokes this path under
|
|
16
|
+
# `scriptsDir` is auto-allowed today — but only in a PLAIN invocation: that
|
|
17
|
+
# guard's construct scan withholds the allow, silently, from any command
|
|
18
|
+
# carrying `$(…)`, a backtick, `|`, `<`, a braced form other than a bare
|
|
19
|
+
# `${IDENT}`, or a `>` that is neither a descriptor duplication (`2>&1`, `>&2`,
|
|
20
|
+
# `2>&-`) nor a redirection to the literal `/dev/null`. Adding the basename to
|
|
21
|
+
# `DENY_SCRIPT_BASENAMES` is what would make the convention enforced.
|
|
22
|
+
#
|
|
23
|
+
# THE FIVE STEPS, AND WHAT EACH DOES WHEN ITS INPUT IS ABSENT.
|
|
24
|
+
#
|
|
25
|
+
# 1. Version manager. A stack that pins its toolchain through a version
|
|
26
|
+
# manager needs that manager's init file sourced before anything installs.
|
|
27
|
+
# Point `HARNESS_VERSION_MANAGER_INIT` at that file (in the watcher's
|
|
28
|
+
# environment, or in the operator's shell profile) and it is sourced here.
|
|
29
|
+
# Unset, or naming a file that is not readable, is a SILENT no-op: most
|
|
30
|
+
# projects have no such file and a note about it every run is noise.
|
|
31
|
+
# 2. The machine-local client env file. When the MAIN checkout has
|
|
32
|
+
# `<appDir>/.env` and this checkout has none, it is SYMLINKED — one file,
|
|
33
|
+
# one place, and gitignored on both sides by the managed `.gitignore`
|
|
34
|
+
# block's `<appDir>/.env` rule, which is what leaves a freshly bootstrapped
|
|
35
|
+
# working copy showing nothing at all in `git status --porcelain`. There is
|
|
36
|
+
# no configuration key for it, and `clientEnvPrefix` is not one (it is
|
|
37
|
+
# review vocabulary — the prefix a build tool requires before exposing a
|
|
38
|
+
# variable to a client bundle — not a path), so the existence test IS the
|
|
39
|
+
# condition. An adopter whose stack needs a different machine-local file
|
|
40
|
+
# adds it here: this script is theirs from `init` on, and a re-run never
|
|
41
|
+
# overwrites it.
|
|
42
|
+
# 3. The test-account credentials at `qa.credentialsPath`, symlinked from the
|
|
43
|
+
# main checkout by the same rule. NEVER copied: the file is sensitive and
|
|
44
|
+
# gitignored, and a symlink keeps the secret in one place and keeps the
|
|
45
|
+
# working copy clean. No key configured means nothing is attempted.
|
|
46
|
+
# 4. The pre-push backstop at `githooksDir`, installed for this working copy
|
|
47
|
+
# alone (see the note at the step itself). A missing hook file is a skip
|
|
48
|
+
# with one line, so a checkout that predates the hook still bootstraps.
|
|
49
|
+
# 5. `commands.depInstall`, then `commands.build`, each run from the
|
|
50
|
+
# repository root. An UNSET key is skipped with one printed line rather
|
|
51
|
+
# than being an error — a project with no build step is ordinary. A build
|
|
52
|
+
# is skipped when the dependency install failed, because a build on a
|
|
53
|
+
# failed install only produces a second, less clear failure.
|
|
54
|
+
#
|
|
55
|
+
# THE REFERENCE TOOLCHAIN IS NOT BOOTSTRAPPED HERE. A project that mirrors a
|
|
56
|
+
# reference implementation lists that implementation's own commands in
|
|
57
|
+
# `parity.toolchainCommands`, which the review phase runs and reports deferred
|
|
58
|
+
# when they are unavailable in an isolated working copy. Installing them is not
|
|
59
|
+
# part of preparing a working copy, and doing it here would make every bootstrap
|
|
60
|
+
# depend on a toolchain most steps never use.
|
|
61
|
+
#
|
|
62
|
+
# WHY NO `set -e`, AND WHY ITS CALLER HAS ONE. Each step's outcome is decided
|
|
63
|
+
# here — skip, warn, or fail — and `-e` would turn the ordinary "this key is not
|
|
64
|
+
# configured" into an abort. Failures are accumulated instead and reported in
|
|
65
|
+
# the exit status, which is what create-worktree.sh's `set -e` then acts on: a
|
|
66
|
+
# working copy whose dependencies did not install is not pushed as if it had.
|
|
67
|
+
#
|
|
68
|
+
# FAIL CLOSED ON A CONFIGURATION IT CANNOT READ. Every path and command below is
|
|
69
|
+
# configured, and none has a remembered fallback, so an absent or unreadable
|
|
70
|
+
# `harness.config.json` is a refusal that changes nothing — not a bootstrap that
|
|
71
|
+
# guesses where the hooks are.
|
|
72
|
+
#
|
|
73
|
+
# Usage: setup-worktree.sh
|
|
74
|
+
# No arguments. The checkout is this script's own location, not $PWD, because
|
|
75
|
+
# the caller runs it from wherever it happens to be.
|
|
76
|
+
#
|
|
77
|
+
# Exit map:
|
|
78
|
+
#
|
|
79
|
+
# 0 every step ran or was deliberately skipped
|
|
80
|
+
# 1 refusal (the library or the configuration could not be read), or a step
|
|
81
|
+
# that was meant to run failed — the message above the exit says which
|
|
82
|
+
#
|
|
83
|
+
# REPRO — reproduce any decision by hand, against a throwaway fixture:
|
|
84
|
+
#
|
|
85
|
+
# w=$(mktemp -d); d="$w/demo"; git init -q -b trunk "$d"; mkdir -p "$d/githooks"
|
|
86
|
+
# printf '#!/usr/bin/env bash\nexit 0\n' > "$d/githooks/pre-push"
|
|
87
|
+
# printf '%s' '{"version":1,"defaultBranch":"trunk","stateDir":"sdlc-harness/","githooksDir":"githooks","layers":[],"commands":{"depInstall":"printf x > deps.marker"}}' > "$d/harness.config.json"
|
|
88
|
+
# mkdir -p "$d/scripts/lib" # copy this script + lib/ there
|
|
89
|
+
#
|
|
90
|
+
# ordinary bash "$d/scripts/setup-worktree.sh"
|
|
91
|
+
# -> exit 0; "$d/deps.marker" exists; the build step prints its
|
|
92
|
+
# skip line; `git -C "$d" config --worktree core.hooksPath`
|
|
93
|
+
# is "$d/githooks"
|
|
94
|
+
# no hook file rm "$d/githooks/pre-push"; re-run -> exit 0 with a skip line
|
|
95
|
+
# env symlink printf 'K=v\n' > "$(git -C "$d" worktree list --porcelain |
|
|
96
|
+
# head -1 | sed 's/^worktree //')/.env"
|
|
97
|
+
# -> a sibling working copy's .env is a SYMLINK to that file;
|
|
98
|
+
# one that already has its own .env is left untouched
|
|
99
|
+
# unresolvable printf 'x' > "$d/harness.config.json"; re-run -> exit 1,
|
|
100
|
+
# nothing installed and no git configuration written
|
|
101
|
+
|
|
102
|
+
set -uo pipefail
|
|
103
|
+
|
|
104
|
+
hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
|
|
105
|
+
if [ ! -r "$hr_lib" ]; then
|
|
106
|
+
echo "setup-worktree.sh: cannot read '$hr_lib' — refusing to bootstrap" >&2
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
# shellcheck source=lib/harness-run-lib.sh
|
|
110
|
+
. "$hr_lib"
|
|
111
|
+
|
|
112
|
+
# The checkout being bootstrapped is where THIS FILE lives — never $PWD, which
|
|
113
|
+
# belongs to whoever invoked it.
|
|
114
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
115
|
+
top="$(hr_repo_root "$script_dir")" || top=""
|
|
116
|
+
if [ -z "$top" ]; then
|
|
117
|
+
echo "setup-worktree.sh: '$script_dir' is not inside a git repository — refusing to bootstrap" >&2
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Warm the library's cache once, unsubstituted, and refuse here rather than
|
|
122
|
+
# letting each reader below fail separately with its own message.
|
|
123
|
+
cfg_status=0
|
|
124
|
+
hr_config_load "$top" || cfg_status=$?
|
|
125
|
+
if [ "$cfg_status" -ne 0 ]; then
|
|
126
|
+
echo "setup-worktree.sh: cannot resolve '$top/harness.config.json' — refusing to bootstrap" >&2
|
|
127
|
+
echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# The main checkout is where every machine-local file has its single source: a
|
|
132
|
+
# working copy created by git carries only tracked content.
|
|
133
|
+
main_repo="$(hr_main_repo "$top")" || main_repo=""
|
|
134
|
+
[ -n "$main_repo" ] || main_repo="$top"
|
|
135
|
+
|
|
136
|
+
status=0
|
|
137
|
+
|
|
138
|
+
# --- 1. Version manager, if this machine uses one. -------------------------
|
|
139
|
+
vm_init="${HARNESS_VERSION_MANAGER_INIT:-}"
|
|
140
|
+
if [ -n "$vm_init" ] && [ -r "$vm_init" ]; then
|
|
141
|
+
echo "setup-worktree.sh: sourcing version-manager init '$vm_init'"
|
|
142
|
+
# shellcheck source=/dev/null
|
|
143
|
+
. "$vm_init" || echo "setup-worktree.sh: version-manager init returned non-zero — continuing" >&2
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# --- 2. The machine-local client env file. ---------------------------------
|
|
147
|
+
# `appDir` is repo-relative and defaults to `.`; joining it as `<root>/.` would
|
|
148
|
+
# work but reads badly in every message below, so normalize it once.
|
|
149
|
+
app_dir="$(hr_app_dir "$top")" || app_dir="."
|
|
150
|
+
if [ "$app_dir" = "." ]; then
|
|
151
|
+
app_here="$top"
|
|
152
|
+
app_main="$main_repo"
|
|
153
|
+
else
|
|
154
|
+
app_here="$top/$app_dir"
|
|
155
|
+
app_main="$main_repo/$app_dir"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ -e "$app_here/.env" ]; then
|
|
159
|
+
: # this checkout has its own; never overwrite it
|
|
160
|
+
elif [ -f "$app_main/.env" ]; then
|
|
161
|
+
if ln -s "$app_main/.env" "$app_here/.env"; then
|
|
162
|
+
echo "setup-worktree.sh: linked $app_here/.env -> $app_main/.env"
|
|
163
|
+
else
|
|
164
|
+
echo "setup-worktree.sh: could not link $app_here/.env — continuing" >&2
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# --- 3. The test-account credentials, linked and never copied. -------------
|
|
169
|
+
qa_creds="$(hr_qa_creds_path "$top")" || qa_creds=""
|
|
170
|
+
if [ -n "$qa_creds" ]; then
|
|
171
|
+
if [ ! -e "$top/$qa_creds" ] && [ -f "$main_repo/$qa_creds" ]; then
|
|
172
|
+
mkdir -p "$(dirname "$top/$qa_creds")"
|
|
173
|
+
if ln -s "$main_repo/$qa_creds" "$top/$qa_creds"; then
|
|
174
|
+
echo "setup-worktree.sh: linked $top/$qa_creds -> $main_repo/$qa_creds"
|
|
175
|
+
else
|
|
176
|
+
echo "setup-worktree.sh: could not link $top/$qa_creds — continuing" >&2
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# --- 4. The pre-push backstop, for THIS working copy. ----------------------
|
|
182
|
+
# All working copies of a repository share one `.git/config`, so a plain
|
|
183
|
+
# `git config core.hooksPath` would write a single value every one of them
|
|
184
|
+
# reads — while each has its own committed hooks directory at a DIFFERENT
|
|
185
|
+
# absolute path. The value written here is therefore ABSOLUTE and
|
|
186
|
+
# WORKTREE-SCOPED, so each working copy resolves to its own tracked hook.
|
|
187
|
+
# Idempotent: both the `config` writes and the `chmod` are no-ops on a re-run.
|
|
188
|
+
hooks_rel="$(hr_githooks_dir "$top")" || hooks_rel=""
|
|
189
|
+
hooks_dir="$top/$hooks_rel"
|
|
190
|
+
if [ -n "$hooks_rel" ] && [ -f "$hooks_dir/pre-push" ]; then
|
|
191
|
+
chmod +x "$hooks_dir/pre-push" || true
|
|
192
|
+
# NOTE: `extensions.worktreeConfig true` is a one-way, REPOSITORY-GLOBAL
|
|
193
|
+
# switch — once enabled it changes config resolution for EVERY working copy
|
|
194
|
+
# sharing this `.git`, including older ones that have not re-run this script.
|
|
195
|
+
# It is safe here because `core.hooksPath` is the only worktree-scoped value
|
|
196
|
+
# and every active working copy re-runs this script; keep that invariant if
|
|
197
|
+
# you add another `--worktree` setting.
|
|
198
|
+
if git -C "$top" config extensions.worktreeConfig true &&
|
|
199
|
+
git -C "$top" config --worktree core.hooksPath "$hooks_dir"; then
|
|
200
|
+
echo "setup-worktree.sh: pre-push hook installed (core.hooksPath -> $hooks_dir)"
|
|
201
|
+
else
|
|
202
|
+
echo "setup-worktree.sh: could not point core.hooksPath at $hooks_dir — this working copy has no pre-push backstop" >&2
|
|
203
|
+
status=1
|
|
204
|
+
fi
|
|
205
|
+
else
|
|
206
|
+
echo "setup-worktree.sh: no pre-push hook at $hooks_dir — skipping hook install" >&2
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# --- 5. Dependencies, then the build. --------------------------------------
|
|
210
|
+
# Run one configured command from the repository root. `commands.*` holds a raw
|
|
211
|
+
# command LINE (the wrapper form is only used for the keys that have wrappers),
|
|
212
|
+
# so it is evaluated — in a subshell, so the `cd` and anything the command sets
|
|
213
|
+
# stay inside it, and in THIS shell's `eval` rather than a fresh `bash -c` so a
|
|
214
|
+
# version manager's shell function from step 1 is still in scope.
|
|
215
|
+
run_configured() {
|
|
216
|
+
local key="$1" label="$2" cmd cmd_status
|
|
217
|
+
cmd="$(hr_command "$top" "$key")"
|
|
218
|
+
cmd_status=$?
|
|
219
|
+
# The unresolvable case is tested FIRST: it also prints nothing, so folding it
|
|
220
|
+
# into the empty-value test would report a configuration this script could not
|
|
221
|
+
# read as an ordinary project with no such step.
|
|
222
|
+
if [ "$cmd_status" -eq 2 ]; then
|
|
223
|
+
echo "setup-worktree.sh: cannot resolve commands.$key — skipping $label" >&2
|
|
224
|
+
return 1
|
|
225
|
+
fi
|
|
226
|
+
if [ "$cmd_status" -ne 0 ] || [ -z "$cmd" ]; then
|
|
227
|
+
echo "setup-worktree.sh: commands.$key is not set — skipping $label"
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
echo "setup-worktree.sh: $label: $cmd"
|
|
231
|
+
(
|
|
232
|
+
cd "$top" || exit 1
|
|
233
|
+
eval "$cmd"
|
|
234
|
+
)
|
|
235
|
+
cmd_status=$?
|
|
236
|
+
if [ "$cmd_status" -ne 0 ]; then
|
|
237
|
+
echo "setup-worktree.sh: $label failed (exit $cmd_status)" >&2
|
|
238
|
+
return 1
|
|
239
|
+
fi
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
deps_ok=1
|
|
244
|
+
if ! run_configured depInstall "dependency install"; then
|
|
245
|
+
deps_ok=0
|
|
246
|
+
status=1
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
if [ "$deps_ok" -eq 1 ]; then
|
|
250
|
+
if ! run_configured build "build"; then
|
|
251
|
+
status=1
|
|
252
|
+
fi
|
|
253
|
+
else
|
|
254
|
+
echo "setup-worktree.sh: skipping build — the dependency install did not succeed" >&2
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
if [ "$status" -eq 0 ]; then
|
|
258
|
+
echo "setup-worktree.sh: bootstrap complete for $top"
|
|
259
|
+
else
|
|
260
|
+
echo "setup-worktree.sh: bootstrap finished with failures for $top (see above)" >&2
|
|
261
|
+
fi
|
|
262
|
+
exit "$status"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Written by `autonomous-sdlc-harness init`, and yours from there on: edit it freely, a re-run
|
|
3
|
+
# keeps your copy. It is what `commands.devServer` invokes: it starts the development server in
|
|
4
|
+
# the background, from the repository root, and prints the pid and the log path a caller needs.
|
|
5
|
+
#
|
|
6
|
+
# Usage: start-dev-server.sh [port] [argument...]
|
|
7
|
+
# The port is optional and must be numeric when it is given; anything else is a usage error, so a
|
|
8
|
+
# mistyped port can never be passed through as a flag. It reaches the command on one channel only,
|
|
9
|
+
# exported as HARNESS_DEV_SERVER_PORT: it is shifted off before the remaining arguments are
|
|
10
|
+
# forwarded, so the raw command line below is never handed the port positionally. A server that
|
|
11
|
+
# takes its port only as an argument has to be given that variable on the raw line below.
|
|
12
|
+
#
|
|
13
|
+
# This wrapper may run a build to completion before it launches, on the pre-start line below; that
|
|
14
|
+
# line is yours to edit like every other, and `:` there means there is nothing to run.
|
|
15
|
+
#
|
|
16
|
+
# Nothing here inspects the port. A stack whose server silently falls through to port+1 when the
|
|
17
|
+
# port is taken has to be told not to, and this wrapper is where you tell it: add its strict-port
|
|
18
|
+
# flag to the raw command line below, or make the command that line runs fail on a taken port. A
|
|
19
|
+
# QA run that quietly moved to another port drives a different run's server.
|
|
20
|
+
|
|
21
|
+
# Deliberately no `-e`, for the same reason as the other wrappers. No captured status either:
|
|
22
|
+
# a development server does not exit while it is doing its job.
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
# Exit with a diagnostic and no pid line: nothing is running, so there is nothing to report. A
|
|
26
|
+
# function rather than an inline `echo … && exit 1`, because the command line below is handed the
|
|
27
|
+
# caller's arguments and `exit` refuses extra ones — `init` writes a call to it as that line when
|
|
28
|
+
# no command line resolved.
|
|
29
|
+
harness_fail() {
|
|
30
|
+
echo "$1" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
port="${1:-}"
|
|
35
|
+
if [ -n "$port" ]; then
|
|
36
|
+
case "$port" in
|
|
37
|
+
*[!0-9]*)
|
|
38
|
+
harness_fail "{{name}}: usage: start-dev-server.sh [port] (port must be numeric, got '${port}')"
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
shift
|
|
42
|
+
export HARNESS_DEV_SERVER_PORT="$port"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Anchor to the repository root, derived from this script's own location and never from the
|
|
46
|
+
# caller's directory: `commands.*` are command lines run from the repository root and every path
|
|
47
|
+
# in `harness.config.json` is repo-relative. `git -C` on the script's own directory answers with
|
|
48
|
+
# the checkout this copy belongs to, so all three forms the permission profile emits — the
|
|
49
|
+
# repo-relative one, its absolute twin and a sibling worktree's own copy — run at the root of the
|
|
50
|
+
# checkout they were invoked out of, whatever directory the caller stood in.
|
|
51
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
52
|
+
repo_root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)"
|
|
53
|
+
if [ -z "$repo_root" ] || ! cd "$repo_root"; then
|
|
54
|
+
harness_fail "{{name}}: could not resolve a repository root from '${script_dir}' (is git on PATH?)"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# A step that has to finish before the server is launched — for a server that serves what was last
|
|
58
|
+
# compiled, the project's own build. Synchronous and above the launch deliberately: the line below
|
|
59
|
+
# is backgrounded and its `$!` is what a caller kills, and a build chained into it with `&&` would
|
|
60
|
+
# make that pid a subshell the kill does not reach past. `:` when there is nothing to run.
|
|
61
|
+
{{prestart}} || harness_fail "{{name}}: the pre-start step failed, so the server was not launched"
|
|
62
|
+
|
|
63
|
+
# $TMPDIR ends in a trailing slash on macOS; strip it so the path carries no doubled separator.
|
|
64
|
+
# With no port the log is the `-default` one, which is also the only case two runs can share.
|
|
65
|
+
tmp_dir="${TMPDIR:-/tmp}"
|
|
66
|
+
tmp_dir="${tmp_dir%/}"
|
|
67
|
+
log_file="${tmp_dir}/harness-dev-server-${port:-default}.log"
|
|
68
|
+
|
|
69
|
+
# Backgrounded rather than `exec`ed, which is the one behaviour a QA phase cannot do without: it
|
|
70
|
+
# needs this shell back to poll the port and it needs a pid of its own to stop the server with,
|
|
71
|
+
# and it needs somewhere to read a startup failure from — which is why both streams go to the log.
|
|
72
|
+
{{command}} "$@" >"$log_file" 2>&1 &
|
|
73
|
+
pid=$!
|
|
74
|
+
|
|
75
|
+
# The only failure this script reports: the launch itself did not happen. A server that started
|
|
76
|
+
# but is not answering yet is success — see the readiness note below.
|
|
77
|
+
#
|
|
78
|
+
# Liveness is the test, not `[ -z "$pid" ]`: `$!` is assigned the moment the job is backgrounded,
|
|
79
|
+
# before the command has been resolved, so it is set even for a command that does not exist and an
|
|
80
|
+
# emptiness test can never fire. The brief settle is what makes the liveness test mean anything —
|
|
81
|
+
# immediately after the `&` the child has not been reaped yet and `kill -0` succeeds for a command
|
|
82
|
+
# that was never found. It is NOT a readiness wait: readiness belongs to the poll named at the foot
|
|
83
|
+
# of this script, and this only has to outlast a command that dies on the spot. Lengthen it if your
|
|
84
|
+
# command forks through a launcher slow enough to be missed here.
|
|
85
|
+
sleep 0.3
|
|
86
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
87
|
+
harness_fail "{{name}}: the development server exited immediately — see $log_file"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# The one wrapper with no PASS/FAIL verdict, because a long-running process has no verdict to
|
|
91
|
+
# give. It reports what a caller has to hold instead: the pid to check liveness and to stop, and
|
|
92
|
+
# the log to read a failed start out of.
|
|
93
|
+
echo "STARTING: {{name}} (pid $pid)"
|
|
94
|
+
echo "LOG: $log_file"
|
|
95
|
+
|
|
96
|
+
# Readiness is deliberately not probed here. The poll already ships as a plugin asset —
|
|
97
|
+
# `scripts/poll-dev-server.sh`, which the QA phase runs against the port once this returns — and a
|
|
98
|
+
# second implementation of it inside a wrapper an adopter edits is how the two come to disagree.
|
|
99
|
+
exit 0
|