ace-git 0.28.1 → 0.30.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 +33 -0
- data/README.md +1 -1
- data/docs/handbook.md +4 -4
- data/docs/usage.md +17 -0
- data/handbook/guides/version-control-system-git.g.md +11 -4
- data/handbook/skills/as-git-pr-create/SKILL.md +23 -0
- data/handbook/skills/as-git-pr-update/SKILL.md +23 -0
- data/handbook/workflow-instructions/git/pr/create.wf.md +70 -0
- data/handbook/workflow-instructions/git/pr/update.wf.md +62 -0
- data/lib/ace/git/atoms/delivery_parameters.rb +54 -0
- data/lib/ace/git/atoms/service_merge_evidence.rb +71 -0
- data/lib/ace/git/atoms/service_pr_evidence.rb +76 -0
- data/lib/ace/git/atoms/service_pr_input.rb +56 -0
- data/lib/ace/git/cli/commands/service_merge.rb +29 -0
- data/lib/ace/git/cli.rb +10 -1
- data/lib/ace/git/organisms/pull_request_lifecycle.rb +49 -6
- data/lib/ace/git/organisms/service_merge.rb +214 -0
- data/lib/ace/git/providers/base.rb +6 -0
- data/lib/ace/git/server_registry.rb +8 -6
- data/lib/ace/git/version.rb +1 -1
- metadata +12 -6
- data/handbook/skills/as-github-pr-create/SKILL.md +0 -72
- data/handbook/skills/as-github-pr-update/SKILL.md +0 -41
- data/handbook/workflow-instructions/github/pr/create.wf.md +0 -282
- data/handbook/workflow-instructions/github/pr/update.wf.md +0 -203
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecccf931f808308fde6ab00654e43fdcbe7b90e0f0c929ade43a25a427302756
|
|
4
|
+
data.tar.gz: 03af540433b453863a3d399a67565724c68288c0a4a2b283462f613293a4a6a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c999f3855347dda4bfac0ce490ec94666f1e9e2cf00b2907d0ad72fa5c083e00495e0f4296036770d106bb1decddca02a37b495e71d87552344899a4321cba08
|
|
7
|
+
data.tar.gz: 0decf0ad84220579f16b3606d9c6ee3d6193e0a4dfbee22474cd4ee8008b5a75c6fc4bfc9f77bb8d27ae14fbdf65a3c95c957644ac318e08caffe03f7d0e768c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.30.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Execute protected draft create/update, ready and merge through named forge providers, binding exact repository provenance, candidate SHA, input and current campaign acceptance.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Resolve receiver provider identity from the authenticated PR URL and preserve uncertain effects without replay.
|
|
19
|
+
|
|
20
|
+
- Verify protected request, status and delivery refuse actual authority endpoint loss without replacing the original claim or dispatching another effect.
|
|
21
|
+
|
|
22
|
+
- Restore process-wide provider registrations and loader state after registry test fixtures, preserving real adapter tests across randomized single-batch execution.
|
|
23
|
+
|
|
24
|
+
- Resolve receiver-side merge provider identity from the authenticated PR URL when the prepared candidate has no remote, refusing mismatched or changed pinned servers.
|
|
25
|
+
|
|
26
|
+
- Route protected delivery through original worker merge request, canonical status and exact result consumption while preserving ordinary delivery and unresolved protected PR policy.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Fixed receiver-side `service merge` validates the claimed candidate/input, uses the selected neutral provider once, and publishes bounded evidence only for a verified exact-head merged outcome.
|
|
32
|
+
|
|
33
|
+
## [0.29.0] - 2026-10-05
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- Remove the obsolete GitHub PR workflow and skill sources; canonical PR creation/update use only `git/pr` and `as-git-pr-*`, with no forwarding aliases.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- PR lifecycle selection can use an explicit repository and pinned forge identity; exact create reconciliation is read-only. Neutral PR workflow and skill sources are prepared for coordinated canonical adoption.
|
|
42
|
+
|
|
10
43
|
## [0.28.1] - 2026-10-04
|
|
11
44
|
|
|
12
45
|
### Technical
|
data/README.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
**Rebase with changelog-safe workflow guardrails** - use the `as-git-rebase` agent workflow to run structured rebase flows that preserve package release metadata and reduce manual conflict-prone steps.
|
|
34
34
|
|
|
35
|
-
**Prepare clean review history before publishing** - run the `as-git-reorganize-commits` workflow to reorganize commit stacks, then use `as-
|
|
35
|
+
**Prepare clean review history before publishing** - run the `as-git-reorganize-commits` workflow to reorganize commit stacks, then use `as-git-pr-create` or `as-git-pr-update` to manage PR metadata in a predictable workflow sequence. See [Handbook](docs/handbook.md) for the full skill and workflow catalog.
|
|
36
36
|
|
|
37
37
|
**Coordinate with commit and worktree tools** - pair with [ace-git-commit](../ace-git-commit) for scoped commit authoring, [ace-git-worktree](../ace-git-worktree) for task-oriented worktree management, and [ace-bundle](../ace-bundle) for loading workflow instructions.
|
|
38
38
|
|
data/docs/handbook.md
CHANGED
|
@@ -17,8 +17,8 @@ Reference for package-owned handbook resources in `ace-git/handbook/`.
|
|
|
17
17
|
|-------|--------------|
|
|
18
18
|
| `as-git-rebase` | Guide changelog-preserving rebases |
|
|
19
19
|
| `as-git-reorganize-commits` | Rework commit history into reviewable logical groups |
|
|
20
|
-
| `as-
|
|
21
|
-
| `as-
|
|
20
|
+
| `as-git-pr-create` | Create an attempt-bound draft on the selected forge |
|
|
21
|
+
| `as-git-pr-update` | Update the exact attempt-bound PR and consume current evidence |
|
|
22
22
|
| `as-github-release-publish` | Publish a GitHub release from the ACE workflow |
|
|
23
23
|
|
|
24
24
|
## Workflow Instructions
|
|
@@ -27,8 +27,8 @@ Reference for package-owned handbook resources in `ace-git/handbook/`.
|
|
|
27
27
|
|--------------|---------|------------|
|
|
28
28
|
| `wfi://git/rebase` | Rebase a branch while preserving changelog/version files | `as-git-rebase` |
|
|
29
29
|
| `wfi://git/reorganize-commits` | Consolidate or regroup commit history before review | `as-git-reorganize-commits` |
|
|
30
|
-
| `wfi://
|
|
31
|
-
| `wfi://
|
|
30
|
+
| `wfi://git/pr/create` | Create or reconcile the exact attempt-bound draft | `as-git-pr-create` |
|
|
31
|
+
| `wfi://git/pr/update` | Update the recorded PR and retain delivery identity | `as-git-pr-update` |
|
|
32
32
|
| `wfi://github/release-publish` | Publish a GitHub release | `as-github-release-publish` |
|
|
33
33
|
|
|
34
34
|
## Guides
|
data/docs/usage.md
CHANGED
|
@@ -289,6 +289,23 @@ Note: `test/**/*` and `spec/**/*` are NOT excluded by default - test changes are
|
|
|
289
289
|
|
|
290
290
|
## Provider Packages
|
|
291
291
|
|
|
292
|
+
`ace-git service create`, `service update`, `service ready` and `service merge`
|
|
293
|
+
are fixed protected receiver handlers. It accepts no
|
|
294
|
+
options; the receiver supplies its claimed JSON envelope on stdin and its
|
|
295
|
+
private materialized candidate directory as the working directory. The handler
|
|
296
|
+
checks the exact input digest, executor, candidate and forge provenance, calls
|
|
297
|
+
the selected neutral operation once, and emits the existing bounded evidence
|
|
298
|
+
response only after verifying the exact PR outcome (and merge commit for merge). An unverified or lost
|
|
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.
|
|
308
|
+
|
|
292
309
|
`ace-git` is forge-neutral. It defines the server registry (`Ace::Git::ServerRegistry`),
|
|
293
310
|
the provider contract (`Ace::Git::Providers::Base`), and normalized evidence types;
|
|
294
311
|
provider packages own all forge CLI behavior:
|
|
@@ -183,13 +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 an attempt-bound draft on the selected forge**:
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
|
-
|
|
190
|
-
--body "Description of changes..."
|
|
189
|
+
ace-bundle wfi://git/pr/create
|
|
191
190
|
```
|
|
192
191
|
|
|
192
|
+
Use the managed assignment and active delivery attempt with explicit
|
|
193
|
+
fork/canonical repository and ref provenance. The canonical operation is
|
|
194
|
+
`ace-assign delivery --assignment ID --attempt ATTEMPT --operation create --title TITLE --body-file DESCRIPTION`.
|
|
195
|
+
Preserve the resolved forge and exact candidate SHA. Continue through
|
|
196
|
+
`wfi://git/pr/update` only with accepted current test and independent review
|
|
197
|
+
receipts; CI status remains advisory. Inside ACE use source `bin/ace-*`.
|
|
198
|
+
|
|
193
199
|
3. **Link to relevant issues/tasks**:
|
|
194
200
|
|
|
195
201
|
```markdown
|
|
@@ -219,7 +225,8 @@ git push --force-with-lease origin feature/your-feature
|
|
|
219
225
|
|
|
220
226
|
#### Fork-Based Workflow
|
|
221
227
|
|
|
222
|
-
|
|
228
|
+
Provider-specific GitHub clone example for external contributors; canonical
|
|
229
|
+
assignment delivery above uses the explicitly selected forge and provenance:
|
|
223
230
|
|
|
224
231
|
```bash
|
|
225
232
|
# Fork repository on GitHub, then:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: as-git-pr-create
|
|
3
|
+
description: Create an attempt-bound forge-neutral pull request
|
|
4
|
+
user-invocable: true
|
|
5
|
+
source: ace-git
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- Bash(ace-assign:*)
|
|
8
|
+
- Bash(ace-git:*)
|
|
9
|
+
- Bash(ace-bundle:*)
|
|
10
|
+
- Read
|
|
11
|
+
assign:
|
|
12
|
+
steps:
|
|
13
|
+
- name: create-pr
|
|
14
|
+
description: Create the exact assignment pull request
|
|
15
|
+
produces: [pull-request]
|
|
16
|
+
consumes: [code-changes, execution-attempt]
|
|
17
|
+
skill:
|
|
18
|
+
kind: workflow
|
|
19
|
+
execution:
|
|
20
|
+
workflow: wfi://git/pr/create
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Load and run `ace-bundle wfi://git/pr/create` in the current project, then execute its complete workflow. Use source `bin/ace-*` entrypoints inside the ACE repository.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: as-git-pr-update
|
|
3
|
+
description: Update an attempt-bound forge-neutral pull request
|
|
4
|
+
user-invocable: true
|
|
5
|
+
source: ace-git
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- Bash(ace-assign:*)
|
|
8
|
+
- Bash(ace-git:*)
|
|
9
|
+
- Bash(ace-bundle:*)
|
|
10
|
+
- Read
|
|
11
|
+
assign:
|
|
12
|
+
steps:
|
|
13
|
+
- name: update-pr-desc
|
|
14
|
+
description: Update the exact assignment pull request
|
|
15
|
+
produces: [pull-request]
|
|
16
|
+
consumes: [code-changes, execution-attempt]
|
|
17
|
+
skill:
|
|
18
|
+
kind: workflow
|
|
19
|
+
execution:
|
|
20
|
+
workflow: wfi://git/pr/update
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Load and run `ace-bundle wfi://git/pr/update` in the current project, then execute its complete workflow. Use source `bin/ace-*` entrypoints inside the ACE repository.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Create an attempt-bound draft pull request
|
|
4
|
+
purpose: Create or reconcile a forge-neutral draft for the exact candidate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Create an attempt-bound draft pull request
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Create one draft PR on the selected forge and retain its exact source/base identity in the assignment attempt journal.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
A reviewed task, managed assignment, active delivery attempt, committed candidate, and pushed source ref. Credentials permit transport only; they grant no integration authority.
|
|
16
|
+
|
|
17
|
+
## Inputs
|
|
18
|
+
|
|
19
|
+
Use `forge_server` or `forge_default: true`, mutually exclusive. Neither means resolve this repository's configured remote. Provide `pr_provenance` containing `mode` (`fork` or `canonical`), `head_repository_url`, `head_ref`, `base_repository_url`, and `base_ref`. Canonical repositories must match; fork mode explicitly names its source even when owners coincide. Inputs contain no credentials.
|
|
20
|
+
|
|
21
|
+
```json
|
|
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
|
+
```
|
|
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
|
+
## Steps
|
|
60
|
+
|
|
61
|
+
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. Use the existing managed assignment and active delivery attempt IDs. If needed, start the delivery step with `ace-assign attempt start --assignment ID --step STEP --project PROJECT` (numeric step scope). The execution boundary supplies actor identity; do not impersonate a reviewer or integrator.
|
|
63
|
+
3. Supply the assignment source's `delivery` mapping, or save the explicit input JSON and pass `--parameters FILE`. For task-mode creation, `ace-assign create --task TASK --delivery-parameters FILE` retains these inputs in the generated job.
|
|
64
|
+
4. Execute `ace-assign delivery --assignment ID --attempt ATTEMPT --operation create --title TITLE --body-file DESCRIPTION`. Add `--parameters FILE` only when supplying an explicit file. In this repository invoke the source `bin/ace-assign` entrypoint.
|
|
65
|
+
5. Read the structured result. Verify the retained server/provider/base and head repository/ref, draft PR number/URL, candidate SHA, base_head, evidence_git_ref, and separate journal_commit. The pushed ref must equal the candidate; journal commits never advance it.
|
|
66
|
+
6. On interruption or unknown create outcome, repeat the delivery command to perform exact-selector reconciliation reads. One matching draft at the exact head is adopted without a second create. Zero remains unresolved; multiple matches conflict. Never bypass uncertainty with a direct provider command.
|
|
67
|
+
|
|
68
|
+
## Success criteria
|
|
69
|
+
|
|
70
|
+
The journal records exact identity, intent before the write, and proven draft outcome afterward. A report or shell exit alone proves nothing. Continue with actual tests and independent current-head review; keep the PR draft until those receipts are accepted. CI is visible and advisory. Publication is a separate authorized operation and is not required for delivery.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Update an attempt-bound pull request
|
|
4
|
+
purpose: Update the exact forge-neutral PR and consume current delivery evidence
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Update an attempt-bound pull request
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Update the recorded PR description for the current candidate and preserve provider identity through readiness and integration handoff.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
A managed assignment with a recorded PR and active delivery attempt. 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.
|
|
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.
|
|
50
|
+
|
|
51
|
+
## Steps
|
|
52
|
+
|
|
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 attempt journal.
|
|
54
|
+
2. Execute `ace-assign delivery --assignment ID --attempt ATTEMPT --operation update --body-file DESCRIPTION`. Supply `--parameters FILE` only if the assignment source lacks the original `delivery` mapping. In this repository use `bin/ace-assign`.
|
|
55
|
+
3. On interrupted update, repeat the delivery command. It reads the exact remote title/body and compares the requested field digest before accepting; an unmatched or unreadable outcome remains unresolved. Never blindly resend a mutation.
|
|
56
|
+
4. To mark ready, supply two JSON reference files, each containing only `attempt_id` and `receipt_digest`, from coordinator-accepted current-head test and independent review attempts in this assignment/project. Execute `ace-assign delivery --assignment ID --attempt ATTEMPT --operation ready --tests TEST-REF --review REVIEW-REF`. Missing, fabricated, stale, or self-approved evidence cannot authorize readiness. Red CI alone does not veto valid executed tests and review.
|
|
57
|
+
5. Merge is an integrator/service operation under the existing exact qjx authorization policy. The authorized executor calls `ace-git pr merge IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --method METHOD --format json` with the retained selection (or `--default-server`, otherwise remote resolution). Both expected SHA and the authorized squash/merge/rebase method are required; there is no default method. It does not recursively request its own future receipt. The worker consumes the completed, verified service receipt using `ace-assign delivery --assignment ID --attempt ATTEMPT --operation merge --tests TEST-REF --review REVIEW-REF --service-request REQUEST`. The request must bind this assignment, attempt, project, candidate and PR URL. No completed receipt means a routed blocker; credentials or caller assertions grant no authority.
|
|
58
|
+
6. Unknown merge remains pending service reconciliation and is never automatically retried. Inspect the authoritative selected remote and verified executor evidence. Changed head or scope blocks acceptance. Merge does not authorize publication, deployment or synchronization.
|
|
59
|
+
|
|
60
|
+
## Success criteria
|
|
61
|
+
|
|
62
|
+
The exact remote outcome and existing qjl evidence agree. Real artifacts, current executed tests, independent verdict and required authorization gate completion. Preserve unresolved history and distinct candidate_head/journal_commit. qkb.1 supplies installed charter/proposal/service adoption; this workflow does not fabricate that acceptance.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "server_url"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Git
|
|
6
|
+
module Atoms
|
|
7
|
+
# Explicit non-secret remote delivery inputs; local recipes need not call this validator.
|
|
8
|
+
module DeliveryParameters
|
|
9
|
+
PROVENANCE_FIELDS = %w[mode head_repository_url head_ref base_repository_url base_ref].freeze
|
|
10
|
+
def self.validate(input)
|
|
11
|
+
raise ArgumentError, "Delivery parameters must be a mapping" unless input.is_a?(Hash)
|
|
12
|
+
unless (input.keys - %w[forge_server forge_default pr_provenance]).empty?
|
|
13
|
+
raise ArgumentError, "Unexpected delivery parameter fields"
|
|
14
|
+
end
|
|
15
|
+
default = input.fetch("forge_default", false)
|
|
16
|
+
raise ArgumentError, "forge_default must be boolean" unless [true, false].include?(default)
|
|
17
|
+
server = input["forge_server"]
|
|
18
|
+
if server && (!server.is_a?(String) || !server.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/))
|
|
19
|
+
raise ArgumentError, "Invalid forge_server"
|
|
20
|
+
end
|
|
21
|
+
raise ArgumentError, "forge_server and forge_default are mutually exclusive" if server && default
|
|
22
|
+
provenance = input["pr_provenance"]
|
|
23
|
+
unless provenance.is_a?(Hash) && provenance.keys.sort == PROVENANCE_FIELDS.sort &&
|
|
24
|
+
%w[fork canonical].include?(provenance["mode"])
|
|
25
|
+
raise ArgumentError, "Explicit complete fork or canonical PR provenance is required"
|
|
26
|
+
end
|
|
27
|
+
%w[head_repository_url base_repository_url].each { |key| validate_url!(provenance[key]) }
|
|
28
|
+
%w[head_ref base_ref].each do |key|
|
|
29
|
+
ref = provenance[key]
|
|
30
|
+
unless ref.is_a?(String) && ref.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_\/.\-]{0,255}\z/) &&
|
|
31
|
+
!ref.include?("..") && !ref.include?("//") && !ref.end_with?("/", ".", ".lock")
|
|
32
|
+
raise ArgumentError, "Invalid PR ref"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
if provenance["mode"] == "canonical" && !Ace::Git::Atoms::ServerUrl.match?(
|
|
36
|
+
provenance["head_repository_url"], provenance["base_repository_url"])
|
|
37
|
+
raise ArgumentError, "Canonical provenance requires matching head and base repositories"
|
|
38
|
+
end
|
|
39
|
+
{"forge_server" => server, "forge_default" => default,
|
|
40
|
+
"pr_provenance" => provenance.transform_values(&:dup)}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.validate_url!(url)
|
|
44
|
+
unless url.is_a?(String) && url.length <= 512 &&
|
|
45
|
+
(url.match?(%r{\Ahttps?://[a-zA-Z0-9.:-]+/[a-zA-Z0-9_./-]+\z}) ||
|
|
46
|
+
url.match?(%r{\Agit@[a-zA-Z0-9.-]+:[a-zA-Z0-9_./-]+\z}) ||
|
|
47
|
+
url.match?(%r{\Assh://git@[a-zA-Z0-9.:-]+/[a-zA-Z0-9_./-]+\z}))
|
|
48
|
+
raise ArgumentError, "PR repository URL must contain a repository and no credentials or query"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "delivery_parameters"
|
|
4
|
+
require_relative "pr_reference"
|
|
5
|
+
require_relative "../providers/evidence"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Git
|
|
9
|
+
module Atoms
|
|
10
|
+
# Parse executor-owned evidence; authority supplies the original binding
|
|
11
|
+
# and separately checks the reconstructed input's canonical digest.
|
|
12
|
+
module ServiceMergeEvidence
|
|
13
|
+
MAX_BYTES = 64 * 1024
|
|
14
|
+
def self.validate(bytes, request_id:, input_digest:, target:, head:)
|
|
15
|
+
unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_BYTES) &&
|
|
16
|
+
bytes.dup.force_encoding(Encoding::UTF_8).valid_encoding? && !bytes.include?("\0")
|
|
17
|
+
raise ArgumentError, "merge evidence is invalid or oversized"
|
|
18
|
+
end
|
|
19
|
+
marker, payload = bytes.split("\n", 2)
|
|
20
|
+
unless marker == "ace-service-attestation request:#{request_id} input:#{input_digest} outcome:succeeded"
|
|
21
|
+
raise ArgumentError, "merge evidence original marker differs"
|
|
22
|
+
end
|
|
23
|
+
value = JSON.parse(payload.to_s, max_nesting: 16, create_additions: false,
|
|
24
|
+
allow_duplicate_key: false, allow_comments: false, allow_nan: false)
|
|
25
|
+
closed!(value, %w[server delivery method candidate_head receipt])
|
|
26
|
+
delivery = DeliveryParameters.validate(value.fetch("delivery"))
|
|
27
|
+
unless value["delivery"] == delivery && value["delivery"].keys.sort == %w[forge_default forge_server pr_provenance] &&
|
|
28
|
+
%w[squash merge rebase].include?(value["method"]) && value["candidate_head"] == head
|
|
29
|
+
raise ArgumentError, "merge evidence selection/method/head differs"
|
|
30
|
+
end
|
|
31
|
+
server, receipt = value.values_at("server", "receipt")
|
|
32
|
+
closed!(server, %w[name provider url])
|
|
33
|
+
closed!(receipt, %w[server_name operation pull_request idempotency])
|
|
34
|
+
unless server["name"].is_a?(String) && server["name"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
|
|
35
|
+
server["provider"].is_a?(String) && server["provider"].match?(/\A[a-zA-Z0-9_-]{1,128}\z/) &&
|
|
36
|
+
receipt["server_name"] == server["name"] && receipt["operation"] == "merge"
|
|
37
|
+
raise ArgumentError, "merge evidence provider receipt differs"
|
|
38
|
+
end
|
|
39
|
+
DeliveryParameters.validate_url!(server["url"])
|
|
40
|
+
pr = receipt.fetch("pull_request")
|
|
41
|
+
closed!(pr, ProviderPullRequest.members.map(&:to_s))
|
|
42
|
+
provenance = delivery.fetch("pr_provenance")
|
|
43
|
+
reference = target.is_a?(Hash) && PrReference.parse(target["resource"])
|
|
44
|
+
unless reference && reference.repository_url && pr["number"].is_a?(Integer) && pr["number"].positive? &&
|
|
45
|
+
pr["number"] == reference.number && pr["url"] == target["resource"] && pr["server_name"] == server["name"] &&
|
|
46
|
+
pr["head_sha"] == head && head.is_a?(String) && head.match?(/\A[0-9a-f]{40}\z/) &&
|
|
47
|
+
pr["state"] == "merged" && [true, false].include?(pr["draft"]) &&
|
|
48
|
+
pr.except("number", "draft").values.all? { |value| value.nil? || value.is_a?(String) } &&
|
|
49
|
+
pr["merge_commit_sha"].is_a?(String) && pr["merge_commit_sha"].match?(/\A[0-9a-f]{40}\z/) &&
|
|
50
|
+
pr["head_ref"] == provenance["head_ref"] && pr["base_ref"] == provenance["base_ref"] &&
|
|
51
|
+
ServerUrl.match?(reference.repository_url, provenance["base_repository_url"]) &&
|
|
52
|
+
ServerUrl.match?(server["url"], provenance["base_repository_url"]) &&
|
|
53
|
+
ServerUrl.match?(pr["head_repository_url"], provenance["head_repository_url"]) &&
|
|
54
|
+
ServerUrl.match?(pr["base_repository_url"], provenance["base_repository_url"]) &&
|
|
55
|
+
(!delivery["forge_server"] || delivery["forge_server"] == server["name"])
|
|
56
|
+
raise ArgumentError, "merge evidence PR identity or outcome differs"
|
|
57
|
+
end
|
|
58
|
+
{input: {"target" => target, "delivery" => delivery, "method" => value.fetch("method")},
|
|
59
|
+
pr: pr, server: server}
|
|
60
|
+
rescue JSON::ParserError, KeyError, TypeError
|
|
61
|
+
raise ArgumentError, "merge evidence is malformed"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.closed!(value, fields)
|
|
65
|
+
raise ArgumentError, "merge evidence fields differ" unless value.is_a?(Hash) && value.keys.sort == fields.sort
|
|
66
|
+
end
|
|
67
|
+
private_class_method :closed!
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "service_pr_input"
|
|
4
|
+
require_relative "../providers"
|
|
5
|
+
require_relative "../server_registry"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Git
|
|
9
|
+
module Atoms
|
|
10
|
+
module ServicePrEvidence
|
|
11
|
+
def self.validate(bytes, operation:, request_id:, input_digest:, target:, head:)
|
|
12
|
+
unless ServicePrInput::DRAFT_OPERATIONS.include?(operation) || operation == "ready"
|
|
13
|
+
raise ArgumentError, "unsupported PR evidence operation"
|
|
14
|
+
end
|
|
15
|
+
unless bytes.is_a?(String) && bytes.bytesize.between?(1, 64 * 1024) &&
|
|
16
|
+
bytes.dup.force_encoding(Encoding::UTF_8).valid_encoding? && !bytes.include?("\0")
|
|
17
|
+
raise ArgumentError, "PR evidence is invalid or oversized"
|
|
18
|
+
end
|
|
19
|
+
marker, payload = bytes.split("\n", 2)
|
|
20
|
+
unless marker == "ace-service-attestation request:#{request_id} input:#{input_digest} outcome:succeeded"
|
|
21
|
+
raise ArgumentError, "PR evidence original marker differs"
|
|
22
|
+
end
|
|
23
|
+
value = JSON.parse(payload.to_s, max_nesting: 16, create_additions: false,
|
|
24
|
+
allow_duplicate_key: false, allow_comments: false, allow_nan: false)
|
|
25
|
+
closed!(value, %w[server operation input candidate_head receipt])
|
|
26
|
+
input = ServicePrInput.validate(value.fetch("input"), operation: operation)
|
|
27
|
+
unless value["operation"] == operation && value["candidate_head"] == head && input["target"] == target
|
|
28
|
+
raise ArgumentError, "PR evidence operation/input/head differs"
|
|
29
|
+
end
|
|
30
|
+
server, receipt = value.values_at("server", "receipt")
|
|
31
|
+
closed!(server, %w[name provider url])
|
|
32
|
+
closed!(receipt, %w[server_name operation pull_request idempotency])
|
|
33
|
+
pr = receipt.fetch("pull_request")
|
|
34
|
+
closed!(pr, ProviderPullRequest.members.map(&:to_s))
|
|
35
|
+
delivery = input.fetch("delivery")
|
|
36
|
+
provenance = delivery.fetch("pr_provenance")
|
|
37
|
+
reference = PrReference.parse(pr["url"])
|
|
38
|
+
unless server["name"].is_a?(String) && server["provider"].is_a?(String) && server["url"].is_a?(String)
|
|
39
|
+
raise ArgumentError, "PR evidence provider identity is malformed"
|
|
40
|
+
end
|
|
41
|
+
selected = ResolvedServer.new(name: server["name"], provider: server["provider"], url: server["url"])
|
|
42
|
+
expected_title = Providers.for(selected).class.pull_request_title(title: input["title"], draft: true) if ServicePrInput::DRAFT_OPERATIONS.include?(operation)
|
|
43
|
+
unless reference && reference.repository_url && pr["number"] == reference.number &&
|
|
44
|
+
pr["number"].is_a?(Integer) && pr["number"].positive? &&
|
|
45
|
+
server["name"].is_a?(String) && server["name"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
|
|
46
|
+
server["provider"].is_a?(String) && server["provider"].match?(/\A[a-zA-Z0-9_-]{1,128}\z/) &&
|
|
47
|
+
receipt["server_name"] == server["name"] && receipt["operation"] == operation &&
|
|
48
|
+
pr["server_name"] == server["name"] && pr["head_sha"] == head &&
|
|
49
|
+
head.is_a?(String) && head.match?(/\A[0-9a-f]{40}\z/) && pr["state"] == "open" &&
|
|
50
|
+
pr["draft"] == ServicePrInput::DRAFT_OPERATIONS.include?(operation) &&
|
|
51
|
+
pr.except("number", "draft").values.all? { |item| item.nil? || item.is_a?(String) } &&
|
|
52
|
+
(operation == "create" ? %w[created existing].include?(receipt["idempotency"]) : receipt["idempotency"].nil?) &&
|
|
53
|
+
pr["head_ref"] == provenance["head_ref"] && pr["base_ref"] == provenance["base_ref"] &&
|
|
54
|
+
ServerUrl.match?(reference.repository_url, provenance["base_repository_url"]) &&
|
|
55
|
+
ServerUrl.match?(server["url"], provenance["base_repository_url"]) &&
|
|
56
|
+
ServerUrl.match?(pr["head_repository_url"], provenance["head_repository_url"]) &&
|
|
57
|
+
ServerUrl.match?(pr["base_repository_url"], provenance["base_repository_url"]) &&
|
|
58
|
+
(!delivery["forge_server"] || delivery["forge_server"] == server["name"]) &&
|
|
59
|
+
(operation == "create" || pr["url"] == target["resource"]) &&
|
|
60
|
+
(operation == "ready" || pr["title"] == expected_title && pr["body"] == input["body"])
|
|
61
|
+
raise ArgumentError, "PR evidence selected identity or outcome differs"
|
|
62
|
+
end
|
|
63
|
+
DeliveryParameters.validate_url!(server["url"])
|
|
64
|
+
{input: input, pr: pr, server: server}
|
|
65
|
+
rescue JSON::ParserError, KeyError, TypeError, Ace::Git::Error
|
|
66
|
+
raise ArgumentError, "PR evidence is malformed"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.closed!(value, fields)
|
|
70
|
+
raise ArgumentError, "PR evidence fields differ" unless value.is_a?(Hash) && value.keys.sort == fields.sort
|
|
71
|
+
end
|
|
72
|
+
private_class_method :closed!
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "delivery_parameters"
|
|
3
|
+
require_relative "pr_reference"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Git
|
|
7
|
+
module Atoms
|
|
8
|
+
# Fixed receiver input vocabulary; draft is source policy, not a flag.
|
|
9
|
+
module ServicePrInput
|
|
10
|
+
OPERATIONS = %w[create update ready merge].freeze
|
|
11
|
+
DRAFT_OPERATIONS = %w[create update].freeze
|
|
12
|
+
|
|
13
|
+
def self.validate(input, operation:)
|
|
14
|
+
fields = case operation
|
|
15
|
+
when "create", "update" then %w[target delivery title body]
|
|
16
|
+
when "ready" then %w[target delivery]
|
|
17
|
+
when "merge" then %w[target delivery method]
|
|
18
|
+
else raise ArgumentError, "unsupported protected PR operation"
|
|
19
|
+
end
|
|
20
|
+
unless input.is_a?(Hash) && input.keys.sort == fields.sort &&
|
|
21
|
+
input["target"].is_a?(Hash) && input["target"].keys.sort == %w[artifact_digest resource]
|
|
22
|
+
raise ArgumentError, "protected PR input fields differ"
|
|
23
|
+
end
|
|
24
|
+
parameters = DeliveryParameters.validate(input.fetch("delivery"))
|
|
25
|
+
unless input["delivery"].keys.sort == %w[forge_default forge_server pr_provenance] && input["delivery"] == parameters
|
|
26
|
+
raise ArgumentError, "protected PR selection must be normalized"
|
|
27
|
+
end
|
|
28
|
+
resource = input.fetch("target").fetch("resource")
|
|
29
|
+
if operation == "create"
|
|
30
|
+
DeliveryParameters.validate_url!(resource)
|
|
31
|
+
unless ServerUrl.match?(resource, parameters.fetch("pr_provenance").fetch("base_repository_url"))
|
|
32
|
+
raise ArgumentError, "draft create target differs from base repository"
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
reference = resource.is_a?(String) && PrReference.parse(resource)
|
|
36
|
+
raise ArgumentError, "protected PR target requires an exact URL" unless reference && reference.repository_url
|
|
37
|
+
end
|
|
38
|
+
digest = input.fetch("target").fetch("artifact_digest")
|
|
39
|
+
unless digest.nil? || digest.is_a?(String) && /\A[0-9a-f]{64}\z/.match?(digest)
|
|
40
|
+
raise ArgumentError, "protected PR target artifact digest differs"
|
|
41
|
+
end
|
|
42
|
+
if DRAFT_OPERATIONS.include?(operation)
|
|
43
|
+
unless input["title"].is_a?(String) && input["title"].bytesize.between?(1, 1024) &&
|
|
44
|
+
input["body"].is_a?(String) && input["body"].bytesize <= 32 * 1024 &&
|
|
45
|
+
[input["title"], input["body"]].all? { |text| text.dup.force_encoding(Encoding::UTF_8).valid_encoding? && !text.include?("\0") }
|
|
46
|
+
raise ArgumentError, "draft PR requires bounded UTF-8 title/body"
|
|
47
|
+
end
|
|
48
|
+
elsif operation == "merge" && !%w[squash merge rebase].include?(input["method"])
|
|
49
|
+
raise ArgumentError, "protected merge requires an exact method"
|
|
50
|
+
end
|
|
51
|
+
input
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../../organisms/service_merge"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Git
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class ServiceMerge < Ace::Support::Cli::Command
|
|
9
|
+
desc "Execute a fixed receiver-owned exact-head PR envelope"
|
|
10
|
+
|
|
11
|
+
def initialize(input: nil, producer: nil, root: nil, operation: "merge")
|
|
12
|
+
@input, @producer, @root, @operation = input, producer, root, operation
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(**options)
|
|
16
|
+
raise ArgumentError, "fixed PR entry accepts no options" unless options.empty?
|
|
17
|
+
bytes = (@input || $stdin).read(Organisms::ServiceMerge::MAX_INPUT + 1)
|
|
18
|
+
result = (@producer || Organisms::ServiceMerge.new(operation: @operation)).call(bytes: bytes, root: @root || Dir.pwd)
|
|
19
|
+
puts JSON.generate(result)
|
|
20
|
+
rescue Ace::Git::Error, ArgumentError, SystemCallError, IOError => error
|
|
21
|
+
# No stdout receipt on an unverified/uncertain effect. The existing
|
|
22
|
+
# receiver retains its original dispatch instead of retrying it.
|
|
23
|
+
raise Ace::Support::Cli::Error, error.message
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|