@agentsfleet/orly 0.11.0 → 0.12.1
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/README.md +44 -11
- package/audits/deinit-pairs.sh +1 -1
- package/audits/design-tokens.sh +1 -1
- package/audits/error-codes.sh +1 -1
- package/audits/logging.sh +1 -1
- package/audits/rust-error.sh +1 -1
- package/audits/scope.sh +9 -1
- package/audits/sql-mod.sh +1 -1
- package/audits/ufs.sh +1 -1
- package/dispatch/lib.sh +1 -1
- package/docs/CHANGELOG.md +18 -0
- package/docs/JUDGMENTS.md +5 -2
- package/docs/ORLY_ARCHITECTURE.md +7 -0
- package/llms.txt +14 -3
- package/package.json +1 -1
- package/skills/orly-babysit-prs/SKILL.md +19 -4
- package/src/review_poll.test.ts +28 -4
- package/src/scanner_snapshot.test.ts +82 -0
package/README.md
CHANGED
|
@@ -42,22 +42,29 @@ Works with Claude Code, Codex, OpenCode, and Amp.
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
## 0.
|
|
45
|
+
## 0.12: migration verification
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
0.12.0 shipped migration verification and setup guidance. The `.orly/` layout
|
|
48
|
+
and six existing `orly judge` questions originally shipped in 0.11.0.
|
|
49
|
+
This release focuses on package verification, migration evidence and clearer setup guidance; it adds no new runtime judgment question.
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
The `agentsfleet` trial upgraded a dedicated worktree from 0.10.14 using the packed candidate.
|
|
52
|
+
Owner files, hooks and verification commands survived; a second update wrote nothing.
|
|
53
|
+
See the [release report](evals/release/release-report.md) for exact proof and remaining release checks.
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
Jev, TypeSafe's bounded judgment model, supplies advice only when requested.
|
|
56
|
+
Existing deterministic checks retain control of gate results.
|
|
57
|
+
The comparison evaluator is parked for proposed 0.13 work; model accuracy and candidate benefits remain unmeasured.
|
|
58
|
+
See the [judgment reference](docs/JUDGMENTS.md) for evidence selection, offline replay and explicit source upload.
|
|
59
|
+
|
|
60
|
+
0.12.1 avoids copying repository files for staged audits with no matching files
|
|
61
|
+
and makes hosted-check monitoring show job states and links before reporting a failure.
|
|
62
|
+
See [releases](https://github.com/agentsfleet/orly/releases) for published versions.
|
|
63
|
+
The install commands below fetch the published package.
|
|
57
64
|
|
|
58
65
|
---
|
|
59
66
|
|
|
60
|
-
## Prerequisites
|
|
67
|
+
## Prerequisites
|
|
61
68
|
|
|
62
69
|
| You need | Why | Version |
|
|
63
70
|
|---|---|---|
|
|
@@ -71,7 +78,7 @@ published package; use them for this behavior after 0.11.0 is published.
|
|
|
71
78
|
|
|
72
79
|
---
|
|
73
80
|
|
|
74
|
-
## Install
|
|
81
|
+
## Install the published package
|
|
75
82
|
|
|
76
83
|
Run this inside the repository you want governed.
|
|
77
84
|
|
|
@@ -120,6 +127,32 @@ Unknown settings and malformed values are refused; `schemas/profile.schema.json`
|
|
|
120
127
|
|
|
121
128
|
---
|
|
122
129
|
|
|
130
|
+
## Upgrade from 0.10.x
|
|
131
|
+
|
|
132
|
+
Run the update from the consumer repository. If the repository owns its hooks, preserve them explicitly:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bunx --bun @agentsfleet/orly update --no-hooks
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Expected: managed files move under `.orly/`; owner hooks remain unchanged. Conflicting owner edits stop the update and name the affected path.
|
|
139
|
+
Use the installed candidate executable for an unpublished release trial; the command above fetches the published version.
|
|
140
|
+
|
|
141
|
+
Update repository-owned callers that still read `.oracle/orly.json`, root `dispatch/`, or managed root audit paths.
|
|
142
|
+
Their replacements are `.orly/orly.json`, `.orly/dispatch/`, and `.orly/audits/`.
|
|
143
|
+
Repository-owned audit scripts and project documents retain their existing locations.
|
|
144
|
+
Review existing hooks before choosing an update without `--no-hooks`.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
bunx --bun @agentsfleet/orly doctor
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Expected: installed rules match `.orly/orly.json` and work and verification commands are declared.
|
|
151
|
+
Then run the repository's declared checks; `doctor` alone does not run them.
|
|
152
|
+
An empty old directory can remain after its files move. Git does not track empty directories; remove one only after checking it is empty.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
123
156
|
## What lands in your repository
|
|
124
157
|
|
|
125
158
|
```text
|
package/audits/deinit-pairs.sh
CHANGED
package/audits/design-tokens.sh
CHANGED
|
@@ -39,7 +39,7 @@ set -euo pipefail
|
|
|
39
39
|
|
|
40
40
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
|
|
41
41
|
audit_scope_init --all "$@"
|
|
42
|
-
audit_index_snapshot
|
|
42
|
+
audit_index_snapshot '*.tsx' '*.jsx' 'ui/packages/app/*' 'ui/packages/website/*' 'ui/packages/design-system/*'
|
|
43
43
|
|
|
44
44
|
MODE="$AUDIT_MODE"
|
|
45
45
|
ROOT="$(git rev-parse --show-toplevel)"
|
package/audits/error-codes.sh
CHANGED
package/audits/logging.sh
CHANGED
|
@@ -43,7 +43,7 @@ set -euo pipefail
|
|
|
43
43
|
|
|
44
44
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
|
|
45
45
|
audit_scope_init --all "$@"
|
|
46
|
-
audit_index_snapshot
|
|
46
|
+
audit_index_snapshot '*.zig' '*.rs' 'agentsfleet/src/*.js' 'agentsfleet/src/*.jsx' 'agentsfleet/src/*.ts' 'agentsfleet/src/*.tsx'
|
|
47
47
|
|
|
48
48
|
MODE="--all"
|
|
49
49
|
STRICT=0
|
package/audits/rust-error.sh
CHANGED
package/audits/scope.sh
CHANGED
|
@@ -37,7 +37,7 @@ audit_scope_init() {
|
|
|
37
37
|
# the index copy and checked-out files belong to this invocation alone.
|
|
38
38
|
audit_index_snapshot() {
|
|
39
39
|
[ "$AUDIT_MODE" = --staged ] || return 0
|
|
40
|
-
local gitdir index
|
|
40
|
+
local gitdir index changed=0
|
|
41
41
|
AUDIT_SNAPSHOT_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/orly-index.XXXXXX")" || return 1
|
|
42
42
|
trap 'rm -rf -- "$AUDIT_SNAPSHOT_ROOT"' EXIT
|
|
43
43
|
trap 'exit 130' INT
|
|
@@ -46,6 +46,14 @@ audit_index_snapshot() {
|
|
|
46
46
|
index="$(git rev-parse --git-path index)" || exit 1
|
|
47
47
|
cp "$index" "$AUDIT_SNAPSHOT_ROOT/index" || exit 1
|
|
48
48
|
export GIT_DIR="$gitdir" GIT_INDEX_FILE="$AUDIT_SNAPSHOT_ROOT/index" GIT_OPTIONAL_LOCKS=0
|
|
49
|
+
if [ "$#" -gt 0 ]; then
|
|
50
|
+
git diff --cached --quiet --diff-filter=ACMRT -- "$@" || changed=$?
|
|
51
|
+
case "$changed" in
|
|
52
|
+
0) return 0 ;;
|
|
53
|
+
1) ;;
|
|
54
|
+
*) printf 'cannot select staged audit files (git exit %s)\n' "$changed" >&2; exit 2 ;;
|
|
55
|
+
esac
|
|
56
|
+
fi
|
|
49
57
|
export GIT_WORK_TREE="$AUDIT_SNAPSHOT_ROOT/tree"
|
|
50
58
|
mkdir "$GIT_WORK_TREE" || exit 1
|
|
51
59
|
git checkout-index --all --prefix="$GIT_WORK_TREE/" || exit 1
|
package/audits/sql-mod.sh
CHANGED
package/audits/ufs.sh
CHANGED
|
@@ -55,7 +55,7 @@ set -euo pipefail
|
|
|
55
55
|
|
|
56
56
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
|
|
57
57
|
audit_scope_init --all "$@"
|
|
58
|
-
audit_index_snapshot
|
|
58
|
+
audit_index_snapshot '*.zig' '*.ts' '*.tsx' '*.js' '*.jsx' '*.rs' '*.go'
|
|
59
59
|
|
|
60
60
|
# Default: full-codebase scan (`--all` is an explicit alias). `--staged` is the
|
|
61
61
|
# pre-commit lens — it narrows the per-file checks to `git diff --cached`. The
|
package/dispatch/lib.sh
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
Staged audits avoid copying repository files when their filename scope is empty.
|
|
6
|
+
|
|
7
|
+
- **Audit preflight** — selects filenames from a copied index before materialising repository files; relevant audits retain coherent staged reads.
|
|
8
|
+
- **Check diagnostics** — prints hosted job or pipeline states and links before reporting pending or failed checks.
|
|
9
|
+
- **Release status** — corrects documentation that still described published 0.12.0 as an unpublished candidate.
|
|
10
|
+
|
|
11
|
+
## 0.12.0 — 2026-10-07
|
|
12
|
+
|
|
13
|
+
Setup guidance now distinguishes the published package from the local release candidate.
|
|
14
|
+
|
|
15
|
+
- **Migration guidance** — names `.orly/` destinations and explains preserving repository-owned hooks with `--no-hooks`.
|
|
16
|
+
- **Release scope** — retains the six existing judgment questions and records the comparison evaluator as parked for proposed 0.13 work.
|
|
17
|
+
|
|
18
|
+
The `.orly/` layout and judgment commands already shipped in 0.11.0; this entry claims no new runtime behavior.
|
package/docs/JUDGMENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: reference
|
|
3
3
|
audience: contributor
|
|
4
|
-
verified: 2026-10-
|
|
5
|
-
product_version: 0.
|
|
4
|
+
verified: 2026-10-07
|
|
5
|
+
product_version: 0.12.0
|
|
6
6
|
executable: false
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,6 +10,9 @@ executable: false
|
|
|
10
10
|
|
|
11
11
|
## Synopsis
|
|
12
12
|
|
|
13
|
+
The 0.12.0 candidate retains the six questions shipped in 0.11.0.
|
|
14
|
+
The comparison evaluator is parked for proposed 0.13 work; this release makes no new model-quality claim.
|
|
15
|
+
|
|
13
16
|
`orly judge` asks one bounded question about evidence you select.
|
|
14
17
|
It accompanies planning, verification, review and documentation.
|
|
15
18
|
Its answer suggests an inspection or a fixed next action.
|
|
@@ -84,6 +84,13 @@ Every criterion is mechanical — it reads an exit code or a file. Claims that
|
|
|
84
84
|
cannot be proven that way stay prose and are graded by the spec's rubric; they
|
|
85
85
|
never become fake criteria.
|
|
86
86
|
|
|
87
|
+
Filename-scoped staged audits copy Git's index before selecting relevant paths.
|
|
88
|
+
An empty selection skips the repository checkout; matching paths still trigger
|
|
89
|
+
a private checkout, dependency reads and indexed-link checks. Each audit owns
|
|
90
|
+
its temporary files and cleanup. Audits with unconditional registry checks
|
|
91
|
+
retain a complete checkout. Separate relevant audits retain separate snapshots;
|
|
92
|
+
this optimization does not introduce a shared snapshot cache.
|
|
93
|
+
|
|
87
94
|
Four behaviours worth knowing:
|
|
88
95
|
|
|
89
96
|
- **No spec, no problem — but closing is not escaping.** Spec criteria skip
|
package/llms.txt
CHANGED
|
@@ -12,9 +12,13 @@ Package: `@agentsfleet/orly` (npm)
|
|
|
12
12
|
Source: https://github.com/agentsfleet/orly
|
|
13
13
|
License: Massachusetts Institute of Technology (MIT)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
0.12.0 shipped migration verification; the runtime commands and `.orly/` layout
|
|
16
|
+
originally shipped in 0.11.0. The 0.12.1 patch avoids irrelevant staged audit
|
|
17
|
+
snapshots and preserves hosted-check diagnostics. The `bunx` commands below
|
|
18
|
+
fetch the published package; available versions are listed at
|
|
19
|
+
https://github.com/agentsfleet/orly/releases. Use an installed local tarball for candidate checks.
|
|
20
|
+
The comparison evaluator is parked for proposed 0.13 work; do not claim measured
|
|
21
|
+
judgment improvement or enable new questions from its unfinished results.
|
|
18
22
|
|
|
19
23
|
---
|
|
20
24
|
|
|
@@ -79,6 +83,13 @@ suites. `init` reports missing configuration; `doctor` rejects it.
|
|
|
79
83
|
|
|
80
84
|
---
|
|
81
85
|
|
|
86
|
+
For a 0.10.x consumer with repository-owned hooks, run `orly update --no-hooks`
|
|
87
|
+
using the intended installed version. Change repository callers from
|
|
88
|
+
`.oracle/orly.json` to `.orly/orly.json` and shared rule/audit paths to `.orly/`.
|
|
89
|
+
Keep repository-owned scripts and documents in place. Run `orly doctor` and the
|
|
90
|
+
declared checks before accepting the migration; doctor alone does not run checks.
|
|
91
|
+
Empty old directories may remain, but contain no tracked managed files.
|
|
92
|
+
|
|
82
93
|
## 4. Read order before you edit anything
|
|
83
94
|
|
|
84
95
|
1. `AGENTS.md` — the user's own rules. **These win on any conflict.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -93,8 +93,17 @@ Each cycle, after the review-thread walk, poll CI and act by cause:
|
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
95
|
set -euo pipefail
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
CHECK_STATUS=0
|
|
97
|
+
CHECKS=$(gh pr checks "$PR_NUMBER" --json name,state,bucket,link) || CHECK_STATUS=$?
|
|
98
|
+
printf '%s' "$CHECKS" | jq -e 'type == "array" and length > 0 and all(.[];
|
|
99
|
+
(.name | type == "string") and (.state | type == "string") and
|
|
100
|
+
(.link | type == "string") and (.bucket | IN("pass", "fail", "pending", "skipping", "cancel")))' >/dev/null \
|
|
101
|
+
|| { printf 'BABYSIT: invalid check response (exit %s); reset quiet polls\n%s\n' "$CHECK_STATUS" "$CHECKS" >&2; exit 1; }
|
|
102
|
+
printf '%s' "$CHECKS" | jq -r '.[] | "\(.name) | \(.state) | \(.bucket) | \(.link)"'
|
|
103
|
+
case "$CHECK_STATUS" in
|
|
104
|
+
0|1|8) ;; # GitHub reports failed and pending checks with nonzero statuses.
|
|
105
|
+
*) printf 'BABYSIT: check fetch incomplete (exit %s); reset quiet polls\n' "$CHECK_STATUS" >&2; exit 1 ;;
|
|
106
|
+
esac
|
|
98
107
|
printf '%s' "$CHECKS" | jq -e 'type == "array" and length > 0 and all(.[]; .bucket == "pass" or .bucket == "skipping")' >/dev/null \
|
|
99
108
|
|| { echo "BABYSIT: checks empty or not green; reset quiet polls" >&2; exit 1; }
|
|
100
109
|
CHECK_HEAD=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
|
|
@@ -107,8 +116,14 @@ echo "BABYSIT: ci=green revision=$CHECK_HEAD"
|
|
|
107
116
|
|
|
108
117
|
```bash
|
|
109
118
|
set -euo pipefail
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
PIPELINE_STATUS=0
|
|
120
|
+
PIPELINES=$(glab api "projects/$PROJ_ENC/merge_requests/$MR_IID/pipelines" --paginate | jq -sc 'add') || PIPELINE_STATUS=$?
|
|
121
|
+
printf '%s' "$PIPELINES" | jq -e 'type == "array" and length > 0 and all(.[];
|
|
122
|
+
(.id | type == "number") and (.sha | type == "string") and (.status | type == "string"))' >/dev/null \
|
|
123
|
+
|| { printf 'BABYSIT: invalid pipeline response (exit %s); reset quiet polls\n%s\n' "$PIPELINE_STATUS" "$PIPELINES" >&2; exit 1; }
|
|
124
|
+
printf '%s' "$PIPELINES" | jq -r '.[] | "pipeline \(.id) | \(.status) | \(.web_url // "URL not reported")"'
|
|
125
|
+
[ "$PIPELINE_STATUS" -eq 0 ] \
|
|
126
|
+
|| { printf 'BABYSIT: pipeline fetch incomplete (exit %s); reset quiet polls\n' "$PIPELINE_STATUS" >&2; exit 1; }
|
|
112
127
|
printf '%s' "$PIPELINES" | jq -e --arg revision "$POLL_HEAD" \
|
|
113
128
|
'[.[] | select(.sha == $revision)] | sort_by(.id) | last | .status == "success"' >/dev/null \
|
|
114
129
|
|| { echo "BABYSIT: current revision has no successful pipeline; reset quiet polls" >&2; exit 1; }
|
package/src/review_poll.test.ts
CHANGED
|
@@ -30,7 +30,11 @@ else if(args[0]==="pr" && args[1]==="view") {
|
|
|
30
30
|
else { const prior=(await Bun.file(process.env.POLL_CALLS).text()).split("\\n").filter(x=>x.includes("headRefOid")).length; console.log((process.env.POLL_MODE==="changed" && prior>1) || (process.env.POLL_MODE==="changed-check" && prior>2) ? "changed-revision" : process.env.POLL_HEAD); }
|
|
31
31
|
} else if(args[0]==="pr" && args[1]==="checks") {
|
|
32
32
|
if(process.env.POLL_STATE==="fetch-failed"){console.error("temporary fetch refusal");process.exit(2);}
|
|
33
|
-
|
|
33
|
+
const state=process.env.POLL_STATE;
|
|
34
|
+
const bucket=state==="failed" ? "fail" : state==="pending" ? "pending" : "pass";
|
|
35
|
+
console.log(JSON.stringify(state==="empty" ? [] : state==="malformed-check" ? [{name:"tests"}] : [{name:"tests",state:bucket==="pass" ? "SUCCESS" : bucket==="fail" ? "FAILURE" : "IN_PROGRESS",bucket,link:"https://example.invalid/check"}]));
|
|
36
|
+
if(state==="failed") process.exit(1);
|
|
37
|
+
if(state==="pending") process.exit(8);
|
|
34
38
|
} else if(args[0]==="api") {
|
|
35
39
|
if(process.env.POLL_MODE==="fetch-failed" && args[1].includes("/issues/")){console.error("temporary summary refusal");process.exit(2);}
|
|
36
40
|
const revision=process.env.POLL_MODE==="stale" ? "old-revision" : process.env.POLL_HEAD;
|
|
@@ -75,7 +79,7 @@ for (const mode of ["fetch-failed", "malformed", "changed", "changed-check", "st
|
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
for (const state of ["empty", "fetch-failed", "failed"]) {
|
|
82
|
+
for (const state of ["empty", "fetch-failed", "failed", "pending", "malformed-check"]) {
|
|
79
83
|
test(`${state} check discovery stays incomplete`, () => {
|
|
80
84
|
const result = poll("complete", state);
|
|
81
85
|
expect(result.code).not.toBe(0);
|
|
@@ -84,6 +88,17 @@ for (const state of ["empty", "fetch-failed", "failed"]) {
|
|
|
84
88
|
});
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
for (const state of ["failed", "pending"]) {
|
|
92
|
+
test(`${state} GitHub checks retain job details despite nonzero status`, () => {
|
|
93
|
+
const result = poll("complete", state);
|
|
94
|
+
expect(result.code).not.toBe(0);
|
|
95
|
+
expect(result.output).toContain("tests |");
|
|
96
|
+
expect(result.output).toContain("https://example.invalid/check");
|
|
97
|
+
expect(result.output).toContain(state === "failed" ? "FAILURE | fail" : "IN_PROGRESS | pending");
|
|
98
|
+
expect(result.output).not.toContain("check fetch incomplete");
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
function gitlabPoll(mode = "complete", state = "green") {
|
|
88
103
|
const root = newRepository();
|
|
89
104
|
const tools = join(root, "tools");
|
|
@@ -105,7 +120,7 @@ else if(args[0]==="mr") {
|
|
|
105
120
|
if(process.env.POLL_MODE==="fetch-failed" && endpoint.endsWith("/notes")){console.error("temporary summary refusal");process.exit(2);}
|
|
106
121
|
if(endpoint.endsWith("/pipelines")) {
|
|
107
122
|
if(process.env.POLL_STATE==="fetch-failed"){console.error("temporary pipeline refusal");process.exit(2);}
|
|
108
|
-
console.log(JSON.stringify(process.env.POLL_STATE==="empty" ? [] : [{id:3,sha:process.env.POLL_HEAD,status:process.env.POLL_STATE==="failed" ? "failed" : "success"}]));
|
|
123
|
+
console.log(JSON.stringify(process.env.POLL_STATE==="empty" ? [] : [{id:3,sha:process.env.POLL_HEAD,status:process.env.POLL_STATE==="failed" ? "failed" : process.env.POLL_STATE==="pending" ? "running" : "success",web_url:"https://example.invalid/pipeline"}]));
|
|
109
124
|
} else if(process.env.POLL_MODE==="malformed") console.log(JSON.stringify({message:"not review evidence"}));
|
|
110
125
|
else {
|
|
111
126
|
const note=id=>({id,author:{username:"greptile"},body:id===1 ? "first-page finding" : "second-page finding",type:null});
|
|
@@ -148,7 +163,7 @@ for (const mode of ["fetch-failed", "malformed", "changed", "changed-check"]) {
|
|
|
148
163
|
});
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
for (const state of ["empty", "fetch-failed", "failed"]) {
|
|
166
|
+
for (const state of ["empty", "fetch-failed", "failed", "pending"]) {
|
|
152
167
|
test(`GitLab ${state} pipeline discovery stays incomplete`, () => {
|
|
153
168
|
const result = gitlabPoll("complete", state);
|
|
154
169
|
expect(result.code).not.toBe(0);
|
|
@@ -156,3 +171,12 @@ for (const state of ["empty", "fetch-failed", "failed"]) {
|
|
|
156
171
|
expect(result.output).not.toContain("ci=green");
|
|
157
172
|
});
|
|
158
173
|
}
|
|
174
|
+
|
|
175
|
+
for (const state of ["failed", "pending"]) {
|
|
176
|
+
test(`GitLab ${state} pipeline retains its state and link`, () => {
|
|
177
|
+
const result = gitlabPoll("complete", state);
|
|
178
|
+
expect(result.code).not.toBe(0);
|
|
179
|
+
expect(result.output).toContain(`pipeline 3 | ${state === "failed" ? "failed" : "running"}`);
|
|
180
|
+
expect(result.output).toContain("https://example.invalid/pipeline");
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { cleanupTemporaryDirectories, git, newRepository, ROOT, temporaryDirectory } from "./gates_test_support";
|
|
6
|
+
import { UNSCOPED_ENVIRONMENT } from "./git_env";
|
|
7
|
+
|
|
8
|
+
const SCOPED_AUDITS = ["ufs.sh", "logging.sh", "rust-error.sh", "deinit-pairs.sh", "sql-mod.sh", "design-tokens.sh"];
|
|
9
|
+
const SOURCE = "lib/odd 'quoted.ts";
|
|
10
|
+
const CLEAN = "export const MESSAGE = 'staged';\n";
|
|
11
|
+
const BAD = 'export function first() { return "duplicate value"; }\nexport function second() { return "duplicate value"; }\n';
|
|
12
|
+
const CHECKOUT = "checkout-index";
|
|
13
|
+
const TIMEOUT_MS = 30_000;
|
|
14
|
+
const TOOL_MODE = 0o755;
|
|
15
|
+
|
|
16
|
+
afterEach(cleanupTemporaryDirectories);
|
|
17
|
+
|
|
18
|
+
async function runScan(project: string, leaf: string, failPreflight = false) {
|
|
19
|
+
const tools = temporaryDirectory();
|
|
20
|
+
const scratch = temporaryDirectory();
|
|
21
|
+
const calls = join(tools, "calls.jsonl");
|
|
22
|
+
const realGit = Bun.which("git")!;
|
|
23
|
+
mkdirSync(join(tools, "bin"));
|
|
24
|
+
writeFileSync(join(tools, "bin/git"), `#!${process.execPath}
|
|
25
|
+
const args = Bun.argv.slice(2);
|
|
26
|
+
const file = Bun.file(process.env.SNAPSHOT_CALLS);
|
|
27
|
+
await Bun.write(file, (await file.exists() ? await file.text() : "") + JSON.stringify(args) + "\\n");
|
|
28
|
+
if (process.env.FAIL_PREFLIGHT === "1" && args.includes("--quiet")) process.exit(2);
|
|
29
|
+
const result = Bun.spawnSync([process.env.REAL_GIT, ...args], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
30
|
+
process.exit(result.exitCode);
|
|
31
|
+
`, { mode: TOOL_MODE });
|
|
32
|
+
const result = Bun.spawnSync(["/bin/bash", join(ROOT, "audits", leaf), "--staged"], {
|
|
33
|
+
cwd: project, env: { ...UNSCOPED_ENVIRONMENT, PATH: `${join(tools, "bin")}:${process.env.PATH}`, TMPDIR: scratch,
|
|
34
|
+
SNAPSHOT_CALLS: calls, REAL_GIT: realGit, FAIL_PREFLIGHT: failPreflight ? "1" : "0" },
|
|
35
|
+
stdout: "pipe", stderr: "pipe", timeout: TIMEOUT_MS,
|
|
36
|
+
});
|
|
37
|
+
const commands: string[][] = (await Bun.file(calls).text()).trim().split("\n").map((line) => JSON.parse(line));
|
|
38
|
+
return { code: result.exitCode, output: result.stdout.toString() + result.stderr.toString(),
|
|
39
|
+
checkouts: commands.filter((args) => args[0] === CHECKOUT).length, leftovers: readdirSync(scratch) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test.each(SCOPED_AUDITS)("%s skips repository checkout for a documentation-only index", async (leaf) => {
|
|
43
|
+
const project = newRepository();
|
|
44
|
+
await Bun.write(join(project, "README.md"), "Changed documentation.\n");
|
|
45
|
+
git(project, "add", "README.md");
|
|
46
|
+
const result = await runScan(project, leaf);
|
|
47
|
+
expect(result.code, result.output).toBe(0);
|
|
48
|
+
expect(result.checkouts).toBe(0);
|
|
49
|
+
expect(result.leftovers).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test.each([true, false])("relevant source snapshot checks staged bytes with violation %s", async (badIndex) => {
|
|
53
|
+
const project = newRepository();
|
|
54
|
+
await Bun.write(join(project, SOURCE), badIndex ? BAD : CLEAN);
|
|
55
|
+
git(project, "add", SOURCE);
|
|
56
|
+
await Bun.write(join(project, SOURCE), badIndex ? CLEAN : BAD);
|
|
57
|
+
const result = await runScan(project, "ufs.sh");
|
|
58
|
+
expect(result.code, result.output).toBe(badIndex ? 1 : 0);
|
|
59
|
+
expect(result.checkouts).toBe(1);
|
|
60
|
+
expect(result.leftovers).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a failed staged filename query refuses instead of reporting an empty scope", async () => {
|
|
64
|
+
const project = newRepository();
|
|
65
|
+
const result = await runScan(project, "ufs.sh", true);
|
|
66
|
+
expect(result.code).toBe(2);
|
|
67
|
+
expect(result.output).toContain("cannot select staged audit files");
|
|
68
|
+
expect(result.checkouts).toBe(0);
|
|
69
|
+
expect(result.leftovers).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a deleted source path needs no repository checkout", async () => {
|
|
73
|
+
const project = newRepository();
|
|
74
|
+
await Bun.write(join(project, SOURCE), CLEAN);
|
|
75
|
+
git(project, "add", SOURCE);
|
|
76
|
+
git(project, "commit", "-qm", "test: seed source");
|
|
77
|
+
git(project, "rm", SOURCE);
|
|
78
|
+
const result = await runScan(project, "ufs.sh");
|
|
79
|
+
expect(result.code, result.output).toBe(0);
|
|
80
|
+
expect(result.checkouts).toBe(0);
|
|
81
|
+
expect(result.leftovers).toEqual([]);
|
|
82
|
+
});
|