@hasna/skills 0.1.61 → 0.1.62
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/agent-skills/skill-publish/SKILL.md +55 -8
- package/agent-skills/skill-publish/scripts/capture_registry.js +47 -0
- package/agent-skills/skill-publish/scripts/publish_with_git_head.sh +143 -0
- package/agent-skills/skill-publish/scripts/test_publish_with_git_head.sh +231 -0
- package/bin/index.js +355 -355
- package/bin/mcp.js +214 -214
- package/bin/server.js +126 -126
- package/bin/worker.js +126 -126
- package/dist/index.js +78 -78
- package/package.json +1 -1
|
@@ -90,7 +90,7 @@ If tests fail → fix them before continuing. Never publish broken tests.
|
|
|
90
90
|
```bash
|
|
91
91
|
git add package.json bun.lock
|
|
92
92
|
SECRET_PATTERN='sk-(ant|proj)-|npm_[[:alpha:]]|gh[op]_|AKIA[[:upper:][:digit:]]|xai[-]|AI[z]a'
|
|
93
|
-
if git diff --cached --diff-filter=ACM --unified=0 | grep -iE "^\\+[^+].*($SECRET_PATTERN)"; then
|
|
93
|
+
if git diff --cached --diff-filter=ACM --unified=0 | grep -iE "^\\+([^+].*)?($SECRET_PATTERN)"; then
|
|
94
94
|
echo "SECRETS - STOP"
|
|
95
95
|
exit 1
|
|
96
96
|
fi
|
|
@@ -121,7 +121,7 @@ test "$(cat package.json | python3 -c "import sys,json; print(json.load(sys.stdi
|
|
|
121
121
|
### 7. Post publish intent to git-publishing (BEFORE publishing)
|
|
122
122
|
|
|
123
123
|
Fleet rule: publish intent goes to the `git-publishing` channel BEFORE any
|
|
124
|
-
`
|
|
124
|
+
`npm publish`, and the result is confirmed in the same thread
|
|
125
125
|
after. Re-check blockers first — an unread `[FREEZE]` means stop and escalate
|
|
126
126
|
to `help`.
|
|
127
127
|
|
|
@@ -136,17 +136,62 @@ echo "Intent posted: message $INTENT_ID"
|
|
|
136
136
|
|
|
137
137
|
### 8. Publish
|
|
138
138
|
|
|
139
|
+
Npm 10.9.8 and 11.16.0 both omit `gitHead` when they publish directly from a
|
|
140
|
+
linked Git worktree, because their package normalizer treats `.git` as a
|
|
141
|
+
directory and silently ignores the worktree's `.git` file. Publish through the
|
|
142
|
+
deterministic helper beside this SKILL.md. It packs the prepared package, stages
|
|
143
|
+
those files in a disposable normal clone at the exact source HEAD/tree, preserves
|
|
144
|
+
the package's repository-relative subdirectory, and never mutates the source
|
|
145
|
+
worktree's Git metadata.
|
|
146
|
+
|
|
147
|
+
Resolve `PUBLISH_HELPER` to `scripts/publish_with_git_head.sh` beside this
|
|
148
|
+
SKILL.md in the installed skill directory. Do not copy the helper into the
|
|
149
|
+
package repo or replace it with raw `npm publish`.
|
|
150
|
+
|
|
139
151
|
```bash
|
|
140
152
|
ACCESS=public
|
|
141
153
|
[ "$ORG" = "hasnaxyz" ] && ACCESS=restricted
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
TOKEN_PATH="$ORG/npm/live/publish-token"
|
|
155
|
+
EXPECTED_GIT_HEAD=$(git rev-parse HEAD)
|
|
156
|
+
PUBLISH_HELPER="<directory containing this SKILL.md>/scripts/publish_with_git_head.sh"
|
|
157
|
+
test -f "$PUBLISH_HELPER"
|
|
158
|
+
|
|
159
|
+
(
|
|
160
|
+
NPMRC=$(mktemp)
|
|
161
|
+
trap 'rm -f "$NPMRC"' EXIT
|
|
162
|
+
chmod 600 "$NPMRC"
|
|
163
|
+
printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > "$NPMRC"
|
|
164
|
+
secrets exec "$TOKEN_PATH" --as NODE_AUTH_TOKEN -- \
|
|
165
|
+
bash "$PUBLISH_HELPER" --userconfig "$NPMRC" --access "$ACCESS"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
VERIFY_DIR=$(mktemp -d)
|
|
169
|
+
PUBLISHED_GIT_HEAD=""
|
|
170
|
+
for attempt in 1 2 3 4 5; do
|
|
171
|
+
if npm view "$PKG@$NEW_VERSION" gitHead --json \
|
|
172
|
+
> "$VERIFY_DIR/githead.out" \
|
|
173
|
+
2> "$VERIFY_DIR/githead.err"; then
|
|
174
|
+
if [ -s "$VERIFY_DIR/githead.out" ]; then
|
|
175
|
+
if PUBLISHED_GIT_HEAD=$(node -e '
|
|
176
|
+
const fs = require("node:fs");
|
|
177
|
+
const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
178
|
+
if (typeof value !== "string") process.exit(2);
|
|
179
|
+
process.stdout.write(value);
|
|
180
|
+
' "$VERIFY_DIR/githead.out"); then
|
|
181
|
+
break
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
sleep 2
|
|
186
|
+
done
|
|
187
|
+
rm -rf "$VERIFY_DIR"
|
|
188
|
+
|
|
189
|
+
if [ "$PUBLISHED_GIT_HEAD" != "$EXPECTED_GIT_HEAD" ]; then
|
|
190
|
+
printf 'GITHEAD_OUTPUT: %s\n' "${PUBLISHED_GIT_HEAD:-ABSENT}"
|
|
191
|
+
printf 'GITHEAD_EXPECTED: %s\n' "$EXPECTED_GIT_HEAD"
|
|
148
192
|
exit 1
|
|
149
193
|
fi
|
|
194
|
+
printf 'GITHEAD_VERIFIED: %s\n' "$PUBLISHED_GIT_HEAD"
|
|
150
195
|
```
|
|
151
196
|
|
|
152
197
|
### 9. Install locally via bun (NOT npm)
|
|
@@ -199,6 +244,8 @@ Repo releases (GitHub tags/releases) are announced on `git-releases`, not
|
|
|
199
244
|
- NEVER publish without running tests and build first
|
|
200
245
|
- NEVER publish with secrets in the staged diff
|
|
201
246
|
- NEVER publish without posting intent to `git-publishing` first (step 7)
|
|
247
|
+
- NEVER publish through raw `bun publish` or `npm publish`; use the worktree-safe helper
|
|
248
|
+
- NEVER report success until registry `gitHead` equals the exact source HEAD
|
|
202
249
|
- ALWAYS confirm in the intent thread after — published or aborted, no silent exits
|
|
203
250
|
- NEVER bump minor or major — patch only unless user explicitly says otherwise
|
|
204
251
|
- NEVER skip the CLI verification step
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
|
|
4
|
+
const [prefix, portFile] = process.argv.slice(2);
|
|
5
|
+
const putStatus = Number(process.env.CAPTURE_PUT_STATUS ?? "201");
|
|
6
|
+
let requestIndex = 0;
|
|
7
|
+
|
|
8
|
+
if (!prefix || !portFile) {
|
|
9
|
+
throw new Error("usage: capture_registry.js <output-prefix> <port-file>");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const server = createServer((request, response) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
|
|
15
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
16
|
+
request.on("end", () => {
|
|
17
|
+
requestIndex += 1;
|
|
18
|
+
const body = Buffer.concat(chunks);
|
|
19
|
+
writeFileSync(
|
|
20
|
+
`${prefix}.request-${requestIndex}.json`,
|
|
21
|
+
JSON.stringify(
|
|
22
|
+
{
|
|
23
|
+
method: request.method,
|
|
24
|
+
url: request.url,
|
|
25
|
+
bodyBytes: body.length,
|
|
26
|
+
},
|
|
27
|
+
null,
|
|
28
|
+
2,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (request.method === "PUT") {
|
|
33
|
+
writeFileSync(`${prefix}.put.json`, body);
|
|
34
|
+
response.writeHead(putStatus, { "content-type": "application/json" });
|
|
35
|
+
response.end(JSON.stringify(putStatus < 400 ? { ok: true } : { error: "probe_failure" }));
|
|
36
|
+
setTimeout(() => server.close(), 25);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
41
|
+
response.end(JSON.stringify({ error: "not_found" }));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
server.listen(0, "127.0.0.1", () => {
|
|
46
|
+
writeFileSync(portFile, String(server.address().port));
|
|
47
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
for required_command in git node npm tar; do
|
|
5
|
+
if ! command -v "$required_command" >/dev/null 2>&1; then
|
|
6
|
+
printf 'publish_with_git_head: %s is required\n' "$required_command" >&2
|
|
7
|
+
exit 127
|
|
8
|
+
fi
|
|
9
|
+
done
|
|
10
|
+
|
|
11
|
+
package_dir=$(pwd -P)
|
|
12
|
+
if [ ! -f "$package_dir/package.json" ]; then
|
|
13
|
+
printf 'publish_with_git_head: no package.json in %s\n' "$package_dir" >&2
|
|
14
|
+
exit 2
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
repo_root=$(git -C "$package_dir" rev-parse --show-toplevel)
|
|
18
|
+
head_sha=$(git -C "$repo_root" rev-parse HEAD)
|
|
19
|
+
source_tree=$(git -C "$repo_root" rev-parse "$head_sha^{tree}")
|
|
20
|
+
package_prefix=$(git -C "$package_dir" rev-parse --show-prefix)
|
|
21
|
+
package_relative=${package_prefix%/}
|
|
22
|
+
source_status=$(git -C "$repo_root" status --porcelain --untracked-files=no)
|
|
23
|
+
|
|
24
|
+
if [ -n "$source_status" ]; then
|
|
25
|
+
printf 'publish_with_git_head: tracked source changes must be committed before publish\n' >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
temp_root=$(mktemp -d "${TMPDIR:-/tmp}/hasna-publish-githead.XXXXXX")
|
|
30
|
+
clone_root="$temp_root/repo"
|
|
31
|
+
pack_dir="$temp_root/pack"
|
|
32
|
+
unpack_dir="$temp_root/unpack"
|
|
33
|
+
pack_json="$temp_root/pack.json"
|
|
34
|
+
|
|
35
|
+
cleanup() {
|
|
36
|
+
local temp_name
|
|
37
|
+
temp_name=$(basename "$temp_root")
|
|
38
|
+
case "$temp_name" in
|
|
39
|
+
hasna-publish-githead.*)
|
|
40
|
+
rm -rf -- "$temp_root"
|
|
41
|
+
;;
|
|
42
|
+
*)
|
|
43
|
+
printf 'publish_with_git_head: refusing to remove unexpected temp path %s\n' \
|
|
44
|
+
"$temp_root" >&2
|
|
45
|
+
return 1
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handle_exit() {
|
|
51
|
+
local command_rc=$?
|
|
52
|
+
trap - EXIT INT TERM
|
|
53
|
+
if ! cleanup; then
|
|
54
|
+
exit 70
|
|
55
|
+
fi
|
|
56
|
+
exit "$command_rc"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
trap handle_exit EXIT
|
|
60
|
+
trap 'exit 130' INT
|
|
61
|
+
trap 'exit 143' TERM
|
|
62
|
+
|
|
63
|
+
mkdir -p "$pack_dir" "$unpack_dir"
|
|
64
|
+
(
|
|
65
|
+
cd "$package_dir"
|
|
66
|
+
npm pack --ignore-scripts --json --pack-destination "$pack_dir"
|
|
67
|
+
) > "$pack_json"
|
|
68
|
+
|
|
69
|
+
tarball_name=$(
|
|
70
|
+
node -e '
|
|
71
|
+
const fs = require("node:fs");
|
|
72
|
+
const result = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
73
|
+
if (!Array.isArray(result) || result.length !== 1 || !result[0]?.filename) {
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(result[0].filename);
|
|
77
|
+
' "$pack_json"
|
|
78
|
+
)
|
|
79
|
+
tarball="$pack_dir/$tarball_name"
|
|
80
|
+
if [ ! -f "$tarball" ]; then
|
|
81
|
+
printf 'publish_with_git_head: npm pack did not create %s\n' "$tarball" >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
git clone --quiet --no-hardlinks --no-checkout "$repo_root" "$clone_root"
|
|
86
|
+
git -C "$clone_root" checkout --quiet --detach "$head_sha"
|
|
87
|
+
|
|
88
|
+
clone_head=$(git -C "$clone_root" rev-parse HEAD)
|
|
89
|
+
clone_tree=$(git -C "$clone_root" rev-parse "HEAD^{tree}")
|
|
90
|
+
clone_status=$(git -C "$clone_root" status --porcelain)
|
|
91
|
+
if [ "$clone_head" != "$head_sha" ] || [ "$clone_tree" != "$source_tree" ]; then
|
|
92
|
+
printf 'publish_with_git_head: disposable clone does not match source HEAD/tree\n' >&2
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
if [ -n "$clone_status" ]; then
|
|
96
|
+
printf 'publish_with_git_head: disposable clone is not clean before staging package files\n' >&2
|
|
97
|
+
exit 2
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [ -n "$package_relative" ]; then
|
|
101
|
+
clone_package_dir="$clone_root/$package_relative"
|
|
102
|
+
else
|
|
103
|
+
clone_package_dir="$clone_root"
|
|
104
|
+
fi
|
|
105
|
+
if [ ! -d "$clone_package_dir" ]; then
|
|
106
|
+
printf 'publish_with_git_head: package subdirectory missing in clone: %s\n' \
|
|
107
|
+
"$package_relative" >&2
|
|
108
|
+
exit 2
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
tar -xzf "$tarball" -C "$unpack_dir"
|
|
112
|
+
cp -a "$unpack_dir/package/." "$clone_package_dir/"
|
|
113
|
+
|
|
114
|
+
if [ -d "$repo_root/node_modules" ] && [ ! -e "$clone_root/node_modules" ]; then
|
|
115
|
+
ln -s "$repo_root/node_modules" "$clone_root/node_modules"
|
|
116
|
+
fi
|
|
117
|
+
if [ "$package_dir" != "$repo_root" ] &&
|
|
118
|
+
[ -d "$package_dir/node_modules" ] &&
|
|
119
|
+
[ ! -e "$clone_package_dir/node_modules" ]; then
|
|
120
|
+
ln -s "$package_dir/node_modules" "$clone_package_dir/node_modules"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
node -e '
|
|
124
|
+
const fs = require("node:fs");
|
|
125
|
+
const source = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
126
|
+
const staged = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
127
|
+
if (source.name !== staged.name || source.version !== staged.version) {
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
' "$package_dir/package.json" "$clone_package_dir/package.json"
|
|
131
|
+
|
|
132
|
+
if [ "$(git -C "$repo_root" rev-parse HEAD)" != "$head_sha" ] ||
|
|
133
|
+
[ "$(git -C "$repo_root" rev-parse "HEAD^{tree}")" != "$source_tree" ]; then
|
|
134
|
+
printf 'publish_with_git_head: source HEAD/tree changed while preparing publish\n' >&2
|
|
135
|
+
exit 2
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
printf 'publish_with_git_head: publishing %s from disposable clone at HEAD %s\n' \
|
|
139
|
+
"${package_relative:-.}" "$head_sha" >&2
|
|
140
|
+
(
|
|
141
|
+
cd "$clone_package_dir"
|
|
142
|
+
npm publish "$@"
|
|
143
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
5
|
+
helper="$script_dir/publish_with_git_head.sh"
|
|
6
|
+
temp_root=$(mktemp -d)
|
|
7
|
+
source_repo="$temp_root/source"
|
|
8
|
+
source_package="$source_repo/packages/npm-githead-regression"
|
|
9
|
+
linked_worktree="$temp_root/linked-worktree"
|
|
10
|
+
linked_package="$linked_worktree/packages/npm-githead-regression"
|
|
11
|
+
|
|
12
|
+
cleanup() {
|
|
13
|
+
rm -rf "$temp_root"
|
|
14
|
+
}
|
|
15
|
+
trap cleanup EXIT INT TERM
|
|
16
|
+
|
|
17
|
+
if [ ! -f "$helper" ]; then
|
|
18
|
+
printf 'MISSING_HELPER: %s\n' "$helper"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
mkdir -p "$source_package"
|
|
23
|
+
git init -q -b main "$source_repo"
|
|
24
|
+
(
|
|
25
|
+
cd "$source_package"
|
|
26
|
+
npm init -y > "$temp_root/npm-init.out" 2> "$temp_root/npm-init.err"
|
|
27
|
+
npm pkg set \
|
|
28
|
+
name=npm-githead-regression \
|
|
29
|
+
version=1.0.0 \
|
|
30
|
+
description="Local npm gitHead regression" \
|
|
31
|
+
license=MIT \
|
|
32
|
+
repository.type=git \
|
|
33
|
+
repository.url=git+https://example.invalid/npm-githead-regression.git \
|
|
34
|
+
scripts.prepublishOnly="node prepublish-probe.cjs" \
|
|
35
|
+
> "$temp_root/npm-pkg-set.out" \
|
|
36
|
+
2> "$temp_root/npm-pkg-set.err"
|
|
37
|
+
)
|
|
38
|
+
node -e '
|
|
39
|
+
require("node:fs").writeFileSync(
|
|
40
|
+
process.argv[1],
|
|
41
|
+
[
|
|
42
|
+
"const fs = require(\"node:fs\");",
|
|
43
|
+
"const marker = process.env.PROBE_KILL_MARKER;",
|
|
44
|
+
"if (marker) {",
|
|
45
|
+
" fs.writeFileSync(marker, \"ready\\n\");",
|
|
46
|
+
" setInterval(() => {}, 1000);",
|
|
47
|
+
"}",
|
|
48
|
+
"",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
);
|
|
51
|
+
' "$source_package/prepublish-probe.cjs"
|
|
52
|
+
git -C "$source_repo" config user.name "Provenance Regression"
|
|
53
|
+
git -C "$source_repo" config user.email "provenance-regression@example.invalid"
|
|
54
|
+
git -C "$source_repo" add packages/npm-githead-regression/package.json \
|
|
55
|
+
packages/npm-githead-regression/prepublish-probe.cjs
|
|
56
|
+
git -C "$source_repo" commit --no-verify -q -m "test: seed provenance package"
|
|
57
|
+
git -C "$source_repo" worktree add -q -b publish-regression "$linked_worktree" HEAD
|
|
58
|
+
|
|
59
|
+
expected_head=$(git -C "$linked_worktree" rev-parse HEAD)
|
|
60
|
+
cp "$linked_worktree/.git" "$temp_root/original-gitfile"
|
|
61
|
+
|
|
62
|
+
start_registry() {
|
|
63
|
+
local label=$1
|
|
64
|
+
local status=$2
|
|
65
|
+
capture_prefix="$temp_root/$label"
|
|
66
|
+
port_file="$capture_prefix.port"
|
|
67
|
+
|
|
68
|
+
CAPTURE_PUT_STATUS="$status" \
|
|
69
|
+
node "$script_dir/capture_registry.js" "$capture_prefix" "$port_file" \
|
|
70
|
+
> "$capture_prefix.server.out" \
|
|
71
|
+
2> "$capture_prefix.server.err" &
|
|
72
|
+
registry_pid=$!
|
|
73
|
+
|
|
74
|
+
for _ in $(seq 1 100); do
|
|
75
|
+
if [ -s "$port_file" ]; then
|
|
76
|
+
break
|
|
77
|
+
fi
|
|
78
|
+
sleep 0.05
|
|
79
|
+
done
|
|
80
|
+
if [ ! -s "$port_file" ]; then
|
|
81
|
+
printf 'REGISTRY_START: FAILED\n'
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
registry_port=$(<"$port_file")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
read_git_head() {
|
|
88
|
+
node -e '
|
|
89
|
+
const body = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
|
|
90
|
+
const version = Object.values(body.versions)[0];
|
|
91
|
+
process.stdout.write(version.gitHead ?? "");
|
|
92
|
+
' "$1"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
assert_source_unchanged() {
|
|
96
|
+
test -f "$linked_worktree/.git"
|
|
97
|
+
cmp -s "$linked_worktree/.git" "$temp_root/original-gitfile"
|
|
98
|
+
test -z "$(git -C "$linked_worktree" status --porcelain)"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
assert_no_helper_temp() {
|
|
102
|
+
local temp_parent=$1
|
|
103
|
+
if compgen -G "$temp_parent/hasna-publish-githead.*" >/dev/null; then
|
|
104
|
+
printf 'HELPER_TEMP_REMAINS: %s\n' "$temp_parent"
|
|
105
|
+
return 1
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
publish_args=(
|
|
110
|
+
--access public
|
|
111
|
+
--json
|
|
112
|
+
--fetch-retries=0
|
|
113
|
+
--fetch-retry-mintimeout=1
|
|
114
|
+
--fetch-retry-maxtimeout=1
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
start_registry plain 201
|
|
118
|
+
(
|
|
119
|
+
cd "$linked_package"
|
|
120
|
+
npm publish \
|
|
121
|
+
--registry "http://127.0.0.1:$registry_port" \
|
|
122
|
+
"${publish_args[@]}" \
|
|
123
|
+
"--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
|
|
124
|
+
) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err"
|
|
125
|
+
wait "$registry_pid" 2>/dev/null || true
|
|
126
|
+
plain_head=$(read_git_head "$capture_prefix.put.json")
|
|
127
|
+
if [ -n "$plain_head" ]; then
|
|
128
|
+
printf 'PLAIN_GITHEAD: %s\n' "$plain_head"
|
|
129
|
+
else
|
|
130
|
+
printf 'PLAIN_GITHEAD: ABSENT\n'
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
success_tmp="$temp_root/success-tmp"
|
|
134
|
+
mkdir "$success_tmp"
|
|
135
|
+
start_registry helper-success 201
|
|
136
|
+
(
|
|
137
|
+
cd "$linked_package"
|
|
138
|
+
TMPDIR="$success_tmp" bash "$helper" \
|
|
139
|
+
--registry "http://127.0.0.1:$registry_port" \
|
|
140
|
+
"${publish_args[@]}" \
|
|
141
|
+
"--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
|
|
142
|
+
) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err"
|
|
143
|
+
wait "$registry_pid" 2>/dev/null || true
|
|
144
|
+
helper_head=$(read_git_head "$capture_prefix.put.json")
|
|
145
|
+
printf 'EXPECTED_HEAD: %s\n' "$expected_head"
|
|
146
|
+
printf 'HELPER_GITHEAD: %s\n' "${helper_head:-ABSENT}"
|
|
147
|
+
test "$helper_head" = "$expected_head"
|
|
148
|
+
assert_source_unchanged
|
|
149
|
+
assert_no_helper_temp "$success_tmp"
|
|
150
|
+
printf 'SUCCESS_SOURCE: unchanged\n'
|
|
151
|
+
|
|
152
|
+
dirty_tmp="$temp_root/dirty-tmp"
|
|
153
|
+
mkdir "$dirty_tmp"
|
|
154
|
+
(
|
|
155
|
+
cd "$linked_package"
|
|
156
|
+
npm pkg set description="dirty source must be refused"
|
|
157
|
+
)
|
|
158
|
+
set +e
|
|
159
|
+
(
|
|
160
|
+
cd "$linked_package"
|
|
161
|
+
TMPDIR="$dirty_tmp" bash "$helper" --dry-run
|
|
162
|
+
) > "$temp_root/dirty-source.out" 2> "$temp_root/dirty-source.err"
|
|
163
|
+
dirty_rc=$?
|
|
164
|
+
set -e
|
|
165
|
+
test "$dirty_rc" -ne 0
|
|
166
|
+
grep -q 'tracked source changes must be committed' "$temp_root/dirty-source.err"
|
|
167
|
+
assert_no_helper_temp "$dirty_tmp"
|
|
168
|
+
git -C "$linked_worktree" restore packages/npm-githead-regression/package.json
|
|
169
|
+
assert_source_unchanged
|
|
170
|
+
printf 'DIRTY_SOURCE: refused (helper rc=%s)\n' "$dirty_rc"
|
|
171
|
+
|
|
172
|
+
failure_tmp="$temp_root/failure-tmp"
|
|
173
|
+
mkdir "$failure_tmp"
|
|
174
|
+
start_registry helper-failure 500
|
|
175
|
+
set +e
|
|
176
|
+
(
|
|
177
|
+
cd "$linked_package"
|
|
178
|
+
TMPDIR="$failure_tmp" bash "$helper" \
|
|
179
|
+
--registry "http://127.0.0.1:$registry_port" \
|
|
180
|
+
"${publish_args[@]}" \
|
|
181
|
+
"--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
|
|
182
|
+
) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err"
|
|
183
|
+
failure_rc=$?
|
|
184
|
+
set -e
|
|
185
|
+
wait "$registry_pid" 2>/dev/null || true
|
|
186
|
+
test "$failure_rc" -ne 0
|
|
187
|
+
assert_source_unchanged
|
|
188
|
+
assert_no_helper_temp "$failure_tmp"
|
|
189
|
+
printf 'FAILURE_SOURCE: unchanged (publish rc=%s)\n' "$failure_rc"
|
|
190
|
+
|
|
191
|
+
kill_tmp="$temp_root/kill-tmp"
|
|
192
|
+
mkdir "$kill_tmp"
|
|
193
|
+
kill_marker="$temp_root/kill-ready"
|
|
194
|
+
start_registry helper-kill 201
|
|
195
|
+
(
|
|
196
|
+
cd "$linked_package"
|
|
197
|
+
exec setsid env TMPDIR="$kill_tmp" PROBE_KILL_MARKER="$kill_marker" \
|
|
198
|
+
bash "$helper" \
|
|
199
|
+
--registry "http://127.0.0.1:$registry_port" \
|
|
200
|
+
"${publish_args[@]}" \
|
|
201
|
+
"--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
|
|
202
|
+
) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err" &
|
|
203
|
+
helper_pid=$!
|
|
204
|
+
|
|
205
|
+
for _ in $(seq 1 400); do
|
|
206
|
+
if [ -s "$kill_marker" ]; then
|
|
207
|
+
break
|
|
208
|
+
fi
|
|
209
|
+
sleep 0.05
|
|
210
|
+
done
|
|
211
|
+
if [ ! -s "$kill_marker" ]; then
|
|
212
|
+
printf 'FORCED_TERMINATION_MARKER: ABSENT\n'
|
|
213
|
+
kill -KILL -- "-$helper_pid" 2>/dev/null || true
|
|
214
|
+
kill "$registry_pid" 2>/dev/null || true
|
|
215
|
+
wait "$helper_pid" 2>/dev/null || true
|
|
216
|
+
wait "$registry_pid" 2>/dev/null || true
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
kill -KILL -- "-$helper_pid" 2>/dev/null || true
|
|
221
|
+
set +e
|
|
222
|
+
wait "$helper_pid" 2>/dev/null
|
|
223
|
+
kill_rc=$?
|
|
224
|
+
set -e
|
|
225
|
+
kill "$registry_pid" 2>/dev/null || true
|
|
226
|
+
wait "$registry_pid" 2>/dev/null || true
|
|
227
|
+
test "$kill_rc" -ne 0
|
|
228
|
+
assert_source_unchanged
|
|
229
|
+
compgen -G "$kill_tmp/hasna-publish-githead.*" >/dev/null
|
|
230
|
+
printf 'FORCED_TERMINATION_SOURCE: unchanged (process rc=%s; disposable clone orphaned)\n' \
|
|
231
|
+
"$kill_rc"
|