ace-assign 0.70.0 → 0.73.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/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +3 -2099
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -1
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +4 -161
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -455
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -207
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/endcap_services.rb +0 -598
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_results.rb +0 -196
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -304
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -187
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "yaml"
|
|
3
|
-
require "ace/git/organisms/pull_request_lifecycle"
|
|
4
|
-
require_relative "../atoms/delivery_parameters"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Organisms
|
|
9
|
-
# Remote delivery is a projection of the existing attempt journal.
|
|
10
|
-
# Intents precede writes; an interrupted intent permits reconciliation
|
|
11
|
-
# reads only. Privileged merge is executed by qjx, never this worker.
|
|
12
|
-
class DeliveryCoordinator
|
|
13
|
-
def initialize(repo_root:, coordinator: nil, journal: nil, lifecycle_factory: nil, exclusion: nil, identity_resolver: nil)
|
|
14
|
-
@mutex = Mutex.new
|
|
15
|
-
@identity_resolver = identity_resolver || Molecules::ExecutionIdentityResolver.new
|
|
16
|
-
@repo_root = repo_root
|
|
17
|
-
@journal = journal || Molecules::EvidenceJournal.new(repo_root: repo_root)
|
|
18
|
-
@coordinator = coordinator || AttemptCoordinator.new(repo_root: repo_root, journal: @journal)
|
|
19
|
-
@factory = lifecycle_factory || ->(selection) { Ace::Git::Organisms::PullRequestLifecycle.new(**selection) }
|
|
20
|
-
@exclusion = exclusion || Molecules::LifecycleExclusion.new(repo_root: repo_root)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def perform(**arguments)
|
|
24
|
-
@mutex.synchronize { execute_request(**arguments) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def execute_request(assignment_id:, attempt_id:, operation:, parameters: nil, title: nil, body: nil,
|
|
30
|
-
tests: nil, review: nil, service_request_id: nil)
|
|
31
|
-
@identity = @identity_resolver.resolve
|
|
32
|
-
unless @identity_resolver.trusted?(@identity)
|
|
33
|
-
raise AttemptErrors::UnauthorizedIdentity, "Only a trusted coordinator or service may accept delivery evidence"
|
|
34
|
-
end
|
|
35
|
-
raise ArgumentError, "Unsupported delivery operation" unless %w[create update ready review merge status].include?(operation)
|
|
36
|
-
if operation == "create" && (!title.is_a?(String) || title.strip.empty?)
|
|
37
|
-
raise ArgumentError, "Create requires a concrete PR title"
|
|
38
|
-
end
|
|
39
|
-
if operation == "update" && title.nil? && body.nil?
|
|
40
|
-
raise ArgumentError, "Update requires title or body"
|
|
41
|
-
end
|
|
42
|
-
raise ArgumentError, "PR body must be text" unless body.nil? || body.is_a?(String)
|
|
43
|
-
params = Atoms::DeliveryParameters.validate(parameters || assignment_parameters(assignment_id))
|
|
44
|
-
binding = {"assignment_id" => assignment_id, "attempt_id" => attempt_id}
|
|
45
|
-
attempt = @coordinator.authoritative_attempt(binding)
|
|
46
|
-
raise AttemptErrors::NotFound, "Delivery attempt not found" unless attempt
|
|
47
|
-
raise AttemptErrors::ReceiptRejected, "Delivery requires a managed attempt" unless attempt.managed?
|
|
48
|
-
@exclusion.with_shared(@exclusion.task_key(attempt.binding.task_id)) do
|
|
49
|
-
@exclusion.with_exclusive(@exclusion.assignment_key(assignment_id)) do
|
|
50
|
-
raise AttemptErrors::Conflict, "Assignment was pruned" if @exclusion.removed?(@exclusion.assignment_key(assignment_id))
|
|
51
|
-
execute(binding, operation, params, title: title, body: body, tests: tests, review: review,
|
|
52
|
-
service_request_id: service_request_id)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def assignment_parameters(assignment_id)
|
|
60
|
-
assignment = Molecules::AssignmentManager.new.load(assignment_id)
|
|
61
|
-
raise AttemptErrors::NotFound, "Assignment not found" unless assignment
|
|
62
|
-
document = YAML.safe_load_file(assignment.source_config, permitted_classes: [Time, Date])
|
|
63
|
-
document.fetch("delivery")
|
|
64
|
-
rescue KeyError, Errno::ENOENT, Psych::Exception
|
|
65
|
-
raise ArgumentError, "Assignment needs explicit delivery parameters"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def execute(binding, operation, params, **options)
|
|
69
|
-
@binding = binding
|
|
70
|
-
@intent_context = nil
|
|
71
|
-
@attempt = @coordinator.authoritative_attempt(binding)
|
|
72
|
-
unless @attempt&.active? && @attempt.managed?
|
|
73
|
-
raise AttemptErrors::ReceiptRejected, "Delivery attempt is no longer active"
|
|
74
|
-
end
|
|
75
|
-
@head = git!("rev-parse", "HEAD").strip
|
|
76
|
-
selection = {server_name: params["forge_server"], use_default: params["forge_default"], repo_root: @repo_root}
|
|
77
|
-
historical = events.find { |event| event.dig("payload", "stage") == "identity" }&.fetch("payload")
|
|
78
|
-
selection = {server_name: historical.dig("server", "name"), use_default: false, repo_root: @repo_root} if historical
|
|
79
|
-
@lifecycle = @factory.call(selection)
|
|
80
|
-
server = @lifecycle.resolved_identity
|
|
81
|
-
@server = server
|
|
82
|
-
Atoms::DeliveryParameters.validate_url!(server.fetch("url"))
|
|
83
|
-
provenance = params.fetch("pr_provenance")
|
|
84
|
-
unless Ace::Git::Atoms::ServerUrl.match?(server["url"], provenance["base_repository_url"])
|
|
85
|
-
raise Ace::Git::ProviderIdentityMismatchError, "Selected forge does not match delivery base"
|
|
86
|
-
end
|
|
87
|
-
if historical
|
|
88
|
-
unless historical["server"] == server && historical["provenance"] == provenance &&
|
|
89
|
-
historical["selection"] == params.slice("forge_server", "forge_default")
|
|
90
|
-
raise AttemptErrors::Conflict, "Delivery identity or selection changed during attempt"
|
|
91
|
-
end
|
|
92
|
-
elsif !%w[status review].include?(operation)
|
|
93
|
-
record("stage" => "identity", "server" => server, "provenance" => provenance,
|
|
94
|
-
"selection" => params.slice("forge_server", "forge_default"))
|
|
95
|
-
end
|
|
96
|
-
@provenance = provenance
|
|
97
|
-
pinned = Ace::Git::ResolvedServer.new(name: server.fetch("name"),
|
|
98
|
-
provider: server.fetch("provider").to_sym, url: server.fetch("url"))
|
|
99
|
-
@lifecycle = @factory.call(server_name: pinned.name, use_default: false,
|
|
100
|
-
repo_root: @repo_root, resolved_server: pinned)
|
|
101
|
-
verify_pushed_head!
|
|
102
|
-
return status if operation == "status"
|
|
103
|
-
return review_snapshot if operation == "review"
|
|
104
|
-
pending = unresolved_intent
|
|
105
|
-
if pending
|
|
106
|
-
result = reconcile(pending)
|
|
107
|
-
return result if same_effect?(pending, operation, options)
|
|
108
|
-
raise AttemptErrors::CurrentEffectRequired.new(operation: operation, recovered_operation: pending["operation"])
|
|
109
|
-
end
|
|
110
|
-
require_evidence!(options[:tests], options[:review]) if %w[ready merge].include?(operation)
|
|
111
|
-
return consume_merge(options[:service_request_id]) if operation == "merge"
|
|
112
|
-
pr = current_pr
|
|
113
|
-
if operation == "create" && pr
|
|
114
|
-
requested = content_input(options)
|
|
115
|
-
metadata = @lifecycle.review_metadata_snapshot(pr.number).pull_request
|
|
116
|
-
verify_pr!(metadata)
|
|
117
|
-
unless content_matches?(requested.keys.map(&:to_s), Atoms::EvidenceDigest.digest(requested), metadata)
|
|
118
|
-
raise AttemptErrors::CurrentEffectRequired.new(operation: operation, recovered_operation: "create")
|
|
119
|
-
end
|
|
120
|
-
return reconcile({"operation" => "create", "head" => @head,
|
|
121
|
-
"fields" => requested.keys.map(&:to_s), "input_digest" => Atoms::EvidenceDigest.digest(requested)})
|
|
122
|
-
end
|
|
123
|
-
raise AttemptErrors::ReceiptRejected, "Create a draft before #{operation}" if operation != "create" && !pr
|
|
124
|
-
verify_pr!(pr) if pr
|
|
125
|
-
intent = {"stage" => "intent", "operation" => operation, "head" => @head,
|
|
126
|
-
"input_digest" => Atoms::EvidenceDigest.digest(content_input(options)),
|
|
127
|
-
"fields" => content_input(options).keys.map(&:to_s),
|
|
128
|
-
"pr_number" => pr&.number, "tests" => options[:tests], "review" => options[:review]}
|
|
129
|
-
record(intent)
|
|
130
|
-
@intent_context = unresolved_intent
|
|
131
|
-
receipt = case operation
|
|
132
|
-
when "create"
|
|
133
|
-
@lifecycle.create(head_ref: provenance["head_ref"], base_ref: provenance["base_ref"],
|
|
134
|
-
head_repository_url: provenance["head_repository_url"], expected_head: @head,
|
|
135
|
-
title: options[:title], body: options[:body], draft: true)
|
|
136
|
-
when "update"
|
|
137
|
-
@lifecycle.update(pr.number, expected_head: @head, title: options[:title], body: options[:body])
|
|
138
|
-
when "ready" then @lifecycle.ready(pr.number, expected_head: @head)
|
|
139
|
-
end
|
|
140
|
-
verify_pr!(receipt.pull_request)
|
|
141
|
-
unless receipt.operation.to_s == operation && receipt.server_name == server["name"] &&
|
|
142
|
-
(operation != "create" || receipt.pull_request.draft == true) &&
|
|
143
|
-
(operation != "ready" || receipt.pull_request.draft == false)
|
|
144
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "Delivery mutation lacks expected outcome"
|
|
145
|
-
end
|
|
146
|
-
record_result(operation, receipt.pull_request, "succeeded")
|
|
147
|
-
status
|
|
148
|
-
rescue Ace::Git::ProviderUnknownOutcomeError
|
|
149
|
-
# The durable intent remains unresolved; no exception text or
|
|
150
|
-
# provider payload is stored (both may contain sensitive data).
|
|
151
|
-
raise
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def events
|
|
155
|
-
@journal.read_events(@binding.fetch("assignment_id")).select do |event|
|
|
156
|
-
event["type"] == "delivery"
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def record(payload)
|
|
161
|
-
producer = {"actor" => @identity.actor, "role" => @identity.role, "runtime" => @identity.runtime}
|
|
162
|
-
@journal.record(**@binding.transform_keys(&:to_sym), type: "delivery", payload: payload.merge("producer" => producer),
|
|
163
|
-
guard: -> {
|
|
164
|
-
live = @coordinator.authoritative_attempt(@binding)
|
|
165
|
-
raise AttemptErrors::ReceiptRejected, "Delivery attempt is no longer active" unless live&.active?
|
|
166
|
-
raise AttemptErrors::ReceiptRejected, "Candidate changed during delivery" unless git!("rev-parse", "HEAD").strip == @head
|
|
167
|
-
})
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def unresolved_intent
|
|
171
|
-
history = events
|
|
172
|
-
settled = history.filter_map do |event|
|
|
173
|
-
payload = event.fetch("payload")
|
|
174
|
-
payload["intent_digest"] if payload["stage"] == "result" && payload["outcome"] == "succeeded"
|
|
175
|
-
end
|
|
176
|
-
pending = history.select do |event|
|
|
177
|
-
event.dig("payload", "stage") == "intent" && !settled.include?(event["digest"])
|
|
178
|
-
end
|
|
179
|
-
raise AttemptErrors::Conflict, "Multiple unresolved delivery intents require reconciliation" if pending.length > 1
|
|
180
|
-
event = pending.first
|
|
181
|
-
event && event.fetch("payload").merge("intent_digest" => event.fetch("digest"),
|
|
182
|
-
"intent_attempt_id" => event.fetch("attempt_id"))
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def current_pr
|
|
186
|
-
result = events.reverse.find { |event| event.dig("payload", "stage") == "result" && event.dig("payload", "pr") }
|
|
187
|
-
result && @lifecycle.show(result.dig("payload", "pr", "number"))
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def reconcile(intent)
|
|
191
|
-
@intent_context = intent
|
|
192
|
-
raise AttemptErrors::ReceiptRejected, "Unresolved delivery belongs to an older candidate" unless intent["head"] == @head
|
|
193
|
-
operation = intent.fetch("operation")
|
|
194
|
-
if operation == "create"
|
|
195
|
-
matches = @lifecycle.reconcile_create(**@provenance.reject { |key, _| key == "mode" }.transform_keys(&:to_sym))
|
|
196
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "No exact draft found; create remains unresolved" if matches.empty?
|
|
197
|
-
raise Ace::Git::ProviderConflictingMatchesError, "Multiple exact drafts found" unless matches.length == 1
|
|
198
|
-
pr = matches.first
|
|
199
|
-
verify_pr!(pr)
|
|
200
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "Created PR draft state is unproven" unless pr.draft == true
|
|
201
|
-
pr = @lifecycle.review_metadata_snapshot(pr.number).pull_request
|
|
202
|
-
verify_pr!(pr)
|
|
203
|
-
unless content_matches?(intent.fetch("fields"), intent.fetch("input_digest"), pr)
|
|
204
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "Created PR content remains unproven"
|
|
205
|
-
end
|
|
206
|
-
elsif operation == "ready"
|
|
207
|
-
require_evidence!(intent["tests"], intent["review"])
|
|
208
|
-
pr = @lifecycle.show(intent.fetch("pr_number"))
|
|
209
|
-
verify_pr!(pr)
|
|
210
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "Readiness remains unresolved" unless pr.draft == false
|
|
211
|
-
elsif operation == "update"
|
|
212
|
-
pr = @lifecycle.review_metadata_snapshot(intent.fetch("pr_number")).pull_request
|
|
213
|
-
verify_pr!(pr)
|
|
214
|
-
unless content_matches?(intent.fetch("fields"), intent.fetch("input_digest"), pr)
|
|
215
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "PR update remains unresolved"
|
|
216
|
-
end
|
|
217
|
-
else
|
|
218
|
-
raise Ace::Git::ProviderUnknownOutcomeError, "#{operation} requires authoritative receipt reconciliation"
|
|
219
|
-
end
|
|
220
|
-
record_result(operation, pr, "succeeded")
|
|
221
|
-
status
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def content_input(options)
|
|
225
|
-
options.slice(:title, :body).compact
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def content_matches?(fields, digest, pr)
|
|
229
|
-
observed = fields.to_h { |field| [field, pr.public_send(field)] }
|
|
230
|
-
Atoms::EvidenceDigest.digest(observed) == digest
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def same_effect?(intent, operation, options)
|
|
234
|
-
return false unless intent["operation"] == operation
|
|
235
|
-
case operation
|
|
236
|
-
when "create", "update"
|
|
237
|
-
intent["input_digest"] == Atoms::EvidenceDigest.digest(content_input(options))
|
|
238
|
-
when "ready"
|
|
239
|
-
%i[tests review].all? { |key| options[key].nil? || options[key] == intent[key.to_s] }
|
|
240
|
-
when "merge"
|
|
241
|
-
options[:service_request_id].is_a?(String) &&
|
|
242
|
-
options[:service_request_id] == intent["service_request_id"] &&
|
|
243
|
-
%i[tests review].all? { |key| options[key].nil? || options[key] == intent[key.to_s] }
|
|
244
|
-
else false
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def require_evidence!(tests, review)
|
|
249
|
-
[[tests, "check"], [review, "review-approval"]].each do |ref, kind|
|
|
250
|
-
unless ref.is_a?(Hash) && ref.keys.sort == %w[attempt_id receipt_digest]
|
|
251
|
-
raise AttemptErrors::ReceiptRejected, "Current accepted #{kind} evidence is required"
|
|
252
|
-
end
|
|
253
|
-
evidence = @coordinator.evidence(attempt_id: ref["attempt_id"], receipt_digest: ref["receipt_digest"], kind: kind)
|
|
254
|
-
unless evidence["head"] == @head && evidence["assignment_id"] == @binding["assignment_id"] &&
|
|
255
|
-
evidence["project_id"] == @attempt.binding.project_id
|
|
256
|
-
raise AttemptErrors::ReceiptRejected, "Delivery evidence does not match assignment, project and candidate"
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def consume_merge(request_id)
|
|
262
|
-
unless request_id.is_a?(String) && request_id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/)
|
|
263
|
-
raise AttemptErrors::ReceiptRejected, "Exact authorized merge service receipt is required"
|
|
264
|
-
end
|
|
265
|
-
request = @coordinator.service_request_status(request_id)
|
|
266
|
-
unless request && request["state"] == "succeeded" && request["operation"] == "merge" &&
|
|
267
|
-
request["assignment_id"] == @binding["assignment_id"] && request["attempt_id"] == @binding["attempt_id"] &&
|
|
268
|
-
request["project_id"] == @attempt.binding.project_id && request["candidate_head"] == @head &&
|
|
269
|
-
request["authorization"].is_a?(String) && !request["authorization"].empty?
|
|
270
|
-
raise AttemptErrors::ReceiptRejected, "Exact authorized merge service receipt is required"
|
|
271
|
-
end
|
|
272
|
-
@coordinator.validate_service_receipt!(request, "succeeded", request["receipt"])
|
|
273
|
-
pr = current_pr
|
|
274
|
-
verify_pr!(pr)
|
|
275
|
-
unless request.dig("target", "resource") == pr.url && pr.state.to_s == "merged" &&
|
|
276
|
-
pr.merge_commit_sha.to_s.match?(/\A[0-9a-f]{40}\z/)
|
|
277
|
-
raise AttemptErrors::ReceiptRejected, "Service receipt and merged PR identity do not agree"
|
|
278
|
-
end
|
|
279
|
-
record_result("merge", pr, "succeeded", "service_request_id" => request_id)
|
|
280
|
-
status
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def review_snapshot
|
|
284
|
-
pr = current_pr
|
|
285
|
-
raise AttemptErrors::ReceiptRejected, "Create a draft before remote review" unless pr
|
|
286
|
-
verify_pr!(pr)
|
|
287
|
-
snapshot = @lifecycle.review_snapshot(pr.number)
|
|
288
|
-
verify_pr!(snapshot.pull_request)
|
|
289
|
-
snapshot
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def verify_pr!(pr)
|
|
293
|
-
reference = pr && Ace::Git::Atoms::PrReference.parse(pr.url)
|
|
294
|
-
unless reference && reference.number == pr.number && reference.repository_url &&
|
|
295
|
-
Ace::Git::Atoms::ServerUrl.match?(reference.repository_url, @provenance["base_repository_url"])
|
|
296
|
-
raise Ace::Git::ProviderIdentityMismatchError, "PR URL does not match delivery repository and number"
|
|
297
|
-
end
|
|
298
|
-
Atoms::DeliveryParameters.validate_url!(pr.url)
|
|
299
|
-
unless pr && pr.server_name == @server["name"] && pr.number.is_a?(Integer) && pr.number.positive? &&
|
|
300
|
-
pr.head_sha == @head && pr.head_ref == @provenance["head_ref"] &&
|
|
301
|
-
pr.base_ref == @provenance["base_ref"] &&
|
|
302
|
-
Ace::Git::Atoms::ServerUrl.match?(pr.head_repository_url, @provenance["head_repository_url"]) &&
|
|
303
|
-
Ace::Git::Atoms::ServerUrl.match?(pr.base_repository_url, @provenance["base_repository_url"])
|
|
304
|
-
raise Ace::Git::ProviderExpectedHeadConflictError, "PR candidate or provenance does not match delivery"
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def verify_pushed_head!
|
|
309
|
-
output = git!("ls-remote", "--heads", @provenance["head_repository_url"], "refs/heads/#{@provenance['head_ref']}")
|
|
310
|
-
expected = "#{@head}\trefs/heads/#{@provenance['head_ref']}"
|
|
311
|
-
raise AttemptErrors::ReceiptRejected, "Pushed source does not match candidate" unless output.strip == expected
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def record_result(operation, pr, outcome, extra = {})
|
|
315
|
-
record({"stage" => "result", "operation" => operation, "head" => @head, "outcome" => outcome,
|
|
316
|
-
"intent_digest" => @intent_context&.fetch("intent_digest", nil),
|
|
317
|
-
"intent_attempt_id" => @intent_context&.fetch("intent_attempt_id", nil),
|
|
318
|
-
"pr" => {"number" => pr.number, "url" => pr.url, "head_sha" => pr.head_sha,
|
|
319
|
-
"draft" => pr.draft, "state" => pr.state.to_s, "merge_commit_sha" => pr.merge_commit_sha}}.merge(extra))
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def status
|
|
323
|
-
attempt = @coordinator.authoritative_attempt(@binding)
|
|
324
|
-
{"attempt_id" => attempt.attempt_id, "assignment_id" => @binding["assignment_id"],
|
|
325
|
-
"task_id" => attempt.binding.task_id, "project_id" => attempt.binding.project_id,
|
|
326
|
-
"actor" => attempt.binding.actor, "role" => attempt.binding.role, "runtime" => attempt.binding.runtime,
|
|
327
|
-
"base_head" => attempt.binding.base_head, "candidate_head" => attempt.candidate_head,
|
|
328
|
-
"evidence_git_ref" => @attempt.binding.evidence_git_ref, "journal_commit" => @journal.ref_value,
|
|
329
|
-
"delivery" => events.map do |event|
|
|
330
|
-
event.fetch("payload").merge("attempt_id" => event.fetch("attempt_id"), "event_digest" => event.fetch("digest"))
|
|
331
|
-
end}
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def git!(*argv)
|
|
335
|
-
result = Ace::Git::Atoms::CommandExecutor.execute("git", "-C", @repo_root, *argv)
|
|
336
|
-
raise AttemptErrors::EvidenceUnavailable, "Git delivery proof unavailable" unless result[:success]
|
|
337
|
-
result[:output]
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
end
|