ace-git 0.30.0 → 0.31.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/docs/handbook.md +5 -0
- data/docs/usage.md +88 -18
- data/handbook/guides/version-control-system-git.g.md +10 -9
- data/handbook/skills/as-git-pr-create/SKILL.md +2 -2
- data/handbook/skills/as-git-pr-update/SKILL.md +2 -2
- data/handbook/workflow-instructions/git/pr/create.wf.md +16 -44
- data/handbook/workflow-instructions/git/pr/update.wf.md +29 -45
- data/lib/ace/git/cli/commands/pr.rb +32 -0
- data/lib/ace/git/cli.rb +3 -1
- data/lib/ace/git/organisms/pull_request_lifecycle.rb +17 -2
- data/lib/ace/git/providers/base.rb +10 -0
- data/lib/ace/git/providers/evidence.rb +7 -0
- data/lib/ace/git/providers/formal_approval.rb +217 -0
- data/lib/ace/git/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83d5b624eab98d754e4bb770fdd1a80e00237d063bc07ba2b412e31cf0b4fa87
|
|
4
|
+
data.tar.gz: 1e8a556aedb3f9ba844ebe46a7b3c64adea58b979842bf6df7835742057c20c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c33109eb792dea197dc9e29a9f63839ed10d53c95aa7e1817d22cbbe2cbecc51123f8fe90ad420a2e908b35a6aa1bbaef35612340bcd667ad21b5736dbdad49
|
|
7
|
+
data.tar.gz: a2b337d9084ff29b01cd0834e2e9e9a3ef36299d0c0704bdad61480d844cd1a0165f1d4b9c482c5acf0feb29d0dc6aaa715e992dca1e2fa50d6abeb736e234f7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.31.0] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Formal reviewer-owned, exact-head provider approval and read-only approval verification through the forge-neutral review flow; comments do not substitute for approval.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Serialize all local review correlations per normalized PR, validate selected/relevant review actors without rejecting unrelated historical comments, and preserve unsafe pending reviews with explicit refusal.
|
|
20
|
+
|
|
21
|
+
## [0.30.1] - 2026-10-17
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Document the ordinary local PR create/update/ready path; remove the protected worker boundary and `ace-lab service request/status` requirements from the git PR workflows and guide.
|
|
26
|
+
- Document explicit remote read/compare reconciliation for an interrupted PR update instead of claiming the command compares and accepts the remote description; an interrupted create adopts one exact-head draft but does not compare or rewrite remote title/body.
|
|
27
|
+
- Drop the `attempt-bound`/`execution-attempt` wording from the `as-git-pr-create` and `as-git-pr-update` skills; they wrap the same forge-neutral PR workflows.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- Remove the protected merge boundary feature test and its fixture, which exercised the removed protected authority model.
|
|
32
|
+
|
|
10
33
|
## [0.30.0] - 2026-10-08
|
|
11
34
|
|
|
12
35
|
### Added
|
data/docs/handbook.md
CHANGED
|
@@ -9,6 +9,11 @@ ace-docs:
|
|
|
9
9
|
|
|
10
10
|
# ace-git Handbook Catalog
|
|
11
11
|
|
|
12
|
+
> **Planned (L5.5):** role rules, independent review rounds, and the
|
|
13
|
+
> attempt-bound forge delivery workflow are not available yet. The PR
|
|
14
|
+
> create/update skills below are projections of that planned workflow; the
|
|
15
|
+
> ordinary `ace-git pr` CLI commands remain the supported local path.
|
|
16
|
+
|
|
12
17
|
Reference for package-owned handbook resources in `ace-git/handbook/`.
|
|
13
18
|
|
|
14
19
|
## Skills
|
data/docs/usage.md
CHANGED
|
@@ -86,6 +86,7 @@ ace-git diff --raw
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
**Options:**
|
|
89
|
+
|
|
89
90
|
- `--format, -f` - Output format: `diff` (default), `summary`, `grouped-stats`
|
|
90
91
|
- `--since, -s` - Changes since date/duration (e.g., "7d", "1 week ago")
|
|
91
92
|
- `--paths, -p` - Include only these glob patterns
|
|
@@ -118,12 +119,14 @@ ace-git status --with-diff
|
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
**Options:**
|
|
122
|
+
|
|
121
123
|
- `--format, -f` - Output format: `markdown` (default), `json`
|
|
122
124
|
- `--no-pr, -n` - Skip PR metadata and activity lookups (faster)
|
|
123
125
|
- `--commits N` - Number of recent commits to show (default: 3 from config)
|
|
124
126
|
- `--with-diff` - Include PR diff in output
|
|
125
127
|
|
|
126
128
|
**Output includes:**
|
|
129
|
+
|
|
127
130
|
- Current branch name with git status -sb output
|
|
128
131
|
- Remote tracking status (ahead/behind)
|
|
129
132
|
- Detected task pattern (from branch name)
|
|
@@ -153,6 +156,7 @@ ace-git branch --format json
|
|
|
153
156
|
```
|
|
154
157
|
|
|
155
158
|
**Options:**
|
|
159
|
+
|
|
156
160
|
- `--format, -f` - Output format: `text` (default), `json`
|
|
157
161
|
|
|
158
162
|
### `ace-git pr`
|
|
@@ -191,16 +195,24 @@ ace-git pr merge 25 --expected-head SHA --method squash
|
|
|
191
195
|
```
|
|
192
196
|
|
|
193
197
|
**Behavior guarantees:**
|
|
198
|
+
|
|
194
199
|
- Create is idempotent: one exact open base/head match returns the existing
|
|
200
|
+
|
|
195
201
|
PR (`idempotency: existing`); multiple matches are a conflict.
|
|
202
|
+
|
|
196
203
|
- `merge` requires the provider to enforce `--expected-head` atomically;
|
|
204
|
+
|
|
197
205
|
providers without that capability return an unsupported-capability failure
|
|
198
206
|
instead of racing.
|
|
207
|
+
|
|
199
208
|
- A create request whose outcome is unknown (transport failure after send)
|
|
209
|
+
|
|
200
210
|
reports an unknown outcome with the exact identity to reconcile; it never
|
|
201
211
|
retries automatically.
|
|
212
|
+
|
|
202
213
|
- With `--format json`, a failed operation prints the classified error as
|
|
203
|
-
|
|
214
|
+
|
|
215
|
+
JSON on stdout (`{"error": {"category": ..., "message": ...}}` -- e.g.
|
|
204
216
|
`expected_head_conflict`, `unsupported_capability`, `unknown_outcome`)
|
|
205
217
|
before the nonzero exit; text mode explains the same failure on stderr.
|
|
206
218
|
|
|
@@ -256,9 +268,10 @@ diff:
|
|
|
256
268
|
| `grouped_stats.layers` | Array | `["lib","test","handbook"]` | Preferred layer grouping order |
|
|
257
269
|
| `grouped_stats.collapse_above` | Integer | `5` | Collapse markdown groups above this file count |
|
|
258
270
|
| `grouped_stats.show_full_tree` | String | `collapsible` | Tree rendering strategy hint |
|
|
259
|
-
| `grouped_stats.dotfile_groups` | Array | `[".ace-
|
|
271
|
+
| `grouped_stats.dotfile_groups` | Array | `[".ace-tasks",".ace"]` | Dot-directories to prioritize in grouping |
|
|
260
272
|
|
|
261
273
|
**Default exclude_patterns** (from `.ace-defaults/git/config.yml`):
|
|
274
|
+
|
|
262
275
|
- Lock files: `**/*.lock`, `package-lock.json`, `yarn.lock`, `Gemfile.lock`
|
|
263
276
|
- Vendored deps: `vendor/**/*`, `node_modules/**/*`
|
|
264
277
|
- Build artifacts: `coverage/**/*`, `dist/**/*`, `build/**/*`, `.cache/**/*`
|
|
@@ -289,30 +302,24 @@ Note: `test/**/*` and `spec/**/*` are NOT excluded by default - test changes are
|
|
|
289
302
|
|
|
290
303
|
## Provider Packages
|
|
291
304
|
|
|
292
|
-
|
|
293
|
-
are
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
the
|
|
298
|
-
|
|
299
|
-
outcome emits no success and never retries. This command does not authorize a
|
|
300
|
-
operation or replace the protected worker's canonical receipt acceptance.
|
|
301
|
-
Create/update use an exact `{target,delivery,title,body}` input and remain draft;
|
|
302
|
-
ready uses `{target,delivery}`, merge uses `{target,delivery,method}`. Create targets
|
|
303
|
-
the base repository; all other operations target the exact PR URL. The original
|
|
304
|
-
title remains part of the accepted input; the provider owns its draft encoding
|
|
305
|
-
(Forgejo's WIP prefix). Updating a draft preserves that encoding. Each handler
|
|
306
|
-
publishes the fixed `pr-result.txt` evidence only after its observed content,
|
|
307
|
-
identity, head and draft/merge outcome agree.
|
|
305
|
+
> **Planned (L5.5):** the forge-neutral worker/integrator role rules and the
|
|
306
|
+
> attempt-bound PR delivery workflow are not available yet. The old protected
|
|
307
|
+
> `service create|update|ready|merge` receiver handlers are not part of the
|
|
308
|
+
> file-based flow and are not a requirement for opening or reviewing a PR.
|
|
309
|
+
>
|
|
310
|
+
> Use the ordinary `ace-git pr` commands documented above for now; they create
|
|
311
|
+
> or reconcile a PR for the pushed branch.
|
|
308
312
|
|
|
309
313
|
`ace-git` is forge-neutral. It defines the server registry (`Ace::Git::ServerRegistry`),
|
|
310
314
|
the provider contract (`Ace::Git::Providers::Base`), and normalized evidence types;
|
|
311
315
|
provider packages own all forge CLI behavior:
|
|
312
316
|
|
|
313
317
|
- **ace-git-github** - GitHub behavior via the `gh` CLI (PR lookups, diffs, issue
|
|
318
|
+
|
|
314
319
|
tracking operations). Registers under the `github` provider type.
|
|
320
|
+
|
|
315
321
|
- **ace-git-forgejo** - Forgejo behavior via the `fj` CLI. Registers under the
|
|
322
|
+
|
|
316
323
|
`forgejo` provider type.
|
|
317
324
|
|
|
318
325
|
Both providers implement the same issue-tracking contract (issue lookup, comment
|
|
@@ -323,3 +330,66 @@ Configure servers in `.ace/git/config.yml` under `git.servers`, then
|
|
|
323
330
|
resolve them by name, by default, or from a git remote URL. Every failure path is
|
|
324
331
|
classified (missing CLI, unauthenticated, unreachable, malformed output, object
|
|
325
332
|
not found) with no silent fallbacks.
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
### Formal provider approval
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
ace-git pr approve 25 --expected-head FULL_SHA --body-file accepted-review.md --correlation review-123 --server forge --format json
|
|
339
|
+
ace-git pr approval 25 --expected-head FULL_SHA --review-id 99 --server forge --format json
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`approve` publishes an already accepted independent review under the configured
|
|
343
|
+
provider identity; it does not invoke a model or decide the review verdict. The
|
|
344
|
+
UTF-8 report must be nonempty, contain no reserved approval marker, and fit in
|
|
345
|
+
64KiB. The PR must be open, ready and at the full lowercase expected SHA; its
|
|
346
|
+
author cannot approve it. GitHub and Forgejo return a verified effective formal
|
|
347
|
+
approval, including exact repository/PR/head, reviewer ID/login, review ID/URL,
|
|
348
|
+
evidence SHA256, correlation, and `created|existing`. Forgejo additionally checks
|
|
349
|
+
official review status. A comment or `pr ready` is a separate operation.
|
|
350
|
+
|
|
351
|
+
`approval` is read-only and verifies the current effective approval identified by
|
|
352
|
+
the numeric provider review ID. Integrators compare its receipt with the accepted
|
|
353
|
+
reviewer artifact; they do not mint a substitute review. Existing server selection
|
|
354
|
+
and JSON error categories apply. Role instructions confer no provider permission;
|
|
355
|
+
credentials remain with the deployment-configured account.
|
|
356
|
+
|
|
357
|
+
Correlation is scoped to the selected provider/server/repository/PR. Reusing it
|
|
358
|
+
with changed visible evidence conflicts. A same-account local per-PR flock serializes all correlations/worktrees
|
|
359
|
+
for concurrent publication; this is not a distributed uniqueness service. Provider
|
|
360
|
+
visibility may hide another account's pending reviews, and separate hosts do not
|
|
361
|
+
share this lock. No success is inferred for invisible evidence. Save the receipt
|
|
362
|
+
in the existing review/assignment artifacts; there is no new journal.
|
|
363
|
+
|
|
364
|
+
After an unknown direct-create outcome, explicitly resume the same publication
|
|
365
|
+
to reconcile provider state first. Mutations are never automatically retried.
|
|
366
|
+
Changed head, stale/dismissed/nonofficial approval, unavailable evidence or
|
|
367
|
+
unsupported capability withholds success rather than using a comment, another
|
|
368
|
+
identity, or weaker branch protection. Actual provider/account acceptance remains
|
|
369
|
+
a separate deployment proof from deterministic package tests.
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
Formal approval requires a **dedicated automation reviewer identity and one
|
|
373
|
+
writer per actor/repository/PR**, supplied by deployment. All local correlations
|
|
374
|
+
share a private per-PR lock in the system temporary directory, independently of
|
|
375
|
+
worktree/TMPDIR. Another host or manual client sharing that identity violates
|
|
376
|
+
the supported execution contract: neither provider offers a pending-review CAS
|
|
377
|
+
through this path. This precondition must be verified before live use; role
|
|
378
|
+
instructions and topology discovery do not establish it.
|
|
379
|
+
|
|
380
|
+
Both adapters directly create approval with the accepted full commit ID.
|
|
381
|
+
Forgejo 11+ preserves that explicit ID; its pending-submit route instead rebinds
|
|
382
|
+
to current HEAD and is never used. Direct create can reuse pending, so visible
|
|
383
|
+
unrelated pending refuses before mutation. A matching, comment-free own pending
|
|
384
|
+
can resume through Forgejo's verified direct-create path. Pending with comments
|
|
385
|
+
or an unverified safe recovery path (including current GitHub pending recovery)
|
|
386
|
+
is retained with an explicit refusal. Lost direct-create replies reconcile the
|
|
387
|
+
submitted review, without a second automatic POST. No claim of server-atomic
|
|
388
|
+
pending isolation or cross-host mutual exclusion is made.
|
|
389
|
+
|
|
390
|
+
GitHub actor proof uses authenticated GraphQL viewer login/databaseId under the
|
|
391
|
+
same configured PAT, App user or App installation token. REST `/user` is not
|
|
392
|
+
used for installation credentials. Unavailable/unverifiable viewer or integration
|
|
393
|
+
access refusal is a truthful capability/authentication failure before approval,
|
|
394
|
+
never a guessed bot identity, borrowed token or apparent network outage. Live
|
|
395
|
+
App/account/public-review proof remains separate from deterministic fixtures.
|
|
@@ -21,7 +21,7 @@ This project uses a **mono-repo architecture** with all packages at the reposito
|
|
|
21
21
|
|
|
22
22
|
- **ace-*/**: Ruby gems for specific functionality (ace-git, ace-task, ace-docs, etc.)
|
|
23
23
|
- **docs/**: Project documentation and architecture decisions
|
|
24
|
-
- **.ace-
|
|
24
|
+
- **.ace-tasks/**: Task management and release coordination
|
|
25
25
|
- **_legacy/**: Archived content from previous multi-repo structure
|
|
26
26
|
|
|
27
27
|
#### Common Git Commands
|
|
@@ -183,18 +183,19 @@ git remote prune origin
|
|
|
183
183
|
git push -u origin feature/your-feature
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
2. **Create
|
|
186
|
+
2. **Create a draft on the selected forge**:
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
189
|
ace-bundle wfi://git/pr/create
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
-
Use the
|
|
193
|
-
fork/canonical repository and ref provenance. The
|
|
194
|
-
|
|
192
|
+
Use the actual assignment and active attempt when the work belongs to an
|
|
193
|
+
assignment, with explicit fork/canonical repository and ref provenance. The
|
|
194
|
+
forge command is
|
|
195
|
+
`ace-git pr create --server NAME --head HEAD-REF --base BASE-REF --expected-head CANDIDATE-SHA --title TITLE --body-file DESCRIPTION --format json`.
|
|
195
196
|
Preserve the resolved forge and exact candidate SHA. Continue through
|
|
196
|
-
`wfi://git/pr/update` only with
|
|
197
|
-
|
|
197
|
+
`wfi://git/pr/update` only with current exact-head test and independent review
|
|
198
|
+
evidence; CI status remains advisory. Inside ACE use source `bin/ace-*`.
|
|
198
199
|
|
|
199
200
|
3. **Link to relevant issues/tasks**:
|
|
200
201
|
|
|
@@ -225,8 +226,8 @@ git push --force-with-lease origin feature/your-feature
|
|
|
225
226
|
|
|
226
227
|
#### Fork-Based Workflow
|
|
227
228
|
|
|
228
|
-
Provider-specific GitHub clone example for external contributors;
|
|
229
|
-
|
|
229
|
+
Provider-specific GitHub clone example for external contributors; the neutral
|
|
230
|
+
draft/update flow above uses the explicitly selected forge and provenance:
|
|
230
231
|
|
|
231
232
|
```bash
|
|
232
233
|
# Fork repository on GitHub, then:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: as-git-pr-create
|
|
3
|
-
description: Create
|
|
3
|
+
description: Create a forge-neutral pull request
|
|
4
4
|
user-invocable: true
|
|
5
5
|
source: ace-git
|
|
6
6
|
allowed-tools:
|
|
@@ -13,7 +13,7 @@ assign:
|
|
|
13
13
|
- name: create-pr
|
|
14
14
|
description: Create the exact assignment pull request
|
|
15
15
|
produces: [pull-request]
|
|
16
|
-
consumes: [code-changes
|
|
16
|
+
consumes: [code-changes]
|
|
17
17
|
skill:
|
|
18
18
|
kind: workflow
|
|
19
19
|
execution:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: as-git-pr-update
|
|
3
|
-
description: Update
|
|
3
|
+
description: Update a forge-neutral pull request
|
|
4
4
|
user-invocable: true
|
|
5
5
|
source: ace-git
|
|
6
6
|
allowed-tools:
|
|
@@ -13,7 +13,7 @@ assign:
|
|
|
13
13
|
- name: update-pr-desc
|
|
14
14
|
description: Update the exact assignment pull request
|
|
15
15
|
produces: [pull-request]
|
|
16
|
-
consumes: [code-changes
|
|
16
|
+
consumes: [code-changes]
|
|
17
17
|
skill:
|
|
18
18
|
kind: workflow
|
|
19
19
|
execution:
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
doc-type: workflow
|
|
3
|
-
title: Create
|
|
3
|
+
title: Create a draft pull request
|
|
4
4
|
purpose: Create or reconcile a forge-neutral draft for the exact candidate
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Create
|
|
7
|
+
# Create a draft pull request
|
|
8
8
|
|
|
9
9
|
## Goal
|
|
10
10
|
|
|
11
|
-
Create one draft PR on the selected forge and
|
|
11
|
+
Create one draft PR on the selected forge and record its exact source/base identity in the assignment report.
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
15
|
-
A reviewed task
|
|
15
|
+
A reviewed task or explicit implementation scope, a committed candidate, and a pushed source ref. Credentials permit transport only; they grant no integration permission.
|
|
16
16
|
|
|
17
17
|
## Inputs
|
|
18
18
|
|
|
@@ -22,49 +22,21 @@ Use `forge_server` or `forge_default: true`, mutually exclusive. Neither means r
|
|
|
22
22
|
{"forge_server":"forge-lab","pr_provenance":{"mode":"canonical","head_repository_url":"https://forge.example/team/repo","head_ref":"feature","base_repository_url":"https://forge.example/team/repo","base_ref":"main"}}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
## Protected worker boundary
|
|
26
|
-
|
|
27
|
-
The steps below are ordinary managed delivery only. An installed protected
|
|
28
|
-
participant uses the configured receiver and the original canonical authority,
|
|
29
|
-
with no caller-local journal or direct-provider fallback.
|
|
30
|
-
|
|
31
|
-
The authenticated worker may create/update a draft before independent review.
|
|
32
|
-
Save a nonsecret input object with exactly `target`, `delivery`, `title`, `body`;
|
|
33
|
-
`delivery` is the normalized original forge/provenance mapping. `target` has
|
|
34
|
-
`resource` and nullable `artifact_digest`: the selected base repository URL for
|
|
35
|
-
create, the exact recorded PR URL for update. Title/body are bounded UTF-8 text.
|
|
36
|
-
Request the installed receiver with:
|
|
37
|
-
|
|
38
|
-
```sh
|
|
39
|
-
ace-lab service request --project PROJECT --assignment ID --attempt ATTEMPT \
|
|
40
|
-
--mapping MAPPING --scope SCOPE --service SERVICE --operation OPERATION \
|
|
41
|
-
--candidate-head SHA --candidate-generation CANDIDATE-GENERATION \
|
|
42
|
-
--expected-generation AUTHORITY-GENERATION --authorization AUTHORIZATION \
|
|
43
|
-
--request-id REQUEST --input INPUT.json
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Use `create` or `update` as OPERATION; each requires its own exact scoped grant.
|
|
47
|
-
The receiver runs the fixed `ace-git service create` or `service update` handler.
|
|
48
|
-
A claim acknowledges admission, not PR success. Retain its complete selection
|
|
49
|
-
and observe `ace-lab service status` as described in
|
|
50
|
-
`wfi://handbook/perform-delivery`. Lost replies stay uncertain; status does not
|
|
51
|
-
repeat an effect. Only canonical completion supplies the created PR URL.
|
|
52
|
-
|
|
53
|
-
For ready, use the same request with OPERATION `ready` and input containing only
|
|
54
|
-
`target` and `delivery`. Ready and merge require executed checks, accepted
|
|
55
|
-
independent review for the current revision and the matching authorization.
|
|
56
|
-
Local report/reference files cannot replace those canonical proofs. Draft
|
|
57
|
-
creation/update does not grant readiness, merge or publication.
|
|
58
|
-
|
|
59
25
|
## Steps
|
|
60
26
|
|
|
61
27
|
1. Read the task and actual committed diff; prepare a concrete title and description explaining behavior and executed validation. Save the description as a local file.
|
|
62
|
-
2.
|
|
63
|
-
3.
|
|
64
|
-
4.
|
|
65
|
-
|
|
66
|
-
|
|
28
|
+
2. When this work belongs to an assignment, use its actual ID and record the PR identity in the assignment report. The execution boundary supplies actor identity; do not impersonate a reviewer or integrator.
|
|
29
|
+
3. Verify the pushed source ref resolves to the candidate SHA. The pushed ref must equal the candidate.
|
|
30
|
+
4. Create the draft with the configured forge command; drafts are the default:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ace-git pr create --server NAME --head HEAD-REF --head-repo HEAD-REPOSITORY-URL --base BASE-REF --expected-head CANDIDATE-SHA --title TITLE --body-file DESCRIPTION --format json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use the retained default/remote selection instead of `--server NAME` when applicable. In this repository invoke the source `bin/ace-git` entrypoint.
|
|
37
|
+
5. Read the structured result. Verify the retained server/provider/base and head repository/ref, draft PR number/URL, candidate SHA and base head. Record the PR identity and observed candidate head in the assignment report.
|
|
38
|
+
6. On interruption or an unknown create outcome, repeat the same command with the same selectors: the implementation re-reads open PRs and adopts one matching draft at the exact head without a second create. Adoption does not compare or rewrite the remote title/body, so verify the remote content explicitly before relying on it, and use `wfi://git/pr/update` if the description differs. Zero matches remain unresolved; multiple matches conflict. Never bypass an uncertain read with a different provider command.
|
|
67
39
|
|
|
68
40
|
## Success criteria
|
|
69
41
|
|
|
70
|
-
The
|
|
42
|
+
The assignment report records exact identity and the proven draft outcome. A report or shell exit alone proves nothing. Continue with actual tests and independent current-head review; keep the PR draft until those results are accepted. CI is visible and advisory. Publication is a separate authorized operation and is not required for delivery.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
doc-type: workflow
|
|
3
|
-
title: Update
|
|
4
|
-
purpose: Update the exact forge-neutral PR and
|
|
3
|
+
title: Update a pull request
|
|
4
|
+
purpose: Update the exact forge-neutral PR and record current delivery evidence
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Update
|
|
7
|
+
# Update a pull request
|
|
8
8
|
|
|
9
9
|
## Goal
|
|
10
10
|
|
|
@@ -12,51 +12,35 @@ Update the recorded PR description for the current candidate and preserve provid
|
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Protected worker boundary
|
|
18
|
-
|
|
19
|
-
The steps below are ordinary managed delivery only. An installed protected
|
|
20
|
-
participant uses the configured receiver and the original canonical authority,
|
|
21
|
-
with no caller-local journal or direct-provider fallback.
|
|
22
|
-
|
|
23
|
-
The authenticated worker may create/update a draft before independent review.
|
|
24
|
-
Save a nonsecret input object with exactly `target`, `delivery`, `title`, `body`;
|
|
25
|
-
`delivery` is the normalized original forge/provenance mapping. `target` has
|
|
26
|
-
`resource` and nullable `artifact_digest`: the selected base repository URL for
|
|
27
|
-
create, the exact recorded PR URL for update. Title/body are bounded UTF-8 text.
|
|
28
|
-
Request the installed receiver with:
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
ace-lab service request --project PROJECT --assignment ID --attempt ATTEMPT \
|
|
32
|
-
--mapping MAPPING --scope SCOPE --service SERVICE --operation OPERATION \
|
|
33
|
-
--candidate-head SHA --candidate-generation CANDIDATE-GENERATION \
|
|
34
|
-
--expected-generation AUTHORITY-GENERATION --authorization AUTHORIZATION \
|
|
35
|
-
--request-id REQUEST --input INPUT.json
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Use `create` or `update` as OPERATION; each requires its own exact scoped grant.
|
|
39
|
-
The receiver runs the fixed `ace-git service create` or `service update` handler.
|
|
40
|
-
A claim acknowledges admission, not PR success. Retain its complete selection
|
|
41
|
-
and observe `ace-lab service status` as described in
|
|
42
|
-
`wfi://handbook/perform-delivery`. Lost replies stay uncertain; status does not
|
|
43
|
-
repeat an effect. Only canonical completion supplies the created PR URL.
|
|
44
|
-
|
|
45
|
-
For ready, use the same request with OPERATION `ready` and input containing only
|
|
46
|
-
`target` and `delivery`. Ready and merge require executed checks, accepted
|
|
47
|
-
independent review for the current revision and the matching authorization.
|
|
48
|
-
Local report/reference files cannot replace those canonical proofs. Draft
|
|
49
|
-
creation/update does not grant readiness, merge or publication.
|
|
15
|
+
An assignment with a recorded PR, or an explicit local PR scope. Reuse the original forge selection and complete fork/canonical provenance; changed configuration or identity is a conflict. A new committed candidate, including task-only changes, requires renewed tests and independent review.
|
|
50
16
|
|
|
51
17
|
## Steps
|
|
52
18
|
|
|
53
|
-
1. Inspect the committed changes and accepted test/review evidence. Prepare the final description with the concrete behavior, executed test results, independent verdict, and actual limitations. Save it as a local file; do not put credentials or terminal dumps in the
|
|
54
|
-
2.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
19
|
+
1. Inspect the committed changes and accepted test/review evidence. Prepare the final description with the concrete behavior, executed test results, independent verdict, and actual limitations. Save it as a local file; do not put credentials or terminal dumps in the assignment report.
|
|
20
|
+
2. Update the PR with the configured forge command:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
ace-git pr update IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --body-file DESCRIPTION --format json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use the retained default/remote selection instead of `--server NAME` when applicable. In this repository invoke the source `bin/ace-git` entrypoint.
|
|
27
|
+
3. On an interrupted update, do not assume the command reconciles for you: `ace-git pr update` verifies the head and rewrites the title/body, so a retry is a new write. Explicitly read the exact remote title/body (for example `ace-git pr show IDENTIFIER --format json`), compare it with the prepared description; if identical, treat the update as done, otherwise re-run the command. An unreadable or uncertain read stays unresolved. Never blindly resend a mutation.
|
|
28
|
+
4. Mark ready only after tests and independent review pass on the exact head. Record the executed tests and independent review evidence for that head, then run:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
ace-git pr ready IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --format json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Missing, stale, or self-approved evidence cannot authorize readiness. Red CI alone does not veto valid executed tests and review.
|
|
35
|
+
5. Merge is a separate authorized operation for the exact candidate. The authorized executor runs:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
ace-git pr merge IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --method METHOD --format json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
with the retained selection (or `--default-server`, otherwise remote resolution). Both the expected SHA and the authorized squash/merge/rebase method are required; there is no default method. Credentials or caller assertions grant no permission.
|
|
42
|
+
6. An unknown merge remains an unresolved effect. Inspect the authoritative selected remote and the actual executor evidence; never retry blindly. Changed head or scope blocks acceptance. Merge does not authorize publication, deployment or synchronization.
|
|
59
43
|
|
|
60
44
|
## Success criteria
|
|
61
45
|
|
|
62
|
-
The exact remote outcome and
|
|
46
|
+
The exact remote outcome and the locally recorded evidence agree. Real artifacts, current executed tests and an independent verdict gate completion. Preserve unresolved history and distinguish the candidate head from evidence commits.
|
|
@@ -204,6 +204,38 @@ module Ace
|
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
+
class Approve < BaseCommand
|
|
208
|
+
desc "Publish a formal approval under the configured reviewer identity"
|
|
209
|
+
argument :identifier, required: true, desc: "PR number, owner/repo#number, or URL"
|
|
210
|
+
option :expected_head, type: :string, required: true, desc: "Exact accepted head SHA"
|
|
211
|
+
option :body_file, type: :string, required: true, desc: "Accepted review report file"
|
|
212
|
+
option :correlation, type: :string, required: true, desc: "Stable approval publication identity"
|
|
213
|
+
|
|
214
|
+
def call(identifier:, expected_head:, body_file:, correlation:, **options)
|
|
215
|
+
result = lifecycle(options).approve(identifier, expected_head: expected_head,
|
|
216
|
+
body_file: body_file, correlation: correlation)
|
|
217
|
+
render_evidence(options, header: "Formal approval confirmed", **result.to_h)
|
|
218
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
219
|
+
render_failure(options, e)
|
|
220
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
class Approval < BaseCommand
|
|
225
|
+
desc "Read and verify an effective exact-head approval without mutation"
|
|
226
|
+
argument :identifier, required: true, desc: "PR number, owner/repo#number, or URL"
|
|
227
|
+
option :expected_head, type: :string, required: true, desc: "Exact accepted head SHA"
|
|
228
|
+
option :review_id, type: :integer, required: true, desc: "Review ID from the approval receipt"
|
|
229
|
+
|
|
230
|
+
def call(identifier:, expected_head:, review_id:, **options)
|
|
231
|
+
result = lifecycle(options).approval(identifier, expected_head: expected_head, review_id: review_id)
|
|
232
|
+
render_evidence(options, header: "Formal approval verified", **result.to_h)
|
|
233
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
234
|
+
render_failure(options, e)
|
|
235
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
207
239
|
# `ace-git pr merge ID --expected-head SHA --method squash|merge|rebase`
|
|
208
240
|
class Merge < BaseCommand
|
|
209
241
|
desc "Merge with provider-side expected-head enforcement; no default method"
|
data/lib/ace/git/cli.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Ace
|
|
|
53
53
|
["diff", "Show filtered git diff output"],
|
|
54
54
|
["status", "Show repository status and PR context"],
|
|
55
55
|
["branch", "Show current branch information"],
|
|
56
|
-
["pr", "Forge-neutral pull request lifecycle: show, create, update, ready, merge"],
|
|
56
|
+
["pr", "Forge-neutral pull request lifecycle: show, create, update, ready, approve, approval, merge"],
|
|
57
57
|
["service merge", "Fixed receiver-owned exact-head merge envelope"],
|
|
58
58
|
["service create", "Fixed receiver-owned draft PR creation"],
|
|
59
59
|
["service update", "Fixed receiver-owned draft PR update"],
|
|
@@ -74,6 +74,8 @@ module Ace
|
|
|
74
74
|
register "pr create", Commands::Pr::Create.new
|
|
75
75
|
register "pr update", Commands::Pr::Update.new
|
|
76
76
|
register "pr ready", Commands::Pr::Ready.new
|
|
77
|
+
register "pr approve", Commands::Pr::Approve.new
|
|
78
|
+
register "pr approval", Commands::Pr::Approval.new
|
|
77
79
|
register "pr merge", Commands::Pr::Merge.new
|
|
78
80
|
register "service merge", Commands::ServiceMerge.new
|
|
79
81
|
%w[create update ready].each do |operation|
|
|
@@ -142,6 +142,21 @@ module Ace
|
|
|
142
142
|
)
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
+
def approve(identifier, expected_head:, body_file:, correlation:)
|
|
146
|
+
body = load_body(body_file, limit: 64 * 1024 + 1)
|
|
147
|
+
reference = parse_identifier(identifier)
|
|
148
|
+
provider = provider_for(resolve_server_for(reference))
|
|
149
|
+
provider.approve_pull_request(number: reference.number, expected_head: expected_head,
|
|
150
|
+
body: body, correlation: correlation)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def approval(identifier, expected_head:, review_id:)
|
|
154
|
+
reference = parse_identifier(identifier)
|
|
155
|
+
provider = provider_for(resolve_server_for(reference))
|
|
156
|
+
provider.pull_request_approval(number: reference.number, expected_head: expected_head,
|
|
157
|
+
review_id: review_id)
|
|
158
|
+
end
|
|
159
|
+
|
|
145
160
|
def post_review_comment(identifier, expected_head:, body:, correlation:)
|
|
146
161
|
reference = parse_identifier(identifier)
|
|
147
162
|
provider = provider_for(resolve_server_for(reference))
|
|
@@ -344,11 +359,11 @@ module Ace
|
|
|
344
359
|
Ace::Git::Providers.for(server, timeout: @timeout, runner: @runner)
|
|
345
360
|
end
|
|
346
361
|
|
|
347
|
-
def load_body(path)
|
|
362
|
+
def load_body(path, limit: nil)
|
|
348
363
|
raise ArgumentError, "--body-file is required" unless path
|
|
349
364
|
raise ArgumentError, "Body file not found: #{path}" unless File.file?(path)
|
|
350
365
|
|
|
351
|
-
File.read(path)
|
|
366
|
+
File.read(path, limit)
|
|
352
367
|
rescue SystemCallError => e
|
|
353
368
|
raise ArgumentError, "Cannot read body file #{path}: #{e.message}"
|
|
354
369
|
end
|
|
@@ -168,6 +168,16 @@ module Ace
|
|
|
168
168
|
raise NotImplementedError, "Providers must implement #{self.class}#repository_file"
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# Formal approval is distinct from comments. Unsupported adapters refuse;
|
|
172
|
+
# they must never project an approval as a comment or switch identity.
|
|
173
|
+
def approve_pull_request(number:, expected_head:, body:, correlation:)
|
|
174
|
+
raise ProviderUnsupportedCapabilityError, "Formal approval is unsupported by #{self.class}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def pull_request_approval(number:, expected_head:, review_id:)
|
|
178
|
+
raise ProviderUnsupportedCapabilityError, "Formal approval evidence is unsupported by #{self.class}"
|
|
179
|
+
end
|
|
180
|
+
|
|
171
181
|
# Create a top-level PR comment. `correlation` is a stable session key
|
|
172
182
|
# embedded in the body so a retry can find exactly one prior send.
|
|
173
183
|
# A possible post-send transport failure raises ProviderUnknownOutcomeError.
|
|
@@ -48,6 +48,13 @@ module Ace
|
|
|
48
48
|
:idempotency
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
# Effective formal review, verified against the current provider state.
|
|
52
|
+
ProviderApprovalReceipt = Data.define(
|
|
53
|
+
:provider, :server_name, :repository_url, :pr_number, :pr_url, :head_sha,
|
|
54
|
+
:reviewer_id, :reviewer, :review_id, :review_url, :state, :effective, :official,
|
|
55
|
+
:correlation, :evidence_sha256, :idempotency
|
|
56
|
+
)
|
|
57
|
+
|
|
51
58
|
# Normalized repository evidence.
|
|
52
59
|
ProviderRepository = Data.define(:server_name, :full_name, :default_branch, :url)
|
|
53
60
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "etc"
|
|
6
|
+
require_relative "evidence"
|
|
7
|
+
require_relative "../atoms/server_url"
|
|
8
|
+
|
|
9
|
+
module Ace
|
|
10
|
+
module Git
|
|
11
|
+
module Providers
|
|
12
|
+
# Shared policy for formal review evidence. Provider packages supply the
|
|
13
|
+
# authenticated I/O ports; no credentials or delivery state live here.
|
|
14
|
+
class FormalApproval
|
|
15
|
+
SHA = /\A[0-9a-f]{40}\z/
|
|
16
|
+
CORRELATION = /\A[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}\z/
|
|
17
|
+
MARKER = /\n\n<!-- ace-approval:([a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}) sha256:([0-9a-f]{64}) -->\z/
|
|
18
|
+
|
|
19
|
+
def initialize(provider:, request:, list:, official: false, pending_reuse: false)
|
|
20
|
+
@provider, @request, @list, @official = provider, request, list, official
|
|
21
|
+
@pending_reuse = pending_reuse
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def approve(number:, expected_head:, body:, correlation:)
|
|
25
|
+
validate_input!(number, expected_head)
|
|
26
|
+
unless body.is_a?(String) && body.dup.force_encoding(Encoding::UTF_8).valid_encoding? && !body.strip.empty? &&
|
|
27
|
+
body.bytesize <= 64 * 1024 && !body.include?("\0") && !body.include?("<!-- ace-approval:") &&
|
|
28
|
+
correlation.is_a?(String) && correlation.match?(CORRELATION)
|
|
29
|
+
raise ArgumentError, "Approval requires a nonempty UTF-8 report (maximum 64KiB) and valid correlation"
|
|
30
|
+
end
|
|
31
|
+
body = body.dup.force_encoding(Encoding::UTF_8)
|
|
32
|
+
digest = Digest::SHA256.hexdigest(body)
|
|
33
|
+
sent_body = "#{body}\n\n<!-- ace-approval:#{correlation} sha256:#{digest} -->"
|
|
34
|
+
# Same existing per-session flock family as comment publication,
|
|
35
|
+
# with a digest filename so caller text never forms a filesystem path.
|
|
36
|
+
# All correlations for this PR share the lock: a provider may reuse
|
|
37
|
+
# an actor's pending review instead of creating a distinct object.
|
|
38
|
+
scope = [@provider.server.provider, Atoms::ServerUrl.normalize(@provider.server.url), number]
|
|
39
|
+
key = Digest::SHA256.hexdigest(scope.join("\0"))
|
|
40
|
+
File.open(File.join(Etc.systmpdir, "ace-review-session-approval-#{Process.uid}-#{key}.lock"),
|
|
41
|
+
File::CREAT | File::RDWR | File::NOFOLLOW, 0600) do |lock|
|
|
42
|
+
unless lock.stat.file? && lock.stat.uid == Process.uid && (lock.stat.mode & 0777) == 0600
|
|
43
|
+
raise ConfigError, "Unsafe approval session lock"
|
|
44
|
+
end
|
|
45
|
+
lock.flock(File::LOCK_EX)
|
|
46
|
+
pr = current!(number, expected_head)
|
|
47
|
+
actor = identity!(@request.call(:get, :actor, nil))
|
|
48
|
+
deny_author!(pr, actor)
|
|
49
|
+
rows = reviews!(number)
|
|
50
|
+
matches = rows.select { |row| row.fetch("body").include?("<!-- ace-approval:#{correlation} ") }
|
|
51
|
+
raise ProviderConflictingMatchesError, "Ambiguous approval correlation" if matches.length > 1
|
|
52
|
+
existing = matches.first
|
|
53
|
+
row!(existing) if existing
|
|
54
|
+
if existing && (existing["body"] != sent_body || existing["commit_id"] != expected_head ||
|
|
55
|
+
identity!(existing["user"]) != actor)
|
|
56
|
+
raise ProviderConflictingMatchesError, "Approval correlation is already bound to different evidence"
|
|
57
|
+
end
|
|
58
|
+
# A provider may reuse its current pending object on direct create.
|
|
59
|
+
# Only an exact known, comment-free pending review is resumable, and
|
|
60
|
+
# only when the adapter proves explicit commit preservation on reuse.
|
|
61
|
+
owned_pending = rows.select do |row|
|
|
62
|
+
row["state"] == "PENDING" && row["user"].is_a?(Hash) && row["user"]["id"] == actor.first
|
|
63
|
+
end
|
|
64
|
+
unrelated = owned_pending.any? { |row| !existing || row["id"] != existing["id"] }
|
|
65
|
+
if unrelated
|
|
66
|
+
raise ProviderConflictingMatchesError,
|
|
67
|
+
"Authenticated reviewer has an unrelated pending review; no publication performed"
|
|
68
|
+
end
|
|
69
|
+
recovering = existing && existing["state"] == "PENDING"
|
|
70
|
+
if recovering && (!@pending_reuse || existing["comments_count"] != 0 ||
|
|
71
|
+
existing["dismissed"] || existing["stale"])
|
|
72
|
+
raise ProviderUnsupportedCapabilityError,
|
|
73
|
+
"This pending review cannot be safely completed with its accepted commit/content; pending is preserved"
|
|
74
|
+
end
|
|
75
|
+
mutated = false
|
|
76
|
+
disposition = existing ? :existing : :created
|
|
77
|
+
begin
|
|
78
|
+
current!(number, expected_head)
|
|
79
|
+
if !existing || recovering
|
|
80
|
+
pending_id = existing && existing["id"]
|
|
81
|
+
mutated = true
|
|
82
|
+
# Direct approval keeps the explicit reviewed commit. Never
|
|
83
|
+
# use a later pending-submit endpoint with implicit current HEAD.
|
|
84
|
+
existing = @request.call(:post, "pulls/#{number}/reviews",
|
|
85
|
+
{"body" => sent_body, "commit_id" => expected_head, "event" => "APPROVE"})
|
|
86
|
+
row!(existing)
|
|
87
|
+
if pending_id && existing["id"] != pending_id
|
|
88
|
+
raise ProviderUnknownOutcomeError, "Pending review identity changed during direct approval"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
verifying = true
|
|
92
|
+
receipt(number: number, expected_head: expected_head, review_id: existing.fetch("id"),
|
|
93
|
+
disposition: disposition, expected_body: sent_body, actor: actor)
|
|
94
|
+
rescue Error => error
|
|
95
|
+
definitive_refusal = error.is_a?(ProviderAuthenticationError) ||
|
|
96
|
+
error.is_a?(ProviderUnsupportedCapabilityError)
|
|
97
|
+
raise unless mutated && (verifying || !definitive_refusal)
|
|
98
|
+
|
|
99
|
+
raise ProviderUnknownOutcomeError,
|
|
100
|
+
"Approval outcome unknown for #{@provider.server.name}/#{number}, head #{expected_head}, " \
|
|
101
|
+
"correlation #{correlation}: #{error.class.name.split('::').last}; reconcile before repeating"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
rescue SystemCallError
|
|
105
|
+
raise ConfigError, "Cannot acquire the private approval session lock"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def read(number:, expected_head:, review_id:)
|
|
109
|
+
validate_input!(number, expected_head)
|
|
110
|
+
unless review_id.is_a?(Integer) && review_id.positive?
|
|
111
|
+
raise ArgumentError, "Approval review ID must be a positive integer"
|
|
112
|
+
end
|
|
113
|
+
receipt(number: number, expected_head: expected_head, review_id: review_id, disposition: :existing)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def validate_input!(number, head)
|
|
119
|
+
unless number.is_a?(Integer) && number.positive? && head.is_a?(String) && head.match?(SHA)
|
|
120
|
+
raise ArgumentError, "Approval requires a positive PR number and full lowercase head SHA"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def current!(number, head)
|
|
125
|
+
pr = @provider.pull_request(number: number)
|
|
126
|
+
unless pr.number == number && pr.server_name == @provider.server.name &&
|
|
127
|
+
pr.head_sha == head && pr.state == :open && pr.draft == false &&
|
|
128
|
+
Atoms::ServerUrl.match?(pr.base_repository_url, @provider.server.url)
|
|
129
|
+
raise ProviderExpectedHeadConflictError,
|
|
130
|
+
"Approval requires the selected open, ready PR at the expected head"
|
|
131
|
+
end
|
|
132
|
+
pr
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def identity!(user)
|
|
136
|
+
unless user.is_a?(Hash) && user["id"].is_a?(Integer) && user["id"].positive? &&
|
|
137
|
+
user["login"].is_a?(String) && !user["login"].empty?
|
|
138
|
+
raise ProviderMalformedOutputError, "Malformed authenticated review identity"
|
|
139
|
+
end
|
|
140
|
+
[user["id"], user["login"]]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def deny_author!(pr, actor)
|
|
144
|
+
unless pr.author.is_a?(String) && !pr.author.empty?
|
|
145
|
+
raise ProviderMalformedOutputError, "PR author is unverified"
|
|
146
|
+
end
|
|
147
|
+
if pr.author.casecmp?(actor.last)
|
|
148
|
+
raise ProviderAuthenticationError, "PR author cannot approve own pull request"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def row!(row)
|
|
153
|
+
unless row.is_a?(Hash) && row["id"].is_a?(Integer) && row["id"].positive? &&
|
|
154
|
+
row["body"].is_a?(String) && row["state"].is_a?(String) &&
|
|
155
|
+
row["commit_id"].is_a?(String) && row["commit_id"].match?(SHA)
|
|
156
|
+
raise ProviderMalformedOutputError, "Malformed formal review evidence"
|
|
157
|
+
end
|
|
158
|
+
identity!(row["user"])
|
|
159
|
+
row
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def reviews!(number)
|
|
163
|
+
rows = @list.call("pulls/#{number}/reviews")
|
|
164
|
+
raise ProviderMalformedOutputError, "Formal reviews are not a list" unless rows.is_a?(Array)
|
|
165
|
+
rows.each do |row|
|
|
166
|
+
unless row.is_a?(Hash) && row["id"].is_a?(Integer) && row["id"].positive? &&
|
|
167
|
+
row["body"].is_a?(String) && row["state"].is_a?(String)
|
|
168
|
+
raise ProviderMalformedOutputError, "Malformed formal review list entry"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
if rows.map { |row| row["id"] }.uniq.length != rows.length
|
|
172
|
+
raise ProviderMalformedOutputError, "Duplicate formal review IDs"
|
|
173
|
+
end
|
|
174
|
+
rows
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def receipt(number:, expected_head:, review_id:, disposition:, expected_body: nil, actor: nil)
|
|
178
|
+
pr = current!(number, expected_head)
|
|
179
|
+
rows = reviews!(number)
|
|
180
|
+
row = rows.find { |entry| entry["id"] == review_id }
|
|
181
|
+
raise ProviderIdentityMismatchError, "Formal approval was not found" unless row
|
|
182
|
+
row!(row)
|
|
183
|
+
actual_actor = identity!(row["user"])
|
|
184
|
+
deny_author!(pr, actual_actor)
|
|
185
|
+
marker = row["body"].match(MARKER)
|
|
186
|
+
content = marker && row["body"][0...marker.begin(0)]
|
|
187
|
+
latest = rows.select do |entry|
|
|
188
|
+
next false unless entry["user"].is_a?(Hash) && entry["user"]["id"] == actual_actor.first
|
|
189
|
+
next false unless %w[APPROVED CHANGES_REQUESTED REQUEST_CHANGES DISMISSED].include?(entry["state"])
|
|
190
|
+
|
|
191
|
+
row!(entry)
|
|
192
|
+
unless identity!(entry["user"]) == actual_actor
|
|
193
|
+
raise ProviderIdentityMismatchError, "Inconsistent identity in relevant formal review history"
|
|
194
|
+
end
|
|
195
|
+
true
|
|
196
|
+
end.max_by { |entry| entry["id"] }
|
|
197
|
+
unless marker && !content.strip.empty? && content.bytesize <= 64 * 1024 &&
|
|
198
|
+
Digest::SHA256.hexdigest(content) == marker[2] &&
|
|
199
|
+
row["state"] == "APPROVED" && row["commit_id"] == expected_head &&
|
|
200
|
+
!row["dismissed"] && !row["stale"] && (!@official || row["official"] == true) &&
|
|
201
|
+
(!latest || latest["id"] == row["id"]) && (!expected_body || row["body"] == expected_body) &&
|
|
202
|
+
(!actor || actor == actual_actor) && row["html_url"].is_a?(String) &&
|
|
203
|
+
row["html_url"].start_with?("#{pr.url}#")
|
|
204
|
+
raise ProviderIdentityMismatchError, "Review is not the effective exact-head formal approval"
|
|
205
|
+
end
|
|
206
|
+
current!(number, expected_head)
|
|
207
|
+
ProviderApprovalReceipt.new(provider: @provider.server.provider, server_name: @provider.server.name,
|
|
208
|
+
repository_url: @provider.server.url, pr_number: number, pr_url: pr.url,
|
|
209
|
+
head_sha: expected_head, reviewer_id: actual_actor.first, reviewer: actual_actor.last,
|
|
210
|
+
review_id: row["id"], review_url: row["html_url"], state: :approved, effective: true,
|
|
211
|
+
official: @official ? row["official"] : nil,
|
|
212
|
+
correlation: marker[1], evidence_sha256: marker[2], idempotency: disposition)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/ace/git/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-git
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-config
|
|
@@ -186,6 +186,7 @@ files:
|
|
|
186
186
|
- lib/ace/git/providers.rb
|
|
187
187
|
- lib/ace/git/providers/base.rb
|
|
188
188
|
- lib/ace/git/providers/evidence.rb
|
|
189
|
+
- lib/ace/git/providers/formal_approval.rb
|
|
189
190
|
- lib/ace/git/resolved_server.rb
|
|
190
191
|
- lib/ace/git/server_registry.rb
|
|
191
192
|
- lib/ace/git/version.rb
|