ace-git 0.31.0 → 0.33.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 +16 -0
- data/handbook/workflow-instructions/git/mirror-sync.wf.md +84 -0
- data/lib/ace/git/cli/commands/pr.rb +1 -0
- data/lib/ace/git/organisms/pull_request_lifecycle.rb +8 -1
- data/lib/ace/git/providers/evidence.rb +5 -1
- data/lib/ace/git/providers/formal_approval.rb +14 -5
- 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: 183166f8ce864a5503f765212e4a0a89e26ec22f03dca7ab8a918c9499ec064a
|
|
4
|
+
data.tar.gz: 5c3eaa10c29b1873bf504cecaff12ac77806740f1bf1ee1e3c8372081c9d18e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21397a89baadadc17c0c18ce73c3fb558a6195133381e6f26df5e4ab897e025cff8aaa563b2cb6169400d8a5a7f43c2ff7c8549d6af261389d0690ba40c35fef
|
|
7
|
+
data.tar.gz: 2067ddf2e8871231ed33e0392aa922a429114b68c25dd9c3fe914c3d88d0df185ecc3ebebb2b4f9a1433a9a882c9c6aa147978616a2c03c31a2967508fd3e790
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.33.0] - 2026-11-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Include PR authors and semantic issue bodies in provider evidence. Verify scoped formal reviewer identity and exact HEAD; reject oversized or invalid UTF-8 PR bodies instead of truncating them.
|
|
15
|
+
## [0.32.0] - 2026-10-30
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `wfi://git/mirror-sync` (task 8xs.t.pu0.d): the repeatable host
|
|
20
|
+
mirror-sync operation — explicit source/target/ref, both full SHAs read
|
|
21
|
+
before acting, identity no-op, fast-forward only (`--force-with-lease`
|
|
22
|
+
with the read SHA), divergence escalated for reviewed reconciliation
|
|
23
|
+
(never reset/force/ref deletion), reversed-direction detection in the
|
|
24
|
+
plan, and a mutation-free dry-run.
|
|
25
|
+
|
|
10
26
|
## [0.31.0] - 2026-10-23
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-mirror-sync
|
|
3
|
+
description: Repeatable host mirror-sync operation — explicit source/target/ref, both full SHAs read before acting, identity no-op, fast-forward only, divergence escalated for reviewed reconciliation
|
|
4
|
+
allowed-tools: Bash, Read
|
|
5
|
+
doc-type: workflow
|
|
6
|
+
title: Host Mirror Sync Workflow
|
|
7
|
+
purpose: Bind the host operator's forge-mirror sync as a repeatable, evidenced operation: never destructive, always checkable, with the actual source and target SHAs in the result.
|
|
8
|
+
ace-docs:
|
|
9
|
+
last-updated: '2026-10-30'
|
|
10
|
+
last-checked: '2026-10-30'
|
|
11
|
+
---
|
|
12
|
+
# Host Mirror Sync Workflow
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
|
|
16
|
+
Synchronize an explicitly named target ref from an explicitly named source
|
|
17
|
+
ref (task 8xs.t.pu0.d — for this family: source `fg` Forgejo `cs3b/ace`,
|
|
18
|
+
target `origin` GitHub `cs3b/ace`) as a REPEATABLE host operation with a
|
|
19
|
+
checkable result. This is an ordinary Git workflow — no new script, no
|
|
20
|
+
hardcoded lab topology, no tool change; if a capability is missing it
|
|
21
|
+
belongs in ace-git's adapters.
|
|
22
|
+
|
|
23
|
+
## Instructions
|
|
24
|
+
|
|
25
|
+
Run the loaded workflow as the source of truth and execute it end-to-end
|
|
26
|
+
instead of only summarizing it.
|
|
27
|
+
|
|
28
|
+
### 1) Name the operation and read BOTH full SHAs first
|
|
29
|
+
|
|
30
|
+
Before ANY action, read and record the full SHAs of the exact source and
|
|
31
|
+
target refs. An operation without both SHAs is not runnable:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git ls-remote fg refs/heads/main # source SHA (record it)
|
|
35
|
+
git ls-remote origin refs/heads/main # target SHA (record it)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2) Classify
|
|
39
|
+
|
|
40
|
+
- **Identity** (source SHA == target SHA): the sync is a NO-OP. Report both
|
|
41
|
+
SHAs and the result `identity`; do not touch anything.
|
|
42
|
+
- **Fast-forward** (target SHA is an ancestor of source): proceed.
|
|
43
|
+
- **Divergence** (neither of the above): STOP. Divergence needs its own
|
|
44
|
+
reviewed reconciliation decision — NEVER resolve it here with a reset,
|
|
45
|
+
force push, or ref deletion. Report both SHAs and the divergence; a
|
|
46
|
+
historical SHA (e.g. a recorded closeout base like `af68f8fc1…`) is INPUT
|
|
47
|
+
EVIDENCE for the review, never a SHA to impose unconditionally on the
|
|
48
|
+
live target.
|
|
49
|
+
- **Reversed direction** (the plan shows the target ahead of the source):
|
|
50
|
+
report it explicitly — a dry-run that names source and target swapped is
|
|
51
|
+
a caught error, not a sync.
|
|
52
|
+
|
|
53
|
+
### 3) Execute (fast-forward only)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git fetch fg main
|
|
57
|
+
git push origin <source-SHA>:refs/heads/main --force-with-lease=refs/heads/main:<target-SHA>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`--force-with-lease` with the READ target SHA is the guard: if anything
|
|
61
|
+
moved the target between the read and the push, the push is REFUSED — the
|
|
62
|
+
operation restarts from step 1. Plain `--force` is forbidden.
|
|
63
|
+
|
|
64
|
+
### 4) Verify and report
|
|
65
|
+
|
|
66
|
+
Re-read the target SHA; the result names the ref, the before/after SHAs of
|
|
67
|
+
BOTH sides, and the verification (`git ls-remote origin` equals the source
|
|
68
|
+
SHA). Repetition after success reports `identity` — the repeatable
|
|
69
|
+
operation is idempotent at the evidence level.
|
|
70
|
+
|
|
71
|
+
### 5) Dry-run
|
|
72
|
+
|
|
73
|
+
The same classification with no mutation: report source/target/ref, both
|
|
74
|
+
SHAs, the planned action (no-op / fast-forward / divergence-blocked /
|
|
75
|
+
reversed), and NOTHING else changed.
|
|
76
|
+
|
|
77
|
+
## Boundaries
|
|
78
|
+
|
|
79
|
+
- Never delete refs, never reset, never `--force` without a lease.
|
|
80
|
+
- Salvage branches, open HITL cases, and active resources on either side
|
|
81
|
+
are untouched by a mirror sync; an active resource conflicting with the
|
|
82
|
+
plan is reported as preserved with its reason.
|
|
83
|
+
- Closing a single Herdr pane remains a separate follow-up — never close a
|
|
84
|
+
whole tab as a substitute.
|
|
@@ -363,7 +363,14 @@ module Ace
|
|
|
363
363
|
raise ArgumentError, "--body-file is required" unless path
|
|
364
364
|
raise ArgumentError, "Body file not found: #{path}" unless File.file?(path)
|
|
365
365
|
|
|
366
|
-
File.
|
|
366
|
+
body = File.binread(path, limit) || +""
|
|
367
|
+
if limit && body.bytesize >= limit
|
|
368
|
+
raise ArgumentError, "Body file exceeds #{limit - 1} bytes: #{path}"
|
|
369
|
+
end
|
|
370
|
+
body.force_encoding(Encoding::UTF_8)
|
|
371
|
+
raise ArgumentError, "Body file must contain valid UTF-8: #{path}" unless body.valid_encoding?
|
|
372
|
+
|
|
373
|
+
body
|
|
367
374
|
rescue SystemCallError => e
|
|
368
375
|
raise ArgumentError, "Cannot read body file #{path}: #{e.message}"
|
|
369
376
|
end
|
|
@@ -16,7 +16,11 @@ module Ace
|
|
|
16
16
|
:head_repository_url, :base_repository_url, :merge_commit_sha
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
ProviderIssue = Data.define(:server_name, :number, :title, :state, :author, :url, :labels)
|
|
19
|
+
ProviderIssue = Data.define(:server_name, :number, :title, :state, :author, :url, :labels, :body) do
|
|
20
|
+
def initialize(body: nil, **fields)
|
|
21
|
+
super(body: body, **fields)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
20
24
|
|
|
21
25
|
# Normalized check/CI evidence for one check run.
|
|
22
26
|
ProviderCheck = Data.define(:server_name, :name, :state, :conclusion, :url)
|
|
@@ -16,9 +16,10 @@ module Ace
|
|
|
16
16
|
CORRELATION = /\A[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}\z/
|
|
17
17
|
MARKER = /\n\n<!-- ace-approval:([a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}) sha256:([0-9a-f]{64}) -->\z/
|
|
18
18
|
|
|
19
|
-
def initialize(provider:, request:, list:, official: false, pending_reuse: false)
|
|
19
|
+
def initialize(provider:, request:, list:, official: false, pending_reuse: false, expected_reviewer: nil)
|
|
20
20
|
@provider, @request, @list, @official = provider, request, list, official
|
|
21
21
|
@pending_reuse = pending_reuse
|
|
22
|
+
@expected_reviewer = expected_reviewer
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def approve(number:, expected_head:, body:, correlation:)
|
|
@@ -44,7 +45,7 @@ module Ace
|
|
|
44
45
|
end
|
|
45
46
|
lock.flock(File::LOCK_EX)
|
|
46
47
|
pr = current!(number, expected_head)
|
|
47
|
-
actor = identity!(@request.call(:get, :actor, nil))
|
|
48
|
+
actor = @expected_reviewer ? [nil, @expected_reviewer] : identity!(@request.call(:get, :actor, nil))
|
|
48
49
|
deny_author!(pr, actor)
|
|
49
50
|
rows = reviews!(number)
|
|
50
51
|
matches = rows.select { |row| row.fetch("body").include?("<!-- ace-approval:#{correlation} ") }
|
|
@@ -52,14 +53,18 @@ module Ace
|
|
|
52
53
|
existing = matches.first
|
|
53
54
|
row!(existing) if existing
|
|
54
55
|
if existing && (existing["body"] != sent_body || existing["commit_id"] != expected_head ||
|
|
55
|
-
identity!(existing["user"])
|
|
56
|
+
!matches_actor?(actor, identity!(existing["user"])))
|
|
56
57
|
raise ProviderConflictingMatchesError, "Approval correlation is already bound to different evidence"
|
|
57
58
|
end
|
|
58
59
|
# A provider may reuse its current pending object on direct create.
|
|
59
60
|
# Only an exact known, comment-free pending review is resumable, and
|
|
60
61
|
# only when the adapter proves explicit commit preservation on reuse.
|
|
61
62
|
owned_pending = rows.select do |row|
|
|
62
|
-
row["state"] == "PENDING"
|
|
63
|
+
next false unless row["state"] == "PENDING"
|
|
64
|
+
|
|
65
|
+
# A repository-scoped Forgejo token can only see its own
|
|
66
|
+
# pending reviews. Preserve every visible unrelated pending.
|
|
67
|
+
@expected_reviewer || (row["user"].is_a?(Hash) && row["user"]["id"] == actor.first)
|
|
63
68
|
end
|
|
64
69
|
unrelated = owned_pending.any? { |row| !existing || row["id"] != existing["id"] }
|
|
65
70
|
if unrelated
|
|
@@ -140,6 +145,10 @@ module Ace
|
|
|
140
145
|
[user["id"], user["login"]]
|
|
141
146
|
end
|
|
142
147
|
|
|
148
|
+
def matches_actor?(expected, actual)
|
|
149
|
+
expected.first ? expected == actual : expected.last.casecmp?(actual.last)
|
|
150
|
+
end
|
|
151
|
+
|
|
143
152
|
def deny_author!(pr, actor)
|
|
144
153
|
unless pr.author.is_a?(String) && !pr.author.empty?
|
|
145
154
|
raise ProviderMalformedOutputError, "PR author is unverified"
|
|
@@ -199,7 +208,7 @@ module Ace
|
|
|
199
208
|
row["state"] == "APPROVED" && row["commit_id"] == expected_head &&
|
|
200
209
|
!row["dismissed"] && !row["stale"] && (!@official || row["official"] == true) &&
|
|
201
210
|
(!latest || latest["id"] == row["id"]) && (!expected_body || row["body"] == expected_body) &&
|
|
202
|
-
(!actor || actor
|
|
211
|
+
(!actor || matches_actor?(actor, actual_actor)) && row["html_url"].is_a?(String) &&
|
|
203
212
|
row["html_url"].start_with?("#{pr.url}#")
|
|
204
213
|
raise ProviderIdentityMismatchError, "Review is not the effective exact-head formal approval"
|
|
205
214
|
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.33.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-11-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-config
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- handbook/templates/pr/bugfix.template.md
|
|
134
134
|
- handbook/templates/pr/default.template.md
|
|
135
135
|
- handbook/templates/pr/feature.template.md
|
|
136
|
+
- handbook/workflow-instructions/git/mirror-sync.wf.md
|
|
136
137
|
- handbook/workflow-instructions/git/pr/create.wf.md
|
|
137
138
|
- handbook/workflow-instructions/git/pr/update.wf.md
|
|
138
139
|
- handbook/workflow-instructions/git/rebase.wf.md
|