Skip to main content

rustferry_github/
provider.rs

1//! GitHub Actions build-provider orchestration and isolated temporary-ref publication.
2//!
3//! Submission publishes one create-only temporary branch based on the exact requested source
4//! revision. The branch changes only the approved workflow and strict request manifest. Git
5//! plumbing never switches branches or mutates the caller's index or working tree.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    error::Error,
10    ffi::OsString,
11    fmt, fs,
12    io::{self, Read, Seek, Write},
13    path::{Component, Path, PathBuf},
14    process::{Child, Command, ExitStatus, Stdio},
15    sync::{
16        Mutex,
17        atomic::{AtomicU64, Ordering},
18        mpsc::{self, Receiver, RecvTimeoutError},
19    },
20    thread,
21    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
22};
23
24#[cfg(any(not(windows), test))]
25#[cfg(test)]
26use std::ffi::OsStr;
27
28use rustferry_remote::{
29    ArtifactDownloadRequest, ArtifactDownloadResult, ArtifactKind, ArtifactListRequest,
30    ArtifactManifest, BuildProvider, CURRENT_PROTOCOL_VERSION, CancellationAck,
31    CancellationRequest, CancellationToken, CleanupConfirmation, CleanupRequest, CleanupStatus,
32    CompilePhaseEvidence, EventPage, EventRequest, GitSnapshotDescriptor, HandshakeRequest,
33    HandshakeResponse, IosArtifactType, IosDeviceBuildRequest, IosDeviceBuildResult, JobHandle,
34    JobState, ProviderCapabilities, ProviderCheck, ProviderCheckStatus, ProviderDoctorReport,
35    ProviderDoctorRequest, ProviderFeature, ProviderFuture, RemoteBuildError, RemoteBuildEvent,
36    RemoteBuildEventKind, RemoteBuildResult, RemoteErrorInfo, SecretReference, SecretReferenceKind,
37    SigningMode, SigningStatus, SourceArchive, SourceBundleDescriptor, SourceMode,
38    canonical_git_snapshot_descriptor_bytes, canonical_request_sha256,
39};
40use semver::Version;
41use serde::{Deserialize, Serialize};
42use sha2::{Digest, Sha256};
43
44use crate::{
45    artifact::GithubArtifactExpectation,
46    git_endpoint::{GitRemoteName, GithubGitEndpoint, GithubRemoteSnapshot},
47    git_process::GitNetworkPolicy,
48    job_logs::GithubDurableRunAttemptLogIdentity,
49    snapshot::{
50        GitSha1ObjectId, GitSnapshotError, GitSnapshotImportInputs, GitSnapshotKeepaliveRef,
51        GitSnapshotObjectGraphV1, GitSnapshotObjectKind, GitSnapshotPrecomputeInputs,
52        GitSnapshotSourceRef, GitSnapshotStageDirectory, GitSnapshotStageLocatorV1,
53        GitSnapshotStageV1, complete_git_snapshot_object_graph,
54        discover_complete_git_snapshot_stages, require_sha1_object_format,
55    },
56    transport::{
57        AuthenticatedPrincipal, BranchName, CommitSha, GhExecutionError, GhRunner, GithubTransport,
58        GithubWorkflowDispatchHttpClient, PollPolicy, Repository, RunConclusion, RunEvent,
59        RunHandle, RunId, RunSnapshot, RunStatus, TemporaryGitRef, TransportError,
60        WorkflowDispatchReceipt, WorkflowDispatchRequest, WorkflowId, WorkflowRegistration,
61    },
62    workflow::{
63        GeneratedWorkflow, MAX_SIGNING_PROFILES, TemporaryBranchNamespace, TrustedSourceRef,
64        WorkflowConfig, WorkflowRunTrigger, generate_workflow,
65    },
66};
67
68#[cfg(feature = "secure-job-log-http")]
69use crate::{
70    job_logs::{GithubJobLogError, GithubJobLogFetcher, GithubJobLogLimits, GithubJobLogPoll},
71    transport::GhProcessRunner,
72};
73
74#[cfg(unix)]
75use crate::git_endpoint::GithubGitTransport;
76
77/// Stable provider identifier exposed through the remote-build protocol.
78pub const GITHUB_PROVIDER_ID: &str = "github-actions";
79/// Strict request file consumed by the generated workflow.
80pub const DISPATCH_MANIFEST_PATH: &str = ".rustferry/goal3/request.json";
81
82const DISPATCH_MANIFEST_SCHEMA_VERSION: u32 = 2;
83const GITHUB_JOB_RESUME_SCHEMA_VERSION: u32 = 1;
84const GITHUB_WORKFLOW_DISPATCH_RESUME_SCHEMA_VERSION: u32 = 1;
85const GITHUB_GIT_SNAPSHOT_RESUME_SCHEMA_VERSION: u32 = 1;
86/// Current attempt-scoped signed-cleanup evidence schema.
87pub const GITHUB_SIGNED_CLEANUP_EVIDENCE_SCHEMA_VERSION: u32 = 1;
88const MAX_JOB_RECORDS: usize = 1_024;
89const MAX_JOB_EVENTS: usize = 64;
90const MAX_JOB_MANIFESTS: usize = 64;
91/// Maximum encoded bytes accepted for one durable GitHub job resume record.
92pub const GITHUB_JOB_RESUME_MAX_BYTES: usize = 16 * 1024 * 1024;
93const MAX_DISPATCH_FILE_BYTES: usize = 2 * 1024 * 1024;
94const MAX_GIT_OUTPUT_BYTES: usize = 64 * 1024;
95const MAX_GIT_STDERR_BYTES: usize = 64 * 1024;
96const MAX_GIT_TIMEOUT: Duration = Duration::from_mins(10);
97const GIT_PROCESS_CLEANUP_GRACE: Duration = Duration::from_secs(1);
98const MAX_CALLER_GIT_OUTPUT_BYTES: usize = 32 * 1024 * 1024;
99const MAX_CALLER_GIT_CONTROL_BYTES: u64 = 1024 * 1024;
100static INDEX_RESERVATION_COUNTER: AtomicU64 = AtomicU64::new(0);
101// For process-fenced publication only: longer than worst-case post-checkpoint identity probes,
102// the Git timeout, and a conservative GitHub ref/run propagation margin.
103/// Minimum quiescence after the first exact absence observation before safe retirement.
104pub const GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS: u64 = 75 * 60 * 1_000;
105
106/// A validated lowercase SHA-256 digest of the exact generated workflow bytes.
107#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
108pub struct WorkflowFingerprint(String);
109
110impl WorkflowFingerprint {
111    /// Validate a lowercase 64-character SHA-256 digest.
112    ///
113    /// # Errors
114    ///
115    /// Rejects uppercase, abbreviated, or non-hexadecimal values.
116    pub fn new(value: impl Into<String>) -> Result<Self, GithubProviderConfigError> {
117        let value = value.into();
118        if !is_lower_sha256(&value) {
119            return Err(GithubProviderConfigError::InvalidWorkflowFingerprint);
120        }
121        Ok(Self(value))
122    }
123
124    /// Digest text.
125    pub fn as_str(&self) -> &str {
126        &self.0
127    }
128
129    /// Compute the fingerprint for one generated workflow.
130    pub fn for_workflow(workflow: &GeneratedWorkflow) -> Self {
131        Self(hex_sha256(workflow.yaml().as_bytes()))
132    }
133}
134
135/// Explicit caller authority for externally mutating GitHub state.
136#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
137#[allow(
138    clippy::struct_excessive_bools,
139    reason = "independent least-privilege mutation authorities must remain separately configurable"
140)]
141pub struct GithubMutationAuthorization {
142    /// Permit one create-only push below the configured temporary namespace.
143    pub publish_temporary_ref: bool,
144    /// Permit cancellation of the immutable mapped GitHub Actions run ID across all attempts.
145    pub cancel_run: bool,
146    /// Permit deletion of the exact provider-created temporary branch.
147    pub delete_temporary_ref: bool,
148    /// Permit one create-only push of an explicitly consented Git snapshot source ref.
149    pub publish_git_snapshot_source: bool,
150    /// Permit exact lease-protected deletion of a provider-owned Git snapshot source ref.
151    pub delete_git_snapshot_source: bool,
152    /// Permit exact creation and lineage-prune release of local Git snapshot keepalive refs.
153    pub manage_git_snapshot_keepalive: bool,
154}
155
156/// One bounded policy shared by run discovery and terminal-run polling.
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct GithubPollingPolicy {
159    transport: PollPolicy,
160    discovery_attempts: u16,
161    discovery_window_ms: u64,
162}
163
164impl GithubPollingPolicy {
165    /// Construct a policy using the same attempt count and interval for discovery and polling.
166    ///
167    /// # Errors
168    ///
169    /// Returns the transport's typed configuration error for attempts outside 1–2000 or an
170    /// interval outside 250 milliseconds through 60 seconds.
171    pub fn new(
172        attempts: u16,
173        interval: Duration,
174    ) -> Result<Self, crate::transport::TransportConfigError> {
175        let transport = PollPolicy::new(attempts, interval)?;
176        let interval_ms = u64::try_from(interval.as_millis()).unwrap_or(u64::MAX);
177        let discovery_window_ms = interval_ms.saturating_mul(u64::from(attempts));
178        Ok(Self {
179            transport,
180            discovery_attempts: attempts,
181            discovery_window_ms,
182        })
183    }
184
185    /// Five-second checks bounded to 1,440 attempts and two hours.
186    ///
187    /// # Panics
188    ///
189    /// Panics only if these compile-time constants stop satisfying `PollPolicy` invariants.
190    pub fn secure_defaults() -> Self {
191        Self::new(1_440, Duration::from_secs(5)).expect("constant polling policy is valid")
192    }
193
194    /// Transport policy used after exact run mapping.
195    pub const fn transport(self) -> PollPolicy {
196        self.transport
197    }
198
199    /// Maximum exact run-discovery attempts.
200    pub const fn discovery_attempts(self) -> u16 {
201        self.discovery_attempts
202    }
203
204    /// Maximum wall-clock run-discovery window.
205    pub const fn discovery_window_ms(self) -> u64 {
206        self.discovery_window_ms
207    }
208}
209
210/// Invalid security-sensitive GitHub provider configuration.
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub enum GithubProviderConfigError {
213    /// Expected workflow fingerprint was malformed.
214    InvalidWorkflowFingerprint,
215    /// Generated workflow bytes did not match the approved fingerprint.
216    WorkflowFingerprintMismatch,
217    /// The configured source repository was not the workflow's canonical public source URL.
218    InvalidSourceRepository,
219    /// The in-memory job bound was outside 1–1024.
220    InvalidJobLimit,
221    /// Worker distribution version was not semantic-version syntax.
222    InvalidWorkerVersion,
223    /// Handshake worker version differed from the hash-pinned workflow worker.
224    WorkerVersionMismatch,
225    /// Persisted Git endpoints did not match configured repository identities.
226    InvalidGitEndpoints,
227    /// This platform has no pinned credential path for an HTTPS execution endpoint.
228    UnsupportedExecutionTransport,
229}
230
231impl fmt::Display for GithubProviderConfigError {
232    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233        match self {
234            Self::InvalidWorkflowFingerprint => {
235                formatter.write_str("workflow fingerprint must be lowercase SHA-256")
236            }
237            Self::WorkflowFingerprintMismatch => {
238                formatter.write_str("generated workflow does not match its approved fingerprint")
239            }
240            Self::InvalidSourceRepository => {
241                formatter.write_str("source repository does not match the workflow public source")
242            }
243            Self::InvalidJobLimit => formatter.write_str("job limit must be between 1 and 1024"),
244            Self::InvalidWorkerVersion => {
245                formatter.write_str("workflow worker version must be semantic-version syntax")
246            }
247            Self::WorkerVersionMismatch => {
248                formatter.write_str("handshake worker version does not match workflow distribution")
249            }
250            Self::InvalidGitEndpoints => {
251                formatter.write_str("Git endpoints do not match provider repositories")
252            }
253            Self::UnsupportedExecutionTransport => formatter
254                .write_str("execution Git fetch and push must use SSH on this secure platform"),
255        }
256    }
257}
258
259impl Error for GithubProviderConfigError {}
260
261/// Fully validated GitHub orchestration policy.
262#[derive(Clone, Debug, Eq, PartialEq)]
263pub struct GithubProviderConfig {
264    repository: Repository,
265    source_repository: String,
266    workflow: WorkflowConfig,
267    workflow_fingerprint: WorkflowFingerprint,
268    poll_policy: GithubPollingPolicy,
269    mutation_authorization: GithubMutationAuthorization,
270    worker_version: Version,
271    max_jobs: usize,
272    git_endpoints: Option<GithubProviderGitEndpoints>,
273}
274
275#[derive(Clone, Debug, Eq, PartialEq)]
276struct GithubProviderGitEndpoints {
277    source: GithubRemoteSnapshot,
278    execution: GithubRemoteSnapshot,
279}
280
281impl GithubProviderConfig {
282    /// Bind repository, workflow identity, immutable content, and mutation policy.
283    ///
284    /// # Errors
285    ///
286    /// Rejects a source identity mismatch, fingerprint mismatch, worker-version mismatch, or
287    /// invalid job bound. The GitHub repository is the execution repository; the source URL may
288    /// identify a distinct repository.
289    #[allow(clippy::too_many_arguments)]
290    pub fn new(
291        repository: Repository,
292        source_repository: impl Into<String>,
293        workflow: WorkflowConfig,
294        workflow_fingerprint: WorkflowFingerprint,
295        poll_policy: GithubPollingPolicy,
296        mutation_authorization: GithubMutationAuthorization,
297        worker_version: &Version,
298        max_jobs: usize,
299    ) -> Result<Self, GithubProviderConfigError> {
300        if !(1..=MAX_JOB_RECORDS).contains(&max_jobs) {
301            return Err(GithubProviderConfigError::InvalidJobLimit);
302        }
303        let source_repository = source_repository.into();
304        if source_repository != workflow.public_source_repository().url() {
305            return Err(GithubProviderConfigError::InvalidSourceRepository);
306        }
307        let generated = generate_workflow(&workflow);
308        if WorkflowFingerprint::for_workflow(&generated) != workflow_fingerprint {
309            return Err(GithubProviderConfigError::WorkflowFingerprintMismatch);
310        }
311        let distribution_worker_version = Version::parse(workflow.worker().version())
312            .map_err(|_| GithubProviderConfigError::InvalidWorkerVersion)?;
313        if *worker_version != distribution_worker_version {
314            return Err(GithubProviderConfigError::WorkerVersionMismatch);
315        }
316        Ok(Self {
317            repository,
318            source_repository,
319            workflow,
320            workflow_fingerprint,
321            poll_policy,
322            mutation_authorization,
323            worker_version: distribution_worker_version,
324            max_jobs,
325            git_endpoints: None,
326        })
327    }
328
329    /// Exact GitHub Actions execution repository.
330    pub fn repository(&self) -> &Repository {
331        &self.repository
332    }
333
334    /// Normalized source repository URL accepted in Git requests.
335    pub fn source_repository(&self) -> &str {
336        &self.source_repository
337    }
338
339    /// Approved workflow configuration.
340    pub fn workflow(&self) -> &WorkflowConfig {
341        &self.workflow
342    }
343
344    /// Approved exact workflow digest.
345    pub fn workflow_fingerprint(&self) -> &WorkflowFingerprint {
346        &self.workflow_fingerprint
347    }
348
349    /// Bounded run polling policy retained for explicit terminal waits.
350    pub const fn poll_policy(&self) -> PollPolicy {
351        self.poll_policy.transport()
352    }
353
354    /// External mutation authority captured from the caller.
355    pub const fn mutation_authorization(&self) -> GithubMutationAuthorization {
356        self.mutation_authorization
357    }
358
359    /// Bind immutable canonical Git endpoints captured by setup into the durable provider identity.
360    ///
361    /// # Errors
362    ///
363    /// Rejects snapshots whose repository identities differ from the configured source or execution
364    /// repositories.
365    pub fn bind_git_endpoints(
366        mut self,
367        source: GithubRemoteSnapshot,
368        execution: GithubRemoteSnapshot,
369    ) -> Result<Self, GithubProviderConfigError> {
370        if !github_remote_matches(source.fetch().canonical_url(), &self.source_repository)
371            || !github_remote_matches(source.push().canonical_url(), &self.source_repository)
372            || !github_remote_matches(
373                execution.fetch().canonical_url(),
374                &repository_url(&self.repository),
375            )
376            || !github_remote_matches(
377                execution.push().canonical_url(),
378                &repository_url(&self.repository),
379            )
380        {
381            return Err(GithubProviderConfigError::InvalidGitEndpoints);
382        }
383        #[cfg(unix)]
384        if execution.fetch().transport() != GithubGitTransport::Ssh
385            || execution.push().transport() != GithubGitTransport::Ssh
386        {
387            return Err(GithubProviderConfigError::UnsupportedExecutionTransport);
388        }
389        self.git_endpoints = Some(GithubProviderGitEndpoints { source, execution });
390        Ok(self)
391    }
392}
393
394/// Secret-free authenticated principal identity bound to a durable job record.
395#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
396#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
397pub enum GithubPrincipalIdentityV1 {
398    /// Human user identity proven by GitHub's authenticated-user endpoint.
399    User {
400        /// Stable GitHub user database identifier.
401        id: u64,
402        /// Exact login returned by GitHub.
403        login: String,
404    },
405    /// Repository-scoped installation credential.
406    ///
407    /// Its durable principal is the composite of this variant and the enclosing exact execution
408    /// repository ID. It is admitted only for explicit workflow dispatch and is re-proven against
409    /// that repository on every live restore.
410    RepositoryCredential,
411}
412
413impl GithubPrincipalIdentityV1 {
414    fn from_authenticated(
415        principal: &AuthenticatedPrincipal,
416        run_trigger: WorkflowRunTrigger,
417        execution_repository_id: u64,
418    ) -> RemoteBuildResult<Self> {
419        match principal {
420            AuthenticatedPrincipal::User(user) => Ok(Self::User {
421                id: user.id(),
422                login: user.login().to_owned(),
423            }),
424            AuthenticatedPrincipal::RepositoryCredential { .. }
425                if run_trigger != WorkflowRunTrigger::WorkflowDispatch =>
426            {
427                Err(resume_failure(
428                    "resume_repository_credential_requires_workflow_dispatch",
429                ))
430            }
431            AuthenticatedPrincipal::RepositoryCredential { repository_id }
432                if *repository_id != execution_repository_id =>
433            {
434                Err(resume_failure(
435                    "resume_repository_credential_repository_mismatch",
436                ))
437            }
438            AuthenticatedPrincipal::RepositoryCredential { .. } => Ok(Self::RepositoryCredential),
439        }
440    }
441}
442
443/// Live, secret-free provider identity needed to create a durable local job before submission.
444#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
445#[serde(deny_unknown_fields)]
446pub struct GithubDurableIdentityV1 {
447    /// Stable provider identifier.
448    pub provider: String,
449    /// Digest of all security-relevant provider configuration.
450    pub provider_config_sha256: String,
451    /// Stable authenticated user identity.
452    pub principal: GithubPrincipalIdentityV1,
453    /// Canonical execution-repository URL.
454    pub execution_repository: String,
455    /// Stable GitHub database ID for the execution repository.
456    pub execution_repository_id: u64,
457}
458
459#[derive(Clone, Debug)]
460enum GithubGitSnapshotStageAuthorityV1 {
461    SameInvocation {
462        locator: GitSnapshotStageLocatorV1,
463        stage: GitSnapshotStageV1,
464    },
465    ExactRetryLineage {
466        locator: GitSnapshotStageLocatorV1,
467        stage: GitSnapshotStageV1,
468    },
469    FreshReconsentAdoption,
470}
471
472/// One-shot, path-free authority to create the first durable Git snapshot provider checkpoint.
473///
474/// This value is intentionally not serializable. The caller must construct it only in the same
475/// invocation that displayed and accepted the exact public-upload plan, or through the explicit
476/// fresh-reconsent constructor used after restart. The provider consumes it synchronously and
477/// compares the approved consent SHA against the canonical stage before any Git or network
478/// mutation.
479#[derive(Clone, Debug)]
480pub struct GithubGitSnapshotSubmissionV1 {
481    durable_identity: GithubDurableIdentityV1,
482    request: IosDeviceBuildRequest,
483    approved_consent_sha256: String,
484    stage_authority: GithubGitSnapshotStageAuthorityV1,
485}
486
487/// Explicit durable-prune authority for releasing one exact local snapshot keepalive.
488///
489/// This value binds the provider checkpoint to a deterministic digest over stable sorted lineage
490/// IDs, edges, and cutoff. The provider verifies and executes the exact local ref mutation; it
491/// does not independently prove lineage completeness or bind mutable job revisions.
492#[derive(Clone, Debug, Eq, PartialEq)]
493pub struct GithubGitSnapshotKeepaliveReleaseAuthorizationV1 {
494    job_id: String,
495    complete_lineage_authorization_sha256: String,
496}
497
498impl GithubGitSnapshotKeepaliveReleaseAuthorizationV1 {
499    /// Bind one exact restored job to one stable complete-lineage authorization digest.
500    ///
501    /// # Errors
502    ///
503    /// Returns a typed validation failure for an unsafe operation identifier or invalid digest.
504    pub fn new(
505        job_id: impl Into<String>,
506        complete_lineage_authorization_sha256: impl Into<String>,
507    ) -> RemoteBuildResult<Self> {
508        let job_id = job_id.into();
509        let complete_lineage_authorization_sha256 = complete_lineage_authorization_sha256.into();
510        GitSnapshotSourceRef::for_operation(&job_id)
511            .map_err(|_| resume_failure("snapshot_prune_job_invalid"))?;
512        if !is_lower_sha256(&complete_lineage_authorization_sha256) {
513            return Err(resume_failure("snapshot_prune_authorization_invalid"));
514        }
515        Ok(Self {
516            job_id,
517            complete_lineage_authorization_sha256,
518        })
519    }
520
521    /// Exact local durable job identifier.
522    pub fn job_id(&self) -> &str {
523        &self.job_id
524    }
525
526    /// SHA-256 of the stable complete-lineage authorization held under exact prune leases.
527    pub fn complete_lineage_authorization_sha256(&self) -> &str {
528        &self.complete_lineage_authorization_sha256
529    }
530}
531
532/// Durable child-lineage authority for staging an exact Git snapshot retry.
533#[derive(Clone, Debug, Eq, PartialEq)]
534pub struct GithubGitSnapshotExactRetryAuthorizationV1 {
535    parent_job_id: String,
536    child_operation_id: String,
537    child_source_created_at_ms: u64,
538    retry_lineage_authorization_sha256: String,
539}
540
541impl GithubGitSnapshotExactRetryAuthorizationV1 {
542    /// Bind an already-durable parent/child lineage edge to one exact retry stage operation.
543    ///
544    /// # Errors
545    ///
546    /// Returns a typed failure for equal or unsafe operation IDs, an invalid timestamp, or a
547    /// malformed stable lineage-authorization digest.
548    pub fn new(
549        parent_job_id: impl Into<String>,
550        child_operation_id: impl Into<String>,
551        child_source_created_at_ms: u64,
552        retry_lineage_authorization_sha256: impl Into<String>,
553    ) -> RemoteBuildResult<Self> {
554        let parent_job_id = parent_job_id.into();
555        let child_operation_id = child_operation_id.into();
556        let retry_lineage_authorization_sha256 = retry_lineage_authorization_sha256.into();
557        if parent_job_id == child_operation_id
558            || child_source_created_at_ms == 0
559            || child_source_created_at_ms / 1_000 > i64::MAX as u64
560            || GitSnapshotSourceRef::for_operation(&parent_job_id).is_err()
561            || GitSnapshotSourceRef::for_operation(&child_operation_id).is_err()
562            || !is_lower_sha256(&retry_lineage_authorization_sha256)
563        {
564            return Err(resume_failure("snapshot_exact_retry_authorization_invalid"));
565        }
566        Ok(Self {
567            parent_job_id,
568            child_operation_id,
569            child_source_created_at_ms,
570            retry_lineage_authorization_sha256,
571        })
572    }
573}
574
575/// Deterministic, no-write plan for one exact Git snapshot retry.
576///
577/// The final child request and object graph are available before the controller persists the
578/// immutable parent/child lineage. Filesystem identities are deliberately absent: they are
579/// captured only when the already-authorized stage is materialized.
580#[derive(Clone, Debug, PartialEq)]
581pub struct GithubGitSnapshotExactRetryPlanV1 {
582    /// Final child request whose revision is the regenerated parentless commit.
583    pub request: IosDeviceBuildRequest,
584    /// Deterministic child snapshot commit timestamp.
585    pub source_created_at_ms: u64,
586    /// Canonical request-template SHA-256 embedded in the regenerated descriptor.
587    pub request_template_sha256: String,
588    /// Canonical source-manifest SHA-256 retained from the parent.
589    pub manifest_sha256: String,
590    /// Exact parent archive retained for the child.
591    pub archive: SourceArchive,
592    /// SHA-256 of the regenerated canonical descriptor bytes.
593    pub descriptor_sha256: String,
594    /// Regenerated six-object graph, including the final child commit.
595    pub graph: GitSnapshotObjectGraphV1,
596}
597
598/// Complete create-only child stage produced from a verified parent archive blob.
599#[derive(Clone, Debug, PartialEq)]
600pub struct GithubGitSnapshotExactRetryStageV1 {
601    /// Final child request whose revision is the regenerated parentless commit.
602    pub request: IosDeviceBuildRequest,
603    /// Path-free exact child stage locator.
604    pub stage_locator: GitSnapshotStageLocatorV1,
605    /// Immutable child stage summary and regenerated six-object graph.
606    pub stage: GitSnapshotStageV1,
607}
608
609/// Complete path-free recovery candidate discovered without adoption or mutation.
610#[derive(Clone, Debug, PartialEq)]
611pub struct GithubGitSnapshotRecoveryCandidateV1 {
612    /// Exact locator required by a later fresh-reconsent adoption.
613    pub stage_locator: GitSnapshotStageLocatorV1,
614    /// Canonical stage, including original operation, timestamp, graph, consent, and final request.
615    pub stage: GitSnapshotStageV1,
616    /// Canonical public-object descriptor bound to the stage and final request.
617    pub descriptor: GitSnapshotDescriptor,
618    /// Secret-free final request reconstructed exactly from canonical stage metadata.
619    pub final_request: IosDeviceBuildRequest,
620}
621
622impl GithubGitSnapshotSubmissionV1 {
623    /// Bind a complete stage created by the same explicit-consent invocation.
624    ///
625    /// # Errors
626    ///
627    /// Returns a typed failure when the accepted plan hash is not canonical SHA-256 text.
628    pub fn same_invocation(
629        durable_identity: GithubDurableIdentityV1,
630        request: IosDeviceBuildRequest,
631        approved_consent_sha256: String,
632        locator: GitSnapshotStageLocatorV1,
633        stage: GitSnapshotStageV1,
634    ) -> RemoteBuildResult<Self> {
635        if !is_lower_sha256(&approved_consent_sha256) {
636            return Err(provider_failure(
637                "snapshot_consent_invalid",
638                "Git snapshot consent identity is invalid",
639                false,
640            ));
641        }
642        Ok(Self {
643            durable_identity,
644            request,
645            approved_consent_sha256,
646            stage_authority: GithubGitSnapshotStageAuthorityV1::SameInvocation { locator, stage },
647        })
648    }
649
650    /// Bind an exact-retry stage created only after durable parent/child lineage publication.
651    ///
652    /// This authority is distinct from user consent. The stable lineage digest must be the exact
653    /// stage authorization captured by the replayable materialization result.
654    ///
655    /// # Errors
656    ///
657    /// Returns a typed failure when the child operation, timestamp, lineage digest, final request,
658    /// locator, or stage binding differs from the durable retry authorization.
659    pub fn exact_retry_lineage(
660        durable_identity: GithubDurableIdentityV1,
661        authorization: &GithubGitSnapshotExactRetryAuthorizationV1,
662        staged: GithubGitSnapshotExactRetryStageV1,
663    ) -> RemoteBuildResult<Self> {
664        if staged.request.operation_id != authorization.child_operation_id
665            || staged.stage.operation_id != authorization.child_operation_id
666            || staged.stage_locator.operation_id != authorization.child_operation_id
667            || staged.stage.source_created_at_ms != authorization.child_source_created_at_ms
668            || staged.stage.consent_sha256 != authorization.retry_lineage_authorization_sha256
669            || staged.stage.final_request != staged.request
670        {
671            return Err(provider_failure(
672                "snapshot_exact_retry_stage_authorization_mismatch",
673                "Git snapshot retry stage differs from durable lineage authority",
674                false,
675            ));
676        }
677        staged
678            .stage
679            .validate_for_request(
680                &GitSnapshotDescriptor::from_request(
681                    &staged.request,
682                    SourceBundleDescriptor::new(
683                        staged.stage.archive.clone(),
684                        staged.request.source.clone(),
685                    ),
686                )
687                .map_err(|_| resume_failure("snapshot_exact_retry_stage_invalid"))?,
688                &staged.request,
689            )
690            .map_err(|_| resume_failure("snapshot_exact_retry_stage_invalid"))?;
691        Ok(Self {
692            durable_identity,
693            request: staged.request,
694            approved_consent_sha256: authorization.retry_lineage_authorization_sha256.clone(),
695            stage_authority: GithubGitSnapshotStageAuthorityV1::ExactRetryLineage {
696                locator: staged.stage_locator,
697                stage: staged.stage,
698            },
699        })
700    }
701
702    /// Require no-mutation stage adoption after a fresh restart preview and explicit reconsent.
703    ///
704    /// # Errors
705    ///
706    /// Returns a typed failure when the newly accepted plan hash is not canonical SHA-256 text.
707    pub fn after_fresh_reconsent(
708        durable_identity: GithubDurableIdentityV1,
709        request: IosDeviceBuildRequest,
710        approved_consent_sha256: String,
711    ) -> RemoteBuildResult<Self> {
712        if !is_lower_sha256(&approved_consent_sha256) {
713            return Err(provider_failure(
714                "snapshot_consent_invalid",
715                "Git snapshot consent identity is invalid",
716                false,
717            ));
718        }
719        Ok(Self {
720            durable_identity,
721            request,
722            approved_consent_sha256,
723            stage_authority: GithubGitSnapshotStageAuthorityV1::FreshReconsentAdoption,
724        })
725    }
726}
727
728/// Trigger event retained with an exact durable run identity.
729#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
730#[serde(rename_all = "snake_case")]
731pub enum GithubRunEventV1 {
732    /// Push to the provider's exact temporary ref.
733    Push,
734    /// Explicit workflow dispatch.
735    WorkflowDispatch,
736}
737
738impl From<RunEvent> for GithubRunEventV1 {
739    fn from(value: RunEvent) -> Self {
740        match value {
741            RunEvent::Push => Self::Push,
742            RunEvent::WorkflowDispatch => Self::WorkflowDispatch,
743        }
744    }
745}
746
747impl From<GithubRunEventV1> for RunEvent {
748    fn from(value: GithubRunEventV1) -> Self {
749        match value {
750            GithubRunEventV1::Push => Self::Push,
751            GithubRunEventV1::WorkflowDispatch => Self::WorkflowDispatch,
752        }
753    }
754}
755
756/// GitHub run status retained in a durable provider checkpoint.
757#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
758#[serde(rename_all = "snake_case")]
759pub enum GithubRunStatusV1 {
760    /// Requested but not queued.
761    Requested,
762    /// Queued by GitHub Actions.
763    Queued,
764    /// Pending execution.
765    Pending,
766    /// Waiting on an external gate.
767    Waiting,
768    /// Running on a worker.
769    InProgress,
770    /// Terminal run.
771    Completed,
772}
773
774impl From<RunStatus> for GithubRunStatusV1 {
775    fn from(value: RunStatus) -> Self {
776        match value {
777            RunStatus::Requested => Self::Requested,
778            RunStatus::Queued => Self::Queued,
779            RunStatus::Pending => Self::Pending,
780            RunStatus::Waiting => Self::Waiting,
781            RunStatus::InProgress => Self::InProgress,
782            RunStatus::Completed => Self::Completed,
783        }
784    }
785}
786
787impl From<GithubRunStatusV1> for RunStatus {
788    fn from(value: GithubRunStatusV1) -> Self {
789        match value {
790            GithubRunStatusV1::Requested => Self::Requested,
791            GithubRunStatusV1::Queued => Self::Queued,
792            GithubRunStatusV1::Pending => Self::Pending,
793            GithubRunStatusV1::Waiting => Self::Waiting,
794            GithubRunStatusV1::InProgress => Self::InProgress,
795            GithubRunStatusV1::Completed => Self::Completed,
796        }
797    }
798}
799
800/// Terminal GitHub conclusion retained in a durable provider checkpoint.
801#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
802#[serde(rename_all = "snake_case")]
803pub enum GithubRunConclusionV1 {
804    /// Successful run.
805    Success,
806    /// Failed run.
807    Failure,
808    /// Cancelled run.
809    Cancelled,
810    /// Timed-out run.
811    TimedOut,
812    /// Neutral run.
813    Neutral,
814    /// Skipped run.
815    Skipped,
816    /// Stale run.
817    Stale,
818    /// Run requiring external action.
819    ActionRequired,
820    /// Run rejected during startup.
821    StartupFailure,
822}
823
824impl From<RunConclusion> for GithubRunConclusionV1 {
825    fn from(value: RunConclusion) -> Self {
826        match value {
827            RunConclusion::Success => Self::Success,
828            RunConclusion::Failure => Self::Failure,
829            RunConclusion::Cancelled => Self::Cancelled,
830            RunConclusion::TimedOut => Self::TimedOut,
831            RunConclusion::Neutral => Self::Neutral,
832            RunConclusion::Skipped => Self::Skipped,
833            RunConclusion::Stale => Self::Stale,
834            RunConclusion::ActionRequired => Self::ActionRequired,
835            RunConclusion::StartupFailure => Self::StartupFailure,
836        }
837    }
838}
839
840impl From<GithubRunConclusionV1> for RunConclusion {
841    fn from(value: GithubRunConclusionV1) -> Self {
842        match value {
843            GithubRunConclusionV1::Success => Self::Success,
844            GithubRunConclusionV1::Failure => Self::Failure,
845            GithubRunConclusionV1::Cancelled => Self::Cancelled,
846            GithubRunConclusionV1::TimedOut => Self::TimedOut,
847            GithubRunConclusionV1::Neutral => Self::Neutral,
848            GithubRunConclusionV1::Skipped => Self::Skipped,
849            GithubRunConclusionV1::Stale => Self::Stale,
850            GithubRunConclusionV1::ActionRequired => Self::ActionRequired,
851            GithubRunConclusionV1::StartupFailure => Self::StartupFailure,
852        }
853    }
854}
855
856/// Exact GitHub Actions run and attempt identity safe to persist without credentials.
857#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
858#[serde(deny_unknown_fields)]
859pub struct GithubRunIdentityV1 {
860    /// Stable numeric run identifier.
861    pub run_id: u64,
862    /// Stable numeric workflow identifier.
863    pub workflow_id: u64,
864    /// Exact repository-relative workflow path.
865    pub workflow_path: String,
866    /// Exact dispatched commit.
867    pub head_sha: String,
868    /// Exact temporary branch without `refs/heads/`.
869    pub branch: String,
870    /// Exact trigger event.
871    pub event: GithubRunEventV1,
872    /// Repository-local run number.
873    pub run_number: u64,
874    /// Exact run attempt observed by the provider.
875    pub run_attempt: u64,
876    /// Last identity-checked status.
877    pub status: GithubRunStatusV1,
878    /// Terminal conclusion, only when status is completed.
879    #[serde(skip_serializing_if = "Option::is_none")]
880    pub conclusion: Option<GithubRunConclusionV1>,
881}
882
883/// Secret-free proof that one exact signed GitHub run attempt completed protected cleanup.
884///
885/// The production verifier creates this envelope only after independently validating the compile
886/// handoff, worker manifest, signing report, validation report, and every protected-cleanup flag.
887/// It is scoped to the immutable provider-bound attempt; a later GitHub rerun is a distinct
888/// attempt and neither replaces nor invalidates this evidence.
889#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
890#[serde(deny_unknown_fields)]
891pub struct GithubSignedCleanupEvidenceV1 {
892    /// Evidence schema version; currently `1`.
893    pub schema_version: u32,
894    /// Stable provider identifier.
895    pub provider: String,
896    /// Caller operation identifier.
897    pub operation_id: String,
898    /// Provider job identifier.
899    pub job_id: String,
900    /// Canonical GitHub execution repository URL.
901    pub execution_repository: String,
902    /// Stable database ID of the exact execution repository.
903    pub execution_repository_id: u64,
904    /// Canonical SHA-256 of the complete submitted request.
905    pub request_sha256: String,
906    /// Exact configured source repository URL.
907    pub source_repository: String,
908    /// Exact requested source revision.
909    pub source_revision: String,
910    /// Exact verified source-manifest SHA-256.
911    pub source_sha256: String,
912    /// Exact dispatched commit.
913    pub dispatch_revision: String,
914    /// Full immutable GitHub run identity and attempt that emitted the evidence.
915    pub run: GithubRunIdentityV1,
916    /// SHA-256 of canonical serialized compile-phase evidence.
917    pub compile_evidence_sha256: String,
918    /// SHA-256 of the exact immutable worker manifest bytes.
919    pub manifest_sha256: String,
920    /// SHA-256 of the exact protected signing-report bytes.
921    pub signing_report_sha256: String,
922    /// SHA-256 of the exact independently generated validation-report bytes.
923    pub validation_report_sha256: String,
924    /// Stable numeric ID of the exact final GitHub Actions artifact.
925    pub github_artifact_id: u64,
926    /// GitHub API SHA-256 of the exact final outer artifact ZIP.
927    pub github_artifact_api_sha256: String,
928}
929
930/// Durable mutation phase for one exact Git snapshot.
931#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
932#[serde(rename_all = "snake_case")]
933pub enum GithubGitSnapshotPhaseV1 {
934    /// Locator, consent, stage, and graph are durable; no object or ref mutation followed yet.
935    Prepared,
936    /// Local keepalive creation intent is durable; object import may have completed.
937    KeepaliveIntent,
938    /// The exact local keepalive was confirmed present.
939    KeepaliveExact,
940    /// Exact stage deletion intent is durable.
941    StageDeleteIntent,
942    /// The bounded stage was deleted after keepalive confirmation.
943    StageDeleted,
944    /// Create-only public source-ref mutation intent is durable.
945    SourcePublishIntent,
946    /// The exact custom source ref was confirmed at the staged commit.
947    SourceExact,
948    /// Bounded quiescent reconciliation proved the attempted source ref absent.
949    SourceAbsent,
950    /// Exact custom-source ownership was contradicted; the job is failed closed.
951    SourceConflict,
952    /// Terminal exact source-ref deletion intent is durable.
953    SourceCleanupIntent,
954    /// The exact custom source ref is confirmed absent after terminal cleanup.
955    SourceDeleted,
956    /// Complete-lineage prune durably authorized exact local keepalive release.
957    KeepaliveReleaseIntent,
958    /// The exact local keepalive is confirmed absent; no Git GC was requested.
959    KeepaliveReleased,
960}
961
962/// Versioned, path-free, secret-free resume state for one Git snapshot.
963#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
964#[serde(deny_unknown_fields)]
965pub struct GithubGitSnapshotResumeV1 {
966    /// Snapshot resume schema version; currently `1`.
967    pub schema_version: u32,
968    /// Exact path-free private-stage locator checkpointed before import.
969    pub stage_locator: GitSnapshotStageLocatorV1,
970    /// Immutable canonical stage summary, including consent, archive, refs, and six-object graph.
971    pub stage: GitSnapshotStageV1,
972    /// Current exact mutation phase.
973    pub phase: GithubGitSnapshotPhaseV1,
974    /// Number of source-ref pushes armed after durable intent.
975    pub source_publication_attempts: u8,
976    /// Most recent source push-boundary timestamp; zero before the first armed attempt.
977    pub source_publication_started_at_ms: u64,
978    /// Earliest safe absent-ref retry time; `u64::MAX` while an intent is not armed.
979    pub source_publication_quiescence_deadline_ms: u64,
980    /// Source publication held a process lease and its Git child tree was parent-fenced.
981    pub source_publication_process_fenced: bool,
982    /// Digest of the exact local source-publication lease scope.
983    #[serde(default, skip_serializing_if = "Option::is_none")]
984    pub source_publication_lease_scope_sha256: Option<String>,
985    /// Capped independent exact source-ref absence observations.
986    pub source_publication_absence_observations: u8,
987    /// Timestamp of the first exact source-ref absence observation; zero before observation.
988    pub source_publication_absence_first_observed_at_ms: u64,
989    /// Timestamp of the latest independent exact source-ref absence observation.
990    pub source_publication_absence_last_observed_at_ms: u64,
991    /// Stable complete-lineage authorization authorizing local keepalive release.
992    #[serde(default, skip_serializing_if = "Option::is_none")]
993    pub keepalive_release_authorization_sha256: Option<String>,
994}
995
996/// Versioned, secret-free durable state for one GitHub provider job.
997#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
998#[serde(deny_unknown_fields)]
999pub struct GithubWorkflowDispatchReceiptV1 {
1000    /// Positive workflow-run identifier returned or uniquely reconciled.
1001    pub run_id: u64,
1002    /// Stable active workflow identifier used by the exact dispatch request.
1003    pub workflow_id: u64,
1004    /// Exact repository-relative workflow path.
1005    pub workflow_path: String,
1006    /// Exact temporary branch without `refs/heads/`.
1007    pub branch: String,
1008    /// Exact temporary-ref commit dispatched as `GITHUB_SHA`.
1009    pub dispatch_revision: String,
1010    /// Canonical public run title binding all four dispatch inputs.
1011    pub run_name: String,
1012}
1013
1014/// Durable no-resend boundary for one exact no-secret workflow dispatch.
1015#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1016#[serde(deny_unknown_fields)]
1017pub struct GithubWorkflowDispatchResumeV1 {
1018    /// Dispatch-resume schema version; currently `1`.
1019    pub schema_version: u32,
1020    /// Stable active workflow identifier selected before the intent checkpoint.
1021    pub workflow_id: u64,
1022    /// Exact repository-relative workflow path.
1023    pub workflow_path: String,
1024    /// Exact temporary branch without `refs/heads/`.
1025    pub branch: String,
1026    /// Caller operation identifier copied into the dispatch input set.
1027    pub operation_id: String,
1028    /// Canonical build-request SHA-256 copied into the dispatch input set.
1029    pub request_sha256: String,
1030    /// Exact trusted source commit copied into the dispatch input set.
1031    pub source_revision: String,
1032    /// Exact temporary-ref commit copied into the dispatch input set.
1033    pub dispatch_revision: String,
1034    /// SHA-256 of the complete canonical no-secret dispatch request body.
1035    pub body_sha256: String,
1036    /// Canonical public run title binding all four dispatch inputs.
1037    pub run_name: String,
1038    /// The POST may have completed without a durably mapped receipt.
1039    pub uncertain: bool,
1040    /// Exact run-ID receipt, persisted before ordinary polling begins.
1041    #[serde(default, skip_serializing_if = "Option::is_none")]
1042    pub receipt: Option<GithubWorkflowDispatchReceiptV1>,
1043}
1044
1045/// Versioned, secret-free durable state for one GitHub provider job.
1046#[allow(clippy::struct_excessive_bools)]
1047#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1048#[serde(deny_unknown_fields)]
1049pub struct GithubJobResumeV1 {
1050    /// Resume-record schema version; currently `1`.
1051    pub schema_version: u32,
1052    /// Stable provider identifier.
1053    pub provider: String,
1054    /// Digest of security-relevant provider configuration.
1055    pub provider_config_sha256: String,
1056    /// Authenticated principal kind and identity.
1057    pub principal: GithubPrincipalIdentityV1,
1058    /// Exact execution repository URL.
1059    pub execution_repository: String,
1060    /// Stable GitHub database ID of the exact execution repository.
1061    pub execution_repository_id: u64,
1062    /// Exact source repository URL.
1063    pub source_repository: String,
1064    /// Exact trusted source ref.
1065    pub trusted_source_ref: String,
1066    /// Exact approved workflow path.
1067    pub workflow_path: String,
1068    /// Exact approved workflow digest.
1069    pub workflow_sha256: String,
1070    /// Exact full provider-owned temporary ref.
1071    pub temporary_ref: String,
1072    /// Caller operation identifier.
1073    pub operation_id: String,
1074    /// Provider job identifier.
1075    pub job_id: String,
1076    /// Complete declarative request; contains references, never secret values.
1077    pub request: IosDeviceBuildRequest,
1078    /// Canonical wire-request SHA-256.
1079    pub request_sha256: String,
1080    /// Exact requested source commit.
1081    pub source_revision: String,
1082    /// Exact Git snapshot stage/ref checkpoint, absent for formal legacy Git jobs.
1083    #[serde(default, skip_serializing_if = "Option::is_none")]
1084    pub git_snapshot: Option<GithubGitSnapshotResumeV1>,
1085    /// Deterministic dispatch commit prepared after read-only trust preflight and before mutation.
1086    #[serde(skip_serializing_if = "Option::is_none")]
1087    pub prepared_dispatch_commit: Option<String>,
1088    /// Exact published dispatch commit when publication was observed.
1089    #[serde(skip_serializing_if = "Option::is_none")]
1090    pub dispatch_commit: Option<String>,
1091    /// Explicit workflow-dispatch intent and optional exact run receipt.
1092    ///
1093    /// Absence is canonical for every legacy or Push-triggered record.
1094    #[serde(default, skip_serializing_if = "Option::is_none")]
1095    pub workflow_dispatch: Option<Box<GithubWorkflowDispatchResumeV1>>,
1096    /// Last exact run identity and attempt observed through GET revalidation.
1097    #[serde(skip_serializing_if = "Option::is_none")]
1098    pub run: Option<GithubRunIdentityV1>,
1099    /// Provider creation timestamp.
1100    pub created_at_ms: u64,
1101    /// Durable push-boundary timestamp; zero means publication was prepared but never armed.
1102    pub publication_started_at_ms: u64,
1103    /// Earliest safe absence-retirement time; `u64::MAX` while the boundary is unarmed.
1104    pub publication_quiescence_deadline_ms: u64,
1105    /// Current protocol-v1 job state.
1106    pub state: JobState,
1107    /// A durable checkpoint was written before temporary-ref publication began.
1108    pub publication_intent: bool,
1109    /// Publication may have completed without a conclusive response.
1110    pub publication_uncertain: bool,
1111    /// Strong external evidence proved publication absent; ref absence alone is insufficient.
1112    pub publication_absent: bool,
1113    /// This provider invocation ended before any remote mutation was attempted.
1114    ///
1115    /// This does not assert that the deterministic ref name is globally absent or provider-owned.
1116    pub publication_not_attempted: bool,
1117    /// Publication held an exclusive process lease and its child tree was parent-lifetime fenced.
1118    pub publication_process_fenced: bool,
1119    /// Digest of the canonical local lease directory; absent when leasing is unsupported.
1120    #[serde(skip_serializing_if = "Option::is_none")]
1121    pub publication_lease_scope_sha256: Option<String>,
1122    /// Capped independent observations that both the exact ref and exact run history were absent.
1123    pub publication_absence_observations: u8,
1124    /// Timestamp of the first exact ref-and-run absence observation; zero before any observation.
1125    pub publication_absence_first_observed_at_ms: u64,
1126    /// Cancellation intent was accepted locally.
1127    pub cancellation_requested: bool,
1128    /// Run-ID-wide cancellation was accepted by GitHub after exact identity/attempt revalidation.
1129    pub cancellation_dispatched: bool,
1130    /// Cleanup intent was accepted locally.
1131    pub cleanup_requested: bool,
1132    /// Remote artifact removal was requested.
1133    pub remove_artifacts_requested: bool,
1134    /// Exact-run artifact removal completed.
1135    pub artifacts_removed: bool,
1136    /// Exact lease-protected temporary-ref deletion completed.
1137    pub temporary_ref_deleted: bool,
1138    /// The verification-pending event has already been emitted.
1139    pub verification_pending_event: bool,
1140    /// Number of bounded exact-run discovery attempts already made.
1141    pub run_discovery_attempts: u16,
1142    /// Absolute discovery deadline in Unix milliseconds.
1143    pub run_discovery_deadline_ms: u64,
1144    /// Independently verified artifact manifests retained by the provider.
1145    pub manifests: Vec<ArtifactManifest>,
1146    /// Exact credential-free compile evidence retained after independent artifact verification.
1147    #[serde(skip_serializing_if = "Option::is_none")]
1148    pub compile_evidence: Option<CompilePhaseEvidence>,
1149    /// Attempt-scoped proof of protected signing-material cleanup, for signed builds only.
1150    #[serde(skip_serializing_if = "Option::is_none")]
1151    pub signed_cleanup_evidence: Option<GithubSignedCleanupEvidenceV1>,
1152    /// Bounded validated protocol events retained by the provider.
1153    pub events: Vec<RemoteBuildEvent>,
1154}
1155
1156/// Injectable durable checkpoint boundary.
1157///
1158/// Each callback is a complete atomic replacement for the same job ID. Implementations must reject
1159/// stale replacements with [`GithubJobResumeV1::validate_successor`] and complete the durable write
1160/// before returning success. The callback is invoked while provider state is locked and therefore
1161/// must not call back into the provider. Any error is treated as an ambiguous write acknowledgement:
1162/// the provider retains the matching live intent and performs no following remote mutation. Durable
1163/// job IDs are not implicitly retired or recycled.
1164pub trait GithubJobCheckpointSink: Send {
1165    /// Persist one complete secret-free job record.
1166    ///
1167    /// # Errors
1168    ///
1169    /// Must return a typed failure when durable persistence cannot be proven.
1170    fn checkpoint(&mut self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()>;
1171}
1172
1173/// Strict immutable request committed to the temporary dispatch ref.
1174#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1175#[serde(deny_unknown_fields)]
1176pub struct GithubDispatchManifest {
1177    /// Manifest schema version.
1178    pub schema_version: u32,
1179    /// Stable provider identifier.
1180    pub provider: String,
1181    /// Exact GitHub Actions execution repository.
1182    pub execution_repository: String,
1183    /// Exact normalized GitHub source repository.
1184    pub source_repository: String,
1185    /// Exact trusted branch or tag containing the source revision.
1186    pub trusted_source_ref: String,
1187    /// Exact provider-created full temporary branch ref.
1188    pub temporary_ref: String,
1189    /// Repository-relative generated workflow path.
1190    pub workflow_path: String,
1191    /// Approved SHA-256 of the generated workflow bytes.
1192    pub workflow_sha256: String,
1193    /// Complete declarative physical-iPhone build request.
1194    pub request: IosDeviceBuildRequest,
1195}
1196
1197impl GithubDispatchManifest {
1198    fn new(
1199        config: &GithubProviderConfig,
1200        temporary_ref: &TemporaryGitRef,
1201        request: IosDeviceBuildRequest,
1202    ) -> Self {
1203        Self {
1204            schema_version: DISPATCH_MANIFEST_SCHEMA_VERSION,
1205            provider: GITHUB_PROVIDER_ID.to_owned(),
1206            execution_repository: repository_url(&config.repository),
1207            source_repository: config.source_repository.clone(),
1208            trusted_source_ref: config.workflow.trusted_source_ref().as_str().to_owned(),
1209            temporary_ref: format!("refs/heads/{}", temporary_ref.branch().as_str()),
1210            workflow_path: config.workflow.filename().repository_path(),
1211            workflow_sha256: config.workflow_fingerprint.as_str().to_owned(),
1212            request,
1213        }
1214    }
1215
1216    /// Validate all duplicated identity fields and the nested build request.
1217    ///
1218    /// # Errors
1219    ///
1220    /// Returns a typed protocol failure when any identity binding differs.
1221    pub fn validate_for(&self, config: &GithubProviderConfig) -> RemoteBuildResult<()> {
1222        if self.schema_version != DISPATCH_MANIFEST_SCHEMA_VERSION
1223            || self.provider != GITHUB_PROVIDER_ID
1224            || !github_remote_matches(
1225                &self.execution_repository,
1226                &repository_url(&config.repository),
1227            )
1228            || self.source_repository != config.source_repository
1229            || self.trusted_source_ref != config.workflow.trusted_source_ref().as_str()
1230            || self.workflow_path != config.workflow.filename().repository_path()
1231            || self.workflow_sha256 != config.workflow_fingerprint.as_str()
1232            || self.temporary_ref
1233                != format!(
1234                    "refs/heads/{}/{}",
1235                    config.workflow.temporary_branch_namespace().as_str(),
1236                    self.request.operation_id
1237                )
1238        {
1239            return Err(provider_failure(
1240                "dispatch_manifest_identity_mismatch",
1241                "dispatch manifest identity does not match provider configuration",
1242                false,
1243            ));
1244        }
1245        validate_provider_request(config, &self.request)
1246    }
1247
1248    fn encode(&self) -> RemoteBuildResult<Vec<u8>> {
1249        let mut bytes =
1250            serde_json::to_vec_pretty(self).map_err(|error| RemoteBuildError::Serialization {
1251                message: bounded_message(&error.to_string()),
1252            })?;
1253        bytes.push(b'\n');
1254        if bytes.len() > MAX_DISPATCH_FILE_BYTES {
1255            return Err(provider_failure(
1256                "dispatch_manifest_too_large",
1257                "dispatch manifest exceeds provider size policy",
1258                false,
1259            ));
1260        }
1261        Ok(bytes)
1262    }
1263}
1264
1265/// Exact immutable inputs for reopening and importing one consented Git snapshot stage.
1266pub struct GitSnapshotImportRequest<'a> {
1267    /// Durable path-free stage locator checkpointed before any object import.
1268    pub locator: &'a GitSnapshotStageLocatorV1,
1269    /// Exact immutable stage summary independently bound into the durable resume record.
1270    pub expected_stage: &'a GitSnapshotStageV1,
1271    /// Complete durable request whose revision must equal the staged parentless commit.
1272    pub request: &'a IosDeviceBuildRequest,
1273}
1274
1275/// Exact private-DB inputs for no-write planning of one operation-bound child retry.
1276pub struct GitSnapshotExactRetryPrecomputeRequest<'a> {
1277    /// Terminal parent request retained in the durable job.
1278    pub parent_request: &'a IosDeviceBuildRequest,
1279    /// Exact parent stage containing archive blob, manifest, and keepalive identity.
1280    pub parent_stage: &'a GitSnapshotStageV1,
1281    /// New operation identifier proposed for the immutable child lineage.
1282    pub child_operation_id: &'a str,
1283    /// Deterministic child snapshot commit timestamp.
1284    pub child_source_created_at_ms: u64,
1285}
1286
1287/// Exact private-DB inputs for materializing one operation-bound child retry stage.
1288pub struct GitSnapshotExactRetryStageRequest<'a> {
1289    /// Terminal parent request retained in the durable job.
1290    pub parent_request: &'a IosDeviceBuildRequest,
1291    /// Exact parent stage containing archive blob, manifest, and keepalive identity.
1292    pub parent_stage: &'a GitSnapshotStageV1,
1293    /// Exact no-write plan already persisted into the immutable child lineage.
1294    pub expected_plan: &'a GithubGitSnapshotExactRetryPlanV1,
1295    /// Stable digest proving controller-held durable retry-lineage authority.
1296    pub retry_lineage_authorization_sha256: &'a str,
1297}
1298
1299/// Exact inputs for one local create-only keepalive ref mutation.
1300pub struct GitSnapshotKeepaliveRequest<'a> {
1301    /// Caller operation identifier.
1302    pub operation_id: &'a str,
1303    /// Exact local keepalive ref derived from the operation.
1304    pub keepalive_ref: &'a GitSnapshotKeepaliveRef,
1305    /// Exact staged parentless commit.
1306    pub commit: &'a GitSha1ObjectId,
1307}
1308
1309/// Exact inputs for one create-only source-ref publication.
1310pub struct GitSnapshotSourcePublishRequest<'a> {
1311    /// Configured public source repository.
1312    pub source_repository: &'a str,
1313    /// Exact custom full ref derived from the operation.
1314    pub source_ref: &'a GitSnapshotSourceRef,
1315    /// Exact local parentless snapshot commit.
1316    pub commit: &'a GitSha1ObjectId,
1317    /// Explicit consent-derived authority for this public mutation.
1318    pub authorized: bool,
1319}
1320
1321/// Exact inputs for lease-protected deletion of one provider-owned source ref.
1322pub struct GitSnapshotSourceDeleteRequest<'a> {
1323    /// Configured public source repository.
1324    pub source_repository: &'a str,
1325    /// Exact provider-owned custom full ref.
1326    pub source_ref: &'a GitSnapshotSourceRef,
1327    /// Only remote tip permitted to be deleted.
1328    pub expected_commit: &'a GitSha1ObjectId,
1329    /// Durable terminal cleanup authority.
1330    pub authorized: bool,
1331}
1332
1333/// Read-only result for one exact local or remote Git snapshot ref.
1334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1335pub enum GitSnapshotRefReconciliation {
1336    /// The exact ref is absent.
1337    Absent,
1338    /// The exact ref resolves to the expected snapshot commit.
1339    Exact,
1340    /// The ref exists at another commit and must not be adopted or deleted.
1341    Conflict,
1342}
1343
1344/// Result of a create-only source publication whose process response may be ambiguous.
1345#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1346pub enum GitSnapshotSourcePublication {
1347    /// Read-back proves the custom source ref points at the exact commit.
1348    Exact,
1349    /// The push may have mutated GitHub but exact read-back was unavailable or absent.
1350    Uncertain,
1351    /// A different commit occupies the exact custom ref.
1352    Conflict,
1353}
1354
1355/// Read-only inputs supplied to one temporary-ref publication.
1356pub struct TemporaryRefPublishRequest<'a> {
1357    /// Exact GitHub repository.
1358    pub repository: &'a Repository,
1359    /// Normalized HTTPS repository URL.
1360    pub source_repository: &'a str,
1361    /// Trusted branch or tag containing the source revision.
1362    pub trusted_source_ref: &'a TrustedSourceRef,
1363    /// Exact source commit selected by the caller.
1364    pub source_revision: &'a CommitSha,
1365    /// Exact custom source ref for a Git snapshot; absent for ordinary Git requests.
1366    pub snapshot_source_ref: Option<&'a GitSnapshotSourceRef>,
1367    /// Create-only target below the configured temporary namespace.
1368    pub temporary_ref: &'a TemporaryGitRef,
1369    /// Repository-relative approved workflow path.
1370    pub workflow_path: &'a str,
1371    /// Approved workflow bytes.
1372    pub workflow_bytes: &'a [u8],
1373    /// Approved workflow digest.
1374    pub workflow_fingerprint: &'a WorkflowFingerprint,
1375    /// Strict request-manifest bytes.
1376    pub manifest_bytes: &'a [u8],
1377    /// Safe operation identifier used for local isolation.
1378    pub operation_id: &'a str,
1379    /// Milliseconds since Unix epoch, used for deterministic commit metadata.
1380    pub created_at_ms: u64,
1381    /// Explicit caller authorization for this push.
1382    pub authorized: bool,
1383}
1384
1385/// Verified publisher readiness for the exact configured remote and trusted ref.
1386#[derive(Clone, Debug, Eq, PartialEq)]
1387pub struct TemporaryRefPublisherReadiness {
1388    /// Remote tip currently advertised for the trusted ref.
1389    pub trusted_ref_tip: CommitSha,
1390}
1391
1392/// Exact immutable inputs for local trusted-workflow bootstrap verification.
1393pub struct TemporaryRefDoctorRequest<'a> {
1394    /// Exact GitHub repository.
1395    pub repository: &'a Repository,
1396    /// Normalized HTTPS repository URL.
1397    pub source_repository: &'a str,
1398    /// Trusted source branch or tag.
1399    pub trusted_source_ref: &'a TrustedSourceRef,
1400    /// Exact configured namespace used by real provider publications.
1401    pub temporary_branch_namespace: &'a TemporaryBranchNamespace,
1402    /// Repository-relative approved workflow path.
1403    pub workflow_path: &'a str,
1404    /// Approved workflow SHA-256.
1405    pub workflow_fingerprint: &'a WorkflowFingerprint,
1406}
1407
1408/// Exact immutable inputs for execution-default workflow verification.
1409pub struct ExecutionWorkflowDoctorRequest<'a> {
1410    /// Exact execution repository.
1411    pub repository: &'a Repository,
1412    /// Exact default branch ref reported by GitHub metadata.
1413    pub default_branch_ref: &'a TrustedSourceRef,
1414    /// Repository-relative approved workflow path.
1415    pub workflow_path: &'a str,
1416    /// Approved `WorkflowDispatch` workflow SHA-256.
1417    pub workflow_fingerprint: &'a WorkflowFingerprint,
1418}
1419
1420/// Exact inputs for one lease-protected temporary-ref deletion.
1421pub struct TemporaryRefDeleteRequest<'a> {
1422    /// Exact GitHub repository.
1423    pub repository: &'a Repository,
1424    /// Normalized HTTPS repository URL.
1425    pub source_repository: &'a str,
1426    /// Provider-created full temporary branch.
1427    pub temporary_ref: &'a TemporaryGitRef,
1428    /// Only remote tip permitted to be deleted.
1429    pub expected_commit: &'a CommitSha,
1430    /// Explicit caller authorization for this deletion.
1431    pub authorized: bool,
1432}
1433
1434/// Exact immutable inputs for read-only crash reconciliation of one temporary ref.
1435pub struct TemporaryRefReconcileRequest<'a> {
1436    /// Exact GitHub execution repository.
1437    pub repository: &'a Repository,
1438    /// Normalized source repository used for trusted-ref ancestry verification.
1439    pub source_repository: &'a str,
1440    /// Trusted source branch or tag whose workflow and ancestry must be revalidated.
1441    pub trusted_source_ref: &'a TrustedSourceRef,
1442    /// Exact requested source revision that must remain reachable from the trusted ref.
1443    pub source_revision: &'a CommitSha,
1444    /// Exact custom Git snapshot source ref; absent for ordinary trusted Git source.
1445    pub snapshot_source_ref: Option<&'a GitSnapshotSourceRef>,
1446    /// Provider-created temporary ref to inspect.
1447    pub temporary_ref: &'a TemporaryGitRef,
1448    /// Repository-relative approved workflow path.
1449    pub workflow_path: &'a str,
1450    /// Exact approved workflow bytes.
1451    pub workflow_bytes: &'a [u8],
1452    /// Approved digest of the trusted workflow bytes.
1453    pub workflow_fingerprint: &'a WorkflowFingerprint,
1454    /// Exact strict request-manifest bytes.
1455    pub manifest_bytes: &'a [u8],
1456    /// Stable operation identifier used to reconstruct the deterministic commit.
1457    pub operation_id: &'a str,
1458    /// Original provider creation timestamp used in deterministic Git author metadata.
1459    pub created_at_ms: u64,
1460    /// Exact dispatch commit prepared and checkpointed before the remote mutation boundary.
1461    pub expected_prepared_commit: &'a CommitSha,
1462}
1463
1464/// Deterministic local ownership key held across one publication or crash takeover.
1465pub struct TemporaryRefPublicationLeaseRequest<'a> {
1466    /// Exact GitHub execution repository bound into the lease key.
1467    pub repository: &'a Repository,
1468    /// Safe operation identifier used for the lease filename.
1469    pub operation_id: &'a str,
1470    /// Exact provider-owned temporary ref protected by the lease.
1471    pub temporary_ref: &'a TemporaryGitRef,
1472}
1473
1474/// Exact cross-process ownership key for one custom Git snapshot source ref.
1475pub struct GitSnapshotSourcePublicationLeaseRequest<'a> {
1476    /// Canonical configured public source repository.
1477    pub source_repository: &'a str,
1478    /// Exact provider-owned custom source ref.
1479    pub source_ref: &'a GitSnapshotSourceRef,
1480}
1481
1482/// Result of a nonblocking cross-process publication-ownership attempt.
1483#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1484pub enum TemporaryRefPublicationLease {
1485    /// This publisher acquired the process-lifetime lease and retains it until explicit release
1486    /// or publisher drop.
1487    Acquired,
1488    /// This publisher instance already retains the lease.
1489    AlreadyOwned,
1490    /// Another live process owns the lease.
1491    HeldByOther,
1492    /// This alternate publisher cannot provide a process-lifetime lease.
1493    Unsupported,
1494}
1495
1496/// Read-only reconciliation result for one deterministic provider ref.
1497#[derive(Clone, Debug, Eq, PartialEq)]
1498pub enum TemporaryRefReconciliation {
1499    /// The exact ref is absent; callers must not silently resubmit the old operation.
1500    Missing,
1501    /// The advertised tip equals the locally recomputed, durably prepared dispatch commit.
1502    Exact(PublishedTemporaryRef),
1503    /// The ref exists but its commit shape or bytes differ; ownership is not adopted.
1504    Conflict,
1505}
1506
1507/// Provider-level result of reconciling a durable pre-publication intent.
1508#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1509pub enum GithubJobReconciliation {
1510    /// No publication had been intended.
1511    NotStarted,
1512    /// A dispatch commit was already durably mapped.
1513    AlreadyMapped,
1514    /// The exact temporary ref is absent; no duplicate submission was attempted.
1515    Missing,
1516    /// Exact remote bytes were adopted and run discovery may continue.
1517    Recovered,
1518    /// A conflicting ref was found and the job failed closed.
1519    Conflict,
1520    /// A conflicting ref and exact-SHA run history remained unresolved through the fenced,
1521    /// bounded quiescence window; the durable terminal checkpoint was written.
1522    ConflictResolved,
1523}
1524
1525/// Result of one create-only temporary-ref publication.
1526#[derive(Clone, Debug, Eq, PartialEq)]
1527pub struct PublishedTemporaryRef {
1528    temporary_ref: TemporaryGitRef,
1529    commit: CommitSha,
1530}
1531
1532impl PublishedTemporaryRef {
1533    /// Construct publication evidence returned by an alternate publisher.
1534    pub fn new(temporary_ref: TemporaryGitRef, commit: CommitSha) -> Self {
1535        Self {
1536            temporary_ref,
1537            commit,
1538        }
1539    }
1540
1541    /// Exact temporary branch.
1542    pub fn temporary_ref(&self) -> &TemporaryGitRef {
1543        &self.temporary_ref
1544    }
1545
1546    /// Exact published dispatch commit.
1547    pub fn commit(&self) -> &CommitSha {
1548        &self.commit
1549    }
1550}
1551
1552/// Redacted temporary-ref publication failure.
1553#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1554pub enum TemporaryRefPublishError {
1555    /// Caller did not explicitly authorize a temporary-ref mutation.
1556    Unauthorized,
1557    /// Local repository remote differs from the configured GitHub repository.
1558    RemoteMismatch,
1559    /// Trusted source ref was absent or malformed.
1560    TrustedRefUnavailable,
1561    /// Requested source commit was not reachable from the trusted ref.
1562    SourceRevisionNotTrusted,
1563    /// Exact temporary branch already existed remotely.
1564    TemporaryRefExists,
1565    /// Workflow bytes differed from the approved fingerprint.
1566    WorkflowFingerprintMismatch,
1567    /// A bounded local isolation file could not be reserved or removed.
1568    LocalIsolationFailed,
1569    /// Git failed without exposing its output.
1570    Git(GitExecutionError),
1571    /// Git returned malformed object or ref metadata.
1572    MalformedGitOutput,
1573    /// Push completed without exact remote-ref confirmation.
1574    PublicationVerificationFailed,
1575    /// Atomic deletion lease was rejected because the remote ref changed.
1576    TemporaryRefLeaseRejected,
1577    /// Lease-protected deletion completed without exact absence confirmation.
1578    DeletionVerificationFailed,
1579    /// The selected publisher does not implement the Git snapshot contract.
1580    GitSnapshotUnsupported,
1581    /// The selected publisher cannot verify execution-default workflow bytes.
1582    ExecutionWorkflowVerificationUnsupported,
1583    /// A retained stage, canonical graph, or local snapshot ref failed exact validation.
1584    GitSnapshotInvalid,
1585    /// Snapshot-store membership changed during one bounded recovery discovery.
1586    GitSnapshotDiscoveryChanged,
1587    /// The exact local or remote snapshot ref is occupied by another commit.
1588    GitSnapshotRefConflict,
1589}
1590
1591impl fmt::Display for TemporaryRefPublishError {
1592    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1593        match self {
1594            Self::Unauthorized => formatter.write_str("temporary-ref mutation is not authorized"),
1595            Self::RemoteMismatch => formatter.write_str("Git remote does not match configuration"),
1596            Self::TrustedRefUnavailable => formatter.write_str("trusted source ref is unavailable"),
1597            Self::SourceRevisionNotTrusted => {
1598                formatter.write_str("source revision is not reachable from trusted source ref")
1599            }
1600            Self::TemporaryRefExists => formatter.write_str("temporary ref already exists"),
1601            Self::WorkflowFingerprintMismatch => {
1602                formatter.write_str("workflow bytes do not match approved fingerprint")
1603            }
1604            Self::LocalIsolationFailed => formatter.write_str("local Git isolation failed"),
1605            Self::Git(error) => error.fmt(formatter),
1606            Self::MalformedGitOutput => formatter.write_str("Git returned malformed metadata"),
1607            Self::PublicationVerificationFailed => {
1608                formatter.write_str("published ref failed exact verification")
1609            }
1610            Self::TemporaryRefLeaseRejected => {
1611                formatter.write_str("temporary ref changed before lease-protected deletion")
1612            }
1613            Self::DeletionVerificationFailed => {
1614                formatter.write_str("deleted ref failed exact absence verification")
1615            }
1616            Self::GitSnapshotUnsupported => {
1617                formatter.write_str("Git snapshot publication is unsupported")
1618            }
1619            Self::ExecutionWorkflowVerificationUnsupported => {
1620                formatter.write_str("execution workflow verification is unsupported")
1621            }
1622            Self::GitSnapshotInvalid => {
1623                formatter.write_str("Git snapshot stage or object graph is invalid")
1624            }
1625            Self::GitSnapshotDiscoveryChanged => {
1626                formatter.write_str("Git snapshot recovery discovery changed during inspection")
1627            }
1628            Self::GitSnapshotRefConflict => {
1629                formatter.write_str("Git snapshot ref conflicts with another commit")
1630            }
1631        }
1632    }
1633}
1634
1635impl Error for TemporaryRefPublishError {}
1636
1637impl From<GitExecutionError> for TemporaryRefPublishError {
1638    fn from(value: GitExecutionError) -> Self {
1639        Self::Git(value)
1640    }
1641}
1642
1643impl From<GitSnapshotError> for TemporaryRefPublishError {
1644    fn from(value: GitSnapshotError) -> Self {
1645        if value == GitSnapshotError::DiscoveryChanged {
1646            Self::GitSnapshotDiscoveryChanged
1647        } else {
1648            Self::GitSnapshotInvalid
1649        }
1650    }
1651}
1652
1653/// Publication error plus exact cleanup ownership when a push may have succeeded.
1654#[derive(Clone, Debug, Eq, PartialEq)]
1655pub struct TemporaryRefPublishFailure {
1656    error: TemporaryRefPublishError,
1657    possible_publication: Option<PublishedTemporaryRef>,
1658}
1659
1660impl TemporaryRefPublishFailure {
1661    fn uncertain(error: TemporaryRefPublishError, publication: PublishedTemporaryRef) -> Self {
1662        Self {
1663            error,
1664            possible_publication: Some(publication),
1665        }
1666    }
1667
1668    /// Redacted failure category.
1669    pub const fn error(&self) -> TemporaryRefPublishError {
1670        self.error
1671    }
1672
1673    /// Exact ref and commit that may now exist remotely.
1674    pub fn possible_publication(&self) -> Option<&PublishedTemporaryRef> {
1675        self.possible_publication.as_ref()
1676    }
1677}
1678
1679impl From<TemporaryRefPublishError> for TemporaryRefPublishFailure {
1680    fn from(error: TemporaryRefPublishError) -> Self {
1681        Self {
1682            error,
1683            possible_publication: None,
1684        }
1685    }
1686}
1687
1688/// Injectable publication boundary used by the provider.
1689///
1690/// Implementations must not call back into the provider. Some operations are invoked while the
1691/// provider holds internal job state needed to serialize remote mutations and their checkpoints.
1692pub trait TemporaryRefPublisher: Send {
1693    /// Whether this publisher implements the complete retained-byte Git snapshot contract.
1694    fn supports_git_snapshot(&self) -> bool {
1695        false
1696    }
1697
1698    /// Verify local Git and exact remote/trusted-ref readiness without mutation.
1699    ///
1700    /// # Errors
1701    ///
1702    /// Returns a redacted readiness error when remote identity, trusted-ref availability, or
1703    /// approved workflow bytes cannot be verified exactly.
1704    fn doctor(
1705        &mut self,
1706        request: &TemporaryRefDoctorRequest<'_>,
1707    ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError>;
1708
1709    /// Verify exact workflow bytes from the execution repository default branch without mutation.
1710    ///
1711    /// # Errors
1712    ///
1713    /// Returns a redacted readiness error when the execution remote, stable default ref, or exact
1714    /// approved bytes cannot be proven.
1715    fn verify_execution_workflow(
1716        &mut self,
1717        _request: &ExecutionWorkflowDoctorRequest<'_>,
1718    ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
1719        Err(TemporaryRefPublishError::ExecutionWorkflowVerificationUnsupported)
1720    }
1721
1722    /// Hash the two retained staged payloads and canonical four-object suffix without writing.
1723    ///
1724    /// # Errors
1725    ///
1726    /// Returns a redacted fixed-Git, stage-binding, object-format, or graph failure.
1727    fn precompute_git_snapshot(
1728        &mut self,
1729        _inputs: &mut GitSnapshotPrecomputeInputs,
1730        _operation_id: &str,
1731        _created_at_ms: u64,
1732    ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
1733        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1734    }
1735
1736    /// Reopen a complete uncheckpointed stage without mutation for a caller that will perform
1737    /// fresh explicit-consent validation before creating the first durable snapshot checkpoint.
1738    ///
1739    /// # Errors
1740    ///
1741    /// Returns a redacted stage or final-request binding failure.
1742    fn adopt_git_snapshot_stage(
1743        &mut self,
1744        _request: &IosDeviceBuildRequest,
1745    ) -> Result<(GitSnapshotStageLocatorV1, GitSnapshotStageV1), TemporaryRefPublishError> {
1746        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1747    }
1748
1749    /// Enumerate complete private stages using reads only.
1750    ///
1751    /// Implementations must fail the whole bounded enumeration on any partial, malformed, unknown,
1752    /// or identity-rebound entry. Discovery never adopts or deletes a stage.
1753    ///
1754    /// # Errors
1755    ///
1756    /// Returns a redacted unsupported, recovery-changed, identity, or stage validation failure.
1757    fn discover_git_snapshot_stages(
1758        &mut self,
1759    ) -> Result<Vec<GithubGitSnapshotRecoveryCandidateV1>, TemporaryRefPublishError> {
1760        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1761    }
1762
1763    /// Reopen the exact durable stage and import all six objects while retaining its guards.
1764    ///
1765    /// The retained import remains live inside the publisher until keepalive creation or explicit
1766    /// release. Implementations must compare every actual `hash-object -w` result and revalidate
1767    /// all staged bytes after each write.
1768    ///
1769    /// # Errors
1770    ///
1771    /// Returns a redacted stage, object-format, import, or graph failure.
1772    fn import_git_snapshot(
1773        &mut self,
1774        _request: &GitSnapshotImportRequest<'_>,
1775    ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
1776        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1777    }
1778
1779    /// Re-read a terminal parent's exact archive blob and derive the immutable child request.
1780    ///
1781    /// This operation may perform bounded reads and offline no-write hashing only. It must not
1782    /// create a stage, write a Git object, mutate a ref, or access the network.
1783    ///
1784    /// # Errors
1785    ///
1786    /// Returns a redacted parent blob, SHA-1, descriptor, or graph failure.
1787    fn precompute_exact_git_snapshot_retry(
1788        &mut self,
1789        _request: &GitSnapshotExactRetryPrecomputeRequest<'_>,
1790    ) -> Result<GithubGitSnapshotExactRetryPlanV1, TemporaryRefPublishError> {
1791        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1792    }
1793
1794    /// Re-read a terminal parent's exact archive blob and create one operation-bound child stage.
1795    ///
1796    /// The caller must have durably created the parent/child lineage before this method. The
1797    /// implementation must first reproduce the already-durable no-write plan exactly. It may then
1798    /// write only the bounded private child stage; it must not recapture a workspace or mutate
1799    /// local/remote refs.
1800    ///
1801    /// # Errors
1802    ///
1803    /// Returns a redacted parent blob, stage, SHA-1, descriptor, or graph failure.
1804    fn stage_exact_git_snapshot_retry(
1805        &mut self,
1806        _request: &GitSnapshotExactRetryStageRequest<'_>,
1807    ) -> Result<GithubGitSnapshotExactRetryStageV1, TemporaryRefPublishError> {
1808        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1809    }
1810
1811    /// Delete the exact bounded stage after its keepalive is durably confirmed.
1812    ///
1813    /// Replay succeeds only when the whole stage is already absent or every remaining entry still
1814    /// has its durable identity. Partial or rebound stages fail closed.
1815    ///
1816    /// # Errors
1817    ///
1818    /// Returns a redacted identity, exact-removal, or directory-durability failure.
1819    fn delete_git_snapshot_stage(
1820        &mut self,
1821        _request: &GitSnapshotImportRequest<'_>,
1822    ) -> Result<(), TemporaryRefPublishError> {
1823        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1824    }
1825
1826    /// Reconcile one exact local keepalive ref without mutation.
1827    ///
1828    /// # Errors
1829    ///
1830    /// Returns a redacted local Git or ref-parse failure.
1831    fn reconcile_git_snapshot_keepalive(
1832        &mut self,
1833        _request: &GitSnapshotKeepaliveRequest<'_>,
1834    ) -> Result<GitSnapshotRefReconciliation, TemporaryRefPublishError> {
1835        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1836    }
1837
1838    /// Create the exact local keepalive after its durable intent checkpoint.
1839    ///
1840    /// Implementations must keep retained staged-file guards live and revalidate them immediately
1841    /// before and after the create-only ref mutation.
1842    ///
1843    /// # Errors
1844    ///
1845    /// Returns a redacted stage, local Git, or conflicting-ref failure.
1846    fn create_git_snapshot_keepalive(
1847        &mut self,
1848        _request: &GitSnapshotKeepaliveRequest<'_>,
1849    ) -> Result<(), TemporaryRefPublishError> {
1850        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1851    }
1852
1853    /// Publish one exact create-only custom source ref after durable intent.
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns a redacted authorization, endpoint, Git, or verification failure.
1858    fn publish_git_snapshot_source(
1859        &mut self,
1860        _request: &GitSnapshotSourcePublishRequest<'_>,
1861    ) -> Result<GitSnapshotSourcePublication, TemporaryRefPublishError> {
1862        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1863    }
1864
1865    /// Reconcile one exact custom source ref using reads only.
1866    ///
1867    /// # Errors
1868    ///
1869    /// Returns a redacted endpoint, Git, or ref-parse failure.
1870    fn reconcile_git_snapshot_source(
1871        &mut self,
1872        _request: &GitSnapshotSourcePublishRequest<'_>,
1873    ) -> Result<GitSnapshotRefReconciliation, TemporaryRefPublishError> {
1874        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1875    }
1876
1877    /// Delete only the exact provider-owned custom source ref.
1878    ///
1879    /// # Errors
1880    ///
1881    /// Returns a redacted authorization, lease-conflict, Git, or absence-verification failure.
1882    fn delete_git_snapshot_source(
1883        &mut self,
1884        _request: &GitSnapshotSourceDeleteRequest<'_>,
1885    ) -> Result<(), TemporaryRefPublishError> {
1886        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1887    }
1888
1889    /// Release one exact local keepalive after its durable complete-lineage prune intent.
1890    ///
1891    /// # Errors
1892    ///
1893    /// Returns a redacted local Git, conflict, or absence-verification failure.
1894    fn release_git_snapshot_keepalive(
1895        &mut self,
1896        _request: &GitSnapshotKeepaliveRequest<'_>,
1897    ) -> Result<(), TemporaryRefPublishError> {
1898        Err(TemporaryRefPublishError::GitSnapshotUnsupported)
1899    }
1900
1901    /// Complete read-only trust checks and optionally prepare the exact deterministic dispatch
1902    /// commit before the provider checkpoints mutation intent.
1903    ///
1904    /// The default preserves alternate publishers that cannot expose a prepared commit. Such
1905    /// publishers remain fail-closed, but cannot use bounded missing-ref retirement after a crash.
1906    ///
1907    /// # Errors
1908    ///
1909    /// Returns a redacted preflight or local preparation error. This method must not mutate the
1910    /// remote repository.
1911    fn prepare(
1912        &mut self,
1913        _request: &TemporaryRefPublishRequest<'_>,
1914    ) -> Result<Option<PublishedTemporaryRef>, TemporaryRefPublishError> {
1915        Ok(None)
1916    }
1917
1918    /// Whether a publication started after successful `prepare` is unable to outlive this process.
1919    ///
1920    /// Strong bounded absence retirement is disabled unless this guarantee is durable.
1921    fn publication_attempt_is_process_fenced(&self) -> bool {
1922        false
1923    }
1924
1925    /// Acquire the source-repository/custom-ref scoped mutation lease.
1926    ///
1927    /// # Errors
1928    ///
1929    /// Returns a redacted local-isolation failure.
1930    fn acquire_git_snapshot_source_lease(
1931        &mut self,
1932        _request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
1933    ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
1934        Ok(TemporaryRefPublicationLease::Unsupported)
1935    }
1936
1937    /// Release this publisher's exact source-repository/custom-ref lease, if owned.
1938    fn release_git_snapshot_source_lease(
1939        &mut self,
1940        _request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
1941    ) {
1942    }
1943
1944    /// Digest of the canonical local lease path and exact directory filesystem identity.
1945    fn publication_lease_scope_sha256(&self) -> Option<&str> {
1946        None
1947    }
1948
1949    /// Try to own the deterministic publication across checkpoints and process crashes.
1950    ///
1951    /// An `Acquired` or `AlreadyOwned` result requires the publisher to retain the underlying
1952    /// operating-system lease until `release_publication_lease` or publisher drop.
1953    ///
1954    /// # Errors
1955    ///
1956    /// Returns a redacted local-isolation failure when the lease cannot be opened safely.
1957    fn acquire_publication_lease(
1958        &mut self,
1959        _request: &TemporaryRefPublicationLeaseRequest<'_>,
1960    ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
1961        Ok(TemporaryRefPublicationLease::Unsupported)
1962    }
1963
1964    /// Release one lease owned by this publisher instance after durable resolution.
1965    fn release_publication_lease(&mut self, _request: &TemporaryRefPublicationLeaseRequest<'_>) {}
1966
1967    /// Create and push one exact temporary dispatch ref.
1968    ///
1969    /// # Errors
1970    ///
1971    /// Returns a redacted failure and, after a possibly mutating push, exact cleanup ownership.
1972    fn publish(
1973        &mut self,
1974        request: &TemporaryRefPublishRequest<'_>,
1975    ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure>;
1976
1977    /// Publish after a durable intent checkpoint, binding any prepared commit exactly.
1978    /// Publishers returning prepared evidence must override this method and validate that evidence
1979    /// before their first remote mutation; the default refuses prepared publication.
1980    ///
1981    /// # Errors
1982    ///
1983    /// Returns a redacted publication failure and exact possible cleanup ownership after mutation.
1984    fn publish_prepared(
1985        &mut self,
1986        request: &TemporaryRefPublishRequest<'_>,
1987        prepared: Option<&PublishedTemporaryRef>,
1988    ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
1989        if prepared.is_some() {
1990            return Err(TemporaryRefPublishError::PublicationVerificationFailed.into());
1991        }
1992        self.publish(request)
1993    }
1994
1995    /// Delete one provider-owned temporary ref with an exact expected-tip lease.
1996    /// Replaying the same exact deletion after a crash must succeed when the ref is already absent.
1997    ///
1998    /// # Errors
1999    ///
2000    /// Returns a redacted failure when the lease is rejected or exact deletion cannot be verified.
2001    fn delete_temporary_ref(
2002        &mut self,
2003        request: &TemporaryRefDeleteRequest<'_>,
2004    ) -> Result<(), TemporaryRefPublishError>;
2005}
2006
2007/// Optional read-only crash-reconciliation boundary for temporary-ref publishers.
2008///
2009/// Implementations must not call back into the provider. Reconciliation may run while the
2010/// provider holds the matching job state stable.
2011pub trait TemporaryRefReconciler: Send {
2012    /// Inspect one deterministic ref and adopt it only after exact prepared-commit verification.
2013    ///
2014    /// Implementations must also revalidate the trusted workflow and prove that the requested
2015    /// source revision remains an ancestor of the trusted ref, locally recompute the deterministic
2016    /// commit, and compare it with `expected_prepared_commit` before returning exact evidence.
2017    ///
2018    /// # Errors
2019    ///
2020    /// Returns a redacted transport or local Git failure. A content mismatch is returned as
2021    /// [`TemporaryRefReconciliation::Conflict`], never as trusted publication evidence.
2022    fn reconcile_temporary_ref(
2023        &mut self,
2024        request: &TemporaryRefReconcileRequest<'_>,
2025    ) -> Result<TemporaryRefReconciliation, TemporaryRefPublishError>;
2026}
2027
2028/// Fixed Git argument vector plus optional bounded stdin.
2029pub struct GitInvocation {
2030    arguments: Vec<OsString>,
2031    stdin: Vec<u8>,
2032    environment: Vec<(GitEnvironmentVariable, OsString)>,
2033    timeout: Duration,
2034    stdout_limit: usize,
2035    network: GitNetworkPolicy,
2036}
2037
2038/// Exact non-secret environment roles that Git plumbing may override after the sealed policy.
2039#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2040pub enum GitEnvironmentVariable {
2041    /// Provider-private temporary index path.
2042    IndexFile,
2043    /// Fixed dispatch-commit author name.
2044    AuthorName,
2045    /// Fixed dispatch-commit author email.
2046    AuthorEmail,
2047    /// Fixed dispatch-commit committer name.
2048    CommitterName,
2049    /// Fixed dispatch-commit committer email.
2050    CommitterEmail,
2051    /// Deterministic dispatch-commit author timestamp.
2052    AuthorDate,
2053    /// Deterministic dispatch-commit committer timestamp.
2054    CommitterDate,
2055}
2056
2057impl GitEnvironmentVariable {
2058    const fn as_str(self) -> &'static str {
2059        match self {
2060            Self::IndexFile => "GIT_INDEX_FILE",
2061            Self::AuthorName => "GIT_AUTHOR_NAME",
2062            Self::AuthorEmail => "GIT_AUTHOR_EMAIL",
2063            Self::CommitterName => "GIT_COMMITTER_NAME",
2064            Self::CommitterEmail => "GIT_COMMITTER_EMAIL",
2065            Self::AuthorDate => "GIT_AUTHOR_DATE",
2066            Self::CommitterDate => "GIT_COMMITTER_DATE",
2067        }
2068    }
2069}
2070
2071impl GitInvocation {
2072    fn new(arguments: impl IntoIterator<Item = impl Into<OsString>>, timeout: Duration) -> Self {
2073        Self {
2074            arguments: arguments.into_iter().map(Into::into).collect(),
2075            stdin: Vec::new(),
2076            environment: Vec::new(),
2077            timeout,
2078            stdout_limit: MAX_GIT_OUTPUT_BYTES,
2079            network: GitNetworkPolicy::Offline,
2080        }
2081    }
2082
2083    fn with_stdin(mut self, stdin: &[u8]) -> Self {
2084        self.stdin.extend_from_slice(stdin);
2085        self
2086    }
2087
2088    fn with_environment(
2089        mut self,
2090        variable: GitEnvironmentVariable,
2091        value: impl Into<OsString>,
2092    ) -> Self {
2093        self.environment.push((variable, value.into()));
2094        self
2095    }
2096
2097    fn with_network(mut self, endpoint: &GithubGitEndpoint) -> Self {
2098        self.network = endpoint.transport().into();
2099        self
2100    }
2101
2102    fn with_stdout_limit(mut self, limit: usize) -> Self {
2103        self.stdout_limit = limit;
2104        self
2105    }
2106
2107    /// Exact arguments after the `git` executable.
2108    pub fn arguments(&self) -> &[OsString] {
2109        &self.arguments
2110    }
2111
2112    /// Number of stdin bytes, without exposing their content.
2113    pub const fn stdin_len(&self) -> usize {
2114        self.stdin.len()
2115    }
2116
2117    /// Explicit non-secret environment overrides.
2118    pub fn environment(&self) -> &[(GitEnvironmentVariable, OsString)] {
2119        &self.environment
2120    }
2121
2122    /// Process deadline.
2123    pub const fn timeout(&self) -> Duration {
2124        self.timeout
2125    }
2126
2127    /// Maximum retained standard-output bytes.
2128    pub const fn stdout_limit(&self) -> usize {
2129        self.stdout_limit
2130    }
2131}
2132
2133impl fmt::Debug for GitInvocation {
2134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2135        formatter
2136            .debug_struct("GitInvocation")
2137            .field("arguments", &self.arguments)
2138            .field("stdin_len", &self.stdin.len())
2139            .field(
2140                "environment_names",
2141                &self
2142                    .environment
2143                    .iter()
2144                    .map(|pair| pair.0.as_str())
2145                    .collect::<Vec<_>>(),
2146            )
2147            .field("network", &self.network)
2148            .field("timeout", &self.timeout)
2149            .field("stdout_limit", &self.stdout_limit)
2150            .finish()
2151    }
2152}
2153
2154/// Redacted fixed-argv Git execution failure.
2155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2156pub enum GitExecutionError {
2157    /// Git process could not be started.
2158    SpawnFailed,
2159    /// Git process exceeded its deadline and was terminated.
2160    TimedOut,
2161    /// Process pipes or waiting failed.
2162    ProcessIo,
2163    /// Captured output exceeded its bound.
2164    OutputLimitExceeded,
2165    /// Git exited unsuccessfully; output is deliberately discarded.
2166    CommandFailed {
2167        /// Exit code when available.
2168        exit_code: Option<i32>,
2169    },
2170}
2171
2172impl fmt::Display for GitExecutionError {
2173    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2174        match self {
2175            Self::SpawnFailed => formatter.write_str("failed to start Git"),
2176            Self::TimedOut => formatter.write_str("Git operation timed out"),
2177            Self::ProcessIo => formatter.write_str("Git process I/O failed"),
2178            Self::OutputLimitExceeded => formatter.write_str("Git output exceeded its limit"),
2179            Self::CommandFailed {
2180                exit_code: Some(code),
2181            } => {
2182                write!(formatter, "Git operation failed with status {code}")
2183            }
2184            Self::CommandFailed { exit_code: None } => formatter.write_str("Git operation failed"),
2185        }
2186    }
2187}
2188
2189impl Error for GitExecutionError {}
2190
2191/// Injectable fixed-argv Git executor.
2192pub trait GitRunner: Send {
2193    /// Execute one bounded invocation.
2194    ///
2195    /// # Errors
2196    ///
2197    /// Returns a redacted process, I/O, timeout, output-limit, or exit-status failure.
2198    fn execute(&mut self, invocation: &GitInvocation) -> Result<Vec<u8>, GitExecutionError>;
2199
2200    /// Execute one bounded invocation with stdin streamed from an already-open regular file.
2201    ///
2202    /// The default refuses the operation. Production and snapshot-aware test runners must prove
2203    /// the exact expected byte count without resolving a pathname.
2204    ///
2205    /// # Errors
2206    ///
2207    /// Returns a redacted process or exact-input failure.
2208    fn execute_file(
2209        &mut self,
2210        _invocation: &GitInvocation,
2211        _input: fs::File,
2212        _expected_len: u64,
2213    ) -> Result<Vec<u8>, GitExecutionError> {
2214        Err(GitExecutionError::ProcessIo)
2215    }
2216
2217    /// Whether process trees spawned after a successful `execute` setup are killed on parent exit.
2218    ///
2219    /// Production currently proves this only on Windows. Unix therefore keeps bounded strong
2220    /// missing-publication retirement disabled.
2221    fn process_tree_is_parent_fenced(&self) -> bool {
2222        false
2223    }
2224}
2225
2226/// Production Git executor rooted at a private bare object database.
2227#[derive(Debug)]
2228pub struct GitProcessRunner {
2229    #[cfg(windows)]
2230    toolchain: crate::git_process::WindowsGitToolchain,
2231    #[cfg(windows)]
2232    repository: crate::git_repository::PrivateBareGitRepository,
2233    #[cfg(unix)]
2234    toolchain: crate::git_process::UnixGitToolchain,
2235    #[cfg(unix)]
2236    repository: crate::git_repository::PrivateBareGitRepository,
2237}
2238
2239impl GitProcessRunner {
2240    /// Validate the Git executable and initialize or reopen a private bare repository.
2241    ///
2242    /// # Errors
2243    ///
2244    /// Rejects relative, missing, linked, non-private, or unsuitable paths and tool layouts.
2245    pub fn new(
2246        executable: impl AsRef<Path>,
2247        isolation_root: impl AsRef<Path>,
2248    ) -> Result<Self, GitPublisherConfigError> {
2249        #[cfg(windows)]
2250        {
2251            let toolchain = crate::git_process::WindowsGitToolchain::new(executable)
2252                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2253            let preparation =
2254                crate::git_repository::PrivateGitRepositoryPreparation::prepare(isolation_root)
2255                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2256            if preparation.needs_initialization() {
2257                let spec = preparation
2258                    .initialization_spec(&toolchain)
2259                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2260                let mut command = spec
2261                    .command()
2262                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2263                command
2264                    .stdin(Stdio::piped())
2265                    .stdout(Stdio::piped())
2266                    .stderr(Stdio::piped());
2267                run_git_process(command, &[], MAX_GIT_TIMEOUT, MAX_GIT_OUTPUT_BYTES)
2268                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2269            }
2270            let repository = preparation
2271                .finish()
2272                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2273            Ok(Self {
2274                toolchain,
2275                repository,
2276            })
2277        }
2278
2279        #[cfg(unix)]
2280        {
2281            let toolchain = crate::git_process::UnixGitToolchain::new(executable)
2282                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2283            let preparation =
2284                crate::git_repository::PrivateGitRepositoryPreparation::prepare(isolation_root)
2285                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2286            if preparation.needs_initialization() {
2287                let spec = preparation
2288                    .initialization_spec(&toolchain)
2289                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2290                let mut command = spec
2291                    .command()
2292                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2293                command
2294                    .stdin(Stdio::piped())
2295                    .stdout(Stdio::piped())
2296                    .stderr(Stdio::piped());
2297                run_git_process(command, &[], MAX_GIT_TIMEOUT, MAX_GIT_OUTPUT_BYTES)
2298                    .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2299            }
2300            let repository = preparation
2301                .finish()
2302                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2303            Ok(Self {
2304                toolchain,
2305                repository,
2306            })
2307        }
2308
2309        #[cfg(not(any(unix, windows)))]
2310        {
2311            let _ = (executable.as_ref(), isolation_root.as_ref());
2312            Err(GitPublisherConfigError::UnsupportedSecurePlatform)
2313        }
2314    }
2315}
2316
2317/// One retained, offline-only reader for a caller checkout.
2318///
2319/// Construction resolves the fixed platform Git without `PATH`, creates private process state,
2320/// rejects local include/worktree/external-filter policy by key name, and binds one canonical
2321/// repository root. Every exposed operation is fixed read-only plumbing; callers cannot supply a
2322/// subcommand, network capability, environment override, or output path.
2323#[derive(Debug)]
2324pub struct CallerGitRepository {
2325    #[cfg(windows)]
2326    toolchain: crate::git_process::WindowsGitToolchain,
2327    #[cfg(unix)]
2328    toolchain: crate::git_process::UnixGitToolchain,
2329    root: PathBuf,
2330    git_dir: Option<PathBuf>,
2331    common_dir: Option<PathBuf>,
2332    root_retained: Option<rustferry_core::RetainedDirectoryIdentity>,
2333    git_dir_retained: Option<rustferry_core::RetainedDirectoryIdentity>,
2334    common_dir_retained: Option<rustferry_core::RetainedDirectoryIdentity>,
2335    control_files: Vec<CallerGitControlFile>,
2336    home: PathBuf,
2337    xdg: PathBuf,
2338    temp: PathBuf,
2339    private_guards: Vec<std::fs::File>,
2340    _private_state: tempfile::TempDir,
2341}
2342
2343/// Bounded result of one fixed caller-checkout Git read.
2344#[derive(Debug)]
2345pub struct CallerGitOutput {
2346    status: ExitStatus,
2347    stdout: Vec<u8>,
2348}
2349
2350#[derive(Debug)]
2351struct CallerGitControlFile {
2352    path: PathBuf,
2353    retained: rustferry_core::RetainedRegularFileIdentity,
2354    sha256: String,
2355}
2356
2357impl CallerGitControlFile {
2358    fn capture(path: PathBuf) -> Result<Self, GitPublisherConfigError> {
2359        let retained = rustferry_core::RetainedRegularFileIdentity::open(&path)
2360            .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2361        let sha256 = caller_git_control_sha256(&path)?;
2362        retained
2363            .verify_path(&path)
2364            .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2365        Ok(Self {
2366            path,
2367            retained,
2368            sha256,
2369        })
2370    }
2371
2372    fn verify(&self) -> Result<(), GitPublisherConfigError> {
2373        self.retained
2374            .verify_path(&self.path)
2375            .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2376        if caller_git_control_sha256(&self.path)? != self.sha256 {
2377            return Err(GitPublisherConfigError::InvalidDirectory);
2378        }
2379        self.retained
2380            .verify_path(&self.path)
2381            .map_err(|_| GitPublisherConfigError::InvalidDirectory)
2382    }
2383}
2384
2385impl CallerGitOutput {
2386    /// Whether Git accepted the fixed read operation.
2387    pub fn success(&self) -> bool {
2388        self.status.success()
2389    }
2390
2391    /// Platform exit code, when one was available.
2392    pub fn exit_code(&self) -> Option<i32> {
2393        self.status.code()
2394    }
2395
2396    /// Bounded stdout. Stderr is always discarded.
2397    pub fn stdout(&self) -> &[u8] {
2398        &self.stdout
2399    }
2400}
2401
2402impl CallerGitRepository {
2403    /// Bind a caller checkout to the fixed platform Git and a config-neutral private process.
2404    ///
2405    /// # Errors
2406    ///
2407    /// Rejects unsupported platforms, unsafe paths, missing repositories, executable local Git
2408    /// policy, malformed output, and any failure to establish the retained standard toolchain.
2409    #[allow(
2410        clippy::too_many_lines,
2411        reason = "the security binding is intentionally one linear pre-spawn validation sequence"
2412    )]
2413    pub fn open(start: impl AsRef<Path>) -> Result<Self, GitPublisherConfigError> {
2414        #[cfg(not(any(unix, windows)))]
2415        {
2416            let _ = start.as_ref();
2417            return Err(GitPublisherConfigError::UnsupportedSecurePlatform);
2418        }
2419        #[cfg(any(unix, windows))]
2420        {
2421            let start = canonical_directory(start.as_ref())?;
2422            let executable = crate::git_process::trusted_git_executable()
2423                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2424            #[cfg(windows)]
2425            let toolchain = crate::git_process::WindowsGitToolchain::new(&executable)
2426                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2427            #[cfg(unix)]
2428            let toolchain = crate::git_process::UnixGitToolchain::new(&executable)
2429                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2430            let private_state =
2431                tempfile::tempdir().map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2432            let state_root = private_state.path().join("state");
2433            let home = state_root.join("home");
2434            let xdg = state_root.join("xdg");
2435            let temp = state_root.join("tmp");
2436            let private_guards =
2437                create_private_git_process_directories([&state_root, &home, &xdg, &temp])?;
2438            let mut repository = Self {
2439                toolchain,
2440                root: start.clone(),
2441                git_dir: None,
2442                common_dir: None,
2443                root_retained: None,
2444                git_dir_retained: None,
2445                common_dir_retained: None,
2446                control_files: Vec::new(),
2447                home,
2448                xdg,
2449                temp,
2450                private_guards,
2451                _private_state: private_state,
2452            };
2453            repository.validate_local_config(&start)?;
2454            let root = repository
2455                .execute_at(
2456                    &start,
2457                    ["rev-parse", "--show-toplevel"],
2458                    &[],
2459                    MAX_GIT_OUTPUT_BYTES,
2460                )
2461                .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?;
2462            if !root.status.success() {
2463                return Err(GitPublisherConfigError::CallerRepositoryReadFailed);
2464            }
2465            let root = parse_caller_git_directory(&root.stdout)?;
2466            if !start.starts_with(&root) {
2467                return Err(GitPublisherConfigError::InvalidDirectory);
2468            }
2469            repository.root = root;
2470            let git_dir = repository
2471                .execute_at(
2472                    &repository.root,
2473                    ["rev-parse", "--absolute-git-dir"],
2474                    &[],
2475                    MAX_GIT_OUTPUT_BYTES,
2476                )
2477                .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?;
2478            if !git_dir.status.success() {
2479                return Err(GitPublisherConfigError::CallerRepositoryReadFailed);
2480            }
2481            let git_dir = parse_caller_git_directory(&git_dir.stdout)?;
2482            let root_retained = rustferry_core::RetainedDirectoryIdentity::open(&repository.root)
2483                .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2484            let git_dir_retained = rustferry_core::RetainedDirectoryIdentity::open(&git_dir)
2485                .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2486            repository.git_dir = Some(git_dir);
2487            repository.root_retained = Some(root_retained);
2488            repository.git_dir_retained = Some(git_dir_retained);
2489            repository.verify_repository_paths()?;
2490            let common_dir = repository
2491                .execute(["rev-parse", "--git-common-dir"], &[], MAX_GIT_OUTPUT_BYTES)
2492                .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?;
2493            if !common_dir.status.success() {
2494                return Err(GitPublisherConfigError::CallerRepositoryReadFailed);
2495            }
2496            let common_dir = parse_caller_git_directory_relative(
2497                &common_dir.stdout,
2498                repository.git_dir.as_deref().unwrap_or(&repository.root),
2499            )?;
2500            let common_dir_retained = rustferry_core::RetainedDirectoryIdentity::open(&common_dir)
2501                .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2502            repository.common_dir = Some(common_dir.clone());
2503            repository.common_dir_retained = Some(common_dir_retained);
2504            let mut control_paths = BTreeSet::from([common_dir.join("config")]);
2505            let dot_git = repository.root.join(".git");
2506            if fs::symlink_metadata(&dot_git).is_ok_and(|metadata| metadata.is_file()) {
2507                control_paths.insert(dot_git);
2508            }
2509            let common_pointer = repository
2510                .git_dir
2511                .as_ref()
2512                .ok_or(GitPublisherConfigError::InvalidDirectory)?
2513                .join("commondir");
2514            if fs::symlink_metadata(&common_pointer).is_ok_and(|metadata| metadata.is_file()) {
2515                control_paths.insert(common_pointer);
2516            }
2517            repository.control_files = control_paths
2518                .into_iter()
2519                .map(CallerGitControlFile::capture)
2520                .collect::<Result<Vec<_>, _>>()?;
2521            repository.verify_repository_paths()?;
2522            repository.validate_local_config(&repository.root)?;
2523            let rebound = repository
2524                .execute(
2525                    ["rev-parse", "--absolute-git-dir"],
2526                    &[],
2527                    MAX_GIT_OUTPUT_BYTES,
2528                )
2529                .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?;
2530            if !rebound.status.success()
2531                || parse_caller_git_directory(&rebound.stdout)?
2532                    != *repository
2533                        .git_dir
2534                        .as_ref()
2535                        .ok_or(GitPublisherConfigError::InvalidDirectory)?
2536            {
2537                return Err(GitPublisherConfigError::InvalidDirectory);
2538            }
2539            let rebound_common = repository
2540                .execute(["rev-parse", "--git-common-dir"], &[], MAX_GIT_OUTPUT_BYTES)
2541                .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?;
2542            if !rebound_common.status.success()
2543                || parse_caller_git_directory_relative(
2544                    &rebound_common.stdout,
2545                    repository
2546                        .git_dir
2547                        .as_deref()
2548                        .ok_or(GitPublisherConfigError::InvalidDirectory)?,
2549                )? != common_dir
2550            {
2551                return Err(GitPublisherConfigError::InvalidDirectory);
2552            }
2553            Ok(repository)
2554        }
2555    }
2556
2557    /// Exact canonical repository root retained by this reader.
2558    pub fn root(&self) -> &Path {
2559        &self.root
2560    }
2561
2562    /// Read the exact, replacement-disabled `HEAD` object name.
2563    ///
2564    /// # Errors
2565    ///
2566    /// Returns a redacted process-policy, timeout, output-limit, or Git failure.
2567    pub fn head_revision(&self) -> Result<CallerGitOutput, GitExecutionError> {
2568        self.execute(["rev-parse", "--verify", "HEAD"], &[], MAX_GIT_OUTPUT_BYTES)
2569    }
2570
2571    /// Inspect tracked and untracked changes without refreshing or writing the caller index.
2572    ///
2573    /// # Errors
2574    ///
2575    /// Returns a redacted process-policy, timeout, output-limit, or Git failure.
2576    pub fn working_tree_status(&self) -> Result<CallerGitOutput, GitExecutionError> {
2577        self.execute(
2578            [
2579                "status",
2580                "--porcelain=v1",
2581                "--untracked-files=all",
2582                "--ignore-submodules=all",
2583            ],
2584            &[],
2585            MAX_CALLER_GIT_OUTPUT_BYTES,
2586        )
2587    }
2588
2589    /// Read exact tree metadata for bounded literal repository-relative paths.
2590    ///
2591    /// # Errors
2592    ///
2593    /// Rejects invalid revisions/paths and returns redacted bounded Git execution failures.
2594    pub fn tree_entries(
2595        &self,
2596        revision: &str,
2597        paths: &[String],
2598    ) -> Result<CallerGitOutput, GitExecutionError> {
2599        if !is_lower_object_id(revision)
2600            || paths.is_empty()
2601            || paths.len() > 128
2602            || paths.iter().any(|path| !is_safe_caller_git_path(path))
2603            || paths
2604                .iter()
2605                .map(|path| path.len().saturating_add(1))
2606                .sum::<usize>()
2607                > 8 * 1024
2608        {
2609            return Err(GitExecutionError::ProcessIo);
2610        }
2611        let mut arguments = vec![
2612            OsString::from("--literal-pathspecs"),
2613            OsString::from("ls-tree"),
2614            OsString::from("-l"),
2615            OsString::from("-z"),
2616            OsString::from(revision),
2617            OsString::from("--"),
2618        ];
2619        arguments.extend(paths.iter().map(OsString::from));
2620        self.execute(arguments, &[], MAX_CALLER_GIT_OUTPUT_BYTES)
2621    }
2622
2623    /// Read a bounded batch of exact object IDs through `cat-file --batch`.
2624    ///
2625    /// # Errors
2626    ///
2627    /// Rejects malformed input/output bounds and returns redacted bounded Git failures.
2628    pub fn blob_batch(
2629        &self,
2630        object_ids: &[u8],
2631        output_limit: usize,
2632    ) -> Result<CallerGitOutput, GitExecutionError> {
2633        if object_ids.is_empty()
2634            || object_ids.len() > 128 * 41
2635            || output_limit == 0
2636            || output_limit > MAX_CALLER_GIT_OUTPUT_BYTES
2637            || !object_ids
2638                .split(|byte| *byte == b'\n')
2639                .filter(|value| !value.is_empty())
2640                .all(|value| value.len() == 40 && value.iter().copied().all(is_lower_hex_byte))
2641        {
2642            return Err(GitExecutionError::ProcessIo);
2643        }
2644        self.execute(["cat-file", "--batch"], object_ids, output_limit)
2645    }
2646
2647    /// Read the current symbolic branch without invoking aliases or external helpers.
2648    ///
2649    /// # Errors
2650    ///
2651    /// Returns a redacted process-policy, timeout, output-limit, or Git failure.
2652    pub fn current_branch(&self) -> Result<CallerGitOutput, GitExecutionError> {
2653        self.execute(
2654            ["symbolic-ref", "--quiet", "--short", "HEAD"],
2655            &[],
2656            MAX_GIT_OUTPUT_BYTES,
2657        )
2658    }
2659
2660    /// Check one exact repository-relative path against repository ignore rules.
2661    ///
2662    /// # Errors
2663    ///
2664    /// Rejects unsafe paths and returns redacted bounded Git execution failures.
2665    pub fn check_ignore(&self, path: &str) -> Result<CallerGitOutput, GitExecutionError> {
2666        if !is_safe_caller_git_path(path) {
2667            return Err(GitExecutionError::ProcessIo);
2668        }
2669        self.execute(
2670            ["check-ignore", "--quiet", "--", path],
2671            &[],
2672            MAX_GIT_OUTPUT_BYTES,
2673        )
2674    }
2675
2676    /// Capture one local remote URL/push URL without includes or rewrite evaluation.
2677    ///
2678    /// # Errors
2679    ///
2680    /// Rejects invalid names, malformed/duplicate endpoints, and redacted Git failures.
2681    pub fn discover_remote(
2682        &self,
2683        remote_name: &str,
2684    ) -> Result<GithubRemoteSnapshot, GitPublisherConfigError> {
2685        let remote = GitRemoteName::new(remote_name)
2686            .map_err(|_| GitPublisherConfigError::InvalidRemoteName)?;
2687        let output = self
2688            .execute(remote.discovery_arguments(), &[], MAX_GIT_OUTPUT_BYTES)
2689            .map_err(|_| GitPublisherConfigError::RemoteDiscoveryFailed)?;
2690        if !output.status.success() {
2691            return Err(GitPublisherConfigError::RemoteDiscoveryFailed);
2692        }
2693        GithubRemoteSnapshot::from_local_config_output(&remote, &output.stdout)
2694            .map_err(|_| GitPublisherConfigError::RemoteDiscoveryFailed)
2695    }
2696
2697    fn validate_local_config(&self, directory: &Path) -> Result<(), GitPublisherConfigError> {
2698        let output = self
2699            .execute_at(
2700                directory,
2701                [
2702                    "config",
2703                    "--local",
2704                    "--no-includes",
2705                    "--null",
2706                    "--name-only",
2707                    "--list",
2708                ],
2709                &[],
2710                MAX_GIT_OUTPUT_BYTES,
2711            )
2712            .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?;
2713        if !output.status.success() {
2714            return Err(GitPublisherConfigError::CallerRepositoryReadFailed);
2715        }
2716        validate_caller_git_config_names(&output.stdout)
2717    }
2718
2719    fn execute(
2720        &self,
2721        arguments: impl IntoIterator<Item = impl Into<OsString>>,
2722        stdin: &[u8],
2723        output_limit: usize,
2724    ) -> Result<CallerGitOutput, GitExecutionError> {
2725        self.execute_at(&self.root, arguments, stdin, output_limit)
2726    }
2727
2728    fn execute_at(
2729        &self,
2730        directory: &Path,
2731        arguments: impl IntoIterator<Item = impl Into<OsString>>,
2732        stdin: &[u8],
2733        output_limit: usize,
2734    ) -> Result<CallerGitOutput, GitExecutionError> {
2735        self.execute_at_with_pre_spawn_hook(directory, arguments, stdin, output_limit, || {})
2736    }
2737
2738    fn execute_at_with_pre_spawn_hook(
2739        &self,
2740        directory: &Path,
2741        arguments: impl IntoIterator<Item = impl Into<OsString>>,
2742        stdin: &[u8],
2743        output_limit: usize,
2744        pre_spawn_hook: impl FnOnce(),
2745    ) -> Result<CallerGitOutput, GitExecutionError> {
2746        if stdin.len() > 128 * 41 || output_limit == 0 || output_limit > MAX_CALLER_GIT_OUTPUT_BYTES
2747        {
2748            return Err(GitExecutionError::ProcessIo);
2749        }
2750        #[cfg(not(any(unix, windows)))]
2751        {
2752            let _ = (directory, arguments, stdin, output_limit);
2753            Err(GitExecutionError::ProcessIo)
2754        }
2755        #[cfg(any(unix, windows))]
2756        {
2757            verify_private_git_process_directories(&self.private_guards)
2758                .map_err(|_| GitExecutionError::ProcessIo)?;
2759            self.verify_repository_paths()
2760                .map_err(|_| GitExecutionError::ProcessIo)?;
2761            let context = crate::git_process::GitProcessContext::new(
2762                directory, None, &self.home, &self.xdg, &self.temp,
2763            )
2764            .map_err(|_| GitExecutionError::ProcessIo)?;
2765            let mut arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
2766            if let Some(git_dir) = &self.git_dir {
2767                let mut bound = vec![
2768                    OsString::from("--git-dir"),
2769                    caller_git_argument_path(git_dir),
2770                    OsString::from("--work-tree"),
2771                    caller_git_argument_path(&self.root),
2772                ];
2773                bound.append(&mut arguments);
2774                arguments = bound;
2775            }
2776            let spec = self
2777                .toolchain
2778                .process_spec(&context, GitNetworkPolicy::Offline, arguments)
2779                .map_err(|_| GitExecutionError::ProcessIo)?;
2780            let mut command = spec.command().map_err(|_| GitExecutionError::ProcessIo)?;
2781            command
2782                .stdin(Stdio::piped())
2783                .stdout(Stdio::piped())
2784                .stderr(Stdio::piped());
2785            pre_spawn_hook();
2786            verify_private_git_process_directories(&self.private_guards)
2787                .map_err(|_| GitExecutionError::ProcessIo)?;
2788            self.verify_repository_paths()
2789                .map_err(|_| GitExecutionError::ProcessIo)?;
2790            let output =
2791                run_git_process_allow_status(command, stdin, MAX_GIT_TIMEOUT, output_limit)?;
2792            verify_private_git_process_directories(&self.private_guards)
2793                .map_err(|_| GitExecutionError::ProcessIo)?;
2794            self.verify_repository_paths()
2795                .map_err(|_| GitExecutionError::ProcessIo)?;
2796            Ok(CallerGitOutput {
2797                status: output.status,
2798                stdout: output.stdout,
2799            })
2800        }
2801    }
2802
2803    fn verify_repository_paths(&self) -> Result<(), GitPublisherConfigError> {
2804        match (
2805            self.git_dir.as_ref(),
2806            self.root_retained.as_ref(),
2807            self.git_dir_retained.as_ref(),
2808        ) {
2809            (None, None, None) => Ok(()),
2810            (Some(git_dir), Some(root), Some(retained_git_dir)) => {
2811                root.verify_path(&self.root)
2812                    .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2813                retained_git_dir
2814                    .verify_path(git_dir)
2815                    .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2816                match (&self.common_dir, &self.common_dir_retained) {
2817                    (Some(common_dir), Some(retained)) => retained
2818                        .verify_path(common_dir)
2819                        .map_err(|_| GitPublisherConfigError::InvalidDirectory)?,
2820                    (None, None) => {}
2821                    _ => return Err(GitPublisherConfigError::InvalidDirectory),
2822                }
2823                for control in &self.control_files {
2824                    control.verify()?;
2825                }
2826                Ok(())
2827            }
2828            _ => Err(GitPublisherConfigError::InvalidDirectory),
2829        }
2830    }
2831}
2832
2833fn create_private_git_process_directories<'a>(
2834    paths: impl IntoIterator<Item = &'a PathBuf>,
2835) -> Result<Vec<std::fs::File>, GitPublisherConfigError> {
2836    #[cfg(windows)]
2837    let mut guards = Vec::new();
2838    #[cfg(not(windows))]
2839    let guards = Vec::new();
2840    #[cfg(windows)]
2841    for path in paths {
2842        guards.push(
2843            rustferry_core::windows_private_directory::create_private_directory(path)
2844                .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?,
2845        );
2846    }
2847    #[cfg(unix)]
2848    for path in paths {
2849        use std::os::unix::fs::DirBuilderExt as _;
2850
2851        let mut builder = fs::DirBuilder::new();
2852        builder.mode(0o700);
2853        builder
2854            .create(path)
2855            .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2856    }
2857    #[cfg(not(any(unix, windows)))]
2858    {
2859        let _ = paths;
2860        return Err(GitPublisherConfigError::UnsupportedSecurePlatform);
2861    }
2862    Ok(guards)
2863}
2864
2865fn verify_private_git_process_directories(
2866    guards: &[std::fs::File],
2867) -> Result<(), GitPublisherConfigError> {
2868    #[cfg(windows)]
2869    {
2870        use std::os::windows::io::AsHandle as _;
2871
2872        for guard in guards {
2873            rustferry_core::windows_private_directory::verify_private_directory_handle(
2874                guard.as_handle(),
2875            )
2876            .map_err(|_| GitPublisherConfigError::SecureIsolationFailed)?;
2877        }
2878    }
2879    #[cfg(not(windows))]
2880    if !guards.is_empty() {
2881        return Err(GitPublisherConfigError::SecureIsolationFailed);
2882    }
2883    Ok(())
2884}
2885
2886fn parse_caller_git_directory(output: &[u8]) -> Result<PathBuf, GitPublisherConfigError> {
2887    let value = std::str::from_utf8(output)
2888        .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?
2889        .trim();
2890    if value.is_empty() || value.contains(['\r', '\n']) {
2891        return Err(GitPublisherConfigError::CallerRepositoryReadFailed);
2892    }
2893    canonical_directory(Path::new(value))
2894}
2895
2896fn caller_git_argument_path(path: &Path) -> OsString {
2897    #[cfg(windows)]
2898    {
2899        crate::git_process::external_path(path)
2900            .as_os_str()
2901            .to_owned()
2902    }
2903    #[cfg(not(windows))]
2904    path.as_os_str().to_owned()
2905}
2906
2907fn parse_caller_git_directory_relative(
2908    output: &[u8],
2909    relative_to: &Path,
2910) -> Result<PathBuf, GitPublisherConfigError> {
2911    let value = std::str::from_utf8(output)
2912        .map_err(|_| GitPublisherConfigError::CallerRepositoryReadFailed)?
2913        .trim();
2914    if value.is_empty() || value.contains(['\r', '\n']) {
2915        return Err(GitPublisherConfigError::CallerRepositoryReadFailed);
2916    }
2917    let path = Path::new(value);
2918    let resolved = if path.is_absolute() {
2919        path.to_path_buf()
2920    } else {
2921        relative_to.join(path)
2922    };
2923    canonical_directory(&resolved)
2924}
2925
2926fn caller_git_control_sha256(path: &Path) -> Result<String, GitPublisherConfigError> {
2927    let metadata =
2928        fs::symlink_metadata(path).map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2929    if !metadata.is_file()
2930        || metadata.file_type().is_symlink()
2931        || metadata.len() > MAX_CALLER_GIT_CONTROL_BYTES
2932    {
2933        return Err(GitPublisherConfigError::InvalidDirectory);
2934    }
2935    let bytes = fs::read(path).map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
2936    if u64::try_from(bytes.len()).ok() != Some(metadata.len()) {
2937        return Err(GitPublisherConfigError::InvalidDirectory);
2938    }
2939    Ok(hex::encode(Sha256::digest(bytes)))
2940}
2941
2942#[allow(
2943    clippy::case_sensitive_file_extension_comparisons,
2944    reason = "the suffix is a Git config-key component, not a file extension"
2945)]
2946fn validate_caller_git_config_names(output: &[u8]) -> Result<(), GitPublisherConfigError> {
2947    if output.len() > MAX_GIT_OUTPUT_BYTES {
2948        return Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig);
2949    }
2950    if output.is_empty() {
2951        return Ok(());
2952    }
2953    let Some(output) = output.strip_suffix(&[0]) else {
2954        return Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig);
2955    };
2956    for raw in output.split(|byte| *byte == 0) {
2957        let key = std::str::from_utf8(raw)
2958            .map_err(|_| GitPublisherConfigError::UnsafeCallerRepositoryConfig)?;
2959        if key.is_empty() || key.len() > 512 || key.chars().any(char::is_control) {
2960            return Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig);
2961        }
2962        let key = key.to_ascii_lowercase();
2963        let external_diff = key == "diff.external"
2964            || key.starts_with("diff.")
2965                && (key.ends_with(".command") || key.ends_with(".textconv"));
2966        let partial_clone_remote = key.starts_with("remote.")
2967            && (key.ends_with(".promisor") || key.ends_with(".partialclonefilter"));
2968        if key == "include.path"
2969            || key.starts_with("includeif.") && key.ends_with(".path")
2970            || key == "extensions.worktreeconfig"
2971            || key == "extensions.partialclone"
2972            || key == "core.worktree"
2973            || key.starts_with("filter.")
2974            || key.starts_with("protocol.")
2975            || partial_clone_remote
2976            || external_diff
2977        {
2978            return Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig);
2979        }
2980    }
2981    Ok(())
2982}
2983
2984fn is_lower_object_id(value: &str) -> bool {
2985    value.len() == 40 && value.bytes().all(is_lower_hex_byte)
2986}
2987
2988const fn is_lower_hex_byte(byte: u8) -> bool {
2989    byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')
2990}
2991
2992fn is_safe_caller_git_path(value: &str) -> bool {
2993    !value.is_empty()
2994        && value.len() <= 4_096
2995        && !value.starts_with(['/', '\\', '-'])
2996        && !value.contains(['\0', '\r', '\n', '\\'])
2997        && value
2998            .split('/')
2999            .all(|component| !component.is_empty() && !matches!(component, "." | ".."))
3000}
3001
3002impl GitRunner for GitProcessRunner {
3003    fn execute(&mut self, invocation: &GitInvocation) -> Result<Vec<u8>, GitExecutionError> {
3004        #[cfg(not(any(unix, windows)))]
3005        {
3006            let _ = invocation;
3007            return Err(GitExecutionError::ProcessIo);
3008        }
3009        #[cfg(any(unix, windows))]
3010        {
3011            let context = self
3012                .repository
3013                .process_context()
3014                .map_err(|_| GitExecutionError::ProcessIo)?;
3015            let spec = self
3016                .toolchain
3017                .process_spec(
3018                    &context,
3019                    invocation.network,
3020                    invocation.arguments.iter().cloned(),
3021                )
3022                .map_err(|_| GitExecutionError::ProcessIo)?;
3023            let mut command = spec.command().map_err(|_| GitExecutionError::ProcessIo)?;
3024            command
3025                .stdin(Stdio::piped())
3026                .stdout(Stdio::piped())
3027                .stderr(Stdio::piped());
3028            for (variable, value) in &invocation.environment {
3029                command.env(variable.as_str(), value);
3030            }
3031            run_git_process(
3032                command,
3033                &invocation.stdin,
3034                invocation.timeout,
3035                invocation.stdout_limit,
3036            )
3037        }
3038    }
3039
3040    fn execute_file(
3041        &mut self,
3042        invocation: &GitInvocation,
3043        input: fs::File,
3044        expected_len: u64,
3045    ) -> Result<Vec<u8>, GitExecutionError> {
3046        #[cfg(not(any(unix, windows)))]
3047        {
3048            let _ = (invocation, input, expected_len);
3049            return Err(GitExecutionError::ProcessIo);
3050        }
3051        #[cfg(any(unix, windows))]
3052        {
3053            if !invocation.stdin.is_empty() {
3054                return Err(GitExecutionError::ProcessIo);
3055            }
3056            let context = self
3057                .repository
3058                .process_context()
3059                .map_err(|_| GitExecutionError::ProcessIo)?;
3060            let spec = self
3061                .toolchain
3062                .process_spec(
3063                    &context,
3064                    invocation.network,
3065                    invocation.arguments.iter().cloned(),
3066                )
3067                .map_err(|_| GitExecutionError::ProcessIo)?;
3068            let mut command = spec.command().map_err(|_| GitExecutionError::ProcessIo)?;
3069            command
3070                .stdin(Stdio::piped())
3071                .stdout(Stdio::piped())
3072                .stderr(Stdio::piped());
3073            for (variable, value) in &invocation.environment {
3074                command.env(variable.as_str(), value);
3075            }
3076            run_git_process_file(
3077                command,
3078                input,
3079                expected_len,
3080                invocation.timeout,
3081                invocation.stdout_limit,
3082            )
3083        }
3084    }
3085
3086    fn process_tree_is_parent_fenced(&self) -> bool {
3087        cfg!(windows)
3088    }
3089}
3090
3091/// Invalid local Git publisher configuration.
3092#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3093pub enum GitPublisherConfigError {
3094    /// Git executable was not a canonical executable file named `git`.
3095    InvalidGitExecutable,
3096    /// Repository or isolation directory was not canonical and usable.
3097    InvalidDirectory,
3098    /// Remote name contained option, ref, or path syntax.
3099    InvalidRemoteName,
3100    /// Timeout was zero or exceeded ten minutes.
3101    InvalidTimeout,
3102    /// Fixed tool policy or private bare repository initialization failed.
3103    SecureIsolationFailed,
3104    /// One-shot local remote discovery failed closed.
3105    RemoteDiscoveryFailed,
3106    /// Caller repository metadata could not be read through the sealed policy.
3107    CallerRepositoryReadFailed,
3108    /// Local Git policy could execute code or redirect the selected worktree.
3109    UnsafeCallerRepositoryConfig,
3110    /// The platform has no implemented secure private Git runner.
3111    UnsupportedSecurePlatform,
3112}
3113
3114impl fmt::Display for GitPublisherConfigError {
3115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3116        match self {
3117            Self::InvalidGitExecutable => formatter.write_str("Git executable is invalid"),
3118            Self::InvalidDirectory => formatter.write_str("Git publisher directory is invalid"),
3119            Self::InvalidRemoteName => formatter.write_str("Git remote name is invalid"),
3120            Self::InvalidTimeout => formatter.write_str("Git operation timeout is invalid"),
3121            Self::SecureIsolationFailed => {
3122                formatter.write_str("secure Git publisher isolation could not be established")
3123            }
3124            Self::RemoteDiscoveryFailed => {
3125                formatter.write_str("Git remote could not be captured safely")
3126            }
3127            Self::CallerRepositoryReadFailed => {
3128                formatter.write_str("caller Git repository could not be read safely")
3129            }
3130            Self::UnsafeCallerRepositoryConfig => {
3131                formatter.write_str("caller Git repository config is unsafe")
3132            }
3133            Self::UnsupportedSecurePlatform => {
3134                formatter.write_str("secure Git publication is unsupported on this platform")
3135            }
3136        }
3137    }
3138}
3139
3140impl Error for GitPublisherConfigError {}
3141
3142/// Git-plumbing implementation that publishes without checkout or branch mutation.
3143///
3144/// On Windows the isolation directory must satisfy `RustFerry`'s strict private-directory policy;
3145/// the publisher retains its no-delete-sharing handle for its complete lifetime so a same-path
3146/// replacement cannot redirect publication-lease acquisition.
3147#[derive(Debug)]
3148pub struct GitTemporaryRefPublisher<R> {
3149    runner: R,
3150    snapshot_imports: BTreeMap<String, GitSnapshotImportInputs>,
3151    isolation_directory: PathBuf,
3152    #[cfg(windows)]
3153    _isolation_directory_guard: std::fs::File,
3154    isolation_directory_identity: rustferry_core::DirectoryFilesystemIdentity,
3155    publication_lease_scope_sha256: String,
3156    publication_leases: BTreeMap<String, rustferry_core::process_control::ProcessFileLease>,
3157    #[cfg(test)]
3158    source_remote_name: String,
3159    #[cfg(test)]
3160    execution_remote_name: String,
3161    frozen_remotes: Option<VerifiedPublisherRemotes>,
3162    timeout: Duration,
3163}
3164
3165#[derive(Clone, Debug, Eq, PartialEq)]
3166struct VerifiedGitRemote {
3167    fetch: GithubGitEndpoint,
3168    push: GithubGitEndpoint,
3169}
3170
3171#[derive(Clone, Debug, Eq, PartialEq)]
3172struct VerifiedPublisherRemotes {
3173    source: VerifiedGitRemote,
3174    execution: VerifiedGitRemote,
3175}
3176
3177fn execution_readiness_ref(
3178    remote: &VerifiedGitRemote,
3179    trusted_tip: &CommitSha,
3180    namespace: &TemporaryBranchNamespace,
3181) -> String {
3182    let counter = INDEX_RESERVATION_COUNTER.fetch_add(1, Ordering::Relaxed);
3183    let now = SystemTime::now()
3184        .duration_since(UNIX_EPOCH)
3185        .unwrap_or_default()
3186        .as_nanos();
3187    let mut digest = Sha256::new();
3188    digest.update(b"rustferry-github-execution-readiness-ref-v1\0");
3189    digest.update(remote.fetch.canonical_url().as_bytes());
3190    digest.update(b"\0");
3191    digest.update(remote.push.canonical_url().as_bytes());
3192    digest.update(b"\0");
3193    digest.update(trusted_tip.as_str().as_bytes());
3194    digest.update(b"\0");
3195    digest.update(namespace.as_str().as_bytes());
3196    digest.update(b"\0");
3197    digest.update(std::process::id().to_le_bytes());
3198    digest.update(counter.to_le_bytes());
3199    digest.update(now.to_le_bytes());
3200    let encoded = hex::encode(digest.finalize());
3201    format!(
3202        "refs/heads/{}/readiness-{}",
3203        namespace.as_str(),
3204        &encoded[..16]
3205    )
3206}
3207
3208fn temporary_namespace_for_publication(
3209    temporary_ref: &TemporaryGitRef,
3210    operation_id: &str,
3211) -> Result<TemporaryBranchNamespace, TemporaryRefPublishError> {
3212    let suffix = format!("/{operation_id}");
3213    let namespace = temporary_ref
3214        .branch()
3215        .as_str()
3216        .strip_suffix(&suffix)
3217        .filter(|value| !value.is_empty())
3218        .ok_or(TemporaryRefPublishError::MalformedGitOutput)?;
3219    TemporaryBranchNamespace::new(namespace.to_owned())
3220        .map_err(|_| TemporaryRefPublishError::MalformedGitOutput)
3221}
3222
3223impl<R> GitTemporaryRefPublisher<R> {
3224    fn initialize(
3225        runner: R,
3226        isolation_directory: impl AsRef<Path>,
3227        timeout: Duration,
3228    ) -> Result<Self, GitPublisherConfigError> {
3229        let isolation_directory = canonical_directory(isolation_directory.as_ref())?;
3230        if timeout.is_zero() || timeout > MAX_GIT_TIMEOUT {
3231            return Err(GitPublisherConfigError::InvalidTimeout);
3232        }
3233        #[cfg(windows)]
3234        let isolation_directory_guard =
3235            rustferry_core::windows_private_directory::open_private_directory_read_guard(
3236                &isolation_directory,
3237            )
3238            .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
3239        let isolation_directory_identity =
3240            rustferry_core::DirectoryFilesystemIdentity::capture(&isolation_directory)
3241                .map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
3242        Ok(Self {
3243            runner,
3244            snapshot_imports: BTreeMap::new(),
3245            publication_lease_scope_sha256: publication_lease_scope_sha256(
3246                &isolation_directory,
3247                &isolation_directory_identity,
3248            ),
3249            isolation_directory,
3250            #[cfg(windows)]
3251            _isolation_directory_guard: isolation_directory_guard,
3252            isolation_directory_identity,
3253            publication_leases: BTreeMap::new(),
3254            #[cfg(test)]
3255            source_remote_name: String::new(),
3256            #[cfg(test)]
3257            execution_remote_name: String::new(),
3258            frozen_remotes: None,
3259            timeout,
3260        })
3261    }
3262
3263    /// Bind a runner to a canonical isolation directory and conservative source/execution remotes.
3264    ///
3265    /// # Errors
3266    ///
3267    /// Rejects symlink-final/non-canonical directories, unsafe remote names, and invalid timeouts.
3268    #[cfg(test)]
3269    pub fn new(
3270        runner: R,
3271        isolation_directory: impl AsRef<Path>,
3272        source_remote_name: impl Into<String>,
3273        execution_remote_name: impl Into<String>,
3274        timeout: Duration,
3275    ) -> Result<Self, GitPublisherConfigError> {
3276        let source_remote_name = source_remote_name.into();
3277        let execution_remote_name = execution_remote_name.into();
3278        if !is_safe_remote_name(&source_remote_name) || !is_safe_remote_name(&execution_remote_name)
3279        {
3280            return Err(GitPublisherConfigError::InvalidRemoteName);
3281        }
3282        let mut publisher = Self::initialize(runner, isolation_directory, timeout)?;
3283        publisher.source_remote_name = source_remote_name;
3284        publisher.execution_remote_name = execution_remote_name;
3285        Ok(publisher)
3286    }
3287
3288    /// Bind a runner to immutable canonical endpoints and a canonical isolation directory.
3289    ///
3290    /// This constructor never accepts or consults Git remote names. The endpoint snapshots must
3291    /// have been captured once during setup and persisted in the private provider configuration.
3292    ///
3293    /// # Errors
3294    ///
3295    /// Rejects symlink-final/non-canonical directories and invalid timeouts.
3296    pub fn new_with_endpoints(
3297        runner: R,
3298        isolation_directory: impl AsRef<Path>,
3299        source: &GithubRemoteSnapshot,
3300        execution: &GithubRemoteSnapshot,
3301        timeout: Duration,
3302    ) -> Result<Self, GitPublisherConfigError> {
3303        let mut publisher = Self::initialize(runner, isolation_directory, timeout)?;
3304        publisher.frozen_remotes = Some(VerifiedPublisherRemotes {
3305            source: VerifiedGitRemote {
3306                fetch: source.fetch().clone(),
3307                push: source.push().clone(),
3308            },
3309            execution: VerifiedGitRemote {
3310                fetch: execution.fetch().clone(),
3311                push: execution.push().clone(),
3312            },
3313        });
3314        Ok(publisher)
3315    }
3316
3317    /// Recover the runner, including captured invocations in tests.
3318    pub fn into_runner(self) -> R {
3319        self.runner
3320    }
3321}
3322
3323impl<R: GitRunner> GitTemporaryRefPublisher<R> {
3324    #[cfg(test)]
3325    fn execute(
3326        &mut self,
3327        arguments: impl IntoIterator<Item = impl Into<OsString>>,
3328    ) -> Result<Vec<u8>, TemporaryRefPublishError> {
3329        self.runner
3330            .execute(&GitInvocation::new(arguments, self.timeout))
3331            .map_err(Into::into)
3332    }
3333
3334    fn execute_invocation(
3335        &mut self,
3336        invocation: &GitInvocation,
3337    ) -> Result<Vec<u8>, TemporaryRefPublishError> {
3338        self.runner.execute(invocation).map_err(Into::into)
3339    }
3340
3341    fn execute_without_replacements(
3342        &mut self,
3343        arguments: impl IntoIterator<Item = impl Into<OsString>>,
3344    ) -> Result<Vec<u8>, TemporaryRefPublishError> {
3345        self.execute_invocation(&GitInvocation::new(arguments, self.timeout))
3346    }
3347
3348    #[cfg(test)]
3349    fn verify_remote(
3350        &mut self,
3351        remote_name: &str,
3352        expected_repository: &str,
3353    ) -> Result<VerifiedGitRemote, TemporaryRefPublishError> {
3354        let fetch_output = self.execute(["remote", "get-url", remote_name])?;
3355        let fetch = GithubGitEndpoint::parse(parse_single_line(&fetch_output)?)
3356            .map_err(|_| TemporaryRefPublishError::RemoteMismatch)?;
3357        let push_output = self.execute(["remote", "get-url", "--push", remote_name])?;
3358        let push = GithubGitEndpoint::parse(parse_single_line(&push_output)?)
3359            .map_err(|_| TemporaryRefPublishError::RemoteMismatch)?;
3360        if !github_remote_matches(fetch.canonical_url(), expected_repository)
3361            || !github_remote_matches(push.canonical_url(), expected_repository)
3362            || fetch.repository() != push.repository()
3363        {
3364            return Err(TemporaryRefPublishError::RemoteMismatch);
3365        }
3366        Ok(VerifiedGitRemote { fetch, push })
3367    }
3368
3369    fn remote_ref(
3370        &mut self,
3371        endpoint: &GithubGitEndpoint,
3372        full_ref: &str,
3373    ) -> Result<Option<CommitSha>, TemporaryRefPublishError> {
3374        let invocation = GitInvocation::new(
3375            ["ls-remote", "--refs", endpoint.canonical_url(), full_ref],
3376            self.timeout,
3377        )
3378        .with_network(endpoint);
3379        let output = self.execute_invocation(&invocation)?;
3380        parse_ls_remote(&output, full_ref)
3381    }
3382
3383    fn execution_remote(
3384        &mut self,
3385        repository: &Repository,
3386    ) -> Result<VerifiedGitRemote, TemporaryRefPublishError> {
3387        let expected = repository_url(repository);
3388        if let Some(remotes) = &self.frozen_remotes {
3389            if !github_remote_matches(remotes.execution.fetch.canonical_url(), &expected)
3390                || !github_remote_matches(remotes.execution.push.canonical_url(), &expected)
3391            {
3392                return Err(TemporaryRefPublishError::RemoteMismatch);
3393            }
3394            return Ok(remotes.execution.clone());
3395        }
3396        #[cfg(test)]
3397        {
3398            let execution_remote_name = self.execution_remote_name.clone();
3399            self.verify_remote(&execution_remote_name, &expected)
3400        }
3401        #[cfg(not(test))]
3402        {
3403            Err(TemporaryRefPublishError::RemoteMismatch)
3404        }
3405    }
3406
3407    fn verify_execution_workflow_bytes(
3408        &mut self,
3409        request: &ExecutionWorkflowDoctorRequest<'_>,
3410    ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
3411        validate_repository_path(request.workflow_path)
3412            .map_err(|()| TemporaryRefPublishError::MalformedGitOutput)?;
3413        let remote = self.execution_remote(request.repository)?;
3414        let tip = self
3415            .remote_ref(&remote.fetch, request.default_branch_ref.as_str())?
3416            .ok_or(TemporaryRefPublishError::TrustedRefUnavailable)?;
3417        let fetch = GitInvocation::new(
3418            [
3419                "fetch",
3420                "--quiet",
3421                "--no-tags",
3422                "--no-write-fetch-head",
3423                remote.fetch.canonical_url(),
3424                request.default_branch_ref.as_str(),
3425            ],
3426            self.timeout,
3427        )
3428        .with_network(&remote.fetch);
3429        self.execute_invocation(&fetch)?;
3430        if self
3431            .remote_ref(&remote.fetch, request.default_branch_ref.as_str())?
3432            .as_ref()
3433            != Some(&tip)
3434        {
3435            return Err(TemporaryRefPublishError::TrustedRefUnavailable);
3436        }
3437        let workflow = self
3438            .execute_without_replacements([
3439                "show",
3440                &format!("{}:{}", tip.as_str(), request.workflow_path),
3441            ])
3442            .map_err(|_| TemporaryRefPublishError::WorkflowFingerprintMismatch)?;
3443        if workflow.len() > MAX_DISPATCH_FILE_BYTES
3444            || WorkflowFingerprint::for_workflow_bytes(&workflow) != *request.workflow_fingerprint
3445        {
3446            return Err(TemporaryRefPublishError::WorkflowFingerprintMismatch);
3447        }
3448        Ok(TemporaryRefPublisherReadiness {
3449            trusted_ref_tip: tip,
3450        })
3451    }
3452
3453    fn doctor_with_remote(
3454        &mut self,
3455        request: &TemporaryRefDoctorRequest<'_>,
3456    ) -> Result<(TemporaryRefPublisherReadiness, VerifiedPublisherRemotes), TemporaryRefPublishError>
3457    {
3458        validate_repository_path(request.workflow_path)
3459            .map_err(|()| TemporaryRefPublishError::MalformedGitOutput)?;
3460        let execution_repository = repository_url(request.repository);
3461        let remotes = if let Some(remotes) = &self.frozen_remotes {
3462            if !github_remote_matches(
3463                remotes.source.fetch.canonical_url(),
3464                request.source_repository,
3465            ) || !github_remote_matches(
3466                remotes.source.push.canonical_url(),
3467                request.source_repository,
3468            ) || !github_remote_matches(
3469                remotes.execution.fetch.canonical_url(),
3470                &execution_repository,
3471            ) || !github_remote_matches(
3472                remotes.execution.push.canonical_url(),
3473                &execution_repository,
3474            ) {
3475                return Err(TemporaryRefPublishError::RemoteMismatch);
3476            }
3477            remotes.clone()
3478        } else {
3479            #[cfg(test)]
3480            {
3481                let source_remote_name = self.source_remote_name.clone();
3482                let execution_remote_name = self.execution_remote_name.clone();
3483                let source = self.verify_remote(&source_remote_name, request.source_repository)?;
3484                let execution = if source_remote_name == execution_remote_name
3485                    && github_remote_matches(request.source_repository, &execution_repository)
3486                {
3487                    source.clone()
3488                } else {
3489                    self.verify_remote(&execution_remote_name, &execution_repository)?
3490                };
3491                VerifiedPublisherRemotes { source, execution }
3492            }
3493            #[cfg(not(test))]
3494            {
3495                return Err(TemporaryRefPublishError::RemoteMismatch);
3496            }
3497        };
3498        let tip = self
3499            .remote_ref(&remotes.source.fetch, request.trusted_source_ref.as_str())?
3500            .ok_or(TemporaryRefPublishError::TrustedRefUnavailable)?;
3501        let fetch = GitInvocation::new(
3502            [
3503                "fetch",
3504                "--quiet",
3505                "--no-tags",
3506                "--no-write-fetch-head",
3507                remotes.source.fetch.canonical_url(),
3508                request.trusted_source_ref.as_str(),
3509            ],
3510            self.timeout,
3511        )
3512        .with_network(&remotes.source.fetch);
3513        self.execute_invocation(&fetch)?;
3514        let trusted_workflow = self
3515            .execute_without_replacements([
3516                "show",
3517                &format!("{}:{}", tip.as_str(), request.workflow_path),
3518            ])
3519            .map_err(|_| TemporaryRefPublishError::WorkflowFingerprintMismatch)?;
3520        if WorkflowFingerprint::for_workflow_bytes(&trusted_workflow)
3521            != *request.workflow_fingerprint
3522        {
3523            return Err(TemporaryRefPublishError::WorkflowFingerprintMismatch);
3524        }
3525        if self.frozen_remotes.is_some() {
3526            self.prove_execution_readiness(
3527                &remotes.execution,
3528                &tip,
3529                request.temporary_branch_namespace,
3530            )?;
3531        }
3532        Ok((
3533            TemporaryRefPublisherReadiness {
3534                trusted_ref_tip: tip,
3535            },
3536            remotes,
3537        ))
3538    }
3539
3540    fn prove_execution_readiness(
3541        &mut self,
3542        execution: &VerifiedGitRemote,
3543        trusted_tip: &CommitSha,
3544        namespace: &TemporaryBranchNamespace,
3545    ) -> Result<(), TemporaryRefPublishError> {
3546        let readiness_ref = execution_readiness_ref(execution, trusted_tip, namespace);
3547        if self.remote_ref(&execution.fetch, &readiness_ref)?.is_some()
3548            || self.remote_ref(&execution.push, &readiness_ref)?.is_some()
3549        {
3550            return Err(TemporaryRefPublishError::PublicationVerificationFailed);
3551        }
3552        let lease = format!("--force-with-lease={readiness_ref}:");
3553        let refspec = format!("{}:{readiness_ref}", trusted_tip.as_str());
3554        let dry_run = GitInvocation::new(
3555            [
3556                "push",
3557                "--dry-run",
3558                "--porcelain",
3559                "--no-verify",
3560                &lease,
3561                execution.push.canonical_url(),
3562                &refspec,
3563            ],
3564            self.timeout,
3565        )
3566        .with_network(&execution.push);
3567        self.execute_invocation(&dry_run)?;
3568        if self.remote_ref(&execution.push, &readiness_ref)?.is_some() {
3569            return Err(TemporaryRefPublishError::PublicationVerificationFailed);
3570        }
3571        Ok(())
3572    }
3573
3574    fn index_invocation(
3575        &self,
3576        arguments: impl IntoIterator<Item = impl Into<OsString>>,
3577        index_path: &Path,
3578    ) -> GitInvocation {
3579        GitInvocation::new(arguments, self.timeout)
3580            .with_environment(GitEnvironmentVariable::IndexFile, index_path.as_os_str())
3581    }
3582
3583    fn hash_blob(&mut self, bytes: &[u8]) -> Result<String, TemporaryRefPublishError> {
3584        let invocation =
3585            GitInvocation::new(["hash-object", "-w", "--stdin"], self.timeout).with_stdin(bytes);
3586        parse_object_id(&self.execute_invocation(&invocation)?)
3587    }
3588
3589    fn require_snapshot_sha1(&mut self) -> Result<(), TemporaryRefPublishError> {
3590        let output = self.execute_without_replacements(["rev-parse", "--show-object-format"])?;
3591        require_sha1_object_format(&output).map_err(Into::into)
3592    }
3593
3594    fn hash_snapshot_bytes(
3595        &mut self,
3596        kind: GitSnapshotObjectKind,
3597        bytes: &[u8],
3598        write: bool,
3599    ) -> Result<GitSha1ObjectId, TemporaryRefPublishError> {
3600        let mut arguments = vec![OsString::from("hash-object")];
3601        if write {
3602            arguments.push(OsString::from("-w"));
3603        }
3604        if kind != GitSnapshotObjectKind::Blob {
3605            arguments.push(OsString::from("-t"));
3606            arguments.push(OsString::from(kind.git_type()));
3607        }
3608        arguments.push(OsString::from("--stdin"));
3609        let invocation = GitInvocation::new(arguments, self.timeout).with_stdin(bytes);
3610        let object = parse_object_id(&self.execute_invocation(&invocation)?)?;
3611        GitSha1ObjectId::new(object).map_err(Into::into)
3612    }
3613
3614    fn hash_snapshot_file(
3615        &mut self,
3616        mut file: fs::File,
3617        expected_len: u64,
3618        write: bool,
3619    ) -> Result<GitSha1ObjectId, TemporaryRefPublishError> {
3620        file.seek(io::SeekFrom::Start(0))
3621            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3622        let arguments = if write {
3623            ["hash-object", "-w", "--stdin"].as_slice()
3624        } else {
3625            ["hash-object", "--stdin"].as_slice()
3626        };
3627        let invocation = GitInvocation::new(arguments, self.timeout);
3628        let output = self
3629            .runner
3630            .execute_file(&invocation, file, expected_len)
3631            .map_err(TemporaryRefPublishError::from)?;
3632        GitSha1ObjectId::new(parse_object_id(&output)?).map_err(Into::into)
3633    }
3634
3635    fn precompute_snapshot_graph(
3636        &mut self,
3637        inputs: &mut GitSnapshotPrecomputeInputs,
3638        operation_id: &str,
3639        created_at_ms: u64,
3640    ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
3641        self.require_snapshot_sha1()?;
3642        let archive_size = inputs.expected_archive().size;
3643        let archive_file = inputs
3644            .archive_reader()?
3645            .try_clone()
3646            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3647        let archive_blob = self.hash_snapshot_file(archive_file, archive_size, false)?;
3648        inputs.verify_contents()?;
3649        let descriptor_bytes = inputs.descriptor_bytes().to_vec();
3650        let descriptor_blob =
3651            self.hash_snapshot_bytes(GitSnapshotObjectKind::Blob, &descriptor_bytes, false)?;
3652        inputs.verify_contents()?;
3653        let graph = complete_git_snapshot_object_graph(
3654            archive_blob,
3655            descriptor_blob,
3656            operation_id,
3657            created_at_ms,
3658            |kind, bytes| {
3659                let object = self.hash_snapshot_bytes(kind, bytes, false)?;
3660                inputs.verify_contents()?;
3661                Ok::<_, TemporaryRefPublishError>(object)
3662            },
3663        )?;
3664        inputs.verify_contents()?;
3665        Ok(graph)
3666    }
3667
3668    fn import_snapshot_graph(
3669        &mut self,
3670        request: &GitSnapshotImportRequest<'_>,
3671    ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
3672        if let Some(inputs) = self.snapshot_imports.get_mut(&request.request.operation_id) {
3673            if inputs.locator() != request.locator || inputs.stage() != request.expected_stage {
3674                return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3675            }
3676            inputs.verify_bindings()?;
3677            return Ok(inputs.stage().graph.clone());
3678        }
3679        self.require_snapshot_sha1()?;
3680        let mut inputs = GitSnapshotImportInputs::load(
3681            &self.isolation_directory,
3682            request.locator,
3683            request.request,
3684        )?;
3685        if inputs.stage() != request.expected_stage {
3686            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3687        }
3688        let graph = inputs.stage().graph.clone();
3689        let created_at_ms = inputs.stage().source_created_at_ms;
3690        let archive_size = inputs.stage().archive.size;
3691        let archive_file = inputs
3692            .archive_reader()?
3693            .try_clone()
3694            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3695        let archive_blob = self.hash_snapshot_file(archive_file, archive_size, true)?;
3696        inputs.verify_imported_archive_blob(&archive_blob)?;
3697        let descriptor_bytes = inputs.descriptor_bytes().to_vec();
3698        let descriptor_blob =
3699            self.hash_snapshot_bytes(GitSnapshotObjectKind::Blob, &descriptor_bytes, true)?;
3700        inputs.verify_imported_descriptor_blob(&descriptor_blob)?;
3701        graph.verify_rehashed(
3702            archive_blob,
3703            descriptor_blob,
3704            &request.request.operation_id,
3705            created_at_ms,
3706            |kind, bytes| {
3707                let object = self.hash_snapshot_bytes(kind, bytes, true)?;
3708                inputs.verify_bindings()?;
3709                Ok::<_, TemporaryRefPublishError>(object)
3710            },
3711        )?;
3712        inputs.verify_bindings()?;
3713        self.snapshot_imports
3714            .insert(request.request.operation_id.clone(), inputs);
3715        Ok(graph)
3716    }
3717
3718    fn precompute_exact_snapshot_retry(
3719        &mut self,
3720        request: &GitSnapshotExactRetryPrecomputeRequest<'_>,
3721    ) -> Result<(GithubGitSnapshotExactRetryPlanV1, Vec<u8>), TemporaryRefPublishError> {
3722        if request.parent_request.operation_id == request.child_operation_id
3723            || request.child_source_created_at_ms == 0
3724            || request.child_source_created_at_ms / 1_000 > i64::MAX as u64
3725            || GitSnapshotSourceRef::for_operation(request.child_operation_id).is_err()
3726        {
3727            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3728        }
3729        let parent_descriptor = GitSnapshotDescriptor::from_request(
3730            request.parent_request,
3731            SourceBundleDescriptor::new(
3732                request.parent_stage.archive.clone(),
3733                request.parent_request.source.clone(),
3734            ),
3735        )
3736        .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3737        request
3738            .parent_stage
3739            .validate_for_request(&parent_descriptor, request.parent_request)?;
3740        self.require_snapshot_sha1()?;
3741        let stdout_limit = usize::try_from(
3742            request
3743                .parent_stage
3744                .archive
3745                .size
3746                .checked_add(1)
3747                .ok_or(TemporaryRefPublishError::GitSnapshotInvalid)?,
3748        )
3749        .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3750        let archive_bytes = self.execute_invocation(
3751            &GitInvocation::new(
3752                [
3753                    "cat-file",
3754                    "blob",
3755                    request.parent_stage.graph.archive_blob.as_str(),
3756                ],
3757                self.timeout,
3758            )
3759            .with_stdout_limit(stdout_limit),
3760        )?;
3761        if archive_bytes.len() as u64 != request.parent_stage.archive.size
3762            || hex_sha256(&archive_bytes) != request.parent_stage.archive.sha256
3763        {
3764            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3765        }
3766        let archive_blob =
3767            self.hash_snapshot_bytes(GitSnapshotObjectKind::Blob, &archive_bytes, false)?;
3768        if archive_blob != request.parent_stage.graph.archive_blob {
3769            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3770        }
3771        let mut child_request = request.parent_request.clone();
3772        request
3773            .child_operation_id
3774            .clone_into(&mut child_request.operation_id);
3775        child_request.source_revision = None;
3776        let descriptor = GitSnapshotDescriptor::from_request(
3777            &child_request,
3778            SourceBundleDescriptor::new(
3779                request.parent_stage.archive.clone(),
3780                child_request.source.clone(),
3781            ),
3782        )
3783        .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3784        let descriptor_bytes = canonical_git_snapshot_descriptor_bytes(&descriptor)
3785            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3786        let descriptor_blob =
3787            self.hash_snapshot_bytes(GitSnapshotObjectKind::Blob, &descriptor_bytes, false)?;
3788        let graph = complete_git_snapshot_object_graph(
3789            archive_blob,
3790            descriptor_blob,
3791            request.child_operation_id,
3792            request.child_source_created_at_ms,
3793            |kind, bytes| self.hash_snapshot_bytes(kind, bytes, false),
3794        )?;
3795        child_request.source_revision = Some(graph.commit.as_str().to_owned());
3796        child_request
3797            .validate()
3798            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3799        let plan = GithubGitSnapshotExactRetryPlanV1 {
3800            request: child_request,
3801            source_created_at_ms: request.child_source_created_at_ms,
3802            request_template_sha256: descriptor.request_template_sha256,
3803            manifest_sha256: request.parent_stage.manifest_sha256.clone(),
3804            archive: request.parent_stage.archive.clone(),
3805            descriptor_sha256: hex_sha256(&descriptor_bytes),
3806            graph,
3807        };
3808        Ok((plan, archive_bytes))
3809    }
3810
3811    #[allow(
3812        clippy::too_many_lines,
3813        reason = "the exact retry reproduces the durable plan before its bounded replayable stage publication"
3814    )]
3815    fn stage_exact_snapshot_retry(
3816        &mut self,
3817        request: &GitSnapshotExactRetryStageRequest<'_>,
3818    ) -> Result<GithubGitSnapshotExactRetryStageV1, TemporaryRefPublishError> {
3819        if !is_lower_sha256(request.retry_lineage_authorization_sha256) {
3820            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3821        }
3822        let precompute_request = GitSnapshotExactRetryPrecomputeRequest {
3823            parent_request: request.parent_request,
3824            parent_stage: request.parent_stage,
3825            child_operation_id: &request.expected_plan.request.operation_id,
3826            child_source_created_at_ms: request.expected_plan.source_created_at_ms,
3827        };
3828        let (actual_plan, archive_bytes) =
3829            self.precompute_exact_snapshot_retry(&precompute_request)?;
3830        if &actual_plan != request.expected_plan {
3831            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3832        }
3833        let descriptor = GitSnapshotDescriptor::from_request(
3834            &actual_plan.request,
3835            SourceBundleDescriptor::new(
3836                actual_plan.archive.clone(),
3837                actual_plan.request.source.clone(),
3838            ),
3839        )
3840        .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3841        let descriptor_bytes = canonical_git_snapshot_descriptor_bytes(&descriptor)
3842            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3843        if actual_plan.request_template_sha256 != descriptor.request_template_sha256
3844            || actual_plan.manifest_sha256 != descriptor.bundle.manifest.sha256
3845            || actual_plan.descriptor_sha256 != hex_sha256(&descriptor_bytes)
3846        {
3847            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
3848        }
3849        let operation_id = &actual_plan.request.operation_id;
3850        let stage_directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
3851            &self.isolation_directory,
3852            operation_id,
3853        )?;
3854        let archive_identity = stage_directory
3855            .write_or_verify_archive_bytes_exact_retry(&archive_bytes, &actual_plan.archive)?;
3856        let descriptor_identity =
3857            stage_directory.write_or_verify_descriptor_exact_retry(&descriptor)?;
3858        let stage = GitSnapshotStageV1 {
3859            schema_version: crate::snapshot::GIT_SNAPSHOT_STAGE_SCHEMA_VERSION,
3860            operation_id: operation_id.clone(),
3861            isolation_root_identity: stage_directory.isolation_root_identity().to_owned(),
3862            snapshots_store_identity: stage_directory.snapshots_store_identity().to_owned(),
3863            stage_directory_identity: stage_directory.stage_directory_identity().to_owned(),
3864            source_repository: request.parent_stage.source_repository.clone(),
3865            source_ref: GitSnapshotSourceRef::for_operation(operation_id)?,
3866            keepalive_ref: GitSnapshotKeepaliveRef::for_operation(operation_id)?,
3867            source_created_at_ms: actual_plan.source_created_at_ms,
3868            consent_sha256: request.retry_lineage_authorization_sha256.to_owned(),
3869            request_template_sha256: actual_plan.request_template_sha256.clone(),
3870            manifest_sha256: actual_plan.manifest_sha256.clone(),
3871            archive: actual_plan.archive.clone(),
3872            descriptor_sha256: actual_plan.descriptor_sha256.clone(),
3873            final_request: actual_plan.request.clone(),
3874            archive_file_identity: archive_identity.to_string(),
3875            descriptor_file_identity: descriptor_identity.to_string(),
3876            graph: actual_plan.graph.clone(),
3877        };
3878        let stage_locator = stage_directory.publish_or_verify_metadata_exact_retry(
3879            &stage,
3880            &descriptor,
3881            &actual_plan.request,
3882        )?;
3883        Ok(GithubGitSnapshotExactRetryStageV1 {
3884            request: actual_plan.request,
3885            stage_locator,
3886            stage,
3887        })
3888    }
3889
3890    fn local_snapshot_ref(
3891        &mut self,
3892        full_ref: &str,
3893    ) -> Result<Option<GitSha1ObjectId>, TemporaryRefPublishError> {
3894        let format = "--format=%(refname)%00%(objectname)";
3895        let output = self.execute_without_replacements(["for-each-ref", format, full_ref])?;
3896        if output.is_empty() {
3897            return Ok(None);
3898        }
3899        let mut lines = output
3900            .split(|byte| *byte == b'\n')
3901            .filter(|line| !line.is_empty());
3902        let line = lines
3903            .next()
3904            .ok_or(TemporaryRefPublishError::MalformedGitOutput)?;
3905        if lines.next().is_some() {
3906            return Err(TemporaryRefPublishError::MalformedGitOutput);
3907        }
3908        let Some(separator) = line.iter().position(|byte| *byte == 0) else {
3909            return Err(TemporaryRefPublishError::MalformedGitOutput);
3910        };
3911        if &line[..separator] != full_ref.as_bytes() {
3912            return Err(TemporaryRefPublishError::MalformedGitOutput);
3913        }
3914        let object = std::str::from_utf8(&line[separator + 1..])
3915            .map_err(|_| TemporaryRefPublishError::MalformedGitOutput)?;
3916        GitSha1ObjectId::new(object.to_owned())
3917            .map(Some)
3918            .map_err(Into::into)
3919    }
3920
3921    fn source_snapshot_remote(
3922        &mut self,
3923        expected_repository: &str,
3924    ) -> Result<VerifiedGitRemote, TemporaryRefPublishError> {
3925        if let Some(remotes) = &self.frozen_remotes {
3926            if !github_remote_matches(remotes.source.fetch.canonical_url(), expected_repository)
3927                || !github_remote_matches(remotes.source.push.canonical_url(), expected_repository)
3928            {
3929                return Err(TemporaryRefPublishError::RemoteMismatch);
3930            }
3931            return Ok(remotes.source.clone());
3932        }
3933        #[cfg(test)]
3934        {
3935            let source_remote_name = self.source_remote_name.clone();
3936            self.verify_remote(&source_remote_name, expected_repository)
3937        }
3938        #[cfg(not(test))]
3939        {
3940            Err(TemporaryRefPublishError::RemoteMismatch)
3941        }
3942    }
3943
3944    #[allow(clippy::too_many_lines)]
3945    fn prepare_publication_with_ref_policy(
3946        &mut self,
3947        request: &TemporaryRefPublishRequest<'_>,
3948        require_temporary_ref_absent: bool,
3949    ) -> Result<(PublishedTemporaryRef, VerifiedGitRemote), TemporaryRefPublishError> {
3950        if !request.authorized {
3951            return Err(TemporaryRefPublishError::Unauthorized);
3952        }
3953        if request.workflow_bytes.len() > MAX_DISPATCH_FILE_BYTES
3954            || request.manifest_bytes.len() > MAX_DISPATCH_FILE_BYTES
3955            || WorkflowFingerprint::for_workflow_bytes(request.workflow_bytes)
3956                != *request.workflow_fingerprint
3957        {
3958            return Err(TemporaryRefPublishError::WorkflowFingerprintMismatch);
3959        }
3960        validate_repository_path(request.workflow_path)
3961            .map_err(|()| TemporaryRefPublishError::MalformedGitOutput)?;
3962        let temporary_branch_namespace =
3963            temporary_namespace_for_publication(request.temporary_ref, request.operation_id)?;
3964        let (readiness, remotes) = self.doctor_with_remote(&TemporaryRefDoctorRequest {
3965            repository: request.repository,
3966            source_repository: request.source_repository,
3967            trusted_source_ref: request.trusted_source_ref,
3968            temporary_branch_namespace: &temporary_branch_namespace,
3969            workflow_path: request.workflow_path,
3970            workflow_fingerprint: request.workflow_fingerprint,
3971        })?;
3972        if let Some(snapshot_ref) = request.snapshot_source_ref {
3973            if self
3974                .remote_ref(&remotes.source.fetch, snapshot_ref.as_str())?
3975                .as_ref()
3976                != Some(request.source_revision)
3977            {
3978                return Err(TemporaryRefPublishError::GitSnapshotRefConflict);
3979            }
3980            let fetch = GitInvocation::new(
3981                [
3982                    "fetch",
3983                    "--quiet",
3984                    "--no-tags",
3985                    "--no-write-fetch-head",
3986                    remotes.source.fetch.canonical_url(),
3987                    snapshot_ref.as_str(),
3988                ],
3989                self.timeout,
3990            )
3991            .with_network(&remotes.source.fetch);
3992            self.execute_invocation(&fetch)?;
3993            self.execute_without_replacements([
3994                "cat-file",
3995                "-e",
3996                &format!("{}^{{commit}}", request.source_revision.as_str()),
3997            ])
3998            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
3999            if self
4000                .remote_ref(&remotes.source.fetch, snapshot_ref.as_str())?
4001                .as_ref()
4002                != Some(request.source_revision)
4003            {
4004                return Err(TemporaryRefPublishError::GitSnapshotRefConflict);
4005            }
4006        } else {
4007            self.execute_without_replacements([
4008                "cat-file",
4009                "-e",
4010                &format!("{}^{{commit}}", request.source_revision.as_str()),
4011            ])
4012            .map_err(|_| TemporaryRefPublishError::SourceRevisionNotTrusted)?;
4013        }
4014        self.execute_without_replacements([
4015            "cat-file",
4016            "-e",
4017            &format!("{}^{{commit}}", readiness.trusted_ref_tip.as_str()),
4018        ])
4019        .map_err(|_| TemporaryRefPublishError::TrustedRefUnavailable)?;
4020        if request.snapshot_source_ref.is_none() {
4021            self.execute_without_replacements([
4022                "merge-base",
4023                "--is-ancestor",
4024                request.source_revision.as_str(),
4025                readiness.trusted_ref_tip.as_str(),
4026            ])
4027            .map_err(|_| TemporaryRefPublishError::SourceRevisionNotTrusted)?;
4028        }
4029
4030        let full_temporary_ref = format!("refs/heads/{}", request.temporary_ref.branch().as_str());
4031        if require_temporary_ref_absent
4032            && self
4033                .remote_ref(&remotes.execution.fetch, &full_temporary_ref)?
4034                .is_some()
4035        {
4036            return Err(TemporaryRefPublishError::TemporaryRefExists);
4037        }
4038
4039        let mut reservation =
4040            IndexReservation::create(&self.isolation_directory, request.operation_id)?;
4041        let index_path = reservation.index_path().to_path_buf();
4042        let workflow_blob = self.hash_blob(request.workflow_bytes)?;
4043        let manifest_blob = self.hash_blob(request.manifest_bytes)?;
4044        self.execute_invocation(&self.index_invocation(
4045            [
4046                "update-index",
4047                "--add",
4048                "--cacheinfo",
4049                "100644",
4050                &workflow_blob,
4051                request.workflow_path,
4052            ],
4053            &index_path,
4054        ))?;
4055        self.execute_invocation(&self.index_invocation(
4056            [
4057                "update-index",
4058                "--add",
4059                "--cacheinfo",
4060                "100644",
4061                &manifest_blob,
4062                DISPATCH_MANIFEST_PATH,
4063            ],
4064            &index_path,
4065        ))?;
4066        let tree = parse_object_id(
4067            &self.execute_invocation(&self.index_invocation(["write-tree"], &index_path))?,
4068        )?;
4069        let commit_message = format!(
4070            "RustFerry temporary iPhone build {}\n",
4071            request.operation_id
4072        );
4073        let git_date = format!("@{} +0000", request.created_at_ms / 1_000);
4074        let commit_invocation = GitInvocation::new(["commit-tree", &tree], self.timeout)
4075            .with_stdin(commit_message.as_bytes())
4076            .with_environment(GitEnvironmentVariable::AuthorName, "ShiroKSH")
4077            .with_environment(
4078                GitEnvironmentVariable::AuthorEmail,
4079                "kushidashiro@gmail.com",
4080            )
4081            .with_environment(GitEnvironmentVariable::CommitterName, "ShiroKSH")
4082            .with_environment(
4083                GitEnvironmentVariable::CommitterEmail,
4084                "kushidashiro@gmail.com",
4085            )
4086            .with_environment(GitEnvironmentVariable::AuthorDate, &git_date)
4087            .with_environment(GitEnvironmentVariable::CommitterDate, &git_date);
4088        let commit = CommitSha::new(parse_object_id(
4089            &self.execute_invocation(&commit_invocation)?,
4090        )?)
4091        .map_err(|_| TemporaryRefPublishError::MalformedGitOutput)?;
4092        reservation.finish()?;
4093        Ok((
4094            PublishedTemporaryRef::new(request.temporary_ref.clone(), commit),
4095            remotes.execution,
4096        ))
4097    }
4098
4099    fn prepare_publication(
4100        &mut self,
4101        request: &TemporaryRefPublishRequest<'_>,
4102    ) -> Result<(PublishedTemporaryRef, VerifiedGitRemote), TemporaryRefPublishError> {
4103        self.prepare_publication_with_ref_policy(request, true)
4104    }
4105
4106    fn push_publication(
4107        &mut self,
4108        request: &TemporaryRefPublishRequest<'_>,
4109        publication: PublishedTemporaryRef,
4110        remote: &VerifiedGitRemote,
4111    ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
4112        let full_temporary_ref = format!("refs/heads/{}", request.temporary_ref.branch().as_str());
4113        let lease = format!("--force-with-lease={full_temporary_ref}:");
4114        let refspec = format!("{}:{full_temporary_ref}", publication.commit().as_str());
4115        let push = GitInvocation::new(
4116            [
4117                "push",
4118                "--porcelain",
4119                "--no-verify",
4120                "--atomic",
4121                &lease,
4122                remote.push.canonical_url(),
4123                &refspec,
4124            ],
4125            self.timeout,
4126        )
4127        .with_network(&remote.push);
4128        if let Err(error) = self.execute_invocation(&push) {
4129            return Err(TemporaryRefPublishFailure::uncertain(error, publication));
4130        }
4131        let confirmed = self
4132            .remote_ref(&remote.fetch, &full_temporary_ref)
4133            .map_err(|error| TemporaryRefPublishFailure::uncertain(error, publication.clone()))?;
4134        if confirmed.as_ref() != Some(publication.commit()) {
4135            return Err(TemporaryRefPublishFailure::uncertain(
4136                TemporaryRefPublishError::PublicationVerificationFailed,
4137                publication,
4138            ));
4139        }
4140        Ok(publication)
4141    }
4142}
4143
4144impl<R: GitRunner> TemporaryRefPublisher for GitTemporaryRefPublisher<R> {
4145    fn supports_git_snapshot(&self) -> bool {
4146        cfg!(any(unix, windows))
4147    }
4148
4149    fn precompute_git_snapshot(
4150        &mut self,
4151        inputs: &mut GitSnapshotPrecomputeInputs,
4152        operation_id: &str,
4153        created_at_ms: u64,
4154    ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
4155        self.precompute_snapshot_graph(inputs, operation_id, created_at_ms)
4156    }
4157
4158    fn adopt_git_snapshot_stage(
4159        &mut self,
4160        request: &IosDeviceBuildRequest,
4161    ) -> Result<(GitSnapshotStageLocatorV1, GitSnapshotStageV1), TemporaryRefPublishError> {
4162        let (locator, inputs) =
4163            GitSnapshotImportInputs::adopt_without_locator(&self.isolation_directory, request)?;
4164        let stage = inputs.stage().clone();
4165        drop(inputs);
4166        Ok((locator, stage))
4167    }
4168
4169    fn discover_git_snapshot_stages(
4170        &mut self,
4171    ) -> Result<Vec<GithubGitSnapshotRecoveryCandidateV1>, TemporaryRefPublishError> {
4172        discover_complete_git_snapshot_stages(&self.isolation_directory)?
4173            .into_iter()
4174            .map(|candidate| {
4175                let final_request = candidate.stage.final_request.clone();
4176                Ok(GithubGitSnapshotRecoveryCandidateV1 {
4177                    stage_locator: candidate.locator,
4178                    stage: candidate.stage,
4179                    descriptor: candidate.descriptor,
4180                    final_request,
4181                })
4182            })
4183            .collect()
4184    }
4185
4186    fn import_git_snapshot(
4187        &mut self,
4188        request: &GitSnapshotImportRequest<'_>,
4189    ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
4190        self.import_snapshot_graph(request)
4191    }
4192
4193    fn precompute_exact_git_snapshot_retry(
4194        &mut self,
4195        request: &GitSnapshotExactRetryPrecomputeRequest<'_>,
4196    ) -> Result<GithubGitSnapshotExactRetryPlanV1, TemporaryRefPublishError> {
4197        self.precompute_exact_snapshot_retry(request)
4198            .map(|(plan, _archive_bytes)| plan)
4199    }
4200
4201    fn stage_exact_git_snapshot_retry(
4202        &mut self,
4203        request: &GitSnapshotExactRetryStageRequest<'_>,
4204    ) -> Result<GithubGitSnapshotExactRetryStageV1, TemporaryRefPublishError> {
4205        self.stage_exact_snapshot_retry(request)
4206    }
4207
4208    fn delete_git_snapshot_stage(
4209        &mut self,
4210        request: &GitSnapshotImportRequest<'_>,
4211    ) -> Result<(), TemporaryRefPublishError> {
4212        GitSnapshotImportInputs::delete_stage_exact(
4213            &self.isolation_directory,
4214            request.locator,
4215            request.expected_stage,
4216            request.request,
4217        )
4218        .map_err(Into::into)
4219    }
4220
4221    fn reconcile_git_snapshot_keepalive(
4222        &mut self,
4223        request: &GitSnapshotKeepaliveRequest<'_>,
4224    ) -> Result<GitSnapshotRefReconciliation, TemporaryRefPublishError> {
4225        if GitSnapshotKeepaliveRef::for_operation(request.operation_id)? != *request.keepalive_ref {
4226            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
4227        }
4228        Ok(
4229            match self.local_snapshot_ref(request.keepalive_ref.as_str())? {
4230                None => GitSnapshotRefReconciliation::Absent,
4231                Some(commit) if commit == *request.commit => GitSnapshotRefReconciliation::Exact,
4232                Some(_) => GitSnapshotRefReconciliation::Conflict,
4233            },
4234        )
4235    }
4236
4237    fn create_git_snapshot_keepalive(
4238        &mut self,
4239        request: &GitSnapshotKeepaliveRequest<'_>,
4240    ) -> Result<(), TemporaryRefPublishError> {
4241        if GitSnapshotKeepaliveRef::for_operation(request.operation_id)? != *request.keepalive_ref {
4242            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
4243        }
4244        let mut inputs = self
4245            .snapshot_imports
4246            .remove(request.operation_id)
4247            .ok_or(TemporaryRefPublishError::GitSnapshotInvalid)?;
4248        if inputs.stage().graph.commit != *request.commit {
4249            self.snapshot_imports
4250                .insert(request.operation_id.to_owned(), inputs);
4251            return Err(TemporaryRefPublishError::GitSnapshotInvalid);
4252        }
4253        inputs.verify_bindings()?;
4254        let invocation = GitInvocation::new(
4255            [
4256                "update-ref",
4257                request.keepalive_ref.as_str(),
4258                request.commit.as_str(),
4259                "0000000000000000000000000000000000000000",
4260            ],
4261            self.timeout,
4262        );
4263        let mutation = self.execute_invocation(&invocation);
4264        inputs.verify_bindings()?;
4265        let reconciliation = self.reconcile_git_snapshot_keepalive(request)?;
4266        match (mutation, reconciliation) {
4267            (_, GitSnapshotRefReconciliation::Exact) => Ok(()),
4268            (_, GitSnapshotRefReconciliation::Conflict) => {
4269                Err(TemporaryRefPublishError::GitSnapshotRefConflict)
4270            }
4271            (Err(error), GitSnapshotRefReconciliation::Absent) => {
4272                self.snapshot_imports
4273                    .insert(request.operation_id.to_owned(), inputs);
4274                Err(error)
4275            }
4276            (Ok(_), GitSnapshotRefReconciliation::Absent) => {
4277                self.snapshot_imports
4278                    .insert(request.operation_id.to_owned(), inputs);
4279                Err(TemporaryRefPublishError::PublicationVerificationFailed)
4280            }
4281        }
4282    }
4283
4284    fn publish_git_snapshot_source(
4285        &mut self,
4286        request: &GitSnapshotSourcePublishRequest<'_>,
4287    ) -> Result<GitSnapshotSourcePublication, TemporaryRefPublishError> {
4288        if !request.authorized {
4289            return Err(TemporaryRefPublishError::Unauthorized);
4290        }
4291        let remote = self.source_snapshot_remote(request.source_repository)?;
4292        let full_ref = request.source_ref.as_str();
4293        let lease = format!("--force-with-lease={full_ref}:");
4294        let refspec = format!("{}:{full_ref}", request.commit.as_str());
4295        let invocation = GitInvocation::new(
4296            [
4297                "push",
4298                "--porcelain",
4299                "--no-verify",
4300                "--atomic",
4301                &lease,
4302                remote.push.canonical_url(),
4303                &refspec,
4304            ],
4305            self.timeout,
4306        )
4307        .with_network(&remote.push);
4308        let mutation = self.execute_invocation(&invocation);
4309        let reconciliation = self.reconcile_git_snapshot_source(request)?;
4310        Ok(match reconciliation {
4311            GitSnapshotRefReconciliation::Exact => GitSnapshotSourcePublication::Exact,
4312            GitSnapshotRefReconciliation::Conflict => GitSnapshotSourcePublication::Conflict,
4313            GitSnapshotRefReconciliation::Absent if mutation.is_err() => {
4314                GitSnapshotSourcePublication::Uncertain
4315            }
4316            GitSnapshotRefReconciliation::Absent => GitSnapshotSourcePublication::Uncertain,
4317        })
4318    }
4319
4320    fn reconcile_git_snapshot_source(
4321        &mut self,
4322        request: &GitSnapshotSourcePublishRequest<'_>,
4323    ) -> Result<GitSnapshotRefReconciliation, TemporaryRefPublishError> {
4324        let remote = self.source_snapshot_remote(request.source_repository)?;
4325        let expected = CommitSha::new(request.commit.as_str().to_owned())
4326            .map_err(|_| TemporaryRefPublishError::GitSnapshotInvalid)?;
4327        Ok(
4328            match self.remote_ref(&remote.fetch, request.source_ref.as_str())? {
4329                None => GitSnapshotRefReconciliation::Absent,
4330                Some(commit) if commit == expected => GitSnapshotRefReconciliation::Exact,
4331                Some(_) => GitSnapshotRefReconciliation::Conflict,
4332            },
4333        )
4334    }
4335
4336    fn delete_git_snapshot_source(
4337        &mut self,
4338        request: &GitSnapshotSourceDeleteRequest<'_>,
4339    ) -> Result<(), TemporaryRefPublishError> {
4340        if !request.authorized {
4341            return Err(TemporaryRefPublishError::Unauthorized);
4342        }
4343        let remote = self.source_snapshot_remote(request.source_repository)?;
4344        let full_ref = request.source_ref.as_str();
4345        let lease = format!(
4346            "--force-with-lease={full_ref}:{}",
4347            request.expected_commit.as_str()
4348        );
4349        let refspec = format!(":{full_ref}");
4350        let invocation = GitInvocation::new(
4351            [
4352                "push",
4353                "--porcelain",
4354                "--no-verify",
4355                "--atomic",
4356                &lease,
4357                remote.push.canonical_url(),
4358                &refspec,
4359            ],
4360            self.timeout,
4361        )
4362        .with_network(&remote.push);
4363        let mutation = self.execute_invocation(&invocation);
4364        let reconciliation =
4365            self.reconcile_git_snapshot_source(&GitSnapshotSourcePublishRequest {
4366                source_repository: request.source_repository,
4367                source_ref: request.source_ref,
4368                commit: request.expected_commit,
4369                authorized: true,
4370            })?;
4371        match (mutation, reconciliation) {
4372            (_, GitSnapshotRefReconciliation::Absent) => Ok(()),
4373            (_, GitSnapshotRefReconciliation::Conflict) => {
4374                Err(TemporaryRefPublishError::GitSnapshotRefConflict)
4375            }
4376            (Err(error), GitSnapshotRefReconciliation::Exact) => Err(error),
4377            (Ok(_), GitSnapshotRefReconciliation::Exact) => {
4378                Err(TemporaryRefPublishError::DeletionVerificationFailed)
4379            }
4380        }
4381    }
4382
4383    fn release_git_snapshot_keepalive(
4384        &mut self,
4385        request: &GitSnapshotKeepaliveRequest<'_>,
4386    ) -> Result<(), TemporaryRefPublishError> {
4387        match self.reconcile_git_snapshot_keepalive(request)? {
4388            GitSnapshotRefReconciliation::Absent => return Ok(()),
4389            GitSnapshotRefReconciliation::Conflict => {
4390                return Err(TemporaryRefPublishError::GitSnapshotRefConflict);
4391            }
4392            GitSnapshotRefReconciliation::Exact => {}
4393        }
4394        let invocation = GitInvocation::new(
4395            [
4396                "update-ref",
4397                "-d",
4398                request.keepalive_ref.as_str(),
4399                request.commit.as_str(),
4400            ],
4401            self.timeout,
4402        );
4403        let mutation = self.execute_invocation(&invocation);
4404        match (mutation, self.reconcile_git_snapshot_keepalive(request)?) {
4405            (_, GitSnapshotRefReconciliation::Absent) => Ok(()),
4406            (_, GitSnapshotRefReconciliation::Conflict) => {
4407                Err(TemporaryRefPublishError::GitSnapshotRefConflict)
4408            }
4409            (Err(error), GitSnapshotRefReconciliation::Exact) => Err(error),
4410            (Ok(_), GitSnapshotRefReconciliation::Exact) => {
4411                Err(TemporaryRefPublishError::DeletionVerificationFailed)
4412            }
4413        }
4414    }
4415
4416    fn doctor(
4417        &mut self,
4418        request: &TemporaryRefDoctorRequest<'_>,
4419    ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
4420        self.doctor_with_remote(request)
4421            .map(|(readiness, _)| readiness)
4422    }
4423
4424    fn verify_execution_workflow(
4425        &mut self,
4426        request: &ExecutionWorkflowDoctorRequest<'_>,
4427    ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
4428        self.verify_execution_workflow_bytes(request)
4429    }
4430
4431    fn prepare(
4432        &mut self,
4433        request: &TemporaryRefPublishRequest<'_>,
4434    ) -> Result<Option<PublishedTemporaryRef>, TemporaryRefPublishError> {
4435        let (publication, _) = self.prepare_publication(request)?;
4436        Ok(Some(publication))
4437    }
4438
4439    fn publication_attempt_is_process_fenced(&self) -> bool {
4440        self.runner.process_tree_is_parent_fenced()
4441    }
4442
4443    fn publication_lease_scope_sha256(&self) -> Option<&str> {
4444        Some(&self.publication_lease_scope_sha256)
4445    }
4446
4447    fn acquire_publication_lease(
4448        &mut self,
4449        request: &TemporaryRefPublicationLeaseRequest<'_>,
4450    ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
4451        if !cfg!(any(unix, windows)) {
4452            return Ok(TemporaryRefPublicationLease::Unsupported);
4453        }
4454        let key = publication_lease_key(request);
4455        if self.publication_leases.contains_key(&key) {
4456            return Ok(TemporaryRefPublicationLease::AlreadyOwned);
4457        }
4458        if self.publication_leases.len() >= MAX_JOB_RECORDS {
4459            return Err(TemporaryRefPublishError::LocalIsolationFailed);
4460        }
4461        rustferry_core::verify_directory_identity(
4462            &self.isolation_directory,
4463            &self.isolation_directory_identity,
4464        )
4465        .map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)?;
4466        let path = self
4467            .isolation_directory
4468            .join(format!(".rustferry-publication-{key}.lock"));
4469        let lease = rustferry_core::process_control::try_acquire_process_file_lease(&path)
4470            .map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)?;
4471        let Some(lease) = lease else {
4472            return Ok(TemporaryRefPublicationLease::HeldByOther);
4473        };
4474        self.publication_leases.insert(key, lease);
4475        Ok(TemporaryRefPublicationLease::Acquired)
4476    }
4477
4478    fn acquire_git_snapshot_source_lease(
4479        &mut self,
4480        request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
4481    ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
4482        if !cfg!(any(unix, windows)) {
4483            return Ok(TemporaryRefPublicationLease::Unsupported);
4484        }
4485        if repository_from_url(request.source_repository).is_none() {
4486            return Err(TemporaryRefPublishError::RemoteMismatch);
4487        }
4488        let key = git_snapshot_source_lease_key(request);
4489        if self.publication_leases.contains_key(&key) {
4490            return Ok(TemporaryRefPublicationLease::AlreadyOwned);
4491        }
4492        if self.publication_leases.len() >= MAX_JOB_RECORDS {
4493            return Err(TemporaryRefPublishError::LocalIsolationFailed);
4494        }
4495        rustferry_core::verify_directory_identity(
4496            &self.isolation_directory,
4497            &self.isolation_directory_identity,
4498        )
4499        .map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)?;
4500        let path = self
4501            .isolation_directory
4502            .join(format!(".rustferry-snapshot-source-{key}.lock"));
4503        let lease = rustferry_core::process_control::try_acquire_process_file_lease(&path)
4504            .map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)?;
4505        let Some(lease) = lease else {
4506            return Ok(TemporaryRefPublicationLease::HeldByOther);
4507        };
4508        self.publication_leases.insert(key, lease);
4509        Ok(TemporaryRefPublicationLease::Acquired)
4510    }
4511
4512    fn release_publication_lease(&mut self, request: &TemporaryRefPublicationLeaseRequest<'_>) {
4513        self.publication_leases
4514            .remove(&publication_lease_key(request));
4515    }
4516
4517    fn release_git_snapshot_source_lease(
4518        &mut self,
4519        request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
4520    ) {
4521        self.publication_leases
4522            .remove(&git_snapshot_source_lease_key(request));
4523    }
4524
4525    fn publish(
4526        &mut self,
4527        request: &TemporaryRefPublishRequest<'_>,
4528    ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
4529        let (publication, remote) = self.prepare_publication(request)?;
4530        self.push_publication(request, publication, &remote)
4531    }
4532
4533    fn publish_prepared(
4534        &mut self,
4535        request: &TemporaryRefPublishRequest<'_>,
4536        prepared: Option<&PublishedTemporaryRef>,
4537    ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
4538        let Some(expected) = prepared else {
4539            return self.publish(request);
4540        };
4541        let (publication, remote) = self.prepare_publication(request)?;
4542        if expected != &publication || expected.temporary_ref() != request.temporary_ref {
4543            return Err(TemporaryRefPublishError::PublicationVerificationFailed.into());
4544        }
4545        self.push_publication(request, publication, &remote)
4546    }
4547
4548    fn delete_temporary_ref(
4549        &mut self,
4550        request: &TemporaryRefDeleteRequest<'_>,
4551    ) -> Result<(), TemporaryRefPublishError> {
4552        if !request.authorized {
4553            return Err(TemporaryRefPublishError::Unauthorized);
4554        }
4555        let remote = self.execution_remote(request.repository)?;
4556        let full_ref = format!("refs/heads/{}", request.temporary_ref.branch().as_str());
4557        let lease = format!(
4558            "--force-with-lease={full_ref}:{}",
4559            request.expected_commit.as_str()
4560        );
4561        let delete_refspec = format!(":{full_ref}");
4562        let deletion_invocation = GitInvocation::new(
4563            [
4564                "push",
4565                "--porcelain",
4566                "--no-verify",
4567                "--atomic",
4568                &lease,
4569                remote.push.canonical_url(),
4570                &delete_refspec,
4571            ],
4572            self.timeout,
4573        )
4574        .with_network(&remote.push);
4575        let deletion = self.execute_invocation(&deletion_invocation);
4576        match deletion {
4577            Ok(_) => match self.remote_ref(&remote.fetch, &full_ref)? {
4578                None => Ok(()),
4579                Some(_) => Err(TemporaryRefPublishError::DeletionVerificationFailed),
4580            },
4581            Err(push_error) => match self.remote_ref(&remote.fetch, &full_ref) {
4582                Ok(None) => Ok(()),
4583                Ok(Some(current)) if current != *request.expected_commit => {
4584                    Err(TemporaryRefPublishError::TemporaryRefLeaseRejected)
4585                }
4586                Ok(Some(_)) | Err(_) => Err(push_error),
4587            },
4588        }
4589    }
4590}
4591
4592impl<R: GitRunner> TemporaryRefReconciler for GitTemporaryRefPublisher<R> {
4593    fn reconcile_temporary_ref(
4594        &mut self,
4595        request: &TemporaryRefReconcileRequest<'_>,
4596    ) -> Result<TemporaryRefReconciliation, TemporaryRefPublishError> {
4597        let reconstruction = TemporaryRefPublishRequest {
4598            repository: request.repository,
4599            source_repository: request.source_repository,
4600            trusted_source_ref: request.trusted_source_ref,
4601            source_revision: request.source_revision,
4602            snapshot_source_ref: request.snapshot_source_ref,
4603            temporary_ref: request.temporary_ref,
4604            workflow_path: request.workflow_path,
4605            workflow_bytes: request.workflow_bytes,
4606            workflow_fingerprint: request.workflow_fingerprint,
4607            manifest_bytes: request.manifest_bytes,
4608            operation_id: request.operation_id,
4609            created_at_ms: request.created_at_ms,
4610            authorized: true,
4611        };
4612        let (recomputed, remote) =
4613            self.prepare_publication_with_ref_policy(&reconstruction, false)?;
4614        if recomputed.temporary_ref() != request.temporary_ref
4615            || recomputed.commit() != request.expected_prepared_commit
4616        {
4617            return Ok(TemporaryRefReconciliation::Conflict);
4618        }
4619        let full_ref = format!("refs/heads/{}", request.temporary_ref.branch().as_str());
4620        let Some(tip) = self.remote_ref(&remote.fetch, &full_ref)? else {
4621            return Ok(TemporaryRefReconciliation::Missing);
4622        };
4623        if tip != *request.expected_prepared_commit {
4624            return Ok(TemporaryRefReconciliation::Conflict);
4625        }
4626        Ok(TemporaryRefReconciliation::Exact(
4627            PublishedTemporaryRef::new(request.temporary_ref.clone(), tip),
4628        ))
4629    }
4630}
4631
4632impl WorkflowFingerprint {
4633    fn for_workflow_bytes(bytes: &[u8]) -> Self {
4634        Self(hex_sha256(bytes))
4635    }
4636}
4637
4638struct IndexReservation {
4639    index_path: PathBuf,
4640    #[cfg(windows)]
4641    directory_guard: Option<std::fs::File>,
4642    finished: bool,
4643}
4644
4645impl IndexReservation {
4646    fn create(directory: &Path, operation_id: &str) -> Result<Self, TemporaryRefPublishError> {
4647        validate_ref_segment(operation_id)
4648            .map_err(|()| TemporaryRefPublishError::LocalIsolationFailed)?;
4649        let created = create_unique_index_directory(directory, operation_id)?;
4650        let directory = created.path;
4651        let index_path = directory.join("index");
4652        Ok(Self {
4653            index_path,
4654            #[cfg(windows)]
4655            directory_guard: Some(created.guard),
4656            finished: false,
4657        })
4658    }
4659
4660    fn index_path(&self) -> &Path {
4661        &self.index_path
4662    }
4663
4664    fn finish(&mut self) -> Result<(), TemporaryRefPublishError> {
4665        self.cleanup()?;
4666        self.finished = true;
4667        Ok(())
4668    }
4669
4670    fn cleanup(&mut self) -> Result<(), TemporaryRefPublishError> {
4671        remove_exact_file_if_present(&self.index_path)?;
4672        remove_exact_file_if_present(&self.index_path.with_extension("lock"))?;
4673        #[cfg(windows)]
4674        {
4675            let guard = self
4676                .directory_guard
4677                .take()
4678                .ok_or(TemporaryRefPublishError::LocalIsolationFailed)?;
4679            rustferry_core::windows_private_directory::remove_private_directory_handle(guard)
4680                .map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)
4681        }
4682        #[cfg(not(windows))]
4683        {
4684            let directory = self
4685                .index_path
4686                .parent()
4687                .ok_or(TemporaryRefPublishError::LocalIsolationFailed)?;
4688            fs::remove_dir(directory).map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)
4689        }
4690    }
4691}
4692
4693impl Drop for IndexReservation {
4694    fn drop(&mut self) {
4695        if !self.finished {
4696            let _ = self.cleanup();
4697        }
4698    }
4699}
4700
4701struct UniqueIndexDirectory {
4702    path: PathBuf,
4703    #[cfg(windows)]
4704    guard: std::fs::File,
4705}
4706
4707fn create_unique_index_directory(
4708    parent: &Path,
4709    operation_id: &str,
4710) -> Result<UniqueIndexDirectory, TemporaryRefPublishError> {
4711    for _ in 0..16 {
4712        let counter = INDEX_RESERVATION_COUNTER.fetch_add(1, Ordering::Relaxed);
4713        let mut digest = Sha256::new();
4714        digest.update(b"rustferry-index-reservation-v1\0");
4715        digest.update(operation_id.as_bytes());
4716        digest.update(std::process::id().to_be_bytes());
4717        digest.update(counter.to_be_bytes());
4718        digest.update(
4719            SystemTime::now()
4720                .duration_since(UNIX_EPOCH)
4721                .unwrap_or_default()
4722                .as_nanos()
4723                .to_be_bytes(),
4724        );
4725        let name = format!(".rustferry-index-{}", hex::encode(digest.finalize()));
4726        let path = parent.join(name);
4727        #[cfg(windows)]
4728        match rustferry_core::windows_private_directory::create_private_directory(&path) {
4729            Ok(guard) => return Ok(UniqueIndexDirectory { path, guard }),
4730            Err(_) if path.exists() => {}
4731            Err(_) => return Err(TemporaryRefPublishError::LocalIsolationFailed),
4732        }
4733        #[cfg(unix)]
4734        {
4735            use std::os::unix::fs::DirBuilderExt as _;
4736
4737            let mut builder = fs::DirBuilder::new();
4738            builder.mode(0o700);
4739            match builder.create(&path) {
4740                Ok(()) => return Ok(UniqueIndexDirectory { path }),
4741                Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
4742                Err(_) => return Err(TemporaryRefPublishError::LocalIsolationFailed),
4743            }
4744        }
4745    }
4746    Err(TemporaryRefPublishError::LocalIsolationFailed)
4747}
4748
4749/// Exact provider/run identity exposed to an independent artifact verifier.
4750#[derive(Clone, Debug, PartialEq)]
4751pub struct GithubArtifactContext {
4752    /// Provider job identifier.
4753    pub job_id: String,
4754    /// Caller operation identifier.
4755    pub operation_id: String,
4756    /// Exact GitHub Actions execution repository used for run and artifact APIs.
4757    pub repository: Repository,
4758    /// Stable GitHub database ID of the exact execution repository when durable evidence is
4759    /// requested. Stores that cannot produce durable evidence may receive `None`.
4760    pub execution_repository_id: Option<u64>,
4761    /// Exact mapped run.
4762    pub run: RunSnapshot,
4763    /// Exact configured source repository URL bound to the request provenance.
4764    pub source_repository: String,
4765    /// Exact source revision.
4766    pub source_revision: CommitSha,
4767    /// Exact temporary-ref commit dispatched to GitHub Actions.
4768    pub dispatch_revision: CommitSha,
4769    /// Canonical declarative request submitted for this job.
4770    pub request: IosDeviceBuildRequest,
4771    /// Lowercase SHA-256 of canonical compact request JSON.
4772    pub request_sha256: String,
4773}
4774
4775/// Independently verified evidence retained for one exact GitHub run attempt.
4776///
4777/// Fields are intentionally private. Only the built-in verifier can construct this value after
4778/// validating the raw artifact bytes; alternate stores remain unable to assert signed cleanup
4779/// from a manifest alone.
4780#[derive(Clone, Debug, Eq, PartialEq)]
4781pub struct GithubVerifiedRunEvidence {
4782    compile_evidence: CompilePhaseEvidence,
4783    signed_cleanup_evidence: Option<GithubSignedCleanupEvidenceV1>,
4784}
4785
4786impl GithubVerifiedRunEvidence {
4787    pub(crate) fn new(
4788        compile_evidence: CompilePhaseEvidence,
4789        signed_cleanup_evidence: Option<GithubSignedCleanupEvidenceV1>,
4790    ) -> Self {
4791        Self {
4792            compile_evidence,
4793            signed_cleanup_evidence,
4794        }
4795    }
4796
4797    /// Exact independently verified compile-phase evidence.
4798    pub fn compile_evidence(&self) -> &CompilePhaseEvidence {
4799        &self.compile_evidence
4800    }
4801
4802    /// Attempt-scoped protected-cleanup evidence, present only for a verified signed artifact.
4803    pub fn signed_cleanup_evidence(&self) -> Option<&GithubSignedCleanupEvidenceV1> {
4804        self.signed_cleanup_evidence.as_ref()
4805    }
4806}
4807
4808/// Injectable independent artifact ingestion and verification seam.
4809///
4810/// Implementations must not call back into the provider. Artifact operations may run while the
4811/// provider holds the exact job identity stable.
4812pub trait VerifiedArtifactStore: Send {
4813    /// Whether verified manifest enumeration is implemented.
4814    fn supports_listing(&self) -> bool;
4815    /// Whether verified no-clobber download is implemented.
4816    fn supports_download(&self) -> bool;
4817    /// Whether exact retained-artifact deletion is implemented.
4818    fn supports_removal(&self) -> bool;
4819    /// Whether this verifier can produce attempt-scoped signed-cleanup evidence from raw reports.
4820    fn supports_signed_cleanup_evidence(&self) -> bool {
4821        false
4822    }
4823
4824    /// Return only independently verified manifests, or an empty vector while pending.
4825    ///
4826    /// # Errors
4827    ///
4828    /// Returns a typed verification or provider failure; unverified manifests must not be returned.
4829    fn list_verified(
4830        &mut self,
4831        context: &GithubArtifactContext,
4832    ) -> RemoteBuildResult<Vec<ArtifactManifest>>;
4833
4834    /// Return evidence produced by the same independent verification as [`Self::list_verified`].
4835    ///
4836    /// The default cannot claim evidence. Implementations must never synthesize this value from a
4837    /// caller-provided manifest or durable checkpoint.
4838    ///
4839    /// # Errors
4840    ///
4841    /// Returns a typed verification failure or `None` while exact artifacts are unavailable.
4842    fn verified_run_evidence(
4843        &mut self,
4844        _context: &GithubArtifactContext,
4845    ) -> RemoteBuildResult<Option<GithubVerifiedRunEvidence>> {
4846        Ok(None)
4847    }
4848
4849    /// Download and independently verify one exact manifest artifact.
4850    ///
4851    /// # Errors
4852    ///
4853    /// Returns a typed download, identity, or integrity failure.
4854    fn download_verified(
4855        &mut self,
4856        context: &GithubArtifactContext,
4857        request: &ArtifactDownloadRequest,
4858    ) -> RemoteBuildResult<ArtifactDownloadResult>;
4859
4860    /// Delete only artifacts owned by the exact mapped run.
4861    ///
4862    /// The same exact request may be replayed after a crash. Implementations must therefore be
4863    /// idempotent, succeed when those artifacts are already absent, and never broaden deletion
4864    /// beyond the validated run identity.
4865    ///
4866    /// # Errors
4867    ///
4868    /// Returns a typed failure when exact ownership cannot be established or deletion fails.
4869    fn remove_artifacts(&mut self, context: &GithubArtifactContext) -> RemoteBuildResult<()>;
4870}
4871
4872/// Artifact store used when verified ingestion is not configured.
4873#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
4874pub struct NoVerifiedArtifactStore;
4875
4876impl VerifiedArtifactStore for NoVerifiedArtifactStore {
4877    fn supports_listing(&self) -> bool {
4878        false
4879    }
4880
4881    fn supports_download(&self) -> bool {
4882        false
4883    }
4884
4885    fn supports_removal(&self) -> bool {
4886        false
4887    }
4888
4889    fn list_verified(
4890        &mut self,
4891        _context: &GithubArtifactContext,
4892    ) -> RemoteBuildResult<Vec<ArtifactManifest>> {
4893        Err(unsupported(ProviderFeature::ArtifactListing))
4894    }
4895
4896    fn download_verified(
4897        &mut self,
4898        _context: &GithubArtifactContext,
4899        _request: &ArtifactDownloadRequest,
4900    ) -> RemoteBuildResult<ArtifactDownloadResult> {
4901        Err(unsupported(ProviderFeature::ArtifactDownload))
4902    }
4903
4904    fn remove_artifacts(&mut self, _context: &GithubArtifactContext) -> RemoteBuildResult<()> {
4905        Err(provider_failure(
4906            "artifact_removal_unsupported",
4907            "verified artifact removal is not configured",
4908            false,
4909        ))
4910    }
4911}
4912
4913/// Injectable UTC clock for deterministic protocol events.
4914pub trait ProviderClock: Send + Sync {
4915    /// Milliseconds since Unix epoch.
4916    fn now_ms(&self) -> u64;
4917}
4918
4919/// Production wall clock.
4920#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
4921pub struct SystemProviderClock;
4922
4923impl ProviderClock for SystemProviderClock {
4924    fn now_ms(&self) -> u64 {
4925        SystemTime::now()
4926            .duration_since(UNIX_EPOCH)
4927            .unwrap_or_default()
4928            .as_millis()
4929            .try_into()
4930            .unwrap_or(u64::MAX)
4931    }
4932}
4933
4934#[allow(clippy::struct_excessive_bools)]
4935#[derive(Clone, Debug)]
4936struct JobRecord {
4937    principal: Option<GithubPrincipalIdentityV1>,
4938    execution_repository_id: Option<u64>,
4939    operation_id: String,
4940    job_id: String,
4941    source_revision: CommitSha,
4942    git_snapshot: Option<GithubGitSnapshotResumeV1>,
4943    request: IosDeviceBuildRequest,
4944    request_sha256: String,
4945    temporary_ref: TemporaryGitRef,
4946    prepared_dispatch_commit: Option<CommitSha>,
4947    dispatch_commit: Option<CommitSha>,
4948    workflow_dispatch: Option<GithubWorkflowDispatchResumeV1>,
4949    created_at_ms: u64,
4950    publication_started_at_ms: u64,
4951    publication_quiescence_deadline_ms: u64,
4952    state: JobState,
4953    run: Option<RunHandle>,
4954    run_snapshot: Option<RunSnapshot>,
4955    publication_intent: bool,
4956    publication_absent: bool,
4957    publication_not_attempted: bool,
4958    publication_process_fenced: bool,
4959    publication_lease_scope_sha256: Option<String>,
4960    publication_takeover_lease_owned: bool,
4961    snapshot_source_lease_owned: bool,
4962    publication_absence_observations: u8,
4963    publication_absence_first_observed_at_ms: u64,
4964    cancellation_requested: bool,
4965    cancellation_dispatched: bool,
4966    cleanup_requested: bool,
4967    remove_artifacts_requested: bool,
4968    artifacts_removed: bool,
4969    temporary_ref_deleted: bool,
4970    verification_pending_event: bool,
4971    publication_uncertain: bool,
4972    run_discovery_attempts: u16,
4973    run_discovery_deadline_ms: u64,
4974    manifests: Vec<ArtifactManifest>,
4975    compile_evidence: Option<CompilePhaseEvidence>,
4976    signed_cleanup_evidence: Option<GithubSignedCleanupEvidenceV1>,
4977    events: Vec<RemoteBuildEvent>,
4978}
4979
4980#[derive(Debug)]
4981struct WorkflowDispatchPostAuthority {
4982    job_id: String,
4983    request: WorkflowDispatchRequest,
4984}
4985
4986impl GithubWorkflowDispatchResumeV1 {
4987    fn from_request(request: &WorkflowDispatchRequest) -> Self {
4988        Self {
4989            schema_version: GITHUB_WORKFLOW_DISPATCH_RESUME_SCHEMA_VERSION,
4990            workflow_id: request.workflow().id().get(),
4991            workflow_path: request.workflow().path().to_owned(),
4992            branch: request.temporary_ref().branch().as_str().to_owned(),
4993            operation_id: request.operation_id().to_owned(),
4994            request_sha256: request.request_sha256().to_owned(),
4995            source_revision: request.source_revision().as_str().to_owned(),
4996            dispatch_revision: request.dispatch_revision().as_str().to_owned(),
4997            body_sha256: request.body_sha256().to_owned(),
4998            run_name: request.run_name().to_owned(),
4999            uncertain: true,
5000            receipt: None,
5001        }
5002    }
5003
5004    fn request_for_record(
5005        &self,
5006        config: &GithubProviderConfig,
5007        record: &JobRecord,
5008    ) -> RemoteBuildResult<WorkflowDispatchRequest> {
5009        let dispatch_revision = record
5010            .dispatch_commit
5011            .clone()
5012            .ok_or_else(|| resume_failure("resume_workflow_dispatch_commit_missing"))?;
5013        if self.schema_version != GITHUB_WORKFLOW_DISPATCH_RESUME_SCHEMA_VERSION
5014            || config.workflow.run_trigger() != WorkflowRunTrigger::WorkflowDispatch
5015            || self.workflow_path != config.workflow.filename().repository_path()
5016            || self.branch != record.temporary_ref.branch().as_str()
5017            || self.operation_id != record.operation_id
5018            || self.request_sha256 != record.request_sha256
5019            || self.source_revision != record.source_revision.as_str()
5020            || self.dispatch_revision != dispatch_revision.as_str()
5021            || !is_lower_sha256(&self.body_sha256)
5022            || self.run_name.is_empty()
5023            || self.uncertain != self.receipt.is_none()
5024        {
5025            return Err(resume_failure("resume_workflow_dispatch_identity_mismatch"));
5026        }
5027        let workflow_id = WorkflowId::new(self.workflow_id)
5028            .map_err(|_| resume_failure("resume_workflow_dispatch_identity_invalid"))?;
5029        let registration = WorkflowRegistration::restore(
5030            config.repository.clone(),
5031            workflow_id,
5032            self.workflow_path.clone(),
5033        )
5034        .map_err(|_| resume_failure("resume_workflow_dispatch_identity_invalid"))?;
5035        let request = WorkflowDispatchRequest::new(
5036            config.repository.clone(),
5037            registration,
5038            record.temporary_ref.clone(),
5039            record.operation_id.clone(),
5040            record.request_sha256.clone(),
5041            record.source_revision.clone(),
5042            dispatch_revision,
5043        )
5044        .map_err(|_| resume_failure("resume_workflow_dispatch_identity_invalid"))?;
5045        if request.body_sha256() != self.body_sha256 || request.run_name() != self.run_name {
5046            return Err(resume_failure("resume_workflow_dispatch_body_mismatch"));
5047        }
5048        Ok(request)
5049    }
5050
5051    fn bind_receipt(&mut self, receipt: &WorkflowDispatchReceipt) -> RemoteBuildResult<()> {
5052        let candidate = GithubWorkflowDispatchReceiptV1 {
5053            run_id: receipt.run_id().get(),
5054            workflow_id: receipt.workflow_id().get(),
5055            workflow_path: receipt.workflow_path().to_owned(),
5056            branch: receipt.branch().as_str().to_owned(),
5057            dispatch_revision: receipt.dispatch_revision().as_str().to_owned(),
5058            run_name: receipt.run_name().to_owned(),
5059        };
5060        if candidate.workflow_id != self.workflow_id
5061            || candidate.workflow_path != self.workflow_path
5062            || candidate.branch != self.branch
5063            || candidate.dispatch_revision != self.dispatch_revision
5064            || candidate.run_name != self.run_name
5065            || self
5066                .receipt
5067                .as_ref()
5068                .is_some_and(|existing| existing != &candidate)
5069        {
5070            return Err(resume_failure("resume_workflow_dispatch_receipt_mismatch"));
5071        }
5072        self.receipt = Some(candidate);
5073        self.uncertain = false;
5074        Ok(())
5075    }
5076
5077    fn restored_receipt(
5078        &self,
5079        config: &GithubProviderConfig,
5080        record: &JobRecord,
5081    ) -> RemoteBuildResult<Option<WorkflowDispatchReceipt>> {
5082        self.request_for_record(config, record)?;
5083        let Some(receipt) = self.receipt.as_ref() else {
5084            return Ok(None);
5085        };
5086        if receipt.workflow_id != self.workflow_id
5087            || receipt.workflow_path != self.workflow_path
5088            || receipt.branch != self.branch
5089            || receipt.dispatch_revision != self.dispatch_revision
5090            || receipt.run_name != self.run_name
5091        {
5092            return Err(resume_failure("resume_workflow_dispatch_receipt_mismatch"));
5093        }
5094        let restored = WorkflowDispatchReceipt::restore(
5095            config.repository.clone(),
5096            RunId::new(receipt.run_id)
5097                .map_err(|_| resume_failure("resume_workflow_dispatch_receipt_invalid"))?,
5098            WorkflowId::new(receipt.workflow_id)
5099                .map_err(|_| resume_failure("resume_workflow_dispatch_receipt_invalid"))?,
5100            receipt.workflow_path.clone(),
5101            CommitSha::new(receipt.dispatch_revision.clone())
5102                .map_err(|_| resume_failure("resume_workflow_dispatch_receipt_invalid"))?,
5103            BranchName::new(receipt.branch.clone())
5104                .map_err(|_| resume_failure("resume_workflow_dispatch_receipt_invalid"))?,
5105            receipt.run_name.clone(),
5106        )
5107        .map_err(|_| resume_failure("resume_workflow_dispatch_receipt_invalid"))?;
5108        Ok(Some(restored))
5109    }
5110}
5111
5112const fn workflow_run_event(trigger: WorkflowRunTrigger) -> RunEvent {
5113    match trigger {
5114        WorkflowRunTrigger::Push => RunEvent::Push,
5115        WorkflowRunTrigger::WorkflowDispatch => RunEvent::WorkflowDispatch,
5116    }
5117}
5118
5119fn validate_workflow_dispatch_record(
5120    config: &GithubProviderConfig,
5121    record: &JobRecord,
5122) -> RemoteBuildResult<()> {
5123    let expected_event = workflow_run_event(config.workflow.run_trigger());
5124    if record
5125        .run
5126        .as_ref()
5127        .is_some_and(|run| run.event() != expected_event)
5128    {
5129        return Err(resume_failure("resume_run_trigger_mismatch"));
5130    }
5131    match config.workflow.run_trigger() {
5132        WorkflowRunTrigger::Push => {
5133            if record.workflow_dispatch.is_some()
5134                || matches!(
5135                    record.principal,
5136                    Some(GithubPrincipalIdentityV1::RepositoryCredential)
5137                )
5138            {
5139                return Err(resume_failure("resume_workflow_dispatch_unexpected"));
5140            }
5141        }
5142        WorkflowRunTrigger::WorkflowDispatch => {
5143            let Some(dispatch) = record.workflow_dispatch.as_ref() else {
5144                if record.run.is_some() {
5145                    return Err(resume_failure("resume_workflow_dispatch_intent_missing"));
5146                }
5147                return Ok(());
5148            };
5149            dispatch.request_for_record(config, record)?;
5150            let receipt = dispatch.restored_receipt(config, record)?;
5151            if record.publication_uncertain
5152                || record.dispatch_commit.is_none()
5153                || record.run.as_ref().is_some_and(|run| {
5154                    receipt
5155                        .as_ref()
5156                        .is_none_or(|receipt| receipt.run_id() != run.id())
5157                })
5158            {
5159                return Err(resume_failure(
5160                    "resume_workflow_dispatch_state_inconsistent",
5161                ));
5162            }
5163        }
5164    }
5165    Ok(())
5166}
5167
5168impl GithubRunIdentityV1 {
5169    fn from_snapshot(snapshot: &RunSnapshot) -> Self {
5170        let handle = snapshot.handle();
5171        Self {
5172            run_id: handle.id().get(),
5173            workflow_id: handle.workflow_id().get(),
5174            workflow_path: handle.workflow_path().to_owned(),
5175            head_sha: handle.head_sha().as_str().to_owned(),
5176            branch: handle.branch().as_str().to_owned(),
5177            event: handle.event().into(),
5178            run_number: snapshot.run_number(),
5179            run_attempt: snapshot.run_attempt(),
5180            status: snapshot.status().into(),
5181            conclusion: snapshot.conclusion().map(Into::into),
5182        }
5183    }
5184}
5185
5186/// Validate credential-free compile evidence against one exact GitHub request and job.
5187///
5188/// This helper is suitable for durable stores that retain compile evidence before a complete
5189/// provider resume record is available.
5190///
5191/// # Errors
5192///
5193/// Returns a secret-free provider failure when any request, source, sealed-archive, toolchain, or
5194/// provider binding is invalid.
5195pub fn validate_github_compile_phase_evidence(
5196    evidence: &CompilePhaseEvidence,
5197    request: &IosDeviceBuildRequest,
5198    job_id: &str,
5199) -> RemoteBuildResult<()> {
5200    GithubArtifactExpectation::new(
5201        job_id.to_owned(),
5202        GITHUB_PROVIDER_ID,
5203        request.clone(),
5204        evidence.clone(),
5205    )
5206    .map(|_| ())
5207    .map_err(|_| {
5208        provider_failure(
5209            "compile_evidence_invalid",
5210            "compile evidence does not match the exact GitHub request",
5211            false,
5212        )
5213    })
5214}
5215
5216fn compile_evidence_sha256(evidence: &CompilePhaseEvidence) -> RemoteBuildResult<String> {
5217    let bytes = serde_json::to_vec(evidence).map_err(|error| RemoteBuildError::Serialization {
5218        message: bounded_message(&error.to_string()),
5219    })?;
5220    Ok(hex_sha256(&bytes))
5221}
5222
5223impl GithubSignedCleanupEvidenceV1 {
5224    #[allow(clippy::too_many_arguments)]
5225    pub(crate) fn from_verified_artifact(
5226        context: &GithubArtifactContext,
5227        compile_evidence: &CompilePhaseEvidence,
5228        manifest: &ArtifactManifest,
5229        manifest_sha256: &str,
5230        github_artifact_id: u64,
5231        github_artifact_api_sha256: &str,
5232    ) -> RemoteBuildResult<Self> {
5233        validate_github_compile_phase_evidence(
5234            compile_evidence,
5235            &context.request,
5236            &context.job_id,
5237        )?;
5238        validate_manifests_for_context(context, std::slice::from_ref(manifest))?;
5239        let execution_repository_id = context
5240            .execution_repository_id
5241            .filter(|identity| *identity != 0)
5242            .ok_or_else(|| resume_failure("signed_cleanup_repository_identity_missing"))?;
5243        if context.operation_id != context.job_id
5244            || context.operation_id != context.request.operation_id
5245            || context.request.signing.mode != SigningMode::ManualDevelopment
5246            || context.request_sha256 != canonical_request_sha256(&context.request)?
5247            || !run_snapshot_is_successful(&context.run)
5248            || manifest.cleanup_status != CleanupStatus::Confirmed
5249            || manifest.signing.mode != SigningMode::ManualDevelopment
5250            || manifest.signing.status != SigningStatus::ArtifactValidated
5251            || github_artifact_id == 0
5252            || !is_lower_sha256(manifest_sha256)
5253            || !is_lower_sha256(github_artifact_api_sha256)
5254        {
5255            return Err(resume_failure("signed_cleanup_evidence_context_invalid"));
5256        }
5257        let signing_report = manifest
5258            .one_artifact(ArtifactKind::SigningReport)
5259            .map_err(|_| resume_failure("signed_cleanup_evidence_reports_invalid"))?;
5260        let validation_report = manifest
5261            .one_artifact(ArtifactKind::ValidationReport)
5262            .map_err(|_| resume_failure("signed_cleanup_evidence_reports_invalid"))?;
5263        if !is_lower_sha256(&signing_report.sha256) || !is_lower_sha256(&validation_report.sha256) {
5264            return Err(resume_failure("signed_cleanup_evidence_reports_invalid"));
5265        }
5266        Ok(Self {
5267            schema_version: GITHUB_SIGNED_CLEANUP_EVIDENCE_SCHEMA_VERSION,
5268            provider: GITHUB_PROVIDER_ID.to_owned(),
5269            operation_id: context.operation_id.clone(),
5270            job_id: context.job_id.clone(),
5271            execution_repository: repository_url(&context.repository),
5272            execution_repository_id,
5273            request_sha256: context.request_sha256.clone(),
5274            source_repository: context.source_repository.clone(),
5275            source_revision: context.source_revision.as_str().to_owned(),
5276            source_sha256: context.request.source.sha256.clone(),
5277            dispatch_revision: context.dispatch_revision.as_str().to_owned(),
5278            run: GithubRunIdentityV1::from_snapshot(&context.run),
5279            compile_evidence_sha256: compile_evidence_sha256(compile_evidence)?,
5280            manifest_sha256: manifest_sha256.to_owned(),
5281            signing_report_sha256: signing_report.sha256.clone(),
5282            validation_report_sha256: validation_report.sha256.clone(),
5283            github_artifact_id,
5284            github_artifact_api_sha256: github_artifact_api_sha256.to_owned(),
5285        })
5286    }
5287
5288    /// Validate this proof against every durable identity and artifact digest in a resume record.
5289    ///
5290    /// # Errors
5291    ///
5292    /// Rejects replay across repositories, requests, sources, dispatches, jobs, runs, attempts, or
5293    /// verified artifact sets.
5294    pub fn validate_for_resume(&self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
5295        let compile_evidence = resume
5296            .compile_evidence
5297            .as_ref()
5298            .ok_or_else(|| resume_failure("signed_cleanup_compile_evidence_missing"))?;
5299        validate_github_compile_phase_evidence(compile_evidence, &resume.request, &resume.job_id)?;
5300        let run = resume
5301            .run
5302            .as_ref()
5303            .ok_or_else(|| resume_failure("signed_cleanup_run_identity_missing"))?;
5304        let [manifest] = resume.manifests.as_slice() else {
5305            return Err(resume_failure("signed_cleanup_manifest_set_invalid"));
5306        };
5307        let manifest_record = manifest
5308            .one_artifact(ArtifactKind::Manifest)
5309            .map_err(|_| resume_failure("signed_cleanup_manifest_digest_missing"))?;
5310        let signing_report = manifest
5311            .one_artifact(ArtifactKind::SigningReport)
5312            .map_err(|_| resume_failure("signed_cleanup_report_digest_missing"))?;
5313        let validation_report = manifest
5314            .one_artifact(ArtifactKind::ValidationReport)
5315            .map_err(|_| resume_failure("signed_cleanup_report_digest_missing"))?;
5316        if self.schema_version != GITHUB_SIGNED_CLEANUP_EVIDENCE_SCHEMA_VERSION
5317            || self.provider != GITHUB_PROVIDER_ID
5318            || self.operation_id != resume.operation_id
5319            || self.job_id != resume.job_id
5320            || self.execution_repository != resume.execution_repository
5321            || self.execution_repository_id == 0
5322            || self.execution_repository_id != resume.execution_repository_id
5323            || self.request_sha256 != resume.request_sha256
5324            || self.source_repository != resume.source_repository
5325            || self.source_revision != resume.source_revision
5326            || self.source_sha256 != resume.request.source.sha256
5327            || resume.dispatch_commit.as_deref() != Some(self.dispatch_revision.as_str())
5328            || &self.run != run
5329            || self.run.status != GithubRunStatusV1::Completed
5330            || self.run.conclusion != Some(GithubRunConclusionV1::Success)
5331            || self.compile_evidence_sha256 != compile_evidence_sha256(compile_evidence)?
5332            || self.manifest_sha256 != manifest_record.sha256
5333            || self.signing_report_sha256 != signing_report.sha256
5334            || self.validation_report_sha256 != validation_report.sha256
5335            || self.github_artifact_id == 0
5336            || !is_lower_sha256(&self.compile_evidence_sha256)
5337            || !is_lower_sha256(&self.manifest_sha256)
5338            || !is_lower_sha256(&self.signing_report_sha256)
5339            || !is_lower_sha256(&self.validation_report_sha256)
5340            || !is_lower_sha256(&self.github_artifact_api_sha256)
5341            || resume.request.signing.mode != SigningMode::ManualDevelopment
5342            || manifest.operation_id != resume.operation_id
5343            || manifest.job_id != resume.job_id
5344            || manifest.provider != GITHUB_PROVIDER_ID
5345            || manifest.source_repository.as_deref() != Some(resume.source_repository.as_str())
5346            || manifest.source_revision.as_deref() != Some(resume.source_revision.as_str())
5347            || manifest.source_sha256 != compile_evidence.source_sha256
5348            || manifest.cargo_lock_sha256 != compile_evidence.cargo_lock_sha256
5349            || manifest.config_sha256 != compile_evidence.config_sha256
5350            || manifest.cleanup_status != CleanupStatus::Confirmed
5351            || manifest.signing.mode != SigningMode::ManualDevelopment
5352            || manifest.signing.status != SigningStatus::ArtifactValidated
5353        {
5354            return Err(resume_failure("signed_cleanup_evidence_mismatch"));
5355        }
5356        Ok(())
5357    }
5358}
5359
5360impl GithubGitSnapshotResumeV1 {
5361    #[allow(
5362        clippy::too_many_lines,
5363        reason = "one validator keeps every snapshot phase field invariant adjacent"
5364    )]
5365    fn validate_for_request(&self, request: &IosDeviceBuildRequest) -> RemoteBuildResult<()> {
5366        if self.schema_version != GITHUB_GIT_SNAPSHOT_RESUME_SCHEMA_VERSION
5367            || request.source_mode != SourceMode::GitSnapshot
5368            || request.signing.mode != SigningMode::UnsignedCompileOnly
5369        {
5370            return Err(resume_failure("resume_git_snapshot_mode_invalid"));
5371        }
5372        self.stage_locator
5373            .validate_for_operation(&request.operation_id)
5374            .map_err(|_| resume_failure("resume_git_snapshot_locator_invalid"))?;
5375        let descriptor = GitSnapshotDescriptor::from_request(
5376            request,
5377            SourceBundleDescriptor::new(self.stage.archive.clone(), request.source.clone()),
5378        )
5379        .map_err(|_| resume_failure("resume_git_snapshot_descriptor_invalid"))?;
5380        self.stage
5381            .validate_for_request(&descriptor, request)
5382            .map_err(|_| resume_failure("resume_git_snapshot_stage_invalid"))?;
5383        if self.stage_locator.operation_id != self.stage.operation_id
5384            || self.stage_locator.isolation_root_identity != self.stage.isolation_root_identity
5385            || self.stage_locator.snapshots_store_identity != self.stage.snapshots_store_identity
5386            || self.stage_locator.stage_directory_identity != self.stage.stage_directory_identity
5387            || self.source_publication_attempts > 3
5388            || self.source_publication_absence_observations > 2
5389            || (self.source_publication_absence_observations == 0)
5390                != (self.source_publication_absence_first_observed_at_ms == 0)
5391            || (self.source_publication_absence_observations == 0)
5392                != (self.source_publication_absence_last_observed_at_ms == 0)
5393            || self.source_publication_absence_last_observed_at_ms
5394                < self.source_publication_absence_first_observed_at_ms
5395            || self.source_publication_process_fenced
5396                != self.source_publication_lease_scope_sha256.is_some()
5397            || self
5398                .source_publication_lease_scope_sha256
5399                .as_ref()
5400                .is_some_and(|scope| !is_lower_sha256(scope))
5401            || self
5402                .keepalive_release_authorization_sha256
5403                .as_ref()
5404                .is_some_and(|digest| !is_lower_sha256(digest))
5405        {
5406            return Err(resume_failure("resume_git_snapshot_binding_invalid"));
5407        }
5408        let before_source = matches!(
5409            self.phase,
5410            GithubGitSnapshotPhaseV1::Prepared
5411                | GithubGitSnapshotPhaseV1::KeepaliveIntent
5412                | GithubGitSnapshotPhaseV1::KeepaliveExact
5413                | GithubGitSnapshotPhaseV1::StageDeleteIntent
5414                | GithubGitSnapshotPhaseV1::StageDeleted
5415        );
5416        if before_source
5417            && (self.source_publication_attempts != 0
5418                || self.source_publication_started_at_ms != 0
5419                || self.source_publication_quiescence_deadline_ms != 0
5420                || self.source_publication_process_fenced
5421                || self.source_publication_absence_observations != 0)
5422        {
5423            return Err(resume_failure("resume_git_snapshot_source_started_early"));
5424        }
5425        let source_phase = !before_source;
5426        if source_phase && self.source_publication_attempts == 0 {
5427            return Err(resume_failure("resume_git_snapshot_source_intent_missing"));
5428        }
5429        if source_phase && !self.source_publication_process_fenced {
5430            return Err(resume_failure("resume_git_snapshot_source_fence_missing"));
5431        }
5432        if self.source_publication_started_at_ms == 0
5433            && source_phase
5434            && self.source_publication_quiescence_deadline_ms != u64::MAX
5435        {
5436            return Err(resume_failure(
5437                "resume_git_snapshot_source_boundary_invalid",
5438            ));
5439        }
5440        if self.source_publication_started_at_ms != 0
5441            && self.source_publication_quiescence_deadline_ms
5442                < self.source_publication_started_at_ms
5443        {
5444            return Err(resume_failure(
5445                "resume_git_snapshot_source_boundary_invalid",
5446            ));
5447        }
5448        if matches!(
5449            self.phase,
5450            GithubGitSnapshotPhaseV1::SourceExact
5451                | GithubGitSnapshotPhaseV1::SourceConflict
5452                | GithubGitSnapshotPhaseV1::SourceCleanupIntent
5453                | GithubGitSnapshotPhaseV1::SourceDeleted
5454                | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
5455                | GithubGitSnapshotPhaseV1::KeepaliveReleased
5456        ) && self.source_publication_started_at_ms == 0
5457        {
5458            return Err(resume_failure(
5459                "resume_git_snapshot_source_ownership_unarmed",
5460            ));
5461        }
5462        if self.phase == GithubGitSnapshotPhaseV1::SourceAbsent
5463            && (!self.source_publication_process_fenced
5464                || self.source_publication_absence_observations < 2
5465                || self.source_publication_started_at_ms == 0
5466                || self.source_publication_absence_last_observed_at_ms
5467                    < self.source_publication_quiescence_deadline_ms)
5468        {
5469            return Err(resume_failure("resume_git_snapshot_absence_unproven"));
5470        }
5471        let keepalive_release_phase = matches!(
5472            self.phase,
5473            GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
5474                | GithubGitSnapshotPhaseV1::KeepaliveReleased
5475        );
5476        if keepalive_release_phase != self.keepalive_release_authorization_sha256.is_some() {
5477            return Err(resume_failure(
5478                "resume_git_snapshot_keepalive_release_authority_invalid",
5479            ));
5480        }
5481        Ok(())
5482    }
5483
5484    #[allow(
5485        clippy::too_many_lines,
5486        reason = "the snapshot reducer keeps every durable phase and retry edge in one audit boundary"
5487    )]
5488    fn validate_successor(&self, next: &Self) -> RemoteBuildResult<()> {
5489        if self.schema_version != next.schema_version
5490            || self.stage_locator != next.stage_locator
5491            || self.stage != next.stage
5492            || self.source_publication_process_fenced && !next.source_publication_process_fenced
5493            || !option_can_bind_once(
5494                self.source_publication_lease_scope_sha256.as_deref(),
5495                next.source_publication_lease_scope_sha256.as_deref(),
5496            )
5497            || !option_can_bind_once(
5498                self.keepalive_release_authorization_sha256.as_deref(),
5499                next.keepalive_release_authorization_sha256.as_deref(),
5500            )
5501        {
5502            return Err(resume_failure("resume_git_snapshot_identity_changed"));
5503        }
5504        let allowed_phase = self.phase == next.phase
5505            || matches!(
5506                (self.phase, next.phase),
5507                (
5508                    GithubGitSnapshotPhaseV1::Prepared,
5509                    GithubGitSnapshotPhaseV1::KeepaliveIntent
5510                ) | (
5511                    GithubGitSnapshotPhaseV1::KeepaliveIntent,
5512                    GithubGitSnapshotPhaseV1::KeepaliveExact
5513                ) | (
5514                    GithubGitSnapshotPhaseV1::KeepaliveExact,
5515                    GithubGitSnapshotPhaseV1::StageDeleteIntent
5516                ) | (
5517                    GithubGitSnapshotPhaseV1::StageDeleteIntent,
5518                    GithubGitSnapshotPhaseV1::StageDeleted
5519                ) | (
5520                    GithubGitSnapshotPhaseV1::StageDeleted | GithubGitSnapshotPhaseV1::SourceAbsent,
5521                    GithubGitSnapshotPhaseV1::SourcePublishIntent
5522                ) | (
5523                    GithubGitSnapshotPhaseV1::SourcePublishIntent,
5524                    GithubGitSnapshotPhaseV1::SourceExact
5525                        | GithubGitSnapshotPhaseV1::SourceAbsent
5526                        | GithubGitSnapshotPhaseV1::SourceConflict
5527                ) | (
5528                    GithubGitSnapshotPhaseV1::SourceExact,
5529                    GithubGitSnapshotPhaseV1::SourceCleanupIntent
5530                        | GithubGitSnapshotPhaseV1::SourceConflict
5531                ) | (
5532                    GithubGitSnapshotPhaseV1::SourceCleanupIntent,
5533                    GithubGitSnapshotPhaseV1::SourceDeleted
5534                ) | (
5535                    GithubGitSnapshotPhaseV1::SourceDeleted
5536                        | GithubGitSnapshotPhaseV1::SourceAbsent
5537                        | GithubGitSnapshotPhaseV1::SourceConflict,
5538                    GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
5539                ) | (
5540                    GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent,
5541                    GithubGitSnapshotPhaseV1::KeepaliveReleased
5542                )
5543            );
5544        if !allowed_phase {
5545            return Err(resume_failure("resume_git_snapshot_phase_regressed"));
5546        }
5547        let retrying_absent = self.phase == GithubGitSnapshotPhaseV1::SourceAbsent
5548            && next.phase == GithubGitSnapshotPhaseV1::SourcePublishIntent;
5549        let first_source_intent = self.phase == GithubGitSnapshotPhaseV1::StageDeleted
5550            && next.phase == GithubGitSnapshotPhaseV1::SourcePublishIntent;
5551        let expected_attempts = if retrying_absent || first_source_intent {
5552            self.source_publication_attempts
5553                .checked_add(1)
5554                .ok_or_else(|| resume_failure("resume_git_snapshot_attempt_overflow"))?
5555        } else {
5556            self.source_publication_attempts
5557        };
5558        if next.source_publication_attempts != expected_attempts {
5559            return Err(resume_failure("resume_git_snapshot_attempt_invalid"));
5560        }
5561        if retrying_absent {
5562            if next.source_publication_started_at_ms != 0
5563                || next.source_publication_quiescence_deadline_ms != u64::MAX
5564                || next.source_publication_absence_observations != 0
5565                || next.source_publication_absence_first_observed_at_ms != 0
5566                || next.source_publication_absence_last_observed_at_ms != 0
5567            {
5568                return Err(resume_failure("resume_git_snapshot_retry_not_rearmed"));
5569            }
5570        } else {
5571            if self.source_publication_started_at_ms != 0
5572                && self.source_publication_started_at_ms != next.source_publication_started_at_ms
5573            {
5574                return Err(resume_failure("resume_git_snapshot_start_changed"));
5575            }
5576            if self.source_publication_quiescence_deadline_ms != 0
5577                && self.source_publication_started_at_ms != 0
5578                && self.source_publication_quiescence_deadline_ms
5579                    != next.source_publication_quiescence_deadline_ms
5580            {
5581                return Err(resume_failure("resume_git_snapshot_deadline_changed"));
5582            }
5583            if self.source_publication_absence_observations
5584                > next.source_publication_absence_observations
5585                || self.source_publication_absence_first_observed_at_ms != 0
5586                    && self.source_publication_absence_first_observed_at_ms
5587                        != next.source_publication_absence_first_observed_at_ms
5588                || self.source_publication_absence_last_observed_at_ms
5589                    > next.source_publication_absence_last_observed_at_ms
5590            {
5591                return Err(resume_failure("resume_git_snapshot_absence_regressed"));
5592            }
5593        }
5594        Ok(())
5595    }
5596}
5597
5598impl GithubJobResumeV1 {
5599    /// Validate the trigger-specific durable projection without live configuration or credentials.
5600    ///
5601    /// # Errors
5602    ///
5603    /// Rejects a forged or incomplete `WorkflowDispatch` intent, receipt, correlation title, run
5604    /// identity, or a legacy Push record carrying repository-scoped identity.
5605    #[allow(clippy::too_many_lines)]
5606    pub fn validate_trigger_binding(&self) -> RemoteBuildResult<()> {
5607        let Some(dispatch) = self.workflow_dispatch.as_deref() else {
5608            if self.run.as_ref().is_some_and(|run| {
5609                matches!(
5610                    self.principal,
5611                    GithubPrincipalIdentityV1::RepositoryCredential
5612                ) || run.event != GithubRunEventV1::Push
5613            }) {
5614                return Err(resume_failure("resume_run_trigger_mismatch"));
5615            }
5616            return Ok(());
5617        };
5618        if !self.publication_intent
5619            || self.publication_uncertain
5620            || self.dispatch_commit.as_deref() != Some(dispatch.dispatch_revision.as_str())
5621            || dispatch.schema_version != GITHUB_WORKFLOW_DISPATCH_RESUME_SCHEMA_VERSION
5622            || dispatch.workflow_path != self.workflow_path
5623            || dispatch.operation_id != self.operation_id
5624            || dispatch.operation_id != self.request.operation_id
5625            || dispatch.request_sha256 != self.request_sha256
5626            || dispatch.source_revision != self.source_revision
5627            || dispatch.uncertain != dispatch.receipt.is_none()
5628        {
5629            return Err(resume_failure("resume_workflow_dispatch_identity_mismatch"));
5630        }
5631        self.request.validate()?;
5632        if canonical_request_sha256(&self.request)? != self.request_sha256 {
5633            return Err(resume_failure("resume_workflow_dispatch_request_mismatch"));
5634        }
5635        let repository = repository_from_url(&self.execution_repository)
5636            .ok_or_else(|| resume_failure("resume_execution_repository_invalid"))?;
5637        let branch = self
5638            .temporary_ref
5639            .strip_prefix("refs/heads/")
5640            .ok_or_else(|| resume_failure("resume_temporary_ref_invalid"))?;
5641        let suffix = format!("/{}", self.operation_id);
5642        let namespace = branch
5643            .strip_suffix(&suffix)
5644            .ok_or_else(|| resume_failure("resume_workflow_dispatch_branch_mismatch"))?;
5645        let namespace = TemporaryBranchNamespace::new(namespace)
5646            .map_err(|_| resume_failure("resume_temporary_ref_invalid"))?;
5647        let temporary_ref = TemporaryGitRef::new(
5648            &namespace,
5649            BranchName::new(branch).map_err(|_| resume_failure("resume_temporary_ref_invalid"))?,
5650        )
5651        .map_err(|_| resume_failure("resume_temporary_ref_invalid"))?;
5652        if dispatch.branch != temporary_ref.branch().as_str() {
5653            return Err(resume_failure("resume_workflow_dispatch_branch_mismatch"));
5654        }
5655        let registration = WorkflowRegistration::restore(
5656            repository.clone(),
5657            WorkflowId::new(dispatch.workflow_id)
5658                .map_err(|_| resume_failure("resume_workflow_dispatch_identity_invalid"))?,
5659            dispatch.workflow_path.clone(),
5660        )
5661        .map_err(|_| resume_failure("resume_workflow_dispatch_identity_invalid"))?;
5662        let request = WorkflowDispatchRequest::new(
5663            repository,
5664            registration,
5665            temporary_ref,
5666            self.operation_id.clone(),
5667            self.request_sha256.clone(),
5668            CommitSha::new(self.source_revision.clone())
5669                .map_err(|_| resume_failure("resume_source_revision_invalid"))?,
5670            CommitSha::new(dispatch.dispatch_revision.clone())
5671                .map_err(|_| resume_failure("resume_dispatch_commit_invalid"))?,
5672        )
5673        .map_err(|_| resume_failure("resume_workflow_dispatch_identity_invalid"))?;
5674        if request.body_sha256() != dispatch.body_sha256 || request.run_name() != dispatch.run_name
5675        {
5676            return Err(resume_failure("resume_workflow_dispatch_body_mismatch"));
5677        }
5678        if dispatch.receipt.as_ref().is_some_and(|receipt| {
5679            receipt.run_id == 0
5680                || receipt.workflow_id != dispatch.workflow_id
5681                || receipt.workflow_path != dispatch.workflow_path
5682                || receipt.branch != dispatch.branch
5683                || receipt.dispatch_revision != dispatch.dispatch_revision
5684                || receipt.run_name != dispatch.run_name
5685        }) {
5686            return Err(resume_failure("resume_workflow_dispatch_receipt_mismatch"));
5687        }
5688        match (&dispatch.receipt, &self.run) {
5689            (None, Some(_)) => Err(resume_failure("resume_workflow_dispatch_receipt_missing")),
5690            (None | Some(_), None) => Ok(()),
5691            (Some(receipt), Some(run))
5692                if receipt.run_id == run.run_id
5693                    && receipt.workflow_id == dispatch.workflow_id
5694                    && receipt.workflow_path == dispatch.workflow_path
5695                    && receipt.branch == dispatch.branch
5696                    && receipt.dispatch_revision == dispatch.dispatch_revision
5697                    && receipt.run_name == dispatch.run_name
5698                    && run.workflow_id == receipt.workflow_id
5699                    && run.workflow_path == receipt.workflow_path
5700                    && run.head_sha == receipt.dispatch_revision
5701                    && run.branch == receipt.branch
5702                    && run.event == GithubRunEventV1::WorkflowDispatch
5703                    && run.run_attempt == 1 =>
5704            {
5705                Ok(())
5706            }
5707            (Some(_), Some(_)) => Err(resume_failure("resume_workflow_dispatch_run_mismatch")),
5708        }
5709    }
5710
5711    /// Encode one complete compact JSON checkpoint followed by a newline.
5712    ///
5713    /// # Errors
5714    ///
5715    /// Returns a typed serialization or size-bound failure.
5716    pub fn encode(&self) -> RemoteBuildResult<Vec<u8>> {
5717        let mut bytes =
5718            serde_json::to_vec(self).map_err(|error| RemoteBuildError::Serialization {
5719                message: bounded_message(&error.to_string()),
5720            })?;
5721        bytes.push(b'\n');
5722        if bytes.len() > GITHUB_JOB_RESUME_MAX_BYTES {
5723            return Err(resume_failure("resume_record_too_large"));
5724        }
5725        Ok(bytes)
5726    }
5727
5728    /// Decode one bounded JSON checkpoint, rejecting duplicate and unknown fields.
5729    ///
5730    /// # Errors
5731    ///
5732    /// Returns a secret-free typed failure for empty, oversized, duplicate-key, trailing, or
5733    /// structurally invalid JSON. Provider identity is revalidated by `restore_job_resumes`.
5734    pub fn decode(bytes: &[u8]) -> RemoteBuildResult<Self> {
5735        let raw: serde_json::Value = crate::strict_json::decode(bytes, GITHUB_JOB_RESUME_MAX_BYTES)
5736            .map_err(|_| resume_failure("resume_record_json_invalid"))?;
5737        let resume: Self = serde_json::from_value(raw.clone())
5738            .map_err(|_| resume_failure("resume_record_json_invalid"))?;
5739        let normalized = serde_json::to_value(&resume)
5740            .map_err(|_| resume_failure("resume_record_json_invalid"))?;
5741        if normalized != raw {
5742            return Err(resume_failure("resume_record_json_invalid"));
5743        }
5744        Ok(resume)
5745    }
5746
5747    /// Validate that `next` is a non-stale complete replacement for this checkpoint.
5748    ///
5749    /// This validates revision ordering only. A decoded record still requires provider-specific
5750    /// configuration and live identity validation through [`GithubBuildProvider::restore_job_resumes`].
5751    ///
5752    /// # Errors
5753    ///
5754    /// Returns a secret-free typed failure when immutable identity changes or durable state,
5755    /// events, manifests, attempts, or mutation results regress.
5756    #[allow(clippy::too_many_lines)]
5757    pub fn validate_successor(&self, next: &Self) -> RemoteBuildResult<()> {
5758        if self.schema_version != next.schema_version
5759            || self.provider != next.provider
5760            || self.provider_config_sha256 != next.provider_config_sha256
5761            || self.principal != next.principal
5762            || self.execution_repository != next.execution_repository
5763            || self.execution_repository_id != next.execution_repository_id
5764            || self.source_repository != next.source_repository
5765            || self.trusted_source_ref != next.trusted_source_ref
5766            || self.workflow_path != next.workflow_path
5767            || self.workflow_sha256 != next.workflow_sha256
5768            || self.temporary_ref != next.temporary_ref
5769            || self.operation_id != next.operation_id
5770            || self.job_id != next.job_id
5771            || self.request != next.request
5772            || self.request_sha256 != next.request_sha256
5773            || self.source_revision != next.source_revision
5774            || self.created_at_ms != next.created_at_ms
5775            || self.publication_process_fenced && !next.publication_process_fenced
5776            || !option_can_bind_once(
5777                self.publication_lease_scope_sha256.as_deref(),
5778                next.publication_lease_scope_sha256.as_deref(),
5779            )
5780            || next
5781                .publication_lease_scope_sha256
5782                .as_ref()
5783                .is_some_and(|scope| !is_lower_sha256(scope))
5784            || self.run_discovery_deadline_ms != next.run_discovery_deadline_ms
5785        {
5786            return Err(resume_failure("resume_successor_identity_changed"));
5787        }
5788        match (&self.git_snapshot, &next.git_snapshot) {
5789            (None, None) => {}
5790            (Some(previous), Some(successor)) => {
5791                previous.validate_for_request(&self.request)?;
5792                successor.validate_for_request(&next.request)?;
5793                previous.validate_successor(successor)?;
5794            }
5795            _ => return Err(resume_failure("resume_successor_git_snapshot_replaced")),
5796        }
5797        if next.publication_intent
5798            && next.git_snapshot.as_ref().is_some_and(|snapshot| {
5799                !matches!(
5800                    snapshot.phase,
5801                    GithubGitSnapshotPhaseV1::SourceExact
5802                        | GithubGitSnapshotPhaseV1::SourceCleanupIntent
5803                        | GithubGitSnapshotPhaseV1::SourceDeleted
5804                        | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
5805                        | GithubGitSnapshotPhaseV1::KeepaliveReleased
5806                )
5807            })
5808        {
5809            return Err(resume_failure(
5810                "resume_successor_dispatch_before_snapshot_source",
5811            ));
5812        }
5813        if !publication_boundary_can_advance(self, next) {
5814            return Err(resume_failure(
5815                "resume_successor_publication_boundary_changed",
5816            ));
5817        }
5818        if !job_state_can_reach(self.state, next.state) {
5819            return Err(resume_failure("resume_successor_state_regressed"));
5820        }
5821        if !option_can_bind_once(
5822            self.compile_evidence.as_ref(),
5823            next.compile_evidence.as_ref(),
5824        ) {
5825            return Err(resume_failure("resume_successor_compile_evidence_changed"));
5826        }
5827        if !option_can_bind_once(
5828            self.signed_cleanup_evidence.as_ref(),
5829            next.signed_cleanup_evidence.as_ref(),
5830        ) {
5831            return Err(resume_failure(
5832                "resume_successor_signed_cleanup_evidence_changed",
5833            ));
5834        }
5835        if let Some(compile_evidence) = next.compile_evidence.as_ref() {
5836            validate_github_compile_phase_evidence(compile_evidence, &next.request, &next.job_id)?;
5837        }
5838        if let Some(evidence) = next.signed_cleanup_evidence.as_ref() {
5839            evidence.validate_for_resume(next)?;
5840        }
5841        if next.state == JobState::Cleaned {
5842            let cleanup_proven = match next.request.signing.mode {
5843                SigningMode::UnsignedCompileOnly => next
5844                    .run
5845                    .as_ref()
5846                    .is_some_and(|run| run.status == GithubRunStatusV1::Completed),
5847                SigningMode::ManualDevelopment => next.signed_cleanup_evidence.is_some(),
5848                _ => false,
5849            };
5850            if !cleanup_proven {
5851                return Err(resume_failure(
5852                    "resume_successor_signing_cleanup_evidence_missing",
5853                ));
5854            }
5855        }
5856        if matches!(
5857            next.state,
5858            JobState::Cleaning | JobState::Cleaned | JobState::CleanupFailed
5859        ) && next.git_snapshot.as_ref().is_some_and(|snapshot| {
5860            !matches!(
5861                snapshot.phase,
5862                GithubGitSnapshotPhaseV1::SourceDeleted
5863                    | GithubGitSnapshotPhaseV1::SourceAbsent
5864                    | GithubGitSnapshotPhaseV1::SourceConflict
5865                    | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
5866                    | GithubGitSnapshotPhaseV1::KeepaliveReleased
5867            )
5868        }) {
5869            return Err(resume_failure(
5870                "resume_successor_snapshot_source_cleanup_missing",
5871            ));
5872        }
5873        if !option_can_bind_once(
5874            self.prepared_dispatch_commit.as_deref(),
5875            next.prepared_dispatch_commit.as_deref(),
5876        ) {
5877            return Err(resume_failure("resume_successor_prepared_dispatch_changed"));
5878        }
5879        if !option_can_bind_once(
5880            self.dispatch_commit.as_deref(),
5881            next.dispatch_commit.as_deref(),
5882        ) {
5883            return Err(resume_failure("resume_successor_dispatch_changed"));
5884        }
5885        if matches!(
5886            (
5887                next.prepared_dispatch_commit.as_deref(),
5888                next.dispatch_commit.as_deref(),
5889            ),
5890            (Some(prepared), Some(dispatch)) if prepared != dispatch
5891        ) {
5892            return Err(resume_failure("resume_successor_dispatch_binding_mismatch"));
5893        }
5894        match (&self.workflow_dispatch, &next.workflow_dispatch) {
5895            (None, _) => {}
5896            (Some(_), None) => {
5897                return Err(resume_failure(
5898                    "resume_successor_workflow_dispatch_intent_removed",
5899                ));
5900            }
5901            (Some(previous), Some(candidate)) => {
5902                if previous.schema_version != candidate.schema_version
5903                    || previous.workflow_id != candidate.workflow_id
5904                    || previous.workflow_path != candidate.workflow_path
5905                    || previous.branch != candidate.branch
5906                    || previous.operation_id != candidate.operation_id
5907                    || previous.request_sha256 != candidate.request_sha256
5908                    || previous.source_revision != candidate.source_revision
5909                    || previous.dispatch_revision != candidate.dispatch_revision
5910                    || previous.body_sha256 != candidate.body_sha256
5911                    || previous.run_name != candidate.run_name
5912                    || previous.receipt.is_some() && previous.receipt != candidate.receipt
5913                    || previous.receipt.is_none()
5914                        && candidate.receipt.is_none()
5915                        && previous.uncertain != candidate.uncertain
5916                    || candidate.uncertain != candidate.receipt.is_none()
5917                {
5918                    return Err(resume_failure("resume_successor_workflow_dispatch_changed"));
5919                }
5920            }
5921        }
5922        match (&self.run, &next.run) {
5923            (None, _) => {}
5924            (Some(_), None) => return Err(resume_failure("resume_successor_run_removed")),
5925            (Some(previous), Some(candidate)) => {
5926                if previous.run_id != candidate.run_id
5927                    || previous.workflow_id != candidate.workflow_id
5928                    || previous.workflow_path != candidate.workflow_path
5929                    || previous.head_sha != candidate.head_sha
5930                    || previous.branch != candidate.branch
5931                    || previous.event != candidate.event
5932                    || previous.run_number != candidate.run_number
5933                    || previous.run_attempt != candidate.run_attempt
5934                {
5935                    return Err(resume_failure("resume_successor_run_identity_changed"));
5936                }
5937                if run_status_phase(candidate.status) < run_status_phase(previous.status)
5938                    || previous.conclusion.is_some() && previous.conclusion != candidate.conclusion
5939                {
5940                    return Err(resume_failure("resume_successor_run_regressed"));
5941                }
5942            }
5943        }
5944        if !bool_is_monotonic(self.publication_intent, next.publication_intent)
5945            || !bool_is_monotonic(self.publication_absent, next.publication_absent)
5946            || !bool_is_monotonic(
5947                self.publication_not_attempted,
5948                next.publication_not_attempted,
5949            )
5950            || !bool_is_monotonic(self.cancellation_requested, next.cancellation_requested)
5951            || !bool_is_monotonic(self.cancellation_dispatched, next.cancellation_dispatched)
5952            || !bool_is_monotonic(self.cleanup_requested, next.cleanup_requested)
5953            || !bool_is_monotonic(
5954                self.remove_artifacts_requested,
5955                next.remove_artifacts_requested,
5956            )
5957            || !bool_is_monotonic(self.artifacts_removed, next.artifacts_removed)
5958            || !bool_is_monotonic(self.temporary_ref_deleted, next.temporary_ref_deleted)
5959            || !bool_is_monotonic(
5960                self.verification_pending_event,
5961                next.verification_pending_event,
5962            )
5963        {
5964            return Err(resume_failure("resume_successor_flag_regressed"));
5965        }
5966        if self.publication_uncertain
5967            && !next.publication_uncertain
5968            && next.dispatch_commit.is_none()
5969            && !next.publication_absent
5970        {
5971            return Err(resume_failure(
5972                "resume_successor_publication_evidence_removed",
5973            ));
5974        }
5975        if next.run_discovery_attempts < self.run_discovery_attempts {
5976            return Err(resume_failure("resume_successor_discovery_regressed"));
5977        }
5978        if next.publication_absence_observations < self.publication_absence_observations {
5979            return Err(resume_failure(
5980                "resume_successor_publication_observations_regressed",
5981            ));
5982        }
5983        if self.publication_absence_first_observed_at_ms != 0
5984            && self.publication_absence_first_observed_at_ms
5985                != next.publication_absence_first_observed_at_ms
5986        {
5987            return Err(resume_failure(
5988                "resume_successor_publication_first_observation_changed",
5989            ));
5990        }
5991        if self.events.len() > next.events.len()
5992            || self.events.as_slice() != &next.events[..self.events.len()]
5993        {
5994            return Err(resume_failure("resume_successor_events_regressed"));
5995        }
5996        if !self.manifests.is_empty() && self.manifests != next.manifests {
5997            return Err(resume_failure("resume_successor_manifests_changed"));
5998        }
5999        Ok(())
6000    }
6001
6002    fn from_record(
6003        config: &GithubProviderConfig,
6004        principal: GithubPrincipalIdentityV1,
6005        execution_repository_id: u64,
6006        record: &JobRecord,
6007    ) -> RemoteBuildResult<Self> {
6008        if execution_repository_id == 0 {
6009            return Err(resume_failure("resume_repository_identity_invalid"));
6010        }
6011        validate_workflow_dispatch_record(config, record)?;
6012        let run = match (&record.run, &record.run_snapshot) {
6013            (None, None) => None,
6014            (Some(handle), Some(snapshot)) if snapshot.handle() == handle => {
6015                Some(GithubRunIdentityV1::from_snapshot(snapshot))
6016            }
6017            _ => return Err(resume_failure("resume_run_identity_incomplete")),
6018        };
6019        Ok(Self {
6020            schema_version: GITHUB_JOB_RESUME_SCHEMA_VERSION,
6021            provider: GITHUB_PROVIDER_ID.to_owned(),
6022            provider_config_sha256: provider_config_sha256(config)?,
6023            principal,
6024            execution_repository: repository_url(&config.repository),
6025            execution_repository_id,
6026            source_repository: config.source_repository.clone(),
6027            trusted_source_ref: config.workflow.trusted_source_ref().as_str().to_owned(),
6028            workflow_path: config.workflow.filename().repository_path(),
6029            workflow_sha256: config.workflow_fingerprint.as_str().to_owned(),
6030            temporary_ref: format!("refs/heads/{}", record.temporary_ref.branch().as_str()),
6031            operation_id: record.operation_id.clone(),
6032            job_id: record.job_id.clone(),
6033            request: record.request.clone(),
6034            request_sha256: record.request_sha256.clone(),
6035            source_revision: record.source_revision.as_str().to_owned(),
6036            git_snapshot: record.git_snapshot.clone(),
6037            prepared_dispatch_commit: record
6038                .prepared_dispatch_commit
6039                .as_ref()
6040                .map(|commit| commit.as_str().to_owned()),
6041            dispatch_commit: record
6042                .dispatch_commit
6043                .as_ref()
6044                .map(|commit| commit.as_str().to_owned()),
6045            workflow_dispatch: record.workflow_dispatch.clone().map(Box::new),
6046            run,
6047            created_at_ms: record.created_at_ms,
6048            publication_started_at_ms: record.publication_started_at_ms,
6049            publication_quiescence_deadline_ms: record.publication_quiescence_deadline_ms,
6050            state: record.state,
6051            publication_intent: record.publication_intent,
6052            publication_uncertain: record.publication_uncertain,
6053            publication_absent: record.publication_absent,
6054            publication_not_attempted: record.publication_not_attempted,
6055            publication_process_fenced: record.publication_process_fenced,
6056            publication_lease_scope_sha256: record.publication_lease_scope_sha256.clone(),
6057            publication_absence_observations: record.publication_absence_observations,
6058            publication_absence_first_observed_at_ms: record
6059                .publication_absence_first_observed_at_ms,
6060            cancellation_requested: record.cancellation_requested,
6061            cancellation_dispatched: record.cancellation_dispatched,
6062            cleanup_requested: record.cleanup_requested,
6063            remove_artifacts_requested: record.remove_artifacts_requested,
6064            artifacts_removed: record.artifacts_removed,
6065            temporary_ref_deleted: record.temporary_ref_deleted,
6066            verification_pending_event: record.verification_pending_event,
6067            run_discovery_attempts: record.run_discovery_attempts,
6068            run_discovery_deadline_ms: record.run_discovery_deadline_ms,
6069            manifests: record.manifests.clone(),
6070            compile_evidence: record.compile_evidence.clone(),
6071            signed_cleanup_evidence: record.signed_cleanup_evidence.clone(),
6072            events: record.events.clone(),
6073        })
6074    }
6075
6076    #[allow(clippy::too_many_lines)]
6077    fn into_record(
6078        self,
6079        config: &GithubProviderConfig,
6080        principal: &GithubPrincipalIdentityV1,
6081        execution_repository_id: u64,
6082    ) -> RemoteBuildResult<JobRecord> {
6083        if self.schema_version != GITHUB_JOB_RESUME_SCHEMA_VERSION
6084            || self.provider != GITHUB_PROVIDER_ID
6085            || self.provider_config_sha256 != provider_config_sha256(config)?
6086            || self.principal != *principal
6087            || !github_remote_matches(
6088                &self.execution_repository,
6089                &repository_url(&config.repository),
6090            )
6091            || self.execution_repository_id == 0
6092            || self.execution_repository_id != execution_repository_id
6093            || self.source_repository != config.source_repository
6094            || self.trusted_source_ref != config.workflow.trusted_source_ref().as_str()
6095            || self.workflow_path != config.workflow.filename().repository_path()
6096            || self.workflow_sha256 != config.workflow_fingerprint.as_str()
6097        {
6098            return Err(resume_failure("resume_provider_identity_mismatch"));
6099        }
6100        validate_provider_request(config, &self.request)?;
6101        if self.operation_id != self.job_id
6102            || self.operation_id != self.request.operation_id
6103            || self.request_sha256 != canonical_request_sha256(&self.request)?
6104            || self.request.source_revision.as_deref() != Some(self.source_revision.as_str())
6105        {
6106            return Err(resume_failure("resume_request_identity_mismatch"));
6107        }
6108        if let Some(compile_evidence) = self.compile_evidence.as_ref() {
6109            validate_github_compile_phase_evidence(compile_evidence, &self.request, &self.job_id)?;
6110        }
6111        if let Some(evidence) = self.signed_cleanup_evidence.as_ref() {
6112            evidence.validate_for_resume(&self)?;
6113        }
6114        let branch = BranchName::new(format!(
6115            "{}/{}",
6116            config.workflow.temporary_branch_namespace().as_str(),
6117            self.operation_id
6118        ))
6119        .map_err(|_| resume_failure("resume_temporary_ref_invalid"))?;
6120        let temporary_ref =
6121            TemporaryGitRef::new(config.workflow.temporary_branch_namespace(), branch)
6122                .map_err(|_| resume_failure("resume_temporary_ref_invalid"))?;
6123        if self.temporary_ref != format!("refs/heads/{}", temporary_ref.branch().as_str()) {
6124            return Err(resume_failure("resume_temporary_ref_mismatch"));
6125        }
6126        let dispatch_manifest =
6127            GithubDispatchManifest::new(config, &temporary_ref, self.request.clone());
6128        dispatch_manifest
6129            .validate_for(config)
6130            .and_then(|()| dispatch_manifest.encode().map(|_| ()))
6131            .map_err(|_| resume_failure("resume_dispatch_manifest_invalid"))?;
6132        let source_revision = CommitSha::new(self.source_revision.clone())
6133            .map_err(|_| resume_failure("resume_source_revision_invalid"))?;
6134        match (&self.request.source_mode, &self.git_snapshot) {
6135            (SourceMode::Git, None) => {}
6136            (SourceMode::GitSnapshot, Some(snapshot)) => {
6137                snapshot.validate_for_request(&self.request)?;
6138            }
6139            _ => return Err(resume_failure("resume_git_snapshot_binding_missing")),
6140        }
6141        let dispatch_commit = self
6142            .dispatch_commit
6143            .as_deref()
6144            .map(CommitSha::new)
6145            .transpose()
6146            .map_err(|_| resume_failure("resume_dispatch_revision_invalid"))?;
6147        let prepared_dispatch_commit = self
6148            .prepared_dispatch_commit
6149            .as_deref()
6150            .map(CommitSha::new)
6151            .transpose()
6152            .map_err(|_| resume_failure("resume_prepared_dispatch_revision_invalid"))?;
6153        if matches!(
6154            (&prepared_dispatch_commit, &dispatch_commit),
6155            (Some(prepared), Some(dispatch)) if prepared != dispatch
6156        ) {
6157            return Err(resume_failure("resume_prepared_dispatch_revision_mismatch"));
6158        }
6159        let (run, run_snapshot) = match self.run.as_ref() {
6160            None => (None, None),
6161            Some(identity) => {
6162                let dispatch = dispatch_commit
6163                    .as_ref()
6164                    .ok_or_else(|| resume_failure("resume_run_without_dispatch"))?;
6165                if identity.workflow_path != self.workflow_path
6166                    || identity.head_sha != dispatch.as_str()
6167                    || identity.branch != temporary_ref.branch().as_str()
6168                    || RunEvent::from(identity.event)
6169                        != workflow_run_event(config.workflow.run_trigger())
6170                {
6171                    return Err(resume_failure("resume_run_identity_mismatch"));
6172                }
6173                let handle = RunHandle::restore(
6174                    identity.run_id,
6175                    identity.workflow_id,
6176                    identity.workflow_path.clone(),
6177                    identity.head_sha.clone(),
6178                    identity.branch.clone(),
6179                    identity.event.into(),
6180                )
6181                .map_err(|_| resume_failure("resume_run_identity_invalid"))?;
6182                let snapshot = RunSnapshot::restore(
6183                    handle.clone(),
6184                    identity.run_number,
6185                    identity.run_attempt,
6186                    identity.status.into(),
6187                    identity.conclusion.map(Into::into),
6188                )
6189                .map_err(|_| resume_failure("resume_run_snapshot_invalid"))?;
6190                (Some(handle), Some(snapshot))
6191            }
6192        };
6193        if self.events.len() > MAX_JOB_EVENTS || self.manifests.len() > MAX_JOB_MANIFESTS {
6194            return Err(resume_failure("resume_record_capacity_exceeded"));
6195        }
6196        for (index, event) in self.events.iter().enumerate() {
6197            event.validate()?;
6198            if event.operation_id != self.operation_id
6199                || event.job_id != self.job_id
6200                || event.provider != GITHUB_PROVIDER_ID
6201                || event.sequence != u64::try_from(index).unwrap_or(u64::MAX) + 1
6202                || matches!(&event.kind, RemoteBuildEventKind::Unknown)
6203            {
6204                return Err(resume_failure("resume_event_identity_mismatch"));
6205            }
6206        }
6207        let snapshot_pre_dispatch = self.git_snapshot.as_ref().is_some_and(|snapshot| {
6208            matches!(
6209                snapshot.phase,
6210                GithubGitSnapshotPhaseV1::Prepared
6211                    | GithubGitSnapshotPhaseV1::KeepaliveIntent
6212                    | GithubGitSnapshotPhaseV1::KeepaliveExact
6213                    | GithubGitSnapshotPhaseV1::StageDeleteIntent
6214                    | GithubGitSnapshotPhaseV1::StageDeleted
6215                    | GithubGitSnapshotPhaseV1::SourcePublishIntent
6216                    | GithubGitSnapshotPhaseV1::SourceExact
6217                    | GithubGitSnapshotPhaseV1::SourceAbsent
6218                    | GithubGitSnapshotPhaseV1::SourceConflict
6219                    | GithubGitSnapshotPhaseV1::SourceCleanupIntent
6220                    | GithubGitSnapshotPhaseV1::SourceDeleted
6221            )
6222        }) && !self.publication_intent
6223            && prepared_dispatch_commit.is_none()
6224            && dispatch_commit.is_none()
6225            && run.is_none()
6226            && self.publication_started_at_ms == 0
6227            && self.publication_quiescence_deadline_ms == u64::MAX
6228            && !self.publication_uncertain
6229            && !self.publication_absent
6230            && !self.publication_not_attempted
6231            && !self.publication_process_fenced
6232            && self.publication_lease_scope_sha256.is_none()
6233            && self.publication_absence_observations == 0
6234            && self.publication_absence_first_observed_at_ms == 0;
6235        let snapshot_terminal_pre_dispatch = self.git_snapshot.as_ref().is_some_and(|snapshot| {
6236            snapshot.phase == GithubGitSnapshotPhaseV1::SourceConflict
6237                || snapshot.phase == GithubGitSnapshotPhaseV1::SourceAbsent
6238                    && snapshot.source_publication_attempts >= 3
6239        });
6240        let snapshot_cancel_cleanup_proven = self.git_snapshot.as_ref().is_some_and(|snapshot| {
6241            snapshot.phase == GithubGitSnapshotPhaseV1::StageDeleted
6242                && snapshot.source_publication_attempts == 0
6243                || snapshot.phase == GithubGitSnapshotPhaseV1::SourcePublishIntent
6244                    && snapshot.source_publication_started_at_ms == 0
6245                || matches!(
6246                    snapshot.phase,
6247                    GithubGitSnapshotPhaseV1::SourceAbsent
6248                        | GithubGitSnapshotPhaseV1::SourceConflict
6249                        | GithubGitSnapshotPhaseV1::SourceDeleted
6250                )
6251        });
6252        let snapshot_source_cleanup_proven = self.git_snapshot.as_ref().is_none_or(|snapshot| {
6253            matches!(
6254                snapshot.phase,
6255                GithubGitSnapshotPhaseV1::SourceDeleted
6256                    | GithubGitSnapshotPhaseV1::SourceAbsent
6257                    | GithubGitSnapshotPhaseV1::SourceConflict
6258                    | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
6259                    | GithubGitSnapshotPhaseV1::KeepaliveReleased
6260            )
6261        });
6262        let no_dispatch_state_valid = match self.state {
6263            JobState::Created => !self.publication_absent,
6264            JobState::Cancelling => {
6265                self.cancellation_requested
6266                    && !self.publication_absent
6267                    && (self.publication_intent || snapshot_pre_dispatch)
6268            }
6269            JobState::Cancelled => {
6270                self.cancellation_requested
6271                    && (self.publication_absent
6272                        || snapshot_pre_dispatch && snapshot_cancel_cleanup_proven)
6273            }
6274            JobState::Failed => {
6275                self.publication_uncertain
6276                    || self.publication_absent
6277                    || self.publication_not_attempted
6278                    || snapshot_pre_dispatch && snapshot_terminal_pre_dispatch
6279            }
6280            JobState::Cleaning | JobState::CleanupFailed => {
6281                (self.publication_absent || self.publication_not_attempted)
6282                    && self.cleanup_requested
6283                    || snapshot_pre_dispatch
6284                        && snapshot_terminal_pre_dispatch
6285                        && self.cleanup_requested
6286            }
6287            JobState::Queued | JobState::Running | JobState::Succeeded | JobState::Cleaned => false,
6288        };
6289        let expected_discovery_deadline = self
6290            .created_at_ms
6291            .saturating_add(config.poll_policy.discovery_window_ms());
6292        let publication_boundary_armed = publication_boundary_is_armed(&self);
6293        let publication_boundary_unarmed = publication_boundary_is_unarmed(&self);
6294        let successful_run = run_snapshot
6295            .as_ref()
6296            .is_some_and(run_snapshot_is_successful);
6297        let cleanup_success_run = match self.request.signing.mode {
6298            SigningMode::UnsignedCompileOnly => run_snapshot
6299                .as_ref()
6300                .is_some_and(|snapshot| snapshot.status() == RunStatus::Completed),
6301            SigningMode::ManualDevelopment => self.signed_cleanup_evidence.is_some(),
6302            _ => false,
6303        };
6304        let completed_run = run_snapshot
6305            .as_ref()
6306            .is_some_and(|snapshot| snapshot.status() == RunStatus::Completed);
6307        let cancelled_run = run_snapshot.as_ref().is_some_and(|snapshot| {
6308            snapshot.status() == RunStatus::Completed
6309                && snapshot.conclusion() == Some(RunConclusion::Cancelled)
6310        });
6311        let manifest_state_valid = matches!(
6312            self.state,
6313            JobState::Succeeded | JobState::Cleaning | JobState::Cleaned | JobState::CleanupFailed
6314        );
6315        if self.run_discovery_attempts > config.poll_policy.discovery_attempts().max(2)
6316            || self.publication_absence_observations > 2
6317            || self
6318                .publication_lease_scope_sha256
6319                .as_ref()
6320                .is_some_and(|scope| !is_lower_sha256(scope))
6321            || self.publication_process_fenced && self.publication_lease_scope_sha256.is_none()
6322            || (self.publication_absence_observations == 0)
6323                != (self.publication_absence_first_observed_at_ms == 0)
6324            || self.run_discovery_deadline_ms != expected_discovery_deadline
6325            || !(publication_boundary_armed || publication_boundary_unarmed)
6326            || !publication_boundary_armed
6327                && (dispatch_commit.is_some()
6328                    || self.publication_absent
6329                    || self.publication_absence_observations != 0)
6330            || !self.publication_intent && !snapshot_pre_dispatch
6331            || self.publication_absent
6332                && (!self.publication_intent
6333                    || prepared_dispatch_commit.is_none()
6334                    || !self.publication_process_fenced
6335                    || self.publication_absence_observations < 2
6336                    || dispatch_commit.is_some()
6337                    || !self.temporary_ref_deleted
6338                    || self.publication_uncertain)
6339            || self.publication_not_attempted
6340                && (!self.publication_intent
6341                    || self.publication_absent
6342                    || self.publication_uncertain
6343                    || self.publication_absence_observations != 0
6344                    || dispatch_commit.is_some()
6345                    || run.is_some()
6346                    || !matches!(
6347                        self.state,
6348                        JobState::Failed | JobState::Cleaning | JobState::CleanupFailed
6349                    ))
6350            || dispatch_commit.is_none()
6351                && (run.is_some()
6352                    || self.cancellation_dispatched
6353                    || self.artifacts_removed
6354                    || !no_dispatch_state_valid)
6355            || self.cancellation_dispatched && run.is_none()
6356            || self.cancellation_dispatched && !self.cancellation_requested
6357            || self.state == JobState::Cancelling && !self.cancellation_requested
6358            || self.cancellation_requested
6359                && !matches!(
6360                    self.state,
6361                    JobState::Cancelling
6362                        | JobState::Cancelled
6363                        | JobState::Failed
6364                        | JobState::Cleaning
6365                        | JobState::Cleaned
6366                        | JobState::CleanupFailed
6367                )
6368            || self.remove_artifacts_requested && !self.cleanup_requested
6369            || self.artifacts_removed && !self.remove_artifacts_requested
6370            || self.temporary_ref_deleted && !self.cleanup_requested && !self.publication_absent
6371            || matches!(
6372                self.state,
6373                JobState::Cleaning | JobState::Cleaned | JobState::CleanupFailed
6374            ) && (!self.cleanup_requested || !snapshot_source_cleanup_proven)
6375            || self.state == JobState::Succeeded && (self.manifests.is_empty() || !successful_run)
6376            || self.manifests.is_empty() != self.compile_evidence.is_none()
6377            || !self.manifests.is_empty() && (!successful_run || !manifest_state_valid)
6378            || self.signed_cleanup_evidence.is_some()
6379                && (self.request.signing.mode != SigningMode::ManualDevelopment
6380                    || !successful_run
6381                    || !manifest_state_valid)
6382            || self.request.signing.mode == SigningMode::ManualDevelopment
6383                && !self.manifests.is_empty()
6384                && self.signed_cleanup_evidence.is_none()
6385            || matches!(
6386                self.state,
6387                JobState::Succeeded
6388                    | JobState::Failed
6389                    | JobState::Cancelled
6390                    | JobState::Cleaning
6391                    | JobState::Cleaned
6392                    | JobState::CleanupFailed
6393            ) && run_snapshot.is_some()
6394                && !completed_run
6395            || self.state == JobState::Cancelled
6396                && !(self.publication_absent && run_snapshot.is_none() || cancelled_run)
6397            || self.state == JobState::Cleaned && !cleanup_success_run
6398            || matches!(self.state, JobState::Cleaned | JobState::CleanupFailed)
6399                && (!(self.temporary_ref_deleted
6400                    || self.publication_not_attempted
6401                    || snapshot_terminal_pre_dispatch)
6402                    || self.remove_artifacts_requested && !self.artifacts_removed)
6403        {
6404            return Err(resume_failure("resume_state_inconsistent"));
6405        }
6406        let record = JobRecord {
6407            principal: Some(principal.clone()),
6408            execution_repository_id: Some(execution_repository_id),
6409            operation_id: self.operation_id,
6410            job_id: self.job_id,
6411            source_revision,
6412            git_snapshot: self.git_snapshot,
6413            request: self.request,
6414            request_sha256: self.request_sha256,
6415            temporary_ref,
6416            prepared_dispatch_commit,
6417            dispatch_commit,
6418            workflow_dispatch: self.workflow_dispatch.map(|dispatch| *dispatch),
6419            created_at_ms: self.created_at_ms,
6420            publication_started_at_ms: self.publication_started_at_ms,
6421            publication_quiescence_deadline_ms: self.publication_quiescence_deadline_ms,
6422            state: self.state,
6423            run,
6424            run_snapshot,
6425            publication_intent: self.publication_intent,
6426            publication_absent: self.publication_absent,
6427            publication_not_attempted: self.publication_not_attempted,
6428            publication_process_fenced: self.publication_process_fenced,
6429            publication_lease_scope_sha256: self.publication_lease_scope_sha256,
6430            publication_takeover_lease_owned: false,
6431            snapshot_source_lease_owned: false,
6432            publication_absence_observations: self.publication_absence_observations,
6433            publication_absence_first_observed_at_ms: self.publication_absence_first_observed_at_ms,
6434            cancellation_requested: self.cancellation_requested,
6435            cancellation_dispatched: self.cancellation_dispatched,
6436            cleanup_requested: self.cleanup_requested,
6437            remove_artifacts_requested: self.remove_artifacts_requested,
6438            artifacts_removed: self.artifacts_removed,
6439            temporary_ref_deleted: self.temporary_ref_deleted,
6440            verification_pending_event: self.verification_pending_event,
6441            publication_uncertain: self.publication_uncertain,
6442            run_discovery_attempts: self.run_discovery_attempts,
6443            run_discovery_deadline_ms: self.run_discovery_deadline_ms,
6444            manifests: self.manifests,
6445            compile_evidence: self.compile_evidence,
6446            signed_cleanup_evidence: self.signed_cleanup_evidence,
6447            events: self.events,
6448        };
6449        validate_workflow_dispatch_record(config, &record)?;
6450        if !record.manifests.is_empty() {
6451            validate_manifests(&record, config, &record.manifests)?;
6452        }
6453        Ok(record)
6454    }
6455}
6456
6457#[derive(Debug, Default)]
6458struct ProviderState {
6459    jobs: BTreeMap<String, JobRecord>,
6460    reservations: BTreeSet<String>,
6461    cancellation_reservations: BTreeSet<String>,
6462}
6463
6464struct JobReservation<'a> {
6465    state: &'a Mutex<ProviderState>,
6466    job_id: String,
6467    active: bool,
6468}
6469
6470impl<'a> JobReservation<'a> {
6471    fn acquire(
6472        state: &'a Mutex<ProviderState>,
6473        job_id: String,
6474        maximum: usize,
6475        allow_recycling: bool,
6476    ) -> RemoteBuildResult<Self> {
6477        let mut locked = lock_provider_state(state)?;
6478        if locked.reservations.contains(&job_id)
6479            || locked
6480                .jobs
6481                .get(&job_id)
6482                .is_some_and(|record| !allow_recycling || !record_is_recyclable(record))
6483        {
6484            return Err(provider_failure(
6485                "duplicate_job_id",
6486                "operation already has a GitHub provider job",
6487                false,
6488            ));
6489        }
6490        if allow_recycling && locked.jobs.get(&job_id).is_some_and(record_is_recyclable) {
6491            locked.jobs.remove(&job_id);
6492        }
6493        if locked.jobs.len() + locked.reservations.len() >= maximum {
6494            if !allow_recycling {
6495                return Err(provider_failure(
6496                    "job_capacity_reached",
6497                    "GitHub provider reached its bounded durable job capacity",
6498                    true,
6499                ));
6500            }
6501            let recyclable = locked
6502                .jobs
6503                .iter()
6504                .filter(|(_, record)| record_is_recyclable(record))
6505                .min_by(|(left_id, left), (right_id, right)| {
6506                    left.created_at_ms
6507                        .cmp(&right.created_at_ms)
6508                        .then_with(|| left_id.cmp(right_id))
6509                })
6510                .map(|(candidate_id, _)| candidate_id.clone());
6511            if let Some(recyclable) = recyclable {
6512                locked.jobs.remove(&recyclable);
6513            } else {
6514                return Err(provider_failure(
6515                    "job_capacity_reached",
6516                    "GitHub provider reached its bounded in-memory job capacity",
6517                    true,
6518                ));
6519            }
6520        }
6521        locked.reservations.insert(job_id.clone());
6522        drop(locked);
6523        Ok(Self {
6524            state,
6525            job_id,
6526            active: true,
6527        })
6528    }
6529
6530    fn insert_pending(mut self, record: JobRecord) -> RemoteBuildResult<PendingJobRecord<'a>> {
6531        let mut state = lock_provider_state(self.state)?;
6532        state.reservations.remove(&self.job_id);
6533        state.jobs.insert(self.job_id.clone(), record);
6534        self.active = false;
6535        drop(state);
6536        Ok(PendingJobRecord {
6537            state: self.state,
6538            job_id: self.job_id.clone(),
6539            retain: false,
6540        })
6541    }
6542}
6543
6544impl Drop for JobReservation<'_> {
6545    fn drop(&mut self) {
6546        if self.active
6547            && let Ok(mut state) = self.state.lock()
6548        {
6549            state.reservations.remove(&self.job_id);
6550        }
6551    }
6552}
6553
6554struct PendingJobRecord<'a> {
6555    state: &'a Mutex<ProviderState>,
6556    job_id: String,
6557    retain: bool,
6558}
6559
6560impl PendingJobRecord<'_> {
6561    fn retain(&mut self) {
6562        self.retain = true;
6563    }
6564}
6565
6566impl Drop for PendingJobRecord<'_> {
6567    fn drop(&mut self) {
6568        if !self.retain
6569            && let Ok(mut state) = self.state.lock()
6570        {
6571            state.jobs.remove(&self.job_id);
6572        }
6573    }
6574}
6575
6576/// Concrete GitHub Actions implementation of the runtime-neutral build-provider contract.
6577pub struct GithubBuildProvider<R, P, A = NoVerifiedArtifactStore, C = SystemProviderClock> {
6578    config: GithubProviderConfig,
6579    capabilities: ProviderCapabilities,
6580    transport: Mutex<GithubTransport<R>>,
6581    publisher: Mutex<P>,
6582    artifacts: Mutex<A>,
6583    clock: C,
6584    state: Mutex<ProviderState>,
6585    checkpoint_sink: Option<Mutex<Box<dyn GithubJobCheckpointSink>>>,
6586}
6587
6588impl<R, P> GithubBuildProvider<R, P, NoVerifiedArtifactStore, SystemProviderClock>
6589where
6590    P: TemporaryRefPublisher,
6591{
6592    /// Construct the provider without artifact ingestion.
6593    ///
6594    /// Artifact listing/download capabilities remain false and GitHub success is reported as
6595    /// verification-pending, never as protocol success.
6596    pub fn new(config: GithubProviderConfig, transport: GithubTransport<R>, publisher: P) -> Self {
6597        Self::with_artifact_store_and_clock(
6598            config,
6599            transport,
6600            publisher,
6601            NoVerifiedArtifactStore,
6602            SystemProviderClock,
6603        )
6604    }
6605}
6606
6607impl<R, P, A, C> GithubBuildProvider<R, P, A, C>
6608where
6609    P: TemporaryRefPublisher,
6610    A: VerifiedArtifactStore,
6611    C: ProviderClock,
6612{
6613    /// Construct with explicit verified artifact ingestion and clock.
6614    pub fn with_artifact_store_and_clock(
6615        config: GithubProviderConfig,
6616        transport: GithubTransport<R>,
6617        publisher: P,
6618        artifacts: A,
6619        clock: C,
6620    ) -> Self {
6621        let capabilities =
6622            provider_capabilities(&config, &artifacts, publisher.supports_git_snapshot());
6623        Self {
6624            config,
6625            capabilities,
6626            transport: Mutex::new(transport),
6627            publisher: Mutex::new(publisher),
6628            artifacts: Mutex::new(artifacts),
6629            clock,
6630            state: Mutex::new(ProviderState::default()),
6631            checkpoint_sink: None,
6632        }
6633    }
6634
6635    /// Attach an atomic durable checkpoint sink without changing the protocol-v1 provider type.
6636    #[must_use]
6637    pub fn with_checkpoint_sink(mut self, sink: impl GithubJobCheckpointSink + 'static) -> Self {
6638        self.checkpoint_sink = Some(Mutex::new(Box::new(sink)));
6639        self
6640    }
6641
6642    /// Configured workflow trigger governing publication-to-run mapping.
6643    pub const fn workflow_run_trigger(&self) -> WorkflowRunTrigger {
6644        self.config.workflow.run_trigger()
6645    }
6646
6647    /// Explicitly poll one mapped run to terminal using the configured bounded policy.
6648    ///
6649    /// This does not synthesize protocol success; a later `events` call still requires verified
6650    /// artifact manifests. Cancellation is checked before and after the bounded transport poll.
6651    ///
6652    /// # Errors
6653    ///
6654    /// Returns a typed provider failure for an unknown/unmapped job or transport failure.
6655    pub fn wait_for_github_run(
6656        &self,
6657        job_id: &str,
6658        cancellation: &CancellationToken,
6659    ) -> RemoteBuildResult<RunSnapshot>
6660    where
6661        R: GhRunner,
6662    {
6663        cancellation.check()?;
6664        let handle = {
6665            let state = self.lock_state()?;
6666            state
6667                .jobs
6668                .get(job_id)
6669                .and_then(|record| record.run.clone())
6670                .ok_or_else(|| job_not_mapped(job_id))?
6671        };
6672        let snapshot = self
6673            .lock_transport()?
6674            .wait_for_run(
6675                &self.config.repository,
6676                &handle,
6677                self.config.poll_policy.transport(),
6678            )
6679            .map_err(transport_failure)?;
6680        cancellation.check()?;
6681        Ok(snapshot)
6682    }
6683
6684    fn lock_state(&self) -> RemoteBuildResult<std::sync::MutexGuard<'_, ProviderState>> {
6685        lock_provider_state(&self.state)
6686    }
6687
6688    fn lock_transport(&self) -> RemoteBuildResult<std::sync::MutexGuard<'_, GithubTransport<R>>> {
6689        self.transport.lock().map_err(|_| {
6690            provider_failure(
6691                "transport_unavailable",
6692                "GitHub transport is unavailable",
6693                true,
6694            )
6695        })
6696    }
6697
6698    fn lock_publisher(&self) -> RemoteBuildResult<std::sync::MutexGuard<'_, P>> {
6699        self.publisher.lock().map_err(|_| {
6700            provider_failure(
6701                "publisher_unavailable",
6702                "temporary-ref publisher is unavailable",
6703                true,
6704            )
6705        })
6706    }
6707
6708    fn lock_artifacts(&self) -> RemoteBuildResult<std::sync::MutexGuard<'_, A>> {
6709        self.artifacts.lock().map_err(|_| {
6710            provider_failure(
6711                "artifact_store_unavailable",
6712                "verified artifact store is unavailable",
6713                true,
6714            )
6715        })
6716    }
6717
6718    fn current_resume_identity(&self) -> RemoteBuildResult<(GithubPrincipalIdentityV1, u64)>
6719    where
6720        R: GhRunner,
6721    {
6722        let mut transport = self.lock_transport()?;
6723        let principal = transport
6724            .authenticate(&self.config.repository)
6725            .map_err(transport_failure)?;
6726        let repository = transport
6727            .repository(&self.config.repository)
6728            .map_err(transport_failure)?;
6729        let principal = GithubPrincipalIdentityV1::from_authenticated(
6730            &principal,
6731            self.config.workflow.run_trigger(),
6732            repository.id(),
6733        )?;
6734        Ok((principal, repository.id()))
6735    }
6736
6737    /// Resolve the exact secret-free identity for a local durable record before submission.
6738    ///
6739    /// This performs only bounded GitHub metadata reads. It never publishes a ref, starts a run,
6740    /// cancels a run, or deletes remote state. Repository-scoped credentials are rejected until a
6741    /// stable installation principal ID can be proven.
6742    ///
6743    /// # Errors
6744    ///
6745    /// Returns a typed transport or stable-identity failure.
6746    pub fn durable_identity(&self) -> RemoteBuildResult<GithubDurableIdentityV1>
6747    where
6748        R: GhRunner,
6749    {
6750        let (principal, execution_repository_id) = self.current_resume_identity()?;
6751        Ok(GithubDurableIdentityV1 {
6752            provider: GITHUB_PROVIDER_ID.to_owned(),
6753            provider_config_sha256: provider_config_sha256(&self.config)?,
6754            principal,
6755            execution_repository: repository_url(&self.config.repository),
6756            execution_repository_id,
6757        })
6758    }
6759
6760    fn validate_durable_identity_binding(
6761        &self,
6762        identity: &GithubDurableIdentityV1,
6763    ) -> RemoteBuildResult<()> {
6764        if identity.provider != GITHUB_PROVIDER_ID
6765            || identity.provider_config_sha256 != provider_config_sha256(&self.config)?
6766            || !github_remote_matches(
6767                &identity.execution_repository,
6768                &repository_url(&self.config.repository),
6769            )
6770            || identity.execution_repository_id == 0
6771            || matches!(
6772                identity.principal,
6773                GithubPrincipalIdentityV1::RepositoryCredential
6774            ) && self.config.workflow.run_trigger() != WorkflowRunTrigger::WorkflowDispatch
6775        {
6776            return Err(resume_failure("resume_provider_identity_mismatch"));
6777        }
6778        Ok(())
6779    }
6780
6781    /// Precompute one initial snapshot's exact six-object graph without mutation.
6782    ///
6783    /// This specialized boundary validates provider snapshot support and the operation-derived
6784    /// source ref, then delegates only to the retained-byte no-write publisher primitive. It does
6785    /// not create a stage, write a Git object, mutate a ref, access the network, or submit a job.
6786    ///
6787    /// # Errors
6788    ///
6789    /// Returns a typed cancellation, unsupported-capability, unsafe-operation, retained-stage,
6790    /// object-format, fixed-Git, or canonical graph failure.
6791    pub fn precompute_git_snapshot_graph(
6792        &self,
6793        inputs: &mut GitSnapshotPrecomputeInputs,
6794        operation_id: &str,
6795        source_created_at_ms: u64,
6796        cancellation: &CancellationToken,
6797    ) -> RemoteBuildResult<GitSnapshotObjectGraphV1> {
6798        cancellation.check()?;
6799        if !self
6800            .capabilities
6801            .source_modes
6802            .contains(&SourceMode::GitSnapshot)
6803        {
6804            return Err(unsupported(ProviderFeature::SourceMode(
6805                SourceMode::GitSnapshot,
6806            )));
6807        }
6808        GitSnapshotSourceRef::for_operation(operation_id)
6809            .map_err(TemporaryRefPublishError::from)
6810            .map_err(publisher_failure)?;
6811        let graph = {
6812            let mut publisher = self.lock_publisher()?;
6813            publisher
6814                .precompute_git_snapshot(inputs, operation_id, source_created_at_ms)
6815                .map_err(publisher_failure)?
6816        };
6817        cancellation.check()?;
6818        Ok(graph)
6819    }
6820
6821    /// Discover complete provider-unowned snapshot stages using bounded private reads only.
6822    ///
6823    /// This method neither adopts nor deletes any candidate. Provider jobs and in-process
6824    /// reservations are excluded, but the controller must separately prove that no on-disk
6825    /// `JobStore` record owns the operation before atomically creating that owner and invoking
6826    /// fresh-reconsent submission.
6827    ///
6828    /// # Errors
6829    ///
6830    /// Returns a typed failure when snapshot support is unavailable or any private store entry is
6831    /// partial, unknown, over the fixed bound, non-canonical, rebound, or configuration-mismatched.
6832    pub fn discover_uncheckpointed_git_snapshot_stages(
6833        &self,
6834    ) -> RemoteBuildResult<Vec<GithubGitSnapshotRecoveryCandidateV1>> {
6835        if !self
6836            .capabilities
6837            .source_modes
6838            .contains(&SourceMode::GitSnapshot)
6839        {
6840            return Err(unsupported(ProviderFeature::SourceMode(
6841                SourceMode::GitSnapshot,
6842            )));
6843        }
6844        let mut candidates = self
6845            .lock_publisher()?
6846            .discover_git_snapshot_stages()
6847            .map_err(publisher_failure)?;
6848        let occupied = {
6849            let state = self.lock_state()?;
6850            state
6851                .jobs
6852                .keys()
6853                .chain(state.reservations.iter())
6854                .cloned()
6855                .collect::<BTreeSet<_>>()
6856        };
6857        candidates.retain(|candidate| !occupied.contains(&candidate.final_request.operation_id));
6858        candidates.sort_by(|left, right| {
6859            left.final_request
6860                .operation_id
6861                .cmp(&right.final_request.operation_id)
6862        });
6863        let mut previous = None;
6864        for candidate in &candidates {
6865            let operation_id = candidate.final_request.operation_id.as_str();
6866            if previous == Some(operation_id)
6867                || candidate.stage.final_request != candidate.final_request
6868                || candidate.stage.operation_id != operation_id
6869                || candidate.stage_locator.operation_id != operation_id
6870                || candidate.stage.source_repository != self.config.source_repository
6871            {
6872                return Err(provider_failure(
6873                    "snapshot_stage_discovery_invalid",
6874                    "Git snapshot recovery discovery found an invalid candidate",
6875                    false,
6876                ));
6877            }
6878            candidate
6879                .stage
6880                .validate_for_request(&candidate.descriptor, &candidate.final_request)
6881                .map_err(|_| {
6882                    provider_failure(
6883                        "snapshot_stage_discovery_invalid",
6884                        "Git snapshot recovery discovery found an invalid candidate",
6885                        false,
6886                    )
6887                })?;
6888            validate_git_snapshot_submission(&self.config, &candidate.final_request)?;
6889            previous = Some(operation_id);
6890        }
6891        Ok(candidates)
6892    }
6893
6894    /// Submit one explicit-consent unsigned Git snapshot without an ordinary clean-Git fallback.
6895    ///
6896    /// The first `Prepared` checkpoint is synchronously durable before this method permits a Git
6897    /// object write, ref mutation, or network request. Same-invocation stages must carry the exact
6898    /// accepted consent SHA. Restart adoption is available only through the distinct fresh-
6899    /// reconsent constructor and is no-mutation until that same checkpoint succeeds.
6900    ///
6901    /// # Errors
6902    ///
6903    /// Returns a typed validation, checkpoint, retained-stage, Git, identity, transport, or
6904    /// publication failure. A checkpointed job remains resumable after any later failure.
6905    #[allow(
6906        clippy::too_many_lines,
6907        reason = "snapshot reservation, consent, immutable record construction, and first checkpoint form one security boundary"
6908    )]
6909    pub fn submit_git_snapshot(
6910        &self,
6911        submission: GithubGitSnapshotSubmissionV1,
6912        cancellation: &CancellationToken,
6913    ) -> RemoteBuildResult<JobHandle>
6914    where
6915        R: GhRunner,
6916        P: TemporaryRefReconciler,
6917    {
6918        cancellation.check()?;
6919        if self.checkpoint_sink.is_none() {
6920            return Err(provider_failure(
6921                "snapshot_checkpoint_required",
6922                "Git snapshot submission requires durable provider checkpoints",
6923                false,
6924            ));
6925        }
6926        validate_git_snapshot_submission(&self.config, &submission.request)?;
6927        if !self
6928            .capabilities
6929            .source_modes
6930            .contains(&SourceMode::GitSnapshot)
6931        {
6932            return Err(unsupported(ProviderFeature::SourceMode(
6933                SourceMode::GitSnapshot,
6934            )));
6935        }
6936        self.validate_durable_identity_binding(&submission.durable_identity)?;
6937        let mut workflow_dispatch_client =
6938            if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
6939                self.preflight_workflow_dispatch_registration()?;
6940                Some(self.prepare_workflow_dispatch_client()?)
6941            } else {
6942                None
6943            };
6944        let job_id = submission.request.operation_id.clone();
6945        let reservation =
6946            JobReservation::acquire(&self.state, job_id.clone(), self.config.max_jobs, false)?;
6947        let (stage_locator, stage) = match submission.stage_authority {
6948            GithubGitSnapshotStageAuthorityV1::SameInvocation { locator, stage }
6949            | GithubGitSnapshotStageAuthorityV1::ExactRetryLineage { locator, stage } => {
6950                (locator, stage)
6951            }
6952            GithubGitSnapshotStageAuthorityV1::FreshReconsentAdoption => self
6953                .lock_publisher()?
6954                .adopt_git_snapshot_stage(&submission.request)
6955                .map_err(publisher_failure)?,
6956        };
6957        if stage.consent_sha256 != submission.approved_consent_sha256 {
6958            return Err(provider_failure(
6959                "snapshot_consent_changed",
6960                "Git snapshot stage differs from the explicitly accepted upload plan",
6961                false,
6962            ));
6963        }
6964        let snapshot = GithubGitSnapshotResumeV1 {
6965            schema_version: GITHUB_GIT_SNAPSHOT_RESUME_SCHEMA_VERSION,
6966            stage_locator,
6967            stage,
6968            phase: GithubGitSnapshotPhaseV1::Prepared,
6969            source_publication_attempts: 0,
6970            source_publication_started_at_ms: 0,
6971            source_publication_quiescence_deadline_ms: 0,
6972            source_publication_process_fenced: false,
6973            source_publication_lease_scope_sha256: None,
6974            source_publication_absence_observations: 0,
6975            source_publication_absence_first_observed_at_ms: 0,
6976            source_publication_absence_last_observed_at_ms: 0,
6977            keepalive_release_authorization_sha256: None,
6978        };
6979        snapshot.validate_for_request(&submission.request)?;
6980        let branch = BranchName::new(format!(
6981            "{}/{}",
6982            self.config.workflow.temporary_branch_namespace().as_str(),
6983            submission.request.operation_id
6984        ))
6985        .map_err(|_| {
6986            provider_failure(
6987                "operation_id_not_ref_safe",
6988                "operation ID cannot form a safe temporary Git ref",
6989                false,
6990            )
6991        })?;
6992        let temporary_ref =
6993            TemporaryGitRef::new(self.config.workflow.temporary_branch_namespace(), branch)
6994                .map_err(|_| {
6995                    provider_failure(
6996                        "operation_id_not_ref_safe",
6997                        "operation ID cannot form a safe temporary Git ref",
6998                        false,
6999                    )
7000                })?;
7001        let source_revision = CommitSha::new(
7002            submission
7003                .request
7004                .source_revision
7005                .clone()
7006                .ok_or_else(|| resume_failure("resume_source_revision_invalid"))?,
7007        )
7008        .map_err(|_| resume_failure("resume_source_revision_invalid"))?;
7009        let created_at_ms = snapshot.stage.source_created_at_ms;
7010        let manifest =
7011            GithubDispatchManifest::new(&self.config, &temporary_ref, submission.request.clone());
7012        manifest.validate_for(&self.config)?;
7013        manifest.encode()?;
7014        let request_sha256 = canonical_request_sha256(&submission.request)?;
7015        let discovery_deadline_ms =
7016            created_at_ms.saturating_add(self.config.poll_policy.discovery_window_ms());
7017        let mut record = JobRecord {
7018            principal: Some(submission.durable_identity.principal),
7019            execution_repository_id: Some(submission.durable_identity.execution_repository_id),
7020            operation_id: job_id.clone(),
7021            job_id: job_id.clone(),
7022            source_revision,
7023            git_snapshot: Some(snapshot),
7024            request: submission.request,
7025            request_sha256,
7026            temporary_ref,
7027            prepared_dispatch_commit: None,
7028            dispatch_commit: None,
7029            workflow_dispatch: None,
7030            created_at_ms,
7031            publication_started_at_ms: 0,
7032            publication_quiescence_deadline_ms: u64::MAX,
7033            state: JobState::Created,
7034            run: None,
7035            run_snapshot: None,
7036            publication_intent: false,
7037            publication_absent: false,
7038            publication_not_attempted: false,
7039            publication_process_fenced: false,
7040            publication_lease_scope_sha256: None,
7041            publication_takeover_lease_owned: false,
7042            snapshot_source_lease_owned: false,
7043            publication_absence_observations: 0,
7044            publication_absence_first_observed_at_ms: 0,
7045            cancellation_requested: false,
7046            cancellation_dispatched: false,
7047            cleanup_requested: false,
7048            remove_artifacts_requested: false,
7049            artifacts_removed: false,
7050            temporary_ref_deleted: false,
7051            verification_pending_event: false,
7052            publication_uncertain: false,
7053            run_discovery_attempts: 0,
7054            run_discovery_deadline_ms: discovery_deadline_ms,
7055            manifests: Vec::new(),
7056            compile_evidence: None,
7057            signed_cleanup_evidence: None,
7058            events: Vec::new(),
7059        };
7060        append_event(
7061            &mut record,
7062            created_at_ms,
7063            "submit",
7064            RemoteBuildEventKind::OperationStarted {
7065                command: "build-iphone --snapshot".to_owned(),
7066            },
7067        )?;
7068        append_event(
7069            &mut record,
7070            created_at_ms,
7071            "submit",
7072            RemoteBuildEventKind::JobCreated {
7073                state: JobState::Created,
7074            },
7075        )?;
7076        let mut pending = reservation.insert_pending(record)?;
7077        {
7078            let mut provider_state = self.lock_state()?;
7079            let record = provider_state
7080                .jobs
7081                .get_mut(&job_id)
7082                .ok_or_else(|| resume_failure("snapshot_pending_job_lost"))?;
7083            self.checkpoint_record(record)?;
7084        }
7085        pending.retain();
7086        self.continue_git_snapshot_submission(&job_id, cancellation)?;
7087        let start_dispatch = {
7088            let provider_state = self.lock_state()?;
7089            let record = provider_state
7090                .jobs
7091                .get(&job_id)
7092                .ok_or_else(|| job_not_found(&job_id))?;
7093            self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch
7094                && record.dispatch_commit.is_some()
7095                && !record.publication_uncertain
7096                && !record.state.is_terminal()
7097                && !record.state.is_build_terminal()
7098        };
7099        if start_dispatch {
7100            let client = workflow_dispatch_client.as_deref_mut().ok_or_else(|| {
7101                provider_failure(
7102                    "workflow_dispatch_client_unavailable",
7103                    "Preflighted workflow dispatch client is unavailable",
7104                    true,
7105                )
7106            })?;
7107            self.start_workflow_dispatch_with_client(&job_id, client, cancellation)?;
7108        }
7109        let provider_state = self.lock_state()?;
7110        let record = provider_state
7111            .jobs
7112            .get(&job_id)
7113            .ok_or_else(|| job_not_found(&job_id))?;
7114        Ok(JobHandle {
7115            job_id,
7116            state: record.state,
7117            created_at_ms,
7118        })
7119    }
7120
7121    /// Derive one immutable exact-retry child request without creating a stage or mutating Git.
7122    ///
7123    /// The terminal parent's archive blob and keepalive are verified before and after bounded
7124    /// offline no-write hashing. The returned request and graph can therefore be persisted in the
7125    /// parent/child lineage before any child stage write.
7126    ///
7127    /// # Errors
7128    ///
7129    /// Returns a typed failure for a nonterminal or releasing parent, missing/conflicting
7130    /// keepalive, invalid child identity, parent blob mismatch, or deterministic graph failure.
7131    #[allow(
7132        clippy::too_many_lines,
7133        reason = "parent terminal, keepalive, archive, and no-write child-plan checks are one read-only authority boundary"
7134    )]
7135    pub fn precompute_exact_git_snapshot_retry(
7136        &self,
7137        parent_job_id: &str,
7138        child_operation_id: &str,
7139        child_source_created_at_ms: u64,
7140        cancellation: &CancellationToken,
7141    ) -> RemoteBuildResult<GithubGitSnapshotExactRetryPlanV1> {
7142        cancellation.check()?;
7143        if !self
7144            .capabilities
7145            .source_modes
7146            .contains(&SourceMode::GitSnapshot)
7147        {
7148            return Err(unsupported(ProviderFeature::SourceMode(
7149                SourceMode::GitSnapshot,
7150            )));
7151        }
7152        if parent_job_id == child_operation_id
7153            || child_source_created_at_ms == 0
7154            || child_source_created_at_ms / 1_000 > i64::MAX as u64
7155            || GitSnapshotSourceRef::for_operation(child_operation_id).is_err()
7156        {
7157            return Err(provider_failure(
7158                "snapshot_exact_retry_child_invalid",
7159                "Git snapshot exact retry child identity is invalid",
7160                false,
7161            ));
7162        }
7163        let state = self.lock_state()?;
7164        if state.jobs.contains_key(child_operation_id)
7165            || state.reservations.contains(child_operation_id)
7166        {
7167            return Err(provider_failure(
7168                "snapshot_exact_retry_child_exists",
7169                "Git snapshot retry child operation already exists",
7170                false,
7171            ));
7172        }
7173        let parent = state
7174            .jobs
7175            .get(parent_job_id)
7176            .ok_or_else(|| job_not_found(parent_job_id))?;
7177        if parent.operation_id != parent_job_id
7178            || !parent.cleanup_requested
7179            || parent.state != JobState::Cleaned
7180        {
7181            return Err(provider_failure(
7182                "snapshot_exact_retry_parent_not_terminal",
7183                "Git snapshot exact retry requires completed parent cleanup",
7184                false,
7185            ));
7186        }
7187        let snapshot = parent
7188            .git_snapshot
7189            .as_ref()
7190            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7191        if !matches!(
7192            snapshot.phase,
7193            GithubGitSnapshotPhaseV1::SourceDeleted
7194                | GithubGitSnapshotPhaseV1::SourceAbsent
7195                | GithubGitSnapshotPhaseV1::SourceConflict
7196        ) || snapshot.keepalive_release_authorization_sha256.is_some()
7197        {
7198            return Err(provider_failure(
7199                "snapshot_exact_retry_parent_releasing",
7200                "Git snapshot parent keepalive is already releasing or unavailable",
7201                false,
7202            ));
7203        }
7204        let keepalive_request = GitSnapshotKeepaliveRequest {
7205            operation_id: &parent.operation_id,
7206            keepalive_ref: &snapshot.stage.keepalive_ref,
7207            commit: &snapshot.stage.graph.commit,
7208        };
7209        let precompute_request = GitSnapshotExactRetryPrecomputeRequest {
7210            parent_request: &parent.request,
7211            parent_stage: &snapshot.stage,
7212            child_operation_id,
7213            child_source_created_at_ms,
7214        };
7215        let mut publisher = self.lock_publisher()?;
7216        match publisher
7217            .reconcile_git_snapshot_keepalive(&keepalive_request)
7218            .map_err(publisher_failure)?
7219        {
7220            GitSnapshotRefReconciliation::Exact => {}
7221            GitSnapshotRefReconciliation::Absent => {
7222                return Err(provider_failure(
7223                    "snapshot_exact_retry_parent_keepalive_missing",
7224                    "Git snapshot parent keepalive is absent",
7225                    false,
7226                ));
7227            }
7228            GitSnapshotRefReconciliation::Conflict => {
7229                return Err(provider_failure(
7230                    "snapshot_exact_retry_parent_keepalive_conflict",
7231                    "Git snapshot parent keepalive changed",
7232                    false,
7233                ));
7234            }
7235        }
7236        let plan = publisher
7237            .precompute_exact_git_snapshot_retry(&precompute_request)
7238            .map_err(publisher_failure)?;
7239        match publisher
7240            .reconcile_git_snapshot_keepalive(&keepalive_request)
7241            .map_err(publisher_failure)?
7242        {
7243            GitSnapshotRefReconciliation::Exact => {}
7244            GitSnapshotRefReconciliation::Absent => {
7245                return Err(provider_failure(
7246                    "snapshot_exact_retry_parent_keepalive_lost",
7247                    "Git snapshot parent keepalive disappeared during retry planning",
7248                    false,
7249                ));
7250            }
7251            GitSnapshotRefReconciliation::Conflict => {
7252                return Err(provider_failure(
7253                    "snapshot_exact_retry_parent_keepalive_changed",
7254                    "Git snapshot parent keepalive changed during retry planning",
7255                    false,
7256                ));
7257            }
7258        }
7259        drop(publisher);
7260        cancellation.check()?;
7261        Ok(plan)
7262    }
7263
7264    /// Create one exact child retry stage from a verified terminal parent's private archive blob.
7265    ///
7266    /// The caller must durably bind the parent/child retry lineage before invoking this method.
7267    /// No workspace is recaptured and no Git object or ref is mutated. The parent keepalive is
7268    /// required at its exact commit before and after the bounded child stage is published.
7269    ///
7270    /// # Errors
7271    ///
7272    /// Returns a typed failure for missing lineage authority, a nonterminal or releasing parent,
7273    /// a missing/conflicting keepalive, parent blob size/SHA/object mismatch, or child stage and
7274    /// descriptor/graph failure.
7275    #[allow(
7276        clippy::too_many_lines,
7277        reason = "lineage authorization, child lease, replayable stage, and parent recheck must stay visibly ordered"
7278    )]
7279    pub fn stage_exact_git_snapshot_retry(
7280        &self,
7281        authorization: &GithubGitSnapshotExactRetryAuthorizationV1,
7282        expected_plan: &GithubGitSnapshotExactRetryPlanV1,
7283        cancellation: &CancellationToken,
7284    ) -> RemoteBuildResult<GithubGitSnapshotExactRetryStageV1> {
7285        cancellation.check()?;
7286        if !self
7287            .capabilities
7288            .source_modes
7289            .contains(&SourceMode::GitSnapshot)
7290        {
7291            return Err(unsupported(ProviderFeature::SourceMode(
7292                SourceMode::GitSnapshot,
7293            )));
7294        }
7295        if expected_plan.request.operation_id != authorization.child_operation_id
7296            || expected_plan.source_created_at_ms != authorization.child_source_created_at_ms
7297            || expected_plan.request.source_revision.as_deref()
7298                != Some(expected_plan.graph.commit.as_str())
7299        {
7300            return Err(provider_failure(
7301                "snapshot_exact_retry_plan_mismatch",
7302                "Git snapshot retry plan differs from durable lineage authority",
7303                false,
7304            ));
7305        }
7306        let state = self.lock_state()?;
7307        if state.jobs.contains_key(&authorization.child_operation_id)
7308            || state
7309                .reservations
7310                .contains(&authorization.child_operation_id)
7311        {
7312            return Err(provider_failure(
7313                "snapshot_exact_retry_child_exists",
7314                "Git snapshot retry child operation already exists",
7315                false,
7316            ));
7317        }
7318        let parent = state
7319            .jobs
7320            .get(&authorization.parent_job_id)
7321            .ok_or_else(|| job_not_found(&authorization.parent_job_id))?;
7322        if parent.operation_id != authorization.parent_job_id
7323            || !parent.cleanup_requested
7324            || parent.state != JobState::Cleaned
7325        {
7326            return Err(provider_failure(
7327                "snapshot_exact_retry_parent_not_terminal",
7328                "Git snapshot exact retry requires completed parent cleanup",
7329                false,
7330            ));
7331        }
7332        let snapshot = parent
7333            .git_snapshot
7334            .as_ref()
7335            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7336        if !matches!(
7337            snapshot.phase,
7338            GithubGitSnapshotPhaseV1::SourceDeleted
7339                | GithubGitSnapshotPhaseV1::SourceAbsent
7340                | GithubGitSnapshotPhaseV1::SourceConflict
7341        ) || snapshot.keepalive_release_authorization_sha256.is_some()
7342        {
7343            return Err(provider_failure(
7344                "snapshot_exact_retry_parent_releasing",
7345                "Git snapshot parent keepalive is already releasing or unavailable",
7346                false,
7347            ));
7348        }
7349        let keepalive_request = GitSnapshotKeepaliveRequest {
7350            operation_id: &parent.operation_id,
7351            keepalive_ref: &snapshot.stage.keepalive_ref,
7352            commit: &snapshot.stage.graph.commit,
7353        };
7354        let stage_request = GitSnapshotExactRetryStageRequest {
7355            parent_request: &parent.request,
7356            parent_stage: &snapshot.stage,
7357            expected_plan,
7358            retry_lineage_authorization_sha256: &authorization.retry_lineage_authorization_sha256,
7359        };
7360        let mut publisher = self.lock_publisher()?;
7361        if !publisher.publication_attempt_is_process_fenced() {
7362            return Err(provider_failure(
7363                "snapshot_exact_retry_process_unfenced",
7364                "Git snapshot retry staging requires a cross-process child lease",
7365                false,
7366            ));
7367        }
7368        let child_source_ref =
7369            GitSnapshotSourceRef::for_operation(&authorization.child_operation_id)
7370                .map_err(|_| resume_failure("snapshot_exact_retry_authorization_invalid"))?;
7371        let child_lease = GitSnapshotSourcePublicationLeaseRequest {
7372            source_repository: &self.config.source_repository,
7373            source_ref: &child_source_ref,
7374        };
7375        if publisher
7376            .acquire_git_snapshot_source_lease(&child_lease)
7377            .map_err(publisher_failure)?
7378            != TemporaryRefPublicationLease::Acquired
7379        {
7380            return Err(provider_failure(
7381                "snapshot_exact_retry_child_active",
7382                "Another live provider owns this Git snapshot retry child",
7383                true,
7384            ));
7385        }
7386        let result = (|| {
7387            match publisher
7388                .reconcile_git_snapshot_keepalive(&keepalive_request)
7389                .map_err(publisher_failure)?
7390            {
7391                GitSnapshotRefReconciliation::Exact => {}
7392                GitSnapshotRefReconciliation::Absent => {
7393                    return Err(provider_failure(
7394                        "snapshot_exact_retry_parent_keepalive_missing",
7395                        "Git snapshot parent keepalive is absent",
7396                        false,
7397                    ));
7398                }
7399                GitSnapshotRefReconciliation::Conflict => {
7400                    return Err(provider_failure(
7401                        "snapshot_exact_retry_parent_keepalive_conflict",
7402                        "Git snapshot parent keepalive changed",
7403                        false,
7404                    ));
7405                }
7406            }
7407            let child = publisher
7408                .stage_exact_git_snapshot_retry(&stage_request)
7409                .map_err(publisher_failure)?;
7410            match publisher
7411                .reconcile_git_snapshot_keepalive(&keepalive_request)
7412                .map_err(publisher_failure)?
7413            {
7414                GitSnapshotRefReconciliation::Exact => {}
7415                GitSnapshotRefReconciliation::Absent => {
7416                    return Err(provider_failure(
7417                        "snapshot_exact_retry_parent_keepalive_lost",
7418                        "Git snapshot parent keepalive disappeared during retry staging",
7419                        false,
7420                    ));
7421                }
7422                GitSnapshotRefReconciliation::Conflict => {
7423                    return Err(provider_failure(
7424                        "snapshot_exact_retry_parent_keepalive_changed",
7425                        "Git snapshot parent keepalive changed during retry staging",
7426                        false,
7427                    ));
7428                }
7429            }
7430            cancellation.check()?;
7431            Ok(child)
7432        })();
7433        publisher.release_git_snapshot_source_lease(&child_lease);
7434        drop(publisher);
7435        result
7436    }
7437
7438    /// Release one exact local snapshot keepalive under complete-lineage prune authority.
7439    ///
7440    /// The caller remains responsible for proving complete-lineage prune authority and retaining
7441    /// every controller/store lease until this method returns. The provider binds the stable
7442    /// lineage IDs/edges/cutoff digest into its own checkpoint before any local ref mutation,
7443    /// verifies the exact expected commit, and checkpoints confirmed absence before returning.
7444    ///
7445    /// # Errors
7446    ///
7447    /// Returns a typed failure unless the exact restored job has completed terminal cleanup, its
7448    /// remote source ref is already absent or conflict-proven, checkpoints are durable, and the
7449    /// local keepalive can be reconciled and removed without conflict.
7450    #[allow(
7451        clippy::too_many_lines,
7452        reason = "prune authorization, durable intent, exact release, and absence checkpoint are one crash-safe primitive"
7453    )]
7454    pub fn release_git_snapshot_keepalive_for_prune(
7455        &self,
7456        authorization: &GithubGitSnapshotKeepaliveReleaseAuthorizationV1,
7457        cancellation: &CancellationToken,
7458    ) -> RemoteBuildResult<()> {
7459        cancellation.check()?;
7460        if self.checkpoint_sink.is_none() {
7461            return Err(provider_failure(
7462                "snapshot_checkpoint_required",
7463                "Git snapshot keepalive release requires durable provider checkpoints",
7464                false,
7465            ));
7466        }
7467        if !self
7468            .config
7469            .mutation_authorization
7470            .manage_git_snapshot_keepalive
7471        {
7472            return Err(provider_failure(
7473                "snapshot_keepalive_unauthorized",
7474                "Git snapshot keepalive mutation is not authorized",
7475                false,
7476            ));
7477        }
7478        let mut state = self.lock_state()?;
7479        let record = state
7480            .jobs
7481            .get_mut(authorization.job_id())
7482            .ok_or_else(|| job_not_found(authorization.job_id()))?;
7483        if record.operation_id != authorization.job_id
7484            || !record.cleanup_requested
7485            || record.state != JobState::Cleaned
7486        {
7487            return Err(provider_failure(
7488                "snapshot_keepalive_prune_not_terminal",
7489                "Git snapshot keepalive release requires completed terminal cleanup",
7490                false,
7491            ));
7492        }
7493        let snapshot = record
7494            .git_snapshot
7495            .as_mut()
7496            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7497        let reacknowledge_release_intent =
7498            snapshot.phase == GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent;
7499        match snapshot.phase {
7500            GithubGitSnapshotPhaseV1::SourceDeleted
7501            | GithubGitSnapshotPhaseV1::SourceAbsent
7502            | GithubGitSnapshotPhaseV1::SourceConflict => {
7503                snapshot.keepalive_release_authorization_sha256 =
7504                    Some(authorization.complete_lineage_authorization_sha256.clone());
7505                snapshot.phase = GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent;
7506                self.checkpoint_record(record)?;
7507            }
7508            GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
7509            | GithubGitSnapshotPhaseV1::KeepaliveReleased => {
7510                if snapshot.keepalive_release_authorization_sha256.as_deref()
7511                    != Some(authorization.complete_lineage_authorization_sha256())
7512                {
7513                    return Err(provider_failure(
7514                        "snapshot_prune_authorization_changed",
7515                        "Git snapshot complete-lineage authorization changed after intent",
7516                        false,
7517                    ));
7518                }
7519                if snapshot.phase == GithubGitSnapshotPhaseV1::KeepaliveReleased {
7520                    self.checkpoint_record(record)?;
7521                    return Ok(());
7522                }
7523            }
7524            _ => {
7525                return Err(provider_failure(
7526                    "snapshot_keepalive_prune_source_not_clean",
7527                    "Git snapshot source cleanup is incomplete",
7528                    false,
7529                ));
7530            }
7531        }
7532        if reacknowledge_release_intent {
7533            self.checkpoint_record(record)?;
7534        }
7535        cancellation.check()?;
7536        let snapshot = record
7537            .git_snapshot
7538            .as_ref()
7539            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7540        let request = GitSnapshotKeepaliveRequest {
7541            operation_id: &record.operation_id,
7542            keepalive_ref: &snapshot.stage.keepalive_ref,
7543            commit: &snapshot.stage.graph.commit,
7544        };
7545        let reconciliation = {
7546            let mut publisher = self.lock_publisher()?;
7547            publisher
7548                .reconcile_git_snapshot_keepalive(&request)
7549                .map_err(publisher_failure)?
7550        };
7551        match reconciliation {
7552            GitSnapshotRefReconciliation::Absent => {}
7553            GitSnapshotRefReconciliation::Exact => {
7554                self.lock_publisher()?
7555                    .release_git_snapshot_keepalive(&request)
7556                    .map_err(publisher_failure)?;
7557            }
7558            GitSnapshotRefReconciliation::Conflict => {
7559                return Err(provider_failure(
7560                    "snapshot_keepalive_conflict",
7561                    "Git snapshot keepalive changed before lineage prune",
7562                    false,
7563                ));
7564            }
7565        }
7566        let final_reconciliation = {
7567            let mut publisher = self.lock_publisher()?;
7568            publisher
7569                .reconcile_git_snapshot_keepalive(&request)
7570                .map_err(publisher_failure)?
7571        };
7572        if final_reconciliation != GitSnapshotRefReconciliation::Absent {
7573            return Err(provider_failure(
7574                "snapshot_keepalive_release_unconfirmed",
7575                "Git snapshot keepalive absence was not confirmed",
7576                false,
7577            ));
7578        }
7579        record
7580            .git_snapshot
7581            .as_mut()
7582            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?
7583            .phase = GithubGitSnapshotPhaseV1::KeepaliveReleased;
7584        self.checkpoint_record(record)
7585    }
7586
7587    fn current_public_snapshot_source_identity(
7588        &self,
7589        record: &JobRecord,
7590    ) -> RemoteBuildResult<(GithubPrincipalIdentityV1, u64)>
7591    where
7592        R: GhRunner,
7593    {
7594        let identity = self.current_resume_identity()?;
7595        Self::validate_record_identity(record, &identity)?;
7596        let source_repository =
7597            repository_from_url(&self.config.source_repository).ok_or_else(|| {
7598                provider_failure(
7599                    "source_repository_invalid",
7600                    "configured source repository identity is invalid",
7601                    false,
7602                )
7603            })?;
7604        let source = self
7605            .lock_transport()?
7606            .repository(&source_repository)
7607            .map_err(transport_failure)?;
7608        if source.is_private() || source.is_archived() || source.is_disabled() {
7609            return Err(provider_failure(
7610                "snapshot_source_repository_not_public_writable",
7611                "Git snapshots require the exact configured public writable source repository",
7612                false,
7613            ));
7614        }
7615        Ok(identity)
7616    }
7617
7618    fn revalidate_public_snapshot_source(&self, record: &JobRecord) -> RemoteBuildResult<()>
7619    where
7620        R: GhRunner,
7621    {
7622        self.current_public_snapshot_source_identity(record)
7623            .map(drop)
7624    }
7625
7626    fn acquire_snapshot_source_lease(
7627        &self,
7628        record: &mut JobRecord,
7629    ) -> RemoteBuildResult<(bool, String)> {
7630        let expected_scope = record
7631            .git_snapshot
7632            .as_ref()
7633            .and_then(|snapshot| snapshot.source_publication_lease_scope_sha256.as_deref());
7634        let source_ref = record
7635            .git_snapshot
7636            .as_ref()
7637            .map(|snapshot| snapshot.stage.source_ref.clone())
7638            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7639        let mut publisher = self.lock_publisher()?;
7640        let actual_scope = publisher
7641            .publication_lease_scope_sha256()
7642            .filter(|scope| is_lower_sha256(scope))
7643            .ok_or_else(|| {
7644                provider_failure(
7645                    "snapshot_publication_lease_unavailable",
7646                    "Git snapshot source publication has no exact process lease scope",
7647                    true,
7648                )
7649            })?
7650            .to_owned();
7651        if expected_scope.is_some_and(|expected| expected != actual_scope) {
7652            return Err(provider_failure(
7653                "snapshot_publication_lease_scope_changed",
7654                "Git snapshot source publication lease scope changed",
7655                true,
7656            ));
7657        }
7658        if !publisher.publication_attempt_is_process_fenced() {
7659            return Err(provider_failure(
7660                "snapshot_publication_process_unfenced",
7661                "Git snapshot source publication process cannot be fenced across restart",
7662                false,
7663            ));
7664        }
7665        let lease_request = GitSnapshotSourcePublicationLeaseRequest {
7666            source_repository: &self.config.source_repository,
7667            source_ref: &source_ref,
7668        };
7669        match publisher
7670            .acquire_git_snapshot_source_lease(&lease_request)
7671            .map_err(publisher_failure)?
7672        {
7673            TemporaryRefPublicationLease::Acquired => {
7674                record.snapshot_source_lease_owned = true;
7675            }
7676            TemporaryRefPublicationLease::AlreadyOwned if record.snapshot_source_lease_owned => {}
7677            TemporaryRefPublicationLease::AlreadyOwned
7678            | TemporaryRefPublicationLease::HeldByOther => {
7679                return Err(provider_failure(
7680                    "snapshot_publication_owner_active",
7681                    "Another live provider owns Git snapshot source publication",
7682                    true,
7683                ));
7684            }
7685            TemporaryRefPublicationLease::Unsupported => {
7686                return Err(provider_failure(
7687                    "snapshot_publication_lease_unavailable",
7688                    "Git snapshot source publication requires a cross-process lease",
7689                    false,
7690                ));
7691            }
7692        }
7693        Ok((true, actual_scope))
7694    }
7695
7696    fn release_snapshot_source_lease(&self, record: &mut JobRecord) -> RemoteBuildResult<()> {
7697        if !record.snapshot_source_lease_owned {
7698            return Ok(());
7699        }
7700        let source_ref = record
7701            .git_snapshot
7702            .as_ref()
7703            .map(|snapshot| snapshot.stage.source_ref.clone())
7704            .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7705        self.lock_publisher()?.release_git_snapshot_source_lease(
7706            &GitSnapshotSourcePublicationLeaseRequest {
7707                source_repository: &self.config.source_repository,
7708                source_ref: &source_ref,
7709            },
7710        );
7711        record.snapshot_source_lease_owned = false;
7712        Ok(())
7713    }
7714
7715    fn fail_snapshot_before_dispatch(
7716        &self,
7717        record: &mut JobRecord,
7718        code: &'static str,
7719        message: &'static str,
7720    ) -> RemoteBuildResult<()> {
7721        if record.state == JobState::Created {
7722            transition_record(record, JobState::Failed)?;
7723            append_failed(record, self.clock.now_ms(), code, message, false)?;
7724        }
7725        self.checkpoint_record(record)
7726    }
7727
7728    fn cleanup_git_snapshot_source(&self, record: &mut JobRecord) -> RemoteBuildResult<()>
7729    where
7730        R: GhRunner,
7731    {
7732        let Some(snapshot) = record.git_snapshot.clone() else {
7733            return Ok(());
7734        };
7735        if matches!(
7736            snapshot.phase,
7737            GithubGitSnapshotPhaseV1::SourceExact | GithubGitSnapshotPhaseV1::SourceCleanupIntent
7738        ) && !self
7739            .config
7740            .mutation_authorization
7741            .delete_git_snapshot_source
7742        {
7743            return Err(provider_failure(
7744                "snapshot_source_cleanup_unauthorized",
7745                "Git snapshot source cleanup is not authorized",
7746                false,
7747            ));
7748        }
7749        match snapshot.phase {
7750            GithubGitSnapshotPhaseV1::SourceExact => {
7751                record
7752                    .git_snapshot
7753                    .as_mut()
7754                    .expect("checked snapshot")
7755                    .phase = GithubGitSnapshotPhaseV1::SourceCleanupIntent;
7756                self.checkpoint_record(record)?;
7757            }
7758            GithubGitSnapshotPhaseV1::SourceCleanupIntent => {
7759                self.checkpoint_record(record)?;
7760            }
7761            GithubGitSnapshotPhaseV1::SourceDeleted => {
7762                if record.snapshot_source_lease_owned {
7763                    self.checkpoint_record(record)?;
7764                    self.release_snapshot_source_lease(record)?;
7765                }
7766                return Ok(());
7767            }
7768            GithubGitSnapshotPhaseV1::SourceAbsent
7769            | GithubGitSnapshotPhaseV1::SourceConflict
7770            | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
7771            | GithubGitSnapshotPhaseV1::KeepaliveReleased => return Ok(()),
7772            _ => {
7773                return Err(provider_failure(
7774                    "snapshot_source_cleanup_not_owned",
7775                    "Git snapshot source cleanup lacks exact publication ownership",
7776                    false,
7777                ));
7778            }
7779        }
7780        let snapshot = record.git_snapshot.clone().expect("checked snapshot");
7781        self.acquire_snapshot_source_lease(record)?;
7782        self.revalidate_record_identity(record)?;
7783        self.lock_publisher()?
7784            .delete_git_snapshot_source(&GitSnapshotSourceDeleteRequest {
7785                source_repository: &self.config.source_repository,
7786                source_ref: &snapshot.stage.source_ref,
7787                expected_commit: &snapshot.stage.graph.commit,
7788                authorized: self
7789                    .config
7790                    .mutation_authorization
7791                    .delete_git_snapshot_source,
7792            })
7793            .map_err(publisher_failure)?;
7794        record
7795            .git_snapshot
7796            .as_mut()
7797            .expect("checked snapshot")
7798            .phase = GithubGitSnapshotPhaseV1::SourceDeleted;
7799        self.checkpoint_record(record)?;
7800        self.release_snapshot_source_lease(record)
7801    }
7802
7803    #[allow(
7804        clippy::too_many_lines,
7805        reason = "each snapshot crash boundary is kept adjacent to its durable checkpoint"
7806    )]
7807    fn continue_git_snapshot_submission(
7808        &self,
7809        job_id: &str,
7810        cancellation: &CancellationToken,
7811    ) -> RemoteBuildResult<()>
7812    where
7813        R: GhRunner,
7814        P: TemporaryRefReconciler,
7815    {
7816        let mut acknowledged_phase = None;
7817        loop {
7818            cancellation.check()?;
7819            let workflow_dispatch_snapshot_identity =
7820                if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
7821                    let working = {
7822                        let state = self.lock_state()?;
7823                        state
7824                            .jobs
7825                            .get(job_id)
7826                            .cloned()
7827                            .ok_or_else(|| job_not_found(job_id))?
7828                    };
7829                    let phase = working
7830                        .git_snapshot
7831                        .as_ref()
7832                        .map(|snapshot| snapshot.phase)
7833                        .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7834                    if matches!(
7835                        phase,
7836                        GithubGitSnapshotPhaseV1::Prepared | GithubGitSnapshotPhaseV1::SourceExact
7837                    ) {
7838                        self.verify_execution_workflow_default()?;
7839                    }
7840                    matches!(
7841                        phase,
7842                        GithubGitSnapshotPhaseV1::Prepared
7843                            | GithubGitSnapshotPhaseV1::SourcePublishIntent
7844                            | GithubGitSnapshotPhaseV1::SourceExact
7845                    )
7846                    .then(|| self.current_public_snapshot_source_identity(&working))
7847                    .transpose()?
7848                } else {
7849                    None
7850                };
7851            let mut state = self.lock_state()?;
7852            let record = state
7853                .jobs
7854                .get_mut(job_id)
7855                .ok_or_else(|| job_not_found(job_id))?;
7856            let snapshot = record
7857                .git_snapshot
7858                .clone()
7859                .ok_or_else(|| resume_failure("resume_git_snapshot_binding_missing"))?;
7860            let phase_was_acknowledged = acknowledged_phase.take() == Some(snapshot.phase);
7861            if snapshot.phase != GithubGitSnapshotPhaseV1::Prepared && !phase_was_acknowledged {
7862                self.checkpoint_record(record)?;
7863            }
7864            match snapshot.phase {
7865                GithubGitSnapshotPhaseV1::Prepared => {
7866                    if let Some(identity) = &workflow_dispatch_snapshot_identity {
7867                        Self::validate_record_identity(record, identity)?;
7868                    } else {
7869                        self.revalidate_public_snapshot_source(record)?;
7870                    }
7871                    record
7872                        .git_snapshot
7873                        .as_mut()
7874                        .expect("checked snapshot")
7875                        .phase = GithubGitSnapshotPhaseV1::KeepaliveIntent;
7876                    self.checkpoint_record(record)?;
7877                    acknowledged_phase = Some(GithubGitSnapshotPhaseV1::KeepaliveIntent);
7878                }
7879                GithubGitSnapshotPhaseV1::KeepaliveIntent => {
7880                    if !self
7881                        .config
7882                        .mutation_authorization
7883                        .manage_git_snapshot_keepalive
7884                    {
7885                        return Err(provider_failure(
7886                            "snapshot_keepalive_unauthorized",
7887                            "Git snapshot keepalive mutation is not authorized",
7888                            false,
7889                        ));
7890                    }
7891                    let keepalive_request = GitSnapshotKeepaliveRequest {
7892                        operation_id: &record.operation_id,
7893                        keepalive_ref: &snapshot.stage.keepalive_ref,
7894                        commit: &snapshot.stage.graph.commit,
7895                    };
7896                    let reconciliation = self
7897                        .lock_publisher()?
7898                        .reconcile_git_snapshot_keepalive(&keepalive_request)
7899                        .map_err(publisher_failure)?;
7900                    match reconciliation {
7901                        GitSnapshotRefReconciliation::Exact => {}
7902                        GitSnapshotRefReconciliation::Conflict => {
7903                            return Err(provider_failure(
7904                                "snapshot_keepalive_conflict",
7905                                "Git snapshot keepalive ref is occupied by another commit",
7906                                false,
7907                            ));
7908                        }
7909                        GitSnapshotRefReconciliation::Absent => {
7910                            let import_request = GitSnapshotImportRequest {
7911                                locator: &snapshot.stage_locator,
7912                                expected_stage: &snapshot.stage,
7913                                request: &record.request,
7914                            };
7915                            let mut publisher = self.lock_publisher()?;
7916                            let imported = publisher
7917                                .import_git_snapshot(&import_request)
7918                                .map_err(publisher_failure)?;
7919                            if imported != snapshot.stage.graph {
7920                                return Err(provider_failure(
7921                                    "snapshot_import_graph_mismatch",
7922                                    "Git snapshot import differs from the durable object graph",
7923                                    false,
7924                                ));
7925                            }
7926                            publisher
7927                                .create_git_snapshot_keepalive(&keepalive_request)
7928                                .map_err(publisher_failure)?;
7929                        }
7930                    }
7931                    record
7932                        .git_snapshot
7933                        .as_mut()
7934                        .expect("checked snapshot")
7935                        .phase = GithubGitSnapshotPhaseV1::KeepaliveExact;
7936                    self.checkpoint_record(record)?;
7937                    acknowledged_phase = Some(GithubGitSnapshotPhaseV1::KeepaliveExact);
7938                }
7939                GithubGitSnapshotPhaseV1::KeepaliveExact => {
7940                    record
7941                        .git_snapshot
7942                        .as_mut()
7943                        .expect("checked snapshot")
7944                        .phase = GithubGitSnapshotPhaseV1::StageDeleteIntent;
7945                    self.checkpoint_record(record)?;
7946                    acknowledged_phase = Some(GithubGitSnapshotPhaseV1::StageDeleteIntent);
7947                }
7948                GithubGitSnapshotPhaseV1::StageDeleteIntent => {
7949                    let keepalive_request = GitSnapshotKeepaliveRequest {
7950                        operation_id: &record.operation_id,
7951                        keepalive_ref: &snapshot.stage.keepalive_ref,
7952                        commit: &snapshot.stage.graph.commit,
7953                    };
7954                    match self
7955                        .lock_publisher()?
7956                        .reconcile_git_snapshot_keepalive(&keepalive_request)
7957                        .map_err(publisher_failure)?
7958                    {
7959                        GitSnapshotRefReconciliation::Exact => {}
7960                        GitSnapshotRefReconciliation::Absent => {
7961                            return Err(provider_failure(
7962                                "snapshot_keepalive_missing",
7963                                "Git snapshot keepalive disappeared before stage deletion",
7964                                false,
7965                            ));
7966                        }
7967                        GitSnapshotRefReconciliation::Conflict => {
7968                            return Err(provider_failure(
7969                                "snapshot_keepalive_conflict",
7970                                "Git snapshot keepalive changed before stage deletion",
7971                                false,
7972                            ));
7973                        }
7974                    }
7975                    self.lock_publisher()?
7976                        .delete_git_snapshot_stage(&GitSnapshotImportRequest {
7977                            locator: &snapshot.stage_locator,
7978                            expected_stage: &snapshot.stage,
7979                            request: &record.request,
7980                        })
7981                        .map_err(publisher_failure)?;
7982                    record
7983                        .git_snapshot
7984                        .as_mut()
7985                        .expect("checked snapshot")
7986                        .phase = GithubGitSnapshotPhaseV1::StageDeleted;
7987                    self.checkpoint_record(record)?;
7988                    acknowledged_phase = Some(GithubGitSnapshotPhaseV1::StageDeleted);
7989                }
7990                GithubGitSnapshotPhaseV1::StageDeleted => {
7991                    let (process_fenced, scope) = self.acquire_snapshot_source_lease(record)?;
7992                    let snapshot = record.git_snapshot.as_mut().expect("checked snapshot");
7993                    snapshot.phase = GithubGitSnapshotPhaseV1::SourcePublishIntent;
7994                    snapshot.source_publication_attempts = 1;
7995                    snapshot.source_publication_started_at_ms = 0;
7996                    snapshot.source_publication_quiescence_deadline_ms = u64::MAX;
7997                    snapshot.source_publication_process_fenced = process_fenced;
7998                    snapshot.source_publication_lease_scope_sha256 = Some(scope);
7999                    snapshot.source_publication_absence_observations = 0;
8000                    snapshot.source_publication_absence_first_observed_at_ms = 0;
8001                    snapshot.source_publication_absence_last_observed_at_ms = 0;
8002                    self.checkpoint_record(record)?;
8003                    acknowledged_phase = Some(GithubGitSnapshotPhaseV1::SourcePublishIntent);
8004                }
8005                GithubGitSnapshotPhaseV1::SourcePublishIntent => {
8006                    self.acquire_snapshot_source_lease(record)?;
8007                    if snapshot.source_publication_started_at_ms == 0 {
8008                        if let Some(identity) = &workflow_dispatch_snapshot_identity {
8009                            Self::validate_record_identity(record, identity)?;
8010                        } else {
8011                            self.revalidate_public_snapshot_source(record)?;
8012                        }
8013                        let started_at_ms = self.clock.now_ms();
8014                        {
8015                            let snapshot = record.git_snapshot.as_mut().expect("checked snapshot");
8016                            snapshot.source_publication_started_at_ms = started_at_ms;
8017                            snapshot.source_publication_quiescence_deadline_ms =
8018                                publication_quiescence_deadline(started_at_ms, 0);
8019                        }
8020                        self.checkpoint_record(record)?;
8021                        let result = self
8022                            .lock_publisher()?
8023                            .publish_git_snapshot_source(&GitSnapshotSourcePublishRequest {
8024                                source_repository: &self.config.source_repository,
8025                                source_ref: &snapshot.stage.source_ref,
8026                                commit: &snapshot.stage.graph.commit,
8027                                authorized: self
8028                                    .config
8029                                    .mutation_authorization
8030                                    .publish_git_snapshot_source,
8031                            })
8032                            .map_err(publisher_failure)?;
8033                        match result {
8034                            GitSnapshotSourcePublication::Exact => {
8035                                record
8036                                    .git_snapshot
8037                                    .as_mut()
8038                                    .expect("checked snapshot")
8039                                    .phase = GithubGitSnapshotPhaseV1::SourceExact;
8040                                self.checkpoint_record(record)?;
8041                                acknowledged_phase = Some(GithubGitSnapshotPhaseV1::SourceExact);
8042                            }
8043                            GitSnapshotSourcePublication::Conflict => {
8044                                record
8045                                    .git_snapshot
8046                                    .as_mut()
8047                                    .expect("checked snapshot")
8048                                    .phase = GithubGitSnapshotPhaseV1::SourceConflict;
8049                                self.fail_snapshot_before_dispatch(
8050                                    record,
8051                                    "snapshot_source_ref_conflict",
8052                                    "Git snapshot source ref is occupied by another commit",
8053                                )?;
8054                                self.release_snapshot_source_lease(record)?;
8055                                return Ok(());
8056                            }
8057                            GitSnapshotSourcePublication::Uncertain => {
8058                                let observed_at_ms = self.clock.now_ms();
8059                                let snapshot =
8060                                    record.git_snapshot.as_mut().expect("checked snapshot");
8061                                snapshot.source_publication_absence_observations = 1;
8062                                snapshot.source_publication_absence_first_observed_at_ms =
8063                                    observed_at_ms;
8064                                snapshot.source_publication_absence_last_observed_at_ms =
8065                                    observed_at_ms;
8066                                snapshot.source_publication_quiescence_deadline_ms =
8067                                    publication_quiescence_deadline(
8068                                        snapshot.source_publication_started_at_ms,
8069                                        observed_at_ms,
8070                                    );
8071                                self.checkpoint_record(record)?;
8072                                return Err(provider_failure(
8073                                    "snapshot_source_publication_uncertain",
8074                                    "Git snapshot source publication requires exact reconciliation",
8075                                    true,
8076                                ));
8077                            }
8078                        }
8079                    } else {
8080                        if let Some(identity) = &workflow_dispatch_snapshot_identity {
8081                            Self::validate_record_identity(record, identity)?;
8082                        } else {
8083                            self.revalidate_public_snapshot_source(record)?;
8084                        }
8085                        let reconciliation = self
8086                            .lock_publisher()?
8087                            .reconcile_git_snapshot_source(&GitSnapshotSourcePublishRequest {
8088                                source_repository: &self.config.source_repository,
8089                                source_ref: &snapshot.stage.source_ref,
8090                                commit: &snapshot.stage.graph.commit,
8091                                authorized: true,
8092                            })
8093                            .map_err(publisher_failure)?;
8094                        match reconciliation {
8095                            GitSnapshotRefReconciliation::Exact => {
8096                                record
8097                                    .git_snapshot
8098                                    .as_mut()
8099                                    .expect("checked snapshot")
8100                                    .phase = GithubGitSnapshotPhaseV1::SourceExact;
8101                                self.checkpoint_record(record)?;
8102                                acknowledged_phase = Some(GithubGitSnapshotPhaseV1::SourceExact);
8103                            }
8104                            GitSnapshotRefReconciliation::Conflict => {
8105                                record
8106                                    .git_snapshot
8107                                    .as_mut()
8108                                    .expect("checked snapshot")
8109                                    .phase = GithubGitSnapshotPhaseV1::SourceConflict;
8110                                self.fail_snapshot_before_dispatch(
8111                                    record,
8112                                    "snapshot_source_ref_conflict",
8113                                    "Git snapshot source ref is occupied by another commit",
8114                                )?;
8115                                self.release_snapshot_source_lease(record)?;
8116                                return Ok(());
8117                            }
8118                            GitSnapshotRefReconciliation::Absent => {
8119                                let observed_at_ms = self.clock.now_ms();
8120                                let snapshot =
8121                                    record.git_snapshot.as_mut().expect("checked snapshot");
8122                                if snapshot.source_publication_absence_observations == 0 {
8123                                    snapshot.source_publication_absence_first_observed_at_ms =
8124                                        observed_at_ms;
8125                                    snapshot.source_publication_quiescence_deadline_ms =
8126                                        publication_quiescence_deadline(
8127                                            snapshot.source_publication_started_at_ms,
8128                                            observed_at_ms,
8129                                        );
8130                                }
8131                                snapshot.source_publication_absence_observations = snapshot
8132                                    .source_publication_absence_observations
8133                                    .saturating_add(1)
8134                                    .min(2);
8135                                snapshot.source_publication_absence_last_observed_at_ms =
8136                                    observed_at_ms;
8137                                let absent = snapshot.source_publication_absence_observations >= 2
8138                                    && snapshot.source_publication_process_fenced
8139                                    && observed_at_ms
8140                                        >= snapshot.source_publication_quiescence_deadline_ms;
8141                                if absent {
8142                                    snapshot.phase = GithubGitSnapshotPhaseV1::SourceAbsent;
8143                                }
8144                                self.checkpoint_record(record)?;
8145                                acknowledged_phase = Some(
8146                                    record
8147                                        .git_snapshot
8148                                        .as_ref()
8149                                        .expect("checked snapshot")
8150                                        .phase,
8151                                );
8152                                if !absent {
8153                                    return Err(provider_failure(
8154                                        "snapshot_source_reconciliation_pending",
8155                                        "Git snapshot source absence is not yet strongly proven",
8156                                        true,
8157                                    ));
8158                                }
8159                            }
8160                        }
8161                    }
8162                }
8163                GithubGitSnapshotPhaseV1::SourceAbsent => {
8164                    if snapshot.source_publication_attempts >= 3 {
8165                        self.fail_snapshot_before_dispatch(
8166                            record,
8167                            "snapshot_source_publication_absent",
8168                            "Git snapshot source publication remained absent after bounded retries",
8169                        )?;
8170                        self.release_snapshot_source_lease(record)?;
8171                        return Ok(());
8172                    }
8173                    self.acquire_snapshot_source_lease(record)?;
8174                    let snapshot = record.git_snapshot.as_mut().expect("checked snapshot");
8175                    snapshot.phase = GithubGitSnapshotPhaseV1::SourcePublishIntent;
8176                    snapshot.source_publication_attempts += 1;
8177                    snapshot.source_publication_started_at_ms = 0;
8178                    snapshot.source_publication_quiescence_deadline_ms = u64::MAX;
8179                    snapshot.source_publication_absence_observations = 0;
8180                    snapshot.source_publication_absence_first_observed_at_ms = 0;
8181                    snapshot.source_publication_absence_last_observed_at_ms = 0;
8182                    self.checkpoint_record(record)?;
8183                    acknowledged_phase = Some(GithubGitSnapshotPhaseV1::SourcePublishIntent);
8184                }
8185                GithubGitSnapshotPhaseV1::SourceConflict => {
8186                    self.release_snapshot_source_lease(record)?;
8187                    return Ok(());
8188                }
8189                GithubGitSnapshotPhaseV1::SourceExact => {
8190                    self.acquire_snapshot_source_lease(record)?;
8191                    if let Some(identity) = &workflow_dispatch_snapshot_identity {
8192                        Self::validate_record_identity(record, identity)?;
8193                    } else {
8194                        self.revalidate_public_snapshot_source(record)?;
8195                    }
8196                    let reconciliation = self
8197                        .lock_publisher()?
8198                        .reconcile_git_snapshot_source(&GitSnapshotSourcePublishRequest {
8199                            source_repository: &self.config.source_repository,
8200                            source_ref: &snapshot.stage.source_ref,
8201                            commit: &snapshot.stage.graph.commit,
8202                            authorized: true,
8203                        })
8204                        .map_err(publisher_failure)?;
8205                    if reconciliation != GitSnapshotRefReconciliation::Exact {
8206                        record
8207                            .git_snapshot
8208                            .as_mut()
8209                            .expect("checked snapshot")
8210                            .phase = GithubGitSnapshotPhaseV1::SourceConflict;
8211                        self.fail_snapshot_before_dispatch(
8212                            record,
8213                            "snapshot_source_ownership_lost",
8214                            "Git snapshot source ref changed before dispatch publication",
8215                        )?;
8216                        self.release_snapshot_source_lease(record)?;
8217                        return Ok(());
8218                    }
8219                    if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
8220                        drop(state);
8221                        return self
8222                            .publish_git_snapshot_dispatch_workflow_dispatch(job_id, cancellation);
8223                    }
8224                    let result = self.publish_git_snapshot_dispatch(record, cancellation);
8225                    if !record.publication_intent {
8226                        self.release_snapshot_source_lease(record)?;
8227                    }
8228                    return result;
8229                }
8230                GithubGitSnapshotPhaseV1::SourceCleanupIntent
8231                | GithubGitSnapshotPhaseV1::SourceDeleted
8232                | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
8233                | GithubGitSnapshotPhaseV1::KeepaliveReleased => return Ok(()),
8234            }
8235        }
8236    }
8237
8238    #[allow(
8239        clippy::too_many_lines,
8240        reason = "the snapshot dispatch intent, fresh identity barrier, ref mutation, and lease release are one ordered boundary"
8241    )]
8242    fn publish_git_snapshot_dispatch_workflow_dispatch(
8243        &self,
8244        job_id: &str,
8245        cancellation: &CancellationToken,
8246    ) -> RemoteBuildResult<()>
8247    where
8248        R: GhRunner,
8249    {
8250        cancellation.check()?;
8251        let mut working = {
8252            let state = self.lock_state()?;
8253            state
8254                .jobs
8255                .get(job_id)
8256                .cloned()
8257                .ok_or_else(|| job_not_found(job_id))?
8258        };
8259        if working.publication_intent {
8260            return Ok(());
8261        }
8262        let snapshot = working
8263            .git_snapshot
8264            .clone()
8265            .filter(|snapshot| snapshot.phase == GithubGitSnapshotPhaseV1::SourceExact)
8266            .ok_or_else(|| resume_failure("resume_git_snapshot_source_not_exact"))?;
8267        let initial_identity = self.current_public_snapshot_source_identity(&working)?;
8268        let workflow = generate_workflow(&self.config.workflow);
8269        let manifest = GithubDispatchManifest::new(
8270            &self.config,
8271            &working.temporary_ref,
8272            working.request.clone(),
8273        );
8274        manifest.validate_for(&self.config)?;
8275        let manifest_bytes = manifest.encode()?;
8276        let source_revision = working.source_revision.clone();
8277        let snapshot_source_ref = snapshot.stage.source_ref.clone();
8278        let temporary_ref = working.temporary_ref.clone();
8279        let operation_id = working.operation_id.clone();
8280        let created_at_ms = working.created_at_ms;
8281        let publish_request = TemporaryRefPublishRequest {
8282            repository: &self.config.repository,
8283            source_repository: &self.config.source_repository,
8284            trusted_source_ref: self.config.workflow.trusted_source_ref(),
8285            source_revision: &source_revision,
8286            snapshot_source_ref: Some(&snapshot_source_ref),
8287            temporary_ref: &temporary_ref,
8288            workflow_path: workflow.path(),
8289            workflow_bytes: workflow.yaml().as_bytes(),
8290            workflow_fingerprint: &self.config.workflow_fingerprint,
8291            manifest_bytes: &manifest_bytes,
8292            operation_id: &operation_id,
8293            created_at_ms,
8294            authorized: self.config.mutation_authorization.publish_temporary_ref,
8295        };
8296        let prepared = self
8297            .lock_publisher()?
8298            .prepare(&publish_request)
8299            .map_err(publisher_failure)?;
8300        if prepared
8301            .as_ref()
8302            .is_some_and(|publication| publication.temporary_ref() != &temporary_ref)
8303        {
8304            return Err(publisher_failure(
8305                TemporaryRefPublishError::PublicationVerificationFailed,
8306            ));
8307        }
8308        let (publication_process_fenced, publication_lease_scope_sha256) =
8309            self.acquire_new_snapshot_dispatch_lease(&mut working)?;
8310        working.prepared_dispatch_commit = prepared
8311            .as_ref()
8312            .map(|publication| publication.commit().clone());
8313        working.publication_started_at_ms = 0;
8314        working.publication_quiescence_deadline_ms = u64::MAX;
8315        working.publication_intent = true;
8316        working.publication_process_fenced = publication_process_fenced;
8317        working.publication_lease_scope_sha256 = Some(publication_lease_scope_sha256.clone());
8318        {
8319            let mut state = self.lock_state()?;
8320            let record = state
8321                .jobs
8322                .get_mut(job_id)
8323                .ok_or_else(|| job_not_found(job_id))?;
8324            if record.git_snapshot.as_ref() != Some(&snapshot)
8325                || record.publication_intent
8326                || record.prepared_dispatch_commit.is_some()
8327                || record.dispatch_commit.is_some()
8328            {
8329                return Err(resume_failure("snapshot_dispatch_identity_changed"));
8330            }
8331            Self::validate_record_identity(record, &initial_identity)?;
8332            record
8333                .prepared_dispatch_commit
8334                .clone_from(&working.prepared_dispatch_commit);
8335            record.publication_started_at_ms = 0;
8336            record.publication_quiescence_deadline_ms = u64::MAX;
8337            record.publication_intent = true;
8338            record.publication_process_fenced = publication_process_fenced;
8339            record.publication_lease_scope_sha256 = Some(publication_lease_scope_sha256);
8340            record.publication_takeover_lease_owned = true;
8341            self.checkpoint_record(record)?;
8342        }
8343
8344        let publication_identity = self.current_public_snapshot_source_identity(&working);
8345        let boundary_cancellation = cancellation.check();
8346        let boundary_result = (|| {
8347            let mut state = self.lock_state()?;
8348            let record = state
8349                .jobs
8350                .get_mut(job_id)
8351                .ok_or_else(|| job_not_found(job_id))?;
8352            let identity = publication_identity?;
8353            boundary_cancellation?;
8354            Self::validate_record_identity(record, &identity)?;
8355            if record.prepared_dispatch_commit != working.prepared_dispatch_commit
8356                || !record.publication_intent
8357                || record.publication_uncertain
8358                || record.dispatch_commit.is_some()
8359                || record.git_snapshot.as_ref() != Some(&snapshot)
8360            {
8361                return Err(resume_failure("snapshot_dispatch_identity_changed"));
8362            }
8363            if record.cancellation_requested {
8364                return Err(provider_failure(
8365                    "publication_cancelled_before_mutation",
8366                    "GitHub snapshot dispatch was cancelled before any remote ref mutation",
8367                    false,
8368                ));
8369            }
8370            record.publication_started_at_ms = self.clock.now_ms();
8371            record.publication_quiescence_deadline_ms =
8372                publication_quiescence_deadline(record.publication_started_at_ms, 0);
8373            working.publication_started_at_ms = record.publication_started_at_ms;
8374            working.publication_quiescence_deadline_ms = record.publication_quiescence_deadline_ms;
8375            self.checkpoint_record(record)
8376        })();
8377        if let Err(error) = boundary_result {
8378            let mut state = self.lock_state()?;
8379            let record = state
8380                .jobs
8381                .get_mut(job_id)
8382                .ok_or_else(|| job_not_found(job_id))?;
8383            mark_publication_not_attempted(
8384                record,
8385                self.clock.now_ms(),
8386                "snapshot_dispatch_identity_revalidation_failed",
8387                "GitHub identity or public snapshot source changed before dispatch publication",
8388                true,
8389            )?;
8390            record.publication_takeover_lease_owned = false;
8391            record.snapshot_source_lease_owned = false;
8392            self.checkpoint_record(record)?;
8393            drop(state);
8394            let _ = self.release_record_publication_lease(&mut working);
8395            let _ = self.release_snapshot_source_lease(&mut working);
8396            return Err(error);
8397        }
8398
8399        let publish_result = self
8400            .lock_publisher()?
8401            .publish_prepared(&publish_request, prepared.as_ref());
8402        let (published, publication_uncertain) = match publish_result {
8403            Ok(published) => (published, false),
8404            Err(failure) => {
8405                if let Some(publication) = failure.possible_publication().cloned() {
8406                    (publication, true)
8407                } else {
8408                    let error = publisher_failure(failure.error());
8409                    let mut state = self.lock_state()?;
8410                    let record = state
8411                        .jobs
8412                        .get_mut(job_id)
8413                        .ok_or_else(|| job_not_found(job_id))?;
8414                    mark_publication_not_attempted(
8415                        record,
8416                        self.clock.now_ms(),
8417                        "snapshot_dispatch_publication_not_attempted",
8418                        "Snapshot dispatch publication failed before any remote mutation",
8419                        false,
8420                    )?;
8421                    record.publication_takeover_lease_owned = false;
8422                    record.snapshot_source_lease_owned = false;
8423                    self.checkpoint_record(record)?;
8424                    drop(state);
8425                    let _ = self.release_record_publication_lease(&mut working);
8426                    let _ = self.release_snapshot_source_lease(&mut working);
8427                    return Err(error);
8428                }
8429            }
8430        };
8431        let publisher_binding_mismatch = published.temporary_ref() != &temporary_ref
8432            || prepared
8433                .as_ref()
8434                .is_some_and(|expected| expected.commit() != published.commit());
8435        let release_publication = !publication_uncertain || publisher_binding_mismatch;
8436        {
8437            let mut state = self.lock_state()?;
8438            let record = state
8439                .jobs
8440                .get_mut(job_id)
8441                .ok_or_else(|| job_not_found(job_id))?;
8442            if record.prepared_dispatch_commit != working.prepared_dispatch_commit
8443                || record.publication_started_at_ms != working.publication_started_at_ms
8444                || record.dispatch_commit.is_some()
8445                || record.git_snapshot.as_ref() != Some(&snapshot)
8446            {
8447                return Err(resume_failure("snapshot_dispatch_identity_changed"));
8448            }
8449            record.dispatch_commit = (!publisher_binding_mismatch && !publication_uncertain)
8450                .then(|| published.commit().clone());
8451            record.publication_uncertain = publication_uncertain || publisher_binding_mismatch;
8452            record.cancellation_requested |= cancellation.is_cancelled();
8453            if record.cancellation_requested && record.state != JobState::Cancelling {
8454                transition_record(record, JobState::Cancelling)?;
8455            }
8456            if publisher_binding_mismatch {
8457                transition_record(record, JobState::Failed)?;
8458                append_event(
8459                    record,
8460                    self.clock.now_ms(),
8461                    "cleanup",
8462                    RemoteBuildEventKind::Warning {
8463                        code: "github.cleanup_required".to_owned(),
8464                        message: "Snapshot dispatch publisher violated its prepared ref/commit binding; automatic adoption was refused".to_owned(),
8465                        help: Some("Inspect the alternate publisher and exact remote ref".to_owned()),
8466                    },
8467                )?;
8468                append_failed(
8469                    record,
8470                    self.clock.now_ms(),
8471                    "snapshot_dispatch_binding_mismatch",
8472                    "Snapshot dispatch publisher returned a different ref or commit after possible mutation",
8473                    false,
8474                )?;
8475            } else if publication_uncertain {
8476                append_event(
8477                    record,
8478                    self.clock.now_ms(),
8479                    "submit",
8480                    RemoteBuildEventKind::Warning {
8481                        code: "github.snapshot_dispatch_publication_uncertain".to_owned(),
8482                        message: "Snapshot dispatch ref push may have succeeded; cleanup ownership was retained".to_owned(),
8483                        help: Some("Continue polling or reconcile this exact job".to_owned()),
8484                    },
8485                )?;
8486            }
8487            record.snapshot_source_lease_owned = false;
8488            if release_publication {
8489                record.publication_takeover_lease_owned = false;
8490            }
8491            self.checkpoint_record(record)?;
8492        }
8493        self.release_snapshot_source_lease(&mut working)?;
8494        if release_publication {
8495            self.release_record_publication_lease(&mut working)?;
8496        }
8497        Ok(())
8498    }
8499
8500    #[allow(
8501        clippy::too_many_lines,
8502        reason = "source-ref revalidation and dispatch publication checkpoints form one ordered mutation boundary"
8503    )]
8504    fn publish_git_snapshot_dispatch(
8505        &self,
8506        record: &mut JobRecord,
8507        cancellation: &CancellationToken,
8508    ) -> RemoteBuildResult<()>
8509    where
8510        R: GhRunner,
8511    {
8512        if record.publication_intent {
8513            return Ok(());
8514        }
8515        cancellation.check()?;
8516        let snapshot = record
8517            .git_snapshot
8518            .clone()
8519            .filter(|snapshot| snapshot.phase == GithubGitSnapshotPhaseV1::SourceExact)
8520            .ok_or_else(|| resume_failure("resume_git_snapshot_source_not_exact"))?;
8521        self.revalidate_public_snapshot_source(record)?;
8522        let workflow = generate_workflow(&self.config.workflow);
8523        let manifest = GithubDispatchManifest::new(
8524            &self.config,
8525            &record.temporary_ref,
8526            record.request.clone(),
8527        );
8528        manifest.validate_for(&self.config)?;
8529        let manifest_bytes = manifest.encode()?;
8530        let source_revision = record.source_revision.clone();
8531        let snapshot_source_ref = snapshot.stage.source_ref.clone();
8532        let temporary_ref = record.temporary_ref.clone();
8533        let operation_id = record.operation_id.clone();
8534        let created_at_ms = record.created_at_ms;
8535        let publish_request = TemporaryRefPublishRequest {
8536            repository: &self.config.repository,
8537            source_repository: &self.config.source_repository,
8538            trusted_source_ref: self.config.workflow.trusted_source_ref(),
8539            source_revision: &source_revision,
8540            snapshot_source_ref: Some(&snapshot_source_ref),
8541            temporary_ref: &temporary_ref,
8542            workflow_path: workflow.path(),
8543            workflow_bytes: workflow.yaml().as_bytes(),
8544            workflow_fingerprint: &self.config.workflow_fingerprint,
8545            manifest_bytes: &manifest_bytes,
8546            operation_id: &operation_id,
8547            created_at_ms,
8548            authorized: self.config.mutation_authorization.publish_temporary_ref,
8549        };
8550        let prepared = self
8551            .lock_publisher()?
8552            .prepare(&publish_request)
8553            .map_err(publisher_failure)?;
8554        if prepared
8555            .as_ref()
8556            .is_some_and(|publication| publication.temporary_ref() != &record.temporary_ref)
8557        {
8558            return Err(publisher_failure(
8559                TemporaryRefPublishError::PublicationVerificationFailed,
8560            ));
8561        }
8562        let (publication_process_fenced, publication_lease_scope_sha256) =
8563            self.acquire_new_snapshot_dispatch_lease(record)?;
8564        record.prepared_dispatch_commit = prepared
8565            .as_ref()
8566            .map(|publication| publication.commit().clone());
8567        record.publication_started_at_ms = 0;
8568        record.publication_quiescence_deadline_ms = u64::MAX;
8569        record.publication_intent = true;
8570        record.publication_process_fenced = publication_process_fenced;
8571        record.publication_lease_scope_sha256 = Some(publication_lease_scope_sha256);
8572        self.checkpoint_record(record)?;
8573        if let Err(error) = self.revalidate_public_snapshot_source(record) {
8574            mark_publication_not_attempted(
8575                record,
8576                self.clock.now_ms(),
8577                "snapshot_dispatch_identity_revalidation_failed",
8578                "GitHub identity or public snapshot source changed before dispatch publication",
8579                true,
8580            )?;
8581            self.checkpoint_record(record)?;
8582            self.release_record_publication_lease(record)?;
8583            self.release_snapshot_source_lease(record)?;
8584            return Err(error);
8585        }
8586        record.publication_started_at_ms = self.clock.now_ms();
8587        record.publication_quiescence_deadline_ms =
8588            publication_quiescence_deadline(record.publication_started_at_ms, 0);
8589        self.checkpoint_record(record)?;
8590        cancellation.check()?;
8591        let publish_result = self
8592            .lock_publisher()?
8593            .publish_prepared(&publish_request, prepared.as_ref());
8594        let (published, publication_uncertain) = match publish_result {
8595            Ok(published) => (published, false),
8596            Err(failure) => {
8597                if let Some(publication) = failure.possible_publication().cloned() {
8598                    (publication, true)
8599                } else {
8600                    let error = publisher_failure(failure.error());
8601                    mark_publication_not_attempted(
8602                        record,
8603                        self.clock.now_ms(),
8604                        "snapshot_dispatch_publication_not_attempted",
8605                        "Snapshot dispatch publication failed before any remote mutation",
8606                        false,
8607                    )?;
8608                    self.checkpoint_record(record)?;
8609                    self.release_record_publication_lease(record)?;
8610                    self.release_snapshot_source_lease(record)?;
8611                    return Err(error);
8612                }
8613            }
8614        };
8615        let publisher_binding_mismatch = published.temporary_ref() != &record.temporary_ref
8616            || prepared
8617                .as_ref()
8618                .is_some_and(|expected| expected.commit() != published.commit());
8619        record.dispatch_commit = (!publisher_binding_mismatch && !publication_uncertain)
8620            .then(|| published.commit().clone());
8621        record.publication_uncertain = publication_uncertain || publisher_binding_mismatch;
8622        self.checkpoint_record(record)?;
8623        if publisher_binding_mismatch {
8624            transition_record(record, JobState::Failed)?;
8625            append_event(
8626                record,
8627                self.clock.now_ms(),
8628                "cleanup",
8629                RemoteBuildEventKind::Warning {
8630                    code: "github.cleanup_required".to_owned(),
8631                    message: "Snapshot dispatch publisher violated its prepared ref/commit binding; automatic adoption was refused".to_owned(),
8632                    help: Some("Inspect the alternate publisher and exact remote ref".to_owned()),
8633                },
8634            )?;
8635            append_failed(
8636                record,
8637                self.clock.now_ms(),
8638                "snapshot_dispatch_binding_mismatch",
8639                "Snapshot dispatch publisher returned a different ref or commit after possible mutation",
8640                false,
8641            )?;
8642            self.checkpoint_record(record)?;
8643            self.release_record_publication_lease(record)?;
8644            self.release_snapshot_source_lease(record)?;
8645            return Ok(());
8646        }
8647        if publication_uncertain {
8648            append_event(
8649                record,
8650                self.clock.now_ms(),
8651                "submit",
8652                RemoteBuildEventKind::Warning {
8653                    code: "github.snapshot_dispatch_publication_uncertain".to_owned(),
8654                    message: "Snapshot dispatch ref push may have succeeded; cleanup ownership was retained".to_owned(),
8655                    help: Some("Continue polling or reconcile this exact job".to_owned()),
8656                },
8657            )?;
8658        } else if self.config.workflow.run_trigger() == WorkflowRunTrigger::Push {
8659            if record.state == JobState::Created {
8660                transition_record(record, JobState::Queued)?;
8661            }
8662            append_event(
8663                record,
8664                self.clock.now_ms(),
8665                "queue",
8666                RemoteBuildEventKind::JobQueued { position: None },
8667            )?;
8668        }
8669        record.cancellation_requested |= cancellation.is_cancelled();
8670        if record.cancellation_requested && record.state != JobState::Cancelling {
8671            transition_record(record, JobState::Cancelling)?;
8672        }
8673        self.checkpoint_record(record)?;
8674        self.release_snapshot_source_lease(record)?;
8675        if !record.publication_uncertain {
8676            self.release_record_publication_lease(record)?;
8677        }
8678        Ok(())
8679    }
8680
8681    fn revalidate_record_identity(&self, record: &JobRecord) -> RemoteBuildResult<()>
8682    where
8683        R: GhRunner,
8684    {
8685        if record.principal.is_none() || record.execution_repository_id.is_none() {
8686            return Ok(());
8687        }
8688        let identity = self.current_resume_identity()?;
8689        Self::validate_record_identity(record, &identity)
8690    }
8691
8692    fn validate_record_identity(
8693        record: &JobRecord,
8694        identity: &(GithubPrincipalIdentityV1, u64),
8695    ) -> RemoteBuildResult<()> {
8696        let (Some(expected_principal), Some(expected_repository_id)) =
8697            (record.principal.as_ref(), record.execution_repository_id)
8698        else {
8699            return Ok(());
8700        };
8701        if &identity.0 != expected_principal || identity.1 != expected_repository_id {
8702            return Err(resume_failure("resume_live_identity_changed"));
8703        }
8704        Ok(())
8705    }
8706
8707    fn checkpoint_record(&self, record: &JobRecord) -> RemoteBuildResult<()> {
8708        let Some(sink) = &self.checkpoint_sink else {
8709            return Ok(());
8710        };
8711        let principal = record
8712            .principal
8713            .clone()
8714            .ok_or_else(|| resume_failure("checkpoint_principal_unavailable"))?;
8715        let repository_id = record
8716            .execution_repository_id
8717            .ok_or_else(|| resume_failure("checkpoint_repository_identity_unavailable"))?;
8718        let resume =
8719            GithubJobResumeV1::from_record(&self.config, principal, repository_id, record)?;
8720        sink.lock()
8721            .map_err(|_| {
8722                provider_failure(
8723                    "checkpoint_sink_unavailable",
8724                    "GitHub job checkpoint sink is unavailable",
8725                    true,
8726                )
8727            })?
8728            .checkpoint(&resume)
8729    }
8730
8731    fn verify_execution_workflow_default(&self) -> RemoteBuildResult<()>
8732    where
8733        P: TemporaryRefPublisher,
8734    {
8735        let workflow_path = self.config.workflow.filename().repository_path();
8736        self.lock_publisher()?
8737            .verify_execution_workflow(&ExecutionWorkflowDoctorRequest {
8738                repository: &self.config.repository,
8739                default_branch_ref: self.config.workflow.trusted_source_ref(),
8740                workflow_path: &workflow_path,
8741                workflow_fingerprint: &self.config.workflow_fingerprint,
8742            })
8743            .map_err(publisher_failure)?;
8744        Ok(())
8745    }
8746
8747    fn preflight_workflow_dispatch_registration(&self) -> RemoteBuildResult<WorkflowRegistration>
8748    where
8749        R: GhRunner,
8750    {
8751        if self.config.workflow.run_trigger() != WorkflowRunTrigger::WorkflowDispatch {
8752            return Err(resume_failure("workflow_dispatch_trigger_mismatch"));
8753        }
8754        let mut transport = self.lock_transport()?;
8755        let repository = transport
8756            .repository(&self.config.repository)
8757            .map_err(transport_failure)?;
8758        let expected_trusted_ref = format!("refs/heads/{}", repository.default_branch().as_str());
8759        if self.config.workflow.trusted_source_ref().as_str() != expected_trusted_ref {
8760            return Err(provider_failure(
8761                "workflow_dispatch_default_branch_required",
8762                "Workflow dispatch requires its exact workflow on the execution repository default branch",
8763                false,
8764            ));
8765        }
8766        let registration = transport
8767            .workflow_registration(
8768                &self.config.repository,
8769                &self.config.workflow.filename().repository_path(),
8770            )
8771            .map_err(transport_failure)?;
8772        drop(transport);
8773        self.verify_execution_workflow_default()?;
8774        Ok(registration)
8775    }
8776
8777    fn prepare_workflow_dispatch_client(
8778        &self,
8779    ) -> RemoteBuildResult<Box<dyn GithubWorkflowDispatchHttpClient + Send>>
8780    where
8781        R: GhRunner,
8782    {
8783        let factory = {
8784            let transport = self.lock_transport()?;
8785            transport.runner().workflow_dispatch_client_factory()
8786        }
8787        .map_err(workflow_dispatch_client_failure)?;
8788        factory.create().map_err(workflow_dispatch_client_failure)
8789    }
8790
8791    fn ensure_workflow_dispatch_intent(
8792        &self,
8793        job_id: &str,
8794    ) -> RemoteBuildResult<Option<WorkflowDispatchPostAuthority>>
8795    where
8796        R: GhRunner,
8797    {
8798        if self.config.workflow.run_trigger() != WorkflowRunTrigger::WorkflowDispatch {
8799            return Err(resume_failure("workflow_dispatch_trigger_mismatch"));
8800        }
8801        if self.checkpoint_sink.is_none() {
8802            return Err(provider_failure(
8803                "workflow_dispatch_checkpoint_required",
8804                "Workflow dispatch requires a durable provider checkpoint sink",
8805                false,
8806            ));
8807        }
8808        {
8809            let state = self.lock_state()?;
8810            let record = state
8811                .jobs
8812                .get(job_id)
8813                .ok_or_else(|| job_not_found(job_id))?;
8814            if let Some(dispatch) = record.workflow_dispatch.as_ref() {
8815                dispatch.request_for_record(&self.config, record)?;
8816                return Ok(None);
8817            }
8818            if !record.publication_intent
8819                || record.publication_uncertain
8820                || record.dispatch_commit.is_none()
8821                || record.state.is_build_terminal()
8822                || record.state.is_terminal()
8823            {
8824                return Err(provider_failure(
8825                    "workflow_dispatch_not_ready",
8826                    "Workflow dispatch lacks an exact active published ref",
8827                    true,
8828                ));
8829            }
8830        }
8831
8832        let identity = self.current_resume_identity()?;
8833        let registration = self.preflight_workflow_dispatch_registration()?;
8834        let mut state = self.lock_state()?;
8835        let record = state
8836            .jobs
8837            .get_mut(job_id)
8838            .ok_or_else(|| job_not_found(job_id))?;
8839        if let Some(dispatch) = record.workflow_dispatch.as_ref() {
8840            dispatch.request_for_record(&self.config, record)?;
8841            return Ok(None);
8842        }
8843        if !record.publication_intent
8844            || record.publication_uncertain
8845            || record.dispatch_commit.is_none()
8846            || record.state.is_build_terminal()
8847            || record.state.is_terminal()
8848        {
8849            return Err(provider_failure(
8850                "workflow_dispatch_not_ready",
8851                "Workflow dispatch lacks an exact active published ref",
8852                true,
8853            ));
8854        }
8855        Self::validate_record_identity(record, &identity)?;
8856        let request = WorkflowDispatchRequest::new(
8857            self.config.repository.clone(),
8858            registration,
8859            record.temporary_ref.clone(),
8860            record.operation_id.clone(),
8861            record.request_sha256.clone(),
8862            record.source_revision.clone(),
8863            record
8864                .dispatch_commit
8865                .clone()
8866                .expect("checked dispatch commit"),
8867        )
8868        .map_err(|_| resume_failure("workflow_dispatch_request_invalid"))?;
8869        record.workflow_dispatch = Some(GithubWorkflowDispatchResumeV1::from_request(&request));
8870        self.checkpoint_record(record)?;
8871        Ok(Some(WorkflowDispatchPostAuthority {
8872            job_id: job_id.to_owned(),
8873            request,
8874        }))
8875    }
8876
8877    fn dispatch_workflow_once(
8878        &self,
8879        authority: WorkflowDispatchPostAuthority,
8880        client: &mut (dyn GithubWorkflowDispatchHttpClient + Send),
8881    ) -> RemoteBuildResult<()>
8882    where
8883        R: GhRunner,
8884    {
8885        let WorkflowDispatchPostAuthority { job_id, request } = authority;
8886        {
8887            let state = self.lock_state()?;
8888            let record = state
8889                .jobs
8890                .get(&job_id)
8891                .ok_or_else(|| job_not_found(&job_id))?;
8892            let dispatch = record
8893                .workflow_dispatch
8894                .as_ref()
8895                .ok_or_else(|| resume_failure("workflow_dispatch_intent_lost"))?;
8896            let current = dispatch.request_for_record(&self.config, record)?;
8897            if current != request || dispatch.receipt.is_some() || record.run.is_some() {
8898                return Err(provider_failure(
8899                    "workflow_dispatch_post_forbidden",
8900                    "Workflow dispatch no longer permits a first POST",
8901                    false,
8902                ));
8903            }
8904        }
8905        let attempt = {
8906            let transport = self.lock_transport()?;
8907            transport.workflow_dispatch_attempt(&request)
8908        };
8909        let receipt = attempt.send(client).map_err(transport_failure)?;
8910        let mut state = self.lock_state()?;
8911        let record = state
8912            .jobs
8913            .get_mut(&job_id)
8914            .ok_or_else(|| job_not_found(&job_id))?;
8915        let dispatch = record
8916            .workflow_dispatch
8917            .as_ref()
8918            .ok_or_else(|| resume_failure("workflow_dispatch_intent_lost"))?;
8919        if dispatch.request_for_record(&self.config, record)? != request {
8920            return Err(resume_failure("workflow_dispatch_intent_changed"));
8921        }
8922        if let Some(existing) = dispatch.restored_receipt(&self.config, record)? {
8923            if existing != receipt {
8924                return Err(resume_failure("workflow_dispatch_receipt_changed"));
8925            }
8926        } else {
8927            record
8928                .workflow_dispatch
8929                .as_mut()
8930                .expect("checked workflow dispatch")
8931                .bind_receipt(&receipt)?;
8932        }
8933        self.checkpoint_record(record)
8934    }
8935
8936    fn workflow_dispatch_receipt_for_reconciliation(
8937        &self,
8938        job_id: &str,
8939        request: &WorkflowDispatchRequest,
8940        existing_receipt: Option<WorkflowDispatchReceipt>,
8941    ) -> RemoteBuildResult<WorkflowDispatchReceipt>
8942    where
8943        R: GhRunner,
8944    {
8945        if let Some(receipt) = existing_receipt {
8946            return Ok(receipt);
8947        }
8948        let handle = self
8949            .lock_transport()?
8950            .find_workflow_dispatch_run(request)
8951            .map_err(workflow_dispatch_reconciliation_failure)?;
8952        if handle.workflow_id() != request.workflow().id() {
8953            return Err(resume_failure("workflow_dispatch_workflow_id_mismatch"));
8954        }
8955        let receipt = WorkflowDispatchReceipt::restore(
8956            self.config.repository.clone(),
8957            handle.id(),
8958            handle.workflow_id(),
8959            handle.workflow_path().to_owned(),
8960            handle.head_sha().clone(),
8961            handle.branch().clone(),
8962            request.run_name().to_owned(),
8963        )
8964        .map_err(|_| resume_failure("workflow_dispatch_receipt_invalid"))?;
8965        let mut state = self.lock_state()?;
8966        let record = state
8967            .jobs
8968            .get_mut(job_id)
8969            .ok_or_else(|| job_not_found(job_id))?;
8970        let receipt_was_missing = record
8971            .workflow_dispatch
8972            .as_ref()
8973            .ok_or_else(|| resume_failure("workflow_dispatch_intent_lost"))?
8974            .receipt
8975            .is_none();
8976        if receipt_was_missing {
8977            record
8978                .workflow_dispatch
8979                .as_mut()
8980                .expect("checked workflow dispatch")
8981                .bind_receipt(&receipt)?;
8982            self.checkpoint_record(record)?;
8983        } else if record
8984            .workflow_dispatch
8985            .as_ref()
8986            .expect("checked workflow dispatch")
8987            .restored_receipt(&self.config, record)?
8988            .as_ref()
8989            != Some(&receipt)
8990        {
8991            return Err(resume_failure("workflow_dispatch_receipt_changed"));
8992        }
8993        Ok(receipt)
8994    }
8995
8996    fn reconcile_workflow_dispatch_run(
8997        &self,
8998        job_id: &str,
8999        cancellation: &CancellationToken,
9000    ) -> RemoteBuildResult<()>
9001    where
9002        R: GhRunner,
9003    {
9004        cancellation.check()?;
9005        let (request, existing_receipt, existing_run) = {
9006            let state = self.lock_state()?;
9007            let record = state
9008                .jobs
9009                .get(job_id)
9010                .ok_or_else(|| job_not_found(job_id))?;
9011            let dispatch = record
9012                .workflow_dispatch
9013                .as_ref()
9014                .ok_or_else(|| resume_failure("workflow_dispatch_intent_missing"))?;
9015            (
9016                dispatch.request_for_record(&self.config, record)?,
9017                dispatch.restored_receipt(&self.config, record)?,
9018                record.run.clone(),
9019            )
9020        };
9021        if existing_run.is_some() {
9022            return Ok(());
9023        }
9024
9025        let receipt =
9026            self.workflow_dispatch_receipt_for_reconciliation(job_id, &request, existing_receipt)?;
9027
9028        let snapshot = self
9029            .lock_transport()?
9030            .run_by_id(&receipt)
9031            .map_err(workflow_dispatch_reconciliation_failure)?;
9032        if snapshot.run_attempt() != 1 {
9033            return Err(provider_failure(
9034                "run_initial_attempt_mismatch",
9035                "Initial GitHub run mapping observed an external rerun attempt",
9036                false,
9037            ));
9038        }
9039        let identity = self.current_resume_identity()?;
9040        let mut state = self.lock_state()?;
9041        let record = state
9042            .jobs
9043            .get_mut(job_id)
9044            .ok_or_else(|| job_not_found(job_id))?;
9045        let durable_receipt = record
9046            .workflow_dispatch
9047            .as_ref()
9048            .ok_or_else(|| resume_failure("workflow_dispatch_intent_lost"))?
9049            .restored_receipt(&self.config, record)?
9050            .ok_or_else(|| resume_failure("workflow_dispatch_receipt_lost"))?;
9051        if durable_receipt != receipt {
9052            return Err(resume_failure("workflow_dispatch_receipt_changed"));
9053        }
9054        Self::validate_record_identity(record, &identity)?;
9055        record.run = Some(snapshot.handle().clone());
9056        record.run_snapshot = Some(snapshot);
9057        if record.state == JobState::Created {
9058            transition_record(record, JobState::Queued)?;
9059            append_event(
9060                record,
9061                self.clock.now_ms(),
9062                "queue",
9063                RemoteBuildEventKind::JobQueued { position: None },
9064            )?;
9065        }
9066        append_event(
9067            record,
9068            self.clock.now_ms(),
9069            "queue",
9070            RemoteBuildEventKind::Progress {
9071                message: "Exact workflow-dispatch run mapped to this job".to_owned(),
9072                current: None,
9073                total: None,
9074            },
9075        )?;
9076        self.checkpoint_record(record)
9077    }
9078
9079    fn start_or_reconcile_workflow_dispatch(
9080        &self,
9081        job_id: &str,
9082        cancellation: &CancellationToken,
9083    ) -> RemoteBuildResult<()>
9084    where
9085        R: GhRunner,
9086    {
9087        let start = self.start_or_reconcile_workflow_dispatch_inner(job_id, cancellation);
9088        self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
9089        start
9090    }
9091
9092    fn start_or_reconcile_workflow_dispatch_inner(
9093        &self,
9094        job_id: &str,
9095        cancellation: &CancellationToken,
9096    ) -> RemoteBuildResult<()>
9097    where
9098        R: GhRunner,
9099    {
9100        let intent_exists = {
9101            let state = self.lock_state()?;
9102            state
9103                .jobs
9104                .get(job_id)
9105                .ok_or_else(|| job_not_found(job_id))?
9106                .workflow_dispatch
9107                .is_some()
9108        };
9109        if intent_exists {
9110            return self
9111                .reconcile_workflow_dispatch_with_cancellation_checkpoint(job_id, cancellation);
9112        }
9113        let mut client = self.prepare_workflow_dispatch_client()?;
9114        self.start_workflow_dispatch_with_client(job_id, &mut *client, cancellation)
9115    }
9116
9117    fn reconcile_workflow_dispatch_with_cancellation_checkpoint(
9118        &self,
9119        job_id: &str,
9120        cancellation: &CancellationToken,
9121    ) -> RemoteBuildResult<()>
9122    where
9123        R: GhRunner,
9124    {
9125        let reconciliation = self.reconcile_workflow_dispatch_run(job_id, cancellation);
9126        self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
9127        reconciliation
9128    }
9129
9130    fn checkpoint_workflow_dispatch_cancellation(
9131        &self,
9132        job_id: &str,
9133        cancellation: &CancellationToken,
9134    ) -> RemoteBuildResult<bool> {
9135        let mut state = self.lock_state()?;
9136        let record = state
9137            .jobs
9138            .get_mut(job_id)
9139            .ok_or_else(|| job_not_found(job_id))?;
9140        if (record.publication_intent || record.git_snapshot.is_none())
9141            && !record.state.is_build_terminal()
9142            && !record.state.is_terminal()
9143            && !record.cancellation_requested
9144            && cancellation.is_cancelled()
9145        {
9146            record.cancellation_requested = true;
9147            if record.state != JobState::Cancelling {
9148                transition_record(record, JobState::Cancelling)?;
9149            }
9150            self.checkpoint_record(record)?;
9151        }
9152        Ok(record.cancellation_requested)
9153    }
9154
9155    fn start_workflow_dispatch_with_client(
9156        &self,
9157        job_id: &str,
9158        client: &mut (dyn GithubWorkflowDispatchHttpClient + Send),
9159        cancellation: &CancellationToken,
9160    ) -> RemoteBuildResult<()>
9161    where
9162        R: GhRunner,
9163    {
9164        let start = self.start_workflow_dispatch_with_client_inner(job_id, client, cancellation);
9165        self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
9166        start
9167    }
9168
9169    fn start_workflow_dispatch_with_client_inner(
9170        &self,
9171        job_id: &str,
9172        client: &mut (dyn GithubWorkflowDispatchHttpClient + Send),
9173        cancellation: &CancellationToken,
9174    ) -> RemoteBuildResult<()>
9175    where
9176        R: GhRunner,
9177    {
9178        if let Some(authority) = self.ensure_workflow_dispatch_intent(job_id)? {
9179            let dispatch_result = self.dispatch_workflow_once(authority, client);
9180            let cancellation_result =
9181                self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation);
9182            cancellation_result?;
9183            dispatch_result?;
9184            let mapping_cancellation = CancellationToken::new();
9185            let mapping_result =
9186                self.reconcile_workflow_dispatch_run(job_id, &mapping_cancellation);
9187            let cancellation_requested =
9188                self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
9189            mapping_result?;
9190            if cancellation_requested {
9191                self.sync_job_with_cancel_policy(job_id, &mapping_cancellation, true)?;
9192            }
9193            return Ok(());
9194        }
9195        self.reconcile_workflow_dispatch_with_cancellation_checkpoint(job_id, cancellation)
9196    }
9197
9198    fn acquire_restored_publication_lease(&self, record: &mut JobRecord) -> RemoteBuildResult<()>
9199    where
9200        P: TemporaryRefPublisher,
9201    {
9202        if record.publication_takeover_lease_owned {
9203            return Ok(());
9204        }
9205        let expected_scope = record
9206            .publication_lease_scope_sha256
9207            .as_deref()
9208            .ok_or_else(|| {
9209                provider_failure(
9210                    "publication_lease_unavailable",
9211                    "Durable publication has no cross-process ownership lease",
9212                    true,
9213                )
9214            })?;
9215        let mut publisher = self.lock_publisher()?;
9216        if publisher.publication_lease_scope_sha256() != Some(expected_scope) {
9217            return Err(provider_failure(
9218                "publication_lease_scope_changed",
9219                "Temporary-ref publication lease scope changed",
9220                true,
9221            ));
9222        }
9223        let lease = publisher
9224            .acquire_publication_lease(&TemporaryRefPublicationLeaseRequest {
9225                repository: &self.config.repository,
9226                operation_id: &record.operation_id,
9227                temporary_ref: &record.temporary_ref,
9228            })
9229            .map_err(publisher_failure)?;
9230        match lease {
9231            TemporaryRefPublicationLease::Acquired => {
9232                record.publication_takeover_lease_owned = true;
9233                Ok(())
9234            }
9235            TemporaryRefPublicationLease::AlreadyOwned
9236            | TemporaryRefPublicationLease::HeldByOther => Err(provider_failure(
9237                "publication_owner_active",
9238                "Another live provider owns temporary-ref publication",
9239                true,
9240            )),
9241            TemporaryRefPublicationLease::Unsupported => Err(provider_failure(
9242                "publication_lease_unavailable",
9243                "Temporary-ref publisher cannot provide cross-process ownership",
9244                true,
9245            )),
9246        }
9247    }
9248
9249    fn acquire_new_snapshot_dispatch_lease(
9250        &self,
9251        record: &mut JobRecord,
9252    ) -> RemoteBuildResult<(bool, String)> {
9253        let mut publisher = self.lock_publisher()?;
9254        let scope = publisher
9255            .publication_lease_scope_sha256()
9256            .filter(|scope| is_lower_sha256(scope))
9257            .ok_or_else(|| {
9258                provider_failure(
9259                    "publication_lease_unavailable",
9260                    "Snapshot dispatch publication has no cross-process ownership scope",
9261                    false,
9262                )
9263            })?
9264            .to_owned();
9265        if !publisher.publication_attempt_is_process_fenced() {
9266            return Err(provider_failure(
9267                "publication_process_unfenced",
9268                "Snapshot dispatch publication process cannot be fenced across restart",
9269                false,
9270            ));
9271        }
9272        let request = TemporaryRefPublicationLeaseRequest {
9273            repository: &self.config.repository,
9274            operation_id: &record.operation_id,
9275            temporary_ref: &record.temporary_ref,
9276        };
9277        match publisher
9278            .acquire_publication_lease(&request)
9279            .map_err(publisher_failure)?
9280        {
9281            TemporaryRefPublicationLease::Acquired => {
9282                record.publication_takeover_lease_owned = true;
9283                Ok((true, scope))
9284            }
9285            TemporaryRefPublicationLease::AlreadyOwned
9286                if record.publication_takeover_lease_owned =>
9287            {
9288                Ok((true, scope))
9289            }
9290            TemporaryRefPublicationLease::AlreadyOwned
9291            | TemporaryRefPublicationLease::HeldByOther => Err(provider_failure(
9292                "publication_owner_active",
9293                "Another live provider owns snapshot dispatch publication",
9294                true,
9295            )),
9296            TemporaryRefPublicationLease::Unsupported => Err(provider_failure(
9297                "publication_lease_unavailable",
9298                "Snapshot dispatch publication requires a cross-process lease",
9299                false,
9300            )),
9301        }
9302    }
9303
9304    fn release_record_publication_lease(&self, record: &mut JobRecord) -> RemoteBuildResult<()>
9305    where
9306        P: TemporaryRefPublisher,
9307    {
9308        if !record.publication_takeover_lease_owned {
9309            return Ok(());
9310        }
9311        self.lock_publisher()?
9312            .release_publication_lease(&TemporaryRefPublicationLeaseRequest {
9313                repository: &self.config.repository,
9314                operation_id: &record.operation_id,
9315                temporary_ref: &record.temporary_ref,
9316            });
9317        record.publication_takeover_lease_owned = false;
9318        Ok(())
9319    }
9320
9321    fn revalidate_restored_run(&self, record: &mut JobRecord) -> RemoteBuildResult<()>
9322    where
9323        R: GhRunner,
9324    {
9325        self.revalidate_restored_run_with_policy(record, false)
9326    }
9327
9328    fn revalidate_restored_run_with_policy(
9329        &self,
9330        record: &mut JobRecord,
9331        include_terminal: bool,
9332    ) -> RemoteBuildResult<()>
9333    where
9334        R: GhRunner,
9335    {
9336        if record.state.is_terminal() && !include_terminal {
9337            return Ok(());
9338        }
9339        let Some(expected) = record.run.clone() else {
9340            return Ok(());
9341        };
9342        let snapshot = self
9343            .lock_transport()?
9344            .run(&self.config.repository, &expected)
9345            .map_err(transport_failure)?;
9346        if record
9347            .run_snapshot
9348            .as_ref()
9349            .is_some_and(|previous| github_run_is_later_attempt(previous, &snapshot))
9350            && record.signed_cleanup_evidence.is_some()
9351        {
9352            // Durable signed evidence belongs to the immutable provider-bound attempt. GitHub's
9353            // later attempt is a distinct execution and must not replace that checkpointed proof.
9354            return Ok(());
9355        }
9356        ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot)?;
9357        if (record.state == JobState::Succeeded || !record.manifests.is_empty())
9358            && !run_snapshot_is_successful(&snapshot)
9359        {
9360            return Err(resume_failure("resume_live_success_state_mismatch"));
9361        }
9362        record.run_snapshot = Some(snapshot);
9363        Ok(())
9364    }
9365
9366    fn records_from_offline_resumes(
9367        &self,
9368        resumes: Vec<GithubJobResumeV1>,
9369        expected: &GithubDurableIdentityV1,
9370    ) -> RemoteBuildResult<BTreeMap<String, JobRecord>> {
9371        if resumes.len() > self.config.max_jobs {
9372            return Err(resume_failure("resume_job_capacity_exceeded"));
9373        }
9374        if expected.provider != GITHUB_PROVIDER_ID
9375            || expected.provider_config_sha256 != provider_config_sha256(&self.config)?
9376            || !github_remote_matches(
9377                &expected.execution_repository,
9378                &repository_url(&self.config.repository),
9379            )
9380            || expected.execution_repository_id == 0
9381        {
9382            return Err(resume_failure("resume_provider_identity_mismatch"));
9383        }
9384        let mut restored = BTreeMap::new();
9385        for resume in resumes {
9386            let record = resume.into_record(
9387                &self.config,
9388                &expected.principal,
9389                expected.execution_repository_id,
9390            )?;
9391            if restored.insert(record.job_id.clone(), record).is_some() {
9392                return Err(resume_failure("resume_duplicate_job_id"));
9393            }
9394        }
9395        Ok(restored)
9396    }
9397
9398    fn insert_restored_records(
9399        &self,
9400        restored: BTreeMap<String, JobRecord>,
9401    ) -> RemoteBuildResult<()> {
9402        let mut state = self.lock_state()?;
9403        if !state.reservations.is_empty()
9404            || state.jobs.len().saturating_add(restored.len()) > self.config.max_jobs
9405            || restored
9406                .keys()
9407                .any(|job_id| state.jobs.contains_key(job_id))
9408        {
9409            return Err(resume_failure("resume_state_conflict"));
9410        }
9411        state.jobs.extend(restored);
9412        Ok(())
9413    }
9414
9415    /// Restore durable jobs using only their stored identity and local provider configuration.
9416    ///
9417    /// This method performs no transport or publisher operation. Call
9418    /// [`Self::revalidate_restored_job_live`] after any controller-owned mutation intent is durable
9419    /// and before relying on live GitHub state.
9420    ///
9421    /// # Errors
9422    ///
9423    /// Rejects any configuration, principal, repository, request, ref, run, event, or state
9424    /// mismatch, duplicate job, capacity violation, or existing provider-state conflict.
9425    pub fn restore_job_resumes_offline(
9426        &self,
9427        resumes: Vec<GithubJobResumeV1>,
9428        expected: &GithubDurableIdentityV1,
9429    ) -> RemoteBuildResult<()> {
9430        let restored = self.records_from_offline_resumes(resumes, expected)?;
9431        self.insert_restored_records(restored)
9432    }
9433
9434    /// Revalidate one offline-restored job against the current principal, repository, and run.
9435    ///
9436    /// This performs bounded read-only GitHub requests and persists any refreshed exact run
9437    /// snapshot through the configured checkpoint sink. It never publishes, cancels, or cleans.
9438    ///
9439    /// # Errors
9440    ///
9441    /// Returns a typed identity, transport, checkpoint, or restored-state failure.
9442    pub fn revalidate_restored_job_live(
9443        &self,
9444        job_id: &str,
9445        cancellation: &CancellationToken,
9446    ) -> RemoteBuildResult<()>
9447    where
9448        R: GhRunner,
9449    {
9450        if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
9451            let live_validation = (|| {
9452                cancellation.check()?;
9453                if self
9454                    .observe_workflow_dispatch_run(job_id, true, cancellation)?
9455                    .is_none()
9456                {
9457                    let identity = self.current_resume_identity()?;
9458                    cancellation.check()?;
9459                    let mut state = self.lock_state()?;
9460                    let record = state
9461                        .jobs
9462                        .get_mut(job_id)
9463                        .ok_or_else(|| job_not_found(job_id))?;
9464                    Self::validate_record_identity(record, &identity)?;
9465                    self.checkpoint_record(record)?;
9466                }
9467                Ok(())
9468            })();
9469            self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
9470            live_validation?;
9471            return cancellation.check();
9472        }
9473        cancellation.check()?;
9474        let mut state = self.lock_state()?;
9475        let record = state
9476            .jobs
9477            .get_mut(job_id)
9478            .ok_or_else(|| job_not_found(job_id))?;
9479        self.revalidate_record_identity(record)?;
9480        self.revalidate_restored_run_with_policy(record, true)?;
9481        self.checkpoint_record(record)?;
9482        cancellation.check()
9483    }
9484
9485    /// Recover an offline-only identity for an exact terminal attempt restored from durable state.
9486    ///
9487    /// This performs no transport or publisher operation. The returned type can validate an
9488    /// append-last durable log marker but cannot be passed to the network log fetcher.
9489    ///
9490    /// # Errors
9491    ///
9492    /// Returns a typed restored-state or terminal-run identity failure.
9493    pub fn restored_terminal_job_log_identity(
9494        &self,
9495        job_id: &str,
9496    ) -> RemoteBuildResult<Option<GithubDurableRunAttemptLogIdentity>> {
9497        let state = self.lock_state()?;
9498        let record = state
9499            .jobs
9500            .get(job_id)
9501            .ok_or_else(|| job_not_found(job_id))?;
9502        let Some(snapshot) = record.run_snapshot.as_ref() else {
9503            return Ok(None);
9504        };
9505        if !snapshot.status().is_terminal() || snapshot.conclusion().is_none() {
9506            return Ok(None);
9507        }
9508        GithubDurableRunAttemptLogIdentity::new(
9509            self.config.repository.clone(),
9510            snapshot.handle().id(),
9511            snapshot.run_attempt(),
9512            snapshot.handle().head_sha().clone(),
9513            snapshot.handle().workflow_id(),
9514        )
9515        .map(Some)
9516        .map_err(|_| job_log_identity_failure())
9517    }
9518
9519    /// Export all current jobs as strictly validated, secret-free durable records.
9520    ///
9521    /// The current credential and stable execution-repository ID are revalidated before records
9522    /// are emitted.
9523    ///
9524    /// # Errors
9525    ///
9526    /// Returns a typed identity, transport, state, or serialization failure.
9527    pub fn export_job_resumes(&self) -> RemoteBuildResult<Vec<GithubJobResumeV1>>
9528    where
9529        R: GhRunner,
9530    {
9531        let records = {
9532            let state = self.lock_state()?;
9533            state.jobs.values().cloned().collect::<Vec<_>>()
9534        };
9535        if records
9536            .iter()
9537            .any(|record| !record.publication_intent && record.git_snapshot.is_none())
9538        {
9539            return Err(resume_failure("resume_submission_not_checkpointable"));
9540        }
9541        if records
9542            .iter()
9543            .any(|record| record.principal.is_none() || record.execution_repository_id.is_none())
9544        {
9545            return Err(resume_failure("resume_creation_identity_unbound"));
9546        }
9547        let (principal, repository_id) = self.current_resume_identity()?;
9548        records
9549            .iter()
9550            .map(|record| {
9551                if record.principal.as_ref() != Some(&principal)
9552                    || record.execution_repository_id != Some(repository_id)
9553                {
9554                    return Err(resume_failure("resume_live_identity_changed"));
9555                }
9556                GithubJobResumeV1::from_record(
9557                    &self.config,
9558                    principal.clone(),
9559                    repository_id,
9560                    record,
9561                )
9562            })
9563            .collect()
9564    }
9565
9566    /// Restore an atomic batch of durable jobs after strict live identity revalidation.
9567    ///
9568    /// Existing jobs and reservations are never overwritten.
9569    ///
9570    /// # Errors
9571    ///
9572    /// Rejects unknown schema fields during deserialization and rejects any repository,
9573    /// principal, configuration, request, ref, run, attempt, event, or state mismatch here.
9574    pub fn restore_job_resumes(&self, resumes: Vec<GithubJobResumeV1>) -> RemoteBuildResult<()>
9575    where
9576        R: GhRunner,
9577    {
9578        let (principal, repository_id) = self.current_resume_identity()?;
9579        let expected = GithubDurableIdentityV1 {
9580            provider: GITHUB_PROVIDER_ID.to_owned(),
9581            provider_config_sha256: provider_config_sha256(&self.config)?,
9582            principal,
9583            execution_repository: repository_url(&self.config.repository),
9584            execution_repository_id: repository_id,
9585        };
9586        let mut restored = self.records_from_offline_resumes(resumes, &expected)?;
9587        for record in restored.values_mut() {
9588            self.revalidate_restored_run(record)?;
9589        }
9590        self.insert_restored_records(restored)
9591    }
9592
9593    /// Reconcile one restored pre-publication intent without issuing a duplicate push.
9594    ///
9595    /// # Errors
9596    ///
9597    /// Returns a typed provider or read-only publisher failure. Exact remote bytes must match
9598    /// before a dispatch commit is adopted.
9599    pub fn reconcile_restored_job(
9600        &self,
9601        job_id: &str,
9602        cancellation: &CancellationToken,
9603    ) -> RemoteBuildResult<GithubJobReconciliation>
9604    where
9605        R: GhRunner,
9606        P: TemporaryRefPublisher + TemporaryRefReconciler,
9607    {
9608        self.reconcile_restored_job_with_cancel_policy(job_id, cancellation, true)
9609    }
9610
9611    /// Reconcile one restored publication using reads only, never cancellation dispatch.
9612    ///
9613    /// This is the restart-safe path for a durable cancellation whose provider checkpoint already
9614    /// records cancellation intent but not dispatch acknowledgement.
9615    ///
9616    /// # Errors
9617    ///
9618    /// Returns a typed provider or read-only publisher failure. Exact remote bytes must match
9619    /// before a dispatch commit is adopted.
9620    pub fn reconcile_restored_job_get_only(
9621        &self,
9622        job_id: &str,
9623        cancellation: &CancellationToken,
9624    ) -> RemoteBuildResult<GithubJobReconciliation>
9625    where
9626        R: GhRunner,
9627        P: TemporaryRefPublisher + TemporaryRefReconciler,
9628    {
9629        self.reconcile_restored_job_with_cancel_policy(job_id, cancellation, false)
9630    }
9631
9632    #[allow(
9633        clippy::too_many_lines,
9634        reason = "the dispatch restart boundary keeps external reconciliation and exact state rebinding adjacent"
9635    )]
9636    fn reconcile_restored_workflow_dispatch_publication(
9637        &self,
9638        job_id: &str,
9639        cancellation: &CancellationToken,
9640        allow_cancel_dispatch: bool,
9641    ) -> RemoteBuildResult<GithubJobReconciliation>
9642    where
9643        R: GhRunner,
9644        P: TemporaryRefPublisher + TemporaryRefReconciler,
9645    {
9646        cancellation.check()?;
9647        let mut working = {
9648            let state = self.lock_state()?;
9649            state
9650                .jobs
9651                .get(job_id)
9652                .cloned()
9653                .ok_or_else(|| job_not_found(job_id))?
9654        };
9655        if working.dispatch_commit.is_some() && !working.publication_uncertain {
9656            {
9657                let mut state = self.lock_state()?;
9658                let record = state
9659                    .jobs
9660                    .get_mut(job_id)
9661                    .ok_or_else(|| job_not_found(job_id))?;
9662                self.checkpoint_record(record)?;
9663            }
9664            if working.workflow_dispatch.is_some() {
9665                self.reconcile_workflow_dispatch_with_cancellation_checkpoint(
9666                    job_id,
9667                    cancellation,
9668                )?;
9669            } else if allow_cancel_dispatch {
9670                self.start_or_reconcile_workflow_dispatch(job_id, cancellation)?;
9671            }
9672            self.sync_job_with_cancel_policy(job_id, cancellation, allow_cancel_dispatch)?;
9673            return Ok(GithubJobReconciliation::AlreadyMapped);
9674        }
9675        if working.publication_absent {
9676            let mut state = self.lock_state()?;
9677            let record = state
9678                .jobs
9679                .get_mut(job_id)
9680                .ok_or_else(|| job_not_found(job_id))?;
9681            self.checkpoint_record(record)?;
9682            return Ok(GithubJobReconciliation::Missing);
9683        }
9684        if working.publication_not_attempted || !working.publication_intent {
9685            let mut state = self.lock_state()?;
9686            let record = state
9687                .jobs
9688                .get_mut(job_id)
9689                .ok_or_else(|| job_not_found(job_id))?;
9690            self.checkpoint_record(record)?;
9691            return Ok(GithubJobReconciliation::NotStarted);
9692        }
9693        self.acquire_restored_publication_lease(&mut working)?;
9694        if publication_record_boundary_is_unarmed(&working) {
9695            let apply_result = (|| {
9696                let mut state = self.lock_state()?;
9697                let record = state
9698                    .jobs
9699                    .get_mut(job_id)
9700                    .ok_or_else(|| job_not_found(job_id))?;
9701                if record.operation_id != working.operation_id
9702                    || record.request_sha256 != working.request_sha256
9703                    || record.temporary_ref != working.temporary_ref
9704                    || record.prepared_dispatch_commit != working.prepared_dispatch_commit
9705                    || !publication_record_boundary_is_unarmed(record)
9706                {
9707                    return Err(resume_failure(
9708                        "workflow_dispatch_publication_identity_changed",
9709                    ));
9710                }
9711                mark_publication_not_attempted(
9712                    record,
9713                    self.clock.now_ms(),
9714                    "publication_not_armed",
9715                    "Prepared publication never reached its durable push boundary",
9716                    false,
9717                )?;
9718                self.checkpoint_record(record)
9719            })();
9720            let release_source = self.release_snapshot_source_lease(&mut working);
9721            let release_publication = self.release_record_publication_lease(&mut working);
9722            apply_result?;
9723            release_source?;
9724            release_publication?;
9725            return Ok(GithubJobReconciliation::NotStarted);
9726        }
9727        let external_result = (|| {
9728            if let Some(snapshot) = working.git_snapshot.clone() {
9729                if snapshot.phase != GithubGitSnapshotPhaseV1::SourceExact {
9730                    return Err(resume_failure("resume_git_snapshot_source_not_exact"));
9731                }
9732                self.acquire_snapshot_source_lease(&mut working)?;
9733                self.revalidate_public_snapshot_source(&working)?;
9734                let source = self
9735                    .lock_publisher()?
9736                    .reconcile_git_snapshot_source(&GitSnapshotSourcePublishRequest {
9737                        source_repository: &self.config.source_repository,
9738                        source_ref: &snapshot.stage.source_ref,
9739                        commit: &snapshot.stage.graph.commit,
9740                        authorized: true,
9741                    })
9742                    .map_err(publisher_failure)?;
9743                if source != GitSnapshotRefReconciliation::Exact {
9744                    return Err(provider_failure(
9745                        "snapshot_source_ownership_lost",
9746                        "Git snapshot source ref changed before dispatch reconciliation",
9747                        false,
9748                    ));
9749                }
9750            } else {
9751                self.revalidate_record_identity(&working)?;
9752            }
9753            let workflow = generate_workflow(&self.config.workflow);
9754            let manifest = GithubDispatchManifest::new(
9755                &self.config,
9756                &working.temporary_ref,
9757                working.request.clone(),
9758            );
9759            manifest.validate_for(&self.config)?;
9760            let manifest_bytes = manifest.encode()?;
9761            let expected_prepared_commit =
9762                working.prepared_dispatch_commit.as_ref().ok_or_else(|| {
9763                    provider_failure(
9764                        "publication_reconciliation_unavailable",
9765                        "Durable publication does not contain an exact prepared commit",
9766                        true,
9767                    )
9768                })?;
9769            self.lock_publisher()?
9770                .reconcile_temporary_ref(&TemporaryRefReconcileRequest {
9771                    repository: &self.config.repository,
9772                    source_repository: &self.config.source_repository,
9773                    trusted_source_ref: self.config.workflow.trusted_source_ref(),
9774                    source_revision: &working.source_revision,
9775                    snapshot_source_ref: working
9776                        .git_snapshot
9777                        .as_ref()
9778                        .map(|snapshot| &snapshot.stage.source_ref),
9779                    temporary_ref: &working.temporary_ref,
9780                    workflow_path: workflow.path(),
9781                    workflow_bytes: workflow.yaml().as_bytes(),
9782                    workflow_fingerprint: &self.config.workflow_fingerprint,
9783                    manifest_bytes: &manifest_bytes,
9784                    operation_id: &working.operation_id,
9785                    created_at_ms: working.created_at_ms,
9786                    expected_prepared_commit,
9787                })
9788                .map_err(publisher_failure)
9789        })();
9790        let reconciliation = match external_result {
9791            Ok(reconciliation) => reconciliation,
9792            Err(error) => {
9793                let _ = self.release_snapshot_source_lease(&mut working);
9794                let _ = self.release_record_publication_lease(&mut working);
9795                return Err(error);
9796            }
9797        };
9798        let expected_prepared_commit = working
9799            .prepared_dispatch_commit
9800            .clone()
9801            .ok_or_else(|| resume_failure("resume_prepared_dispatch_commit_missing"))?;
9802        let recovered = matches!(
9803            &reconciliation,
9804            TemporaryRefReconciliation::Exact(published)
9805                if published.temporary_ref() == &working.temporary_ref
9806                    && published.commit() == &expected_prepared_commit
9807        );
9808        let outcome = if recovered {
9809            GithubJobReconciliation::Recovered
9810        } else if matches!(reconciliation, TemporaryRefReconciliation::Missing) {
9811            GithubJobReconciliation::Missing
9812        } else {
9813            GithubJobReconciliation::Conflict
9814        };
9815        let apply_result = (|| {
9816            let mut state = self.lock_state()?;
9817            let record = state
9818                .jobs
9819                .get_mut(job_id)
9820                .ok_or_else(|| job_not_found(job_id))?;
9821            if record.principal != working.principal
9822                || record.execution_repository_id != working.execution_repository_id
9823                || record.operation_id != working.operation_id
9824                || record.source_revision != working.source_revision
9825                || record.git_snapshot != working.git_snapshot
9826                || record.request_sha256 != working.request_sha256
9827                || record.temporary_ref != working.temporary_ref
9828                || record.prepared_dispatch_commit != working.prepared_dispatch_commit
9829                || record.dispatch_commit != working.dispatch_commit
9830                || record.publication_intent != working.publication_intent
9831                || record.publication_uncertain != working.publication_uncertain
9832                || record.workflow_dispatch != working.workflow_dispatch
9833            {
9834                return Err(resume_failure(
9835                    "workflow_dispatch_publication_identity_changed",
9836                ));
9837            }
9838            if recovered {
9839                record.dispatch_commit = Some(expected_prepared_commit);
9840                record.publication_uncertain = false;
9841                record.publication_absent = false;
9842            } else {
9843                record.publication_uncertain = true;
9844                record.publication_absent = false;
9845                record.publication_absence_observations = record
9846                    .publication_absence_observations
9847                    .saturating_add(1)
9848                    .min(2);
9849                if record.publication_absence_first_observed_at_ms == 0 {
9850                    record.publication_absence_first_observed_at_ms = self.clock.now_ms();
9851                    record.publication_quiescence_deadline_ms = publication_quiescence_deadline(
9852                        record.publication_started_at_ms,
9853                        record.publication_absence_first_observed_at_ms,
9854                    );
9855                }
9856            }
9857            self.checkpoint_record(record)
9858        })();
9859        let release_source = self.release_snapshot_source_lease(&mut working);
9860        let release_publication = self.release_record_publication_lease(&mut working);
9861        apply_result?;
9862        release_source?;
9863        release_publication?;
9864        if recovered {
9865            let has_dispatch_intent = {
9866                let state = self.lock_state()?;
9867                state
9868                    .jobs
9869                    .get(job_id)
9870                    .ok_or_else(|| job_not_found(job_id))?
9871                    .workflow_dispatch
9872                    .is_some()
9873            };
9874            if has_dispatch_intent {
9875                self.reconcile_workflow_dispatch_with_cancellation_checkpoint(
9876                    job_id,
9877                    cancellation,
9878                )?;
9879            } else if allow_cancel_dispatch {
9880                self.start_or_reconcile_workflow_dispatch(job_id, cancellation)?;
9881            } else {
9882                return Err(provider_failure(
9883                    "workflow_dispatch_mutation_required",
9884                    "Exact publication was recovered, but starting its workflow requires mutation authorization",
9885                    true,
9886                ));
9887            }
9888            self.sync_job_with_cancel_policy(job_id, cancellation, allow_cancel_dispatch)?;
9889        }
9890        Ok(outcome)
9891    }
9892
9893    fn reconcile_restored_job_with_cancel_policy(
9894        &self,
9895        job_id: &str,
9896        cancellation: &CancellationToken,
9897        allow_cancel_dispatch: bool,
9898    ) -> RemoteBuildResult<GithubJobReconciliation>
9899    where
9900        R: GhRunner,
9901        P: TemporaryRefPublisher + TemporaryRefReconciler,
9902    {
9903        if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
9904            let reconciliation = self.reconcile_restored_job_with_cancel_policy_inner(
9905                job_id,
9906                cancellation,
9907                allow_cancel_dispatch,
9908            );
9909            self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
9910            return reconciliation;
9911        }
9912        self.reconcile_restored_job_with_cancel_policy_inner(
9913            job_id,
9914            cancellation,
9915            allow_cancel_dispatch,
9916        )
9917    }
9918
9919    #[allow(clippy::too_many_lines)]
9920    fn reconcile_restored_job_with_cancel_policy_inner(
9921        &self,
9922        job_id: &str,
9923        cancellation: &CancellationToken,
9924        allow_cancel_dispatch: bool,
9925    ) -> RemoteBuildResult<GithubJobReconciliation>
9926    where
9927        R: GhRunner,
9928        P: TemporaryRefPublisher + TemporaryRefReconciler,
9929    {
9930        cancellation.check()?;
9931        let snapshot_pending_dispatch = {
9932            let state = self.lock_state()?;
9933            let record = state
9934                .jobs
9935                .get(job_id)
9936                .ok_or_else(|| job_not_found(job_id))?;
9937            record.git_snapshot.is_some() && !record.publication_intent
9938        };
9939        if snapshot_pending_dispatch {
9940            self.continue_git_snapshot_submission(job_id, cancellation)?;
9941            let state = self.lock_state()?;
9942            let record = state
9943                .jobs
9944                .get(job_id)
9945                .ok_or_else(|| job_not_found(job_id))?;
9946            if !record.publication_intent {
9947                return Ok(
9948                    match record.git_snapshot.as_ref().map(|snapshot| snapshot.phase) {
9949                        Some(GithubGitSnapshotPhaseV1::SourceConflict) => {
9950                            GithubJobReconciliation::Conflict
9951                        }
9952                        Some(GithubGitSnapshotPhaseV1::SourceAbsent) => {
9953                            GithubJobReconciliation::Missing
9954                        }
9955                        _ => GithubJobReconciliation::NotStarted,
9956                    },
9957                );
9958            }
9959        }
9960        if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
9961            return self.reconcile_restored_workflow_dispatch_publication(
9962                job_id,
9963                cancellation,
9964                allow_cancel_dispatch,
9965            );
9966        }
9967        let mut state = self.lock_state()?;
9968        let record = state
9969            .jobs
9970            .get_mut(job_id)
9971            .ok_or_else(|| job_not_found(job_id))?;
9972        if record.dispatch_commit.is_some() && !record.publication_uncertain {
9973            if self.config.workflow.run_trigger() == WorkflowRunTrigger::Push {
9974                normalize_published_record(record, self.clock.now_ms())?;
9975            }
9976            let has_dispatch_intent = record.workflow_dispatch.is_some();
9977            self.checkpoint_record(record)?;
9978            self.release_record_publication_lease(record)?;
9979            self.release_snapshot_source_lease(record)?;
9980            if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch
9981                && (has_dispatch_intent || allow_cancel_dispatch)
9982            {
9983                drop(state);
9984                if has_dispatch_intent {
9985                    self.reconcile_workflow_dispatch_with_cancellation_checkpoint(
9986                        job_id,
9987                        cancellation,
9988                    )?;
9989                } else {
9990                    self.start_or_reconcile_workflow_dispatch(job_id, cancellation)?;
9991                }
9992                self.sync_job_with_cancel_policy(job_id, cancellation, allow_cancel_dispatch)?;
9993            }
9994            return Ok(GithubJobReconciliation::AlreadyMapped);
9995        }
9996        if record.publication_absent {
9997            self.checkpoint_record(record)?;
9998            self.release_record_publication_lease(record)?;
9999            self.release_snapshot_source_lease(record)?;
10000            return Ok(GithubJobReconciliation::Missing);
10001        }
10002        if record.publication_not_attempted {
10003            self.checkpoint_record(record)?;
10004            self.release_record_publication_lease(record)?;
10005            self.release_snapshot_source_lease(record)?;
10006            return Ok(GithubJobReconciliation::NotStarted);
10007        }
10008        self.acquire_restored_publication_lease(record)?;
10009        if publication_record_boundary_is_unarmed(record) {
10010            mark_publication_not_attempted(
10011                record,
10012                self.clock.now_ms(),
10013                "publication_not_armed",
10014                "Prepared publication never reached its durable push boundary",
10015                false,
10016            )?;
10017            self.checkpoint_record(record)?;
10018            self.release_record_publication_lease(record)?;
10019            self.release_snapshot_source_lease(record)?;
10020            return Ok(GithubJobReconciliation::NotStarted);
10021        }
10022        if !record.publication_intent {
10023            self.release_snapshot_source_lease(record)?;
10024            return Ok(GithubJobReconciliation::NotStarted);
10025        }
10026        let snapshot = record
10027            .git_snapshot
10028            .clone()
10029            .filter(|snapshot| snapshot.phase == GithubGitSnapshotPhaseV1::SourceExact);
10030        if let Some(snapshot) = snapshot {
10031            self.acquire_snapshot_source_lease(record)?;
10032            self.revalidate_public_snapshot_source(record)?;
10033            let source = self
10034                .lock_publisher()?
10035                .reconcile_git_snapshot_source(&GitSnapshotSourcePublishRequest {
10036                    source_repository: &self.config.source_repository,
10037                    source_ref: &snapshot.stage.source_ref,
10038                    commit: &snapshot.stage.graph.commit,
10039                    authorized: true,
10040                })
10041                .map_err(publisher_failure)?;
10042            if source != GitSnapshotRefReconciliation::Exact {
10043                self.release_record_publication_lease(record)?;
10044                self.release_snapshot_source_lease(record)?;
10045                return Err(provider_failure(
10046                    "snapshot_source_ownership_lost",
10047                    "Git snapshot source ref changed before dispatch reconciliation",
10048                    false,
10049                ));
10050            }
10051        } else if record.git_snapshot.is_some() {
10052            self.release_record_publication_lease(record)?;
10053            return Err(resume_failure("resume_git_snapshot_source_not_exact"));
10054        }
10055        let workflow = generate_workflow(&self.config.workflow);
10056        let manifest = GithubDispatchManifest::new(
10057            &self.config,
10058            &record.temporary_ref,
10059            record.request.clone(),
10060        );
10061        manifest.validate_for(&self.config)?;
10062        let manifest_bytes = manifest.encode()?;
10063        let expected_prepared_commit =
10064            record.prepared_dispatch_commit.as_ref().ok_or_else(|| {
10065                provider_failure(
10066                    "publication_reconciliation_unavailable",
10067                    "Durable publication does not contain an exact prepared commit",
10068                    true,
10069                )
10070            })?;
10071        self.revalidate_record_identity(record)?;
10072        let reconciliation = self
10073            .lock_publisher()?
10074            .reconcile_temporary_ref(&TemporaryRefReconcileRequest {
10075                repository: &self.config.repository,
10076                source_repository: &self.config.source_repository,
10077                trusted_source_ref: self.config.workflow.trusted_source_ref(),
10078                source_revision: &record.source_revision,
10079                snapshot_source_ref: record
10080                    .git_snapshot
10081                    .as_ref()
10082                    .map(|snapshot| &snapshot.stage.source_ref),
10083                temporary_ref: &record.temporary_ref,
10084                workflow_path: workflow.path(),
10085                workflow_bytes: workflow.yaml().as_bytes(),
10086                workflow_fingerprint: &self.config.workflow_fingerprint,
10087                manifest_bytes: &manifest_bytes,
10088                operation_id: &record.operation_id,
10089                created_at_ms: record.created_at_ms,
10090                expected_prepared_commit,
10091            })
10092            .map_err(publisher_failure)?;
10093        cancellation.check()?;
10094        self.revalidate_record_identity(record)?;
10095        match reconciliation {
10096            TemporaryRefReconciliation::Exact(published)
10097                if published.temporary_ref() == &record.temporary_ref
10098                    && record
10099                        .prepared_dispatch_commit
10100                        .as_ref()
10101                        .is_none_or(|prepared| prepared == published.commit()) =>
10102            {
10103                record.dispatch_commit = Some(published.commit().clone());
10104                record.publication_uncertain = false;
10105                record.publication_absent = false;
10106                if record.state == JobState::Created
10107                    && self.config.workflow.run_trigger() == WorkflowRunTrigger::Push
10108                {
10109                    transition_record(record, JobState::Queued)?;
10110                    append_event(
10111                        record,
10112                        self.clock.now_ms(),
10113                        "queue",
10114                        RemoteBuildEventKind::JobQueued { position: None },
10115                    )?;
10116                }
10117                self.checkpoint_record(record)?;
10118                self.release_record_publication_lease(record)?;
10119                self.release_snapshot_source_lease(record)?;
10120                drop(state);
10121                self.sync_job_with_cancel_policy(job_id, cancellation, allow_cancel_dispatch)?;
10122                Ok(GithubJobReconciliation::Recovered)
10123            }
10124            observation => {
10125                let ref_is_missing = matches!(observation, TemporaryRefReconciliation::Missing);
10126                let outcome = if ref_is_missing {
10127                    GithubJobReconciliation::Missing
10128                } else {
10129                    GithubJobReconciliation::Conflict
10130                };
10131                record.publication_uncertain = true;
10132                record.publication_absent = false;
10133                let prepared_commit = record.prepared_dispatch_commit.clone().ok_or_else(|| {
10134                    provider_failure(
10135                        "publication_reconciliation_unavailable",
10136                        "Durable publication does not contain an exact prepared commit",
10137                        true,
10138                    )
10139                })?;
10140                if self.config.workflow.run_trigger() == WorkflowRunTrigger::Push {
10141                    let run = self.lock_transport()?.find_run(
10142                        &self.config.repository,
10143                        &self.config.workflow.filename().repository_path(),
10144                        &prepared_commit,
10145                        record.temporary_ref.branch(),
10146                        RunEvent::Push,
10147                    );
10148                    self.revalidate_record_identity(record)?;
10149                    match run {
10150                        Ok(handle) => {
10151                            let snapshot = self
10152                                .lock_transport()?
10153                                .run(&self.config.repository, &handle)
10154                                .map_err(transport_failure)?;
10155                            self.revalidate_record_identity(record)?;
10156                            if snapshot.run_attempt() != 1 {
10157                                return Err(resume_failure("resume_recovered_run_attempt_invalid"));
10158                            }
10159                            record.dispatch_commit = Some(prepared_commit);
10160                            record.run = Some(handle);
10161                            record.run_snapshot = Some(snapshot);
10162                            record.publication_uncertain = false;
10163                            if record.state == JobState::Created {
10164                                transition_record(record, JobState::Queued)?;
10165                                append_event(
10166                                    record,
10167                                    self.clock.now_ms(),
10168                                    "queue",
10169                                    RemoteBuildEventKind::JobQueued { position: None },
10170                                )?;
10171                            }
10172                            append_event(
10173                                record,
10174                                self.clock.now_ms(),
10175                                "queue",
10176                                RemoteBuildEventKind::Progress {
10177                                    message: "Exact GitHub Actions run recovered from publication uncertainty"
10178                                        .to_owned(),
10179                                    current: None,
10180                                    total: None,
10181                                },
10182                            )?;
10183                            self.checkpoint_record(record)?;
10184                            self.release_record_publication_lease(record)?;
10185                            self.release_snapshot_source_lease(record)?;
10186                            drop(state);
10187                            self.sync_job_with_cancel_policy(
10188                                job_id,
10189                                cancellation,
10190                                allow_cancel_dispatch,
10191                            )?;
10192                            return Ok(GithubJobReconciliation::Recovered);
10193                        }
10194                        Err(TransportError::RunNotFound) => {}
10195                        Err(error) => return Err(transport_failure(error)),
10196                    }
10197                }
10198
10199                if record.publication_absence_first_observed_at_ms == 0 {
10200                    record.publication_absence_first_observed_at_ms = self.clock.now_ms();
10201                    record.publication_quiescence_deadline_ms = publication_quiescence_deadline(
10202                        record.publication_started_at_ms,
10203                        record.publication_absence_first_observed_at_ms,
10204                    );
10205                }
10206                record.publication_absence_observations = record
10207                    .publication_absence_observations
10208                    .saturating_add(1)
10209                    .min(2);
10210                let strongly_resolved = record.publication_absence_observations >= 2
10211                    && self.clock.now_ms() >= record.publication_quiescence_deadline_ms
10212                    && record.publication_process_fenced;
10213                if strongly_resolved {
10214                    if ref_is_missing && record.dispatch_commit.is_none() {
10215                        record.publication_uncertain = false;
10216                        record.publication_absent = true;
10217                        record.temporary_ref_deleted = true;
10218                        if record.cancellation_requested {
10219                            transition_record(record, JobState::Cancelled)?;
10220                            append_event(
10221                                record,
10222                                self.clock.now_ms(),
10223                                "finished",
10224                                RemoteBuildEventKind::OperationCancelled {
10225                                    reason: "publication_not_observed".to_owned(),
10226                                    duration_ms: self
10227                                        .clock
10228                                        .now_ms()
10229                                        .saturating_sub(record.created_at_ms),
10230                                },
10231                            )?;
10232                        } else {
10233                            transition_record(record, JobState::Failed)?;
10234                            append_failed(
10235                                record,
10236                                self.clock.now_ms(),
10237                                "publication_not_observed",
10238                                "Prepared publication remained absent from both the ref and bounded run history",
10239                                false,
10240                            )?;
10241                        }
10242                    } else if !record.state.is_build_terminal() && !record.state.is_terminal() {
10243                        transition_record(record, JobState::Failed)?;
10244                        append_failed(
10245                            record,
10246                            self.clock.now_ms(),
10247                            if ref_is_missing {
10248                                "publication_not_observed_after_uncertain_mapping"
10249                            } else {
10250                                "temporary_ref_reconciliation_conflict"
10251                            },
10252                            if ref_is_missing {
10253                                "Possible publication remained absent from bounded ref and run history"
10254                            } else {
10255                                "Temporary Git ref differed and no exact prepared-commit run appeared during bounded reconciliation"
10256                            },
10257                            false,
10258                        )?;
10259                    }
10260                }
10261                self.checkpoint_record(record)?;
10262                if strongly_resolved {
10263                    self.release_record_publication_lease(record)?;
10264                    self.release_snapshot_source_lease(record)?;
10265                }
10266                Ok(if strongly_resolved && !ref_is_missing {
10267                    GithubJobReconciliation::ConflictResolved
10268                } else {
10269                    outcome
10270                })
10271            }
10272        }
10273    }
10274
10275    fn artifact_context(
10276        record: &JobRecord,
10277        config: &GithubProviderConfig,
10278    ) -> Option<GithubArtifactContext> {
10279        Some(GithubArtifactContext {
10280            job_id: record.job_id.clone(),
10281            operation_id: record.operation_id.clone(),
10282            repository: config.repository.clone(),
10283            execution_repository_id: record.execution_repository_id,
10284            run: record.run_snapshot.clone()?,
10285            source_repository: config.source_repository.clone(),
10286            source_revision: record.source_revision.clone(),
10287            dispatch_revision: record.dispatch_commit.clone()?,
10288            request: record.request.clone(),
10289            request_sha256: record.request_sha256.clone(),
10290        })
10291    }
10292
10293    fn has_valid_signed_cleanup_evidence(&self, record: &JobRecord) -> RemoteBuildResult<bool> {
10294        let Some(evidence) = record.signed_cleanup_evidence.as_ref() else {
10295            return Ok(false);
10296        };
10297        let principal = record
10298            .principal
10299            .clone()
10300            .ok_or_else(|| resume_failure("checkpoint_principal_unavailable"))?;
10301        let repository_id = record
10302            .execution_repository_id
10303            .ok_or_else(|| resume_failure("checkpoint_repository_identity_unavailable"))?;
10304        let resume =
10305            GithubJobResumeV1::from_record(&self.config, principal, repository_id, record)?;
10306        evidence.validate_for_resume(&resume)?;
10307        Ok(true)
10308    }
10309
10310    fn sync_job(&self, job_id: &str, cancellation: &CancellationToken) -> RemoteBuildResult<()>
10311    where
10312        R: GhRunner,
10313    {
10314        self.sync_job_with_cancel_policy(job_id, cancellation, true)
10315    }
10316
10317    fn observe_workflow_dispatch_run(
10318        &self,
10319        job_id: &str,
10320        include_terminal: bool,
10321        cancellation: &CancellationToken,
10322    ) -> RemoteBuildResult<Option<RunSnapshot>>
10323    where
10324        R: GhRunner,
10325    {
10326        let (handle, previous) = {
10327            let state = self.lock_state()?;
10328            let record = state
10329                .jobs
10330                .get(job_id)
10331                .ok_or_else(|| job_not_found(job_id))?;
10332            if record.workflow_dispatch.is_none() || record.state.is_terminal() && !include_terminal
10333            {
10334                return Ok(None);
10335            }
10336            let Some(handle) = record.run.clone() else {
10337                return Ok(None);
10338            };
10339            (handle, record.run_snapshot.clone())
10340        };
10341        let snapshot = self
10342            .lock_transport()?
10343            .run(&self.config.repository, &handle)
10344            .map_err(transport_failure)?;
10345        ensure_same_run_attempt(previous.as_ref(), &snapshot)?;
10346        let identity = self.current_resume_identity()?;
10347        cancellation.check()?;
10348
10349        let mut state = self.lock_state()?;
10350        let record = state
10351            .jobs
10352            .get_mut(job_id)
10353            .ok_or_else(|| job_not_found(job_id))?;
10354        if record.workflow_dispatch.is_none() || record.run.as_ref() != Some(&handle) {
10355            return Err(resume_failure("workflow_dispatch_run_identity_changed"));
10356        }
10357        Self::validate_record_identity(record, &identity)?;
10358        if record
10359            .run_snapshot
10360            .as_ref()
10361            .is_some_and(|current| github_run_is_later_attempt(current, &snapshot))
10362            && record.signed_cleanup_evidence.is_some()
10363        {
10364            return Ok(record.run_snapshot.clone());
10365        }
10366        ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot)?;
10367        if (record.state == JobState::Succeeded || !record.manifests.is_empty())
10368            && !run_snapshot_is_successful(&snapshot)
10369        {
10370            return Err(resume_failure("resume_live_success_state_mismatch"));
10371        }
10372        record.run_snapshot = Some(snapshot.clone());
10373        self.checkpoint_record(record)?;
10374        Ok(Some(snapshot))
10375    }
10376
10377    fn apply_workflow_dispatch_snapshot(
10378        &self,
10379        record: &mut JobRecord,
10380        snapshot: &RunSnapshot,
10381    ) -> RemoteBuildResult<()>
10382    where
10383        R: GhRunner,
10384    {
10385        match snapshot.status() {
10386            RunStatus::Requested | RunStatus::Queued | RunStatus::Pending | RunStatus::Waiting => {}
10387            RunStatus::InProgress => {
10388                if record.state == JobState::Queued {
10389                    transition_record(record, JobState::Running)?;
10390                    append_event(
10391                        record,
10392                        self.clock.now_ms(),
10393                        "remote-build",
10394                        RemoteBuildEventKind::PhaseStarted {
10395                            message: Some("GitHub macOS worker started".to_owned()),
10396                        },
10397                    )?;
10398                }
10399            }
10400            RunStatus::Completed => self.finish_from_snapshot(record, snapshot)?,
10401        }
10402        Ok(())
10403    }
10404
10405    #[allow(clippy::too_many_lines)]
10406    fn sync_workflow_dispatch_job_with_cancel_policy(
10407        &self,
10408        job_id: &str,
10409        cancellation: &CancellationToken,
10410        allow_cancel_dispatch: bool,
10411    ) -> RemoteBuildResult<()>
10412    where
10413        R: GhRunner,
10414    {
10415        cancellation.check()?;
10416        {
10417            let mut state = self.lock_state()?;
10418            let record = state
10419                .jobs
10420                .get_mut(job_id)
10421                .ok_or_else(|| job_not_found(job_id))?;
10422            if record.state.is_build_terminal() || record.state.is_terminal() {
10423                self.checkpoint_record(record)?;
10424                return Ok(());
10425            }
10426            if !record.publication_intent {
10427                return Ok(());
10428            }
10429            if record.publication_uncertain {
10430                self.checkpoint_record(record)?;
10431                return Err(provider_failure(
10432                    "publication_reconciliation_required",
10433                    "Possible temporary-ref publication requires exact ref and run-history reconciliation",
10434                    true,
10435                ));
10436            }
10437            normalize_published_record(record, self.clock.now_ms())?;
10438            self.checkpoint_record(record)?;
10439            if record.run.is_none() {
10440                return Err(resume_failure("workflow_dispatch_run_mapping_missing"));
10441            }
10442        }
10443
10444        let Some(mut snapshot) = self.observe_workflow_dispatch_run(job_id, false, cancellation)?
10445        else {
10446            return Ok(());
10447        };
10448        let cancellation_authority = {
10449            let mut state = self.lock_state()?;
10450            let should_reserve = {
10451                let record = state
10452                    .jobs
10453                    .get_mut(job_id)
10454                    .ok_or_else(|| job_not_found(job_id))?;
10455                ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot)?;
10456                record.run_snapshot = Some(snapshot.clone());
10457                record.cancellation_requested
10458                    && !record.cancellation_dispatched
10459                    && !snapshot.status().is_terminal()
10460                    && allow_cancel_dispatch
10461            };
10462            if should_reserve && !self.config.mutation_authorization.cancel_run {
10463                return Err(unsupported(ProviderFeature::Cancellation));
10464            }
10465            should_reserve && state.cancellation_reservations.insert(job_id.to_owned())
10466        };
10467
10468        if cancellation_authority {
10469            let cancellation_result = (|| {
10470                let (handle, previous) = {
10471                    let state = self.lock_state()?;
10472                    let record = state
10473                        .jobs
10474                        .get(job_id)
10475                        .ok_or_else(|| job_not_found(job_id))?;
10476                    let handle = record
10477                        .run
10478                        .clone()
10479                        .ok_or_else(|| resume_failure("workflow_dispatch_run_mapping_missing"))?;
10480                    (handle, record.run_snapshot.clone())
10481                };
10482                let checked = self
10483                    .lock_transport()?
10484                    .run(&self.config.repository, &handle)
10485                    .map_err(transport_failure)?;
10486                ensure_same_run_attempt(previous.as_ref(), &checked)?;
10487                let identity = self.current_resume_identity()?;
10488                let should_cancel = {
10489                    let mut state = self.lock_state()?;
10490                    let record = state
10491                        .jobs
10492                        .get_mut(job_id)
10493                        .ok_or_else(|| job_not_found(job_id))?;
10494                    if record.workflow_dispatch.is_none() || record.run.as_ref() != Some(&handle) {
10495                        return Err(resume_failure("workflow_dispatch_run_identity_changed"));
10496                    }
10497                    Self::validate_record_identity(record, &identity)?;
10498                    ensure_same_run_attempt(record.run_snapshot.as_ref(), &checked)?;
10499                    record.run_snapshot = Some(checked.clone());
10500                    snapshot = checked.clone();
10501                    self.checkpoint_record(record)?;
10502                    record.cancellation_requested
10503                        && !record.cancellation_dispatched
10504                        && !checked.status().is_terminal()
10505                };
10506                if should_cancel {
10507                    self.lock_transport()?
10508                        .cancel_run(&self.config.repository, handle.id())
10509                        .map_err(transport_failure)?;
10510                }
10511                let mut state = self.lock_state()?;
10512                let record = state
10513                    .jobs
10514                    .get_mut(job_id)
10515                    .ok_or_else(|| job_not_found(job_id))?;
10516                if record.workflow_dispatch.is_none() || record.run.as_ref() != Some(&handle) {
10517                    return Err(resume_failure("workflow_dispatch_run_identity_changed"));
10518                }
10519                ensure_same_run_attempt(record.run_snapshot.as_ref(), &checked)?;
10520                record.run_snapshot = Some(checked.clone());
10521                if should_cancel {
10522                    record.cancellation_dispatched = true;
10523                }
10524                self.apply_workflow_dispatch_snapshot(record, &checked)?;
10525                self.checkpoint_record(record)
10526            })();
10527            self.lock_state()?.cancellation_reservations.remove(job_id);
10528            cancellation_result?;
10529        } else {
10530            let mut state = self.lock_state()?;
10531            let record = state
10532                .jobs
10533                .get_mut(job_id)
10534                .ok_or_else(|| job_not_found(job_id))?;
10535            ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot)?;
10536            record.run_snapshot = Some(snapshot.clone());
10537            self.apply_workflow_dispatch_snapshot(record, &snapshot)?;
10538            self.checkpoint_record(record)?;
10539        }
10540        cancellation.check()
10541    }
10542
10543    /// Refresh one restored job through bounded GET requests without cancellation dispatch.
10544    ///
10545    /// Provider state and its checkpoint may advance from exact run observations, including to a
10546    /// terminal state. No remote mutation is issued even when durable cancellation intent exists.
10547    ///
10548    /// # Errors
10549    ///
10550    /// Returns a typed identity, transport, checkpoint, or restored-state failure.
10551    pub fn refresh_restored_job_get_only(
10552        &self,
10553        job_id: &str,
10554        cancellation: &CancellationToken,
10555    ) -> RemoteBuildResult<()>
10556    where
10557        R: GhRunner,
10558    {
10559        self.sync_job_with_cancel_policy(job_id, cancellation, false)
10560    }
10561
10562    fn sync_job_with_cancel_policy(
10563        &self,
10564        job_id: &str,
10565        cancellation: &CancellationToken,
10566        allow_cancel_dispatch: bool,
10567    ) -> RemoteBuildResult<()>
10568    where
10569        R: GhRunner,
10570    {
10571        if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
10572            let synchronization =
10573                self.sync_job_with_cancel_policy_inner(job_id, cancellation, allow_cancel_dispatch);
10574            self.checkpoint_workflow_dispatch_cancellation(job_id, cancellation)?;
10575            return synchronization;
10576        }
10577        self.sync_job_with_cancel_policy_inner(job_id, cancellation, allow_cancel_dispatch)
10578    }
10579
10580    #[allow(clippy::too_many_lines)]
10581    fn sync_job_with_cancel_policy_inner(
10582        &self,
10583        job_id: &str,
10584        cancellation: &CancellationToken,
10585        allow_cancel_dispatch: bool,
10586    ) -> RemoteBuildResult<()>
10587    where
10588        R: GhRunner,
10589    {
10590        cancellation.check()?;
10591        if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
10592            let dispatch_state = {
10593                let state = self.lock_state()?;
10594                let record = state
10595                    .jobs
10596                    .get(job_id)
10597                    .ok_or_else(|| job_not_found(job_id))?;
10598                (!record.state.is_build_terminal()
10599                    && !record.state.is_terminal()
10600                    && record.publication_intent
10601                    && !record.publication_uncertain
10602                    && record.dispatch_commit.is_some()
10603                    && record.run.is_none())
10604                .then_some(record.workflow_dispatch.is_some())
10605            };
10606            match dispatch_state {
10607                Some(true) => self.reconcile_workflow_dispatch_with_cancellation_checkpoint(
10608                    job_id,
10609                    cancellation,
10610                )?,
10611                Some(false) if allow_cancel_dispatch => {
10612                    self.start_or_reconcile_workflow_dispatch(job_id, cancellation)?;
10613                }
10614                Some(false) => return Ok(()),
10615                None => {}
10616            }
10617            return self.sync_workflow_dispatch_job_with_cancel_policy(
10618                job_id,
10619                cancellation,
10620                allow_cancel_dispatch,
10621            );
10622        }
10623        let mut state = self.lock_state()?;
10624        let record = state
10625            .jobs
10626            .get_mut(job_id)
10627            .ok_or_else(|| job_not_found(job_id))?;
10628        if record.state.is_build_terminal() || record.state.is_terminal() {
10629            self.checkpoint_record(record)?;
10630            return Ok(());
10631        }
10632        if !record.publication_intent {
10633            return Ok(());
10634        }
10635        if record.publication_uncertain {
10636            self.checkpoint_record(record)?;
10637            return Err(provider_failure(
10638                "publication_reconciliation_required",
10639                "Possible temporary-ref publication requires exact ref and run-history reconciliation",
10640                true,
10641            ));
10642        }
10643        if record.dispatch_commit.is_some() {
10644            normalize_published_record(record, self.clock.now_ms())?;
10645            self.checkpoint_record(record)?;
10646            self.revalidate_record_identity(record)?;
10647        }
10648
10649        let mut mapped_snapshot = None;
10650        if record.run.is_none() {
10651            if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
10652                return Err(resume_failure("workflow_dispatch_run_mapping_missing"));
10653            }
10654            let Some(dispatch_commit) = record.dispatch_commit.as_ref() else {
10655                self.checkpoint_record(record)?;
10656                return Ok(());
10657            };
10658            let result = self.lock_transport()?.find_run(
10659                &self.config.repository,
10660                &self.config.workflow.filename().repository_path(),
10661                dispatch_commit,
10662                record.temporary_ref.branch(),
10663                RunEvent::Push,
10664            );
10665            match result {
10666                Ok(handle) => {
10667                    let snapshot = self
10668                        .lock_transport()?
10669                        .run(&self.config.repository, &handle)
10670                        .map_err(transport_failure)?;
10671                    self.revalidate_record_identity(record)?;
10672                    if snapshot.run_attempt() != 1 {
10673                        return Err(provider_failure(
10674                            "run_initial_attempt_mismatch",
10675                            "Initial GitHub run mapping observed an external rerun attempt",
10676                            false,
10677                        ));
10678                    }
10679                    record.run = Some(handle);
10680                    record.run_snapshot = Some(snapshot.clone());
10681                    append_event(
10682                        record,
10683                        self.clock.now_ms(),
10684                        "queue",
10685                        RemoteBuildEventKind::Progress {
10686                            message: "Exact GitHub Actions run mapped to this job".to_owned(),
10687                            current: None,
10688                            total: None,
10689                        },
10690                    )?;
10691                    self.checkpoint_record(record)?;
10692                    mapped_snapshot = Some(snapshot);
10693                }
10694                Err(TransportError::RunNotFound) => {
10695                    record.run_discovery_attempts = record.run_discovery_attempts.saturating_add(1);
10696                    if record.run_discovery_attempts >= self.config.poll_policy.discovery_attempts()
10697                        || self.clock.now_ms() >= record.run_discovery_deadline_ms
10698                    {
10699                        ensure_running_or_cancelling(record)?;
10700                        transition_record(record, JobState::Failed)?;
10701                        append_event(
10702                            record,
10703                            self.clock.now_ms(),
10704                            "cleanup",
10705                            RemoteBuildEventKind::Warning {
10706                                code: "github.cleanup_required".to_owned(),
10707                                message: "Temporary Git ref cleanup is required after run discovery expired"
10708                                    .to_owned(),
10709                                help: Some(
10710                                    "Run provider cleanup for this exact job before retrying"
10711                                        .to_owned(),
10712                                ),
10713                            },
10714                        )?;
10715                        append_failed(
10716                            record,
10717                            self.clock.now_ms(),
10718                            "run_discovery_exhausted",
10719                            "Exact GitHub Actions run was not found within the bounded discovery policy",
10720                            true,
10721                        )?;
10722                    }
10723                    self.checkpoint_record(record)?;
10724                    return Ok(());
10725                }
10726                Err(error) => return Err(transport_failure(error)),
10727            }
10728        }
10729
10730        let handle = record.run.clone().ok_or_else(|| {
10731            provider_failure(
10732                "run_identity_unavailable",
10733                "GitHub run identity was unavailable after lookup",
10734                true,
10735            )
10736        })?;
10737        let mut snapshot = if let Some(snapshot) = mapped_snapshot {
10738            snapshot
10739        } else {
10740            let snapshot = self
10741                .lock_transport()?
10742                .run(&self.config.repository, &handle)
10743                .map_err(transport_failure)?;
10744            ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot)?;
10745            snapshot
10746        };
10747        record.run_snapshot = Some(snapshot.clone());
10748
10749        if record.cancellation_requested
10750            && !record.cancellation_dispatched
10751            && !snapshot.status().is_terminal()
10752            && allow_cancel_dispatch
10753        {
10754            if !self.config.mutation_authorization.cancel_run {
10755                return Err(unsupported(ProviderFeature::Cancellation));
10756            }
10757            self.checkpoint_record(record)?;
10758            self.revalidate_record_identity(record)?;
10759            let checked = self
10760                .lock_transport()?
10761                .run(&self.config.repository, &handle)
10762                .map_err(transport_failure)?;
10763            ensure_same_run_attempt(record.run_snapshot.as_ref(), &checked)?;
10764            record.run_snapshot = Some(checked.clone());
10765            snapshot = checked;
10766            if !snapshot.status().is_terminal() {
10767                self.revalidate_record_identity(record)?;
10768                // GitHub exposes no attempt-conditioned cancellation endpoint. The provider owns
10769                // and cancels the immutable run ID across every attempt after this exact GET.
10770                self.lock_transport()?
10771                    .cancel_run(&self.config.repository, handle.id())
10772                    .map_err(transport_failure)?;
10773                record.cancellation_dispatched = true;
10774                self.checkpoint_record(record)?;
10775            }
10776        }
10777
10778        match snapshot.status() {
10779            RunStatus::Requested | RunStatus::Queued | RunStatus::Pending | RunStatus::Waiting => {}
10780            RunStatus::InProgress => {
10781                if record.state == JobState::Queued {
10782                    transition_record(record, JobState::Running)?;
10783                    append_event(
10784                        record,
10785                        self.clock.now_ms(),
10786                        "remote-build",
10787                        RemoteBuildEventKind::PhaseStarted {
10788                            message: Some("GitHub macOS worker started".to_owned()),
10789                        },
10790                    )?;
10791                }
10792            }
10793            RunStatus::Completed => self.finish_from_snapshot(record, &snapshot)?,
10794        }
10795        self.checkpoint_record(record)?;
10796        cancellation.check()
10797    }
10798
10799    fn finish_from_snapshot(
10800        &self,
10801        record: &mut JobRecord,
10802        snapshot: &RunSnapshot,
10803    ) -> RemoteBuildResult<()>
10804    where
10805        R: GhRunner,
10806    {
10807        match snapshot.conclusion() {
10808            Some(RunConclusion::Cancelled) => {
10809                ensure_running_or_cancelling(record)?;
10810                transition_record(record, JobState::Cancelled)?;
10811                append_event(
10812                    record,
10813                    self.clock.now_ms(),
10814                    "finished",
10815                    RemoteBuildEventKind::OperationCancelled {
10816                        reason: "github_run_cancelled".to_owned(),
10817                        duration_ms: self.clock.now_ms().saturating_sub(record.created_at_ms),
10818                    },
10819                )
10820            }
10821            Some(RunConclusion::Success) if record.cancellation_requested => {
10822                ensure_running_or_cancelling(record)?;
10823                transition_record(record, JobState::Failed)?;
10824                append_failed(
10825                    record,
10826                    self.clock.now_ms(),
10827                    "cancel_race",
10828                    "GitHub run completed after cancellation was requested",
10829                    false,
10830                )
10831            }
10832            Some(RunConclusion::Success) => self.finish_success(record),
10833            Some(conclusion) => {
10834                ensure_running_or_cancelling(record)?;
10835                transition_record(record, JobState::Failed)?;
10836                append_failed(
10837                    record,
10838                    self.clock.now_ms(),
10839                    conclusion_code(conclusion),
10840                    "GitHub Actions build did not succeed",
10841                    conclusion_retryable(conclusion),
10842                )
10843            }
10844            None => Err(provider_failure(
10845                "missing_run_conclusion",
10846                "completed GitHub Actions run has no conclusion",
10847                false,
10848            )),
10849        }
10850    }
10851
10852    #[allow(clippy::too_many_lines)]
10853    fn finish_success(&self, record: &mut JobRecord) -> RemoteBuildResult<()>
10854    where
10855        R: GhRunner,
10856    {
10857        if record.state == JobState::Queued {
10858            transition_record(record, JobState::Running)?;
10859        }
10860        let mut artifacts = self.lock_artifacts()?;
10861        let supports_listing = artifacts.supports_listing();
10862        if !supports_listing {
10863            if !record.verification_pending_event {
10864                append_event(
10865                    record,
10866                    self.clock.now_ms(),
10867                    "artifact-verification",
10868                    RemoteBuildEventKind::Warning {
10869                        code: "github.artifact_verification_pending".to_owned(),
10870                        message: "GitHub run succeeded; independent artifact ingestion is not configured"
10871                            .to_owned(),
10872                        help: Some(
10873                            "Configure a verified artifact store before treating this build as successful"
10874                                .to_owned(),
10875                        ),
10876                    },
10877                )?;
10878                record.verification_pending_event = true;
10879            }
10880            return Ok(());
10881        }
10882        if artifacts.supports_signed_cleanup_evidence()
10883            && (record.principal.is_none() || record.execution_repository_id.is_none())
10884        {
10885            drop(artifacts);
10886            let (principal, repository_id) = self.current_resume_identity()?;
10887            record.principal = Some(principal);
10888            record.execution_repository_id = Some(repository_id);
10889            artifacts = self.lock_artifacts()?;
10890        }
10891        self.revalidate_record_identity(record)?;
10892        let context = Self::artifact_context(record, &self.config).ok_or_else(|| {
10893            provider_failure(
10894                "run_identity_unavailable",
10895                "artifact verification requires a terminal run snapshot",
10896                true,
10897            )
10898        })?;
10899        let manifests = artifacts.list_verified(&context)?;
10900        if manifests.is_empty() {
10901            return Ok(());
10902        }
10903        let verified_evidence = artifacts.verified_run_evidence(&context)?.ok_or_else(|| {
10904            provider_failure(
10905                "verified_run_evidence_missing",
10906                "verified artifacts lack independently retained compile evidence",
10907                false,
10908            )
10909        })?;
10910        drop(artifacts);
10911        validate_manifests(record, &self.config, &manifests)?;
10912        validate_github_compile_phase_evidence(
10913            verified_evidence.compile_evidence(),
10914            &record.request,
10915            &record.job_id,
10916        )?;
10917        let signed_cleanup_evidence = verified_evidence.signed_cleanup_evidence().cloned();
10918        match record.request.signing.mode {
10919            SigningMode::ManualDevelopment if signed_cleanup_evidence.is_none() => {
10920                return Err(provider_failure(
10921                    "signed_cleanup_evidence_missing",
10922                    "signed artifacts lack attempt-scoped protected-cleanup evidence",
10923                    false,
10924                ));
10925            }
10926            SigningMode::UnsignedCompileOnly if signed_cleanup_evidence.is_some() => {
10927                return Err(provider_failure(
10928                    "signed_cleanup_evidence_unexpected",
10929                    "unsigned artifacts cannot carry signed-cleanup evidence",
10930                    false,
10931                ));
10932            }
10933            _ => {}
10934        }
10935        let mut candidate = record.clone();
10936        candidate.manifests.clone_from(&manifests);
10937        candidate.compile_evidence = Some(verified_evidence.compile_evidence().clone());
10938        candidate.signed_cleanup_evidence = signed_cleanup_evidence;
10939        if let Some(evidence) = candidate.signed_cleanup_evidence.as_ref() {
10940            let principal = candidate
10941                .principal
10942                .clone()
10943                .ok_or_else(|| resume_failure("checkpoint_principal_unavailable"))?;
10944            let repository_id = candidate
10945                .execution_repository_id
10946                .ok_or_else(|| resume_failure("checkpoint_repository_identity_unavailable"))?;
10947            let resume =
10948                GithubJobResumeV1::from_record(&self.config, principal, repository_id, &candidate)?;
10949            evidence.validate_for_resume(&resume)?;
10950        }
10951        for manifest in &manifests {
10952            append_event(
10953                record,
10954                self.clock.now_ms(),
10955                "artifact-verification",
10956                RemoteBuildEventKind::ArtifactValidated {
10957                    artifact: manifest.clone(),
10958                },
10959            )?;
10960        }
10961        record.manifests = candidate.manifests;
10962        record.compile_evidence = candidate.compile_evidence;
10963        record.signed_cleanup_evidence = candidate.signed_cleanup_evidence;
10964        transition_record(record, JobState::Succeeded)?;
10965        let result = IosDeviceBuildResult {
10966            protocol_version: CURRENT_PROTOCOL_VERSION,
10967            operation_id: record.operation_id.clone(),
10968            job_id: record.job_id.clone(),
10969            state: JobState::Succeeded,
10970            artifacts: manifests,
10971            cleanup: None,
10972        };
10973        result.validate()?;
10974        append_event(
10975            record,
10976            self.clock.now_ms(),
10977            "finished",
10978            RemoteBuildEventKind::OperationFinished {
10979                success: true,
10980                duration_ms: self.clock.now_ms().saturating_sub(record.created_at_ms),
10981                result: Some(result),
10982                error: None,
10983            },
10984        )
10985    }
10986}
10987
10988#[cfg(feature = "secure-job-log-http")]
10989impl<P, A, C> GithubBuildProvider<GhProcessRunner, P, A, C>
10990where
10991    P: TemporaryRefPublisher,
10992    A: VerifiedArtifactStore,
10993    C: ProviderClock,
10994{
10995    /// Live-revalidate one restored job, then fetch its exact terminal attempt's sanitized logs.
10996    ///
10997    /// Active or unmapped attempts return `None`. A successful poll contains only bounded,
10998    /// sanitized projections; raw bodies, redirect URLs, and credentials never cross this API.
10999    ///
11000    /// # Errors
11001    ///
11002    /// Returns a typed identity, checkpoint, authentication, transport, redirect, parsing, or
11003    /// resource-bound failure.
11004    pub fn fetch_restored_job_logs(
11005        &self,
11006        job_id: &str,
11007        cancellation: &CancellationToken,
11008    ) -> RemoteBuildResult<Option<GithubJobLogPoll>> {
11009        self.revalidate_restored_job_live(job_id, cancellation)?;
11010        let identity = {
11011            let state = self.lock_state()?;
11012            let record = state
11013                .jobs
11014                .get(job_id)
11015                .ok_or_else(|| job_not_found(job_id))?;
11016            let Some(snapshot) = record.run_snapshot.as_ref() else {
11017                return Ok(None);
11018            };
11019            if !snapshot.status().is_terminal() || snapshot.conclusion().is_none() {
11020                return Ok(None);
11021            }
11022            crate::job_logs::GithubRunAttemptLogIdentity::from_terminal_run(
11023                self.config.repository.clone(),
11024                snapshot,
11025            )
11026            .map_err(|_| job_log_identity_failure())?
11027        };
11028        let runner = { self.lock_transport()?.runner().clone() };
11029        let client = runner
11030            .job_log_http_client()
11031            .map_err(job_log_client_failure)?;
11032        GithubJobLogFetcher::new(client, GithubJobLogLimits::secure_defaults())
11033            .fetch_attempt(&identity, cancellation)
11034            .map(Some)
11035            .map_err(job_log_fetch_failure)
11036    }
11037}
11038
11039impl<R, P, A, C> BuildProvider for GithubBuildProvider<R, P, A, C>
11040where
11041    R: GhRunner + Send,
11042    P: TemporaryRefPublisher + Send,
11043    A: VerifiedArtifactStore + Send,
11044    C: ProviderClock,
11045{
11046    fn id(&self) -> &str {
11047        GITHUB_PROVIDER_ID
11048    }
11049
11050    fn capabilities(&self) -> &ProviderCapabilities {
11051        &self.capabilities
11052    }
11053
11054    fn handshake(
11055        &self,
11056        request: HandshakeRequest,
11057        cancellation: CancellationToken,
11058    ) -> ProviderFuture<'_, HandshakeResponse> {
11059        let result = cancellation.check().and_then(|()| {
11060            HandshakeResponse::negotiate(
11061                &request,
11062                GITHUB_PROVIDER_ID,
11063                "github-hosted-macos",
11064                self.config.worker_version.clone(),
11065                self.capabilities.clone(),
11066            )
11067        });
11068        Box::pin(async move { result })
11069    }
11070
11071    #[allow(clippy::too_many_lines)]
11072    fn doctor(
11073        &self,
11074        request: ProviderDoctorRequest,
11075        cancellation: CancellationToken,
11076    ) -> ProviderFuture<'_, ProviderDoctorReport> {
11077        let result = (|| {
11078            cancellation.check()?;
11079            CURRENT_PROTOCOL_VERSION.negotiate(request.protocol_version)?;
11080            validate_provider_identifier("operation_id", &request.operation_id)?;
11081
11082            let mut checks = Vec::new();
11083            let authenticated = self.lock_transport()?.authenticate(&self.config.repository);
11084            cancellation.check()?;
11085
11086            let execution_info = match self.lock_transport()?.repository(&self.config.repository) {
11087                Ok(repository) => {
11088                    checks.push(if repository.is_archived() || repository.is_disabled() {
11089                        ProviderCheck {
11090                            code: "github.repository".to_owned(),
11091                            status: ProviderCheckStatus::Error,
11092                            message: "Configured GitHub repository cannot run new builds"
11093                                .to_owned(),
11094                            help: Some("Use an active, non-archived repository".to_owned()),
11095                        }
11096                    } else {
11097                        ProviderCheck {
11098                            code: "github.repository".to_owned(),
11099                            status: ProviderCheckStatus::Ready,
11100                            message: "Exact configured GitHub execution repository is available"
11101                                .to_owned(),
11102                            help: None,
11103                        }
11104                    });
11105                    if request.require_signing {
11106                        checks.push(if repository.is_private() {
11107                            ProviderCheck {
11108                                code: "github.signing_repository_visibility".to_owned(),
11109                                status: ProviderCheckStatus::Ready,
11110                                message: "Exact GitHub signing execution repository is private"
11111                                    .to_owned(),
11112                                help: None,
11113                            }
11114                        } else {
11115                            ProviderCheck {
11116                                code: "github.signing_repository_visibility".to_owned(),
11117                                status: ProviderCheckStatus::Error,
11118                                message:
11119                                    "Signed artifacts require a private GitHub execution repository"
11120                                        .to_owned(),
11121                                help: Some(
11122                                    "Configure a private signing repository so embedded provisioning metadata is not published to public readers"
11123                                        .to_owned(),
11124                                ),
11125                            }
11126                        });
11127                    }
11128                    Some(repository)
11129                }
11130                Err(error) => {
11131                    checks.push(ProviderCheck {
11132                        code: "github.repository".to_owned(),
11133                        status: ProviderCheckStatus::Error,
11134                        message: transport_public_message(error),
11135                        help: Some(
11136                            "Check execution repository identity and token access".to_owned(),
11137                        ),
11138                    });
11139                    if request.require_signing {
11140                        checks.push(ProviderCheck {
11141                            code: "github.signing_repository_visibility".to_owned(),
11142                            status: ProviderCheckStatus::Error,
11143                            message: "Private signing repository visibility could not be proven"
11144                                .to_owned(),
11145                            help: Some(
11146                                "Grant repository metadata access and verify the private execution repository"
11147                                    .to_owned(),
11148                            ),
11149                        });
11150                    }
11151                    None
11152                }
11153            };
11154            let authentication_check = match (&authenticated, &execution_info) {
11155                (Ok(AuthenticatedPrincipal::User(user)), _) => ProviderCheck {
11156                    code: "github.authentication".to_owned(),
11157                    status: ProviderCheckStatus::Ready,
11158                    message: format!(
11159                        "Authenticated GitHub account `{}` is available",
11160                        user.login()
11161                    ),
11162                    help: None,
11163                },
11164                (
11165                    Ok(AuthenticatedPrincipal::RepositoryCredential { .. }),
11166                    _,
11167                ) if self.config.workflow.run_trigger() != WorkflowRunTrigger::WorkflowDispatch => {
11168                    ProviderCheck {
11169                        code: "github.authentication".to_owned(),
11170                        status: ProviderCheckStatus::Error,
11171                        message: "Repository-scoped GitHub credentials require explicit workflow dispatch"
11172                            .to_owned(),
11173                        help: Some(
11174                            "Use an authenticated user for Push or configure WorkflowDispatch"
11175                                .to_owned(),
11176                        ),
11177                    }
11178                }
11179                (
11180                    Ok(AuthenticatedPrincipal::RepositoryCredential { repository_id }),
11181                    Some(repository),
11182                ) if *repository_id == repository.id() => ProviderCheck {
11183                    code: "github.authentication".to_owned(),
11184                    status: ProviderCheckStatus::Ready,
11185                    message: "Repository-scoped GitHub credential is bound to the exact execution repository"
11186                        .to_owned(),
11187                    help: None,
11188                },
11189                (Ok(AuthenticatedPrincipal::RepositoryCredential { .. }), _) => ProviderCheck {
11190                    code: "github.authentication".to_owned(),
11191                    status: ProviderCheckStatus::Error,
11192                    message: "Repository-scoped GitHub credential identity could not be bound to the exact execution repository"
11193                        .to_owned(),
11194                    help: Some(
11195                        "Grant the credential access to the exact execution repository".to_owned(),
11196                    ),
11197                },
11198                (Err(error), _) => ProviderCheck {
11199                    code: "github.authentication".to_owned(),
11200                    status: ProviderCheckStatus::Error,
11201                    message: transport_public_message(*error),
11202                    help: Some("Authenticate the configured GitHub transport and retry".to_owned()),
11203                },
11204            };
11205            checks.insert(0, authentication_check);
11206            let execution_repository_url = repository_url(&self.config.repository);
11207            let source_info =
11208                if github_remote_matches(&self.config.source_repository, &execution_repository_url)
11209                {
11210                    execution_info.clone()
11211                } else {
11212                    let source_repository = repository_from_url(&self.config.source_repository)
11213                        .ok_or_else(|| {
11214                            provider_failure(
11215                                "source_repository_invalid",
11216                                "configured source repository identity is invalid",
11217                                false,
11218                            )
11219                        })?;
11220                    match self.lock_transport()?.repository(&source_repository) {
11221                        Ok(repository) => Some(repository),
11222                        Err(error) => {
11223                            checks.push(ProviderCheck {
11224                                code: "github.source_repository".to_owned(),
11225                                status: ProviderCheckStatus::Error,
11226                                message: transport_public_message(error),
11227                                help: Some(
11228                                    "Check public source repository identity and token access"
11229                                        .to_owned(),
11230                                ),
11231                            });
11232                            None
11233                        }
11234                    }
11235                };
11236            match source_info.as_ref() {
11237                Some(repository) if !repository.is_private() => checks.push(ProviderCheck {
11238                    code: "github.source_repository_visibility".to_owned(),
11239                    status: ProviderCheckStatus::Ready,
11240                    message: "Exact configured source repository is public".to_owned(),
11241                    help: None,
11242                }),
11243                Some(_) => checks.push(ProviderCheck {
11244                    code: "github.source_repository_visibility".to_owned(),
11245                    status: ProviderCheckStatus::Error,
11246                    message: "Trusted project source must be in a public GitHub repository"
11247                        .to_owned(),
11248                    help: Some(
11249                        "Keep public source separate from the private signing execution repository"
11250                            .to_owned(),
11251                    ),
11252                }),
11253                None => checks.push(ProviderCheck {
11254                    code: "github.source_repository_visibility".to_owned(),
11255                    status: ProviderCheckStatus::Error,
11256                    message: "Public source repository visibility could not be proven".to_owned(),
11257                    help: Some(
11258                        "Grant repository metadata access and verify the public source repository"
11259                            .to_owned(),
11260                    ),
11261                }),
11262            }
11263            if request.require_signing {
11264                checks.push(match (&source_info, &execution_info) {
11265                    (Some(source), Some(execution)) if source.id() != execution.id() => {
11266                        ProviderCheck {
11267                            code: "github.signing_repository_distinct".to_owned(),
11268                            status: ProviderCheckStatus::Ready,
11269                            message: "Public source and private signing execution use distinct GitHub repositories"
11270                                .to_owned(),
11271                            help: None,
11272                        }
11273                    }
11274                    (Some(_), Some(_)) => ProviderCheck {
11275                        code: "github.signing_repository_distinct".to_owned(),
11276                        status: ProviderCheckStatus::Error,
11277                        message: "Signed builds require distinct source and execution repositories"
11278                            .to_owned(),
11279                        help: Some(
11280                            "Keep project source public and configure a separate private signing execution repository"
11281                                .to_owned(),
11282                        ),
11283                    },
11284                    _ => ProviderCheck {
11285                        code: "github.signing_repository_distinct".to_owned(),
11286                        status: ProviderCheckStatus::Error,
11287                        message: "Distinct signing repository identity could not be proven"
11288                            .to_owned(),
11289                        help: Some(
11290                            "Grant repository metadata access for both configured repositories"
11291                                .to_owned(),
11292                        ),
11293                        },
11294                });
11295            }
11296
11297            if request.require_signing {
11298                match self
11299                    .lock_transport()?
11300                    .actions_permissions(&self.config.repository)
11301                {
11302                    Ok(permissions) if permissions.enabled() => checks.push(ProviderCheck {
11303                        code: "github.actions.enabled".to_owned(),
11304                        status: ProviderCheckStatus::Ready,
11305                        message: "GitHub Actions is enabled for the execution repository".to_owned(),
11306                        help: None,
11307                    }),
11308                    Ok(_) => checks.push(ProviderCheck {
11309                        code: "github.actions.enabled".to_owned(),
11310                        status: ProviderCheckStatus::Error,
11311                        message: "GitHub Actions is disabled for the execution repository".to_owned(),
11312                        help: Some(
11313                            "Enable GitHub Actions for the exact execution repository".to_owned(),
11314                        ),
11315                    }),
11316                    Err(error) => checks.push(ProviderCheck {
11317                        code: "github.actions.enabled".to_owned(),
11318                        status: ProviderCheckStatus::Error,
11319                        message: transport_public_message(error),
11320                        help: Some(
11321                            "Grant repository administration read access and verify GitHub Actions policy"
11322                                .to_owned(),
11323                        ),
11324                    }),
11325                }
11326            }
11327
11328            if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
11329                let default_branch_check = execution_info.as_ref().map_or_else(
11330                    || ProviderCheck {
11331                        code: "github.workflow_dispatch_default_branch".to_owned(),
11332                        status: ProviderCheckStatus::Error,
11333                        message: "Execution repository default branch could not be proven"
11334                            .to_owned(),
11335                        help: Some(
11336                            "Grant repository metadata access and configure workflow dispatch on the exact default branch"
11337                                .to_owned(),
11338                        ),
11339                    },
11340                    |repository| {
11341                        let expected = format!("refs/heads/{}", repository.default_branch().as_str());
11342                        if self.config.workflow.trusted_source_ref().as_str() == expected {
11343                            ProviderCheck {
11344                                code: "github.workflow_dispatch_default_branch".to_owned(),
11345                                status: ProviderCheckStatus::Ready,
11346                                message: "Workflow dispatch is bound to the execution repository default branch"
11347                                    .to_owned(),
11348                                help: None,
11349                            }
11350                        } else {
11351                            ProviderCheck {
11352                                code: "github.workflow_dispatch_default_branch".to_owned(),
11353                                status: ProviderCheckStatus::Error,
11354                                message: "Workflow dispatch trusted ref is not the execution repository default branch"
11355                                    .to_owned(),
11356                                help: Some(
11357                                    "Configure the exact execution repository default branch for workflow dispatch"
11358                                        .to_owned(),
11359                                ),
11360                            }
11361                        }
11362                    },
11363                );
11364                checks.push(default_branch_check);
11365                let workflow_path = self.config.workflow.filename().repository_path();
11366                match self
11367                    .lock_transport()?
11368                    .workflow_registration(&self.config.repository, &workflow_path)
11369                {
11370                    Ok(_) => checks.push(ProviderCheck {
11371                        code: "github.workflow_registration".to_owned(),
11372                        status: ProviderCheckStatus::Ready,
11373                        message: "Exact workflow_dispatch workflow is actively registered"
11374                            .to_owned(),
11375                        help: None,
11376                    }),
11377                    Err(error) => checks.push(ProviderCheck {
11378                        code: "github.workflow_registration".to_owned(),
11379                        status: ProviderCheckStatus::Error,
11380                        message: transport_public_message(error),
11381                        help: Some(
11382                            "Register and enable the exact configured workflow on the execution repository's default branch"
11383                                .to_owned(),
11384                        ),
11385                    }),
11386                }
11387                checks.push(match self.verify_execution_workflow_default() {
11388                    Ok(()) => ProviderCheck {
11389                        code: "github.workflow_dispatch_default_workflow".to_owned(),
11390                        status: ProviderCheckStatus::Ready,
11391                        message: "Exact WorkflowDispatch bytes are present on the execution repository default branch"
11392                            .to_owned(),
11393                        help: None,
11394                    },
11395                    Err(_) => ProviderCheck {
11396                        code: "github.workflow_dispatch_default_workflow".to_owned(),
11397                        status: ProviderCheckStatus::Error,
11398                        message: "Exact WorkflowDispatch bytes could not be proven on the execution repository default branch"
11399                            .to_owned(),
11400                        help: Some(
11401                            "Install the generated workflow unchanged on the exact execution default branch"
11402                                .to_owned(),
11403                        ),
11404                    },
11405                });
11406            }
11407            cancellation.check()?;
11408
11409            match self.lock_publisher()?.doctor(&TemporaryRefDoctorRequest {
11410                repository: &self.config.repository,
11411                source_repository: &self.config.source_repository,
11412                trusted_source_ref: self.config.workflow.trusted_source_ref(),
11413                temporary_branch_namespace: self.config.workflow.temporary_branch_namespace(),
11414                workflow_path: &self.config.workflow.filename().repository_path(),
11415                workflow_fingerprint: &self.config.workflow_fingerprint,
11416            }) {
11417                Ok(_) => {
11418                    checks.push(ProviderCheck {
11419                        code: "github.trusted_source".to_owned(),
11420                        status: ProviderCheckStatus::Ready,
11421                        message: "Trusted ref contains the exact approved workflow bytes"
11422                            .to_owned(),
11423                        help: None,
11424                    });
11425                    checks.push(ProviderCheck {
11426                        code: "github.temporary_ref_policy_unproven".to_owned(),
11427                        status: ProviderCheckStatus::Warning,
11428                        message: "Credentialed receive-pack is reachable; branch mutation policy is proven only by a durable build publication"
11429                            .to_owned(),
11430                        help: Some(
11431                            "Run an authorized build to prove the configured temporary-ref policy"
11432                                .to_owned(),
11433                        ),
11434                    });
11435                }
11436                Err(error) => checks.push(ProviderCheck {
11437                    code: "github.trusted_source".to_owned(),
11438                    status: ProviderCheckStatus::Error,
11439                    message: publisher_public_message(error),
11440                    help: Some("Check the configured Git remote and trusted source ref".to_owned()),
11441                }),
11442            }
11443
11444            checks.push(
11445                if self.config.mutation_authorization.publish_temporary_ref {
11446                    ProviderCheck {
11447                        code: "github.temporary_ref_authorization".to_owned(),
11448                        status: ProviderCheckStatus::Ready,
11449                        message: "Caller authorized create-only temporary build refs".to_owned(),
11450                        help: None,
11451                    }
11452                } else {
11453                    ProviderCheck {
11454                        code: "github.temporary_ref_authorization".to_owned(),
11455                        status: ProviderCheckStatus::Error,
11456                        message: "Temporary build ref publication is not authorized".to_owned(),
11457                        help: Some("Confirm the exact temporary ref before submission".to_owned()),
11458                    }
11459                },
11460            );
11461            if request.require_signing {
11462                let mut transport = self.lock_transport()?;
11463                append_signing_environment_checks(&mut checks, &mut transport, &self.config);
11464                drop(transport);
11465                cancellation.check()?;
11466                checks.push(if self.lock_artifacts()?.supports_signed_cleanup_evidence() {
11467                    ProviderCheck {
11468                        code: "github.signing_cleanup_evidence".to_owned(),
11469                        status: ProviderCheckStatus::Ready,
11470                        message: "Independent verification retains attempt-scoped protected-cleanup evidence"
11471                            .to_owned(),
11472                        help: None,
11473                    }
11474                } else {
11475                    ProviderCheck {
11476                        code: "github.signing_cleanup_evidence".to_owned(),
11477                        status: ProviderCheckStatus::Error,
11478                        message: "Signed cleanup lacks attempt-scoped worker evidence that signing material was removed"
11479                            .to_owned(),
11480                        help: Some(
11481                            "Configure the verified GitHub artifact store before signed builds"
11482                                .to_owned(),
11483                        ),
11484                    }
11485                });
11486            }
11487            if !self
11488                .artifacts
11489                .lock()
11490                .map_err(|_| {
11491                    provider_failure(
11492                        "artifact_store_unavailable",
11493                        "verified artifact store is unavailable",
11494                        true,
11495                    )
11496                })?
11497                .supports_download()
11498            {
11499                checks.push(ProviderCheck {
11500                    code: "github.artifact_ingestion".to_owned(),
11501                    status: ProviderCheckStatus::Error,
11502                    message: "Independent artifact download and verification are not configured"
11503                        .to_owned(),
11504                    help: Some("Configure a verified GitHub artifact store".to_owned()),
11505                });
11506            }
11507            let ready = checks
11508                .iter()
11509                .all(|check| check.status != ProviderCheckStatus::Error);
11510            Ok(ProviderDoctorReport {
11511                protocol_version: CURRENT_PROTOCOL_VERSION,
11512                provider: GITHUB_PROVIDER_ID.to_owned(),
11513                ready,
11514                checks,
11515                capabilities: self.capabilities.clone(),
11516            })
11517        })();
11518        Box::pin(async move { result })
11519    }
11520
11521    #[allow(clippy::too_many_lines)]
11522    fn submit(
11523        &self,
11524        request: IosDeviceBuildRequest,
11525        cancellation: CancellationToken,
11526    ) -> ProviderFuture<'_, JobHandle> {
11527        let result = (|| {
11528            cancellation.check()?;
11529            validate_submission(&self.config, &request)?;
11530            if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch
11531                && self.checkpoint_sink.is_none()
11532            {
11533                return Err(provider_failure(
11534                    "workflow_dispatch_checkpoint_required",
11535                    "Workflow dispatch requires a durable provider checkpoint sink",
11536                    false,
11537                ));
11538            }
11539            let branch = BranchName::new(format!(
11540                "{}/{}",
11541                self.config.workflow.temporary_branch_namespace().as_str(),
11542                request.operation_id
11543            ))
11544            .map_err(|_| {
11545                provider_failure(
11546                    "operation_id_not_ref_safe",
11547                    "operation ID cannot form a safe temporary Git ref",
11548                    false,
11549                )
11550            })?;
11551            let temporary_ref =
11552                TemporaryGitRef::new(self.config.workflow.temporary_branch_namespace(), branch)
11553                    .map_err(|_| {
11554                        provider_failure(
11555                            "operation_id_not_ref_safe",
11556                            "operation ID cannot form a safe temporary Git ref",
11557                            false,
11558                        )
11559                    })?;
11560            let source_revision =
11561                CommitSha::new(request.source_revision.clone().ok_or_else(|| {
11562                    provider_failure(
11563                        "invalid_source_revision",
11564                        "source revision is not an exact commit SHA",
11565                        false,
11566                    )
11567                })?)
11568                .map_err(|_| {
11569                    provider_failure(
11570                        "invalid_source_revision",
11571                        "source revision is not an exact commit SHA",
11572                        false,
11573                    )
11574                })?;
11575            let job_id = request.operation_id.clone();
11576            let reservation = JobReservation::acquire(
11577                &self.state,
11578                job_id.clone(),
11579                self.config.max_jobs,
11580                self.checkpoint_sink.is_none(),
11581            )?;
11582            let resume_identity = if self.checkpoint_sink.is_some() {
11583                Some(self.current_resume_identity()?)
11584            } else {
11585                None
11586            };
11587            let mut workflow_dispatch_client =
11588                if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
11589                    self.preflight_workflow_dispatch_registration()?;
11590                    Some(self.prepare_workflow_dispatch_client()?)
11591                } else {
11592                    None
11593                };
11594            let created_at_ms = self.clock.now_ms();
11595            let workflow = generate_workflow(&self.config.workflow);
11596            let manifest =
11597                GithubDispatchManifest::new(&self.config, &temporary_ref, request.clone());
11598            manifest.validate_for(&self.config)?;
11599            let manifest_bytes = manifest.encode()?;
11600            let request_sha256 = canonical_request_sha256(&request)?;
11601            let discovery_deadline_ms =
11602                created_at_ms.saturating_add(self.config.poll_policy.discovery_window_ms());
11603            let mut record = JobRecord {
11604                principal: resume_identity
11605                    .as_ref()
11606                    .map(|(principal, _)| principal.clone()),
11607                execution_repository_id: resume_identity
11608                    .as_ref()
11609                    .map(|(_, repository_id)| *repository_id),
11610                operation_id: request.operation_id.clone(),
11611                job_id: job_id.clone(),
11612                source_revision: source_revision.clone(),
11613                git_snapshot: None,
11614                request: request.clone(),
11615                request_sha256,
11616                temporary_ref: temporary_ref.clone(),
11617                prepared_dispatch_commit: None,
11618                dispatch_commit: None,
11619                workflow_dispatch: None,
11620                created_at_ms,
11621                publication_started_at_ms: 0,
11622                publication_quiescence_deadline_ms: 0,
11623                state: JobState::Created,
11624                run: None,
11625                run_snapshot: None,
11626                publication_intent: false,
11627                publication_absent: false,
11628                publication_not_attempted: false,
11629                publication_process_fenced: false,
11630                publication_lease_scope_sha256: None,
11631                publication_takeover_lease_owned: false,
11632                snapshot_source_lease_owned: false,
11633                publication_absence_observations: 0,
11634                publication_absence_first_observed_at_ms: 0,
11635                cancellation_requested: false,
11636                cancellation_dispatched: false,
11637                cleanup_requested: false,
11638                remove_artifacts_requested: false,
11639                artifacts_removed: false,
11640                temporary_ref_deleted: false,
11641                verification_pending_event: false,
11642                publication_uncertain: false,
11643                run_discovery_attempts: 0,
11644                run_discovery_deadline_ms: discovery_deadline_ms,
11645                manifests: Vec::new(),
11646                compile_evidence: None,
11647                signed_cleanup_evidence: None,
11648                events: Vec::new(),
11649            };
11650            append_event(
11651                &mut record,
11652                created_at_ms,
11653                "submit",
11654                RemoteBuildEventKind::OperationStarted {
11655                    command: "build-iphone".to_owned(),
11656                },
11657            )?;
11658            append_event(
11659                &mut record,
11660                created_at_ms,
11661                "submit",
11662                RemoteBuildEventKind::JobCreated {
11663                    state: JobState::Created,
11664                },
11665            )?;
11666            let mut pending = reservation.insert_pending(record)?;
11667            if request.signing.mode == SigningMode::ManualDevelopment {
11668                let source_repository = repository_from_url(&self.config.source_repository)
11669                    .ok_or_else(|| {
11670                        provider_failure(
11671                            "source_repository_invalid",
11672                            "configured source repository identity is invalid",
11673                            false,
11674                        )
11675                    })?;
11676                let source = self
11677                    .lock_transport()?
11678                    .repository(&source_repository)
11679                    .map_err(|error| {
11680                        provider_failure(
11681                            "source_repository_visibility_unavailable",
11682                            transport_public_message(error),
11683                            true,
11684                        )
11685                    })?;
11686                if source.is_private() {
11687                    return Err(provider_failure(
11688                        "private_source_repository",
11689                        "signed builds require a public GitHub source repository",
11690                        false,
11691                    ));
11692                }
11693                let repository = self
11694                    .lock_transport()?
11695                    .repository(&self.config.repository)
11696                    .map_err(|error| {
11697                        provider_failure(
11698                            "signing_repository_visibility_unavailable",
11699                            transport_public_message(error),
11700                            true,
11701                        )
11702                    })?;
11703                if !repository.is_private() {
11704                    return Err(provider_failure(
11705                        "public_signing_repository",
11706                        "signed artifacts require a private GitHub execution repository",
11707                        false,
11708                    ));
11709                }
11710                if repository.is_archived() || repository.is_disabled() {
11711                    return Err(provider_failure(
11712                        "signing_repository_read_only",
11713                        "the private GitHub signing repository cannot run new builds",
11714                        false,
11715                    ));
11716                }
11717                let mut signing_checks = Vec::new();
11718                let mut transport = self.lock_transport()?;
11719                append_signing_environment_checks(
11720                    &mut signing_checks,
11721                    &mut transport,
11722                    &self.config,
11723                );
11724                if signing_checks
11725                    .iter()
11726                    .any(|check| check.status != ProviderCheckStatus::Ready)
11727                {
11728                    return Err(provider_failure(
11729                        "signing_environment_not_ready",
11730                        "protected signing environment policy or secret metadata is not ready",
11731                        false,
11732                    ));
11733                }
11734                cancellation.check()?;
11735            }
11736            let publish_request = TemporaryRefPublishRequest {
11737                repository: &self.config.repository,
11738                source_repository: &self.config.source_repository,
11739                trusted_source_ref: self.config.workflow.trusted_source_ref(),
11740                source_revision: &source_revision,
11741                snapshot_source_ref: None,
11742                temporary_ref: &temporary_ref,
11743                workflow_path: workflow.path(),
11744                workflow_bytes: workflow.yaml().as_bytes(),
11745                workflow_fingerprint: &self.config.workflow_fingerprint,
11746                manifest_bytes: &manifest_bytes,
11747                operation_id: &request.operation_id,
11748                created_at_ms,
11749                authorized: self.config.mutation_authorization.publish_temporary_ref,
11750            };
11751            let prepared = {
11752                let mut publisher = self.lock_publisher()?;
11753                let prepared = publisher
11754                    .prepare(&publish_request)
11755                    .map_err(publisher_failure)?;
11756                if prepared
11757                    .as_ref()
11758                    .is_some_and(|publication| publication.temporary_ref() != &temporary_ref)
11759                {
11760                    return Err(publisher_failure(
11761                        TemporaryRefPublishError::PublicationVerificationFailed,
11762                    ));
11763                }
11764                prepared
11765            };
11766            let (
11767                publication_process_fenced,
11768                publication_lease_scope_sha256,
11769                publication_lease_owned,
11770            ) = {
11771                let mut publisher = self.lock_publisher()?;
11772                let lease_request = TemporaryRefPublicationLeaseRequest {
11773                    repository: &self.config.repository,
11774                    operation_id: &request.operation_id,
11775                    temporary_ref: &temporary_ref,
11776                };
11777                let lease = publisher
11778                    .acquire_publication_lease(&lease_request)
11779                    .map_err(publisher_failure)?;
11780                match lease {
11781                    TemporaryRefPublicationLease::Acquired => {
11782                        let scope = publisher
11783                            .publication_lease_scope_sha256()
11784                            .filter(|scope| is_lower_sha256(scope))
11785                            .map(str::to_owned);
11786                        let Some(scope) = scope else {
11787                            publisher.release_publication_lease(&lease_request);
11788                            return Err(publisher_failure(
11789                                TemporaryRefPublishError::LocalIsolationFailed,
11790                            ));
11791                        };
11792                        (
11793                            publisher.publication_attempt_is_process_fenced(),
11794                            Some(scope),
11795                            true,
11796                        )
11797                    }
11798                    TemporaryRefPublicationLease::Unsupported => (false, None, false),
11799                    TemporaryRefPublicationLease::AlreadyOwned
11800                    | TemporaryRefPublicationLease::HeldByOther => {
11801                        return Err(provider_failure(
11802                            "publication_owner_active",
11803                            "Another live provider owns temporary-ref publication",
11804                            true,
11805                        ));
11806                    }
11807                }
11808            };
11809            let mut state = self.lock_state()?;
11810            let record = state.jobs.get_mut(&job_id).ok_or_else(|| {
11811                provider_failure(
11812                    "pending_job_lost",
11813                    "pending GitHub job disappeared before publication",
11814                    false,
11815                )
11816            })?;
11817            record.prepared_dispatch_commit = prepared
11818                .as_ref()
11819                .map(|publication| publication.commit().clone());
11820            record.publication_started_at_ms = 0;
11821            record.publication_quiescence_deadline_ms = u64::MAX;
11822            record.publication_intent = true;
11823            record.publication_process_fenced = publication_process_fenced;
11824            record.publication_lease_scope_sha256 = publication_lease_scope_sha256;
11825            record.publication_takeover_lease_owned = publication_lease_owned;
11826            if self.checkpoint_sink.is_some() {
11827                pending.retain();
11828            }
11829            self.checkpoint_record(record)?;
11830            let prepared_dispatch_commit = record.prepared_dispatch_commit.clone();
11831            drop(state);
11832            let publication_identity = if resume_identity.is_some() {
11833                self.current_resume_identity().map(Some)
11834            } else {
11835                Ok(None)
11836            };
11837            let boundary_cancellation = cancellation.check();
11838            let mut state = self.lock_state()?;
11839            let record = state.jobs.get_mut(&job_id).ok_or_else(|| {
11840                provider_failure(
11841                    "pending_job_lost",
11842                    "pending GitHub job disappeared before publication",
11843                    false,
11844                )
11845            })?;
11846            let publication_boundary = publication_identity
11847                .and_then(|identity| boundary_cancellation.map(|()| identity))
11848                .and_then(|identity| {
11849                    identity.as_ref().map_or(Ok(()), |identity| {
11850                        Self::validate_record_identity(record, identity)
11851                    })
11852                })
11853                .and_then(|()| {
11854                    if record.prepared_dispatch_commit != prepared_dispatch_commit
11855                        || !record.publication_intent
11856                        || record.publication_uncertain
11857                        || record.dispatch_commit.is_some()
11858                    {
11859                        return Err(resume_failure("publication_identity_changed_before_push"));
11860                    }
11861                    if record.cancellation_requested {
11862                        return Err(provider_failure(
11863                            "publication_cancelled_before_mutation",
11864                            "GitHub publication was cancelled before any remote ref mutation",
11865                            false,
11866                        ));
11867                    }
11868                    Ok(())
11869                });
11870            if let Err(error) = publication_boundary {
11871                mark_publication_not_attempted(
11872                    record,
11873                    self.clock.now_ms(),
11874                    "publication_identity_revalidation_failed",
11875                    "GitHub identity changed before temporary-ref publication began",
11876                    true,
11877                )?;
11878                self.checkpoint_record(record)?;
11879                let release_publication_lease = record.publication_takeover_lease_owned;
11880                record.publication_takeover_lease_owned = false;
11881                drop(state);
11882                if release_publication_lease {
11883                    self.lock_publisher()?.release_publication_lease(
11884                        &TemporaryRefPublicationLeaseRequest {
11885                            repository: &self.config.repository,
11886                            operation_id: &request.operation_id,
11887                            temporary_ref: &temporary_ref,
11888                        },
11889                    );
11890                }
11891                return Err(error);
11892            }
11893            record.publication_started_at_ms = self.clock.now_ms();
11894            record.publication_quiescence_deadline_ms =
11895                publication_quiescence_deadline(record.publication_started_at_ms, 0);
11896            self.checkpoint_record(record)?;
11897            drop(state);
11898            let publish_result = self
11899                .lock_publisher()?
11900                .publish_prepared(&publish_request, prepared.as_ref());
11901            let (published, publication_uncertain) = match publish_result {
11902                Ok(published) => (published, false),
11903                Err(failure) => {
11904                    if let Some(publication) = failure.possible_publication().cloned() {
11905                        (publication, true)
11906                    } else {
11907                        let error = publisher_failure(failure.error());
11908                        let mut state = self.lock_state()?;
11909                        let record = state.jobs.get_mut(&job_id).ok_or_else(|| {
11910                            provider_failure(
11911                                "pending_job_lost",
11912                                "pending GitHub job disappeared after publication failure",
11913                                false,
11914                            )
11915                        })?;
11916                        mark_publication_not_attempted(
11917                            record,
11918                            self.clock.now_ms(),
11919                            "temporary_ref_publication_not_attempted",
11920                            "Temporary-ref publication failed before any remote mutation",
11921                            false,
11922                        )?;
11923                        self.checkpoint_record(record)?;
11924                        self.release_record_publication_lease(record)?;
11925                        return Err(error);
11926                    }
11927                }
11928            };
11929            pending.retain();
11930            let publisher_binding_mismatch = published.temporary_ref() != &temporary_ref
11931                || prepared
11932                    .as_ref()
11933                    .is_some_and(|expected| expected.commit() != published.commit());
11934            let mut state = self.lock_state()?;
11935            let record = state.jobs.get_mut(&job_id).ok_or_else(|| {
11936                provider_failure(
11937                    "pending_job_lost",
11938                    "pending GitHub job disappeared during publication",
11939                    false,
11940                )
11941            })?;
11942            record.dispatch_commit = (!publisher_binding_mismatch && !publication_uncertain)
11943                .then(|| published.commit().clone());
11944            record.publication_uncertain = publication_uncertain || publisher_binding_mismatch;
11945            self.checkpoint_record(record)?;
11946            if publisher_binding_mismatch {
11947                transition_record(record, JobState::Failed)?;
11948                append_event(
11949                    record,
11950                    self.clock.now_ms(),
11951                    "cleanup",
11952                    RemoteBuildEventKind::Warning {
11953                        code: "github.cleanup_required".to_owned(),
11954                        message: "Temporary-ref publisher violated its prepared ref/commit binding; automatic adoption was refused"
11955                            .to_owned(),
11956                        help: Some("Inspect the alternate publisher and remote ref manually".to_owned()),
11957                    },
11958                )?;
11959                append_failed(
11960                    record,
11961                    self.clock.now_ms(),
11962                    "publisher_binding_mismatch",
11963                    "Temporary-ref publisher returned a different ref or commit after possible mutation",
11964                    false,
11965                )?;
11966                self.checkpoint_record(record)?;
11967                self.release_record_publication_lease(record)?;
11968                return Ok(JobHandle {
11969                    job_id,
11970                    state: JobState::Failed,
11971                    created_at_ms,
11972                });
11973            }
11974            if publication_uncertain {
11975                append_event(
11976                    record,
11977                    self.clock.now_ms(),
11978                    "submit",
11979                    RemoteBuildEventKind::Warning {
11980                        code: "github.temporary_ref_publication_uncertain".to_owned(),
11981                        message: "Temporary ref push may have succeeded; cleanup ownership was retained"
11982                            .to_owned(),
11983                        help: Some(
11984                            "Continue polling this job or run exact-ref cleanup after it becomes terminal"
11985                                .to_owned(),
11986                        ),
11987                    },
11988                )?;
11989            }
11990            if !record.publication_uncertain
11991                && self.config.workflow.run_trigger() == WorkflowRunTrigger::Push
11992            {
11993                if record.state == JobState::Created {
11994                    transition_record(record, JobState::Queued)?;
11995                }
11996                append_event(
11997                    record,
11998                    created_at_ms,
11999                    "queue",
12000                    RemoteBuildEventKind::JobQueued { position: None },
12001                )?;
12002            }
12003            record.cancellation_requested |= cancellation.is_cancelled();
12004            if record.cancellation_requested && record.state != JobState::Cancelling {
12005                transition_record(record, JobState::Cancelling)?;
12006            }
12007            self.checkpoint_record(record)?;
12008            if !record.publication_uncertain {
12009                self.release_record_publication_lease(record)?;
12010            }
12011            let start_dispatch = !record.publication_uncertain
12012                && self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch;
12013            drop(state);
12014            if start_dispatch {
12015                let client = workflow_dispatch_client.as_deref_mut().ok_or_else(|| {
12016                    provider_failure(
12017                        "workflow_dispatch_client_unavailable",
12018                        "Preflighted workflow dispatch client is unavailable",
12019                        true,
12020                    )
12021                })?;
12022                self.start_workflow_dispatch_with_client(&job_id, client, &cancellation)?;
12023            }
12024            let returned_state = self
12025                .lock_state()?
12026                .jobs
12027                .get(&job_id)
12028                .ok_or_else(|| job_not_found(&job_id))?
12029                .state;
12030            Ok(JobHandle {
12031                job_id,
12032                state: returned_state,
12033                created_at_ms,
12034            })
12035        })();
12036        Box::pin(async move { result })
12037    }
12038
12039    fn events(
12040        &self,
12041        request: EventRequest,
12042        cancellation: CancellationToken,
12043    ) -> ProviderFuture<'_, EventPage> {
12044        let result = (|| {
12045            request.validate()?;
12046            cancellation.check()?;
12047            self.sync_job(&request.job_id, &cancellation)?;
12048            let state = self.lock_state()?;
12049            let record = state
12050                .jobs
12051                .get(&request.job_id)
12052                .ok_or_else(|| job_not_found(&request.job_id))?;
12053            let after = request.after_sequence.unwrap_or(0);
12054            let mut matching = record
12055                .events
12056                .iter()
12057                .filter(|event| event.sequence > after)
12058                .cloned();
12059            let events = matching
12060                .by_ref()
12061                .take(request.limit as usize)
12062                .collect::<Vec<_>>();
12063            let truncated = matching.next().is_some();
12064            let next_sequence = events.last().map(|event| event.sequence);
12065            Ok(EventPage {
12066                job_id: record.job_id.clone(),
12067                state: record.state,
12068                events,
12069                next_sequence,
12070                complete: record.state.is_terminal() && !truncated,
12071            })
12072        })();
12073        Box::pin(async move { result })
12074    }
12075
12076    #[allow(clippy::too_many_lines)]
12077    fn cancel(
12078        &self,
12079        request: CancellationRequest,
12080        cancellation: CancellationToken,
12081    ) -> ProviderFuture<'_, CancellationAck> {
12082        let result = (|| {
12083            cancellation.check()?;
12084            validate_provider_identifier("job_id", &request.job_id)?;
12085            validate_provider_identifier("cancellation_reason", &request.reason)?;
12086            if !self.config.mutation_authorization.cancel_run {
12087                return Err(unsupported(ProviderFeature::Cancellation));
12088            }
12089            if self.config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch {
12090                let accepted = {
12091                    let mut state = self.lock_state()?;
12092                    let record = state
12093                        .jobs
12094                        .get_mut(&request.job_id)
12095                        .ok_or_else(|| job_not_found(&request.job_id))?;
12096                    if !record.publication_intent {
12097                        return Err(provider_failure(
12098                            "job_submission_pending",
12099                            "GitHub job has not reached its durable publication boundary",
12100                            true,
12101                        ));
12102                    }
12103                    if record.state.is_build_terminal() || record.state.is_terminal() {
12104                        self.checkpoint_record(record)?;
12105                        return Ok(CancellationAck {
12106                            job_id: record.job_id.clone(),
12107                            accepted: false,
12108                            state: record.state,
12109                        });
12110                    }
12111                    let accepted = !record.cancellation_requested;
12112                    record.cancellation_requested = true;
12113                    if record.state != JobState::Cancelling {
12114                        transition_record(record, JobState::Cancelling)?;
12115                    }
12116                    self.checkpoint_record(record)?;
12117                    accepted
12118                };
12119                self.sync_job_with_cancel_policy(&request.job_id, &CancellationToken::new(), true)?;
12120                let state = self.lock_state()?;
12121                let record = state
12122                    .jobs
12123                    .get(&request.job_id)
12124                    .ok_or_else(|| job_not_found(&request.job_id))?;
12125                return Ok(CancellationAck {
12126                    job_id: record.job_id.clone(),
12127                    accepted,
12128                    state: record.state,
12129                });
12130            }
12131            let mut state = self.lock_state()?;
12132            let record = state
12133                .jobs
12134                .get_mut(&request.job_id)
12135                .ok_or_else(|| job_not_found(&request.job_id))?;
12136            if !record.publication_intent {
12137                return Err(provider_failure(
12138                    "job_submission_pending",
12139                    "GitHub job has not reached its durable publication boundary",
12140                    true,
12141                ));
12142            }
12143            if record.state.is_build_terminal() || record.state.is_terminal() {
12144                self.checkpoint_record(record)?;
12145                return Ok(CancellationAck {
12146                    job_id: record.job_id.clone(),
12147                    accepted: false,
12148                    state: record.state,
12149                });
12150            }
12151            let accepted = !record.cancellation_requested;
12152            record.cancellation_requested = true;
12153            if record.state != JobState::Cancelling {
12154                transition_record(record, JobState::Cancelling)?;
12155            }
12156            self.checkpoint_record(record)?;
12157            if let Some(handle) = record.run.clone()
12158                && !record.cancellation_dispatched
12159            {
12160                self.revalidate_record_identity(record)?;
12161                let snapshot = self
12162                    .lock_transport()?
12163                    .run(&self.config.repository, &handle)
12164                    .map_err(transport_failure)?;
12165                ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot)?;
12166                record.run_snapshot = Some(snapshot.clone());
12167                if snapshot.status().is_terminal() {
12168                    self.finish_from_snapshot(record, &snapshot)?;
12169                } else {
12170                    self.checkpoint_record(record)?;
12171                    let checked = self
12172                        .lock_transport()?
12173                        .run(&self.config.repository, &handle)
12174                        .map_err(transport_failure)?;
12175                    ensure_same_run_attempt(record.run_snapshot.as_ref(), &checked)?;
12176                    record.run_snapshot = Some(checked.clone());
12177                    if checked.status().is_terminal() {
12178                        self.finish_from_snapshot(record, &checked)?;
12179                    } else {
12180                        self.revalidate_record_identity(record)?;
12181                        // GitHub cancellation is run-ID-wide; the exact GET above rejects drift
12182                        // before POST but cannot turn the endpoint into an attempt lease.
12183                        self.lock_transport()?
12184                            .cancel_run(&self.config.repository, handle.id())
12185                            .map_err(transport_failure)?;
12186                        record.cancellation_dispatched = true;
12187                        self.checkpoint_record(record)?;
12188                    }
12189                }
12190            }
12191            self.checkpoint_record(record)?;
12192            Ok(CancellationAck {
12193                job_id: record.job_id.clone(),
12194                accepted,
12195                state: record.state,
12196            })
12197        })();
12198        Box::pin(async move { result })
12199    }
12200
12201    fn list_artifacts(
12202        &self,
12203        request: ArtifactListRequest,
12204        cancellation: CancellationToken,
12205    ) -> ProviderFuture<'_, Vec<ArtifactManifest>> {
12206        let result = (|| {
12207            cancellation.check()?;
12208            validate_provider_identifier("job_id", &request.job_id)?;
12209            if !self.capabilities.artifact_listing {
12210                return Err(unsupported(ProviderFeature::ArtifactListing));
12211            }
12212            self.sync_job(&request.job_id, &cancellation)?;
12213            let state = self.lock_state()?;
12214            let record = state
12215                .jobs
12216                .get(&request.job_id)
12217                .ok_or_else(|| job_not_found(&request.job_id))?;
12218            if !record.manifests.is_empty() {
12219                return Ok(record.manifests.clone());
12220            }
12221            let context = Self::artifact_context(record, &self.config).ok_or_else(|| {
12222                provider_failure(
12223                    "artifacts_pending",
12224                    "GitHub run has not completed artifact production",
12225                    true,
12226                )
12227            })?;
12228            drop(state);
12229            let manifests = self.lock_artifacts()?.list_verified(&context)?;
12230            validate_manifests_for_context(&context, &manifests)?;
12231            Ok(manifests)
12232        })();
12233        Box::pin(async move { result })
12234    }
12235
12236    fn download_artifact(
12237        &self,
12238        request: ArtifactDownloadRequest,
12239        cancellation: CancellationToken,
12240    ) -> ProviderFuture<'_, ArtifactDownloadResult> {
12241        let result = (|| {
12242            request.validate()?;
12243            cancellation.check()?;
12244            if !self.capabilities.artifact_download {
12245                return Err(unsupported(ProviderFeature::ArtifactDownload));
12246            }
12247            self.sync_job(&request.job_id, &cancellation)?;
12248            let state = self.lock_state()?;
12249            let record = state
12250                .jobs
12251                .get(&request.job_id)
12252                .ok_or_else(|| job_not_found(&request.job_id))?;
12253            let context = Self::artifact_context(record, &self.config).ok_or_else(|| {
12254                provider_failure(
12255                    "artifact_pending",
12256                    "GitHub run has not completed artifact production",
12257                    true,
12258                )
12259            })?;
12260            drop(state);
12261            let result = self
12262                .lock_artifacts()?
12263                .download_verified(&context, &request)?;
12264            if result.manifest.operation_id != context.operation_id
12265                || result.manifest.job_id != context.job_id
12266                || result.local_path != request.destination
12267            {
12268                return Err(provider_failure(
12269                    "artifact_identity_mismatch",
12270                    "verified artifact result does not match the exact request",
12271                    false,
12272                ));
12273            }
12274            Ok(result)
12275        })();
12276        Box::pin(async move { result })
12277    }
12278
12279    #[allow(clippy::too_many_lines)]
12280    fn cleanup(
12281        &self,
12282        request: CleanupRequest,
12283        cancellation: CancellationToken,
12284    ) -> ProviderFuture<'_, CleanupConfirmation> {
12285        let result = (|| {
12286            cancellation.check()?;
12287            validate_provider_identifier("job_id", &request.job_id)?;
12288            if !self.config.mutation_authorization.delete_temporary_ref {
12289                return Err(unsupported(ProviderFeature::Cleanup));
12290            }
12291            let mut state = self.lock_state()?;
12292            let record = state
12293                .jobs
12294                .get_mut(&request.job_id)
12295                .ok_or_else(|| job_not_found(&request.job_id))?;
12296            let terminal_run = record
12297                .run_snapshot
12298                .as_ref()
12299                .is_some_and(|snapshot| snapshot.status().is_terminal());
12300            if !record.state.is_build_terminal() && !record.state.is_terminal() && terminal_run {
12301                ensure_running_or_cancelling(record)?;
12302                transition_record(record, JobState::Failed)?;
12303                append_failed(
12304                    record,
12305                    self.clock.now_ms(),
12306                    "artifact_verification_failed",
12307                    "GitHub Actions completed, but independent artifact verification did not complete",
12308                    false,
12309                )?;
12310            }
12311            if !record.state.is_build_terminal()
12312                && !matches!(record.state, JobState::Cleaned | JobState::CleanupFailed)
12313            {
12314                return Err(provider_failure(
12315                    "cleanup_before_terminal",
12316                    "GitHub job must reach a terminal run state before cleanup",
12317                    true,
12318                ));
12319            }
12320            let signed_cleanup_proven = self.has_valid_signed_cleanup_evidence(record)?;
12321            if record.state == JobState::Cleaned {
12322                self.checkpoint_record(record)?;
12323                return Err(provider_failure(
12324                    "cleanup_already_attempted",
12325                    "GitHub job cleanup was already attempted",
12326                    false,
12327                ));
12328            }
12329            if record.cleanup_requested
12330                && record.remove_artifacts_requested != request.remove_artifacts
12331            {
12332                return Err(provider_failure(
12333                    "cleanup_resume_request_mismatch",
12334                    "cleanup retry differs from its durable artifact-removal intent",
12335                    false,
12336                ));
12337            }
12338            if request.remove_artifacts && !record.artifacts_removed {
12339                Self::artifact_context(record, &self.config).ok_or_else(|| {
12340                    provider_failure(
12341                        "artifact_identity_unavailable",
12342                        "exact run identity is unavailable for artifact removal",
12343                        false,
12344                    )
12345                })?;
12346                if !self.lock_artifacts()?.supports_removal() {
12347                    return Err(provider_failure(
12348                        "artifact_removal_unsupported",
12349                        "verified artifact removal is not configured",
12350                        false,
12351                    ));
12352                }
12353            }
12354            if let Some(handle) = record.run.clone() {
12355                self.revalidate_record_identity(record)?;
12356                let snapshot = self
12357                    .lock_transport()?
12358                    .run(&self.config.repository, &handle)
12359                    .map_err(transport_failure)?;
12360                match ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot) {
12361                    Ok(()) => {
12362                        self.revalidate_record_identity(record)?;
12363                        if !snapshot.status().is_terminal() {
12364                            return Err(resume_failure("cleanup_live_run_not_terminal"));
12365                        }
12366                        record.run_snapshot = Some(snapshot);
12367                    }
12368                    Err(_)
12369                        if signed_cleanup_proven
12370                            && record.run_snapshot.as_ref().is_some_and(|previous| {
12371                                github_run_is_later_attempt(previous, &snapshot)
12372                            }) =>
12373                    {
12374                        self.revalidate_record_identity(record)?;
12375                    }
12376                    Err(error) => return Err(error),
12377                }
12378            }
12379            let context = Self::artifact_context(record, &self.config);
12380            record.cleanup_requested = true;
12381            record.remove_artifacts_requested = request.remove_artifacts;
12382            self.checkpoint_record(record)?;
12383            if let Some(handle) = record.run.clone() {
12384                let snapshot = self
12385                    .lock_transport()?
12386                    .run(&self.config.repository, &handle)
12387                    .map_err(transport_failure)?;
12388                match ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot) {
12389                    Ok(()) => {
12390                        self.revalidate_record_identity(record)?;
12391                        if !snapshot.status().is_terminal() {
12392                            return Err(resume_failure("cleanup_live_run_not_terminal"));
12393                        }
12394                        record.run_snapshot = Some(snapshot);
12395                    }
12396                    Err(_)
12397                        if signed_cleanup_proven
12398                            && record.run_snapshot.as_ref().is_some_and(|previous| {
12399                                github_run_is_later_attempt(previous, &snapshot)
12400                            }) =>
12401                    {
12402                        self.revalidate_record_identity(record)?;
12403                    }
12404                    Err(error) => return Err(error),
12405                }
12406            }
12407            if record.remove_artifacts_requested && !record.artifacts_removed {
12408                let context = context.as_ref().ok_or_else(|| {
12409                    provider_failure(
12410                        "artifact_identity_unavailable",
12411                        "exact run identity is unavailable for artifact removal",
12412                        false,
12413                    )
12414                })?;
12415                self.revalidate_record_identity(record)?;
12416                self.lock_artifacts()?.remove_artifacts(context)?;
12417                record.artifacts_removed = true;
12418                self.checkpoint_record(record)?;
12419            }
12420            self.cleanup_git_snapshot_source(record)?;
12421            if record.publication_intent
12422                && !record.temporary_ref_deleted
12423                && !record.publication_not_attempted
12424            {
12425                let expected_commit = record.dispatch_commit.as_ref().ok_or_else(|| {
12426                    provider_failure(
12427                        "dispatch_commit_unavailable",
12428                        "exact dispatch commit is unavailable for safe ref cleanup",
12429                        false,
12430                    )
12431                })?;
12432                self.revalidate_record_identity(record)?;
12433                self.lock_publisher()?
12434                    .delete_temporary_ref(&TemporaryRefDeleteRequest {
12435                        repository: &self.config.repository,
12436                        source_repository: &self.config.source_repository,
12437                        temporary_ref: &record.temporary_ref,
12438                        expected_commit,
12439                        authorized: self.config.mutation_authorization.delete_temporary_ref,
12440                    })
12441                    .map_err(publisher_failure)?;
12442                record.temporary_ref_deleted = true;
12443                self.checkpoint_record(record)?;
12444            }
12445            if let Some(handle) = record.run.clone() {
12446                let snapshot = self
12447                    .lock_transport()?
12448                    .run(&self.config.repository, &handle)
12449                    .map_err(transport_failure)?;
12450                match ensure_same_run_attempt(record.run_snapshot.as_ref(), &snapshot) {
12451                    Ok(()) => {
12452                        self.revalidate_record_identity(record)?;
12453                        if !snapshot.status().is_terminal() {
12454                            return Err(provider_failure(
12455                                "cleanup_run_reactivated_after_mutation",
12456                                "GitHub run became active during cleanup",
12457                                true,
12458                            ));
12459                        }
12460                        record.run_snapshot = Some(snapshot);
12461                    }
12462                    Err(_)
12463                        if signed_cleanup_proven
12464                            && record.run_snapshot.as_ref().is_some_and(|previous| {
12465                                github_run_is_later_attempt(previous, &snapshot)
12466                            }) =>
12467                    {
12468                        self.revalidate_record_identity(record)?;
12469                    }
12470                    Err(_) => {
12471                        return Err(provider_failure(
12472                            "cleanup_run_changed_after_mutation",
12473                            "GitHub run attempt changed during cleanup",
12474                            true,
12475                        ));
12476                    }
12477                }
12478            }
12479            transition_record(record, JobState::Cleaning)?;
12480            append_event(
12481                record,
12482                self.clock.now_ms(),
12483                "cleanup",
12484                RemoteBuildEventKind::CleanupStarted,
12485            )?;
12486
12487            let run_completed = record
12488                .run_snapshot
12489                .as_ref()
12490                .is_some_and(|snapshot| snapshot.status().is_terminal());
12491            let signing_material_removed = record.request.signing.mode
12492                == SigningMode::UnsignedCompileOnly
12493                || signed_cleanup_proven;
12494            let confirmation = CleanupConfirmation {
12495                job_id: record.job_id.clone(),
12496                completed_at_ms: self.clock.now_ms(),
12497                workspace_removed: run_completed,
12498                signing_material_removed,
12499                artifacts_retained: !request.remove_artifacts,
12500            };
12501            let cleanup_state = if run_completed && signing_material_removed {
12502                JobState::Cleaned
12503            } else {
12504                JobState::CleanupFailed
12505            };
12506            transition_record(record, cleanup_state)?;
12507            append_event(
12508                record,
12509                self.clock.now_ms(),
12510                "cleanup",
12511                RemoteBuildEventKind::CleanupFinished {
12512                    confirmation: confirmation.clone(),
12513                },
12514            )?;
12515            self.checkpoint_record(record)?;
12516            Ok(confirmation)
12517        })();
12518        Box::pin(async move { result })
12519    }
12520}
12521
12522fn provider_capabilities(
12523    config: &GithubProviderConfig,
12524    artifacts: &impl VerifiedArtifactStore,
12525    snapshot_publisher_supported: bool,
12526) -> ProviderCapabilities {
12527    let mut source_modes = BTreeSet::from([SourceMode::Git]);
12528    let authorization = config.mutation_authorization;
12529    if snapshot_publisher_supported
12530        && authorization.publish_temporary_ref
12531        && authorization.delete_temporary_ref
12532        && authorization.publish_git_snapshot_source
12533        && authorization.delete_git_snapshot_source
12534        && authorization.manage_git_snapshot_keepalive
12535    {
12536        source_modes.insert(SourceMode::GitSnapshot);
12537    }
12538    ProviderCapabilities {
12539        source_modes,
12540        ios_device_build: true,
12541        ios_simulator_build: false,
12542        signing_modes: BTreeSet::from([
12543            SigningMode::ManualDevelopment,
12544            SigningMode::UnsignedCompileOnly,
12545        ]),
12546        personal_team: false,
12547        live_events: true,
12548        live_logs: false,
12549        cancellation: config.mutation_authorization.cancel_run,
12550        artifact_types: BTreeSet::from([
12551            IosArtifactType::Ipa,
12552            IosArtifactType::AppBundle,
12553            IosArtifactType::Dsym,
12554            IosArtifactType::SigningReport,
12555            IosArtifactType::Xcarchive,
12556        ]),
12557        cache: true,
12558        max_source_bytes: None,
12559        retention_seconds: Some(
12560            u64::from(config.workflow.limits().retention_days()) * 24 * 60 * 60,
12561        ),
12562        artifact_listing: artifacts.supports_listing(),
12563        artifact_download: artifacts.supports_download(),
12564        cleanup: config.mutation_authorization.delete_temporary_ref,
12565        physical_device_access: false,
12566    }
12567}
12568
12569fn lock_provider_state(
12570    state: &Mutex<ProviderState>,
12571) -> RemoteBuildResult<std::sync::MutexGuard<'_, ProviderState>> {
12572    state.lock().map_err(|_| {
12573        provider_failure(
12574            "state_unavailable",
12575            "GitHub provider state is unavailable",
12576            true,
12577        )
12578    })
12579}
12580
12581fn append_signing_environment_checks<R: GhRunner>(
12582    checks: &mut Vec<ProviderCheck>,
12583    transport: &mut GithubTransport<R>,
12584    config: &GithubProviderConfig,
12585) {
12586    append_signing_environment_policy_checks(checks, transport, config);
12587    append_signing_branch_policy_check(checks, transport, config);
12588    append_signing_secret_check(checks, transport, config);
12589}
12590
12591fn append_signing_environment_policy_checks<R: GhRunner>(
12592    checks: &mut Vec<ProviderCheck>,
12593    transport: &mut GithubTransport<R>,
12594    config: &GithubProviderConfig,
12595) {
12596    match transport.environment(
12597        config.repository(),
12598        config.workflow().protected_environment(),
12599    ) {
12600        Ok(info) => {
12601            checks.push(if !info.protected_branches() && info.custom_branch_policies() {
12602                ProviderCheck {
12603                    code: "github.signing_environment".to_owned(),
12604                    status: ProviderCheckStatus::Ready,
12605                    message: "Signing environment requires custom deployment branch policies"
12606                        .to_owned(),
12607                    help: None,
12608                }
12609            } else {
12610                ProviderCheck {
12611                    code: "github.signing_environment".to_owned(),
12612                    status: ProviderCheckStatus::Error,
12613                    message: "Signing environment does not exclusively use custom deployment branch policies"
12614                        .to_owned(),
12615                    help: Some(
12616                        "Disable protected-branch selection and enable custom branch policies"
12617                            .to_owned(),
12618                    ),
12619                }
12620            });
12621            checks.push(if info.has_required_reviewers() {
12622                ProviderCheck {
12623                    code: "github.signing_environment.reviewers".to_owned(),
12624                    status: ProviderCheckStatus::Ready,
12625                    message: "Signing environment requires a deployment reviewer".to_owned(),
12626                    help: None,
12627                }
12628            } else {
12629                ProviderCheck {
12630                    code: "github.signing_environment.reviewers".to_owned(),
12631                    status: ProviderCheckStatus::Error,
12632                    message: "Signing environment has no required deployment reviewer".to_owned(),
12633                    help: Some(
12634                        "Require server-side deployment approval until signing uses a trusted SHA-pinned reusable workflow"
12635                            .to_owned(),
12636                    ),
12637                }
12638            });
12639        }
12640        Err(error) => checks.push(ProviderCheck {
12641            code: "github.signing_environment".to_owned(),
12642            status: ProviderCheckStatus::Error,
12643            message: transport_public_message(error),
12644            help: Some("Create the exact configured GitHub signing environment".to_owned()),
12645        }),
12646    }
12647}
12648
12649fn append_signing_branch_policy_check<R: GhRunner>(
12650    checks: &mut Vec<ProviderCheck>,
12651    transport: &mut GithubTransport<R>,
12652    config: &GithubProviderConfig,
12653) {
12654    let workflow = config.workflow();
12655    let expected_policy = format!("{}/*", workflow.temporary_branch_namespace().as_str());
12656    match transport.list_deployment_branch_policies(
12657        config.repository(),
12658        workflow.protected_environment(),
12659    ) {
12660        Ok(policies) if policies.len() == 1 && policies[0].name() == expected_policy.as_str() => {
12661            checks.push(ProviderCheck {
12662                code: "github.signing_branch_policy".to_owned(),
12663                status: ProviderCheckStatus::Ready,
12664                message: "Signing environment allows only the temporary build namespace"
12665                    .to_owned(),
12666                help: None,
12667            });
12668        }
12669        Ok(_) => checks.push(ProviderCheck {
12670            code: "github.signing_branch_policy".to_owned(),
12671            status: ProviderCheckStatus::Error,
12672            message: "Signing environment deployment branch policies do not exactly match the temporary build namespace"
12673                .to_owned(),
12674            help: Some(format!("Configure the single deployment branch policy `{expected_policy}`")),
12675        }),
12676        Err(error) => checks.push(ProviderCheck {
12677            code: "github.signing_branch_policy".to_owned(),
12678            status: ProviderCheckStatus::Error,
12679            message: transport_public_message(error),
12680            help: Some(format!("Configure the single deployment branch policy `{expected_policy}`")),
12681        }),
12682    }
12683}
12684
12685fn append_signing_secret_check<R: GhRunner>(
12686    checks: &mut Vec<ProviderCheck>,
12687    transport: &mut GithubTransport<R>,
12688    config: &GithubProviderConfig,
12689) {
12690    let workflow = config.workflow();
12691    let secret_names = workflow.secret_names();
12692    let expected_secrets = secret_names.all_names().cloned().collect::<BTreeSet<_>>();
12693    match transport.list_environment_secrets(config.repository(), workflow.protected_environment())
12694    {
12695        Ok(secrets)
12696            if secrets
12697                .iter()
12698                .map(|secret| secret.name().clone())
12699                .collect::<BTreeSet<_>>()
12700                == expected_secrets =>
12701        {
12702            checks.push(ProviderCheck {
12703                code: "github.signing_secrets".to_owned(),
12704                status: ProviderCheckStatus::Ready,
12705                message: "Signing environment contains exactly the configured signing secret names"
12706                    .to_owned(),
12707                help: None,
12708            });
12709        }
12710        Ok(_) => checks.push(ProviderCheck {
12711            code: "github.signing_secrets".to_owned(),
12712            status: ProviderCheckStatus::Error,
12713            message:
12714                "Signing environment secret metadata does not match the configured signing roles"
12715                    .to_owned(),
12716            help: Some(
12717                "Configure the certificate, password, and provisioning-profile environment secrets"
12718                    .to_owned(),
12719            ),
12720        }),
12721        Err(error) => checks.push(ProviderCheck {
12722            code: "github.signing_secrets".to_owned(),
12723            status: ProviderCheckStatus::Error,
12724            message: transport_public_message(error),
12725            help: Some(
12726                "Grant environment-secret metadata access and configure all signing roles"
12727                    .to_owned(),
12728            ),
12729        }),
12730    }
12731}
12732
12733fn validate_submission(
12734    config: &GithubProviderConfig,
12735    request: &IosDeviceBuildRequest,
12736) -> RemoteBuildResult<()> {
12737    validate_provider_request(config, request)?;
12738    if request.source_mode != SourceMode::Git {
12739        return Err(provider_failure(
12740            "snapshot_submission_requires_consent",
12741            "Git snapshots require the explicit consent-bound submission path",
12742            false,
12743        ));
12744    }
12745    Ok(())
12746}
12747
12748fn validate_git_snapshot_submission(
12749    config: &GithubProviderConfig,
12750    request: &IosDeviceBuildRequest,
12751) -> RemoteBuildResult<()> {
12752    validate_provider_request(config, request)?;
12753    if request.source_mode != SourceMode::GitSnapshot
12754        || request.signing.mode != SigningMode::UnsignedCompileOnly
12755    {
12756        return Err(provider_failure(
12757            "unsupported_snapshot_request",
12758            "GitHub snapshots require an unsigned physical-iPhone XCArchive request",
12759            false,
12760        ));
12761    }
12762    let authorization = config.mutation_authorization;
12763    if !authorization.publish_temporary_ref
12764        || !authorization.delete_temporary_ref
12765        || !authorization.publish_git_snapshot_source
12766        || !authorization.delete_git_snapshot_source
12767        || !authorization.manage_git_snapshot_keepalive
12768    {
12769        return Err(provider_failure(
12770            "snapshot_mutation_unauthorized",
12771            "Git snapshot publication and cleanup authority are not configured",
12772            false,
12773        ));
12774    }
12775    Ok(())
12776}
12777
12778fn validate_provider_request(
12779    config: &GithubProviderConfig,
12780    request: &IosDeviceBuildRequest,
12781) -> RemoteBuildResult<()> {
12782    if request.signing.mode == SigningMode::ManualDevelopment
12783        && !(1..=MAX_SIGNING_PROFILES).contains(&request.signing.provisioning.len())
12784    {
12785        return Err(provider_failure(
12786            "profile_mapping_unsupported",
12787            "GitHub provider accepts one application profile and at most two extension profiles",
12788            false,
12789        ));
12790    }
12791    request.validate()?;
12792    if !matches!(
12793        request.source_mode,
12794        SourceMode::Git | SourceMode::GitSnapshot
12795    ) || request.source_repository.as_deref() != Some(config.source_repository())
12796        || request.source_revision.is_none()
12797    {
12798        return Err(provider_failure(
12799            "unsupported_source_request",
12800            "GitHub provider requires its exact configured Git source repository and commit",
12801            false,
12802        ));
12803    }
12804    match (request.source_mode, request.signing.mode) {
12805        (SourceMode::Git | SourceMode::GitSnapshot, SigningMode::UnsignedCompileOnly) => {
12806            validate_exact_artifact_request(
12807                request,
12808                &BTreeSet::from([IosArtifactType::Xcarchive]),
12809            )?;
12810        }
12811        (SourceMode::GitSnapshot, _) => {
12812            return Err(provider_failure(
12813                "snapshot_signing_unsupported",
12814                "GitHub snapshots support unsigned compilation only",
12815                false,
12816            ));
12817        }
12818        (SourceMode::Git, SigningMode::ManualDevelopment) => {
12819            if github_remote_matches(
12820                config.source_repository(),
12821                &repository_url(config.repository()),
12822            ) {
12823                return Err(provider_failure(
12824                    "separate_signing_repository_required",
12825                    "signed builds require a separate private execution repository",
12826                    false,
12827                ));
12828            }
12829            validate_signing_secret_references(config, request)?;
12830            validate_signed_artifact_request(request)?;
12831        }
12832        (_, mode) => return Err(unsupported(ProviderFeature::SigningMode(mode))),
12833    }
12834    validate_ref_segment(&request.operation_id).map_err(|()| {
12835        provider_failure(
12836            "operation_id_not_ref_safe",
12837            "operation ID cannot form a safe temporary Git ref",
12838            false,
12839        )
12840    })
12841}
12842
12843fn validate_signing_secret_references(
12844    config: &GithubProviderConfig,
12845    request: &IosDeviceBuildRequest,
12846) -> RemoteBuildResult<()> {
12847    let names = config.workflow.secret_names();
12848    let identity_references_match = request.signing.signing.as_ref().is_some_and(|signing| {
12849        exact_github_secret(
12850            &signing.identity.private_key.reference,
12851            names.certificate_p12().as_str(),
12852        ) && signing.password.as_ref().is_some_and(|password| {
12853            exact_github_secret(password, names.certificate_password().as_str())
12854        })
12855    });
12856    let profiles_match = if names.uses_legacy_profile_binding() {
12857        request.signing.provisioning.len() == 1
12858            && request.signing.provisioning.first().is_some_and(|profile| {
12859                exact_github_secret(&profile.profile, names.provisioning_profile().as_str())
12860            })
12861    } else {
12862        names.matches_target_graph(&request.signing.targets)
12863            && request.signing.provisioning.len() == names.profile_names().count()
12864            && request.signing.provisioning.iter().all(|profile| {
12865                names
12866                    .profile_for_target(&profile.target)
12867                    .is_some_and(|expected| {
12868                        exact_github_secret(&profile.profile, expected.as_str())
12869                    })
12870            })
12871    };
12872    if !identity_references_match || !profiles_match {
12873        return Err(provider_failure(
12874            "signing_secret_reference_mismatch",
12875            "signed GitHub builds require the exact configured GitHub Actions secret roles",
12876            false,
12877        ));
12878    }
12879    Ok(())
12880}
12881
12882fn exact_github_secret(reference: &SecretReference, expected_name: &str) -> bool {
12883    reference.kind() == SecretReferenceKind::GithubActions && reference.name() == expected_name
12884}
12885
12886fn validate_exact_artifact_request(
12887    request: &IosDeviceBuildRequest,
12888    expected: &BTreeSet<IosArtifactType>,
12889) -> RemoteBuildResult<()> {
12890    if request.requested_artifacts == *expected {
12891        return Ok(());
12892    }
12893    if let Some(extra) = request.requested_artifacts.difference(expected).next() {
12894        return Err(unsupported(ProviderFeature::ArtifactType(*extra)));
12895    }
12896    Err(provider_failure(
12897        "artifact_request_mismatch",
12898        "requested artifacts do not exactly match the selected signing mode",
12899        false,
12900    ))
12901}
12902
12903fn validate_signed_artifact_request(request: &IosDeviceBuildRequest) -> RemoteBuildResult<()> {
12904    let required = BTreeSet::from([IosArtifactType::Ipa, IosArtifactType::SigningReport]);
12905    let supported = BTreeSet::from([
12906        IosArtifactType::Ipa,
12907        IosArtifactType::AppBundle,
12908        IosArtifactType::Xcarchive,
12909        IosArtifactType::Dsym,
12910        IosArtifactType::SigningReport,
12911    ]);
12912    if let Some(extra) = request.requested_artifacts.difference(&supported).next() {
12913        return Err(unsupported(ProviderFeature::ArtifactType(*extra)));
12914    }
12915    if required.is_subset(&request.requested_artifacts) {
12916        return Ok(());
12917    }
12918    Err(provider_failure(
12919        "artifact_request_mismatch",
12920        "signed builds require the IPA and signing report artifact set",
12921        false,
12922    ))
12923}
12924
12925fn validate_manifests(
12926    record: &JobRecord,
12927    config: &GithubProviderConfig,
12928    manifests: &[ArtifactManifest],
12929) -> RemoteBuildResult<()> {
12930    let snapshot = record.run_snapshot.clone().ok_or_else(|| {
12931        provider_failure(
12932            "run_identity_unavailable",
12933            "artifact manifests require a terminal run snapshot",
12934            true,
12935        )
12936    })?;
12937    let dispatch_revision = record.dispatch_commit.clone().ok_or_else(|| {
12938        provider_failure(
12939            "dispatch_commit_unavailable",
12940            "artifact manifests require the exact dispatched commit",
12941            true,
12942        )
12943    })?;
12944    let context = GithubArtifactContext {
12945        job_id: record.job_id.clone(),
12946        operation_id: record.operation_id.clone(),
12947        repository: config.repository.clone(),
12948        execution_repository_id: record.execution_repository_id,
12949        run: snapshot,
12950        source_repository: config.source_repository.clone(),
12951        source_revision: record.source_revision.clone(),
12952        dispatch_revision,
12953        request: record.request.clone(),
12954        request_sha256: record.request_sha256.clone(),
12955    };
12956    validate_manifests_for_context(&context, manifests)
12957}
12958
12959fn validate_manifests_for_context(
12960    context: &GithubArtifactContext,
12961    manifests: &[ArtifactManifest],
12962) -> RemoteBuildResult<()> {
12963    if manifests.is_empty() {
12964        return Err(provider_failure(
12965            "artifacts_pending",
12966            "independent artifact verification has not produced a manifest",
12967            true,
12968        ));
12969    }
12970    for manifest in manifests {
12971        if manifest.operation_id != context.operation_id
12972            || manifest.job_id != context.job_id
12973            || manifest.provider != GITHUB_PROVIDER_ID
12974            || manifest.source_repository.as_deref() != Some(context.source_repository.as_str())
12975            || manifest.source_revision.as_deref() != Some(context.source_revision.as_str())
12976        {
12977            return Err(provider_failure(
12978                "artifact_manifest_identity_mismatch",
12979                "verified artifact manifest does not match the exact GitHub job",
12980                false,
12981            ));
12982        }
12983    }
12984    Ok(())
12985}
12986
12987fn ensure_running_or_cancelling(record: &mut JobRecord) -> RemoteBuildResult<()> {
12988    if record.state == JobState::Queued {
12989        transition_record(record, JobState::Running)?;
12990    }
12991    Ok(())
12992}
12993
12994fn transition_record(record: &mut JobRecord, next: JobState) -> RemoteBuildResult<()> {
12995    if record.state == next {
12996        return Ok(());
12997    }
12998    // Provider cleanup retries preserve the immutable build outcome while reopening only the
12999    // cleanup phase after a durable negative confirmation.
13000    if record.state == JobState::CleanupFailed && next == JobState::Cleaning {
13001        record.state = JobState::Cleaning;
13002        return Ok(());
13003    }
13004    record.state = record.state.transition_to(next)?;
13005    Ok(())
13006}
13007
13008fn normalize_published_record(record: &mut JobRecord, timestamp_ms: u64) -> RemoteBuildResult<()> {
13009    if record.state != JobState::Created || record.dispatch_commit.is_none() {
13010        return Ok(());
13011    }
13012    transition_record(record, JobState::Queued)?;
13013    if !record
13014        .events
13015        .iter()
13016        .any(|event| matches!(&event.kind, RemoteBuildEventKind::JobQueued { .. }))
13017    {
13018        append_event(
13019            record,
13020            timestamp_ms,
13021            "queue",
13022            RemoteBuildEventKind::JobQueued { position: None },
13023        )?;
13024    }
13025    Ok(())
13026}
13027
13028fn mark_publication_not_attempted(
13029    record: &mut JobRecord,
13030    timestamp_ms: u64,
13031    code: &'static str,
13032    message: &'static str,
13033    retryable: bool,
13034) -> RemoteBuildResult<()> {
13035    record.publication_uncertain = false;
13036    record.publication_absent = false;
13037    record.publication_not_attempted = true;
13038    record.publication_absence_observations = 0;
13039    record.publication_absence_first_observed_at_ms = 0;
13040    record.publication_quiescence_deadline_ms = if record.publication_started_at_ms == 0 {
13041        u64::MAX
13042    } else {
13043        publication_quiescence_deadline(record.publication_started_at_ms, 0)
13044    };
13045    record.temporary_ref_deleted = false;
13046    if !record.state.is_build_terminal() && !record.state.is_terminal() {
13047        transition_record(record, JobState::Failed)?;
13048        append_failed(record, timestamp_ms, code, message, retryable)?;
13049    }
13050    Ok(())
13051}
13052
13053fn append_event(
13054    record: &mut JobRecord,
13055    timestamp_ms: u64,
13056    phase: &'static str,
13057    kind: RemoteBuildEventKind,
13058) -> RemoteBuildResult<()> {
13059    if record.events.len() >= MAX_JOB_EVENTS {
13060        return Err(provider_failure(
13061            "event_capacity_reached",
13062            "GitHub job reached its bounded event capacity",
13063            false,
13064        ));
13065    }
13066    let sequence = u64::try_from(record.events.len())
13067        .unwrap_or(u64::MAX)
13068        .saturating_add(1);
13069    let event = RemoteBuildEvent::new(
13070        record.operation_id.clone(),
13071        record.job_id.clone(),
13072        timestamp_ms,
13073        GITHUB_PROVIDER_ID,
13074        phase,
13075        sequence,
13076        kind,
13077    )?;
13078    record.events.push(event);
13079    Ok(())
13080}
13081
13082fn append_failed(
13083    record: &mut JobRecord,
13084    timestamp_ms: u64,
13085    code: &'static str,
13086    message: &'static str,
13087    retryable: bool,
13088) -> RemoteBuildResult<()> {
13089    let result = IosDeviceBuildResult {
13090        protocol_version: CURRENT_PROTOCOL_VERSION,
13091        operation_id: record.operation_id.clone(),
13092        job_id: record.job_id.clone(),
13093        state: JobState::Failed,
13094        artifacts: Vec::new(),
13095        cleanup: None,
13096    };
13097    result.validate()?;
13098    append_event(
13099        record,
13100        timestamp_ms,
13101        "finished",
13102        RemoteBuildEventKind::OperationFinished {
13103            success: false,
13104            duration_ms: timestamp_ms.saturating_sub(record.created_at_ms),
13105            result: Some(result),
13106            error: Some(RemoteErrorInfo {
13107                code: format!("github.{code}"),
13108                message: message.to_owned(),
13109                help: Some(
13110                    "Inspect sanitized GitHub Actions diagnostics and retry if appropriate"
13111                        .to_owned(),
13112                ),
13113                retryable,
13114            }),
13115        },
13116    )
13117}
13118
13119fn conclusion_code(conclusion: RunConclusion) -> &'static str {
13120    match conclusion {
13121        RunConclusion::Success => "success",
13122        RunConclusion::Failure => "run_failed",
13123        RunConclusion::Cancelled => "run_cancelled",
13124        RunConclusion::TimedOut => "run_timed_out",
13125        RunConclusion::Neutral => "run_neutral",
13126        RunConclusion::Skipped => "run_skipped",
13127        RunConclusion::Stale => "run_stale",
13128        RunConclusion::ActionRequired => "run_action_required",
13129        RunConclusion::StartupFailure => "run_startup_failure",
13130    }
13131}
13132
13133fn conclusion_retryable(conclusion: RunConclusion) -> bool {
13134    matches!(
13135        conclusion,
13136        RunConclusion::TimedOut | RunConclusion::Stale | RunConclusion::StartupFailure
13137    )
13138}
13139
13140fn publisher_failure(error: TemporaryRefPublishError) -> RemoteBuildError {
13141    let (code, retryable) = match error {
13142        TemporaryRefPublishError::Unauthorized => ("temporary_ref_unauthorized", false),
13143        TemporaryRefPublishError::RemoteMismatch => ("git_remote_mismatch", false),
13144        TemporaryRefPublishError::TrustedRefUnavailable => ("trusted_ref_unavailable", true),
13145        TemporaryRefPublishError::SourceRevisionNotTrusted => {
13146            ("source_revision_not_trusted", false)
13147        }
13148        TemporaryRefPublishError::TemporaryRefExists => ("temporary_ref_exists", false),
13149        TemporaryRefPublishError::WorkflowFingerprintMismatch => {
13150            ("workflow_fingerprint_mismatch", false)
13151        }
13152        TemporaryRefPublishError::LocalIsolationFailed => ("git_isolation_failed", true),
13153        TemporaryRefPublishError::Git(GitExecutionError::TimedOut) => {
13154            ("git_operation_timed_out", true)
13155        }
13156        TemporaryRefPublishError::Git(_) => ("git_operation_failed", true),
13157        TemporaryRefPublishError::MalformedGitOutput => ("malformed_git_output", false),
13158        TemporaryRefPublishError::PublicationVerificationFailed => {
13159            ("temporary_ref_verification_failed", false)
13160        }
13161        TemporaryRefPublishError::TemporaryRefLeaseRejected => ("temporary_ref_tip_changed", false),
13162        TemporaryRefPublishError::DeletionVerificationFailed => {
13163            ("temporary_ref_deletion_unverified", true)
13164        }
13165        TemporaryRefPublishError::GitSnapshotUnsupported => ("git_snapshot_unsupported", false),
13166        TemporaryRefPublishError::ExecutionWorkflowVerificationUnsupported => {
13167            ("execution_workflow_verification_unsupported", false)
13168        }
13169        TemporaryRefPublishError::GitSnapshotInvalid => ("git_snapshot_invalid", false),
13170        TemporaryRefPublishError::GitSnapshotDiscoveryChanged => {
13171            ("git_snapshot_discovery_changed", true)
13172        }
13173        TemporaryRefPublishError::GitSnapshotRefConflict => ("git_snapshot_ref_conflict", false),
13174    };
13175    provider_failure(code, "temporary Git ref mutation failed", retryable)
13176}
13177
13178fn transport_failure(error: TransportError) -> RemoteBuildError {
13179    let retryable = matches!(
13180        error,
13181        TransportError::Execution(GhExecutionError::TimedOut)
13182            | TransportError::RunNotFound
13183            | TransportError::PollLimitReached
13184    );
13185    provider_failure(
13186        "github_transport_failed",
13187        "GitHub API operation failed",
13188        retryable,
13189    )
13190}
13191
13192fn workflow_dispatch_client_failure(error: GhExecutionError) -> RemoteBuildError {
13193    let retryable = matches!(
13194        error,
13195        GhExecutionError::TimedOut | GhExecutionError::ProcessIo
13196    );
13197    provider_failure(
13198        "github_workflow_dispatch_client_unavailable",
13199        "GitHub workflow-dispatch authentication is unavailable",
13200        retryable,
13201    )
13202}
13203
13204fn workflow_dispatch_reconciliation_failure(error: TransportError) -> RemoteBuildError {
13205    match error {
13206        TransportError::RunNotFound => provider_failure(
13207            "github_workflow_dispatch_run_not_found",
13208            "Workflow dispatch remains uncertain because no exact run was found",
13209            true,
13210        ),
13211        TransportError::AmbiguousRun => provider_failure(
13212            "github_workflow_dispatch_run_ambiguous",
13213            "Workflow dispatch remains uncertain because multiple exact runs were found",
13214            false,
13215        ),
13216        error => transport_failure(error),
13217    }
13218}
13219
13220fn job_log_identity_failure() -> RemoteBuildError {
13221    provider_failure(
13222        "github_job_log_identity_invalid",
13223        "the restored GitHub run could not bind an exact job-log identity",
13224        false,
13225    )
13226}
13227
13228#[cfg(feature = "secure-job-log-http")]
13229fn job_log_client_failure(error: GhExecutionError) -> RemoteBuildError {
13230    let (code, retryable) = match error {
13231        GhExecutionError::AuthenticationUnavailable => {
13232            ("github_job_log_authentication_unavailable", false)
13233        }
13234        GhExecutionError::TimedOut | GhExecutionError::ProcessIo => {
13235            ("github_job_log_client_unavailable", true)
13236        }
13237        GhExecutionError::SecretTooLarge
13238        | GhExecutionError::SpawnFailed
13239        | GhExecutionError::OutputLimitExceeded
13240        | GhExecutionError::CommandFailed { .. } => ("github_job_log_client_unavailable", false),
13241    };
13242    provider_failure(code, "GitHub job-log authentication failed", retryable)
13243}
13244
13245#[cfg(feature = "secure-job-log-http")]
13246fn job_log_fetch_failure(error: GithubJobLogError) -> RemoteBuildError {
13247    if error == GithubJobLogError::Cancelled {
13248        return RemoteBuildError::Cancelled;
13249    }
13250    let (code, retryable) = match error {
13251        GithubJobLogError::Cancelled => unreachable!("handled above"),
13252        GithubJobLogError::TimedOut
13253        | GithubJobLogError::Transport(_)
13254        | GithubJobLogError::UnexpectedStatus
13255        | GithubJobLogError::IncompleteJobPagination
13256        | GithubJobLogError::JobSetNotTerminal => ("github_job_logs_unavailable", true),
13257        GithubJobLogError::TransportContractViolation
13258        | GithubJobLogError::MalformedJobMetadata
13259        | GithubJobLogError::JobIdentityMismatch
13260        | GithubJobLogError::ConflictingJobIdentity
13261        | GithubJobLogError::JobPaginationLimitReached
13262        | GithubJobLogError::JobLimitReached
13263        | GithubJobLogError::InvalidRedirect
13264        | GithubJobLogError::RedirectHostNotAllowed
13265        | GithubJobLogError::RedirectLimitReached
13266        | GithubJobLogError::UnexpectedContentType
13267        | GithubJobLogError::JobLogTooLarge
13268        | GithubJobLogError::PollTooLarge
13269        | GithubJobLogError::EventLimitReached
13270        | GithubJobLogError::ProjectedByteLimitReached
13271        | GithubJobLogError::SourceSequenceOverflow
13272        | GithubJobLogError::InvalidCompletionMarker => ("github_job_logs_rejected", false),
13273    };
13274    provider_failure(code, "GitHub job logs could not be accepted", retryable)
13275}
13276
13277fn transport_public_message(error: TransportError) -> String {
13278    match error {
13279        TransportError::Execution(GhExecutionError::AuthenticationUnavailable) => {
13280            "Configured GitHub authentication is unavailable".to_owned()
13281        }
13282        TransportError::RepositoryIdentityMismatch => {
13283            "GitHub returned a different repository identity".to_owned()
13284        }
13285        _ => "GitHub readiness check failed".to_owned(),
13286    }
13287}
13288
13289fn publisher_public_message(error: TemporaryRefPublishError) -> String {
13290    match error {
13291        TemporaryRefPublishError::RemoteMismatch => {
13292            "Local Git remote differs from the configured repository".to_owned()
13293        }
13294        TemporaryRefPublishError::TrustedRefUnavailable => {
13295            "Configured trusted source ref is unavailable".to_owned()
13296        }
13297        _ => "Local Git publisher readiness check failed".to_owned(),
13298    }
13299}
13300
13301fn unsupported(feature: ProviderFeature) -> RemoteBuildError {
13302    RemoteBuildError::UnsupportedCapability {
13303        provider: GITHUB_PROVIDER_ID.to_owned(),
13304        feature,
13305    }
13306}
13307
13308fn provider_failure(
13309    code: impl Into<String>,
13310    message: impl Into<String>,
13311    retryable: bool,
13312) -> RemoteBuildError {
13313    RemoteBuildError::ProviderFailure {
13314        provider: GITHUB_PROVIDER_ID.to_owned(),
13315        code: code.into(),
13316        message: message.into(),
13317        retryable,
13318    }
13319}
13320
13321fn job_not_found(_job_id: &str) -> RemoteBuildError {
13322    provider_failure("job_not_found", "GitHub provider job was not found", false)
13323}
13324
13325fn job_not_mapped(_job_id: &str) -> RemoteBuildError {
13326    provider_failure(
13327        "run_not_mapped",
13328        "GitHub Actions run has not been mapped to this job",
13329        true,
13330    )
13331}
13332
13333fn record_is_recyclable(record: &JobRecord) -> bool {
13334    record.state.is_terminal()
13335        || record.publication_not_attempted
13336        || (record.publication_absent && record.temporary_ref_deleted)
13337}
13338
13339fn option_can_bind_once<T: PartialEq + ?Sized>(previous: Option<&T>, next: Option<&T>) -> bool {
13340    match (previous, next) {
13341        (None, _) => true,
13342        (Some(_), None) => false,
13343        (Some(previous), Some(next)) => previous == next,
13344    }
13345}
13346
13347fn publication_boundary_is_armed(resume: &GithubJobResumeV1) -> bool {
13348    resume.publication_started_at_ms >= resume.created_at_ms
13349        && resume.publication_started_at_ms != 0
13350        && (resume.publication_absence_first_observed_at_ms == 0
13351            || resume.publication_absence_first_observed_at_ms >= resume.publication_started_at_ms)
13352        && resume.publication_quiescence_deadline_ms
13353            == publication_quiescence_deadline(
13354                resume.publication_started_at_ms,
13355                resume.publication_absence_first_observed_at_ms,
13356            )
13357}
13358
13359fn publication_boundary_is_unarmed(resume: &GithubJobResumeV1) -> bool {
13360    resume.publication_started_at_ms == 0
13361        && resume.publication_quiescence_deadline_ms == u64::MAX
13362        && resume.publication_absence_first_observed_at_ms == 0
13363}
13364
13365fn publication_record_boundary_is_unarmed(record: &JobRecord) -> bool {
13366    record.publication_started_at_ms == 0
13367        && record.publication_quiescence_deadline_ms == u64::MAX
13368        && record.publication_absence_first_observed_at_ms == 0
13369}
13370
13371fn publication_boundary_can_advance(
13372    previous: &GithubJobResumeV1,
13373    next: &GithubJobResumeV1,
13374) -> bool {
13375    let previous_valid =
13376        publication_boundary_is_unarmed(previous) || publication_boundary_is_armed(previous);
13377    let next_valid = publication_boundary_is_unarmed(next) || publication_boundary_is_armed(next);
13378    previous_valid
13379        && next_valid
13380        && ((previous.publication_started_at_ms == next.publication_started_at_ms
13381            && (previous.publication_quiescence_deadline_ms
13382                == next.publication_quiescence_deadline_ms
13383                || previous.publication_absence_first_observed_at_ms == 0
13384                    && next.publication_absence_first_observed_at_ms != 0))
13385            || publication_boundary_is_unarmed(previous) && publication_boundary_is_armed(next))
13386}
13387
13388fn publication_quiescence_deadline(
13389    publication_started_at_ms: u64,
13390    publication_absence_first_observed_at_ms: u64,
13391) -> u64 {
13392    publication_started_at_ms
13393        .max(publication_absence_first_observed_at_ms)
13394        .saturating_add(GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS)
13395}
13396
13397const fn bool_is_monotonic(previous: bool, next: bool) -> bool {
13398    !previous || next
13399}
13400
13401const fn run_status_phase(status: GithubRunStatusV1) -> u8 {
13402    match status {
13403        GithubRunStatusV1::Requested => 0,
13404        GithubRunStatusV1::Queued | GithubRunStatusV1::Pending | GithubRunStatusV1::Waiting => 1,
13405        GithubRunStatusV1::InProgress => 2,
13406        GithubRunStatusV1::Completed => 3,
13407    }
13408}
13409
13410fn job_state_can_reach(previous: JobState, next: JobState) -> bool {
13411    previous == next
13412        || match previous {
13413            JobState::Created => true,
13414            JobState::Queued => !matches!(next, JobState::Created),
13415            JobState::Running => !matches!(next, JobState::Created | JobState::Queued),
13416            JobState::Cancelling => matches!(
13417                next,
13418                JobState::Failed
13419                    | JobState::Cancelled
13420                    | JobState::Cleaning
13421                    | JobState::Cleaned
13422                    | JobState::CleanupFailed
13423            ),
13424            JobState::Succeeded | JobState::Failed | JobState::Cancelled => matches!(
13425                next,
13426                JobState::Cleaning | JobState::Cleaned | JobState::CleanupFailed
13427            ),
13428            JobState::Cleaning => matches!(next, JobState::Cleaned | JobState::CleanupFailed),
13429            JobState::Cleaned => false,
13430            JobState::CleanupFailed => {
13431                matches!(
13432                    next,
13433                    JobState::Cleaning | JobState::Cleaned | JobState::CleanupFailed
13434                )
13435            }
13436        }
13437}
13438
13439fn repository_url(repository: &Repository) -> String {
13440    format!(
13441        "https://github.com/{}/{}",
13442        repository.owner(),
13443        repository.name()
13444    )
13445}
13446
13447fn repository_from_url(value: &str) -> Option<Repository> {
13448    let slug = value.strip_prefix("https://github.com/")?;
13449    let (owner, name) = slug.split_once('/')?;
13450    if name.contains('/') {
13451        return None;
13452    }
13453    Repository::new(owner, name).ok()
13454}
13455
13456#[cfg(test)]
13457fn is_safe_remote_name(value: &str) -> bool {
13458    !value.is_empty()
13459        && value.len() <= 64
13460        && !value.starts_with('-')
13461        && value
13462            .bytes()
13463            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
13464}
13465
13466fn github_remote_matches(actual: &str, expected_https: &str) -> bool {
13467    let actual = actual.trim_end_matches('/').trim_end_matches(".git");
13468    if actual.eq_ignore_ascii_case(expected_https) {
13469        return true;
13470    }
13471    let Some(slug) = expected_https.strip_prefix("https://github.com/") else {
13472        return false;
13473    };
13474    actual.eq_ignore_ascii_case(&format!("git@github.com:{slug}"))
13475        || actual.eq_ignore_ascii_case(&format!("ssh://git@github.com/{slug}"))
13476}
13477
13478fn validate_provider_identifier(field: &'static str, value: &str) -> RemoteBuildResult<()> {
13479    if value.is_empty()
13480        || value.len() > 160
13481        || !value
13482            .bytes()
13483            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
13484    {
13485        return Err(RemoteBuildError::InvalidIdentifier {
13486            field,
13487            reason: "value is not a safe provider identifier",
13488        });
13489    }
13490    Ok(())
13491}
13492
13493fn validate_ref_segment(value: &str) -> Result<(), ()> {
13494    if value.is_empty()
13495        || value.len() > 160
13496        || !value.as_bytes()[0].is_ascii_alphanumeric()
13497        || value.ends_with('.')
13498        || value
13499            .rsplit_once('.')
13500            .is_some_and(|(_, extension)| extension.eq_ignore_ascii_case("lock"))
13501        || value.contains("..")
13502        || !value
13503            .bytes()
13504            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
13505    {
13506        Err(())
13507    } else {
13508        Ok(())
13509    }
13510}
13511
13512fn validate_repository_path(value: &str) -> Result<(), ()> {
13513    if value.is_empty()
13514        || value.len() > 512
13515        || value.starts_with('/')
13516        || value.contains('\\')
13517        || value
13518            .split('/')
13519            .any(|part| part.is_empty() || part == "." || part == "..")
13520        || value
13521            .bytes()
13522            .any(|byte| byte.is_ascii_control() || byte == b':')
13523    {
13524        Err(())
13525    } else {
13526        Ok(())
13527    }
13528}
13529
13530fn parse_single_line(output: &[u8]) -> Result<&str, TemporaryRefPublishError> {
13531    let text =
13532        std::str::from_utf8(output).map_err(|_| TemporaryRefPublishError::MalformedGitOutput)?;
13533    let text = text.strip_suffix('\n').unwrap_or(text);
13534    if text.is_empty() || text.contains(['\n', '\r']) {
13535        return Err(TemporaryRefPublishError::MalformedGitOutput);
13536    }
13537    Ok(text)
13538}
13539
13540fn parse_object_id(output: &[u8]) -> Result<String, TemporaryRefPublishError> {
13541    let value = parse_single_line(output)?;
13542    CommitSha::new(value.to_owned()).map_err(|_| TemporaryRefPublishError::MalformedGitOutput)?;
13543    Ok(value.to_owned())
13544}
13545
13546fn parse_ls_remote(
13547    output: &[u8],
13548    expected_ref: &str,
13549) -> Result<Option<CommitSha>, TemporaryRefPublishError> {
13550    if output.is_empty() {
13551        return Ok(None);
13552    }
13553    let text =
13554        std::str::from_utf8(output).map_err(|_| TemporaryRefPublishError::MalformedGitOutput)?;
13555    let text = text.strip_suffix('\n').unwrap_or(text);
13556    if text.is_empty() || text.contains(['\n', '\r']) {
13557        return Err(TemporaryRefPublishError::MalformedGitOutput);
13558    }
13559    let (sha, reference) = text
13560        .split_once('\t')
13561        .ok_or(TemporaryRefPublishError::MalformedGitOutput)?;
13562    if reference != expected_ref {
13563        return Err(TemporaryRefPublishError::MalformedGitOutput);
13564    }
13565    let sha =
13566        CommitSha::new(sha.to_owned()).map_err(|_| TemporaryRefPublishError::MalformedGitOutput)?;
13567    Ok(Some(sha))
13568}
13569
13570fn remove_exact_file_if_present(path: &Path) -> Result<(), TemporaryRefPublishError> {
13571    match fs::symlink_metadata(path) {
13572        Ok(metadata) if metadata.file_type().is_file() => {
13573            fs::remove_file(path).map_err(|_| TemporaryRefPublishError::LocalIsolationFailed)
13574        }
13575        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
13576        Ok(_) | Err(_) => Err(TemporaryRefPublishError::LocalIsolationFailed),
13577    }
13578}
13579
13580#[cfg(test)]
13581fn executable_basename_matches(path: &Path, stem: &str) -> bool {
13582    let Some(name) = path.file_name().and_then(OsStr::to_str) else {
13583        return false;
13584    };
13585    #[cfg(windows)]
13586    {
13587        name.eq_ignore_ascii_case(&format!("{stem}.exe"))
13588    }
13589    #[cfg(not(windows))]
13590    {
13591        name == stem
13592    }
13593}
13594
13595fn canonical_directory(path: &Path) -> Result<PathBuf, GitPublisherConfigError> {
13596    if !is_absolute_normal(path) {
13597        return Err(GitPublisherConfigError::InvalidDirectory);
13598    }
13599    let metadata =
13600        fs::symlink_metadata(path).map_err(|_| GitPublisherConfigError::InvalidDirectory)?;
13601    if metadata.file_type().is_symlink() || !metadata.is_dir() {
13602        return Err(GitPublisherConfigError::InvalidDirectory);
13603    }
13604    fs::canonicalize(path).map_err(|_| GitPublisherConfigError::InvalidDirectory)
13605}
13606
13607fn is_absolute_normal(path: &Path) -> bool {
13608    path.is_absolute()
13609        && path
13610            .components()
13611            .all(|component| !matches!(component, Component::CurDir | Component::ParentDir))
13612}
13613
13614fn ensure_git_process_parent_fence() -> Result<(), GitExecutionError> {
13615    let fenced = rustferry_core::process_control::ensure_descendants_terminate_on_process_exit()
13616        .map_err(|_| GitExecutionError::ProcessIo)?;
13617    if cfg!(windows) && !fenced {
13618        return Err(GitExecutionError::ProcessIo);
13619    }
13620    Ok(())
13621}
13622
13623fn run_git_process(
13624    command: Command,
13625    stdin_bytes: &[u8],
13626    timeout: Duration,
13627    stdout_limit: usize,
13628) -> Result<Vec<u8>, GitExecutionError> {
13629    let output = run_git_process_allow_status(command, stdin_bytes, timeout, stdout_limit)?;
13630    if !output.status.success() {
13631        return Err(git_command_failed(output.status));
13632    }
13633    Ok(output.stdout)
13634}
13635
13636fn run_git_process_file(
13637    command: Command,
13638    input: fs::File,
13639    expected_len: u64,
13640    timeout: Duration,
13641    stdout_limit: usize,
13642) -> Result<Vec<u8>, GitExecutionError> {
13643    let output = run_git_process_allow_status_with_input(
13644        command,
13645        GitProcessInput::File {
13646            input,
13647            expected_len,
13648        },
13649        timeout,
13650        stdout_limit,
13651    )?;
13652    if !output.status.success() {
13653        return Err(git_command_failed(output.status));
13654    }
13655    Ok(output.stdout)
13656}
13657
13658struct GitProcessOutput {
13659    status: ExitStatus,
13660    stdout: Vec<u8>,
13661}
13662
13663fn run_git_process_allow_status(
13664    command: Command,
13665    stdin_bytes: &[u8],
13666    timeout: Duration,
13667    stdout_limit: usize,
13668) -> Result<GitProcessOutput, GitExecutionError> {
13669    run_git_process_allow_status_with_input(
13670        command,
13671        GitProcessInput::Bytes(stdin_bytes.to_vec()),
13672        timeout,
13673        stdout_limit,
13674    )
13675}
13676
13677enum GitProcessInput {
13678    Bytes(Vec<u8>),
13679    File { input: fs::File, expected_len: u64 },
13680}
13681
13682fn run_git_process_allow_status_with_input(
13683    mut command: Command,
13684    input: GitProcessInput,
13685    timeout: Duration,
13686    stdout_limit: usize,
13687) -> Result<GitProcessOutput, GitExecutionError> {
13688    ensure_git_process_parent_fence()?;
13689    let (mut child, platform_guard) =
13690        rustferry_core::process_control::spawn_tracked_child(&mut command)
13691            .map_err(|_| GitExecutionError::ProcessIo)?;
13692    let mut process_tree = GitProcessTreeGuard::from_spawn(&child, platform_guard);
13693    let Some(stdin) = child.stdin.take() else {
13694        let deadline = git_process_cleanup_deadline();
13695        terminate_git_process_tree(&mut child, &mut process_tree, deadline);
13696        return Err(GitExecutionError::ProcessIo);
13697    };
13698    let Some(stdout) = child.stdout.take() else {
13699        let deadline = git_process_cleanup_deadline();
13700        terminate_git_process_tree(&mut child, &mut process_tree, deadline);
13701        return Err(GitExecutionError::ProcessIo);
13702    };
13703    let Some(stderr) = child.stderr.take() else {
13704        let deadline = git_process_cleanup_deadline();
13705        terminate_git_process_tree(&mut child, &mut process_tree, deadline);
13706        return Err(GitExecutionError::ProcessIo);
13707    };
13708    let stdin_writer = spawn_git_process_worker(move || match input {
13709        GitProcessInput::Bytes(input) => {
13710            let mut stdin = stdin;
13711            stdin
13712                .write_all(&input)
13713                .and_then(|()| stdin.flush())
13714                .map_err(|_| GitExecutionError::ProcessIo)
13715        }
13716        GitProcessInput::File {
13717            mut input,
13718            expected_len,
13719        } => copy_exact_git_input(&mut input, stdin, expected_len),
13720    });
13721    let stdout_reader = spawn_git_process_worker(move || read_capped(stdout, stdout_limit, true));
13722    let stderr_reader =
13723        spawn_git_process_worker(move || read_capped(stderr, MAX_GIT_STDERR_BYTES, false));
13724    let Some(deadline) = Instant::now().checked_add(timeout) else {
13725        let cleanup_deadline = git_process_cleanup_deadline();
13726        terminate_git_process_tree(&mut child, &mut process_tree, cleanup_deadline);
13727        let _ = collect_git_process_workers(
13728            &stdin_writer,
13729            &stdout_reader,
13730            &stderr_reader,
13731            cleanup_deadline,
13732        );
13733        return Err(GitExecutionError::TimedOut);
13734    };
13735    let status = loop {
13736        match child.try_wait() {
13737            Ok(Some(status)) => break status,
13738            Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(20)),
13739            Ok(None) => {
13740                let cleanup_deadline = git_process_cleanup_deadline();
13741                terminate_git_process_tree(&mut child, &mut process_tree, cleanup_deadline);
13742                let _ = collect_git_process_workers(
13743                    &stdin_writer,
13744                    &stdout_reader,
13745                    &stderr_reader,
13746                    cleanup_deadline,
13747                );
13748                return Err(GitExecutionError::TimedOut);
13749            }
13750            Err(_) => {
13751                let cleanup_deadline = git_process_cleanup_deadline();
13752                terminate_git_process_tree(&mut child, &mut process_tree, cleanup_deadline);
13753                let _ = collect_git_process_workers(
13754                    &stdin_writer,
13755                    &stdout_reader,
13756                    &stderr_reader,
13757                    cleanup_deadline,
13758                );
13759                return Err(GitExecutionError::ProcessIo);
13760            }
13761        }
13762    };
13763    // Git may exit while a helper or hook descendant still owns its inherited pipes. Kill the
13764    // complete supervised tree before bounded worker collection, on success and failure alike.
13765    process_tree.terminate_descendants();
13766    let (stdout, stderr) = collect_git_process_workers(
13767        &stdin_writer,
13768        &stdout_reader,
13769        &stderr_reader,
13770        git_process_cleanup_deadline(),
13771    )?;
13772    if stdout.truncated || stderr.truncated {
13773        return Err(GitExecutionError::OutputLimitExceeded);
13774    }
13775    Ok(GitProcessOutput {
13776        status,
13777        stdout: stdout.bytes,
13778    })
13779}
13780
13781fn copy_exact_git_input(
13782    input: &mut fs::File,
13783    mut stdin: impl Write,
13784    expected_len: u64,
13785) -> Result<(), GitExecutionError> {
13786    let mut remaining = expected_len;
13787    let mut buffer = [0_u8; 8 * 1024];
13788    while remaining != 0 {
13789        let requested = usize::try_from(remaining.min(buffer.len() as u64))
13790            .map_err(|_| GitExecutionError::ProcessIo)?;
13791        let read = input
13792            .read(&mut buffer[..requested])
13793            .map_err(|_| GitExecutionError::ProcessIo)?;
13794        if read == 0 {
13795            return Err(GitExecutionError::ProcessIo);
13796        }
13797        stdin
13798            .write_all(&buffer[..read])
13799            .map_err(|_| GitExecutionError::ProcessIo)?;
13800        remaining = remaining.saturating_sub(read as u64);
13801    }
13802    let mut extra = [0_u8; 1];
13803    if input
13804        .read(&mut extra)
13805        .map_err(|_| GitExecutionError::ProcessIo)?
13806        != 0
13807    {
13808        return Err(GitExecutionError::ProcessIo);
13809    }
13810    stdin.flush().map_err(|_| GitExecutionError::ProcessIo)
13811}
13812
13813fn spawn_git_process_worker<T>(
13814    worker: impl FnOnce() -> Result<T, GitExecutionError> + Send + 'static,
13815) -> Receiver<Result<T, GitExecutionError>>
13816where
13817    T: Send + 'static,
13818{
13819    let (sender, receiver) = mpsc::sync_channel(1);
13820    drop(thread::spawn(move || {
13821        let _ = sender.send(worker());
13822    }));
13823    receiver
13824}
13825
13826fn collect_git_process_workers(
13827    stdin_writer: &Receiver<Result<(), GitExecutionError>>,
13828    stdout_reader: &Receiver<Result<CapturedGitOutput, GitExecutionError>>,
13829    stderr_reader: &Receiver<Result<CapturedGitOutput, GitExecutionError>>,
13830    deadline: Instant,
13831) -> Result<(CapturedGitOutput, CapturedGitOutput), GitExecutionError> {
13832    receive_git_process_worker(stdin_writer, deadline)?;
13833    let stdout = receive_git_process_worker(stdout_reader, deadline)?;
13834    let stderr = receive_git_process_worker(stderr_reader, deadline)?;
13835    Ok((stdout, stderr))
13836}
13837
13838fn receive_git_process_worker<T>(
13839    receiver: &Receiver<Result<T, GitExecutionError>>,
13840    deadline: Instant,
13841) -> Result<T, GitExecutionError> {
13842    let remaining = deadline.saturating_duration_since(Instant::now());
13843    match receiver.recv_timeout(remaining) {
13844        Ok(result) => result,
13845        Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => {
13846            Err(GitExecutionError::ProcessIo)
13847        }
13848    }
13849}
13850
13851fn git_process_cleanup_deadline() -> Instant {
13852    Instant::now()
13853        .checked_add(GIT_PROCESS_CLEANUP_GRACE)
13854        .unwrap_or_else(Instant::now)
13855}
13856
13857struct GitProcessTreeGuard {
13858    #[cfg(unix)]
13859    process_group: Option<u32>,
13860    platform_guard: Option<rustferry_core::process_control::ProcessGroupGuard>,
13861}
13862
13863impl GitProcessTreeGuard {
13864    fn from_spawn(
13865        child: &Child,
13866        platform_guard: rustferry_core::process_control::ProcessGroupGuard,
13867    ) -> Self {
13868        #[cfg(not(unix))]
13869        let _ = child;
13870        Self {
13871            #[cfg(unix)]
13872            process_group: Some(child.id()),
13873            platform_guard: Some(platform_guard),
13874        }
13875    }
13876
13877    fn terminate_descendants(&mut self) {
13878        #[cfg(unix)]
13879        if let Some(process_group) = self.process_group.take() {
13880            let _ = Command::new("/bin/kill")
13881                .args(["-KILL", "--", &format!("-{process_group}")])
13882                .env_clear()
13883                .stdin(Stdio::null())
13884                .stdout(Stdio::null())
13885                .stderr(Stdio::null())
13886                .status();
13887        }
13888        drop(self.platform_guard.take());
13889    }
13890}
13891
13892impl Drop for GitProcessTreeGuard {
13893    fn drop(&mut self) {
13894        self.terminate_descendants();
13895    }
13896}
13897
13898fn terminate_git_process_tree(
13899    child: &mut Child,
13900    process_tree: &mut GitProcessTreeGuard,
13901    deadline: Instant,
13902) {
13903    process_tree.terminate_descendants();
13904    terminate_git_child(child, deadline);
13905}
13906
13907fn terminate_git_child(child: &mut Child, deadline: Instant) {
13908    let _ = child.kill();
13909    loop {
13910        match child.try_wait() {
13911            Ok(Some(_)) | Err(_) => return,
13912            Ok(None) => {
13913                let remaining = deadline.saturating_duration_since(Instant::now());
13914                if remaining.is_zero() {
13915                    return;
13916                }
13917                thread::sleep(remaining.min(Duration::from_millis(20)));
13918            }
13919        }
13920    }
13921}
13922
13923struct CapturedGitOutput {
13924    bytes: Vec<u8>,
13925    truncated: bool,
13926}
13927
13928fn read_capped(
13929    mut reader: impl Read,
13930    limit: usize,
13931    retain: bool,
13932) -> Result<CapturedGitOutput, GitExecutionError> {
13933    let mut bytes = if retain {
13934        Vec::with_capacity(limit.min(16 * 1024))
13935    } else {
13936        Vec::new()
13937    };
13938    let mut total = 0_usize;
13939    let mut buffer = [0_u8; 16 * 1024];
13940    loop {
13941        let read = reader
13942            .read(&mut buffer)
13943            .map_err(|_| GitExecutionError::ProcessIo)?;
13944        if read == 0 {
13945            break;
13946        }
13947        let retained = limit.saturating_sub(total).min(read);
13948        if retain && retained > 0 {
13949            bytes.extend_from_slice(&buffer[..retained]);
13950        }
13951        total = total.saturating_add(read);
13952    }
13953    Ok(CapturedGitOutput {
13954        bytes,
13955        truncated: total > limit,
13956    })
13957}
13958
13959fn git_command_failed(status: ExitStatus) -> GitExecutionError {
13960    GitExecutionError::CommandFailed {
13961        exit_code: status.code(),
13962    }
13963}
13964
13965fn is_lower_sha256(value: &str) -> bool {
13966    value.len() == 64
13967        && value
13968            .bytes()
13969            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
13970}
13971
13972fn provider_config_sha256(config: &GithubProviderConfig) -> RemoteBuildResult<String> {
13973    #[derive(Serialize)]
13974    #[allow(
13975        clippy::struct_excessive_bools,
13976        reason = "canonical provider identity must bind each independent mutation authority bit"
13977    )]
13978    struct ProviderIdentity<'a> {
13979        schema_version: u32,
13980        provider: &'static str,
13981        execution_repository: String,
13982        source_repository: &'a str,
13983        trusted_source_ref: &'a str,
13984        temporary_ref_namespace: &'a str,
13985        workflow_path: String,
13986        workflow_sha256: &'a str,
13987        #[serde(skip_serializing_if = "Option::is_none")]
13988        run_trigger: Option<&'static str>,
13989        worker_version: String,
13990        maximum_jobs: usize,
13991        discovery_attempts: u16,
13992        discovery_window_ms: u64,
13993        publish_temporary_ref: bool,
13994        cancel_run: bool,
13995        delete_temporary_ref: bool,
13996        publish_git_snapshot_source: bool,
13997        delete_git_snapshot_source: bool,
13998        manage_git_snapshot_keepalive: bool,
13999        #[serde(skip_serializing_if = "Option::is_none")]
14000        source_fetch_endpoint: Option<&'a str>,
14001        #[serde(skip_serializing_if = "Option::is_none")]
14002        source_push_endpoint: Option<&'a str>,
14003        #[serde(skip_serializing_if = "Option::is_none")]
14004        execution_fetch_endpoint: Option<&'a str>,
14005        #[serde(skip_serializing_if = "Option::is_none")]
14006        execution_push_endpoint: Option<&'a str>,
14007    }
14008
14009    let identity = ProviderIdentity {
14010        schema_version: GITHUB_JOB_RESUME_SCHEMA_VERSION,
14011        provider: GITHUB_PROVIDER_ID,
14012        execution_repository: repository_url(&config.repository),
14013        source_repository: &config.source_repository,
14014        trusted_source_ref: config.workflow.trusted_source_ref().as_str(),
14015        temporary_ref_namespace: config.workflow.temporary_branch_namespace().as_str(),
14016        workflow_path: config.workflow.filename().repository_path(),
14017        workflow_sha256: config.workflow_fingerprint.as_str(),
14018        run_trigger: (config.workflow.run_trigger() == WorkflowRunTrigger::WorkflowDispatch)
14019            .then_some("workflow_dispatch"),
14020        worker_version: config.worker_version.to_string(),
14021        maximum_jobs: config.max_jobs,
14022        discovery_attempts: config.poll_policy.discovery_attempts(),
14023        discovery_window_ms: config.poll_policy.discovery_window_ms(),
14024        publish_temporary_ref: config.mutation_authorization.publish_temporary_ref,
14025        cancel_run: config.mutation_authorization.cancel_run,
14026        delete_temporary_ref: config.mutation_authorization.delete_temporary_ref,
14027        publish_git_snapshot_source: config.mutation_authorization.publish_git_snapshot_source,
14028        delete_git_snapshot_source: config.mutation_authorization.delete_git_snapshot_source,
14029        manage_git_snapshot_keepalive: config.mutation_authorization.manage_git_snapshot_keepalive,
14030        source_fetch_endpoint: config
14031            .git_endpoints
14032            .as_ref()
14033            .map(|endpoints| endpoints.source.fetch().canonical_url()),
14034        source_push_endpoint: config
14035            .git_endpoints
14036            .as_ref()
14037            .map(|endpoints| endpoints.source.push().canonical_url()),
14038        execution_fetch_endpoint: config
14039            .git_endpoints
14040            .as_ref()
14041            .map(|endpoints| endpoints.execution.fetch().canonical_url()),
14042        execution_push_endpoint: config
14043            .git_endpoints
14044            .as_ref()
14045            .map(|endpoints| endpoints.execution.push().canonical_url()),
14046    };
14047    serde_json::to_vec(&identity)
14048        .map(|bytes| hex_sha256(&bytes))
14049        .map_err(|error| RemoteBuildError::Serialization {
14050            message: bounded_message(&error.to_string()),
14051        })
14052}
14053
14054fn resume_failure(code: &'static str) -> RemoteBuildError {
14055    provider_failure(
14056        code,
14057        "durable GitHub job state failed strict identity validation",
14058        false,
14059    )
14060}
14061
14062fn ensure_same_run_attempt(
14063    previous: Option<&RunSnapshot>,
14064    current: &RunSnapshot,
14065) -> RemoteBuildResult<()> {
14066    if previous.is_some_and(|previous| {
14067        previous.handle() != current.handle()
14068            || previous.run_number() != current.run_number()
14069            || previous.run_attempt() != current.run_attempt()
14070            || previous.status() == RunStatus::Completed
14071                && (current.status() != RunStatus::Completed
14072                    || previous.conclusion() != current.conclusion())
14073    }) {
14074        return Err(resume_failure("github_run_attempt_changed"));
14075    }
14076    Ok(())
14077}
14078
14079fn github_run_is_later_attempt(previous: &RunSnapshot, current: &RunSnapshot) -> bool {
14080    previous.handle() == current.handle()
14081        && previous.run_number() == current.run_number()
14082        && current.run_attempt() > previous.run_attempt()
14083}
14084
14085fn run_snapshot_is_successful(snapshot: &RunSnapshot) -> bool {
14086    snapshot.status() == RunStatus::Completed
14087        && snapshot.conclusion() == Some(RunConclusion::Success)
14088}
14089
14090fn hex_sha256(bytes: &[u8]) -> String {
14091    const HEX: &[u8; 16] = b"0123456789abcdef";
14092    let digest = Sha256::digest(bytes);
14093    let mut encoded = String::with_capacity(64);
14094    for byte in digest {
14095        encoded.push(char::from(HEX[usize::from(byte >> 4)]));
14096        encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
14097    }
14098    encoded
14099}
14100
14101fn publication_lease_scope_sha256(
14102    directory: &Path,
14103    identity: &rustferry_core::DirectoryFilesystemIdentity,
14104) -> String {
14105    let mut digest = Sha256::new();
14106    digest.update(b"rustferry-github-publication-lease-scope-v1\0");
14107    digest.update(directory.as_os_str().to_string_lossy().as_bytes());
14108    digest.update([0]);
14109    digest.update(identity.as_str());
14110    hex::encode(digest.finalize())
14111}
14112
14113fn publication_lease_key(request: &TemporaryRefPublicationLeaseRequest<'_>) -> String {
14114    let mut digest = Sha256::new();
14115    digest.update(b"rustferry-github-publication-lease-v1\0");
14116    digest.update(repository_url(request.repository));
14117    digest.update([0]);
14118    digest.update(b"refs/heads/");
14119    digest.update(request.temporary_ref.branch().as_str());
14120    digest.update([0]);
14121    digest.update(request.operation_id);
14122    hex::encode(digest.finalize())
14123}
14124
14125fn git_snapshot_source_lease_key(request: &GitSnapshotSourcePublicationLeaseRequest<'_>) -> String {
14126    let mut digest = Sha256::new();
14127    digest.update(b"rustferry-github-snapshot-source-lease-v1\0");
14128    digest.update(request.source_repository);
14129    digest.update([0]);
14130    digest.update(request.source_ref.as_str());
14131    hex::encode(digest.finalize())
14132}
14133
14134fn bounded_message(message: &str) -> String {
14135    message.chars().take(256).collect()
14136}
14137
14138#[cfg(test)]
14139mod tests {
14140    use std::{
14141        collections::{BTreeMap, VecDeque},
14142        sync::{
14143            Arc, Condvar, Mutex,
14144            atomic::{AtomicU64, Ordering},
14145        },
14146        task::{Context, Poll, Waker},
14147        thread,
14148        time::Duration as StdDuration,
14149    };
14150
14151    use crate::{
14152        transport::{
14153            ApiMethod, GhRequest, GithubWorkflowDispatchHttpClient,
14154            GithubWorkflowDispatchHttpClientFactory, GithubWorkflowDispatchHttpError,
14155            GithubWorkflowDispatchHttpRequest, GithubWorkflowDispatchHttpResponse, TransportLimits,
14156        },
14157        workflow::{
14158            ProtectedEnvironment, PublicSourceRepository, SigningSecretNames,
14159            TemporaryBranchNamespace, WorkerDistribution, WorkflowFileName,
14160        },
14161    };
14162    use rustferry_remote::{
14163        ArtifactRecord, ArtifactSigningEvidence, BuildProfile, BundleIdentifier,
14164        COMPILE_PHASE_EVIDENCE_SCHEMA_VERSION, CompileToolchainEvidence, DevelopmentTeam,
14165        DevelopmentTeamPlan, DevicePlan, EntitlementPlan, EntitlementSet,
14166        IosDeviceProductExpectation, ProvisioningPlan, ProvisioningProfileType,
14167        SEALED_UNSIGNED_ARCHIVE_SCHEMA_VERSION, SealedUnsignedArchive, SigningCertificate,
14168        SigningIdentity, SigningPlan, SigningPrivateKeyReference, SigningReference, SigningTarget,
14169        SigningTargetKind, SourceArchive, SourceManifest, SourceManifestEntry,
14170        UnsignedAppInspection, UnsignedNestedBundleExpectation, UnsignedNestedBundleKind,
14171        UnsignedXcarchiveExpectation, UnsignedXcarchiveInspection,
14172    };
14173
14174    use super::*;
14175
14176    const SOURCE_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
14177    const AUTHENTICATED_USER_ID: u64 = 42;
14178    const TRUSTED_TIP: &str = "123456789abcdef0123456789abcdef012345678";
14179    const DISPATCH_SHA: &str = "abcdef0123456789abcdef0123456789abcdef01";
14180    const TREE_SHA: &str = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
14181    const WORKFLOW_BLOB: &str = "cccccccccccccccccccccccccccccccccccccccc";
14182    const MANIFEST_BLOB: &str = "dddddddddddddddddddddddddddddddddddddddd";
14183    const FAKE_LEASE_SCOPE: &str =
14184        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
14185    const BRANCH_BASE64: &str = "cnVzdGZlcnJ5L2dvYWwzL2J1aWxkcy9vcGVyYXRpb24tMQ==";
14186    const WORKFLOW_PATH_BASE64: &str =
14187        "LmdpdGh1Yi93b3JrZmxvd3MvcnVzdGZlcnJ5LWdvYWwzLWlwaG9uZS55bWw=";
14188    const SIGNING_ENVIRONMENT_BASE64: &str = "cnVzdGZlcnJ5LWdvYWwzLXNpZ25pbmc=";
14189    const SIGNING_BRANCH_POLICY_BASE64: &str = "cnVzdGZlcnJ5L2dvYWwzL2J1aWxkcy8q";
14190    const CERTIFICATE_P12_BASE64: &str = "UlVTVEZFUlJZX0dPQUwzX0lPU19DRVJUSUZJQ0FURV9QMTI=";
14191    const CERTIFICATE_PASSWORD_BASE64: &str =
14192        "UlVTVEZFUlJZX0dPQUwzX0lPU19DRVJUSUZJQ0FURV9QQVNTV09SRA==";
14193    const PROVISIONING_PROFILE_BASE64: &str =
14194        "UlVTVEZFUlJZX0dPQUwzX0lPU19QUk9WSVNJT05JTkdfUFJPRklMRQ==";
14195    const WIDGET_PROFILE_BASE64: &str =
14196        "UlVTVEZFUlJZX0dPQUwzX0lPU19QUk9GSUxFXzI2NEI4RkUyNjhCMDAwOEIzNkYwOUJCNUNEMDA4RUE0";
14197    const SNAPSHOT_ARCHIVE_BYTES: &[u8] = b"snapshot archive fixture\n";
14198
14199    fn base64_fixture(value: &str) -> String {
14200        const ALPHABET: &[u8; 64] =
14201            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
14202        let mut encoded = String::with_capacity(value.len().div_ceil(3) * 4);
14203        for chunk in value.as_bytes().chunks(3) {
14204            let first = chunk[0];
14205            let second = chunk.get(1).copied().unwrap_or(0);
14206            let third = chunk.get(2).copied().unwrap_or(0);
14207            encoded.push(char::from(ALPHABET[usize::from(first >> 2)]));
14208            encoded.push(char::from(
14209                ALPHABET[usize::from((first & 0x03) << 4 | second >> 4)],
14210            ));
14211            encoded.push(if chunk.len() > 1 {
14212                char::from(ALPHABET[usize::from((second & 0x0f) << 2 | third >> 6)])
14213            } else {
14214                '='
14215            });
14216            encoded.push(if chunk.len() > 2 {
14217                char::from(ALPHABET[usize::from(third & 0x3f)])
14218            } else {
14219                '='
14220            });
14221        }
14222        encoded
14223    }
14224
14225    struct TestPrivateDirectory {
14226        _root: tempfile::TempDir,
14227        path: PathBuf,
14228    }
14229
14230    impl TestPrivateDirectory {
14231        fn path(&self) -> &Path {
14232            &self.path
14233        }
14234    }
14235
14236    fn tempdir() -> io::Result<TestPrivateDirectory> {
14237        let root = tempfile::tempdir()?;
14238        let path = root.path().join("private");
14239        create_test_private_directory(&path)?;
14240        Ok(TestPrivateDirectory { _root: root, path })
14241    }
14242
14243    fn create_test_private_directory(path: &Path) -> io::Result<()> {
14244        #[cfg(windows)]
14245        {
14246            rustferry_core::windows_private_directory::create_private_directory(path)
14247                .map(drop)
14248                .map_err(io::Error::other)
14249        }
14250        #[cfg(unix)]
14251        {
14252            use std::os::unix::fs::DirBuilderExt as _;
14253
14254            fs::DirBuilder::new().mode(0o700).create(path)
14255        }
14256        #[cfg(not(any(unix, windows)))]
14257        {
14258            fs::create_dir(path)
14259        }
14260    }
14261
14262    #[cfg(windows)]
14263    fn initialize_caller_git_fixture() -> Option<(tempfile::TempDir, PathBuf, PathBuf)> {
14264        let git = crate::git_process::trusted_git_executable().ok()?;
14265        let temporary = tempfile::tempdir().expect("caller Git fixture");
14266        let checkout = temporary.path().join("checkout");
14267        fs::create_dir(&checkout).expect("checkout");
14268        assert!(
14269            Command::new(&git)
14270                .args(["init", "--quiet"])
14271                .current_dir(&checkout)
14272                .status()
14273                .expect("git init")
14274                .success()
14275        );
14276        fs::write(checkout.join("actual.txt"), b"actual\n").expect("source file");
14277        assert!(
14278            Command::new(&git)
14279                .args(["add", "--", "actual.txt"])
14280                .current_dir(&checkout)
14281                .status()
14282                .expect("git add")
14283                .success()
14284        );
14285        assert!(
14286            Command::new(&git)
14287                .args([
14288                    "-c",
14289                    "user.name=RustFerry Test",
14290                    "-c",
14291                    "user.email=rustferry@example.invalid",
14292                    "commit",
14293                    "--quiet",
14294                    "-m",
14295                    "actual",
14296                ])
14297                .current_dir(&checkout)
14298                .status()
14299                .expect("git commit")
14300                .success()
14301        );
14302        Some((temporary, checkout, git))
14303    }
14304
14305    #[cfg(windows)]
14306    fn fixture_git_output(git: &Path, checkout: &Path, arguments: &[&str]) -> Vec<u8> {
14307        let output = Command::new(git)
14308            .args(arguments)
14309            .current_dir(checkout)
14310            .output()
14311            .expect("fixture Git command");
14312        assert!(
14313            output.status.success(),
14314            "{}",
14315            String::from_utf8_lossy(&output.stderr)
14316        );
14317        output.stdout
14318    }
14319
14320    #[test]
14321    fn caller_git_config_accepts_legal_subsection_names() {
14322        assert_eq!(
14323            validate_caller_git_config_names(
14324                b"branch.goal3/windows-live-acceptance.remote\0branch.goal3/windows-live-acceptance.merge\0branch.release_candidate.remote\0",
14325            ),
14326            Ok(())
14327        );
14328    }
14329
14330    #[test]
14331    fn caller_git_config_rejects_dangerous_families() {
14332        for key in [
14333            b"include.path\0".as_slice(),
14334            b"includeIf.gitdir:/tmp/**.path\0",
14335            b"extensions.worktreeConfig\0",
14336            b"extensions.partialClone\0",
14337            b"core.worktree\0",
14338            b"filter.lfs.process\0",
14339            b"protocol.ext.allow\0",
14340            b"remote.origin.promisor\0",
14341            b"remote.origin.partialCloneFilter\0",
14342            b"diff.external\0",
14343            b"diff.custom.command\0",
14344            b"diff.custom.textconv\0",
14345        ] {
14346            assert_eq!(
14347                validate_caller_git_config_names(key),
14348                Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig)
14349            );
14350        }
14351    }
14352
14353    #[test]
14354    fn caller_git_config_rejects_malformed_names_and_framing() {
14355        for output in [
14356            b"\0".as_slice(),
14357            b"core.bare",
14358            b"core.bare\0\0",
14359            b"branch.topic\x01.remote\0",
14360            b"branch.topic\r.remote\0",
14361            b"branch.topic\n.remote\0",
14362            b"branch.topic.\xff\0",
14363        ] {
14364            assert_eq!(
14365                validate_caller_git_config_names(output),
14366                Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig)
14367            );
14368        }
14369
14370        let mut oversized = vec![b'a'; 513];
14371        oversized.push(0);
14372        assert_eq!(
14373            validate_caller_git_config_names(&oversized),
14374            Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig)
14375        );
14376    }
14377
14378    #[cfg(windows)]
14379    #[test]
14380    fn caller_git_config_allows_branch_name_with_slash_in_repository() {
14381        let Some((_temporary, checkout, git)) = initialize_caller_git_fixture() else {
14382            return;
14383        };
14384        fixture_git_output(
14385            &git,
14386            &checkout,
14387            &["branch", "-M", "goal3/windows-live-acceptance"],
14388        );
14389        fixture_git_output(
14390            &git,
14391            &checkout,
14392            &[
14393                "config",
14394                "branch.goal3/windows-live-acceptance.remote",
14395                "origin",
14396            ],
14397        );
14398        fixture_git_output(
14399            &git,
14400            &checkout,
14401            &[
14402                "config",
14403                "branch.goal3/windows-live-acceptance.merge",
14404                "refs/heads/goal3/windows-live-acceptance",
14405            ],
14406        );
14407
14408        CallerGitRepository::open(&checkout).expect("legal branch config");
14409    }
14410
14411    #[cfg(windows)]
14412    #[test]
14413    #[allow(
14414        clippy::too_many_lines,
14415        reason = "one adversarial checkout fixture proves every caller-repository invariant together"
14416    )]
14417    fn caller_git_reader_ignores_replace_refs_and_preserves_git_metadata() {
14418        let Some((_temporary, checkout, git)) = initialize_caller_git_fixture() else {
14419            return;
14420        };
14421        let actual_revision =
14422            String::from_utf8(fixture_git_output(&git, &checkout, &["rev-parse", "HEAD"]))
14423                .expect("revision")
14424                .trim()
14425                .to_owned();
14426        let actual_blob = String::from_utf8(fixture_git_output(
14427            &git,
14428            &checkout,
14429            &["rev-parse", "HEAD:actual.txt"],
14430        ))
14431        .expect("blob")
14432        .trim()
14433        .to_owned();
14434        fs::write(checkout.join("actual.txt"), b"replacement\n").expect("replacement source");
14435        fixture_git_output(&git, &checkout, &["add", "--", "actual.txt"]);
14436        fixture_git_output(
14437            &git,
14438            &checkout,
14439            &[
14440                "-c",
14441                "user.name=RustFerry Test",
14442                "-c",
14443                "user.email=rustferry@example.invalid",
14444                "commit",
14445                "--quiet",
14446                "-m",
14447                "replacement",
14448            ],
14449        );
14450        let replacement_revision =
14451            String::from_utf8(fixture_git_output(&git, &checkout, &["rev-parse", "HEAD"]))
14452                .expect("replacement revision")
14453                .trim()
14454                .to_owned();
14455        fixture_git_output(
14456            &git,
14457            &checkout,
14458            &["checkout", "--quiet", "--detach", &actual_revision],
14459        );
14460        fixture_git_output(
14461            &git,
14462            &checkout,
14463            &["replace", &actual_revision, &replacement_revision],
14464        );
14465        fixture_git_output(
14466            &git,
14467            &checkout,
14468            &[
14469                "config",
14470                "remote.safe.url",
14471                "https://github.com/Owner/Repo.git",
14472            ],
14473        );
14474        fixture_git_output(
14475            &git,
14476            &checkout,
14477            &[
14478                "config",
14479                "remote.safe.pushurl",
14480                "git@github.com:owner/repo.git",
14481            ],
14482        );
14483        let canary = checkout.join("escaped-canary");
14484        let canary_path = canary.to_string_lossy().replace('\\', "/");
14485        let config_path = checkout.join(".git/config");
14486        let mut config = fs::read_to_string(&config_path).expect("config");
14487        std::fmt::Write::write_fmt(
14488            &mut config,
14489            format_args!(
14490                "\n[credential]\n\thelper = !cmd.exe /c echo escaped > {canary_path}\n[http]\n\tproxy = http://127.0.0.1:9\n[core]\n\tsshCommand = cmd.exe /c echo escaped > {canary_path}\n\tfsmonitor = cmd.exe /c echo escaped > {canary_path}\n"
14491            ),
14492        )
14493        .expect("append adversarial config");
14494        fs::write(&config_path, config).expect("malicious inert config");
14495        let nested = checkout.join("nested/project");
14496        fs::create_dir_all(&nested).expect("nested start");
14497        let git_before = test_tree_snapshot(&checkout.join(".git"));
14498        let index_before = fs::metadata(checkout.join(".git/index"))
14499            .expect("index metadata")
14500            .modified()
14501            .expect("index timestamp");
14502
14503        let reader = CallerGitRepository::open(&nested).expect("sealed caller reader");
14504        assert_eq!(
14505            reader.root(),
14506            fs::canonicalize(&checkout).expect("canonical checkout")
14507        );
14508        assert_eq!(
14509            std::str::from_utf8(reader.head_revision().expect("HEAD").stdout())
14510                .expect("HEAD UTF-8")
14511                .trim(),
14512            actual_revision
14513        );
14514        assert!(reader.working_tree_status().expect("status").success());
14515        let remote = reader.discover_remote("safe").expect("remote snapshot");
14516        assert_eq!(
14517            remote.fetch().canonical_url(),
14518            "https://github.com/owner/repo"
14519        );
14520        assert_eq!(remote.push().canonical_url(), "git@github.com:owner/repo");
14521        let tree = reader
14522            .tree_entries(&actual_revision, &["actual.txt".to_owned()])
14523            .expect("tree");
14524        assert!(
14525            tree.stdout()
14526                .windows(actual_blob.len())
14527                .any(|part| part == actual_blob.as_bytes())
14528        );
14529        let batch = reader
14530            .blob_batch(format!("{actual_blob}\n").as_bytes(), 1_024)
14531            .expect("blob batch");
14532        assert!(batch.stdout().windows(7).any(|part| part == b"actual\n"));
14533        assert!(!canary.exists());
14534        assert_eq!(test_tree_snapshot(&checkout.join(".git")), git_before);
14535        assert_eq!(
14536            fs::metadata(checkout.join(".git/index"))
14537                .expect("index metadata after reads")
14538                .modified()
14539                .expect("index timestamp after reads"),
14540            index_before
14541        );
14542    }
14543
14544    #[cfg(windows)]
14545    #[test]
14546    fn caller_git_reader_rejects_external_filters_and_includes_before_execution() {
14547        let Some((_temporary, checkout, _git)) = initialize_caller_git_fixture() else {
14548            return;
14549        };
14550        let canary = checkout.join("filter-canary");
14551        let canary_path = canary.to_string_lossy().replace('\\', "/");
14552        let config_path = checkout.join(".git/config");
14553        let mut config = fs::read_to_string(&config_path).expect("config");
14554        std::fmt::Write::write_fmt(
14555            &mut config,
14556            format_args!(
14557                "\n[filter \"evil\"]\n\tclean = cmd.exe /c echo escaped > {canary_path}\n[include]\n\tpath = missing.config\n"
14558            ),
14559        )
14560        .expect("append unsafe config");
14561        fs::write(config_path, config).expect("unsafe local config");
14562        fs::write(checkout.join(".gitattributes"), b"actual.txt filter=evil\n")
14563            .expect("filter attributes");
14564        assert_eq!(
14565            CallerGitRepository::open(&checkout).expect_err("unsafe config must fail closed"),
14566            GitPublisherConfigError::UnsafeCallerRepositoryConfig
14567        );
14568        assert!(!canary.exists());
14569        for key in [
14570            b"include.path\0".as_slice(),
14571            b"filter.evil.process\0",
14572            b"protocol.ext.allow\0",
14573            b"extensions.partialClone\0",
14574            b"remote.origin.promisor\0",
14575            b"remote.origin.partialCloneFilter\0",
14576        ] {
14577            assert_eq!(
14578                validate_caller_git_config_names(key),
14579                Err(GitPublisherConfigError::UnsafeCallerRepositoryConfig)
14580            );
14581        }
14582    }
14583
14584    #[cfg(windows)]
14585    #[test]
14586    #[allow(
14587        clippy::permissions_set_readonly_false,
14588        reason = "Windows-only fixture clears Git's DOS read-only object bit before removing one loose blob"
14589    )]
14590    fn caller_partial_clone_cannot_lazy_fetch_a_missing_blob_through_ext() {
14591        let Some((_temporary, checkout, git)) = initialize_caller_git_fixture() else {
14592            return;
14593        };
14594        let blob = String::from_utf8(fixture_git_output(
14595            &git,
14596            &checkout,
14597            &["rev-parse", "HEAD:actual.txt"],
14598        ))
14599        .expect("blob ID")
14600        .trim()
14601        .to_owned();
14602        let canary = checkout.join("partial-clone-canary");
14603        let canary_path = canary.to_string_lossy().replace('\\', "/");
14604        for arguments in [
14605            vec!["config", "core.repositoryformatversion", "1"],
14606            vec!["config", "extensions.partialClone", "origin"],
14607            vec!["config", "remote.origin.promisor", "true"],
14608            vec!["config", "remote.origin.partialCloneFilter", "blob:none"],
14609            vec!["config", "protocol.ext.allow", "always"],
14610        ] {
14611            fixture_git_output(&git, &checkout, &arguments);
14612        }
14613        fixture_git_output(
14614            &git,
14615            &checkout,
14616            &[
14617                "config",
14618                "remote.origin.url",
14619                &format!("ext::cmd.exe /c echo escaped > {canary_path}"),
14620            ],
14621        );
14622        let object = checkout
14623            .join(".git/objects")
14624            .join(&blob[..2])
14625            .join(&blob[2..]);
14626        let mut permissions = fs::metadata(&object).expect("loose blob").permissions();
14627        permissions.set_readonly(false);
14628        fs::set_permissions(&object, permissions).expect("writable fixture blob");
14629        fs::remove_file(&object).expect("missing promised blob fixture");
14630        assert_eq!(
14631            CallerGitRepository::open(&checkout)
14632                .expect_err("partial clone config must fail closed"),
14633            GitPublisherConfigError::UnsafeCallerRepositoryConfig
14634        );
14635
14636        let toolchain = crate::git_process::WindowsGitToolchain::new(&git)
14637            .expect("sealed Git-for-Windows toolchain");
14638        let process_state = tempfile::tempdir().expect("process state");
14639        for name in ["home", "xdg", "tmp"] {
14640            fs::create_dir(process_state.path().join(name)).expect("process directory");
14641        }
14642        let context = crate::git_process::GitProcessContext::new(
14643            &checkout,
14644            None,
14645            process_state.path().join("home"),
14646            process_state.path().join("xdg"),
14647            process_state.path().join("tmp"),
14648        )
14649        .expect("offline context");
14650        let spec = toolchain
14651            .process_spec(
14652                &context,
14653                GitNetworkPolicy::Offline,
14654                [
14655                    OsString::from("--git-dir"),
14656                    caller_git_argument_path(&checkout.join(".git")),
14657                    OsString::from("--work-tree"),
14658                    caller_git_argument_path(&checkout),
14659                    OsString::from("cat-file"),
14660                    OsString::from("--batch"),
14661                ],
14662            )
14663            .expect("offline spec");
14664        let mut command = spec.command().expect("sealed command");
14665        command
14666            .stdin(Stdio::piped())
14667            .stdout(Stdio::piped())
14668            .stderr(Stdio::piped());
14669        let output = run_git_process_allow_status(
14670            command,
14671            format!("{blob}\n").as_bytes(),
14672            MAX_GIT_TIMEOUT,
14673            MAX_GIT_OUTPUT_BYTES,
14674        )
14675        .expect("bounded missing-object read");
14676        assert!(output.stdout.windows(7).any(|part| part == b"missing"));
14677        assert!(!canary.exists());
14678    }
14679
14680    #[cfg(windows)]
14681    #[test]
14682    fn caller_git_reader_revalidates_paths_at_the_pre_spawn_boundary() {
14683        let Some((_temporary, checkout, _git)) = initialize_caller_git_fixture() else {
14684            return;
14685        };
14686        let reader = CallerGitRepository::open(&checkout).expect("sealed caller reader");
14687        assert!(fs::rename(&checkout, checkout.with_extension("moved")).is_err());
14688        assert!(
14689            fs::rename(&reader.home, reader.home.with_extension("moved")).is_err(),
14690            "private HOME guard must deny same-path replacement"
14691        );
14692        let config_path = checkout.join(".git/config");
14693        let config_replaced = std::cell::Cell::new(false);
14694        let result = reader.execute_at_with_pre_spawn_hook(
14695            reader.root(),
14696            ["rev-parse", "--verify", "HEAD"],
14697            &[],
14698            MAX_GIT_OUTPUT_BYTES,
14699            || {
14700                config_replaced
14701                    .set(fs::rename(&config_path, config_path.with_extension("replaced")).is_ok());
14702            },
14703        );
14704        assert!(!config_replaced.get());
14705        assert!(result.is_ok());
14706    }
14707
14708    #[cfg(unix)]
14709    #[test]
14710    fn git_timeout_terminates_a_descendant_that_holds_inherited_pipes() {
14711        let temporary = tempfile::tempdir().expect("fixture");
14712        let parent_script = temporary.path().join("parent.sh");
14713        let child_script = temporary.path().join("child.sh");
14714        let ready = temporary.path().join("ready");
14715        let delayed_marker = temporary.path().join("delayed-marker");
14716        fs::write(
14717            &parent_script,
14718            b"#!/bin/sh\n/bin/sh \"$1\" \"$3\" &\n: > \"$2\"\nsleep 30\n",
14719        )
14720        .expect("parent helper");
14721        fs::write(
14722            &child_script,
14723            b"#!/bin/sh\nsleep 2\nprintf escaped > \"$1\"\nsleep 30\n",
14724        )
14725        .expect("child helper");
14726        let mut command = Command::new("/bin/sh");
14727        command
14728            .arg(&parent_script)
14729            .arg(&child_script)
14730            .arg(&ready)
14731            .arg(&delayed_marker)
14732            .stdin(Stdio::piped())
14733            .stdout(Stdio::piped())
14734            .stderr(Stdio::piped());
14735        let started = Instant::now();
14736        assert_eq!(
14737            run_git_process(command, &[], Duration::from_millis(500), 1_024),
14738            Err(GitExecutionError::TimedOut)
14739        );
14740        assert!(started.elapsed() < Duration::from_secs(3));
14741        assert!(ready.exists(), "parent did not spawn its descendant");
14742        thread::sleep(Duration::from_millis(2_250));
14743        assert!(!delayed_marker.exists());
14744    }
14745
14746    #[cfg(windows)]
14747    #[test]
14748    fn git_timeout_terminates_a_descendant_that_holds_inherited_pipes() {
14749        let system_root =
14750            PathBuf::from(std::env::var_os("SystemRoot").expect("Windows SystemRoot"));
14751        let powershell = system_root
14752            .join("System32")
14753            .join("WindowsPowerShell")
14754            .join("v1.0")
14755            .join("powershell.exe");
14756        let temporary = tempfile::tempdir().expect("fixture");
14757        let parent_script = temporary.path().join("parent.ps1");
14758        let child_script = temporary.path().join("child.ps1");
14759        let ready = temporary.path().join("ready");
14760        let delayed_marker = temporary.path().join("delayed-marker");
14761        fs::write(
14762            &parent_script,
14763            concat!(
14764                "param([string]$ChildScript,[string]$Ready,[string]$Marker)\n",
14765                "$child = Start-Process -FilePath \"$PSHOME\\powershell.exe\" ",
14766                "-ArgumentList @('-NoProfile','-NonInteractive','-File',$ChildScript,$Marker) ",
14767                "-NoNewWindow -PassThru\n",
14768                "[IO.File]::WriteAllText($Ready,'ready')\n",
14769                "Start-Sleep -Seconds 30\n",
14770            ),
14771        )
14772        .expect("parent helper");
14773        fs::write(
14774            &child_script,
14775            concat!(
14776                "param([string]$Marker)\n",
14777                "Start-Sleep -Seconds 4\n",
14778                "[IO.File]::WriteAllText($Marker,'escaped')\n",
14779                "Start-Sleep -Seconds 30\n",
14780            ),
14781        )
14782        .expect("child helper");
14783        let mut command = Command::new(powershell);
14784        command
14785            .args(["-NoProfile", "-NonInteractive", "-File"])
14786            .arg(&parent_script)
14787            .arg(&child_script)
14788            .arg(&ready)
14789            .arg(&delayed_marker)
14790            .stdin(Stdio::piped())
14791            .stdout(Stdio::piped())
14792            .stderr(Stdio::piped());
14793        let started = Instant::now();
14794        assert_eq!(
14795            run_git_process(command, &[], Duration::from_secs(2), 1_024),
14796            Err(GitExecutionError::TimedOut)
14797        );
14798        assert!(started.elapsed() < Duration::from_secs(5));
14799        assert!(ready.exists(), "parent did not spawn its descendant");
14800        thread::sleep(Duration::from_millis(4_250));
14801        assert!(!delayed_marker.exists());
14802    }
14803
14804    fn test_tree_snapshot(root: &Path) -> BTreeMap<PathBuf, Option<Vec<u8>>> {
14805        fn visit(root: &Path, path: &Path, snapshot: &mut BTreeMap<PathBuf, Option<Vec<u8>>>) {
14806            let mut entries = fs::read_dir(path)
14807                .expect("read snapshot directory")
14808                .map(|entry| entry.expect("read snapshot entry").path())
14809                .collect::<Vec<_>>();
14810            entries.sort();
14811            for entry in entries {
14812                let relative = entry
14813                    .strip_prefix(root)
14814                    .expect("snapshot path below root")
14815                    .to_owned();
14816                let metadata = fs::symlink_metadata(&entry).expect("snapshot metadata");
14817                if metadata.is_dir() {
14818                    snapshot.insert(relative, None);
14819                    visit(root, &entry, snapshot);
14820                } else {
14821                    snapshot.insert(relative, Some(fs::read(&entry).expect("snapshot file")));
14822                }
14823            }
14824        }
14825
14826        let mut snapshot = BTreeMap::new();
14827        visit(root, root, &mut snapshot);
14828        snapshot
14829    }
14830
14831    #[derive(Clone, Debug)]
14832    struct FixedClock(u64);
14833
14834    impl ProviderClock for FixedClock {
14835        fn now_ms(&self) -> u64 {
14836            self.0
14837        }
14838    }
14839
14840    #[derive(Clone, Debug)]
14841    struct MutableClock(Arc<AtomicU64>);
14842
14843    impl MutableClock {
14844        fn new(now_ms: u64) -> Self {
14845            Self(Arc::new(AtomicU64::new(now_ms)))
14846        }
14847
14848        fn set(&self, now_ms: u64) {
14849            self.0.store(now_ms, Ordering::SeqCst);
14850        }
14851    }
14852
14853    impl ProviderClock for MutableClock {
14854        fn now_ms(&self) -> u64 {
14855            self.0.load(Ordering::SeqCst)
14856        }
14857    }
14858
14859    #[derive(Clone, Debug, Default)]
14860    struct CapturingCheckpointSink {
14861        checkpoints: Arc<Mutex<Vec<GithubJobResumeV1>>>,
14862    }
14863
14864    impl GithubJobCheckpointSink for CapturingCheckpointSink {
14865        fn checkpoint(&mut self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
14866            self.checkpoints
14867                .lock()
14868                .expect("checkpoint capture")
14869                .push(resume.clone());
14870            Ok(())
14871        }
14872    }
14873
14874    #[derive(Clone, Copy, Debug, Default)]
14875    struct FailingCheckpointSink;
14876
14877    impl GithubJobCheckpointSink for FailingCheckpointSink {
14878        fn checkpoint(&mut self, _resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
14879            Err(provider_failure(
14880                "checkpoint_fixture_failed",
14881                "checkpoint fixture failed",
14882                true,
14883            ))
14884        }
14885    }
14886
14887    #[derive(Clone, Debug, Default)]
14888    struct WriteThenFailCheckpointSink {
14889        checkpoints: Arc<Mutex<Vec<GithubJobResumeV1>>>,
14890        failed_once: bool,
14891    }
14892
14893    impl GithubJobCheckpointSink for WriteThenFailCheckpointSink {
14894        fn checkpoint(&mut self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
14895            self.checkpoints
14896                .lock()
14897                .expect("checkpoint capture")
14898                .push(resume.clone());
14899            if !self.failed_once {
14900                self.failed_once = true;
14901                return Err(provider_failure(
14902                    "checkpoint_durability_uncertain",
14903                    "checkpoint may have been published before durability failed",
14904                    true,
14905                ));
14906            }
14907            Ok(())
14908        }
14909    }
14910
14911    #[derive(Clone, Debug, Default)]
14912    struct FailOnWorkflowDispatchReceiptSink {
14913        checkpoints: Arc<Mutex<Vec<GithubJobResumeV1>>>,
14914    }
14915
14916    impl GithubJobCheckpointSink for FailOnWorkflowDispatchReceiptSink {
14917        fn checkpoint(&mut self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
14918            if resume
14919                .workflow_dispatch
14920                .as_ref()
14921                .is_some_and(|dispatch| dispatch.receipt.is_some())
14922                && resume.run.is_none()
14923            {
14924                return Err(provider_failure(
14925                    "workflow_dispatch_receipt_checkpoint_failed",
14926                    "workflow dispatch receipt checkpoint fixture failed",
14927                    true,
14928                ));
14929            }
14930            self.checkpoints
14931                .lock()
14932                .expect("workflow dispatch checkpoints")
14933                .push(resume.clone());
14934            Ok(())
14935        }
14936    }
14937
14938    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
14939    enum SnapshotCleanupAuditEvent {
14940        Checkpoint(GithubGitSnapshotPhaseV1),
14941        Acquire,
14942        Delete,
14943        Release,
14944    }
14945
14946    #[derive(Clone, Debug)]
14947    struct SnapshotCleanupCheckpointSink {
14948        events: Arc<Mutex<Vec<SnapshotCleanupAuditEvent>>>,
14949        fail_deleted_once: bool,
14950    }
14951
14952    impl GithubJobCheckpointSink for SnapshotCleanupCheckpointSink {
14953        fn checkpoint(&mut self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
14954            let phase = resume
14955                .git_snapshot
14956                .as_ref()
14957                .expect("snapshot checkpoint")
14958                .phase;
14959            self.events
14960                .lock()
14961                .expect("snapshot cleanup events")
14962                .push(SnapshotCleanupAuditEvent::Checkpoint(phase));
14963            if phase == GithubGitSnapshotPhaseV1::SourceDeleted && self.fail_deleted_once {
14964                self.fail_deleted_once = false;
14965                return Err(provider_failure(
14966                    "checkpoint_fixture_failed",
14967                    "snapshot source deletion checkpoint failed",
14968                    true,
14969                ));
14970            }
14971            Ok(())
14972        }
14973    }
14974
14975    #[derive(Clone, Debug)]
14976    struct FailOnSnapshotPhaseSink {
14977        checkpoints: Arc<Mutex<Vec<GithubJobResumeV1>>>,
14978        phase: GithubGitSnapshotPhaseV1,
14979        failed_once: bool,
14980    }
14981
14982    impl GithubJobCheckpointSink for FailOnSnapshotPhaseSink {
14983        fn checkpoint(&mut self, resume: &GithubJobResumeV1) -> RemoteBuildResult<()> {
14984            self.checkpoints
14985                .lock()
14986                .expect("snapshot checkpoints")
14987                .push(resume.clone());
14988            if resume.git_snapshot.as_ref().map(|snapshot| snapshot.phase) == Some(self.phase)
14989                && self.failed_once
14990            {
14991                self.failed_once = false;
14992                return Err(provider_failure(
14993                    "checkpoint_fixture_failed",
14994                    "snapshot phase checkpoint failed",
14995                    true,
14996                ));
14997            }
14998            Ok(())
14999        }
15000    }
15001
15002    #[derive(Debug)]
15003    struct SnapshotCleanupAuditPublisher {
15004        events: Arc<Mutex<Vec<SnapshotCleanupAuditEvent>>>,
15005        source_lease_owned: bool,
15006        deletion_count: usize,
15007        keepalive_release_count: usize,
15008    }
15009
15010    impl TemporaryRefPublisher for SnapshotCleanupAuditPublisher {
15011        fn supports_git_snapshot(&self) -> bool {
15012            true
15013        }
15014
15015        fn doctor(
15016            &mut self,
15017            _request: &TemporaryRefDoctorRequest<'_>,
15018        ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
15019            Ok(TemporaryRefPublisherReadiness {
15020                trusted_ref_tip: CommitSha::new(TRUSTED_TIP).expect("trusted tip"),
15021            })
15022        }
15023
15024        fn publication_attempt_is_process_fenced(&self) -> bool {
15025            true
15026        }
15027
15028        fn publication_lease_scope_sha256(&self) -> Option<&str> {
15029            Some(FAKE_LEASE_SCOPE)
15030        }
15031
15032        fn acquire_git_snapshot_source_lease(
15033            &mut self,
15034            _request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
15035        ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
15036            self.events
15037                .lock()
15038                .expect("snapshot cleanup events")
15039                .push(SnapshotCleanupAuditEvent::Acquire);
15040            if self.source_lease_owned {
15041                Ok(TemporaryRefPublicationLease::AlreadyOwned)
15042            } else {
15043                self.source_lease_owned = true;
15044                Ok(TemporaryRefPublicationLease::Acquired)
15045            }
15046        }
15047
15048        fn release_git_snapshot_source_lease(
15049            &mut self,
15050            _request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
15051        ) {
15052            assert!(
15053                self.source_lease_owned,
15054                "cleanup source lease must be owned"
15055            );
15056            self.events
15057                .lock()
15058                .expect("snapshot cleanup events")
15059                .push(SnapshotCleanupAuditEvent::Release);
15060            self.source_lease_owned = false;
15061        }
15062
15063        fn delete_git_snapshot_source(
15064            &mut self,
15065            request: &GitSnapshotSourceDeleteRequest<'_>,
15066        ) -> Result<(), TemporaryRefPublishError> {
15067            assert!(self.source_lease_owned, "delete must hold the source lease");
15068            assert_eq!(request.expected_commit.as_str(), SOURCE_SHA);
15069            self.events
15070                .lock()
15071                .expect("snapshot cleanup events")
15072                .push(SnapshotCleanupAuditEvent::Delete);
15073            self.deletion_count += 1;
15074            Ok(())
15075        }
15076
15077        fn release_git_snapshot_keepalive(
15078            &mut self,
15079            _request: &GitSnapshotKeepaliveRequest<'_>,
15080        ) -> Result<(), TemporaryRefPublishError> {
15081            self.keepalive_release_count += 1;
15082            Ok(())
15083        }
15084
15085        fn publish(
15086            &mut self,
15087            _request: &TemporaryRefPublishRequest<'_>,
15088        ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
15089            Err(TemporaryRefPublishError::GitSnapshotUnsupported.into())
15090        }
15091
15092        fn delete_temporary_ref(
15093            &mut self,
15094            _request: &TemporaryRefDeleteRequest<'_>,
15095        ) -> Result<(), TemporaryRefPublishError> {
15096            Err(TemporaryRefPublishError::GitSnapshotUnsupported)
15097        }
15098    }
15099
15100    #[derive(Clone, Debug, Eq, PartialEq)]
15101    enum SnapshotFlowEvent {
15102        AdoptStage,
15103        ReconcileKeepalive,
15104        Import,
15105        CreateKeepalive,
15106        DeleteStage,
15107        AcquireSourceLease,
15108        PublishSource,
15109        ReconcileSource,
15110        ReleaseSourceLease,
15111        ReleaseKeepalive,
15112        PrepareDispatch {
15113            execution_repository: String,
15114            source_repository: String,
15115            source_ref: String,
15116        },
15117        AcquireDispatchLease,
15118        PublishDispatch,
15119        ReleaseDispatchLease,
15120    }
15121
15122    #[derive(Debug)]
15123    struct SnapshotFlowPublisher {
15124        events: Vec<SnapshotFlowEvent>,
15125        keepalive_exact: bool,
15126        keepalive_reconciliations: VecDeque<GitSnapshotRefReconciliation>,
15127        source_publication: GitSnapshotSourcePublication,
15128        source_reconciliation: GitSnapshotRefReconciliation,
15129        source_reconciliations: VecDeque<GitSnapshotRefReconciliation>,
15130        source_lease_owned: bool,
15131        dispatch_lease_owned: bool,
15132    }
15133
15134    impl SnapshotFlowPublisher {
15135        fn exact() -> Self {
15136            Self {
15137                events: Vec::new(),
15138                keepalive_exact: false,
15139                keepalive_reconciliations: VecDeque::new(),
15140                source_publication: GitSnapshotSourcePublication::Exact,
15141                source_reconciliation: GitSnapshotRefReconciliation::Exact,
15142                source_reconciliations: VecDeque::new(),
15143                source_lease_owned: false,
15144                dispatch_lease_owned: false,
15145            }
15146        }
15147    }
15148
15149    impl TemporaryRefPublisher for SnapshotFlowPublisher {
15150        fn supports_git_snapshot(&self) -> bool {
15151            true
15152        }
15153
15154        fn doctor(
15155            &mut self,
15156            _request: &TemporaryRefDoctorRequest<'_>,
15157        ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
15158            Ok(TemporaryRefPublisherReadiness {
15159                trusted_ref_tip: CommitSha::new(TRUSTED_TIP).expect("trusted tip"),
15160            })
15161        }
15162
15163        fn adopt_git_snapshot_stage(
15164            &mut self,
15165            _request: &IosDeviceBuildRequest,
15166        ) -> Result<(GitSnapshotStageLocatorV1, GitSnapshotStageV1), TemporaryRefPublishError>
15167        {
15168            self.events.push(SnapshotFlowEvent::AdoptStage);
15169            Err(TemporaryRefPublishError::PublicationVerificationFailed)
15170        }
15171
15172        fn import_git_snapshot(
15173            &mut self,
15174            request: &GitSnapshotImportRequest<'_>,
15175        ) -> Result<GitSnapshotObjectGraphV1, TemporaryRefPublishError> {
15176            self.events.push(SnapshotFlowEvent::Import);
15177            Ok(request.expected_stage.graph.clone())
15178        }
15179
15180        fn delete_git_snapshot_stage(
15181            &mut self,
15182            _request: &GitSnapshotImportRequest<'_>,
15183        ) -> Result<(), TemporaryRefPublishError> {
15184            self.events.push(SnapshotFlowEvent::DeleteStage);
15185            Ok(())
15186        }
15187
15188        fn reconcile_git_snapshot_keepalive(
15189            &mut self,
15190            _request: &GitSnapshotKeepaliveRequest<'_>,
15191        ) -> Result<GitSnapshotRefReconciliation, TemporaryRefPublishError> {
15192            self.events.push(SnapshotFlowEvent::ReconcileKeepalive);
15193            Ok(self
15194                .keepalive_reconciliations
15195                .pop_front()
15196                .unwrap_or(if self.keepalive_exact {
15197                    GitSnapshotRefReconciliation::Exact
15198                } else {
15199                    GitSnapshotRefReconciliation::Absent
15200                }))
15201        }
15202
15203        fn create_git_snapshot_keepalive(
15204            &mut self,
15205            _request: &GitSnapshotKeepaliveRequest<'_>,
15206        ) -> Result<(), TemporaryRefPublishError> {
15207            self.events.push(SnapshotFlowEvent::CreateKeepalive);
15208            self.keepalive_exact = true;
15209            Ok(())
15210        }
15211
15212        fn release_git_snapshot_keepalive(
15213            &mut self,
15214            _request: &GitSnapshotKeepaliveRequest<'_>,
15215        ) -> Result<(), TemporaryRefPublishError> {
15216            self.events.push(SnapshotFlowEvent::ReleaseKeepalive);
15217            self.keepalive_exact = false;
15218            Ok(())
15219        }
15220
15221        fn publish_git_snapshot_source(
15222            &mut self,
15223            request: &GitSnapshotSourcePublishRequest<'_>,
15224        ) -> Result<GitSnapshotSourcePublication, TemporaryRefPublishError> {
15225            assert!(request.authorized);
15226            self.events.push(SnapshotFlowEvent::PublishSource);
15227            self.source_reconciliation = match self.source_publication {
15228                GitSnapshotSourcePublication::Exact => GitSnapshotRefReconciliation::Exact,
15229                GitSnapshotSourcePublication::Conflict => GitSnapshotRefReconciliation::Conflict,
15230                GitSnapshotSourcePublication::Uncertain => GitSnapshotRefReconciliation::Absent,
15231            };
15232            Ok(self.source_publication)
15233        }
15234
15235        fn reconcile_git_snapshot_source(
15236            &mut self,
15237            _request: &GitSnapshotSourcePublishRequest<'_>,
15238        ) -> Result<GitSnapshotRefReconciliation, TemporaryRefPublishError> {
15239            self.events.push(SnapshotFlowEvent::ReconcileSource);
15240            Ok(self
15241                .source_reconciliations
15242                .pop_front()
15243                .unwrap_or(self.source_reconciliation))
15244        }
15245
15246        fn publication_attempt_is_process_fenced(&self) -> bool {
15247            true
15248        }
15249
15250        fn publication_lease_scope_sha256(&self) -> Option<&str> {
15251            Some(FAKE_LEASE_SCOPE)
15252        }
15253
15254        fn acquire_git_snapshot_source_lease(
15255            &mut self,
15256            _request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
15257        ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
15258            self.events.push(SnapshotFlowEvent::AcquireSourceLease);
15259            if self.source_lease_owned {
15260                Ok(TemporaryRefPublicationLease::AlreadyOwned)
15261            } else {
15262                self.source_lease_owned = true;
15263                Ok(TemporaryRefPublicationLease::Acquired)
15264            }
15265        }
15266
15267        fn release_git_snapshot_source_lease(
15268            &mut self,
15269            _request: &GitSnapshotSourcePublicationLeaseRequest<'_>,
15270        ) {
15271            assert!(self.source_lease_owned);
15272            self.events.push(SnapshotFlowEvent::ReleaseSourceLease);
15273            self.source_lease_owned = false;
15274        }
15275
15276        fn prepare(
15277            &mut self,
15278            request: &TemporaryRefPublishRequest<'_>,
15279        ) -> Result<Option<PublishedTemporaryRef>, TemporaryRefPublishError> {
15280            let source_ref = request
15281                .snapshot_source_ref
15282                .expect("snapshot dispatch source ref");
15283            self.events.push(SnapshotFlowEvent::PrepareDispatch {
15284                execution_repository: repository_url(request.repository),
15285                source_repository: request.source_repository.to_owned(),
15286                source_ref: source_ref.as_str().to_owned(),
15287            });
15288            Ok(Some(PublishedTemporaryRef::new(
15289                request.temporary_ref.clone(),
15290                CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
15291            )))
15292        }
15293
15294        fn acquire_publication_lease(
15295            &mut self,
15296            _request: &TemporaryRefPublicationLeaseRequest<'_>,
15297        ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
15298            self.events.push(SnapshotFlowEvent::AcquireDispatchLease);
15299            if self.dispatch_lease_owned {
15300                Ok(TemporaryRefPublicationLease::AlreadyOwned)
15301            } else {
15302                self.dispatch_lease_owned = true;
15303                Ok(TemporaryRefPublicationLease::Acquired)
15304            }
15305        }
15306
15307        fn release_publication_lease(
15308            &mut self,
15309            _request: &TemporaryRefPublicationLeaseRequest<'_>,
15310        ) {
15311            assert!(self.dispatch_lease_owned);
15312            self.events.push(SnapshotFlowEvent::ReleaseDispatchLease);
15313            self.dispatch_lease_owned = false;
15314        }
15315
15316        fn publish(
15317            &mut self,
15318            request: &TemporaryRefPublishRequest<'_>,
15319        ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
15320            assert!(request.snapshot_source_ref.is_some());
15321            self.events.push(SnapshotFlowEvent::PublishDispatch);
15322            Ok(PublishedTemporaryRef::new(
15323                request.temporary_ref.clone(),
15324                CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
15325            ))
15326        }
15327
15328        fn publish_prepared(
15329            &mut self,
15330            request: &TemporaryRefPublishRequest<'_>,
15331            prepared: Option<&PublishedTemporaryRef>,
15332        ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
15333            let expected = PublishedTemporaryRef::new(
15334                request.temporary_ref.clone(),
15335                CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
15336            );
15337            if prepared != Some(&expected) {
15338                return Err(TemporaryRefPublishError::PublicationVerificationFailed.into());
15339            }
15340            self.publish(request)
15341        }
15342
15343        fn delete_temporary_ref(
15344            &mut self,
15345            _request: &TemporaryRefDeleteRequest<'_>,
15346        ) -> Result<(), TemporaryRefPublishError> {
15347            Ok(())
15348        }
15349    }
15350
15351    impl TemporaryRefReconciler for SnapshotFlowPublisher {
15352        fn reconcile_temporary_ref(
15353            &mut self,
15354            _request: &TemporaryRefReconcileRequest<'_>,
15355        ) -> Result<TemporaryRefReconciliation, TemporaryRefPublishError> {
15356            Ok(TemporaryRefReconciliation::Missing)
15357        }
15358    }
15359
15360    #[derive(Clone, Copy, Debug, Default)]
15361    struct DoctorArtifactStore;
15362
15363    impl VerifiedArtifactStore for DoctorArtifactStore {
15364        fn supports_listing(&self) -> bool {
15365            true
15366        }
15367
15368        fn supports_download(&self) -> bool {
15369            true
15370        }
15371
15372        fn supports_removal(&self) -> bool {
15373            false
15374        }
15375
15376        fn supports_signed_cleanup_evidence(&self) -> bool {
15377            true
15378        }
15379
15380        fn list_verified(
15381            &mut self,
15382            _context: &GithubArtifactContext,
15383        ) -> RemoteBuildResult<Vec<ArtifactManifest>> {
15384            Ok(Vec::new())
15385        }
15386
15387        fn download_verified(
15388            &mut self,
15389            _context: &GithubArtifactContext,
15390            _request: &ArtifactDownloadRequest,
15391        ) -> RemoteBuildResult<ArtifactDownloadResult> {
15392            Err(unsupported(ProviderFeature::ArtifactDownload))
15393        }
15394
15395        fn remove_artifacts(&mut self, _context: &GithubArtifactContext) -> RemoteBuildResult<()> {
15396            Err(unsupported(ProviderFeature::Cleanup))
15397        }
15398    }
15399
15400    #[derive(Clone, Copy, Debug, Default)]
15401    struct FailingArtifactStore;
15402
15403    impl VerifiedArtifactStore for FailingArtifactStore {
15404        fn supports_listing(&self) -> bool {
15405            true
15406        }
15407
15408        fn supports_download(&self) -> bool {
15409            true
15410        }
15411
15412        fn supports_removal(&self) -> bool {
15413            false
15414        }
15415
15416        fn list_verified(
15417            &mut self,
15418            _context: &GithubArtifactContext,
15419        ) -> RemoteBuildResult<Vec<ArtifactManifest>> {
15420            Err(provider_failure(
15421                "artifact_fixture_rejected",
15422                "artifact fixture rejected",
15423                false,
15424            ))
15425        }
15426
15427        fn download_verified(
15428            &mut self,
15429            _context: &GithubArtifactContext,
15430            _request: &ArtifactDownloadRequest,
15431        ) -> RemoteBuildResult<ArtifactDownloadResult> {
15432            Err(unsupported(ProviderFeature::ArtifactDownload))
15433        }
15434
15435        fn remove_artifacts(&mut self, _context: &GithubArtifactContext) -> RemoteBuildResult<()> {
15436            Err(unsupported(ProviderFeature::Cleanup))
15437        }
15438    }
15439
15440    #[derive(Clone, Debug, Default)]
15441    struct CapturingArtifactStore {
15442        contexts: Arc<Mutex<Vec<GithubArtifactContext>>>,
15443    }
15444
15445    impl VerifiedArtifactStore for CapturingArtifactStore {
15446        fn supports_listing(&self) -> bool {
15447            true
15448        }
15449
15450        fn supports_download(&self) -> bool {
15451            false
15452        }
15453
15454        fn supports_removal(&self) -> bool {
15455            false
15456        }
15457
15458        fn list_verified(
15459            &mut self,
15460            context: &GithubArtifactContext,
15461        ) -> RemoteBuildResult<Vec<ArtifactManifest>> {
15462            self.contexts
15463                .lock()
15464                .expect("contexts")
15465                .push(context.clone());
15466            Ok(Vec::new())
15467        }
15468
15469        fn download_verified(
15470            &mut self,
15471            _context: &GithubArtifactContext,
15472            _request: &ArtifactDownloadRequest,
15473        ) -> RemoteBuildResult<ArtifactDownloadResult> {
15474            Err(unsupported(ProviderFeature::ArtifactDownload))
15475        }
15476
15477        fn remove_artifacts(&mut self, _context: &GithubArtifactContext) -> RemoteBuildResult<()> {
15478            Err(unsupported(ProviderFeature::Cleanup))
15479        }
15480    }
15481
15482    #[derive(Debug, Default)]
15483    struct FakePublisher {
15484        published: Vec<CapturedPublication>,
15485        deletions: Vec<CapturedDeletion>,
15486        doctor_failure: Option<TemporaryRefPublishError>,
15487        execution_workflow_failure: Option<TemporaryRefPublishError>,
15488        execution_workflow_checks: u64,
15489        prepare_failure: Option<TemporaryRefPublishError>,
15490        failure: Option<TemporaryRefPublishFailure>,
15491        delete_failure: Option<TemporaryRefPublishError>,
15492        cancel_on_publish: Option<CancellationToken>,
15493        returned_temporary_ref: Option<TemporaryGitRef>,
15494        prepared_publication: Option<PublishedTemporaryRef>,
15495        reconciliation: Option<TemporaryRefReconciliation>,
15496        publication_leases: BTreeSet<String>,
15497        publication_lease_blocked: bool,
15498        publication_lease_unsupported: bool,
15499    }
15500
15501    #[derive(Debug)]
15502    struct CapturedPublication {
15503        source_revision: String,
15504        branch: String,
15505        workflow_path: String,
15506        workflow_sha256: String,
15507        manifest: GithubDispatchManifest,
15508        authorized: bool,
15509    }
15510
15511    #[derive(Debug)]
15512    struct CapturedDeletion {
15513        branch: String,
15514        expected_commit: String,
15515        authorized: bool,
15516    }
15517
15518    impl TemporaryRefPublisher for FakePublisher {
15519        fn publication_attempt_is_process_fenced(&self) -> bool {
15520            true
15521        }
15522
15523        fn publication_lease_scope_sha256(&self) -> Option<&str> {
15524            (!self.publication_lease_unsupported).then_some(FAKE_LEASE_SCOPE)
15525        }
15526
15527        fn acquire_publication_lease(
15528            &mut self,
15529            request: &TemporaryRefPublicationLeaseRequest<'_>,
15530        ) -> Result<TemporaryRefPublicationLease, TemporaryRefPublishError> {
15531            if self.publication_lease_unsupported {
15532                return Ok(TemporaryRefPublicationLease::Unsupported);
15533            }
15534            if self.publication_lease_blocked {
15535                return Ok(TemporaryRefPublicationLease::HeldByOther);
15536            }
15537            if self
15538                .publication_leases
15539                .insert(publication_lease_key(request))
15540            {
15541                Ok(TemporaryRefPublicationLease::Acquired)
15542            } else {
15543                Ok(TemporaryRefPublicationLease::AlreadyOwned)
15544            }
15545        }
15546
15547        fn release_publication_lease(&mut self, request: &TemporaryRefPublicationLeaseRequest<'_>) {
15548            self.publication_leases
15549                .remove(&publication_lease_key(request));
15550        }
15551
15552        fn doctor(
15553            &mut self,
15554            _request: &TemporaryRefDoctorRequest<'_>,
15555        ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
15556            if let Some(error) = self.doctor_failure.take() {
15557                return Err(error);
15558            }
15559            Ok(TemporaryRefPublisherReadiness {
15560                trusted_ref_tip: CommitSha::new(TRUSTED_TIP).expect("trusted tip"),
15561            })
15562        }
15563
15564        fn verify_execution_workflow(
15565            &mut self,
15566            _request: &ExecutionWorkflowDoctorRequest<'_>,
15567        ) -> Result<TemporaryRefPublisherReadiness, TemporaryRefPublishError> {
15568            self.execution_workflow_checks += 1;
15569            if let Some(error) = self.execution_workflow_failure.take() {
15570                return Err(error);
15571            }
15572            Ok(TemporaryRefPublisherReadiness {
15573                trusted_ref_tip: CommitSha::new(TRUSTED_TIP).expect("trusted tip"),
15574            })
15575        }
15576
15577        fn prepare(
15578            &mut self,
15579            _request: &TemporaryRefPublishRequest<'_>,
15580        ) -> Result<Option<PublishedTemporaryRef>, TemporaryRefPublishError> {
15581            if let Some(error) = self.prepare_failure.take() {
15582                return Err(error);
15583            }
15584            Ok(self.prepared_publication.clone())
15585        }
15586
15587        fn publish(
15588            &mut self,
15589            request: &TemporaryRefPublishRequest<'_>,
15590        ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
15591            if let Some(token) = self.cancel_on_publish.take() {
15592                token.cancel();
15593            }
15594            if let Some(failure) = self.failure.take() {
15595                return Err(failure);
15596            }
15597            self.published.push(CapturedPublication {
15598                source_revision: request.source_revision.as_str().to_owned(),
15599                branch: request.temporary_ref.branch().as_str().to_owned(),
15600                workflow_path: request.workflow_path.to_owned(),
15601                workflow_sha256: hex_sha256(request.workflow_bytes),
15602                manifest: serde_json::from_slice(request.manifest_bytes)
15603                    .expect("strict manifest JSON"),
15604                authorized: request.authorized,
15605            });
15606            Ok(PublishedTemporaryRef::new(
15607                self.returned_temporary_ref
15608                    .clone()
15609                    .unwrap_or_else(|| request.temporary_ref.clone()),
15610                CommitSha::new(DISPATCH_SHA).expect("dispatch SHA"),
15611            ))
15612        }
15613
15614        fn publish_prepared(
15615            &mut self,
15616            request: &TemporaryRefPublishRequest<'_>,
15617            prepared: Option<&PublishedTemporaryRef>,
15618        ) -> Result<PublishedTemporaryRef, TemporaryRefPublishFailure> {
15619            let candidate = PublishedTemporaryRef::new(
15620                self.returned_temporary_ref
15621                    .clone()
15622                    .unwrap_or_else(|| request.temporary_ref.clone()),
15623                CommitSha::new(DISPATCH_SHA).expect("dispatch SHA"),
15624            );
15625            if prepared.is_some_and(|expected| expected != &candidate) {
15626                return Err(TemporaryRefPublishError::PublicationVerificationFailed.into());
15627            }
15628            self.publish(request)
15629        }
15630
15631        fn delete_temporary_ref(
15632            &mut self,
15633            request: &TemporaryRefDeleteRequest<'_>,
15634        ) -> Result<(), TemporaryRefPublishError> {
15635            self.deletions.push(CapturedDeletion {
15636                branch: request.temporary_ref.branch().as_str().to_owned(),
15637                expected_commit: request.expected_commit.as_str().to_owned(),
15638                authorized: request.authorized,
15639            });
15640            if let Some(error) = self.delete_failure.take() {
15641                return Err(error);
15642            }
15643            Ok(())
15644        }
15645    }
15646
15647    impl TemporaryRefReconciler for FakePublisher {
15648        fn reconcile_temporary_ref(
15649            &mut self,
15650            _request: &TemporaryRefReconcileRequest<'_>,
15651        ) -> Result<TemporaryRefReconciliation, TemporaryRefPublishError> {
15652            Ok(self
15653                .reconciliation
15654                .take()
15655                .unwrap_or(TemporaryRefReconciliation::Missing))
15656        }
15657    }
15658
15659    #[derive(Debug, Default)]
15660    struct FakeWorkflowDispatchState {
15661        responses: Mutex<
15662            VecDeque<Result<GithubWorkflowDispatchHttpResponse, GithubWorkflowDispatchHttpError>>,
15663        >,
15664        requests: Mutex<Vec<GithubWorkflowDispatchHttpRequest>>,
15665        clients_created: AtomicU64,
15666        post_gate: Option<Arc<FakeWorkflowDispatchPostGate>>,
15667    }
15668
15669    #[derive(Debug, Default)]
15670    struct FakeWorkflowDispatchPostGate {
15671        entered: Mutex<bool>,
15672        entered_changed: Condvar,
15673        released: Mutex<bool>,
15674        released_changed: Condvar,
15675    }
15676
15677    impl FakeWorkflowDispatchPostGate {
15678        fn block_post(&self) {
15679            *self.entered.lock().expect("dispatch gate entered") = true;
15680            self.entered_changed.notify_all();
15681            let mut released = self.released.lock().expect("dispatch gate released");
15682            while !*released {
15683                released = self
15684                    .released_changed
15685                    .wait(released)
15686                    .expect("dispatch gate wait");
15687            }
15688        }
15689
15690        fn wait_until_entered(&self) {
15691            let entered = self.entered.lock().expect("dispatch gate entered");
15692            let (entered, timeout) = self
15693                .entered_changed
15694                .wait_timeout_while(entered, StdDuration::from_secs(5), |entered| !*entered)
15695                .expect("dispatch gate wait");
15696            assert!(
15697                *entered && !timeout.timed_out(),
15698                "dispatch POST did not start"
15699            );
15700        }
15701
15702        fn release(&self) {
15703            *self.released.lock().expect("dispatch gate released") = true;
15704            self.released_changed.notify_all();
15705        }
15706    }
15707
15708    #[derive(Clone, Debug, Default)]
15709    struct FakeWorkflowDispatchPlan(Arc<FakeWorkflowDispatchState>);
15710
15711    impl FakeWorkflowDispatchPlan {
15712        fn with_response(response: GithubWorkflowDispatchHttpResponse) -> Self {
15713            let plan = Self::default();
15714            plan.0
15715                .responses
15716                .lock()
15717                .expect("dispatch responses")
15718                .push_back(Ok(response));
15719            plan
15720        }
15721
15722        fn with_blocking_response(
15723            response: GithubWorkflowDispatchHttpResponse,
15724        ) -> (Self, Arc<FakeWorkflowDispatchPostGate>) {
15725            Self::with_blocking_result(Ok(response))
15726        }
15727
15728        fn with_blocking_result(
15729            response: Result<GithubWorkflowDispatchHttpResponse, GithubWorkflowDispatchHttpError>,
15730        ) -> (Self, Arc<FakeWorkflowDispatchPostGate>) {
15731            let gate = Arc::new(FakeWorkflowDispatchPostGate::default());
15732            let state = FakeWorkflowDispatchState {
15733                responses: Mutex::new(VecDeque::from([response])),
15734                requests: Mutex::new(Vec::new()),
15735                clients_created: AtomicU64::new(0),
15736                post_gate: Some(Arc::clone(&gate)),
15737            };
15738            (Self(Arc::new(state)), gate)
15739        }
15740    }
15741
15742    #[derive(Debug)]
15743    struct FakeWorkflowDispatchFactory(FakeWorkflowDispatchPlan);
15744
15745    impl GithubWorkflowDispatchHttpClientFactory for FakeWorkflowDispatchFactory {
15746        fn create(
15747            self: Box<Self>,
15748        ) -> Result<Box<dyn GithubWorkflowDispatchHttpClient + Send>, GhExecutionError> {
15749            self.0.0.clients_created.fetch_add(1, Ordering::SeqCst);
15750            Ok(Box::new(FakeWorkflowDispatchClient(self.0)))
15751        }
15752    }
15753
15754    #[derive(Debug)]
15755    struct FakeWorkflowDispatchClient(FakeWorkflowDispatchPlan);
15756
15757    impl GithubWorkflowDispatchHttpClient for FakeWorkflowDispatchClient {
15758        fn post(
15759            &mut self,
15760            request: &GithubWorkflowDispatchHttpRequest,
15761        ) -> Result<GithubWorkflowDispatchHttpResponse, GithubWorkflowDispatchHttpError> {
15762            self.0
15763                .0
15764                .requests
15765                .lock()
15766                .expect("dispatch requests")
15767                .push(request.clone());
15768            if let Some(gate) = &self.0.0.post_gate {
15769                gate.block_post();
15770            }
15771            self.0
15772                .0
15773                .responses
15774                .lock()
15775                .expect("dispatch responses")
15776                .pop_front()
15777                .expect("unexpected workflow dispatch POST")
15778        }
15779    }
15780
15781    #[derive(Debug, Default)]
15782    struct FakeGhRunner {
15783        responses: VecDeque<Result<Vec<u8>, GhExecutionError>>,
15784        requests: Arc<Mutex<Vec<GhRequest>>>,
15785        workflow_dispatch: Option<FakeWorkflowDispatchPlan>,
15786        cancel_before_invocation: Option<(u64, CancellationToken)>,
15787        invocations: u64,
15788    }
15789
15790    impl FakeGhRunner {
15791        fn with(responses: impl IntoIterator<Item = Result<Vec<u8>, GhExecutionError>>) -> Self {
15792            Self {
15793                responses: responses.into_iter().collect(),
15794                requests: Arc::new(Mutex::new(Vec::new())),
15795                workflow_dispatch: None,
15796                cancel_before_invocation: None,
15797                invocations: 0,
15798            }
15799        }
15800
15801        fn with_workflow_dispatch(mut self, plan: FakeWorkflowDispatchPlan) -> Self {
15802            self.workflow_dispatch = Some(plan);
15803            self
15804        }
15805
15806        fn cancelling_before(mut self, invocation: u64, cancellation: CancellationToken) -> Self {
15807            self.cancel_before_invocation = Some((invocation, cancellation));
15808            self
15809        }
15810    }
15811
15812    impl GhRunner for FakeGhRunner {
15813        fn execute(&mut self, request: &GhRequest) -> Result<Vec<u8>, GhExecutionError> {
15814            self.invocations = self.invocations.saturating_add(1);
15815            if self
15816                .cancel_before_invocation
15817                .as_ref()
15818                .is_some_and(|(invocation, _)| *invocation == self.invocations)
15819            {
15820                self.cancel_before_invocation
15821                    .as_ref()
15822                    .expect("checked cancellation")
15823                    .1
15824                    .cancel();
15825            }
15826            self.requests
15827                .lock()
15828                .expect("request capture")
15829                .push(request.clone());
15830            self.responses.pop_front().expect("unexpected gh request")
15831        }
15832
15833        fn workflow_dispatch_client_factory(
15834            &self,
15835        ) -> Result<Box<dyn GithubWorkflowDispatchHttpClientFactory>, GhExecutionError> {
15836            self.workflow_dispatch.clone().map_or_else(
15837                || Err(GhExecutionError::AuthenticationUnavailable),
15838                |plan| {
15839                    Ok(Box::new(FakeWorkflowDispatchFactory(plan))
15840                        as Box<dyn GithubWorkflowDispatchHttpClientFactory>)
15841                },
15842            )
15843        }
15844    }
15845
15846    #[derive(Clone, Debug)]
15847    struct CapturedGitInvocation {
15848        arguments: Vec<OsString>,
15849        stdin_len: usize,
15850        environment: Vec<(GitEnvironmentVariable, OsString)>,
15851        network: GitNetworkPolicy,
15852        stdout_limit: usize,
15853    }
15854
15855    #[derive(Debug, Default)]
15856    struct FakeGitRunner {
15857        responses: VecDeque<Result<Vec<u8>, GitExecutionError>>,
15858        invocations: Vec<CapturedGitInvocation>,
15859        cancel_after_invocations: Option<(usize, CancellationToken)>,
15860    }
15861
15862    impl FakeGitRunner {
15863        fn with(responses: impl IntoIterator<Item = Result<Vec<u8>, GitExecutionError>>) -> Self {
15864            Self {
15865                responses: responses.into_iter().collect(),
15866                invocations: Vec::new(),
15867                cancel_after_invocations: None,
15868            }
15869        }
15870
15871        fn cancelling_after(
15872            responses: impl IntoIterator<Item = Result<Vec<u8>, GitExecutionError>>,
15873            invocation_count: usize,
15874            cancellation: CancellationToken,
15875        ) -> Self {
15876            Self {
15877                responses: responses.into_iter().collect(),
15878                invocations: Vec::new(),
15879                cancel_after_invocations: Some((invocation_count, cancellation)),
15880            }
15881        }
15882
15883        fn record_and_respond(
15884            &mut self,
15885            invocation: &GitInvocation,
15886            stdin_len: usize,
15887        ) -> Result<Vec<u8>, GitExecutionError> {
15888            self.invocations.push(CapturedGitInvocation {
15889                arguments: invocation.arguments().to_vec(),
15890                stdin_len,
15891                environment: invocation.environment().to_vec(),
15892                network: invocation.network,
15893                stdout_limit: invocation.stdout_limit(),
15894            });
15895            if let Some((count, cancellation)) = &self.cancel_after_invocations
15896                && self.invocations.len() == *count
15897            {
15898                cancellation.cancel();
15899            }
15900            self.responses
15901                .pop_front()
15902                .expect("unexpected git invocation")
15903        }
15904    }
15905
15906    impl GitRunner for FakeGitRunner {
15907        fn execute(&mut self, invocation: &GitInvocation) -> Result<Vec<u8>, GitExecutionError> {
15908            self.record_and_respond(invocation, invocation.stdin_len())
15909        }
15910
15911        fn execute_file(
15912            &mut self,
15913            invocation: &GitInvocation,
15914            mut input: fs::File,
15915            expected_len: u64,
15916        ) -> Result<Vec<u8>, GitExecutionError> {
15917            let mut bytes = Vec::new();
15918            input
15919                .read_to_end(&mut bytes)
15920                .map_err(|_| GitExecutionError::ProcessIo)?;
15921            if u64::try_from(bytes.len()).map_err(|_| GitExecutionError::ProcessIo)? != expected_len
15922            {
15923                return Err(GitExecutionError::ProcessIo);
15924            }
15925            self.record_and_respond(invocation, bytes.len())
15926        }
15927
15928        fn process_tree_is_parent_fenced(&self) -> bool {
15929            true
15930        }
15931    }
15932
15933    fn workflow_config_with_secret_names_and_trigger(
15934        secret_names: SigningSecretNames,
15935        run_trigger: WorkflowRunTrigger,
15936    ) -> WorkflowConfig {
15937        let trusted_source_ref = match run_trigger {
15938            WorkflowRunTrigger::Push => "refs/heads/goal3/macless-iphone-builds",
15939            WorkflowRunTrigger::WorkflowDispatch => "refs/heads/main",
15940        };
15941        workflow_config_with_secret_names_trigger_and_trusted_ref(
15942            secret_names,
15943            run_trigger,
15944            trusted_source_ref,
15945        )
15946    }
15947
15948    fn workflow_config_with_secret_names_trigger_and_trusted_ref(
15949        secret_names: SigningSecretNames,
15950        run_trigger: WorkflowRunTrigger,
15951        trusted_source_ref: &str,
15952    ) -> WorkflowConfig {
15953        WorkflowConfig::new_with_run_trigger(
15954            WorkflowFileName::new("rustferry-goal3-iphone.yml").expect("workflow name"),
15955            ProtectedEnvironment::new("rustferry-goal3-signing").expect("environment"),
15956            secret_names,
15957            WorkerDistribution::new(
15958                "https://github.com/ShiroKSH/rust-and-iphone/releases/download/v0.1.0/ferry-worker-macos",
15959                "0".repeat(64),
15960                "0.1.0",
15961            )
15962            .expect("worker"),
15963            PublicSourceRepository::new("shiroksh/rust-and-iphone")
15964                .expect("source repository"),
15965            TrustedSourceRef::new(trusted_source_ref).expect("trusted ref"),
15966            TemporaryBranchNamespace::new("rustferry/goal3/builds").expect("namespace"),
15967            run_trigger,
15968        )
15969        .expect("workflow config")
15970    }
15971
15972    fn workflow_config_with_secret_names(secret_names: SigningSecretNames) -> WorkflowConfig {
15973        workflow_config_with_secret_names_and_trigger(secret_names, WorkflowRunTrigger::Push)
15974    }
15975
15976    fn workflow_config() -> WorkflowConfig {
15977        workflow_config_with_secret_names(SigningSecretNames::goal3_defaults())
15978    }
15979
15980    fn same_repository_provider_config(
15981        authorization: GithubMutationAuthorization,
15982    ) -> GithubProviderConfig {
15983        let workflow = workflow_config();
15984        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
15985        GithubProviderConfig::new(
15986            Repository::new("ShiroKSH", "rust-and-iphone").expect("repository"),
15987            "https://github.com/shiroksh/rust-and-iphone",
15988            workflow,
15989            fingerprint,
15990            GithubPollingPolicy::new(3, Duration::from_millis(250)).expect("poll policy"),
15991            authorization,
15992            &Version::new(0, 1, 0),
15993            8,
15994        )
15995        .expect("provider config")
15996    }
15997
15998    fn provider_config(authorization: GithubMutationAuthorization) -> GithubProviderConfig {
15999        let workflow = workflow_config();
16000        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
16001        GithubProviderConfig::new(
16002            Repository::new("ShiroKSH", "rustferry-signing").expect("execution repository"),
16003            "https://github.com/shiroksh/rust-and-iphone",
16004            workflow,
16005            fingerprint,
16006            GithubPollingPolicy::new(3, Duration::from_millis(250)).expect("poll policy"),
16007            authorization,
16008            &Version::new(0, 1, 0),
16009            8,
16010        )
16011        .expect("split provider config")
16012    }
16013
16014    fn workflow_dispatch_provider_config(
16015        authorization: GithubMutationAuthorization,
16016    ) -> GithubProviderConfig {
16017        let workflow = workflow_config_with_secret_names_and_trigger(
16018            SigningSecretNames::goal3_defaults(),
16019            WorkflowRunTrigger::WorkflowDispatch,
16020        );
16021        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
16022        GithubProviderConfig::new(
16023            Repository::new("ShiroKSH", "rustferry-signing").expect("execution repository"),
16024            "https://github.com/shiroksh/rust-and-iphone",
16025            workflow,
16026            fingerprint,
16027            GithubPollingPolicy::new(3, Duration::from_millis(250)).expect("poll policy"),
16028            authorization,
16029            &Version::new(0, 1, 0),
16030            8,
16031        )
16032        .expect("workflow dispatch provider config")
16033    }
16034
16035    fn workflow_dispatch_provider_config_with_trusted_ref(
16036        authorization: GithubMutationAuthorization,
16037        trusted_source_ref: &str,
16038    ) -> GithubProviderConfig {
16039        let workflow = workflow_config_with_secret_names_trigger_and_trusted_ref(
16040            SigningSecretNames::goal3_defaults(),
16041            WorkflowRunTrigger::WorkflowDispatch,
16042            trusted_source_ref,
16043        );
16044        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
16045        GithubProviderConfig::new(
16046            Repository::new("ShiroKSH", "rustferry-signing").expect("execution repository"),
16047            "https://github.com/shiroksh/rust-and-iphone",
16048            workflow,
16049            fingerprint,
16050            GithubPollingPolicy::new(3, Duration::from_millis(250)).expect("poll policy"),
16051            authorization,
16052            &Version::new(0, 1, 0),
16053            8,
16054        )
16055        .expect("workflow dispatch provider config")
16056    }
16057
16058    fn provider_config_with_secret_names(
16059        authorization: GithubMutationAuthorization,
16060        secret_names: SigningSecretNames,
16061    ) -> GithubProviderConfig {
16062        let workflow = workflow_config_with_secret_names(secret_names);
16063        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
16064        GithubProviderConfig::new(
16065            Repository::new("ShiroKSH", "rustferry-signing").expect("execution repository"),
16066            "https://github.com/shiroksh/rust-and-iphone",
16067            workflow,
16068            fingerprint,
16069            GithubPollingPolicy::new(3, Duration::from_millis(250)).expect("poll policy"),
16070            authorization,
16071            &Version::new(0, 1, 0),
16072            8,
16073        )
16074        .expect("split provider config")
16075    }
16076
16077    fn transport(runner: FakeGhRunner) -> GithubTransport<FakeGhRunner> {
16078        GithubTransport::new(runner, TransportLimits::secure_defaults())
16079    }
16080
16081    fn expected_publication() -> PublishedTemporaryRef {
16082        PublishedTemporaryRef::new(
16083            TemporaryGitRef::new(
16084                provider_config(all_authorized())
16085                    .workflow
16086                    .temporary_branch_namespace(),
16087                BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
16088            )
16089            .expect("temporary ref"),
16090            CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
16091        )
16092    }
16093
16094    fn private_repository_row() -> Vec<u8> {
16095        b"991\tShiroKSH/rustferry-signing\ttrue\tfalse\tfalse\tmain\n".to_vec()
16096    }
16097
16098    fn authenticated_user_row() -> Vec<u8> {
16099        b"42\tU2hpcm9LU0g=\n".to_vec()
16100    }
16101
16102    fn public_source_repository_row() -> Vec<u8> {
16103        b"992\tShiroKSH/rust-and-iphone\tfalse\tfalse\tfalse\tmain\n".to_vec()
16104    }
16105
16106    fn private_source_repository_row() -> Vec<u8> {
16107        b"992\tShiroKSH/rust-and-iphone\ttrue\tfalse\tfalse\tmain\n".to_vec()
16108    }
16109
16110    fn signed_transport(mut runner: FakeGhRunner) -> GithubTransport<FakeGhRunner> {
16111        runner.responses.push_front(Ok(exact_signing_secret_rows()));
16112        runner.responses.push_front(Ok(exact_signing_policy_row()));
16113        runner
16114            .responses
16115            .push_front(Ok(signing_environment_row(false, true, true)));
16116        runner.responses.push_front(Ok(private_repository_row()));
16117        runner
16118            .responses
16119            .push_front(Ok(public_source_repository_row()));
16120        transport(runner)
16121    }
16122
16123    fn valid_request() -> IosDeviceBuildRequest {
16124        let team = DevelopmentTeam::new("ABCDE12345", None).expect("team");
16125        let secret =
16126            |name| SecretReference::new(SecretReferenceKind::GithubActions, name).expect("secret");
16127        let signing = SigningPlan {
16128            mode: SigningMode::ManualDevelopment,
16129            signing: Some(SigningReference {
16130                identity: SigningIdentity {
16131                    certificate: SigningCertificate {
16132                        common_name: "Apple Development".to_owned(),
16133                        sha256_fingerprint: "A".repeat(64),
16134                        team: team.clone(),
16135                        expires_at_unix_seconds: u64::MAX,
16136                    },
16137                    private_key: SigningPrivateKeyReference {
16138                        reference: secret("RUSTFERRY_GOAL3_IOS_CERTIFICATE_P12"),
16139                    },
16140                },
16141                password: Some(secret("RUSTFERRY_GOAL3_IOS_CERTIFICATE_PASSWORD")),
16142            }),
16143            team: Some(DevelopmentTeamPlan {
16144                expected: team.clone(),
16145            }),
16146            device: Some(DevicePlan::from_sha256("0".repeat(64), None).expect("device")),
16147            targets: vec![SigningTarget {
16148                name: "App".to_owned(),
16149                bundle_identifier: BundleIdentifier::new("com.example.app").expect("bundle"),
16150                kind: SigningTargetKind::Application,
16151            }],
16152            provisioning: vec![ProvisioningPlan {
16153                target: "App".to_owned(),
16154                profile: secret("RUSTFERRY_GOAL3_IOS_PROVISIONING_PROFILE"),
16155                profile_type: ProvisioningProfileType::Development,
16156            }],
16157            entitlements: vec![EntitlementPlan {
16158                target: "App".to_owned(),
16159                required: EntitlementSet::new(BTreeMap::new()).expect("entitlements"),
16160            }],
16161            allow_provisioning_updates: false,
16162        };
16163        let request = IosDeviceBuildRequest {
16164            protocol_version: CURRENT_PROTOCOL_VERSION,
16165            operation_id: "operation-1".to_owned(),
16166            product_name: "App".to_owned(),
16167            bundle_identifier: "com.example.app".to_owned(),
16168            minimum_ios_version: "16.0".to_owned(),
16169            product: IosDeviceProductExpectation {
16170                app_directory_name: "App.app".to_owned(),
16171                executable: "App".to_owned(),
16172                app_version: "1.0.0".to_owned(),
16173                build_number: "1".to_owned(),
16174                nested_bundles: Vec::new(),
16175            },
16176            profile: BuildProfile::Release,
16177            source_mode: SourceMode::Git,
16178            source_repository: Some("https://github.com/shiroksh/rust-and-iphone".to_owned()),
16179            source_revision: Some(SOURCE_SHA.to_owned()),
16180            source: compile_source_manifest(),
16181            signing,
16182            requested_artifacts: BTreeSet::from([
16183                IosArtifactType::Ipa,
16184                IosArtifactType::SigningReport,
16185            ]),
16186        };
16187        request.validate().expect("valid Git request");
16188        request
16189    }
16190
16191    fn multi_profile_request() -> (IosDeviceBuildRequest, SigningSecretNames) {
16192        let mut request = valid_request();
16193        let targets = vec![
16194            request.signing.targets[0].clone(),
16195            SigningTarget {
16196                name: "Widget".to_owned(),
16197                bundle_identifier: BundleIdentifier::new("com.example.app.widget")
16198                    .expect("widget bundle"),
16199                kind: SigningTargetKind::Extension,
16200            },
16201        ];
16202        let names = SigningSecretNames::for_targets(&targets).expect("profile names");
16203        let secret = |name: &str| {
16204            SecretReference::new(SecretReferenceKind::GithubActions, name).expect("secret")
16205        };
16206        request.signing.targets = targets;
16207        request.product.nested_bundles = vec![UnsignedNestedBundleExpectation {
16208            relative_path: "PlugIns/Widget.appex".to_owned(),
16209            bundle_identifier: "com.example.app.widget".to_owned(),
16210            executable: "Widget".to_owned(),
16211            kind: UnsignedNestedBundleKind::AppExtension,
16212        }];
16213        request.signing.provisioning = vec![
16214            ProvisioningPlan {
16215                target: "App".to_owned(),
16216                profile: secret(
16217                    names
16218                        .profile_for_target("App")
16219                        .expect("application profile")
16220                        .as_str(),
16221                ),
16222                profile_type: ProvisioningProfileType::Development,
16223            },
16224            ProvisioningPlan {
16225                target: "Widget".to_owned(),
16226                profile: secret(
16227                    names
16228                        .profile_for_target("Widget")
16229                        .expect("widget profile")
16230                        .as_str(),
16231                ),
16232                profile_type: ProvisioningProfileType::Development,
16233            },
16234        ];
16235        request.signing.entitlements.push(EntitlementPlan {
16236            target: "Widget".to_owned(),
16237            required: EntitlementSet::new(BTreeMap::new()).expect("widget entitlements"),
16238        });
16239        request.validate().expect("valid multi-profile request");
16240        (request, names)
16241    }
16242
16243    fn multi_profile_request_with_framework() -> (IosDeviceBuildRequest, SigningSecretNames) {
16244        let (mut request, _) = multi_profile_request();
16245        request.signing.targets.push(SigningTarget {
16246            name: "RuntimeBridge".to_owned(),
16247            bundle_identifier: BundleIdentifier::new("com.example.app.runtime-bridge")
16248                .expect("framework bundle"),
16249            kind: SigningTargetKind::Framework,
16250        });
16251        request
16252            .product
16253            .nested_bundles
16254            .push(UnsignedNestedBundleExpectation {
16255                relative_path: "Frameworks/RuntimeBridge.framework".to_owned(),
16256                bundle_identifier: "com.example.app.runtime-bridge".to_owned(),
16257                executable: "RuntimeBridge".to_owned(),
16258                kind: UnsignedNestedBundleKind::Framework,
16259            });
16260        request
16261            .product
16262            .nested_bundles
16263            .sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
16264        request
16265            .validate()
16266            .expect("valid multi-profile request with framework");
16267        let names = SigningSecretNames::for_targets(&request.signing.targets)
16268            .expect("framework-bound profile names");
16269        (request, names)
16270    }
16271
16272    fn assert_signing_reference_mismatch(
16273        config: &GithubProviderConfig,
16274        request: &IosDeviceBuildRequest,
16275    ) {
16276        let error = validate_submission(config, request)
16277            .expect_err("configured signing target graph must remain exact");
16278        assert!(matches!(
16279            error,
16280            RemoteBuildError::ProviderFailure {
16281                code,
16282                retryable: false,
16283                ..
16284            } if code == "signing_secret_reference_mismatch"
16285        ));
16286    }
16287
16288    fn unsigned_request() -> IosDeviceBuildRequest {
16289        let mut request = valid_request();
16290        request.signing.mode = SigningMode::UnsignedCompileOnly;
16291        request.signing.signing = None;
16292        request.signing.team = None;
16293        request.signing.device = None;
16294        request.signing.provisioning.clear();
16295        request.requested_artifacts = BTreeSet::from([IosArtifactType::Xcarchive]);
16296        request.validate().expect("valid unsigned Git request");
16297        request
16298    }
16299
16300    struct SnapshotResumeFixture {
16301        isolation: TestPrivateDirectory,
16302        resume: GithubJobResumeV1,
16303    }
16304
16305    fn snapshot_object(byte: char) -> GitSha1ObjectId {
16306        GitSha1ObjectId::new(byte.to_string().repeat(40)).expect("snapshot object ID")
16307    }
16308
16309    #[allow(
16310        clippy::too_many_lines,
16311        reason = "the fixture constructs one complete descriptor, stage, locator, and durable resume binding"
16312    )]
16313    fn snapshot_resume_fixture(phase: GithubGitSnapshotPhaseV1) -> SnapshotResumeFixture {
16314        let config = provider_config(all_authorized());
16315        let mut request = unsigned_request();
16316        request.source_mode = SourceMode::GitSnapshot;
16317        request.source_revision = None;
16318        let isolation = tempdir().expect("snapshot isolation");
16319        let stage_directory = crate::snapshot::GitSnapshotStageDirectory::create(
16320            isolation.path(),
16321            &request.operation_id,
16322        )
16323        .expect("snapshot stage directory");
16324        let archive_bytes = SNAPSHOT_ARCHIVE_BYTES;
16325        let archive = SourceArchive {
16326            size: u64::try_from(archive_bytes.len()).expect("archive size"),
16327            sha256: hex_sha256(archive_bytes),
16328        };
16329        let mut archive_file = fs::OpenOptions::new()
16330            .write(true)
16331            .create_new(true)
16332            .open(stage_directory.archive_path())
16333            .expect("create snapshot archive");
16334        archive_file
16335            .write_all(archive_bytes)
16336            .expect("write snapshot archive");
16337        archive_file.sync_all().expect("sync snapshot archive");
16338        drop(archive_file);
16339        let archive_identity = stage_directory
16340            .seal_archive(&archive)
16341            .expect("seal snapshot archive");
16342        let descriptor = GitSnapshotDescriptor::from_request(
16343            &request,
16344            SourceBundleDescriptor::new(archive.clone(), request.source.clone()),
16345        )
16346        .expect("snapshot descriptor");
16347        let descriptor_bytes =
16348            rustferry_remote::canonical_git_snapshot_descriptor_bytes(&descriptor)
16349                .expect("canonical snapshot descriptor");
16350        let descriptor_identity = stage_directory
16351            .write_descriptor_create_new(&descriptor)
16352            .expect("write snapshot descriptor");
16353        let graph = GitSnapshotObjectGraphV1 {
16354            schema_version: crate::snapshot::GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION,
16355            archive_blob: snapshot_object('1'),
16356            descriptor_blob: snapshot_object('2'),
16357            goal3_tree: snapshot_object('3'),
16358            rustferry_tree: snapshot_object('4'),
16359            root_tree: snapshot_object('5'),
16360            commit: GitSha1ObjectId::new(SOURCE_SHA).expect("snapshot commit"),
16361        };
16362        let source_created_at_ms = 1_700_000_000_000;
16363        request.source_revision = Some(SOURCE_SHA.to_owned());
16364        request.validate().expect("valid snapshot request");
16365        let stage = GitSnapshotStageV1 {
16366            schema_version: crate::snapshot::GIT_SNAPSHOT_STAGE_SCHEMA_VERSION,
16367            operation_id: request.operation_id.clone(),
16368            isolation_root_identity: stage_directory.isolation_root_identity().to_owned(),
16369            snapshots_store_identity: stage_directory.snapshots_store_identity().to_owned(),
16370            stage_directory_identity: stage_directory.stage_directory_identity().to_owned(),
16371            source_repository: config.source_repository.clone(),
16372            source_ref: GitSnapshotSourceRef::for_operation(&request.operation_id)
16373                .expect("snapshot source ref"),
16374            keepalive_ref: GitSnapshotKeepaliveRef::for_operation(&request.operation_id)
16375                .expect("snapshot keepalive ref"),
16376            source_created_at_ms,
16377            consent_sha256: "9".repeat(64),
16378            request_template_sha256: descriptor.request_template_sha256.clone(),
16379            manifest_sha256: request.source.sha256.clone(),
16380            archive,
16381            descriptor_sha256: hex_sha256(&descriptor_bytes),
16382            final_request: request.clone(),
16383            archive_file_identity: archive_identity.to_string(),
16384            descriptor_file_identity: descriptor_identity.to_string(),
16385            graph,
16386        };
16387        let locator = stage_directory
16388            .publish_metadata_create_new(&stage, &descriptor, &request)
16389            .expect("publish snapshot metadata");
16390        let source_phase = !matches!(
16391            phase,
16392            GithubGitSnapshotPhaseV1::Prepared
16393                | GithubGitSnapshotPhaseV1::KeepaliveIntent
16394                | GithubGitSnapshotPhaseV1::KeepaliveExact
16395                | GithubGitSnapshotPhaseV1::StageDeleteIntent
16396                | GithubGitSnapshotPhaseV1::StageDeleted
16397        );
16398        let source_armed = matches!(
16399            phase,
16400            GithubGitSnapshotPhaseV1::SourceExact
16401                | GithubGitSnapshotPhaseV1::SourceAbsent
16402                | GithubGitSnapshotPhaseV1::SourceConflict
16403                | GithubGitSnapshotPhaseV1::SourceCleanupIntent
16404                | GithubGitSnapshotPhaseV1::SourceDeleted
16405                | GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent
16406                | GithubGitSnapshotPhaseV1::KeepaliveReleased
16407        );
16408        let absence_proven = phase == GithubGitSnapshotPhaseV1::SourceAbsent;
16409        let source_started_at_ms = if source_armed {
16410            source_created_at_ms
16411        } else {
16412            0
16413        };
16414        let source_deadline_ms = if !source_phase {
16415            0
16416        } else if source_armed {
16417            publication_quiescence_deadline(source_started_at_ms, 0)
16418        } else {
16419            u64::MAX
16420        };
16421        let mut resume = durable_prepublication_resume();
16422        resume.request = request;
16423        resume.request_sha256 = canonical_request_sha256(&resume.request).expect("request digest");
16424        resume.source_revision = SOURCE_SHA.to_owned();
16425        resume.created_at_ms = source_created_at_ms;
16426        resume.git_snapshot = Some(GithubGitSnapshotResumeV1 {
16427            schema_version: GITHUB_GIT_SNAPSHOT_RESUME_SCHEMA_VERSION,
16428            stage_locator: locator,
16429            stage,
16430            phase,
16431            source_publication_attempts: u8::from(source_phase),
16432            source_publication_started_at_ms: source_started_at_ms,
16433            source_publication_quiescence_deadline_ms: source_deadline_ms,
16434            source_publication_process_fenced: source_phase,
16435            source_publication_lease_scope_sha256: source_phase
16436                .then(|| FAKE_LEASE_SCOPE.to_owned()),
16437            source_publication_absence_observations: if absence_proven { 2 } else { 0 },
16438            source_publication_absence_first_observed_at_ms: if absence_proven {
16439                source_deadline_ms
16440            } else {
16441                0
16442            },
16443            source_publication_absence_last_observed_at_ms: if absence_proven {
16444                source_deadline_ms
16445            } else {
16446                0
16447            },
16448            keepalive_release_authorization_sha256: None,
16449        });
16450        resume.prepared_dispatch_commit = None;
16451        resume.publication_started_at_ms = 0;
16452        resume.publication_quiescence_deadline_ms = u64::MAX;
16453        resume.publication_intent = false;
16454        resume.publication_process_fenced = false;
16455        resume.publication_lease_scope_sha256 = None;
16456        resume.publication_absence_observations = 0;
16457        resume.publication_absence_first_observed_at_ms = 0;
16458        resume.run_discovery_deadline_ms =
16459            source_created_at_ms.saturating_add(config.poll_policy.discovery_window_ms());
16460        resume
16461            .git_snapshot
16462            .as_ref()
16463            .expect("snapshot resume")
16464            .validate_for_request(&resume.request)
16465            .expect("valid snapshot resume");
16466        SnapshotResumeFixture { isolation, resume }
16467    }
16468
16469    fn completed_snapshot_resume(
16470        phase: GithubGitSnapshotPhaseV1,
16471        state: JobState,
16472    ) -> SnapshotResumeFixture {
16473        let mut fixture = snapshot_resume_fixture(phase);
16474        let snapshot_request = fixture.resume.request.clone();
16475        let snapshot = fixture.resume.git_snapshot.take().expect("snapshot resume");
16476        let mut resume = durable_success_resume();
16477        resume.request = snapshot_request;
16478        resume.request_sha256 =
16479            canonical_request_sha256(&resume.request).expect("snapshot request digest");
16480        resume.source_revision = SOURCE_SHA.to_owned();
16481        resume.git_snapshot = Some(snapshot);
16482        resume.compile_evidence = Some(compile_evidence_for(&resume.request));
16483        resume.state = state;
16484        resume.cleanup_requested = matches!(
16485            state,
16486            JobState::Cleaning | JobState::Cleaned | JobState::CleanupFailed
16487        );
16488        resume.temporary_ref_deleted = resume.cleanup_requested;
16489        fixture.resume = resume;
16490        fixture
16491    }
16492
16493    fn durable_identity_for_resume(resume: &GithubJobResumeV1) -> GithubDurableIdentityV1 {
16494        GithubDurableIdentityV1 {
16495            provider: resume.provider.clone(),
16496            provider_config_sha256: resume.provider_config_sha256.clone(),
16497            principal: resume.principal.clone(),
16498            execution_repository: resume.execution_repository.clone(),
16499            execution_repository_id: resume.execution_repository_id,
16500        }
16501    }
16502
16503    struct InitialSnapshotPrecomputeFixture {
16504        isolation: TestPrivateDirectory,
16505        stage_directory: GitSnapshotStageDirectory,
16506        archive_identity: rustferry_core::RegularFileFilesystemIdentity,
16507        descriptor_identity: rustferry_core::RegularFileFilesystemIdentity,
16508        archive: SourceArchive,
16509        descriptor: GitSnapshotDescriptor,
16510    }
16511
16512    impl InitialSnapshotPrecomputeFixture {
16513        fn inputs(&self) -> GitSnapshotPrecomputeInputs {
16514            self.stage_directory
16515                .precompute_inputs(
16516                    &self.archive_identity,
16517                    &self.descriptor_identity,
16518                    &self.archive,
16519                    &self.descriptor,
16520                )
16521                .expect("retained initial snapshot inputs")
16522        }
16523    }
16524
16525    fn initial_snapshot_precompute_fixture() -> InitialSnapshotPrecomputeFixture {
16526        let isolation = tempdir().expect("snapshot precompute isolation");
16527        let mut request = unsigned_request();
16528        request.source_mode = SourceMode::GitSnapshot;
16529        request.source_revision = None;
16530        let stage_directory =
16531            GitSnapshotStageDirectory::create(isolation.path(), request.operation_id.as_str())
16532                .expect("snapshot precompute stage");
16533        let archive = SourceArchive {
16534            size: u64::try_from(SNAPSHOT_ARCHIVE_BYTES.len()).expect("snapshot archive size"),
16535            sha256: hex_sha256(SNAPSHOT_ARCHIVE_BYTES),
16536        };
16537        let archive_identity = stage_directory
16538            .write_archive_bytes_create_new(SNAPSHOT_ARCHIVE_BYTES, &archive)
16539            .expect("snapshot precompute archive");
16540        let descriptor = GitSnapshotDescriptor::from_request(
16541            &request,
16542            SourceBundleDescriptor::new(archive.clone(), request.source.clone()),
16543        )
16544        .expect("snapshot precompute descriptor");
16545        let descriptor_identity = stage_directory
16546            .write_descriptor_create_new(&descriptor)
16547            .expect("snapshot precompute descriptor file");
16548        InitialSnapshotPrecomputeFixture {
16549            isolation,
16550            stage_directory,
16551            archive_identity,
16552            descriptor_identity,
16553            archive,
16554            descriptor,
16555        }
16556    }
16557
16558    fn initial_snapshot_graph_responses() -> Vec<Result<Vec<u8>, GitExecutionError>> {
16559        vec![
16560            Ok(b"sha1\n".to_vec()),
16561            Ok(format!("{}\n", "1".repeat(40)).into_bytes()),
16562            Ok(format!("{}\n", "2".repeat(40)).into_bytes()),
16563            Ok(format!("{}\n", "3".repeat(40)).into_bytes()),
16564            Ok(format!("{}\n", "4".repeat(40)).into_bytes()),
16565            Ok(format!("{}\n", "5".repeat(40)).into_bytes()),
16566            Ok(format!("{}\n", "6".repeat(40)).into_bytes()),
16567        ]
16568    }
16569
16570    fn exact_retry_git_cycle(
16571        parent: &GithubJobResumeV1,
16572        archive_bytes: &[u8],
16573        archive_blob: char,
16574    ) -> Vec<Result<Vec<u8>, GitExecutionError>> {
16575        let snapshot = parent.git_snapshot.as_ref().expect("snapshot resume");
16576        vec![
16577            Ok(format!(
16578                "{}\0{}\n",
16579                snapshot.stage.keepalive_ref.as_str(),
16580                snapshot.stage.graph.commit.as_str()
16581            )
16582            .into_bytes()),
16583            Ok(b"sha1\n".to_vec()),
16584            Ok(archive_bytes.to_vec()),
16585            Ok(format!("{}\n", archive_blob.to_string().repeat(40)).into_bytes()),
16586            Ok(format!("{}\n", "a".repeat(40)).into_bytes()),
16587            Ok(format!("{}\n", "b".repeat(40)).into_bytes()),
16588            Ok(format!("{}\n", "c".repeat(40)).into_bytes()),
16589            Ok(format!("{}\n", "d".repeat(40)).into_bytes()),
16590            Ok(format!("{}\n", "e".repeat(40)).into_bytes()),
16591            Ok(format!(
16592                "{}\0{}\n",
16593                snapshot.stage.keepalive_ref.as_str(),
16594                snapshot.stage.graph.commit.as_str()
16595            )
16596            .into_bytes()),
16597        ]
16598    }
16599
16600    fn synthetic_exact_retry_plan(parent: &GithubJobResumeV1) -> GithubGitSnapshotExactRetryPlanV1 {
16601        let parent_stage = &parent.git_snapshot.as_ref().expect("snapshot").stage;
16602        let mut request = parent.request.clone();
16603        request.operation_id = "operation-2".to_owned();
16604        let mut graph = parent_stage.graph.clone();
16605        graph.commit = snapshot_object('e');
16606        request.source_revision = Some(graph.commit.as_str().to_owned());
16607        request.validate().expect("synthetic retry request");
16608        let descriptor = GitSnapshotDescriptor::from_request(
16609            &request,
16610            SourceBundleDescriptor::new(parent_stage.archive.clone(), request.source.clone()),
16611        )
16612        .expect("synthetic retry descriptor");
16613        let descriptor_bytes = canonical_git_snapshot_descriptor_bytes(&descriptor)
16614            .expect("synthetic descriptor bytes");
16615        GithubGitSnapshotExactRetryPlanV1 {
16616            request,
16617            source_created_at_ms: 1_700_000_001_000,
16618            request_template_sha256: descriptor.request_template_sha256,
16619            manifest_sha256: parent_stage.manifest_sha256.clone(),
16620            archive: parent_stage.archive.clone(),
16621            descriptor_sha256: hex_sha256(&descriptor_bytes),
16622            graph,
16623        }
16624    }
16625
16626    fn same_invocation_snapshot_submission(
16627        fixture: &SnapshotResumeFixture,
16628    ) -> GithubGitSnapshotSubmissionV1 {
16629        let resume = &fixture.resume;
16630        let snapshot = resume.git_snapshot.as_ref().expect("snapshot resume");
16631        GithubGitSnapshotSubmissionV1::same_invocation(
16632            GithubDurableIdentityV1 {
16633                provider: resume.provider.clone(),
16634                provider_config_sha256: resume.provider_config_sha256.clone(),
16635                principal: resume.principal.clone(),
16636                execution_repository: resume.execution_repository.clone(),
16637                execution_repository_id: resume.execution_repository_id,
16638            },
16639            resume.request.clone(),
16640            snapshot.stage.consent_sha256.clone(),
16641            snapshot.stage_locator.clone(),
16642            snapshot.stage.clone(),
16643        )
16644        .expect("same-invocation snapshot submission")
16645    }
16646
16647    fn public_snapshot_identity_rows(
16648        boundary_count: usize,
16649    ) -> Vec<Result<Vec<u8>, GhExecutionError>> {
16650        let mut rows = Vec::with_capacity(boundary_count.saturating_mul(3));
16651        for _ in 0..boundary_count {
16652            rows.push(Ok(authenticated_user_row()));
16653            rows.push(Ok(private_repository_row()));
16654            rows.push(Ok(public_source_repository_row()));
16655        }
16656        rows
16657    }
16658
16659    fn compile_source_manifest() -> SourceManifest {
16660        let entries = vec![
16661            SourceManifestEntry {
16662                path: "Cargo.lock".to_owned(),
16663                size: 0,
16664                sha256: "b".repeat(64),
16665                executable: false,
16666            },
16667            SourceManifestEntry {
16668                path: "ferry.toml".to_owned(),
16669                size: 0,
16670                sha256: "c".repeat(64),
16671                executable: false,
16672            },
16673        ];
16674        let mut digest = Sha256::new();
16675        digest.update(b"rustferry-source-manifest-v1\0");
16676        digest.update(1_u64.to_be_bytes());
16677        digest.update(b".");
16678        digest.update((entries.len() as u64).to_be_bytes());
16679        for entry in &entries {
16680            digest.update((entry.path.len() as u64).to_be_bytes());
16681            digest.update(entry.path.as_bytes());
16682            digest.update(entry.size.to_be_bytes());
16683            digest.update((entry.sha256.len() as u64).to_be_bytes());
16684            digest.update(entry.sha256.as_bytes());
16685            digest.update([u8::from(entry.executable)]);
16686        }
16687        digest.update(0_u64.to_be_bytes());
16688        SourceManifest {
16689            schema_version: 1,
16690            project_path: ".".to_owned(),
16691            entries,
16692            total_size: 0,
16693            sha256: hex::encode(digest.finalize()),
16694        }
16695    }
16696
16697    fn compile_evidence_for(request: &IosDeviceBuildRequest) -> CompilePhaseEvidence {
16698        let expectation = UnsignedXcarchiveExpectation {
16699            app_directory_name: request.product.app_directory_name.clone(),
16700            bundle_identifier: request.bundle_identifier.clone(),
16701            executable: request.product.executable.clone(),
16702            app_version: request.product.app_version.clone(),
16703            build_number: request.product.build_number.clone(),
16704            minimum_os: request.minimum_ios_version.clone(),
16705            sdk_version: "26.0".to_owned(),
16706            sdk_build_version: "23A".to_owned(),
16707            nested_bundles: request.product.nested_bundles.clone(),
16708            required_resources: BTreeMap::new(),
16709        };
16710        CompilePhaseEvidence {
16711            schema_version: COMPILE_PHASE_EVIDENCE_SCHEMA_VERSION,
16712            job_id: request.operation_id.clone(),
16713            provider: GITHUB_PROVIDER_ID.to_owned(),
16714            request_sha256: canonical_request_sha256(request).expect("request digest"),
16715            source_sha256: request.source.sha256.clone(),
16716            cargo_lock_sha256: "b".repeat(64),
16717            config_sha256: "c".repeat(64),
16718            rustferry_version: "0.1.0".to_owned(),
16719            worker_version: "0.1.0".to_owned(),
16720            toolchain: CompileToolchainEvidence {
16721                worker_os: "macOS 26.0".to_owned(),
16722                worker_architecture: "arm64".to_owned(),
16723                xcode_version: "26.0".to_owned(),
16724                iphoneos_sdk_version: "26.0".to_owned(),
16725                iphoneos_sdk_build_version: "23A".to_owned(),
16726                developer_directory_sha256: "d".repeat(64),
16727                rust_version: "rustc 1.92.0".to_owned(),
16728                rust_target: rustferry_remote::IOS_DEVICE_RUST_TARGET.to_owned(),
16729            },
16730            sealed_archive: SealedUnsignedArchive {
16731                schema_version: SEALED_UNSIGNED_ARCHIVE_SCHEMA_VERSION,
16732                transport: SourceArchive {
16733                    size: 1,
16734                    sha256: "e".repeat(64),
16735                },
16736                contents: request.source.clone(),
16737                expectation,
16738            },
16739            archive_inspection: UnsignedXcarchiveInspection {
16740                application_path: "Applications/App.app".to_owned(),
16741                architectures: vec!["arm64".to_owned()],
16742                app: UnsignedAppInspection {
16743                    app_directory_name: request.product.app_directory_name.clone(),
16744                    bundle_identifier: request.bundle_identifier.clone(),
16745                    executable: request.product.executable.clone(),
16746                    main_executable: Vec::new(),
16747                    nested_executables: BTreeMap::new(),
16748                    extensions: Vec::new(),
16749                    resources: BTreeMap::new(),
16750                    entries: Vec::new(),
16751                },
16752                entries: Vec::new(),
16753            },
16754            started_at_unix_seconds: 1_700_000_000,
16755            finished_at_unix_seconds: 1_700_000_060,
16756        }
16757    }
16758
16759    fn large_source_manifest() -> SourceManifest {
16760        let entries = (0..20_000)
16761            .map(|index| SourceManifestEntry {
16762                path: format!("file-{index:05}"),
16763                size: 0,
16764                sha256: "0".repeat(64),
16765                executable: false,
16766            })
16767            .collect::<Vec<_>>();
16768        let mut digest = Sha256::new();
16769        digest.update(b"rustferry-source-manifest-v1\0");
16770        digest.update(1_u64.to_be_bytes());
16771        digest.update(b".");
16772        digest.update((entries.len() as u64).to_be_bytes());
16773        for entry in &entries {
16774            digest.update((entry.path.len() as u64).to_be_bytes());
16775            digest.update(entry.path.as_bytes());
16776            digest.update(entry.size.to_be_bytes());
16777            digest.update((entry.sha256.len() as u64).to_be_bytes());
16778            digest.update(entry.sha256.as_bytes());
16779            digest.update([u8::from(entry.executable)]);
16780        }
16781        digest.update(0_u64.to_be_bytes());
16782        SourceManifest {
16783            schema_version: 1,
16784            project_path: ".".to_owned(),
16785            entries,
16786            total_size: 0,
16787            sha256: hex::encode(digest.finalize()),
16788        }
16789    }
16790
16791    fn poll_ready<T>(mut future: ProviderFuture<'_, T>) -> RemoteBuildResult<T> {
16792        let waker = Waker::noop();
16793        let mut context = Context::from_waker(waker);
16794        match future.as_mut().poll(&mut context) {
16795            Poll::Ready(result) => result,
16796            Poll::Pending => panic!("provider future unexpectedly pending"),
16797        }
16798    }
16799
16800    fn all_authorized() -> GithubMutationAuthorization {
16801        GithubMutationAuthorization {
16802            publish_temporary_ref: true,
16803            cancel_run: true,
16804            delete_temporary_ref: true,
16805            publish_git_snapshot_source: true,
16806            delete_git_snapshot_source: true,
16807            manage_git_snapshot_keepalive: true,
16808        }
16809    }
16810
16811    fn completed_run_row(conclusion: &str) -> Vec<u8> {
16812        format!(
16813            "41\t17\t{WORKFLOW_PATH_BASE64}\t9\t1\t{DISPATCH_SHA}\t{BRANCH_BASE64}\tpush\tcompleted\t{conclusion}\n"
16814        )
16815        .into_bytes()
16816    }
16817
16818    fn queued_run_row() -> Vec<u8> {
16819        queued_run_attempt_row(1)
16820    }
16821
16822    fn queued_run_attempt_row(attempt: u64) -> Vec<u8> {
16823        format!(
16824            "41\t17\t{WORKFLOW_PATH_BASE64}\t9\t{attempt}\t{DISPATCH_SHA}\t{BRANCH_BASE64}\tpush\tqueued\t\n"
16825        )
16826        .into_bytes()
16827    }
16828
16829    fn workflow_dispatch_registration_row() -> Vec<u8> {
16830        format!("17\t{WORKFLOW_PATH_BASE64}\tactive\n").into_bytes()
16831    }
16832
16833    fn workflow_dispatch_run_row(run_id: u64) -> Vec<u8> {
16834        let request_sha256 = canonical_request_sha256(&unsigned_request()).expect("request digest");
16835        let run_name = crate::transport::canonical_workflow_dispatch_run_name(
16836            "operation-1",
16837            &request_sha256,
16838            &CommitSha::new(SOURCE_SHA).expect("source revision"),
16839            &CommitSha::new(DISPATCH_SHA).expect("dispatch revision"),
16840        )
16841        .expect("workflow dispatch run name");
16842        let run_name = base64_fixture(&run_name);
16843        workflow_dispatch_run_row_with_title(run_id, &run_name)
16844    }
16845
16846    fn workflow_dispatch_run_row_with_title(run_id: u64, encoded_run_name: &str) -> Vec<u8> {
16847        format!(
16848            "{run_id}\t17\t{WORKFLOW_PATH_BASE64}\t9\t1\t{DISPATCH_SHA}\t{BRANCH_BASE64}\tworkflow_dispatch\tqueued\t\t{encoded_run_name}\n"
16849        )
16850        .into_bytes()
16851    }
16852
16853    fn ordinary_workflow_dispatch_run_row(run_id: u64) -> Vec<u8> {
16854        format!(
16855            "{run_id}\t17\t{WORKFLOW_PATH_BASE64}\t9\t1\t{DISPATCH_SHA}\t{BRANCH_BASE64}\tworkflow_dispatch\tqueued\t\n"
16856        )
16857        .into_bytes()
16858    }
16859
16860    fn workflow_dispatch_receipt_response(run_id: u64) -> GithubWorkflowDispatchHttpResponse {
16861        GithubWorkflowDispatchHttpResponse::new(
16862            200,
16863            Some("application/json".to_owned()),
16864            format!(
16865                "{{\"workflow_run_id\":{run_id},\"run_url\":\"https://api.github.com/repos/ShiroKSH/rustferry-signing/actions/runs/{run_id}\",\"html_url\":\"https://github.com/ShiroKSH/rustferry-signing/actions/runs/{run_id}\"}}"
16866            )
16867            .into_bytes(),
16868        )
16869    }
16870
16871    fn workflow_dispatch_submit_responses(run_id: u64) -> Vec<Result<Vec<u8>, GhExecutionError>> {
16872        vec![
16873            Ok(authenticated_user_row()),
16874            Ok(private_repository_row()),
16875            Ok(private_repository_row()),
16876            Ok(workflow_dispatch_registration_row()),
16877            Ok(authenticated_user_row()),
16878            Ok(private_repository_row()),
16879            Ok(authenticated_user_row()),
16880            Ok(private_repository_row()),
16881            Ok(private_repository_row()),
16882            Ok(workflow_dispatch_registration_row()),
16883            Ok(workflow_dispatch_run_row(run_id)),
16884            Ok(authenticated_user_row()),
16885            Ok(private_repository_row()),
16886        ]
16887    }
16888
16889    fn durable_workflow_dispatch_intent_resume() -> GithubJobResumeV1 {
16890        let push_config = provider_config(all_authorized());
16891        let config = workflow_dispatch_provider_config(all_authorized());
16892        let principal = GithubPrincipalIdentityV1::User {
16893            id: AUTHENTICATED_USER_ID,
16894            login: "ShiroKSH".to_owned(),
16895        };
16896        let mut record = durable_prepublication_resume()
16897            .into_record(&push_config, &principal, 991)
16898            .expect("push fixture record");
16899        let dispatch_commit = CommitSha::new(DISPATCH_SHA).expect("dispatch commit");
16900        record.prepared_dispatch_commit = Some(dispatch_commit.clone());
16901        record.dispatch_commit = Some(dispatch_commit.clone());
16902        record.publication_uncertain = false;
16903        let registration = WorkflowRegistration::restore(
16904            config.repository.clone(),
16905            crate::transport::WorkflowId::new(17).expect("workflow ID"),
16906            config.workflow.filename().repository_path(),
16907        )
16908        .expect("workflow registration");
16909        let request = WorkflowDispatchRequest::new(
16910            config.repository.clone(),
16911            registration,
16912            record.temporary_ref.clone(),
16913            record.operation_id.clone(),
16914            record.request_sha256.clone(),
16915            record.source_revision.clone(),
16916            dispatch_commit,
16917        )
16918        .expect("workflow dispatch request");
16919        record.workflow_dispatch = Some(GithubWorkflowDispatchResumeV1::from_request(&request));
16920        GithubJobResumeV1::from_record(&config, principal, 991, &record)
16921            .expect("workflow dispatch intent resume")
16922    }
16923
16924    fn durable_workflow_dispatch_pre_intent_resume() -> GithubJobResumeV1 {
16925        let push_config = provider_config(all_authorized());
16926        let config = workflow_dispatch_provider_config(all_authorized());
16927        let principal = GithubPrincipalIdentityV1::User {
16928            id: AUTHENTICATED_USER_ID,
16929            login: "ShiroKSH".to_owned(),
16930        };
16931        let record = durable_prepared_prepublication_resume()
16932            .into_record(&push_config, &principal, 991)
16933            .expect("push fixture record");
16934        assert!(record.publication_intent);
16935        assert!(record.dispatch_commit.is_none());
16936        assert!(record.workflow_dispatch.is_none());
16937        GithubJobResumeV1::from_record(&config, principal, 991, &record)
16938            .expect("workflow dispatch pre-intent resume")
16939    }
16940
16941    fn durable_workflow_dispatch_mapped_resume() -> GithubJobResumeV1 {
16942        let mut resume = durable_workflow_dispatch_intent_resume();
16943        let dispatch = resume
16944            .workflow_dispatch
16945            .as_mut()
16946            .expect("workflow dispatch intent");
16947        dispatch.receipt = Some(GithubWorkflowDispatchReceiptV1 {
16948            run_id: 73,
16949            workflow_id: dispatch.workflow_id,
16950            workflow_path: dispatch.workflow_path.clone(),
16951            branch: dispatch.branch.clone(),
16952            dispatch_revision: dispatch.dispatch_revision.clone(),
16953            run_name: dispatch.run_name.clone(),
16954        });
16955        dispatch.uncertain = false;
16956        resume.run = Some(GithubRunIdentityV1 {
16957            run_id: 73,
16958            workflow_id: dispatch.workflow_id,
16959            workflow_path: dispatch.workflow_path.clone(),
16960            head_sha: dispatch.dispatch_revision.clone(),
16961            branch: dispatch.branch.clone(),
16962            event: GithubRunEventV1::WorkflowDispatch,
16963            run_number: 9,
16964            run_attempt: 1,
16965            status: GithubRunStatusV1::Queued,
16966            conclusion: None,
16967        });
16968        resume.state = JobState::Queued;
16969        resume
16970            .validate_trigger_binding()
16971            .expect("mapped workflow dispatch binding");
16972        resume
16973    }
16974
16975    fn durable_resume_with_run() -> GithubJobResumeV1 {
16976        let config = provider_config(all_authorized());
16977        let request = unsigned_request();
16978        let branch = BranchName::new("rustferry/goal3/builds/operation-1").expect("branch");
16979        let temporary_ref =
16980            TemporaryGitRef::new(config.workflow.temporary_branch_namespace(), branch.clone())
16981                .expect("temporary ref");
16982        let dispatch_commit = CommitSha::new(DISPATCH_SHA).expect("dispatch commit");
16983        let run = RunHandle::restore(
16984            41,
16985            17,
16986            config.workflow.filename().repository_path(),
16987            DISPATCH_SHA.to_owned(),
16988            branch.as_str().to_owned(),
16989            RunEvent::Push,
16990        )
16991        .expect("run handle");
16992        let run_snapshot =
16993            RunSnapshot::restore(run.clone(), 9, 1, RunStatus::Queued, None).expect("run snapshot");
16994        let record = JobRecord {
16995            principal: Some(GithubPrincipalIdentityV1::User {
16996                id: AUTHENTICATED_USER_ID,
16997                login: "ShiroKSH".to_owned(),
16998            }),
16999            execution_repository_id: Some(991),
17000            operation_id: request.operation_id.clone(),
17001            job_id: request.operation_id.clone(),
17002            source_revision: CommitSha::new(SOURCE_SHA).expect("source revision"),
17003            git_snapshot: None,
17004            request_sha256: canonical_request_sha256(&request).expect("request digest"),
17005            request,
17006            temporary_ref,
17007            prepared_dispatch_commit: None,
17008            dispatch_commit: Some(dispatch_commit),
17009            workflow_dispatch: None,
17010            created_at_ms: 1_700_000_000_000,
17011            publication_started_at_ms: 1_700_000_000_000,
17012            publication_quiescence_deadline_ms: 1_700_000_000_000
17013                + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS,
17014            state: JobState::Queued,
17015            run: Some(run),
17016            run_snapshot: Some(run_snapshot),
17017            publication_intent: true,
17018            publication_absent: false,
17019            publication_not_attempted: false,
17020            publication_process_fenced: true,
17021            publication_lease_scope_sha256: Some("a".repeat(64)),
17022            publication_takeover_lease_owned: false,
17023            snapshot_source_lease_owned: false,
17024            publication_absence_observations: 0,
17025            publication_absence_first_observed_at_ms: 0,
17026            cancellation_requested: false,
17027            cancellation_dispatched: false,
17028            cleanup_requested: false,
17029            remove_artifacts_requested: false,
17030            artifacts_removed: false,
17031            temporary_ref_deleted: false,
17032            verification_pending_event: false,
17033            publication_uncertain: false,
17034            run_discovery_attempts: 0,
17035            run_discovery_deadline_ms: 1_700_000_000_750,
17036            manifests: Vec::new(),
17037            compile_evidence: None,
17038            signed_cleanup_evidence: None,
17039            events: Vec::new(),
17040        };
17041        GithubJobResumeV1::from_record(
17042            &config,
17043            GithubPrincipalIdentityV1::User {
17044                id: AUTHENTICATED_USER_ID,
17045                login: "ShiroKSH".to_owned(),
17046            },
17047            991,
17048            &record,
17049        )
17050        .expect("durable resume")
17051    }
17052
17053    fn durable_prepublication_resume() -> GithubJobResumeV1 {
17054        let mut resume = durable_resume_with_run();
17055        resume.dispatch_commit = None;
17056        resume.run = None;
17057        resume.state = JobState::Created;
17058        resume.events.clear();
17059        resume
17060    }
17061
17062    fn durable_prepared_prepublication_resume() -> GithubJobResumeV1 {
17063        let mut resume = durable_prepublication_resume();
17064        resume.prepared_dispatch_commit = Some(DISPATCH_SHA.to_owned());
17065        resume
17066    }
17067
17068    fn durable_possible_push_resume() -> GithubJobResumeV1 {
17069        let mut resume = durable_prepared_prepublication_resume();
17070        resume.dispatch_commit = Some(DISPATCH_SHA.to_owned());
17071        resume.publication_uncertain = true;
17072        resume.state = JobState::Queued;
17073        resume
17074    }
17075
17076    fn durable_success_resume() -> GithubJobResumeV1 {
17077        let mut resume = durable_resume_with_run();
17078        let run = resume.run.as_mut().expect("run identity");
17079        run.status = GithubRunStatusV1::Completed;
17080        run.conclusion = Some(GithubRunConclusionV1::Success);
17081        resume.state = JobState::Succeeded;
17082        let mut manifest = ArtifactManifest::new("operation-1", "operation-1");
17083        manifest.provider = GITHUB_PROVIDER_ID.to_owned();
17084        manifest.source_repository = Some(resume.source_repository.clone());
17085        manifest.source_revision = Some(resume.source_revision.clone());
17086        resume.manifests = vec![manifest];
17087        resume.compile_evidence = Some(compile_evidence_for(&resume.request));
17088        resume
17089    }
17090
17091    fn durable_signed_success_resume() -> GithubJobResumeV1 {
17092        let mut resume = durable_resume_with_run();
17093        resume.request = valid_request();
17094        resume.request_sha256 = canonical_request_sha256(&resume.request).expect("request digest");
17095        let run = resume.run.as_mut().expect("run identity");
17096        run.status = GithubRunStatusV1::Completed;
17097        run.conclusion = Some(GithubRunConclusionV1::Success);
17098        resume.state = JobState::Succeeded;
17099        let compile = compile_evidence_for(&resume.request);
17100        let manifest_sha256 = "1".repeat(64);
17101        let signing_report_sha256 = "2".repeat(64);
17102        let validation_report_sha256 = "3".repeat(64);
17103        let mut manifest = ArtifactManifest::new("operation-1", "operation-1");
17104        manifest.provider = GITHUB_PROVIDER_ID.to_owned();
17105        manifest.source_repository = Some(resume.source_repository.clone());
17106        manifest.source_revision = Some(resume.source_revision.clone());
17107        manifest.source_sha256 = compile.source_sha256.clone();
17108        manifest.cargo_lock_sha256 = compile.cargo_lock_sha256.clone();
17109        manifest.config_sha256 = compile.config_sha256.clone();
17110        manifest.cleanup_status = CleanupStatus::Confirmed;
17111        manifest.signing = ArtifactSigningEvidence {
17112            mode: SigningMode::ManualDevelopment,
17113            status: SigningStatus::ArtifactValidated,
17114            team_id: None,
17115            certificate_fingerprint: None,
17116            profile_uuid: None,
17117            profile_expiration: None,
17118            entitlements_sha256: None,
17119        };
17120        manifest.artifacts = vec![
17121            ArtifactRecord {
17122                artifact_id: "manifest".to_owned(),
17123                kind: ArtifactKind::Manifest,
17124                file_name: "artifact-manifest.json".to_owned(),
17125                size: 1,
17126                sha256: manifest_sha256.clone(),
17127                media_type: Some("application/json".to_owned()),
17128            },
17129            ArtifactRecord {
17130                artifact_id: "signing-report".to_owned(),
17131                kind: ArtifactKind::SigningReport,
17132                file_name: "signing-report.json".to_owned(),
17133                size: 1,
17134                sha256: signing_report_sha256.clone(),
17135                media_type: Some("application/json".to_owned()),
17136            },
17137            ArtifactRecord {
17138                artifact_id: "validation-report".to_owned(),
17139                kind: ArtifactKind::ValidationReport,
17140                file_name: "validation-report.json".to_owned(),
17141                size: 1,
17142                sha256: validation_report_sha256.clone(),
17143                media_type: Some("application/json".to_owned()),
17144            },
17145        ];
17146        resume.manifests = vec![manifest];
17147        resume.compile_evidence = Some(compile.clone());
17148        resume.signed_cleanup_evidence = Some(GithubSignedCleanupEvidenceV1 {
17149            schema_version: GITHUB_SIGNED_CLEANUP_EVIDENCE_SCHEMA_VERSION,
17150            provider: GITHUB_PROVIDER_ID.to_owned(),
17151            operation_id: resume.operation_id.clone(),
17152            job_id: resume.job_id.clone(),
17153            execution_repository: resume.execution_repository.clone(),
17154            execution_repository_id: resume.execution_repository_id,
17155            request_sha256: resume.request_sha256.clone(),
17156            source_repository: resume.source_repository.clone(),
17157            source_revision: resume.source_revision.clone(),
17158            source_sha256: resume.request.source.sha256.clone(),
17159            dispatch_revision: resume.dispatch_commit.clone().expect("dispatch"),
17160            run: resume.run.clone().expect("run"),
17161            compile_evidence_sha256: compile_evidence_sha256(&compile).expect("compile digest"),
17162            manifest_sha256,
17163            signing_report_sha256,
17164            validation_report_sha256,
17165            github_artifact_id: 73,
17166            github_artifact_api_sha256: "4".repeat(64),
17167        });
17168        resume
17169            .signed_cleanup_evidence
17170            .as_ref()
17171            .expect("cleanup evidence")
17172            .validate_for_resume(&resume)
17173            .expect("valid signed cleanup evidence");
17174        resume
17175    }
17176
17177    fn signing_environment_row(
17178        protected_branches: bool,
17179        custom_branch_policies: bool,
17180        required_reviewers: bool,
17181    ) -> Vec<u8> {
17182        format!(
17183            "{SIGNING_ENVIRONMENT_BASE64}\t{protected_branches}\t{custom_branch_policies}\t{required_reviewers}\n"
17184        )
17185        .into_bytes()
17186    }
17187
17188    fn exact_signing_policy_row() -> Vec<u8> {
17189        format!("361471\t{SIGNING_BRANCH_POLICY_BASE64}\n").into_bytes()
17190    }
17191
17192    fn exact_signing_secret_rows() -> Vec<u8> {
17193        format!(
17194            "{CERTIFICATE_P12_BASE64}\n{CERTIFICATE_PASSWORD_BASE64}\n{PROVISIONING_PROFILE_BASE64}\n"
17195        )
17196        .into_bytes()
17197    }
17198
17199    fn exact_reconciliation_git_responses(
17200        _workflow_path: &str,
17201        workflow: &[u8],
17202        _manifest: &[u8],
17203        final_tip: &str,
17204    ) -> Vec<Result<Vec<u8>, GitExecutionError>> {
17205        let full_ref = "refs/heads/rustferry/goal3/builds/operation-1";
17206        let trusted_ref = "refs/heads/goal3/macless-iphone-builds";
17207        vec![
17208            Ok(b"https://github.com/ShiroKSH/rust-and-iphone.git\n".to_vec()),
17209            Ok(b"https://github.com/ShiroKSH/rust-and-iphone.git\n".to_vec()),
17210            Ok(b"https://github.com/ShiroKSH/rustferry-signing.git\n".to_vec()),
17211            Ok(b"https://github.com/ShiroKSH/rustferry-signing.git\n".to_vec()),
17212            Ok(format!("{TRUSTED_TIP}\t{trusted_ref}\n").into_bytes()),
17213            Ok(Vec::new()),
17214            Ok(workflow.to_vec()),
17215            Ok(Vec::new()),
17216            Ok(Vec::new()),
17217            Ok(Vec::new()),
17218            Ok(format!("{WORKFLOW_BLOB}\n").into_bytes()),
17219            Ok(format!("{MANIFEST_BLOB}\n").into_bytes()),
17220            Ok(Vec::new()),
17221            Ok(Vec::new()),
17222            Ok(format!("{TREE_SHA}\n").into_bytes()),
17223            Ok(format!("{DISPATCH_SHA}\n").into_bytes()),
17224            Ok(format!("{final_tip}\t{full_ref}\n").into_bytes()),
17225        ]
17226    }
17227
17228    fn successful_prepare_git_responses(
17229        workflow: &[u8],
17230    ) -> Vec<Result<Vec<u8>, GitExecutionError>> {
17231        let full_trusted_ref = "refs/heads/goal3/macless-iphone-builds";
17232        vec![
17233            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
17234            Ok(b"ssh://git@github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
17235            Ok(b"git@github.com:ShiroKSH/rustferry-signing.git\n".to_vec()),
17236            Ok(b"ssh://git@github.com/ShiroKSH/rustferry-signing\n".to_vec()),
17237            Ok(format!("{TRUSTED_TIP}\t{full_trusted_ref}\n").into_bytes()),
17238            Ok(Vec::new()),
17239            Ok(workflow.to_vec()),
17240            Ok(Vec::new()),
17241            Ok(Vec::new()),
17242            Ok(Vec::new()),
17243            Ok(Vec::new()),
17244            Ok(format!("{WORKFLOW_BLOB}\n").into_bytes()),
17245            Ok(format!("{MANIFEST_BLOB}\n").into_bytes()),
17246            Ok(Vec::new()),
17247            Ok(Vec::new()),
17248            Ok(format!("{TREE_SHA}\n").into_bytes()),
17249            Ok(format!("{DISPATCH_SHA}\n").into_bytes()),
17250        ]
17251    }
17252
17253    fn successful_snapshot_prepare_git_responses(
17254        workflow: &[u8],
17255        final_snapshot_tip: &str,
17256    ) -> Vec<Result<Vec<u8>, GitExecutionError>> {
17257        let trusted_ref = "refs/heads/goal3/macless-iphone-builds";
17258        let snapshot_ref = "refs/rustferry/goal3/snapshots/operation-1";
17259        vec![
17260            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
17261            Ok(b"ssh://git@github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
17262            Ok(b"git@github.com:ShiroKSH/rustferry-signing.git\n".to_vec()),
17263            Ok(b"ssh://git@github.com/ShiroKSH/rustferry-signing\n".to_vec()),
17264            Ok(format!("{TRUSTED_TIP}\t{trusted_ref}\n").into_bytes()),
17265            Ok(Vec::new()),
17266            Ok(workflow.to_vec()),
17267            Ok(format!("{SOURCE_SHA}\t{snapshot_ref}\n").into_bytes()),
17268            Ok(Vec::new()),
17269            Ok(Vec::new()),
17270            Ok(format!("{final_snapshot_tip}\t{snapshot_ref}\n").into_bytes()),
17271            Ok(Vec::new()),
17272            Ok(Vec::new()),
17273            Ok(format!("{WORKFLOW_BLOB}\n").into_bytes()),
17274            Ok(format!("{MANIFEST_BLOB}\n").into_bytes()),
17275            Ok(Vec::new()),
17276            Ok(Vec::new()),
17277            Ok(format!("{TREE_SHA}\n").into_bytes()),
17278            Ok(format!("{DISPATCH_SHA}\n").into_bytes()),
17279        ]
17280    }
17281
17282    fn snapshot_source_mutation_git_responses(
17283        final_tip: Option<&str>,
17284    ) -> Vec<Result<Vec<u8>, GitExecutionError>> {
17285        let snapshot_ref = "refs/rustferry/goal3/snapshots/operation-1";
17286        vec![
17287            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
17288            Ok(b"ssh://git@github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
17289            Ok(Vec::new()),
17290            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
17291            Ok(b"ssh://git@github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
17292            Ok(final_tip
17293                .map(|tip| format!("{tip}\t{snapshot_ref}\n").into_bytes())
17294                .unwrap_or_default()),
17295        ]
17296    }
17297
17298    fn signed_doctor_report(
17299        environment: Result<Vec<u8>, GhExecutionError>,
17300        policies: Result<Vec<u8>, GhExecutionError>,
17301        secrets: Result<Vec<u8>, GhExecutionError>,
17302    ) -> ProviderDoctorReport {
17303        signed_doctor_report_with_visibility(true, environment, policies, secrets)
17304    }
17305
17306    fn signed_doctor_report_with_visibility(
17307        private: bool,
17308        environment: Result<Vec<u8>, GhExecutionError>,
17309        policies: Result<Vec<u8>, GhExecutionError>,
17310        secrets: Result<Vec<u8>, GhExecutionError>,
17311    ) -> ProviderDoctorReport {
17312        signed_doctor_report_with_repository_policy(
17313            private,
17314            Ok(b"true\n".to_vec()),
17315            environment,
17316            policies,
17317            secrets,
17318        )
17319    }
17320
17321    fn signed_doctor_report_with_repository_policy(
17322        private: bool,
17323        actions: Result<Vec<u8>, GhExecutionError>,
17324        environment: Result<Vec<u8>, GhExecutionError>,
17325        policies: Result<Vec<u8>, GhExecutionError>,
17326        secrets: Result<Vec<u8>, GhExecutionError>,
17327    ) -> ProviderDoctorReport {
17328        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17329            provider_config(all_authorized()),
17330            transport(FakeGhRunner::with([
17331                Ok(authenticated_user_row()),
17332                Ok(
17333                    format!("991\tShiroKSH/rustferry-signing\t{private}\tfalse\tfalse\tmain\n")
17334                        .into_bytes(),
17335                ),
17336                Ok(b"992\tShiroKSH/rust-and-iphone\tfalse\tfalse\tfalse\tmain\n".to_vec()),
17337                actions,
17338                environment,
17339                policies,
17340                secrets,
17341            ])),
17342            FakePublisher::default(),
17343            DoctorArtifactStore,
17344            FixedClock(1_700_000_000_000),
17345        );
17346        poll_ready(provider.doctor(
17347            ProviderDoctorRequest {
17348                protocol_version: CURRENT_PROTOCOL_VERSION,
17349                operation_id: "signed-doctor".to_owned(),
17350                require_signing: true,
17351            },
17352            CancellationToken::new(),
17353        ))
17354        .expect("doctor report")
17355    }
17356
17357    fn workflow_dispatch_doctor_report(
17358        registration: Result<Vec<u8>, GhExecutionError>,
17359    ) -> ProviderDoctorReport {
17360        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17361            workflow_dispatch_provider_config(all_authorized()),
17362            transport(FakeGhRunner::with([
17363                Ok(authenticated_user_row()),
17364                Ok(private_repository_row()),
17365                Ok(public_source_repository_row()),
17366                Ok(b"true\n".to_vec()),
17367                registration,
17368                Ok(signing_environment_row(false, true, true)),
17369                Ok(exact_signing_policy_row()),
17370                Ok(exact_signing_secret_rows()),
17371            ])),
17372            FakePublisher::default(),
17373            DoctorArtifactStore,
17374            FixedClock(1_700_000_000_000),
17375        );
17376        poll_ready(provider.doctor(
17377            ProviderDoctorRequest {
17378                protocol_version: CURRENT_PROTOCOL_VERSION,
17379                operation_id: "workflow-dispatch-doctor".to_owned(),
17380                require_signing: true,
17381            },
17382            CancellationToken::new(),
17383        ))
17384        .expect("workflow dispatch doctor report")
17385    }
17386
17387    #[test]
17388    fn workflow_fingerprint_is_exact_lowercase_sha256() {
17389        let workflow = generate_workflow(&workflow_config());
17390        let fingerprint = WorkflowFingerprint::for_workflow(&workflow);
17391        assert!(is_lower_sha256(fingerprint.as_str()));
17392        assert_eq!(fingerprint, WorkflowFingerprint::for_workflow(&workflow));
17393        assert!(WorkflowFingerprint::new("A".repeat(64)).is_err());
17394    }
17395
17396    #[test]
17397    fn workflow_dispatch_resume_state_is_heap_indirected() {
17398        assert_eq!(
17399            std::mem::size_of::<Option<Box<GithubWorkflowDispatchResumeV1>>>(),
17400            std::mem::size_of::<usize>()
17401        );
17402        assert!(std::mem::size_of::<GithubJobResumeV1>() <= 4_352);
17403    }
17404
17405    #[test]
17406    fn workflow_dispatch_submit_checkpoints_intent_receipt_then_exact_run_and_posts_once() {
17407        let plan = FakeWorkflowDispatchPlan::with_response(workflow_dispatch_receipt_response(73));
17408        let sink = CapturingCheckpointSink::default();
17409        let checkpoints = Arc::clone(&sink.checkpoints);
17410        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17411            workflow_dispatch_provider_config(all_authorized()),
17412            transport(
17413                FakeGhRunner::with(workflow_dispatch_submit_responses(73))
17414                    .with_workflow_dispatch(plan.clone()),
17415            ),
17416            FakePublisher::default(),
17417            NoVerifiedArtifactStore,
17418            FixedClock(1_700_000_000_000),
17419        )
17420        .with_checkpoint_sink(sink);
17421
17422        let handle = poll_ready(provider.submit(unsigned_request(), CancellationToken::new()))
17423            .unwrap_or_else(|error| {
17424                let requests = provider
17425                    .transport
17426                    .lock()
17427                    .expect("transport")
17428                    .runner()
17429                    .requests
17430                    .lock()
17431                    .expect("requests")
17432                    .iter()
17433                    .map(|request| request.endpoint().to_owned())
17434                    .collect::<Vec<_>>();
17435                panic!("workflow dispatch submit failed: {error:?}; requests: {requests:?}")
17436            });
17437        assert_eq!(handle.state, JobState::Queued);
17438        assert_eq!(plan.0.clients_created.load(Ordering::SeqCst), 1);
17439        assert_eq!(plan.0.requests.lock().expect("dispatch requests").len(), 1);
17440        assert_eq!(
17441            provider
17442                .publisher
17443                .lock()
17444                .expect("publisher")
17445                .published
17446                .len(),
17447            1
17448        );
17449
17450        let checkpoints = checkpoints.lock().expect("dispatch checkpoints");
17451        let intent = checkpoints
17452            .iter()
17453            .position(|resume| {
17454                resume
17455                    .workflow_dispatch
17456                    .as_ref()
17457                    .is_some_and(|dispatch| dispatch.receipt.is_none())
17458                    && resume.run.is_none()
17459            })
17460            .expect("durable dispatch intent");
17461        let receipt = checkpoints
17462            .iter()
17463            .position(|resume| {
17464                resume
17465                    .workflow_dispatch
17466                    .as_ref()
17467                    .is_some_and(|dispatch| dispatch.receipt.is_some())
17468                    && resume.run.is_none()
17469            })
17470            .expect("durable dispatch receipt");
17471        let run = checkpoints
17472            .iter()
17473            .position(|resume| {
17474                resume.run.as_ref().is_some_and(|run| {
17475                    run.run_id == 73 && run.event == GithubRunEventV1::WorkflowDispatch
17476                })
17477            })
17478            .expect("durable workflow dispatch run");
17479        assert!(intent < receipt && receipt < run);
17480        assert!(
17481            checkpoints[..intent]
17482                .iter()
17483                .any(|resume| resume.dispatch_commit.as_deref() == Some(DISPATCH_SHA))
17484        );
17485    }
17486
17487    #[test]
17488    fn workflow_dispatch_post_releases_state_and_transport_and_late_cancel_maps_exact_run() {
17489        let (plan, gate) = FakeWorkflowDispatchPlan::with_blocking_response(
17490            workflow_dispatch_receipt_response(73),
17491        );
17492        let mut responses = workflow_dispatch_submit_responses(73);
17493        responses.extend([
17494            Ok(ordinary_workflow_dispatch_run_row(73)),
17495            Ok(authenticated_user_row()),
17496            Ok(private_repository_row()),
17497            Ok(ordinary_workflow_dispatch_run_row(73)),
17498            Ok(authenticated_user_row()),
17499            Ok(private_repository_row()),
17500            Ok(Vec::new()),
17501        ]);
17502        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17503            workflow_dispatch_provider_config(all_authorized()),
17504            transport(FakeGhRunner::with(responses).with_workflow_dispatch(plan.clone())),
17505            FakePublisher::default(),
17506            NoVerifiedArtifactStore,
17507            FixedClock(1_700_000_000_000),
17508        )
17509        .with_checkpoint_sink(CapturingCheckpointSink::default());
17510        let cancellation = CancellationToken::new();
17511
17512        thread::scope(|scope| {
17513            let submitted = scope
17514                .spawn(|| poll_ready(provider.submit(unsigned_request(), cancellation.clone())));
17515            gate.wait_until_entered();
17516            assert!(provider.state.try_lock().is_ok());
17517            assert!(provider.transport.try_lock().is_ok());
17518            cancellation.cancel();
17519            gate.release();
17520            let handle = submitted
17521                .join()
17522                .expect("dispatch submit thread")
17523                .expect("submit");
17524            assert_eq!(handle.state, JobState::Cancelling);
17525        });
17526
17527        assert_eq!(plan.0.requests.lock().expect("dispatch requests").len(), 1);
17528        let state = provider.lock_state().expect("dispatch state");
17529        let record = state.jobs.get("operation-1").expect("dispatch record");
17530        assert!(record.cancellation_requested);
17531        assert!(record.cancellation_dispatched);
17532        assert_eq!(
17533            record.run.as_ref().map(RunHandle::id),
17534            Some(RunId::new(73).expect("run"))
17535        );
17536    }
17537
17538    #[test]
17539    fn late_cancellation_checkpoint_preserves_every_terminal_state() {
17540        let mapped = durable_workflow_dispatch_mapped_resume();
17541        let sink = CapturingCheckpointSink::default();
17542        let checkpoints = Arc::clone(&sink.checkpoints);
17543        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17544            workflow_dispatch_provider_config(all_authorized()),
17545            transport(FakeGhRunner::with([])),
17546            FakePublisher::default(),
17547            NoVerifiedArtifactStore,
17548            FixedClock(1_700_000_000_000),
17549        )
17550        .with_checkpoint_sink(sink);
17551        provider
17552            .restore_job_resumes_offline(
17553                vec![mapped.clone()],
17554                &durable_identity_for_resume(&mapped),
17555            )
17556            .expect("restore mapped dispatch run");
17557        let cancellation = CancellationToken::new();
17558        cancellation.cancel();
17559
17560        for terminal in [
17561            JobState::Succeeded,
17562            JobState::Failed,
17563            JobState::Cancelled,
17564            JobState::Cleaned,
17565        ] {
17566            {
17567                let mut state = provider.lock_state().expect("dispatch state");
17568                let record = state.jobs.get_mut("operation-1").expect("dispatch record");
17569                record.state = terminal;
17570                record.cancellation_requested = false;
17571            }
17572            assert!(
17573                !provider
17574                    .checkpoint_workflow_dispatch_cancellation("operation-1", &cancellation)
17575                    .expect("terminal cancellation check")
17576            );
17577            let state = provider.lock_state().expect("dispatch state");
17578            let record = state.jobs.get("operation-1").expect("dispatch record");
17579            assert_eq!(record.state, terminal);
17580            assert!(!record.cancellation_requested);
17581        }
17582        assert!(checkpoints.lock().expect("checkpoints").is_empty());
17583    }
17584
17585    #[test]
17586    fn cancelled_ambiguous_dispatch_persists_intent_then_restart_adopts_and_cancels() {
17587        let (plan, gate) = FakeWorkflowDispatchPlan::with_blocking_result(Err(
17588            GithubWorkflowDispatchHttpError::TimedOut,
17589        ));
17590        let sink = CapturingCheckpointSink::default();
17591        let durable = Arc::clone(&sink.checkpoints);
17592        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17593            workflow_dispatch_provider_config(all_authorized()),
17594            transport(
17595                FakeGhRunner::with(workflow_dispatch_submit_responses(73))
17596                    .with_workflow_dispatch(plan.clone()),
17597            ),
17598            FakePublisher::default(),
17599            NoVerifiedArtifactStore,
17600            FixedClock(1_700_000_000_000),
17601        )
17602        .with_checkpoint_sink(sink);
17603        let cancellation = CancellationToken::new();
17604        thread::scope(|scope| {
17605            let submitted = scope
17606                .spawn(|| poll_ready(provider.submit(unsigned_request(), cancellation.clone())));
17607            gate.wait_until_entered();
17608            cancellation.cancel();
17609            gate.release();
17610            submitted
17611                .join()
17612                .expect("dispatch submit thread")
17613                .expect_err("ambiguous dispatch response");
17614        });
17615        let resume = durable
17616            .lock()
17617            .expect("dispatch checkpoints")
17618            .last()
17619            .cloned()
17620            .expect("cancelled dispatch intent");
17621        assert!(resume.cancellation_requested);
17622        assert!(resume.run.is_none());
17623        assert!(
17624            resume
17625                .workflow_dispatch
17626                .as_ref()
17627                .is_some_and(|dispatch| { dispatch.uncertain && dispatch.receipt.is_none() })
17628        );
17629
17630        let restarted = GithubBuildProvider::with_artifact_store_and_clock(
17631            workflow_dispatch_provider_config(all_authorized()),
17632            transport(FakeGhRunner::with([
17633                Ok(workflow_dispatch_run_row(73)),
17634                Ok(workflow_dispatch_run_row(73)),
17635                Ok(authenticated_user_row()),
17636                Ok(private_repository_row()),
17637                Ok(ordinary_workflow_dispatch_run_row(73)),
17638                Ok(authenticated_user_row()),
17639                Ok(private_repository_row()),
17640                Ok(ordinary_workflow_dispatch_run_row(73)),
17641                Ok(authenticated_user_row()),
17642                Ok(private_repository_row()),
17643                Ok(Vec::new()),
17644            ])),
17645            FakePublisher::default(),
17646            NoVerifiedArtifactStore,
17647            FixedClock(1_700_000_000_000),
17648        )
17649        .with_checkpoint_sink(CapturingCheckpointSink::default());
17650        restarted
17651            .restore_job_resumes_offline(
17652                vec![resume.clone()],
17653                &durable_identity_for_resume(&resume),
17654            )
17655            .expect("restore cancelled dispatch intent");
17656        restarted
17657            .reconcile_restored_job("operation-1", &CancellationToken::new())
17658            .expect("adopt and cancel exact dispatch run");
17659        let state = restarted.lock_state().expect("restarted dispatch state");
17660        let record = state.jobs.get("operation-1").expect("dispatch record");
17661        assert!(record.cancellation_requested);
17662        assert!(record.cancellation_dispatched);
17663    }
17664
17665    #[test]
17666    fn cancellation_during_dispatch_mapping_error_is_checkpointed_before_return() {
17667        let plan = FakeWorkflowDispatchPlan::with_response(workflow_dispatch_receipt_response(73));
17668        let cancellation = CancellationToken::new();
17669        let mut responses = workflow_dispatch_submit_responses(73);
17670        responses[10] = Err(GhExecutionError::CommandFailed { exit_code: Some(1) });
17671        let sink = CapturingCheckpointSink::default();
17672        let durable = Arc::clone(&sink.checkpoints);
17673        let provider = GithubBuildProvider::with_artifact_store_and_clock(
17674            workflow_dispatch_provider_config(all_authorized()),
17675            transport(
17676                FakeGhRunner::with(responses)
17677                    .cancelling_before(11, cancellation.clone())
17678                    .with_workflow_dispatch(plan),
17679            ),
17680            FakePublisher::default(),
17681            NoVerifiedArtifactStore,
17682            FixedClock(1_700_000_000_000),
17683        )
17684        .with_checkpoint_sink(sink);
17685
17686        poll_ready(provider.submit(unsigned_request(), cancellation))
17687            .expect_err("run mapping failure");
17688        let resume = durable
17689            .lock()
17690            .expect("dispatch checkpoints")
17691            .last()
17692            .cloned()
17693            .expect("cancelled receipt checkpoint");
17694        assert!(resume.cancellation_requested);
17695        assert!(resume.run.is_none());
17696        assert!(resume.workflow_dispatch.as_ref().is_some_and(|dispatch| {
17697            !dispatch.uncertain
17698                && dispatch
17699                    .receipt
17700                    .as_ref()
17701                    .is_some_and(|receipt| receipt.run_id == 73)
17702        }));
17703    }
17704
17705    #[test]
17706    fn crash_after_dispatch_post_reconciles_exact_run_without_a_second_post() {
17707        let plan = FakeWorkflowDispatchPlan::with_response(workflow_dispatch_receipt_response(73));
17708        let crash_sink = FailOnWorkflowDispatchReceiptSink::default();
17709        let durable = Arc::clone(&crash_sink.checkpoints);
17710        let first = GithubBuildProvider::with_artifact_store_and_clock(
17711            workflow_dispatch_provider_config(all_authorized()),
17712            transport(
17713                FakeGhRunner::with(workflow_dispatch_submit_responses(73))
17714                    .with_workflow_dispatch(plan.clone()),
17715            ),
17716            FakePublisher::default(),
17717            NoVerifiedArtifactStore,
17718            FixedClock(1_700_000_000_000),
17719        )
17720        .with_checkpoint_sink(crash_sink);
17721
17722        let error = poll_ready(first.submit(unsigned_request(), CancellationToken::new()))
17723            .expect_err("receipt checkpoint crash");
17724        assert!(matches!(
17725            error,
17726            RemoteBuildError::ProviderFailure { code, .. }
17727                if code == "workflow_dispatch_receipt_checkpoint_failed"
17728        ));
17729        assert_eq!(plan.0.requests.lock().expect("dispatch requests").len(), 1);
17730        let intent = durable
17731            .lock()
17732            .expect("durable checkpoints")
17733            .last()
17734            .cloned()
17735            .expect("durable dispatch intent");
17736        assert!(
17737            intent
17738                .workflow_dispatch
17739                .as_ref()
17740                .is_some_and(|dispatch| dispatch.receipt.is_none() && dispatch.uncertain)
17741        );
17742        assert!(intent.run.is_none());
17743
17744        let recovery_sink = CapturingCheckpointSink::default();
17745        let recovered = Arc::clone(&recovery_sink.checkpoints);
17746        let restarted = GithubBuildProvider::with_artifact_store_and_clock(
17747            workflow_dispatch_provider_config(all_authorized()),
17748            transport(FakeGhRunner::with([
17749                Ok(workflow_dispatch_run_row(73)),
17750                Ok(workflow_dispatch_run_row(73)),
17751                Ok(authenticated_user_row()),
17752                Ok(private_repository_row()),
17753                Ok(ordinary_workflow_dispatch_run_row(73)),
17754                Ok(authenticated_user_row()),
17755                Ok(private_repository_row()),
17756            ])),
17757            FakePublisher::default(),
17758            NoVerifiedArtifactStore,
17759            FixedClock(1_700_000_000_000),
17760        )
17761        .with_checkpoint_sink(recovery_sink);
17762        let identity = durable_identity_for_resume(&intent);
17763        restarted
17764            .restore_job_resumes_offline(vec![intent], &identity)
17765            .expect("restore dispatch intent");
17766        assert_eq!(
17767            restarted
17768                .reconcile_restored_job("operation-1", &CancellationToken::new())
17769                .expect("GET-only dispatch reconciliation"),
17770            GithubJobReconciliation::AlreadyMapped
17771        );
17772        let latest = recovered
17773            .lock()
17774            .expect("recovery checkpoints")
17775            .last()
17776            .cloned()
17777            .expect("recovered run checkpoint");
17778        assert_eq!(latest.run.as_ref().map(|run| run.run_id), Some(73));
17779        assert!(latest.workflow_dispatch.as_ref().is_some_and(|dispatch| {
17780            dispatch
17781                .receipt
17782                .as_ref()
17783                .is_some_and(|receipt| receipt.run_id == 73)
17784        }));
17785    }
17786
17787    #[test]
17788    fn existing_dispatch_intent_missing_or_ambiguous_run_never_reposts() {
17789        let intent = durable_workflow_dispatch_intent_resume();
17790        for (rows, expected_code) in [
17791            (Vec::new(), "github_workflow_dispatch_run_not_found"),
17792            (
17793                workflow_dispatch_run_row_with_title(
17794                    73,
17795                    &base64_fixture("rustferry-v1|wrong-public-input-correlation"),
17796                ),
17797                "github_workflow_dispatch_run_not_found",
17798            ),
17799            (
17800                [workflow_dispatch_run_row(73), workflow_dispatch_run_row(74)].concat(),
17801                "github_workflow_dispatch_run_ambiguous",
17802            ),
17803        ] {
17804            let provider = GithubBuildProvider::with_artifact_store_and_clock(
17805                workflow_dispatch_provider_config(all_authorized()),
17806                transport(FakeGhRunner::with([Ok(rows)])),
17807                FakePublisher::default(),
17808                NoVerifiedArtifactStore,
17809                FixedClock(1_700_000_000_000),
17810            )
17811            .with_checkpoint_sink(CapturingCheckpointSink::default());
17812            let identity = durable_identity_for_resume(&intent);
17813            provider
17814                .restore_job_resumes_offline(vec![intent.clone()], &identity)
17815                .expect("restore dispatch intent");
17816            let error = provider
17817                .refresh_restored_job_get_only("operation-1", &CancellationToken::new())
17818                .expect_err("unresolved dispatch run");
17819            assert!(matches!(
17820                error,
17821                RemoteBuildError::ProviderFailure { code, .. } if code == expected_code
17822            ));
17823            let state = provider.lock_state().expect("dispatch state");
17824            assert!(
17825                state
17826                    .jobs
17827                    .get("operation-1")
17828                    .expect("dispatch job")
17829                    .workflow_dispatch
17830                    .as_ref()
17831                    .is_some_and(|dispatch| dispatch.receipt.is_none() && dispatch.uncertain)
17832            );
17833        }
17834    }
17835
17836    #[test]
17837    fn cancelled_failing_intent_reconciliation_is_durable_before_restart_adoption() {
17838        let intent = durable_workflow_dispatch_intent_resume();
17839        let cancellation = CancellationToken::new();
17840        let sink = CapturingCheckpointSink::default();
17841        let durable = Arc::clone(&sink.checkpoints);
17842        let failing = GithubBuildProvider::with_artifact_store_and_clock(
17843            workflow_dispatch_provider_config(all_authorized()),
17844            transport(
17845                FakeGhRunner::with([Err(GhExecutionError::CommandFailed { exit_code: Some(1) })])
17846                    .cancelling_before(1, cancellation.clone()),
17847            ),
17848            FakePublisher::default(),
17849            NoVerifiedArtifactStore,
17850            FixedClock(1_700_000_000_000),
17851        )
17852        .with_checkpoint_sink(sink);
17853        let identity = durable_identity_for_resume(&intent);
17854        failing
17855            .restore_job_resumes_offline(vec![intent], &identity)
17856            .expect("restore dispatch intent");
17857
17858        failing
17859            .refresh_restored_job_get_only("operation-1", &cancellation)
17860            .expect_err("failing run lookup");
17861        let cancelled = durable
17862            .lock()
17863            .expect("dispatch checkpoints")
17864            .last()
17865            .cloned()
17866            .expect("durable cancellation after failing lookup");
17867        assert!(cancelled.cancellation_requested);
17868        assert!(cancelled.run.is_none());
17869
17870        let restarted = GithubBuildProvider::with_artifact_store_and_clock(
17871            workflow_dispatch_provider_config(all_authorized()),
17872            transport(FakeGhRunner::with([
17873                Ok(workflow_dispatch_run_row(73)),
17874                Ok(workflow_dispatch_run_row(73)),
17875                Ok(authenticated_user_row()),
17876                Ok(private_repository_row()),
17877                Ok(ordinary_workflow_dispatch_run_row(73)),
17878                Ok(authenticated_user_row()),
17879                Ok(private_repository_row()),
17880                Ok(ordinary_workflow_dispatch_run_row(73)),
17881                Ok(authenticated_user_row()),
17882                Ok(private_repository_row()),
17883                Ok(Vec::new()),
17884            ])),
17885            FakePublisher::default(),
17886            NoVerifiedArtifactStore,
17887            FixedClock(1_700_000_000_000),
17888        )
17889        .with_checkpoint_sink(CapturingCheckpointSink::default());
17890        restarted
17891            .restore_job_resumes_offline(
17892                vec![cancelled.clone()],
17893                &durable_identity_for_resume(&cancelled),
17894            )
17895            .expect("restore cancelled dispatch intent");
17896        restarted
17897            .reconcile_restored_job("operation-1", &CancellationToken::new())
17898            .expect("adopt and cancel exact dispatch run");
17899        let state = restarted.lock_state().expect("restarted dispatch state");
17900        let record = state.jobs.get("operation-1").expect("dispatch record");
17901        assert!(record.cancellation_requested);
17902        assert!(record.cancellation_dispatched);
17903    }
17904
17905    #[test]
17906    fn cancelled_failing_live_observation_is_durable_before_restart_cancel() {
17907        let mapped = durable_workflow_dispatch_mapped_resume();
17908        let cancellation = CancellationToken::new();
17909        let sink = CapturingCheckpointSink::default();
17910        let durable = Arc::clone(&sink.checkpoints);
17911        let failing = GithubBuildProvider::with_artifact_store_and_clock(
17912            workflow_dispatch_provider_config(all_authorized()),
17913            transport(
17914                FakeGhRunner::with([Err(GhExecutionError::CommandFailed { exit_code: Some(1) })])
17915                    .cancelling_before(1, cancellation.clone()),
17916            ),
17917            FakePublisher::default(),
17918            NoVerifiedArtifactStore,
17919            FixedClock(1_700_000_000_000),
17920        )
17921        .with_checkpoint_sink(sink);
17922        failing
17923            .restore_job_resumes_offline(
17924                vec![mapped.clone()],
17925                &durable_identity_for_resume(&mapped),
17926            )
17927            .expect("restore mapped dispatch run");
17928
17929        failing
17930            .revalidate_restored_job_live("operation-1", &cancellation)
17931            .expect_err("failing live run observation");
17932        let cancelled = durable
17933            .lock()
17934            .expect("dispatch checkpoints")
17935            .last()
17936            .cloned()
17937            .expect("durable cancellation after failing observation");
17938        assert!(cancelled.cancellation_requested);
17939        assert_eq!(cancelled.run.as_ref().map(|run| run.run_id), Some(73));
17940
17941        let restarted = GithubBuildProvider::with_artifact_store_and_clock(
17942            workflow_dispatch_provider_config(all_authorized()),
17943            transport(FakeGhRunner::with([
17944                Ok(ordinary_workflow_dispatch_run_row(73)),
17945                Ok(authenticated_user_row()),
17946                Ok(private_repository_row()),
17947                Ok(ordinary_workflow_dispatch_run_row(73)),
17948                Ok(authenticated_user_row()),
17949                Ok(private_repository_row()),
17950                Ok(Vec::new()),
17951            ])),
17952            FakePublisher::default(),
17953            NoVerifiedArtifactStore,
17954            FixedClock(1_700_000_000_000),
17955        )
17956        .with_checkpoint_sink(CapturingCheckpointSink::default());
17957        restarted
17958            .restore_job_resumes_offline(
17959                vec![cancelled.clone()],
17960                &durable_identity_for_resume(&cancelled),
17961            )
17962            .expect("restore cancelled mapped run");
17963        restarted
17964            .reconcile_restored_job("operation-1", &CancellationToken::new())
17965            .expect("cancel exact mapped run");
17966        let state = restarted.lock_state().expect("restarted dispatch state");
17967        let record = state.jobs.get("operation-1").expect("dispatch record");
17968        assert!(record.cancellation_requested);
17969        assert!(record.cancellation_dispatched);
17970    }
17971
17972    #[test]
17973    fn workflow_dispatch_preflight_failures_leave_no_ref_or_intent() {
17974        let cases = [
17975            (
17976                workflow_dispatch_provider_config_with_trusted_ref(
17977                    all_authorized(),
17978                    "refs/heads/not-default",
17979                ),
17980                vec![
17981                    Ok(authenticated_user_row()),
17982                    Ok(private_repository_row()),
17983                    Ok(private_repository_row()),
17984                ],
17985                Some(FakeWorkflowDispatchPlan::with_response(
17986                    workflow_dispatch_receipt_response(73),
17987                )),
17988            ),
17989            (
17990                workflow_dispatch_provider_config(all_authorized()),
17991                vec![
17992                    Ok(authenticated_user_row()),
17993                    Ok(private_repository_row()),
17994                    Ok(private_repository_row()),
17995                    Ok(format!("17\t{WORKFLOW_PATH_BASE64}\tdisabled_manually\n").into_bytes()),
17996                ],
17997                Some(FakeWorkflowDispatchPlan::with_response(
17998                    workflow_dispatch_receipt_response(73),
17999                )),
18000            ),
18001            (
18002                workflow_dispatch_provider_config(all_authorized()),
18003                vec![
18004                    Ok(authenticated_user_row()),
18005                    Ok(private_repository_row()),
18006                    Ok(private_repository_row()),
18007                    Ok(workflow_dispatch_registration_row()),
18008                ],
18009                None,
18010            ),
18011        ];
18012        for (config, responses, plan) in cases {
18013            let sink = CapturingCheckpointSink::default();
18014            let checkpoints = Arc::clone(&sink.checkpoints);
18015            let mut runner = FakeGhRunner::with(responses);
18016            if let Some(plan) = plan {
18017                runner = runner.with_workflow_dispatch(plan);
18018            }
18019            let provider = GithubBuildProvider::with_artifact_store_and_clock(
18020                config,
18021                transport(runner),
18022                FakePublisher::default(),
18023                NoVerifiedArtifactStore,
18024                FixedClock(1_700_000_000_000),
18025            )
18026            .with_checkpoint_sink(sink);
18027            poll_ready(provider.submit(unsigned_request(), CancellationToken::new()))
18028                .expect_err("workflow dispatch preflight must fail");
18029            assert!(checkpoints.lock().expect("checkpoints").is_empty());
18030            assert!(
18031                provider
18032                    .publisher
18033                    .lock()
18034                    .expect("publisher")
18035                    .published
18036                    .is_empty()
18037            );
18038        }
18039    }
18040
18041    #[test]
18042    fn active_push_only_default_workflow_is_rejected_before_ref_or_dispatch_intent() {
18043        let plan = FakeWorkflowDispatchPlan::with_response(workflow_dispatch_receipt_response(73));
18044        let sink = CapturingCheckpointSink::default();
18045        let checkpoints = Arc::clone(&sink.checkpoints);
18046        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18047            workflow_dispatch_provider_config(all_authorized()),
18048            transport(
18049                FakeGhRunner::with([
18050                    Ok(authenticated_user_row()),
18051                    Ok(private_repository_row()),
18052                    Ok(private_repository_row()),
18053                    Ok(workflow_dispatch_registration_row()),
18054                ])
18055                .with_workflow_dispatch(plan.clone()),
18056            ),
18057            FakePublisher {
18058                execution_workflow_failure: Some(
18059                    TemporaryRefPublishError::WorkflowFingerprintMismatch,
18060                ),
18061                ..FakePublisher::default()
18062            },
18063            NoVerifiedArtifactStore,
18064            FixedClock(1_700_000_000_000),
18065        )
18066        .with_checkpoint_sink(sink);
18067
18068        poll_ready(provider.submit(unsigned_request(), CancellationToken::new()))
18069            .expect_err("Push-only default workflow bytes");
18070        assert_eq!(plan.0.clients_created.load(Ordering::SeqCst), 0);
18071        assert!(
18072            plan.0
18073                .requests
18074                .lock()
18075                .expect("dispatch requests")
18076                .is_empty()
18077        );
18078        assert!(checkpoints.lock().expect("checkpoints").is_empty());
18079        assert_eq!(
18080            provider
18081                .publisher
18082                .lock()
18083                .expect("publisher")
18084                .execution_workflow_checks,
18085            1
18086        );
18087        assert!(
18088            provider
18089                .publisher
18090                .lock()
18091                .expect("publisher")
18092                .published
18093                .is_empty()
18094        );
18095    }
18096
18097    #[test]
18098    fn durable_identity_is_live_secret_free_and_stably_bound() {
18099        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18100            provider_config(all_authorized()),
18101            transport(FakeGhRunner::with([
18102                Ok(authenticated_user_row()),
18103                Ok(private_repository_row()),
18104            ])),
18105            FakePublisher::default(),
18106            NoVerifiedArtifactStore,
18107            FixedClock(1_700_000_000_000),
18108        );
18109        let identity = provider.durable_identity().expect("durable identity");
18110        assert_eq!(identity.provider, GITHUB_PROVIDER_ID);
18111        assert_eq!(identity.execution_repository_id, 991);
18112        assert_eq!(
18113            identity.execution_repository,
18114            "https://github.com/ShiroKSH/rustferry-signing"
18115        );
18116        assert_eq!(
18117            identity.principal,
18118            GithubPrincipalIdentityV1::User {
18119                id: AUTHENTICATED_USER_ID,
18120                login: "ShiroKSH".to_owned(),
18121            }
18122        );
18123        assert!(is_lower_sha256(&identity.provider_config_sha256));
18124    }
18125
18126    #[test]
18127    fn repository_credential_list_id_must_match_execution_metadata_id() {
18128        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18129            workflow_dispatch_provider_config(all_authorized()),
18130            transport(FakeGhRunner::with([
18131                Err(GhExecutionError::CommandFailed { exit_code: Some(1) }),
18132                Ok(b"992\tU2hpcm9LU0gvcnVzdGZlcnJ5LXNpZ25pbmc=\n".to_vec()),
18133                Ok(private_repository_row()),
18134            ])),
18135            FakePublisher::default(),
18136            NoVerifiedArtifactStore,
18137            FixedClock(1_700_000_000_000),
18138        );
18139
18140        assert!(matches!(
18141            provider.durable_identity(),
18142            Err(RemoteBuildError::ProviderFailure { code, .. })
18143                if code == "resume_repository_credential_repository_mismatch"
18144        ));
18145    }
18146
18147    #[test]
18148    fn uncheckpointed_visible_submission_cannot_be_exported_or_cancelled() {
18149        let config = provider_config(all_authorized());
18150        let principal = GithubPrincipalIdentityV1::User {
18151            id: AUTHENTICATED_USER_ID,
18152            login: "ShiroKSH".to_owned(),
18153        };
18154        let mut record = durable_prepublication_resume()
18155            .into_record(&config, &principal, 991)
18156            .expect("record fixture");
18157        record.publication_intent = false;
18158        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18159            config,
18160            transport(FakeGhRunner::default()),
18161            FakePublisher::default(),
18162            NoVerifiedArtifactStore,
18163            FixedClock(1_700_000_000_000),
18164        );
18165        provider
18166            .lock_state()
18167            .expect("state")
18168            .jobs
18169            .insert(record.job_id.clone(), record);
18170
18171        assert!(provider.export_job_resumes().is_err());
18172        assert!(
18173            poll_ready(provider.cancel(
18174                CancellationRequest {
18175                    job_id: "operation-1".to_owned(),
18176                    reason: "user-requested".to_owned(),
18177                },
18178                CancellationToken::new(),
18179            ))
18180            .is_err()
18181        );
18182        let state = provider.lock_state().expect("state");
18183        let record = state.jobs.get("operation-1").expect("pending job");
18184        assert_eq!(record.state, JobState::Created);
18185        assert!(!record.cancellation_requested);
18186    }
18187
18188    #[test]
18189    fn export_rejects_jobs_without_their_creation_identity() {
18190        let config = provider_config(all_authorized());
18191        let principal = GithubPrincipalIdentityV1::User {
18192            id: AUTHENTICATED_USER_ID,
18193            login: "ShiroKSH".to_owned(),
18194        };
18195        let mut record = durable_prepublication_resume()
18196            .into_record(&config, &principal, 991)
18197            .expect("record fixture");
18198        record.principal = None;
18199        record.execution_repository_id = None;
18200        let runner = FakeGhRunner::default();
18201        let requests = Arc::clone(&runner.requests);
18202        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18203            config,
18204            transport(runner),
18205            FakePublisher::default(),
18206            NoVerifiedArtifactStore,
18207            FixedClock(1_700_000_000_000),
18208        );
18209        provider
18210            .lock_state()
18211            .expect("state")
18212            .jobs
18213            .insert(record.job_id.clone(), record);
18214
18215        assert!(provider.export_job_resumes().is_err());
18216        assert!(requests.lock().expect("requests").is_empty());
18217    }
18218
18219    #[test]
18220    fn checkpoint_sink_captures_pre_push_intent_and_export_restores_strict_state() {
18221        let sink = CapturingCheckpointSink::default();
18222        let checkpoints = Arc::clone(&sink.checkpoints);
18223        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18224            provider_config(all_authorized()),
18225            transport(FakeGhRunner::with([
18226                Ok(authenticated_user_row()),
18227                Ok(private_repository_row()),
18228                Ok(authenticated_user_row()),
18229                Ok(private_repository_row()),
18230                Ok(authenticated_user_row()),
18231                Ok(private_repository_row()),
18232            ])),
18233            FakePublisher {
18234                prepared_publication: Some(expected_publication()),
18235                ..FakePublisher::default()
18236            },
18237            NoVerifiedArtifactStore,
18238            FixedClock(1_700_000_000_000),
18239        )
18240        .with_checkpoint_sink(sink);
18241
18242        poll_ready(provider.submit(unsigned_request(), CancellationToken::new()))
18243            .expect("checkpointed submission");
18244        let captured = checkpoints.lock().expect("captured checkpoints");
18245        assert_eq!(captured.len(), 4);
18246        assert!(captured[0].publication_intent);
18247        assert_eq!(
18248            captured[0].prepared_dispatch_commit.as_deref(),
18249            Some(DISPATCH_SHA)
18250        );
18251        assert_eq!(captured[0].dispatch_commit, None);
18252        assert_eq!(captured[0].publication_started_at_ms, 0);
18253        assert_eq!(captured[0].publication_quiescence_deadline_ms, u64::MAX);
18254        assert_eq!(captured[1].publication_started_at_ms, 1_700_000_000_000);
18255        assert_eq!(
18256            captured[1].publication_quiescence_deadline_ms,
18257            1_700_000_000_000 + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS
18258        );
18259        assert_eq!(captured[1].dispatch_commit, None);
18260        assert_eq!(captured[2].dispatch_commit.as_deref(), Some(DISPATCH_SHA));
18261        assert_eq!(captured[3].state, JobState::Queued);
18262        assert!(
18263            captured
18264                .iter()
18265                .all(|resume| resume.publication_process_fenced)
18266        );
18267        assert!(captured.iter().all(|resume| {
18268            resume.principal
18269                == GithubPrincipalIdentityV1::User {
18270                    id: AUTHENTICATED_USER_ID,
18271                    login: "ShiroKSH".to_owned(),
18272                }
18273                && resume.execution_repository_id == 991
18274        }));
18275        drop(captured);
18276
18277        let exported = provider.export_job_resumes().expect("durable export");
18278        assert_eq!(exported.len(), 1);
18279        assert_eq!(exported[0].state, JobState::Queued);
18280        let encoded_resume = exported[0].encode().expect("encoded resume");
18281        assert_eq!(
18282            GithubJobResumeV1::decode(&encoded_resume).expect("strict resume JSON"),
18283            exported[0]
18284        );
18285        let mut duplicate = encoded_resume;
18286        duplicate.pop();
18287        duplicate.splice(1..1, b"\"schema_version\":1,".iter().copied());
18288        assert!(GithubJobResumeV1::decode(&duplicate).is_err());
18289        let mut encoded = serde_json::to_value(&exported[0]).expect("resume JSON");
18290        encoded
18291            .as_object_mut()
18292            .expect("resume object")
18293            .insert("unexpected".to_owned(), serde_json::json!(true));
18294        let mut unknown = serde_json::to_vec(&encoded).expect("unknown field JSON");
18295        unknown.push(b'\n');
18296        assert!(GithubJobResumeV1::decode(&unknown).is_err());
18297        let mut encoded = serde_json::to_value(&exported[0]).expect("resume JSON");
18298        encoded
18299            .get_mut("request")
18300            .and_then(serde_json::Value::as_object_mut)
18301            .expect("nested request object")
18302            .insert("future_safety_field".to_owned(), serde_json::json!(true));
18303        let mut nested_unknown = serde_json::to_vec(&encoded).expect("nested unknown field JSON");
18304        nested_unknown.push(b'\n');
18305        assert!(GithubJobResumeV1::decode(&nested_unknown).is_err());
18306
18307        let restored = GithubBuildProvider::with_artifact_store_and_clock(
18308            provider_config(all_authorized()),
18309            transport(FakeGhRunner::with([
18310                Ok(authenticated_user_row()),
18311                Ok(private_repository_row()),
18312            ])),
18313            FakePublisher::default(),
18314            NoVerifiedArtifactStore,
18315            FixedClock(1_700_000_000_000),
18316        );
18317        restored
18318            .restore_job_resumes(exported)
18319            .expect("strict restore");
18320        assert_eq!(restored.lock_state().expect("state").jobs.len(), 1);
18321    }
18322
18323    #[test]
18324    fn resume_successor_rejects_stale_replacements_but_allows_skipped_internal_states() {
18325        let prepublication = durable_prepublication_resume();
18326        let queued = durable_resume_with_run();
18327        prepublication
18328            .validate_successor(&queued)
18329            .expect("publication and run identity bind once");
18330
18331        let succeeded = durable_success_resume();
18332        queued
18333            .validate_successor(&succeeded)
18334            .expect("queued checkpoint may complete before the next durable write");
18335        let mut cleaned = succeeded.clone();
18336        cleaned.state = JobState::Cleaned;
18337        cleaned.cleanup_requested = true;
18338        cleaned.temporary_ref_deleted = true;
18339        succeeded
18340            .validate_successor(&cleaned)
18341            .expect("cleanup may skip its in-memory intermediate state");
18342
18343        let mut signed_succeeded = succeeded.clone();
18344        signed_succeeded.request = valid_request();
18345        signed_succeeded.request_sha256 =
18346            canonical_request_sha256(&signed_succeeded.request).expect("signed request digest");
18347        let mut signed_cleaned = signed_succeeded.clone();
18348        signed_cleaned.state = JobState::Cleaned;
18349        signed_cleaned.cleanup_requested = true;
18350        signed_cleaned.temporary_ref_deleted = true;
18351        assert!(
18352            signed_succeeded
18353                .validate_successor(&signed_cleaned)
18354                .is_err(),
18355            "signed cleanup needs attempt-bound worker evidence"
18356        );
18357
18358        assert!(queued.validate_successor(&prepublication).is_err());
18359        let mut changed_attempt = queued.clone();
18360        changed_attempt.run.as_mut().expect("run").run_attempt = 2;
18361        assert!(queued.validate_successor(&changed_attempt).is_err());
18362        let mut changed_fence = queued.clone();
18363        changed_fence.publication_process_fenced = !changed_fence.publication_process_fenced;
18364        assert!(queued.validate_successor(&changed_fence).is_err());
18365        let mut changed_lease_scope = queued.clone();
18366        changed_lease_scope.publication_lease_scope_sha256 = Some("b".repeat(64));
18367        assert!(queued.validate_successor(&changed_lease_scope).is_err());
18368        let mut removed_run = queued.clone();
18369        removed_run.run = None;
18370        assert!(queued.validate_successor(&removed_run).is_err());
18371        let mut mismatched_dispatch = prepublication.clone();
18372        mismatched_dispatch.prepared_dispatch_commit = Some(SOURCE_SHA.to_owned());
18373        let mut mismatched_successor = mismatched_dispatch.clone();
18374        mismatched_successor.dispatch_commit = Some(DISPATCH_SHA.to_owned());
18375        assert!(
18376            mismatched_dispatch
18377                .validate_successor(&mismatched_successor)
18378                .is_err()
18379        );
18380        let mut changed_manifests = cleaned.clone();
18381        changed_manifests.manifests[0].project_id = "different".to_owned();
18382        assert!(cleaned.validate_successor(&changed_manifests).is_err());
18383    }
18384
18385    #[test]
18386    fn legacy_resume_bytes_omit_the_optional_git_snapshot_checkpoint() {
18387        let resume = durable_resume_with_run();
18388        let bytes = resume.encode().expect("legacy resume bytes");
18389        assert!(
18390            !bytes
18391                .windows(b"git_snapshot".len())
18392                .any(|part| part == b"git_snapshot")
18393        );
18394        let decoded = GithubJobResumeV1::decode(&bytes).expect("legacy resume decode");
18395        assert_eq!(decoded.git_snapshot, None);
18396        assert_eq!(decoded.encode().expect("legacy resume reencode"), bytes);
18397    }
18398
18399    #[test]
18400    fn snapshot_successor_validates_the_complete_next_phase_shape() {
18401        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
18402        let mut malformed = fixture.resume.clone();
18403        let snapshot = malformed.git_snapshot.as_mut().expect("snapshot resume");
18404        snapshot.source_publication_started_at_ms = malformed.created_at_ms;
18405        snapshot.source_publication_quiescence_deadline_ms =
18406            publication_quiescence_deadline(snapshot.source_publication_started_at_ms, 0);
18407        let error = fixture
18408            .resume
18409            .validate_successor(&malformed)
18410            .expect_err("same-phase malformed successor must fail");
18411        assert!(matches!(
18412            error,
18413            RemoteBuildError::ProviderFailure { code, .. }
18414                if code == "resume_git_snapshot_source_started_early"
18415        ));
18416    }
18417
18418    #[test]
18419    fn snapshot_exact_source_requires_an_armed_publication_boundary() {
18420        let mut fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::SourceExact);
18421        let snapshot = fixture
18422            .resume
18423            .git_snapshot
18424            .as_mut()
18425            .expect("snapshot resume");
18426        snapshot.source_publication_started_at_ms = 0;
18427        snapshot.source_publication_quiescence_deadline_ms = u64::MAX;
18428        let error = snapshot
18429            .validate_for_request(&fixture.resume.request)
18430            .expect_err("unarmed exact source must not be adopted");
18431        assert!(matches!(
18432            error,
18433            RemoteBuildError::ProviderFailure { code, .. }
18434                if code == "resume_git_snapshot_source_ownership_unarmed"
18435        ));
18436    }
18437
18438    #[test]
18439    fn standalone_cleanup_restore_requires_terminal_snapshot_source_proof() {
18440        let config = provider_config(all_authorized());
18441        let principal = GithubPrincipalIdentityV1::User {
18442            id: AUTHENTICATED_USER_ID,
18443            login: "ShiroKSH".to_owned(),
18444        };
18445        for state in [JobState::Cleaned, JobState::CleanupFailed] {
18446            let exact = completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceExact, state);
18447            let error = exact
18448                .resume
18449                .clone()
18450                .into_record(&config, &principal, 991)
18451                .expect_err("cleanup state cannot retain an owned source ref");
18452            assert!(matches!(
18453                error,
18454                RemoteBuildError::ProviderFailure { code, .. }
18455                    if code == "resume_state_inconsistent"
18456            ));
18457
18458            let deleted = completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceDeleted, state);
18459            deleted
18460                .resume
18461                .clone()
18462                .into_record(&config, &principal, 991)
18463                .expect("terminal source deletion proof permits cleanup restore");
18464        }
18465    }
18466
18467    #[test]
18468    #[allow(
18469        clippy::too_many_lines,
18470        reason = "one adversarial regression proves the no-write boundary, capability and operation gates, and both cancellation checks"
18471    )]
18472    fn initial_snapshot_graph_precompute_is_exact_no_write_and_cancellation_bounded() {
18473        let fixture = initial_snapshot_precompute_fixture();
18474        let mut inputs = fixture.inputs();
18475        let git_runner = FakeGitRunner::with(initial_snapshot_graph_responses());
18476        let gh_runner = FakeGhRunner::with([]);
18477        let gh_requests = Arc::clone(&gh_runner.requests);
18478        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18479            provider_config(all_authorized()),
18480            transport(gh_runner),
18481            GitTemporaryRefPublisher::new(
18482                git_runner,
18483                fixture.isolation.path(),
18484                "source",
18485                "execution",
18486                Duration::from_secs(30),
18487            )
18488            .expect("snapshot precompute publisher"),
18489            NoVerifiedArtifactStore,
18490            FixedClock(1_700_000_000_000),
18491        );
18492        let tree_before = test_tree_snapshot(fixture.isolation.path());
18493
18494        let graph = provider
18495            .precompute_git_snapshot_graph(
18496                &mut inputs,
18497                "operation-1",
18498                1_700_000_000_000,
18499                &CancellationToken::new(),
18500            )
18501            .expect("initial snapshot graph");
18502        assert_eq!(
18503            graph,
18504            GitSnapshotObjectGraphV1 {
18505                schema_version: crate::snapshot::GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION,
18506                archive_blob: snapshot_object('1'),
18507                descriptor_blob: snapshot_object('2'),
18508                goal3_tree: snapshot_object('3'),
18509                rustferry_tree: snapshot_object('4'),
18510                root_tree: snapshot_object('5'),
18511                commit: snapshot_object('6'),
18512            }
18513        );
18514        inputs
18515            .verify_contents()
18516            .expect("retained inputs after precompute");
18517        assert_eq!(test_tree_snapshot(fixture.isolation.path()), tree_before);
18518        assert!(gh_requests.lock().expect("GitHub requests").is_empty());
18519        {
18520            let publisher = provider.publisher.lock().expect("publisher");
18521            assert!(publisher.snapshot_imports.is_empty());
18522            assert!(publisher.publication_leases.is_empty());
18523            assert!(publisher.frozen_remotes.is_none());
18524            assert!(publisher.runner.responses.is_empty());
18525            assert_eq!(publisher.runner.invocations.len(), 7);
18526            for invocation in &publisher.runner.invocations {
18527                assert_eq!(invocation.network, GitNetworkPolicy::Offline);
18528                let arguments = invocation
18529                    .arguments
18530                    .iter()
18531                    .map(|argument| argument.to_string_lossy())
18532                    .collect::<Vec<_>>();
18533                assert!(!arguments.iter().any(|argument| argument == "-w"));
18534                assert!(!arguments.iter().any(|argument| matches!(
18535                    argument.as_ref(),
18536                    "push" | "fetch" | "update-ref" | "checkout"
18537                )));
18538            }
18539        }
18540
18541        let invocation_count = provider
18542            .publisher
18543            .lock()
18544            .expect("publisher")
18545            .runner
18546            .invocations
18547            .len();
18548        let invalid_operation = provider
18549            .precompute_git_snapshot_graph(
18550                &mut fixture.inputs(),
18551                "../unsafe",
18552                1_700_000_000_000,
18553                &CancellationToken::new(),
18554            )
18555            .expect_err("unsafe operation must fail before Git");
18556        assert!(matches!(
18557            invalid_operation,
18558            RemoteBuildError::ProviderFailure { code, .. } if code == "git_snapshot_invalid"
18559        ));
18560        let cancelled_before = CancellationToken::new();
18561        assert!(cancelled_before.cancel());
18562        assert_eq!(
18563            provider.precompute_git_snapshot_graph(
18564                &mut fixture.inputs(),
18565                "operation-1",
18566                1_700_000_000_000,
18567                &cancelled_before,
18568            ),
18569            Err(RemoteBuildError::Cancelled)
18570        );
18571        assert_eq!(
18572            provider
18573                .publisher
18574                .lock()
18575                .expect("publisher")
18576                .runner
18577                .invocations
18578                .len(),
18579            invocation_count
18580        );
18581        assert_eq!(test_tree_snapshot(fixture.isolation.path()), tree_before);
18582
18583        let unsupported = GithubBuildProvider::with_artifact_store_and_clock(
18584            provider_config(all_authorized()),
18585            transport(FakeGhRunner::with([])),
18586            FakePublisher::default(),
18587            NoVerifiedArtifactStore,
18588            FixedClock(1_700_000_000_000),
18589        );
18590        assert!(matches!(
18591            unsupported.precompute_git_snapshot_graph(
18592                &mut fixture.inputs(),
18593                "operation-1",
18594                1_700_000_000_000,
18595                &CancellationToken::new(),
18596            ),
18597            Err(RemoteBuildError::UnsupportedCapability {
18598                feature: ProviderFeature::SourceMode(SourceMode::GitSnapshot),
18599                ..
18600            })
18601        ));
18602        assert_eq!(test_tree_snapshot(fixture.isolation.path()), tree_before);
18603        drop(provider);
18604
18605        let cancelled_after = CancellationToken::new();
18606        let post_cancel_gh = FakeGhRunner::with([]);
18607        let post_cancel_requests = Arc::clone(&post_cancel_gh.requests);
18608        let post_cancel_provider = GithubBuildProvider::with_artifact_store_and_clock(
18609            provider_config(all_authorized()),
18610            transport(post_cancel_gh),
18611            GitTemporaryRefPublisher::new(
18612                FakeGitRunner::cancelling_after(
18613                    initial_snapshot_graph_responses(),
18614                    7,
18615                    cancelled_after.clone(),
18616                ),
18617                fixture.isolation.path(),
18618                "source",
18619                "execution",
18620                Duration::from_secs(30),
18621            )
18622            .expect("post-compute cancellation publisher"),
18623            NoVerifiedArtifactStore,
18624            FixedClock(1_700_000_000_000),
18625        );
18626        assert_eq!(
18627            post_cancel_provider.precompute_git_snapshot_graph(
18628                &mut fixture.inputs(),
18629                "operation-1",
18630                1_700_000_000_000,
18631                &cancelled_after,
18632            ),
18633            Err(RemoteBuildError::Cancelled)
18634        );
18635        assert!(cancelled_after.is_cancelled());
18636        assert!(
18637            post_cancel_requests
18638                .lock()
18639                .expect("post-cancel GitHub requests")
18640                .is_empty()
18641        );
18642        let publisher = post_cancel_provider.publisher.lock().expect("publisher");
18643        assert_eq!(publisher.runner.invocations.len(), 7);
18644        assert!(publisher.publication_leases.is_empty());
18645        assert!(publisher.snapshot_imports.is_empty());
18646        assert!(publisher.frozen_remotes.is_none());
18647        assert_eq!(test_tree_snapshot(fixture.isolation.path()), tree_before);
18648    }
18649
18650    #[test]
18651    fn snapshot_recovery_discovery_is_read_only_and_filters_provider_owned_operations() {
18652        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
18653        let isolation = fixture.isolation.path().to_path_buf();
18654        let resume = fixture.resume.clone();
18655        let snapshot = resume.git_snapshot.as_ref().expect("snapshot resume");
18656        let archive_path =
18657            crate::snapshot::git_snapshot_stage_directory(&isolation, &resume.operation_id)
18658                .expect("stage path")
18659                .join(crate::snapshot::GIT_SNAPSHOT_STAGE_ARCHIVE_FILE);
18660        let descriptor_path =
18661            crate::snapshot::git_snapshot_stage_directory(&isolation, &resume.operation_id)
18662                .expect("stage path")
18663                .join(crate::snapshot::GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE);
18664        let metadata_path =
18665            crate::snapshot::git_snapshot_stage_directory(&isolation, &resume.operation_id)
18666                .expect("stage path")
18667                .join(crate::snapshot::GIT_SNAPSHOT_STAGE_METADATA_FILE);
18668        let before = [
18669            fs::read(&archive_path).expect("archive bytes"),
18670            fs::read(&descriptor_path).expect("descriptor bytes"),
18671            fs::read(&metadata_path).expect("metadata bytes"),
18672        ];
18673        let publisher = GitTemporaryRefPublisher::new(
18674            FakeGitRunner::default(),
18675            &isolation,
18676            "source",
18677            "execution",
18678            Duration::from_secs(1),
18679        )
18680        .expect("snapshot publisher");
18681        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18682            provider_config(all_authorized()),
18683            transport(FakeGhRunner::with([])),
18684            publisher,
18685            NoVerifiedArtifactStore,
18686            FixedClock(1_700_000_000_000),
18687        );
18688
18689        let candidates = provider
18690            .discover_uncheckpointed_git_snapshot_stages()
18691            .expect("read-only discovery");
18692        assert_eq!(candidates.len(), 1);
18693        assert_eq!(candidates[0].stage_locator, snapshot.stage_locator);
18694        assert_eq!(candidates[0].stage, snapshot.stage);
18695        assert_eq!(candidates[0].final_request, resume.request);
18696        assert!(
18697            provider
18698                .publisher
18699                .lock()
18700                .expect("publisher")
18701                .runner
18702                .invocations
18703                .is_empty()
18704        );
18705        assert_eq!(
18706            [
18707                fs::read(&archive_path).expect("archive bytes"),
18708                fs::read(&descriptor_path).expect("descriptor bytes"),
18709                fs::read(&metadata_path).expect("metadata bytes"),
18710            ],
18711            before
18712        );
18713
18714        provider
18715            .restore_job_resumes_offline(
18716                vec![resume.clone()],
18717                &durable_identity_for_resume(&resume),
18718            )
18719            .expect("restore provider owner");
18720        assert!(
18721            provider
18722                .discover_uncheckpointed_git_snapshot_stages()
18723                .expect("owned-stage discovery")
18724                .is_empty()
18725        );
18726        assert!(
18727            provider
18728                .publisher
18729                .lock()
18730                .expect("publisher")
18731                .runner
18732                .invocations
18733                .is_empty()
18734        );
18735    }
18736
18737    #[test]
18738    fn fresh_reconsent_reserves_the_operation_before_any_stage_adoption() {
18739        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
18740        let resume = fixture.resume.clone();
18741        let snapshot = resume.git_snapshot.as_ref().expect("snapshot resume");
18742        let submission = GithubGitSnapshotSubmissionV1::after_fresh_reconsent(
18743            durable_identity_for_resume(&resume),
18744            resume.request.clone(),
18745            snapshot.stage.consent_sha256.clone(),
18746        )
18747        .expect("fresh reconsent submission");
18748        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18749            provider_config(all_authorized()),
18750            transport(FakeGhRunner::with([])),
18751            SnapshotFlowPublisher::exact(),
18752            NoVerifiedArtifactStore,
18753            FixedClock(1_700_000_000_000),
18754        )
18755        .with_checkpoint_sink(CapturingCheckpointSink::default());
18756        let reservation = JobReservation::acquire(
18757            &provider.state,
18758            resume.operation_id.clone(),
18759            provider.config.max_jobs,
18760            false,
18761        )
18762        .expect("concurrent operation reservation");
18763
18764        let error = provider
18765            .submit_git_snapshot(submission, &CancellationToken::new())
18766            .expect_err("active operation reservation wins before adoption");
18767        assert!(matches!(
18768            error,
18769            RemoteBuildError::ProviderFailure { code, .. } if code == "duplicate_job_id"
18770        ));
18771        assert!(
18772            provider
18773                .publisher
18774                .lock()
18775                .expect("publisher")
18776                .events
18777                .is_empty()
18778        );
18779        drop(reservation);
18780    }
18781
18782    #[test]
18783    fn snapshot_source_cleanup_holds_lease_until_deleted_checkpoint_is_durable() {
18784        let config = provider_config(all_authorized());
18785        let principal = GithubPrincipalIdentityV1::User {
18786            id: AUTHENTICATED_USER_ID,
18787            login: "ShiroKSH".to_owned(),
18788        };
18789        let fixture =
18790            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceExact, JobState::Succeeded);
18791        let mut record = fixture
18792            .resume
18793            .clone()
18794            .into_record(&config, &principal, 991)
18795            .expect("snapshot record");
18796        let events = Arc::new(Mutex::new(Vec::new()));
18797        let publisher = SnapshotCleanupAuditPublisher {
18798            events: Arc::clone(&events),
18799            source_lease_owned: false,
18800            deletion_count: 0,
18801            keepalive_release_count: 0,
18802        };
18803        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18804            config,
18805            transport(FakeGhRunner::with([
18806                Ok(authenticated_user_row()),
18807                Ok(private_repository_row()),
18808            ])),
18809            publisher,
18810            NoVerifiedArtifactStore,
18811            FixedClock(1_700_000_000_000),
18812        )
18813        .with_checkpoint_sink(SnapshotCleanupCheckpointSink {
18814            events: Arc::clone(&events),
18815            fail_deleted_once: true,
18816        });
18817
18818        let error = provider
18819            .cleanup_git_snapshot_source(&mut record)
18820            .expect_err("first SourceDeleted checkpoint must fail");
18821        assert!(matches!(
18822            error,
18823            RemoteBuildError::ProviderFailure { code, .. }
18824                if code == "checkpoint_fixture_failed"
18825        ));
18826        assert_eq!(
18827            record.git_snapshot.as_ref().expect("snapshot").phase,
18828            GithubGitSnapshotPhaseV1::SourceDeleted
18829        );
18830        {
18831            let publisher = provider.publisher.lock().expect("publisher");
18832            assert!(publisher.source_lease_owned);
18833            assert_eq!(publisher.deletion_count, 1);
18834            assert_eq!(publisher.keepalive_release_count, 0);
18835        }
18836        assert_eq!(
18837            *events.lock().expect("snapshot cleanup events"),
18838            vec![
18839                SnapshotCleanupAuditEvent::Checkpoint(
18840                    GithubGitSnapshotPhaseV1::SourceCleanupIntent,
18841                ),
18842                SnapshotCleanupAuditEvent::Acquire,
18843                SnapshotCleanupAuditEvent::Delete,
18844                SnapshotCleanupAuditEvent::Checkpoint(GithubGitSnapshotPhaseV1::SourceDeleted,),
18845            ]
18846        );
18847
18848        provider
18849            .cleanup_git_snapshot_source(&mut record)
18850            .expect("replay deletion checkpoint and release lease");
18851        let publisher = provider.publisher.lock().expect("publisher");
18852        assert!(!publisher.source_lease_owned);
18853        assert_eq!(publisher.deletion_count, 1);
18854        assert_eq!(publisher.keepalive_release_count, 0);
18855        drop(publisher);
18856        assert_eq!(
18857            *events.lock().expect("snapshot cleanup events"),
18858            vec![
18859                SnapshotCleanupAuditEvent::Checkpoint(
18860                    GithubGitSnapshotPhaseV1::SourceCleanupIntent,
18861                ),
18862                SnapshotCleanupAuditEvent::Acquire,
18863                SnapshotCleanupAuditEvent::Delete,
18864                SnapshotCleanupAuditEvent::Checkpoint(GithubGitSnapshotPhaseV1::SourceDeleted,),
18865                SnapshotCleanupAuditEvent::Checkpoint(GithubGitSnapshotPhaseV1::SourceDeleted,),
18866                SnapshotCleanupAuditEvent::Release,
18867            ]
18868        );
18869    }
18870
18871    #[test]
18872    fn snapshot_keepalive_prune_checkpoints_intent_before_release_and_replays() {
18873        let fixture =
18874            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceDeleted, JobState::Cleaned);
18875        let resume = fixture.resume.clone();
18876        let expected = GithubDurableIdentityV1 {
18877            provider: resume.provider.clone(),
18878            provider_config_sha256: resume.provider_config_sha256.clone(),
18879            principal: resume.principal.clone(),
18880            execution_repository: resume.execution_repository.clone(),
18881            execution_repository_id: resume.execution_repository_id,
18882        };
18883        let checkpoints = Arc::new(Mutex::new(Vec::new()));
18884        let mut publisher = SnapshotFlowPublisher::exact();
18885        publisher.keepalive_exact = true;
18886        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18887            provider_config(all_authorized()),
18888            transport(FakeGhRunner::with([])),
18889            publisher,
18890            NoVerifiedArtifactStore,
18891            FixedClock(1_700_000_000_000),
18892        )
18893        .with_checkpoint_sink(FailOnSnapshotPhaseSink {
18894            checkpoints: Arc::clone(&checkpoints),
18895            phase: GithubGitSnapshotPhaseV1::KeepaliveReleased,
18896            failed_once: true,
18897        });
18898        provider
18899            .restore_job_resumes_offline(vec![resume], &expected)
18900            .expect("offline snapshot restore");
18901        let authorization =
18902            GithubGitSnapshotKeepaliveReleaseAuthorizationV1::new("operation-1", "7".repeat(64))
18903                .expect("prune authorization");
18904
18905        let error = provider
18906            .release_git_snapshot_keepalive_for_prune(&authorization, &CancellationToken::new())
18907            .expect_err("Released checkpoint acknowledgement fails once");
18908        assert!(matches!(
18909            error,
18910            RemoteBuildError::ProviderFailure { code, .. }
18911                if code == "checkpoint_fixture_failed"
18912        ));
18913        let publisher = provider.publisher.lock().expect("publisher");
18914        assert_eq!(
18915            publisher.events,
18916            [
18917                SnapshotFlowEvent::ReconcileKeepalive,
18918                SnapshotFlowEvent::ReleaseKeepalive,
18919                SnapshotFlowEvent::ReconcileKeepalive,
18920            ]
18921        );
18922        assert!(!publisher.keepalive_exact);
18923        drop(publisher);
18924        assert_eq!(
18925            checkpoints
18926                .lock()
18927                .expect("snapshot checkpoints")
18928                .iter()
18929                .map(|resume| resume.git_snapshot.as_ref().expect("snapshot").phase)
18930                .collect::<Vec<_>>(),
18931            [
18932                GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent,
18933                GithubGitSnapshotPhaseV1::KeepaliveReleased,
18934            ]
18935        );
18936
18937        provider
18938            .release_git_snapshot_keepalive_for_prune(&authorization, &CancellationToken::new())
18939            .expect("replay Released checkpoint without another mutation");
18940        let publisher = provider.publisher.lock().expect("publisher");
18941        assert_eq!(
18942            publisher.events,
18943            [
18944                SnapshotFlowEvent::ReconcileKeepalive,
18945                SnapshotFlowEvent::ReleaseKeepalive,
18946                SnapshotFlowEvent::ReconcileKeepalive,
18947            ]
18948        );
18949        drop(publisher);
18950        let state = provider.lock_state().expect("state");
18951        let snapshot = state.jobs["operation-1"]
18952            .git_snapshot
18953            .as_ref()
18954            .expect("snapshot");
18955        assert_eq!(snapshot.phase, GithubGitSnapshotPhaseV1::KeepaliveReleased);
18956        assert_eq!(
18957            snapshot.keepalive_release_authorization_sha256.as_deref(),
18958            Some("7777777777777777777777777777777777777777777777777777777777777777")
18959        );
18960    }
18961
18962    #[test]
18963    fn snapshot_keepalive_prune_requires_terminal_cleanup_and_stable_transaction() {
18964        let fixture =
18965            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceDeleted, JobState::Succeeded);
18966        let resume = fixture.resume.clone();
18967        let expected = GithubDurableIdentityV1 {
18968            provider: resume.provider.clone(),
18969            provider_config_sha256: resume.provider_config_sha256.clone(),
18970            principal: resume.principal.clone(),
18971            execution_repository: resume.execution_repository.clone(),
18972            execution_repository_id: resume.execution_repository_id,
18973        };
18974        let provider = GithubBuildProvider::with_artifact_store_and_clock(
18975            provider_config(all_authorized()),
18976            transport(FakeGhRunner::with([])),
18977            SnapshotFlowPublisher::exact(),
18978            NoVerifiedArtifactStore,
18979            FixedClock(1_700_000_000_000),
18980        )
18981        .with_checkpoint_sink(CapturingCheckpointSink::default());
18982        provider
18983            .restore_job_resumes_offline(vec![resume], &expected)
18984            .expect("offline snapshot restore");
18985        let authorization =
18986            GithubGitSnapshotKeepaliveReleaseAuthorizationV1::new("operation-1", "7".repeat(64))
18987                .expect("prune authorization");
18988        let error = provider
18989            .release_git_snapshot_keepalive_for_prune(&authorization, &CancellationToken::new())
18990            .expect_err("build-terminal is not cleanup-complete");
18991        assert!(matches!(
18992            error,
18993            RemoteBuildError::ProviderFailure { code, .. }
18994                if code == "snapshot_keepalive_prune_not_terminal"
18995        ));
18996        assert!(
18997            provider
18998                .publisher
18999                .lock()
19000                .expect("publisher")
19001                .events
19002                .is_empty()
19003        );
19004    }
19005
19006    #[test]
19007    #[allow(
19008        clippy::too_many_lines,
19009        reason = "one regression proves no-write planning, durable-plan staging, complete-stage replay, and fixed-Git policy"
19010    )]
19011    fn exact_retry_precompute_is_no_write_and_complete_stage_replay_is_exact() {
19012        let fixture =
19013            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceDeleted, JobState::Cleaned);
19014        let resume = fixture.resume.clone();
19015        let expected = durable_identity_for_resume(&resume);
19016        let mut responses = Vec::new();
19017        for _ in 0..3 {
19018            responses.extend(exact_retry_git_cycle(&resume, SNAPSHOT_ARCHIVE_BYTES, '1'));
19019        }
19020        let publisher = GitTemporaryRefPublisher::new(
19021            FakeGitRunner::with(responses),
19022            fixture.isolation.path(),
19023            "origin",
19024            "signing",
19025            Duration::from_secs(30),
19026        )
19027        .expect("exact retry publisher");
19028        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19029            provider_config(all_authorized()),
19030            transport(FakeGhRunner::with([])),
19031            publisher,
19032            NoVerifiedArtifactStore,
19033            FixedClock(1_700_000_000_000),
19034        )
19035        .with_checkpoint_sink(CapturingCheckpointSink::default());
19036        provider
19037            .restore_job_resumes_offline(vec![resume.clone()], &expected)
19038            .expect("offline parent restore");
19039        let child_stage_path =
19040            crate::snapshot::git_snapshot_stage_directory(fixture.isolation.path(), "operation-2")
19041                .expect("child stage path");
19042
19043        let plan = provider
19044            .precompute_exact_git_snapshot_retry(
19045                "operation-1",
19046                "operation-2",
19047                1_700_000_001_000,
19048                &CancellationToken::new(),
19049            )
19050            .expect("exact retry plan");
19051        assert!(
19052            !child_stage_path.exists(),
19053            "planning must not create a stage"
19054        );
19055        assert_eq!(plan.request.operation_id, "operation-2");
19056        let expected_child_commit = "e".repeat(40);
19057        assert_eq!(
19058            plan.request.source_revision.as_deref(),
19059            Some(expected_child_commit.as_str())
19060        );
19061        assert_eq!(plan.graph.archive_blob, snapshot_object('1'));
19062        assert_eq!(plan.graph.descriptor_blob, snapshot_object('a'));
19063        assert_eq!(plan.graph.goal3_tree, snapshot_object('b'));
19064        assert_eq!(plan.graph.rustferry_tree, snapshot_object('c'));
19065        assert_eq!(plan.graph.root_tree, snapshot_object('d'));
19066        assert_eq!(plan.graph.commit, snapshot_object('e'));
19067
19068        let authorization = GithubGitSnapshotExactRetryAuthorizationV1::new(
19069            "operation-1",
19070            "operation-2",
19071            1_700_000_001_000,
19072            "6".repeat(64),
19073        )
19074        .expect("retry lineage authorization");
19075        let staged = provider
19076            .stage_exact_git_snapshot_retry(&authorization, &plan, &CancellationToken::new())
19077            .expect("materialize exact retry stage");
19078        assert_eq!(staged.request, plan.request);
19079        assert_eq!(staged.stage.graph, plan.graph);
19080        assert_eq!(staged.stage.final_request, plan.request);
19081        assert_eq!(staged.stage.consent_sha256, "6".repeat(64));
19082        let replay = provider
19083            .stage_exact_git_snapshot_retry(&authorization, &plan, &CancellationToken::new())
19084            .expect("adopt exact complete retry stage");
19085        assert_eq!(replay, staged);
19086        let inputs = GitSnapshotImportInputs::load(
19087            fixture.isolation.path(),
19088            &replay.stage_locator,
19089            &replay.request,
19090        )
19091        .expect("reopen replayed child stage");
19092        assert_eq!(inputs.stage(), &replay.stage);
19093        drop(inputs);
19094        GithubGitSnapshotSubmissionV1::exact_retry_lineage(expected, &authorization, replay)
19095            .expect("distinct exact retry submission authority");
19096
19097        let publisher = provider.publisher.lock().expect("publisher");
19098        assert!(publisher.runner.responses.is_empty());
19099        let cat_file = publisher
19100            .runner
19101            .invocations
19102            .iter()
19103            .filter(|invocation| {
19104                invocation
19105                    .arguments
19106                    .first()
19107                    .and_then(|value| value.to_str())
19108                    == Some("cat-file")
19109            })
19110            .collect::<Vec<_>>();
19111        assert_eq!(cat_file.len(), 3);
19112        assert!(
19113            cat_file
19114                .iter()
19115                .all(|invocation| { invocation.stdout_limit == SNAPSHOT_ARCHIVE_BYTES.len() + 1 })
19116        );
19117        for invocation in &publisher.runner.invocations {
19118            assert_eq!(invocation.network, GitNetworkPolicy::Offline);
19119            let arguments = invocation
19120                .arguments
19121                .iter()
19122                .map(|value| value.to_string_lossy())
19123                .collect::<Vec<_>>();
19124            assert!(!arguments.iter().any(|argument| argument == "-w"));
19125            assert!(!arguments.iter().any(|argument| matches!(
19126                argument.as_ref(),
19127                "push" | "fetch" | "update-ref" | "checkout"
19128            )));
19129        }
19130    }
19131
19132    #[test]
19133    fn exact_retry_and_prune_reject_cleanup_failed_parent_without_publisher_calls() {
19134        let fixture = completed_snapshot_resume(
19135            GithubGitSnapshotPhaseV1::SourceDeleted,
19136            JobState::CleanupFailed,
19137        );
19138        let resume = fixture.resume.clone();
19139        let expected = durable_identity_for_resume(&resume);
19140        let plan = synthetic_exact_retry_plan(&resume);
19141        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19142            provider_config(all_authorized()),
19143            transport(FakeGhRunner::with([])),
19144            SnapshotFlowPublisher::exact(),
19145            NoVerifiedArtifactStore,
19146            FixedClock(1_700_000_000_000),
19147        )
19148        .with_checkpoint_sink(CapturingCheckpointSink::default());
19149        provider
19150            .restore_job_resumes_offline(vec![resume], &expected)
19151            .expect("offline cleanup-failed parent restore");
19152        let retry = GithubGitSnapshotExactRetryAuthorizationV1::new(
19153            "operation-1",
19154            "operation-2",
19155            1_700_000_001_000,
19156            "6".repeat(64),
19157        )
19158        .expect("retry authorization");
19159        let error = provider
19160            .stage_exact_git_snapshot_retry(&retry, &plan, &CancellationToken::new())
19161            .expect_err("CleanupFailed parent cannot stage exact retry");
19162        assert!(matches!(
19163            error,
19164            RemoteBuildError::ProviderFailure { code, .. }
19165                if code == "snapshot_exact_retry_parent_not_terminal"
19166        ));
19167        let prune =
19168            GithubGitSnapshotKeepaliveReleaseAuthorizationV1::new("operation-1", "7".repeat(64))
19169                .expect("prune authorization");
19170        let error = provider
19171            .release_git_snapshot_keepalive_for_prune(&prune, &CancellationToken::new())
19172            .expect_err("CleanupFailed parent cannot release keepalive");
19173        assert!(matches!(
19174            error,
19175            RemoteBuildError::ProviderFailure { code, .. }
19176                if code == "snapshot_keepalive_prune_not_terminal"
19177        ));
19178        assert!(
19179            provider
19180                .publisher
19181                .lock()
19182                .expect("publisher")
19183                .events
19184                .is_empty()
19185        );
19186    }
19187
19188    #[test]
19189    fn exact_retry_rejects_releasing_missing_and_conflicting_parent_keepalives() {
19190        let mut releasing =
19191            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceDeleted, JobState::Cleaned);
19192        let releasing_plan = synthetic_exact_retry_plan(&releasing.resume);
19193        {
19194            let snapshot = releasing.resume.git_snapshot.as_mut().expect("snapshot");
19195            snapshot.phase = GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent;
19196            snapshot.keepalive_release_authorization_sha256 = Some("7".repeat(64));
19197        }
19198        let expected = durable_identity_for_resume(&releasing.resume);
19199        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19200            provider_config(all_authorized()),
19201            transport(FakeGhRunner::with([])),
19202            SnapshotFlowPublisher::exact(),
19203            NoVerifiedArtifactStore,
19204            FixedClock(1_700_000_000_000),
19205        )
19206        .with_checkpoint_sink(CapturingCheckpointSink::default());
19207        provider
19208            .restore_job_resumes_offline(vec![releasing.resume.clone()], &expected)
19209            .expect("offline releasing parent restore");
19210        let authorization = GithubGitSnapshotExactRetryAuthorizationV1::new(
19211            "operation-1",
19212            "operation-2",
19213            1_700_000_001_000,
19214            "6".repeat(64),
19215        )
19216        .expect("retry authorization");
19217        let error = provider
19218            .stage_exact_git_snapshot_retry(
19219                &authorization,
19220                &releasing_plan,
19221                &CancellationToken::new(),
19222            )
19223            .expect_err("releasing parent is unavailable");
19224        assert!(matches!(
19225            error,
19226            RemoteBuildError::ProviderFailure { code, .. }
19227                if code == "snapshot_exact_retry_parent_releasing"
19228        ));
19229        assert!(
19230            provider
19231                .publisher
19232                .lock()
19233                .expect("publisher")
19234                .events
19235                .is_empty()
19236        );
19237
19238        for (reconciliation, expected_code) in [
19239            (
19240                GitSnapshotRefReconciliation::Absent,
19241                "snapshot_exact_retry_parent_keepalive_missing",
19242            ),
19243            (
19244                GitSnapshotRefReconciliation::Conflict,
19245                "snapshot_exact_retry_parent_keepalive_conflict",
19246            ),
19247        ] {
19248            let fixture = completed_snapshot_resume(
19249                GithubGitSnapshotPhaseV1::SourceDeleted,
19250                JobState::Cleaned,
19251            );
19252            let resume = fixture.resume.clone();
19253            let expected = durable_identity_for_resume(&resume);
19254            let plan = synthetic_exact_retry_plan(&resume);
19255            let mut publisher = SnapshotFlowPublisher::exact();
19256            publisher
19257                .keepalive_reconciliations
19258                .push_back(reconciliation);
19259            let provider = GithubBuildProvider::with_artifact_store_and_clock(
19260                provider_config(all_authorized()),
19261                transport(FakeGhRunner::with([])),
19262                publisher,
19263                NoVerifiedArtifactStore,
19264                FixedClock(1_700_000_000_000),
19265            )
19266            .with_checkpoint_sink(CapturingCheckpointSink::default());
19267            provider
19268                .restore_job_resumes_offline(vec![resume], &expected)
19269                .expect("offline parent restore");
19270            let error = provider
19271                .stage_exact_git_snapshot_retry(&authorization, &plan, &CancellationToken::new())
19272                .expect_err("parent keepalive proof fails");
19273            assert!(matches!(
19274                error,
19275                RemoteBuildError::ProviderFailure { code, .. } if code == expected_code
19276            ));
19277            let publisher = provider.publisher.lock().expect("publisher");
19278            assert_eq!(
19279                publisher.events,
19280                [
19281                    SnapshotFlowEvent::AcquireSourceLease,
19282                    SnapshotFlowEvent::ReconcileKeepalive,
19283                    SnapshotFlowEvent::ReleaseSourceLease,
19284                ]
19285            );
19286        }
19287    }
19288
19289    #[test]
19290    fn exact_retry_precompute_rejects_parent_archive_byte_and_object_mismatch() {
19291        for (archive_bytes, archive_object) in [
19292            (b"snapshot archive fixturf\n".as_slice(), '1'),
19293            (SNAPSHOT_ARCHIVE_BYTES, '2'),
19294        ] {
19295            let fixture = completed_snapshot_resume(
19296                GithubGitSnapshotPhaseV1::SourceDeleted,
19297                JobState::Cleaned,
19298            );
19299            let resume = fixture.resume.clone();
19300            let expected = durable_identity_for_resume(&resume);
19301            let snapshot = resume.git_snapshot.as_ref().expect("snapshot");
19302            let responses = vec![
19303                Ok(format!(
19304                    "{}\0{}\n",
19305                    snapshot.stage.keepalive_ref.as_str(),
19306                    snapshot.stage.graph.commit.as_str()
19307                )
19308                .into_bytes()),
19309                Ok(b"sha1\n".to_vec()),
19310                Ok(archive_bytes.to_vec()),
19311                Ok(format!("{}\n", archive_object.to_string().repeat(40)).into_bytes()),
19312            ];
19313            let publisher = GitTemporaryRefPublisher::new(
19314                FakeGitRunner::with(responses),
19315                fixture.isolation.path(),
19316                "origin",
19317                "signing",
19318                Duration::from_secs(30),
19319            )
19320            .expect("exact retry publisher");
19321            let provider = GithubBuildProvider::with_artifact_store_and_clock(
19322                provider_config(all_authorized()),
19323                transport(FakeGhRunner::with([])),
19324                publisher,
19325                NoVerifiedArtifactStore,
19326                FixedClock(1_700_000_000_000),
19327            )
19328            .with_checkpoint_sink(CapturingCheckpointSink::default());
19329            provider
19330                .restore_job_resumes_offline(vec![resume], &expected)
19331                .expect("offline parent restore");
19332            let error = provider
19333                .precompute_exact_git_snapshot_retry(
19334                    "operation-1",
19335                    "operation-2",
19336                    1_700_000_001_000,
19337                    &CancellationToken::new(),
19338                )
19339                .expect_err("parent archive proof must remain exact");
19340            assert!(matches!(
19341                error,
19342                RemoteBuildError::ProviderFailure { code, .. } if code == "git_snapshot_invalid"
19343            ));
19344            assert!(
19345                !crate::snapshot::git_snapshot_stage_directory(
19346                    fixture.isolation.path(),
19347                    "operation-2"
19348                )
19349                .expect("child stage path")
19350                .exists()
19351            );
19352        }
19353    }
19354
19355    #[test]
19356    fn snapshot_prepared_checkpoint_precedes_all_git_and_network_activity() {
19357        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
19358        let submission = same_invocation_snapshot_submission(&fixture);
19359        let runner = FakeGhRunner::with([]);
19360        let requests = Arc::clone(&runner.requests);
19361        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19362            provider_config(all_authorized()),
19363            transport(runner),
19364            SnapshotFlowPublisher::exact(),
19365            NoVerifiedArtifactStore,
19366            FixedClock(1_700_000_000_000),
19367        )
19368        .with_checkpoint_sink(FailingCheckpointSink);
19369
19370        let error = provider
19371            .submit_git_snapshot(submission, &CancellationToken::new())
19372            .expect_err("Prepared checkpoint must be durable");
19373        assert!(matches!(
19374            error,
19375            RemoteBuildError::ProviderFailure { code, .. }
19376                if code == "checkpoint_fixture_failed"
19377        ));
19378        assert!(requests.lock().expect("transport requests").is_empty());
19379        assert!(
19380            provider
19381                .publisher
19382                .lock()
19383                .expect("publisher")
19384                .events
19385                .is_empty()
19386        );
19387    }
19388
19389    #[test]
19390    fn snapshot_submission_publishes_exact_source_before_split_repo_dispatch() {
19391        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
19392        let submission = same_invocation_snapshot_submission(&fixture);
19393        let sink = CapturingCheckpointSink::default();
19394        let checkpoints = Arc::clone(&sink.checkpoints);
19395        let runner = FakeGhRunner::with(public_snapshot_identity_rows(5));
19396        let requests = Arc::clone(&runner.requests);
19397        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19398            provider_config(all_authorized()),
19399            transport(runner),
19400            SnapshotFlowPublisher::exact(),
19401            NoVerifiedArtifactStore,
19402            FixedClock(1_700_000_000_000),
19403        )
19404        .with_checkpoint_sink(sink);
19405
19406        let handle = provider
19407            .submit_git_snapshot(submission, &CancellationToken::new())
19408            .expect("snapshot submission");
19409        assert_eq!(handle.state, JobState::Queued);
19410        assert_eq!(requests.lock().expect("transport requests").len(), 15);
19411        let checkpoints = checkpoints.lock().expect("snapshot checkpoints");
19412        let phases = checkpoints
19413            .iter()
19414            .map(|resume| resume.git_snapshot.as_ref().expect("snapshot").phase)
19415            .collect::<Vec<_>>();
19416        assert_eq!(
19417            &phases[..8],
19418            &[
19419                GithubGitSnapshotPhaseV1::Prepared,
19420                GithubGitSnapshotPhaseV1::KeepaliveIntent,
19421                GithubGitSnapshotPhaseV1::KeepaliveExact,
19422                GithubGitSnapshotPhaseV1::StageDeleteIntent,
19423                GithubGitSnapshotPhaseV1::StageDeleted,
19424                GithubGitSnapshotPhaseV1::SourcePublishIntent,
19425                GithubGitSnapshotPhaseV1::SourcePublishIntent,
19426                GithubGitSnapshotPhaseV1::SourceExact,
19427            ]
19428        );
19429        let first_dispatch_intent = checkpoints
19430            .iter()
19431            .position(|resume| resume.publication_intent)
19432            .expect("dispatch intent checkpoint");
19433        assert!(checkpoints[..first_dispatch_intent].iter().any(|resume| {
19434            resume.git_snapshot.as_ref().map(|snapshot| snapshot.phase)
19435                == Some(GithubGitSnapshotPhaseV1::SourceExact)
19436        }));
19437        drop(checkpoints);
19438
19439        let publisher = provider.publisher.lock().expect("publisher");
19440        let publish_source = publisher
19441            .events
19442            .iter()
19443            .position(|event| event == &SnapshotFlowEvent::PublishSource)
19444            .expect("source publication");
19445        let prepare_dispatch = publisher
19446            .events
19447            .iter()
19448            .position(|event| matches!(event, SnapshotFlowEvent::PrepareDispatch { .. }))
19449            .expect("dispatch preparation");
19450        let publish_dispatch = publisher
19451            .events
19452            .iter()
19453            .position(|event| event == &SnapshotFlowEvent::PublishDispatch)
19454            .expect("dispatch publication");
19455        assert!(publish_source < prepare_dispatch && prepare_dispatch < publish_dispatch);
19456        assert!(publisher.events.iter().any(|event| {
19457            matches!(
19458                event,
19459                SnapshotFlowEvent::PrepareDispatch {
19460                    execution_repository,
19461                    source_repository,
19462                    source_ref,
19463                } if execution_repository == "https://github.com/ShiroKSH/rustferry-signing"
19464                    && source_repository == "https://github.com/shiroksh/rust-and-iphone"
19465                    && source_ref == "refs/rustferry/goal3/snapshots/operation-1"
19466            )
19467        }));
19468        assert!(!publisher.source_lease_owned);
19469        assert!(!publisher.dispatch_lease_owned);
19470    }
19471
19472    #[test]
19473    fn snapshot_source_conflict_fails_without_dispatch_publication() {
19474        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
19475        let submission = same_invocation_snapshot_submission(&fixture);
19476        let mut publisher = SnapshotFlowPublisher::exact();
19477        publisher.source_publication = GitSnapshotSourcePublication::Conflict;
19478        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19479            provider_config(all_authorized()),
19480            transport(FakeGhRunner::with(public_snapshot_identity_rows(2))),
19481            publisher,
19482            NoVerifiedArtifactStore,
19483            FixedClock(1_700_000_000_000),
19484        )
19485        .with_checkpoint_sink(CapturingCheckpointSink::default());
19486
19487        let handle = provider
19488            .submit_git_snapshot(submission, &CancellationToken::new())
19489            .expect("conflict is durably terminal");
19490        assert_eq!(handle.state, JobState::Failed);
19491        let publisher = provider.publisher.lock().expect("publisher");
19492        assert!(publisher.events.contains(&SnapshotFlowEvent::PublishSource));
19493        assert!(!publisher.events.iter().any(|event| matches!(
19494            event,
19495            SnapshotFlowEvent::PrepareDispatch { .. } | SnapshotFlowEvent::PublishDispatch
19496        )));
19497        assert!(!publisher.source_lease_owned);
19498    }
19499
19500    #[test]
19501    fn snapshot_source_ref_drift_after_exact_checkpoint_blocks_dispatch() {
19502        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
19503        let submission = same_invocation_snapshot_submission(&fixture);
19504        let mut publisher = SnapshotFlowPublisher::exact();
19505        publisher
19506            .source_reconciliations
19507            .push_back(GitSnapshotRefReconciliation::Conflict);
19508        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19509            provider_config(all_authorized()),
19510            transport(FakeGhRunner::with(public_snapshot_identity_rows(3))),
19511            publisher,
19512            NoVerifiedArtifactStore,
19513            FixedClock(1_700_000_000_000),
19514        )
19515        .with_checkpoint_sink(CapturingCheckpointSink::default());
19516
19517        let handle = provider
19518            .submit_git_snapshot(submission, &CancellationToken::new())
19519            .expect("source drift is durably terminal");
19520        assert_eq!(handle.state, JobState::Failed);
19521        let publisher = provider.publisher.lock().expect("publisher");
19522        assert!(
19523            publisher
19524                .events
19525                .contains(&SnapshotFlowEvent::ReconcileSource)
19526        );
19527        assert!(!publisher.events.iter().any(|event| matches!(
19528            event,
19529            SnapshotFlowEvent::PrepareDispatch { .. } | SnapshotFlowEvent::PublishDispatch
19530        )));
19531    }
19532
19533    #[test]
19534    fn snapshot_source_exact_checkpoint_crash_replays_before_dispatch() {
19535        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
19536        let submission = same_invocation_snapshot_submission(&fixture);
19537        let checkpoints = Arc::new(Mutex::new(Vec::new()));
19538        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19539            provider_config(all_authorized()),
19540            transport(FakeGhRunner::with(public_snapshot_identity_rows(5))),
19541            SnapshotFlowPublisher::exact(),
19542            NoVerifiedArtifactStore,
19543            FixedClock(1_700_000_000_000),
19544        )
19545        .with_checkpoint_sink(FailOnSnapshotPhaseSink {
19546            checkpoints: Arc::clone(&checkpoints),
19547            phase: GithubGitSnapshotPhaseV1::SourceExact,
19548            failed_once: true,
19549        });
19550
19551        let error = provider
19552            .submit_git_snapshot(submission, &CancellationToken::new())
19553            .expect_err("SourceExact checkpoint acknowledgement fails");
19554        assert!(matches!(
19555            error,
19556            RemoteBuildError::ProviderFailure { code, .. }
19557                if code == "checkpoint_fixture_failed"
19558        ));
19559        {
19560            let publisher = provider.publisher.lock().expect("publisher");
19561            assert!(publisher.source_lease_owned);
19562            assert!(!publisher.events.iter().any(|event| matches!(
19563                event,
19564                SnapshotFlowEvent::PrepareDispatch { .. } | SnapshotFlowEvent::PublishDispatch
19565            )));
19566        }
19567
19568        provider
19569            .continue_git_snapshot_submission("operation-1", &CancellationToken::new())
19570            .expect("replay exact source then dispatch");
19571        let state = provider.lock_state().expect("state");
19572        assert_eq!(state.jobs["operation-1"].state, JobState::Queued);
19573        drop(state);
19574        let publisher = provider.publisher.lock().expect("publisher");
19575        assert!(!publisher.source_lease_owned);
19576        assert!(
19577            publisher
19578                .events
19579                .contains(&SnapshotFlowEvent::PublishDispatch)
19580        );
19581        drop(publisher);
19582        for pair in checkpoints.lock().expect("snapshot checkpoints").windows(2) {
19583            pair[0]
19584                .validate_successor(&pair[1])
19585                .expect("replayed snapshot checkpoint successor");
19586        }
19587    }
19588
19589    #[test]
19590    #[allow(
19591        clippy::too_many_lines,
19592        reason = "one table proves every submission mutation phase is re-acknowledged after an ambiguous checkpoint"
19593    )]
19594    fn snapshot_submission_phase_entry_failure_blocks_mutation_until_same_phase_reack() {
19595        for (phase, armed_source_intent, expected_event) in [
19596            (
19597                GithubGitSnapshotPhaseV1::KeepaliveIntent,
19598                false,
19599                SnapshotFlowEvent::ReconcileKeepalive,
19600            ),
19601            (
19602                GithubGitSnapshotPhaseV1::KeepaliveExact,
19603                false,
19604                SnapshotFlowEvent::ReconcileKeepalive,
19605            ),
19606            (
19607                GithubGitSnapshotPhaseV1::StageDeleteIntent,
19608                false,
19609                SnapshotFlowEvent::DeleteStage,
19610            ),
19611            (
19612                GithubGitSnapshotPhaseV1::StageDeleted,
19613                false,
19614                SnapshotFlowEvent::AcquireSourceLease,
19615            ),
19616            (
19617                GithubGitSnapshotPhaseV1::SourcePublishIntent,
19618                false,
19619                SnapshotFlowEvent::PublishSource,
19620            ),
19621            (
19622                GithubGitSnapshotPhaseV1::SourcePublishIntent,
19623                true,
19624                SnapshotFlowEvent::ReconcileSource,
19625            ),
19626            (
19627                GithubGitSnapshotPhaseV1::SourceExact,
19628                false,
19629                SnapshotFlowEvent::ReconcileSource,
19630            ),
19631            (
19632                GithubGitSnapshotPhaseV1::SourceAbsent,
19633                false,
19634                SnapshotFlowEvent::AcquireSourceLease,
19635            ),
19636            (
19637                GithubGitSnapshotPhaseV1::SourceConflict,
19638                false,
19639                SnapshotFlowEvent::ReleaseSourceLease,
19640            ),
19641        ] {
19642            let mut fixture = snapshot_resume_fixture(phase);
19643            if armed_source_intent {
19644                let snapshot = fixture
19645                    .resume
19646                    .git_snapshot
19647                    .as_mut()
19648                    .expect("snapshot resume");
19649                snapshot.source_publication_started_at_ms = fixture.resume.created_at_ms;
19650                snapshot.source_publication_quiescence_deadline_ms =
19651                    publication_quiescence_deadline(fixture.resume.created_at_ms, 0);
19652                snapshot
19653                    .validate_for_request(&fixture.resume.request)
19654                    .expect("armed source intent");
19655            }
19656            let resume = fixture.resume.clone();
19657            let checkpoints = Arc::new(Mutex::new(Vec::new()));
19658            let mut publisher = SnapshotFlowPublisher::exact();
19659            publisher.keepalive_exact = phase != GithubGitSnapshotPhaseV1::KeepaliveIntent;
19660            let provider = GithubBuildProvider::with_artifact_store_and_clock(
19661                provider_config(all_authorized()),
19662                transport(FakeGhRunner::with(public_snapshot_identity_rows(6))),
19663                publisher,
19664                NoVerifiedArtifactStore,
19665                FixedClock(1_700_000_000_000),
19666            )
19667            .with_checkpoint_sink(FailOnSnapshotPhaseSink {
19668                checkpoints: Arc::clone(&checkpoints),
19669                phase,
19670                failed_once: true,
19671            });
19672            provider
19673                .restore_job_resumes_offline(
19674                    vec![resume.clone()],
19675                    &durable_identity_for_resume(&resume),
19676                )
19677                .expect("restore snapshot phase");
19678            if phase == GithubGitSnapshotPhaseV1::SourceConflict {
19679                provider
19680                    .lock_state()
19681                    .expect("provider state")
19682                    .jobs
19683                    .get_mut(&resume.operation_id)
19684                    .expect("snapshot job")
19685                    .snapshot_source_lease_owned = true;
19686                provider
19687                    .publisher
19688                    .lock()
19689                    .expect("publisher")
19690                    .source_lease_owned = true;
19691            }
19692
19693            let error = provider
19694                .continue_git_snapshot_submission(&resume.operation_id, &CancellationToken::new())
19695                .expect_err("current phase acknowledgement fails once");
19696            assert!(matches!(
19697                error,
19698                RemoteBuildError::ProviderFailure { code, .. }
19699                    if code == "checkpoint_fixture_failed"
19700            ));
19701            assert!(
19702                provider
19703                    .publisher
19704                    .lock()
19705                    .expect("publisher")
19706                    .events
19707                    .is_empty(),
19708                "{phase:?} mutated before its checkpoint"
19709            );
19710
19711            provider
19712                .continue_git_snapshot_submission(&resume.operation_id, &CancellationToken::new())
19713                .expect("same phase re-acknowledges before progressing");
19714            assert!(
19715                provider
19716                    .publisher
19717                    .lock()
19718                    .expect("publisher")
19719                    .events
19720                    .contains(&expected_event),
19721                "{phase:?} did not reach its expected mutation after re-acknowledgement"
19722            );
19723            assert!(
19724                checkpoints
19725                    .lock()
19726                    .expect("snapshot checkpoints")
19727                    .iter()
19728                    .filter(|resume| {
19729                        resume.git_snapshot.as_ref().map(|snapshot| snapshot.phase) == Some(phase)
19730                    })
19731                    .count()
19732                    >= 2,
19733                "{phase:?} was not re-acknowledged"
19734            );
19735        }
19736    }
19737
19738    #[test]
19739    fn snapshot_cleanup_intent_failure_reacks_before_exact_source_deletion() {
19740        let config = provider_config(all_authorized());
19741        let principal = GithubPrincipalIdentityV1::User {
19742            id: AUTHENTICATED_USER_ID,
19743            login: "ShiroKSH".to_owned(),
19744        };
19745        let fixture =
19746            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceExact, JobState::Succeeded);
19747        let mut record = fixture
19748            .resume
19749            .clone()
19750            .into_record(&config, &principal, 991)
19751            .expect("snapshot record");
19752        let events = Arc::new(Mutex::new(Vec::new()));
19753        let checkpoints = Arc::new(Mutex::new(Vec::new()));
19754        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19755            config,
19756            transport(FakeGhRunner::with([
19757                Ok(authenticated_user_row()),
19758                Ok(private_repository_row()),
19759            ])),
19760            SnapshotCleanupAuditPublisher {
19761                events: Arc::clone(&events),
19762                source_lease_owned: false,
19763                deletion_count: 0,
19764                keepalive_release_count: 0,
19765            },
19766            NoVerifiedArtifactStore,
19767            FixedClock(1_700_000_000_000),
19768        )
19769        .with_checkpoint_sink(FailOnSnapshotPhaseSink {
19770            checkpoints: Arc::clone(&checkpoints),
19771            phase: GithubGitSnapshotPhaseV1::SourceCleanupIntent,
19772            failed_once: true,
19773        });
19774
19775        let error = provider
19776            .cleanup_git_snapshot_source(&mut record)
19777            .expect_err("cleanup intent checkpoint fails once");
19778        assert!(matches!(
19779            error,
19780            RemoteBuildError::ProviderFailure { code, .. } if code == "checkpoint_fixture_failed"
19781        ));
19782        assert!(events.lock().expect("cleanup events").is_empty());
19783        provider
19784            .cleanup_git_snapshot_source(&mut record)
19785            .expect("cleanup intent re-acknowledges before delete");
19786        assert_eq!(
19787            checkpoints
19788                .lock()
19789                .expect("snapshot checkpoints")
19790                .iter()
19791                .map(|resume| resume.git_snapshot.as_ref().expect("snapshot").phase)
19792                .collect::<Vec<_>>(),
19793            [
19794                GithubGitSnapshotPhaseV1::SourceCleanupIntent,
19795                GithubGitSnapshotPhaseV1::SourceCleanupIntent,
19796                GithubGitSnapshotPhaseV1::SourceDeleted,
19797            ]
19798        );
19799        assert!(
19800            events
19801                .lock()
19802                .expect("cleanup events")
19803                .contains(&SnapshotCleanupAuditEvent::Delete)
19804        );
19805    }
19806
19807    #[test]
19808    fn snapshot_keepalive_release_intent_failure_reacks_before_local_ref_deletion() {
19809        let fixture =
19810            completed_snapshot_resume(GithubGitSnapshotPhaseV1::SourceDeleted, JobState::Cleaned);
19811        let resume = fixture.resume.clone();
19812        let checkpoints = Arc::new(Mutex::new(Vec::new()));
19813        let mut publisher = SnapshotFlowPublisher::exact();
19814        publisher.keepalive_exact = true;
19815        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19816            provider_config(all_authorized()),
19817            transport(FakeGhRunner::with([])),
19818            publisher,
19819            NoVerifiedArtifactStore,
19820            FixedClock(1_700_000_000_000),
19821        )
19822        .with_checkpoint_sink(FailOnSnapshotPhaseSink {
19823            checkpoints: Arc::clone(&checkpoints),
19824            phase: GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent,
19825            failed_once: true,
19826        });
19827        provider
19828            .restore_job_resumes_offline(
19829                vec![resume.clone()],
19830                &durable_identity_for_resume(&resume),
19831            )
19832            .expect("restore snapshot parent");
19833        let authorization =
19834            GithubGitSnapshotKeepaliveReleaseAuthorizationV1::new("operation-1", "7".repeat(64))
19835                .expect("prune authorization");
19836
19837        let error = provider
19838            .release_git_snapshot_keepalive_for_prune(&authorization, &CancellationToken::new())
19839            .expect_err("release intent checkpoint fails once");
19840        assert!(matches!(
19841            error,
19842            RemoteBuildError::ProviderFailure { code, .. } if code == "checkpoint_fixture_failed"
19843        ));
19844        assert!(
19845            provider
19846                .publisher
19847                .lock()
19848                .expect("publisher")
19849                .events
19850                .is_empty()
19851        );
19852        provider
19853            .release_git_snapshot_keepalive_for_prune(&authorization, &CancellationToken::new())
19854            .expect("release intent re-acknowledges before delete");
19855        assert_eq!(
19856            checkpoints
19857                .lock()
19858                .expect("snapshot checkpoints")
19859                .iter()
19860                .map(|resume| resume.git_snapshot.as_ref().expect("snapshot").phase)
19861                .collect::<Vec<_>>(),
19862            [
19863                GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent,
19864                GithubGitSnapshotPhaseV1::KeepaliveReleaseIntent,
19865                GithubGitSnapshotPhaseV1::KeepaliveReleased,
19866            ]
19867        );
19868        assert!(
19869            provider
19870                .publisher
19871                .lock()
19872                .expect("publisher")
19873                .events
19874                .contains(&SnapshotFlowEvent::ReleaseKeepalive)
19875        );
19876    }
19877
19878    #[test]
19879    fn snapshot_consent_mismatch_stops_before_checkpoint_or_publisher() {
19880        let fixture = snapshot_resume_fixture(GithubGitSnapshotPhaseV1::Prepared);
19881        let resume = &fixture.resume;
19882        let snapshot = resume.git_snapshot.as_ref().expect("snapshot resume");
19883        let submission = GithubGitSnapshotSubmissionV1::same_invocation(
19884            GithubDurableIdentityV1 {
19885                provider: resume.provider.clone(),
19886                provider_config_sha256: resume.provider_config_sha256.clone(),
19887                principal: resume.principal.clone(),
19888                execution_repository: resume.execution_repository.clone(),
19889                execution_repository_id: resume.execution_repository_id,
19890            },
19891            resume.request.clone(),
19892            "8".repeat(64),
19893            snapshot.stage_locator.clone(),
19894            snapshot.stage.clone(),
19895        )
19896        .expect("mismatched consent submission envelope");
19897        let runner = FakeGhRunner::with([]);
19898        let requests = Arc::clone(&runner.requests);
19899        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19900            provider_config(all_authorized()),
19901            transport(runner),
19902            SnapshotFlowPublisher::exact(),
19903            NoVerifiedArtifactStore,
19904            FixedClock(1_700_000_000_000),
19905        )
19906        .with_checkpoint_sink(CapturingCheckpointSink::default());
19907
19908        let error = provider
19909            .submit_git_snapshot(submission, &CancellationToken::new())
19910            .expect_err("consent mismatch");
19911        assert!(matches!(
19912            error,
19913            RemoteBuildError::ProviderFailure { code, .. }
19914                if code == "snapshot_consent_changed"
19915        ));
19916        assert!(requests.lock().expect("transport requests").is_empty());
19917        assert!(
19918            provider
19919                .publisher
19920                .lock()
19921                .expect("publisher")
19922                .events
19923                .is_empty()
19924        );
19925    }
19926
19927    #[test]
19928    fn failed_pre_push_checkpoint_blocks_remote_mutation_and_retains_intent() {
19929        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19930            provider_config(all_authorized()),
19931            transport(FakeGhRunner::with([
19932                Ok(authenticated_user_row()),
19933                Ok(private_repository_row()),
19934            ])),
19935            FakePublisher::default(),
19936            NoVerifiedArtifactStore,
19937            FixedClock(1_700_000_000_000),
19938        )
19939        .with_checkpoint_sink(FailingCheckpointSink);
19940        assert!(poll_ready(provider.submit(unsigned_request(), CancellationToken::new())).is_err());
19941        assert!(
19942            provider
19943                .publisher
19944                .lock()
19945                .expect("publisher")
19946                .published
19947                .is_empty()
19948        );
19949        assert_eq!(
19950            provider
19951                .publisher
19952                .lock()
19953                .expect("publisher")
19954                .publication_leases
19955                .len(),
19956            1
19957        );
19958        let state = provider.lock_state().expect("state");
19959        let record = state.jobs.get("operation-1").expect("retained job");
19960        assert!(record.publication_intent);
19961        assert_eq!(record.state, JobState::Created);
19962        assert!(state.reservations.is_empty());
19963    }
19964
19965    #[test]
19966    fn checkpoint_write_with_uncertain_ack_keeps_live_successor_chain() {
19967        let sink = WriteThenFailCheckpointSink::default();
19968        let checkpoints = Arc::clone(&sink.checkpoints);
19969        let provider = GithubBuildProvider::with_artifact_store_and_clock(
19970            provider_config(all_authorized()),
19971            transport(FakeGhRunner::with([
19972                Ok(authenticated_user_row()),
19973                Ok(private_repository_row()),
19974                Ok(authenticated_user_row()),
19975                Ok(private_repository_row()),
19976            ])),
19977            FakePublisher::default(),
19978            NoVerifiedArtifactStore,
19979            FixedClock(1_700_000_000_000),
19980        )
19981        .with_checkpoint_sink(sink);
19982
19983        assert!(poll_ready(provider.submit(unsigned_request(), CancellationToken::new())).is_err());
19984        assert!(
19985            provider
19986                .publisher
19987                .lock()
19988                .expect("publisher")
19989                .published
19990                .is_empty()
19991        );
19992        assert_eq!(
19993            provider
19994                .publisher
19995                .lock()
19996                .expect("publisher")
19997                .publication_leases
19998                .len(),
19999            1
20000        );
20001        let written = checkpoints
20002            .lock()
20003            .expect("checkpoints")
20004            .first()
20005            .cloned()
20006            .expect("possibly durable checkpoint");
20007        let exported = provider
20008            .export_job_resumes()
20009            .expect("retained in-memory intent can be exported");
20010        assert_eq!(exported.len(), 1);
20011        written
20012            .validate_successor(&exported[0])
20013            .expect("retained record preserves the durable successor chain");
20014    }
20015
20016    #[test]
20017    fn live_identity_failure_records_proven_no_publication_before_returning() {
20018        let sink = CapturingCheckpointSink::default();
20019        let checkpoints = Arc::clone(&sink.checkpoints);
20020        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20021            provider_config(all_authorized()),
20022            transport(FakeGhRunner::with([
20023                Ok(authenticated_user_row()),
20024                Ok(private_repository_row()),
20025                Ok(b"43\tYXR0YWNrZXI=\n".to_vec()),
20026                Ok(private_repository_row()),
20027            ])),
20028            FakePublisher::default(),
20029            NoVerifiedArtifactStore,
20030            FixedClock(1_700_000_000_000),
20031        )
20032        .with_checkpoint_sink(sink);
20033        assert!(poll_ready(provider.submit(unsigned_request(), CancellationToken::new())).is_err());
20034        let checkpoints = checkpoints.lock().expect("checkpoints");
20035        assert_eq!(checkpoints.len(), 2);
20036        checkpoints[0]
20037            .validate_successor(&checkpoints[1])
20038            .expect("terminal no-publication evidence is monotonic");
20039        assert_eq!(checkpoints[1].state, JobState::Failed);
20040        assert!(checkpoints[1].publication_not_attempted);
20041        assert!(!checkpoints[1].publication_absent);
20042        assert!(!checkpoints[1].temporary_ref_deleted);
20043        assert!(!checkpoints[1].publication_uncertain);
20044        drop(checkpoints);
20045        let state = provider.lock_state().expect("state");
20046        let record = state.jobs.get("operation-1").expect("durably owned job");
20047        assert!(record.publication_intent);
20048        assert_eq!(record.dispatch_commit, None);
20049        assert_eq!(record.state, JobState::Failed);
20050        assert!(record.publication_not_attempted);
20051        assert!(!record.publication_absent);
20052        assert!(
20053            provider
20054                .publisher
20055                .lock()
20056                .expect("publisher")
20057                .published
20058                .is_empty()
20059        );
20060    }
20061
20062    #[test]
20063    fn existing_ref_pre_mutation_failure_does_not_claim_ref_absence_or_deletion() {
20064        let sink = CapturingCheckpointSink::default();
20065        let checkpoints = Arc::clone(&sink.checkpoints);
20066        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20067            provider_config(all_authorized()),
20068            transport(FakeGhRunner::with([
20069                Ok(authenticated_user_row()),
20070                Ok(private_repository_row()),
20071                Ok(authenticated_user_row()),
20072                Ok(private_repository_row()),
20073            ])),
20074            FakePublisher {
20075                prepared_publication: Some(expected_publication()),
20076                failure: Some(TemporaryRefPublishError::TemporaryRefExists.into()),
20077                ..FakePublisher::default()
20078            },
20079            NoVerifiedArtifactStore,
20080            FixedClock(1_700_000_000_000),
20081        )
20082        .with_checkpoint_sink(sink);
20083
20084        assert!(poll_ready(provider.submit(unsigned_request(), CancellationToken::new())).is_err());
20085        let checkpoints = checkpoints.lock().expect("checkpoints");
20086        assert_eq!(checkpoints.len(), 3);
20087        checkpoints[0]
20088            .validate_successor(&checkpoints[1])
20089            .expect("publication boundary checkpoint");
20090        checkpoints[1]
20091            .validate_successor(&checkpoints[2])
20092            .expect("definite failure checkpoint");
20093        assert_eq!(checkpoints[2].state, JobState::Failed);
20094        assert_eq!(
20095            checkpoints[2].prepared_dispatch_commit.as_deref(),
20096            Some(DISPATCH_SHA)
20097        );
20098        assert!(checkpoints[2].publication_not_attempted);
20099        assert!(!checkpoints[2].publication_absent);
20100        assert!(!checkpoints[2].temporary_ref_deleted);
20101        assert!(!checkpoints[2].publication_uncertain);
20102        drop(checkpoints);
20103        assert_eq!(
20104            provider
20105                .reconcile_restored_job("operation-1", &CancellationToken::new())
20106                .expect("definite pre-mutation result"),
20107            GithubJobReconciliation::NotStarted
20108        );
20109    }
20110
20111    #[test]
20112    #[allow(clippy::too_many_lines)]
20113    fn resume_validation_rejects_every_security_identity_dimension() {
20114        let config = provider_config(all_authorized());
20115        let principal = GithubPrincipalIdentityV1::User {
20116            id: AUTHENTICATED_USER_ID,
20117            login: "ShiroKSH".to_owned(),
20118        };
20119        let base = durable_resume_with_run();
20120        base.clone()
20121            .into_record(&config, &principal, 991)
20122            .expect("valid resume fixture");
20123        let assert_rejected = |candidate: GithubJobResumeV1| {
20124            assert!(candidate.into_record(&config, &principal, 991).is_err());
20125        };
20126
20127        let mut candidate = base.clone();
20128        candidate.execution_repository_id = 992;
20129        assert_rejected(candidate);
20130        let mut candidate = base.clone();
20131        candidate.principal = GithubPrincipalIdentityV1::User {
20132            id: AUTHENTICATED_USER_ID + 1,
20133            login: "attacker".to_owned(),
20134        };
20135        assert_rejected(candidate);
20136        let mut candidate = base.clone();
20137        candidate.provider_config_sha256 = "f".repeat(64);
20138        assert_rejected(candidate);
20139        let mut candidate = base.clone();
20140        candidate.workflow_sha256 = "f".repeat(64);
20141        assert_rejected(candidate);
20142        let mut candidate = base.clone();
20143        candidate.request.product_name = "Different".to_owned();
20144        assert_rejected(candidate);
20145        let mut candidate = base.clone();
20146        candidate.temporary_ref = "refs/heads/rustferry/goal3/builds/operation-2".to_owned();
20147        assert_rejected(candidate);
20148        let mut candidate = base.clone();
20149        candidate.publication_absent = true;
20150        candidate.temporary_ref_deleted = true;
20151        assert_rejected(candidate);
20152        let mut candidate = base.clone();
20153        candidate.publication_lease_scope_sha256 = None;
20154        assert_rejected(candidate);
20155        let mut candidate = base.clone();
20156        candidate.publication_lease_scope_sha256 = Some("A".repeat(64));
20157        assert_rejected(candidate);
20158        let mut candidate = durable_prepublication_resume();
20159        candidate.state = JobState::Failed;
20160        candidate.publication_absent = true;
20161        candidate.publication_uncertain = false;
20162        candidate.publication_absence_observations = 2;
20163        candidate.temporary_ref_deleted = true;
20164        assert_rejected(candidate);
20165        let mut candidate = durable_prepared_prepublication_resume();
20166        candidate.state = JobState::Failed;
20167        candidate.publication_absent = true;
20168        candidate.publication_uncertain = false;
20169        candidate.publication_absence_observations = 2;
20170        candidate.publication_absence_first_observed_at_ms = candidate.publication_started_at_ms;
20171        candidate.temporary_ref_deleted = true;
20172        candidate.publication_process_fenced = false;
20173        assert_rejected(candidate);
20174        let mut candidate = base.clone();
20175        candidate.publication_intent = false;
20176        assert_rejected(candidate);
20177        let mut candidate = base.clone();
20178        candidate.run_discovery_deadline_ms = u64::MAX;
20179        assert_rejected(candidate);
20180        let mut candidate = base.clone();
20181        candidate.request.source = large_source_manifest();
20182        candidate.request_sha256 =
20183            canonical_request_sha256(&candidate.request).expect("large request digest");
20184        assert!(
20185            candidate.encode().expect("bounded resume record").len() < GITHUB_JOB_RESUME_MAX_BYTES
20186        );
20187        assert_rejected(candidate);
20188        let mut candidate = base.clone();
20189        candidate.state = JobState::Cancelling;
20190        assert_rejected(candidate);
20191        let mut candidate = base.clone();
20192        candidate.state = JobState::Cancelling;
20193        candidate.cancellation_requested = true;
20194        candidate.cancellation_dispatched = true;
20195        candidate.run = None;
20196        assert_rejected(candidate);
20197        let mut candidate = durable_success_resume();
20198        candidate.run.as_mut().expect("run").status = GithubRunStatusV1::Queued;
20199        candidate.run.as_mut().expect("run").conclusion = None;
20200        assert_rejected(candidate);
20201        let mut candidate = base.clone();
20202        candidate.state = JobState::Cancelled;
20203        candidate.cancellation_requested = true;
20204        assert_rejected(candidate);
20205        let mut candidate = durable_success_resume();
20206        candidate.state = JobState::Cleaned;
20207        candidate.cleanup_requested = true;
20208        assert_rejected(candidate);
20209        let mut candidate = base.clone();
20210        candidate.run.as_mut().expect("run").head_sha = "0".repeat(40);
20211        assert_rejected(candidate);
20212        let mut candidate = base.clone();
20213        candidate.run.as_mut().expect("run").branch =
20214            "rustferry/goal3/builds/operation-2".to_owned();
20215        assert_rejected(candidate);
20216        let mut candidate = base.clone();
20217        candidate.run.as_mut().expect("run").event = GithubRunEventV1::WorkflowDispatch;
20218        assert_rejected(candidate);
20219        let mut candidate = base.clone();
20220        candidate.events.push(
20221            RemoteBuildEvent::new(
20222                "different-operation",
20223                "operation-1",
20224                1_700_000_000_000,
20225                GITHUB_PROVIDER_ID,
20226                "queue",
20227                1,
20228                RemoteBuildEventKind::JobQueued { position: None },
20229            )
20230            .expect("valid but foreign event"),
20231        );
20232        assert_rejected(candidate);
20233        let mut candidate = base;
20234        candidate.run.as_mut().expect("run").run_attempt = 0;
20235        assert_rejected(candidate);
20236    }
20237
20238    #[test]
20239    fn restored_cancel_revalidates_exact_run_and_attempt_before_post() {
20240        let runner = FakeGhRunner::with([
20241            Ok(authenticated_user_row()),
20242            Ok(private_repository_row()),
20243            Ok(queued_run_row()),
20244            Ok(authenticated_user_row()),
20245            Ok(private_repository_row()),
20246            Ok(queued_run_row()),
20247            Ok(queued_run_row()),
20248            Ok(authenticated_user_row()),
20249            Ok(private_repository_row()),
20250            Ok(Vec::new()),
20251        ]);
20252        let requests = Arc::clone(&runner.requests);
20253        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20254            provider_config(all_authorized()),
20255            transport(runner),
20256            FakePublisher::default(),
20257            NoVerifiedArtifactStore,
20258            FixedClock(1_700_000_000_000),
20259        );
20260        provider
20261            .restore_job_resumes(vec![durable_resume_with_run()])
20262            .expect("live run restore");
20263        let acknowledgement = poll_ready(provider.cancel(
20264            CancellationRequest {
20265                job_id: "operation-1".to_owned(),
20266                reason: "user-requested".to_owned(),
20267            },
20268            CancellationToken::new(),
20269        ))
20270        .expect("checked cancellation");
20271        assert!(acknowledgement.accepted);
20272
20273        let requests = requests.lock().expect("captured requests");
20274        let final_get = &requests[requests.len() - 4];
20275        assert_eq!(final_get.method(), ApiMethod::Get);
20276        assert_eq!(
20277            final_get.endpoint(),
20278            "/repos/ShiroKSH/rustferry-signing/actions/runs/41"
20279        );
20280        assert_eq!(requests[requests.len() - 3].endpoint(), "/user");
20281        assert_eq!(
20282            requests[requests.len() - 2].endpoint(),
20283            "/repos/ShiroKSH/rustferry-signing"
20284        );
20285        let cancel = &requests[requests.len() - 1];
20286        assert_eq!(cancel.method(), ApiMethod::Post);
20287        assert_eq!(
20288            cancel.endpoint(),
20289            "/repos/ShiroKSH/rustferry-signing/actions/runs/41/cancel"
20290        );
20291    }
20292
20293    #[test]
20294    fn offline_restore_performs_no_transport_or_publisher_operation() {
20295        let runner = FakeGhRunner::with([]);
20296        let requests = Arc::clone(&runner.requests);
20297        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20298            provider_config(all_authorized()),
20299            transport(runner),
20300            FakePublisher::default(),
20301            NoVerifiedArtifactStore,
20302            FixedClock(1_700_000_000_000),
20303        );
20304        let resume = durable_resume_with_run();
20305        let expected = GithubDurableIdentityV1 {
20306            provider: resume.provider.clone(),
20307            provider_config_sha256: resume.provider_config_sha256.clone(),
20308            principal: resume.principal.clone(),
20309            execution_repository: resume.execution_repository.clone(),
20310            execution_repository_id: resume.execution_repository_id,
20311        };
20312
20313        provider
20314            .restore_job_resumes_offline(vec![resume], &expected)
20315            .expect("offline restore");
20316
20317        assert!(requests.lock().expect("requests").is_empty());
20318        assert!(
20319            provider
20320                .publisher
20321                .lock()
20322                .expect("publisher")
20323                .published
20324                .is_empty()
20325        );
20326        assert!(
20327            provider
20328                .lock_state()
20329                .expect("state")
20330                .jobs
20331                .contains_key("operation-1")
20332        );
20333        assert_eq!(
20334            provider
20335                .restored_terminal_job_log_identity("operation-1")
20336                .expect("offline log identity"),
20337            None
20338        );
20339    }
20340
20341    #[test]
20342    fn offline_terminal_restore_exposes_only_a_durable_log_marker_identity() {
20343        let runner = FakeGhRunner::with([]);
20344        let requests = Arc::clone(&runner.requests);
20345        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20346            provider_config(all_authorized()),
20347            transport(runner),
20348            FakePublisher::default(),
20349            NoVerifiedArtifactStore,
20350            FixedClock(1_700_000_000_000),
20351        );
20352        let resume = durable_success_resume();
20353        let expected = GithubDurableIdentityV1 {
20354            provider: resume.provider.clone(),
20355            provider_config_sha256: resume.provider_config_sha256.clone(),
20356            principal: resume.principal.clone(),
20357            execution_repository: resume.execution_repository.clone(),
20358            execution_repository_id: resume.execution_repository_id,
20359        };
20360        provider
20361            .restore_job_resumes_offline(vec![resume], &expected)
20362            .expect("offline terminal restore");
20363
20364        let identity = provider
20365            .restored_terminal_job_log_identity("operation-1")
20366            .expect("durable log identity")
20367            .expect("terminal attempt");
20368        assert_eq!(
20369            identity.expected_completion_source_sequence(),
20370            (1_u64 << 32) | u64::from(u32::MAX)
20371        );
20372        assert!(requests.lock().expect("requests").is_empty());
20373        assert!(
20374            provider
20375                .publisher
20376                .lock()
20377                .expect("publisher")
20378                .published
20379                .is_empty()
20380        );
20381    }
20382
20383    #[test]
20384    fn get_only_cancel_reconciliation_never_posts() {
20385        let runner = FakeGhRunner::with([
20386            Ok(authenticated_user_row()),
20387            Ok(private_repository_row()),
20388            Ok(queued_run_row()),
20389        ]);
20390        let requests = Arc::clone(&runner.requests);
20391        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20392            provider_config(all_authorized()),
20393            transport(runner),
20394            FakePublisher::default(),
20395            NoVerifiedArtifactStore,
20396            FixedClock(1_700_000_000_000),
20397        );
20398        let mut resume = durable_resume_with_run();
20399        resume.state = JobState::Cancelling;
20400        resume.cancellation_requested = true;
20401        let expected = GithubDurableIdentityV1 {
20402            provider: resume.provider.clone(),
20403            provider_config_sha256: resume.provider_config_sha256.clone(),
20404            principal: resume.principal.clone(),
20405            execution_repository: resume.execution_repository.clone(),
20406            execution_repository_id: resume.execution_repository_id,
20407        };
20408        provider
20409            .restore_job_resumes_offline(vec![resume], &expected)
20410            .expect("offline cancelled restore");
20411
20412        provider
20413            .refresh_restored_job_get_only("operation-1", &CancellationToken::new())
20414            .expect("GET-only refresh");
20415
20416        let requests = requests.lock().expect("requests");
20417        assert_eq!(requests.len(), 3);
20418        assert!(
20419            requests
20420                .iter()
20421                .all(|request| request.method() == ApiMethod::Get)
20422        );
20423        let state = provider.lock_state().expect("state");
20424        let record = state.jobs.get("operation-1").expect("restored job");
20425        assert!(record.cancellation_requested);
20426        assert!(!record.cancellation_dispatched);
20427    }
20428
20429    #[test]
20430    fn get_only_exact_ref_recovery_never_creates_dispatch_intent_or_posts() {
20431        let resume = durable_workflow_dispatch_pre_intent_resume();
20432        let runner =
20433            FakeGhRunner::with([Ok(authenticated_user_row()), Ok(private_repository_row())]);
20434        let requests = Arc::clone(&runner.requests);
20435        let plan = FakeWorkflowDispatchPlan::with_response(workflow_dispatch_receipt_response(73));
20436        let sink = CapturingCheckpointSink::default();
20437        let checkpoints = Arc::clone(&sink.checkpoints);
20438        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20439            workflow_dispatch_provider_config(all_authorized()),
20440            transport(runner.with_workflow_dispatch(plan.clone())),
20441            FakePublisher {
20442                reconciliation: Some(TemporaryRefReconciliation::Exact(
20443                    PublishedTemporaryRef::new(
20444                        TemporaryGitRef::new(
20445                            workflow_dispatch_provider_config(all_authorized())
20446                                .workflow
20447                                .temporary_branch_namespace(),
20448                            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
20449                        )
20450                        .expect("temporary ref"),
20451                        CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
20452                    ),
20453                )),
20454                ..FakePublisher::default()
20455            },
20456            NoVerifiedArtifactStore,
20457            FixedClock(1_700_000_000_000),
20458        )
20459        .with_checkpoint_sink(sink);
20460        provider
20461            .restore_job_resumes_offline(
20462                vec![resume.clone()],
20463                &durable_identity_for_resume(&resume),
20464            )
20465            .expect("restore pre-intent publication");
20466
20467        let error = provider
20468            .reconcile_restored_job_get_only("operation-1", &CancellationToken::new())
20469            .expect_err("GET-only recovery cannot start a workflow");
20470        assert!(matches!(
20471            error,
20472            RemoteBuildError::ProviderFailure { code, .. }
20473                if code == "workflow_dispatch_mutation_required"
20474        ));
20475        assert_eq!(plan.0.clients_created.load(Ordering::SeqCst), 0);
20476        assert!(
20477            plan.0
20478                .requests
20479                .lock()
20480                .expect("dispatch requests")
20481                .is_empty()
20482        );
20483        assert!(
20484            requests
20485                .lock()
20486                .expect("requests")
20487                .iter()
20488                .all(|request| request.method() == ApiMethod::Get)
20489        );
20490        assert!(
20491            provider
20492                .publisher
20493                .lock()
20494                .expect("publisher")
20495                .published
20496                .is_empty()
20497        );
20498        let latest = checkpoints
20499            .lock()
20500            .expect("checkpoints")
20501            .last()
20502            .cloned()
20503            .expect("recovered exact ref checkpoint");
20504        assert_eq!(latest.dispatch_commit.as_deref(), Some(DISPATCH_SHA));
20505        assert!(latest.workflow_dispatch.is_none());
20506        assert!(latest.run.is_none());
20507    }
20508
20509    #[test]
20510    fn unarmed_dispatch_publication_intent_is_retired_without_ref_or_workflow_mutation() {
20511        let mut resume = durable_workflow_dispatch_pre_intent_resume();
20512        resume.publication_started_at_ms = 0;
20513        resume.publication_quiescence_deadline_ms = u64::MAX;
20514        resume.publication_absence_first_observed_at_ms = 0;
20515        let plan = FakeWorkflowDispatchPlan::with_response(workflow_dispatch_receipt_response(73));
20516        let sink = CapturingCheckpointSink::default();
20517        let checkpoints = Arc::clone(&sink.checkpoints);
20518        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20519            workflow_dispatch_provider_config(all_authorized()),
20520            transport(FakeGhRunner::with([]).with_workflow_dispatch(plan.clone())),
20521            FakePublisher {
20522                reconciliation: Some(TemporaryRefReconciliation::Exact(
20523                    PublishedTemporaryRef::new(
20524                        TemporaryGitRef::new(
20525                            workflow_dispatch_provider_config(all_authorized())
20526                                .workflow
20527                                .temporary_branch_namespace(),
20528                            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
20529                        )
20530                        .expect("temporary ref"),
20531                        CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
20532                    ),
20533                )),
20534                ..FakePublisher::default()
20535            },
20536            NoVerifiedArtifactStore,
20537            FixedClock(1_700_000_000_000),
20538        )
20539        .with_checkpoint_sink(sink);
20540        provider
20541            .restore_job_resumes_offline(
20542                vec![resume.clone()],
20543                &durable_identity_for_resume(&resume),
20544            )
20545            .expect("restore unarmed publication intent");
20546
20547        assert_eq!(
20548            provider
20549                .reconcile_restored_job("operation-1", &CancellationToken::new())
20550                .expect("retire unarmed publication intent"),
20551            GithubJobReconciliation::NotStarted
20552        );
20553        assert_eq!(plan.0.clients_created.load(Ordering::SeqCst), 0);
20554        assert!(
20555            plan.0
20556                .requests
20557                .lock()
20558                .expect("dispatch requests")
20559                .is_empty()
20560        );
20561        let publisher = provider.publisher.lock().expect("publisher");
20562        assert!(publisher.published.is_empty());
20563        assert!(publisher.reconciliation.is_some());
20564        drop(publisher);
20565        let latest = checkpoints
20566            .lock()
20567            .expect("checkpoints")
20568            .last()
20569            .cloned()
20570            .expect("unarmed retirement checkpoint");
20571        assert!(latest.publication_not_attempted);
20572        assert_eq!(latest.state, JobState::Failed);
20573        assert!(latest.dispatch_commit.is_none());
20574        assert!(latest.workflow_dispatch.is_none());
20575        assert!(latest.run.is_none());
20576    }
20577
20578    #[test]
20579    fn restored_cancel_rejects_changed_live_principal_before_post() {
20580        let runner = FakeGhRunner::with([
20581            Ok(authenticated_user_row()),
20582            Ok(private_repository_row()),
20583            Ok(queued_run_row()),
20584            Ok(b"43\tYXR0YWNrZXI=\n".to_vec()),
20585            Ok(private_repository_row()),
20586        ]);
20587        let requests = Arc::clone(&runner.requests);
20588        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20589            provider_config(all_authorized()),
20590            transport(runner),
20591            FakePublisher::default(),
20592            NoVerifiedArtifactStore,
20593            FixedClock(1_700_000_000_000),
20594        );
20595        provider
20596            .restore_job_resumes(vec![durable_resume_with_run()])
20597            .expect("live run restore");
20598        assert!(
20599            poll_ready(provider.cancel(
20600                CancellationRequest {
20601                    job_id: "operation-1".to_owned(),
20602                    reason: "user-requested".to_owned(),
20603                },
20604                CancellationToken::new(),
20605            ))
20606            .is_err()
20607        );
20608        assert!(
20609            requests
20610                .lock()
20611                .expect("requests")
20612                .iter()
20613                .all(|request| request.method() == ApiMethod::Get)
20614        );
20615    }
20616
20617    #[test]
20618    fn restore_rejects_a_changed_run_attempt_before_any_mutation() {
20619        let runner = FakeGhRunner::with([
20620            Ok(authenticated_user_row()),
20621            Ok(private_repository_row()),
20622            Ok(queued_run_attempt_row(2)),
20623            Ok(queued_run_attempt_row(2)),
20624        ]);
20625        let requests = Arc::clone(&runner.requests);
20626        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20627            provider_config(all_authorized()),
20628            transport(runner),
20629            FakePublisher::default(),
20630            NoVerifiedArtifactStore,
20631            FixedClock(1_700_000_000_000),
20632        );
20633        assert!(
20634            provider
20635                .restore_job_resumes(vec![durable_resume_with_run()])
20636                .is_err()
20637        );
20638        assert!(
20639            requests
20640                .lock()
20641                .expect("requests")
20642                .iter()
20643                .all(|request| request.method() == ApiMethod::Get)
20644        );
20645        assert!(provider.lock_state().expect("state").jobs.is_empty());
20646    }
20647
20648    #[test]
20649    fn restore_rejects_live_run_regression_from_persisted_success() {
20650        let runner = FakeGhRunner::with([
20651            Ok(authenticated_user_row()),
20652            Ok(private_repository_row()),
20653            Ok(queued_run_row()),
20654        ]);
20655        let requests = Arc::clone(&runner.requests);
20656        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20657            provider_config(all_authorized()),
20658            transport(runner),
20659            FakePublisher::default(),
20660            NoVerifiedArtifactStore,
20661            FixedClock(1_700_000_000_000),
20662        );
20663        assert!(
20664            provider
20665                .restore_job_resumes(vec![durable_success_resume()])
20666                .is_err()
20667        );
20668        assert!(
20669            requests
20670                .lock()
20671                .expect("requests")
20672                .iter()
20673                .all(|request| request.method() == ApiMethod::Get)
20674        );
20675        assert!(provider.lock_state().expect("state").jobs.is_empty());
20676    }
20677
20678    #[test]
20679    fn pre_push_resume_reconciles_exact_ref_without_duplicate_publication() {
20680        let resume = durable_prepared_prepublication_resume();
20681        let publisher = FakePublisher {
20682            reconciliation: Some(TemporaryRefReconciliation::Exact(
20683                PublishedTemporaryRef::new(
20684                    TemporaryGitRef::new(
20685                        provider_config(all_authorized())
20686                            .workflow
20687                            .temporary_branch_namespace(),
20688                        BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
20689                    )
20690                    .expect("temporary ref"),
20691                    CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
20692                ),
20693            )),
20694            ..FakePublisher::default()
20695        };
20696        let sink = CapturingCheckpointSink::default();
20697        let checkpoints = Arc::clone(&sink.checkpoints);
20698        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20699            provider_config(all_authorized()),
20700            transport(FakeGhRunner::with([
20701                Ok(authenticated_user_row()),
20702                Ok(private_repository_row()),
20703                Ok(authenticated_user_row()),
20704                Ok(private_repository_row()),
20705                Ok(authenticated_user_row()),
20706                Ok(private_repository_row()),
20707                Ok(authenticated_user_row()),
20708                Ok(private_repository_row()),
20709                Ok(Vec::new()),
20710            ])),
20711            publisher,
20712            NoVerifiedArtifactStore,
20713            FixedClock(1_700_000_000_000),
20714        )
20715        .with_checkpoint_sink(sink);
20716        provider
20717            .restore_job_resumes(vec![resume])
20718            .expect("restore publication intent");
20719        assert_eq!(
20720            provider
20721                .reconcile_restored_job("operation-1", &CancellationToken::new())
20722                .expect("reconciliation"),
20723            GithubJobReconciliation::Recovered
20724        );
20725        assert!(
20726            provider
20727                .publisher
20728                .lock()
20729                .expect("publisher")
20730                .published
20731                .is_empty()
20732        );
20733        assert!(
20734            provider
20735                .publisher
20736                .lock()
20737                .expect("publisher")
20738                .publication_leases
20739                .is_empty()
20740        );
20741        assert_eq!(
20742            checkpoints
20743                .lock()
20744                .expect("checkpoints")
20745                .last()
20746                .and_then(|resume| resume.dispatch_commit.as_deref()),
20747            Some(DISPATCH_SHA)
20748        );
20749    }
20750
20751    #[test]
20752    #[allow(clippy::too_many_lines)]
20753    fn possible_push_checkpoint_requires_exact_missing_or_conflict_reconciliation() {
20754        let exact_resume = durable_possible_push_resume();
20755        let exact_sink = CapturingCheckpointSink::default();
20756        let exact_checkpoints = Arc::clone(&exact_sink.checkpoints);
20757        let exact_provider = GithubBuildProvider::with_artifact_store_and_clock(
20758            provider_config(all_authorized()),
20759            transport(FakeGhRunner::with([
20760                Ok(authenticated_user_row()),
20761                Ok(private_repository_row()),
20762                Ok(authenticated_user_row()),
20763                Ok(private_repository_row()),
20764                Ok(authenticated_user_row()),
20765                Ok(private_repository_row()),
20766                Ok(authenticated_user_row()),
20767                Ok(private_repository_row()),
20768                Ok(Vec::new()),
20769            ])),
20770            FakePublisher {
20771                reconciliation: Some(TemporaryRefReconciliation::Exact(expected_publication())),
20772                ..FakePublisher::default()
20773            },
20774            NoVerifiedArtifactStore,
20775            FixedClock(1_700_000_000_000),
20776        )
20777        .with_checkpoint_sink(exact_sink);
20778        exact_provider
20779            .restore_job_resumes(vec![exact_resume.clone()])
20780            .expect("restore possible push");
20781        assert_eq!(
20782            exact_provider
20783                .reconcile_restored_job("operation-1", &CancellationToken::new())
20784                .expect("exact reconciliation"),
20785            GithubJobReconciliation::Recovered
20786        );
20787        let exact = exact_checkpoints
20788            .lock()
20789            .expect("exact checkpoints")
20790            .last()
20791            .cloned()
20792            .expect("exact checkpoint");
20793        exact_resume
20794            .validate_successor(&exact)
20795            .expect("exact evidence is monotonic");
20796        assert!(!exact.publication_uncertain);
20797        assert!(
20798            exact_provider
20799                .publisher
20800                .lock()
20801                .expect("publisher")
20802                .publication_leases
20803                .is_empty()
20804        );
20805
20806        let missing_resume = durable_possible_push_resume();
20807        let missing_sink = CapturingCheckpointSink::default();
20808        let missing_checkpoints = Arc::clone(&missing_sink.checkpoints);
20809        let missing_provider = GithubBuildProvider::with_artifact_store_and_clock(
20810            provider_config(all_authorized()),
20811            transport(FakeGhRunner::with([
20812                Ok(authenticated_user_row()),
20813                Ok(private_repository_row()),
20814                Ok(authenticated_user_row()),
20815                Ok(private_repository_row()),
20816                Ok(authenticated_user_row()),
20817                Ok(private_repository_row()),
20818                Ok(Vec::new()),
20819                Ok(authenticated_user_row()),
20820                Ok(private_repository_row()),
20821            ])),
20822            FakePublisher::default(),
20823            NoVerifiedArtifactStore,
20824            FixedClock(1_700_000_000_000),
20825        )
20826        .with_checkpoint_sink(missing_sink);
20827        missing_provider
20828            .restore_job_resumes(vec![missing_resume.clone()])
20829            .expect("restore possible push");
20830        assert_eq!(
20831            missing_provider
20832                .reconcile_restored_job("operation-1", &CancellationToken::new())
20833                .expect("missing reconciliation"),
20834            GithubJobReconciliation::Missing
20835        );
20836        let missing = missing_checkpoints
20837            .lock()
20838            .expect("missing checkpoints")
20839            .last()
20840            .cloned()
20841            .expect("missing checkpoint");
20842        missing_resume
20843            .validate_successor(&missing)
20844            .expect("missing observation is monotonic");
20845        assert_eq!(missing.state, JobState::Queued);
20846        assert!(missing.publication_uncertain);
20847        assert_eq!(missing.publication_absence_observations, 1);
20848        assert_eq!(
20849            missing_provider
20850                .publisher
20851                .lock()
20852                .expect("publisher")
20853                .publication_leases
20854                .len(),
20855            1
20856        );
20857
20858        let conflict_resume = durable_possible_push_resume();
20859        let conflict_sink = CapturingCheckpointSink::default();
20860        let conflict_checkpoints = Arc::clone(&conflict_sink.checkpoints);
20861        let conflict_clock = MutableClock::new(1_700_000_000_000);
20862        let conflict_provider = GithubBuildProvider::with_artifact_store_and_clock(
20863            provider_config(all_authorized()),
20864            transport(FakeGhRunner::with([
20865                Ok(authenticated_user_row()),
20866                Ok(private_repository_row()),
20867                Ok(authenticated_user_row()),
20868                Ok(private_repository_row()),
20869                Ok(authenticated_user_row()),
20870                Ok(private_repository_row()),
20871                Ok(Vec::new()),
20872                Ok(authenticated_user_row()),
20873                Ok(private_repository_row()),
20874                Ok(authenticated_user_row()),
20875                Ok(private_repository_row()),
20876                Ok(authenticated_user_row()),
20877                Ok(private_repository_row()),
20878                Ok(Vec::new()),
20879                Ok(authenticated_user_row()),
20880                Ok(private_repository_row()),
20881            ])),
20882            FakePublisher {
20883                reconciliation: Some(TemporaryRefReconciliation::Conflict),
20884                ..FakePublisher::default()
20885            },
20886            NoVerifiedArtifactStore,
20887            conflict_clock.clone(),
20888        )
20889        .with_checkpoint_sink(conflict_sink);
20890        conflict_provider
20891            .restore_job_resumes(vec![conflict_resume.clone()])
20892            .expect("restore possible push");
20893        assert_eq!(
20894            conflict_provider
20895                .reconcile_restored_job("operation-1", &CancellationToken::new())
20896                .expect("conflict reconciliation"),
20897            GithubJobReconciliation::Conflict
20898        );
20899        let conflict = conflict_checkpoints
20900            .lock()
20901            .expect("conflict checkpoints")
20902            .last()
20903            .cloned()
20904            .expect("conflict checkpoint");
20905        conflict_resume
20906            .validate_successor(&conflict)
20907            .expect("conflict observation is monotonic");
20908        assert_eq!(conflict.state, JobState::Queued);
20909        assert!(conflict.publication_uncertain);
20910        assert_eq!(conflict.publication_absence_observations, 1);
20911        assert_eq!(
20912            conflict_provider
20913                .publisher
20914                .lock()
20915                .expect("publisher")
20916                .publication_leases
20917                .len(),
20918            1
20919        );
20920
20921        conflict_provider
20922            .publisher
20923            .lock()
20924            .expect("publisher")
20925            .reconciliation = Some(TemporaryRefReconciliation::Conflict);
20926        conflict_clock.set(1_700_000_000_000 + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS);
20927        assert_eq!(
20928            conflict_provider
20929                .reconcile_restored_job("operation-1", &CancellationToken::new())
20930                .expect("bounded conflict reconciliation"),
20931            GithubJobReconciliation::ConflictResolved
20932        );
20933        let resolved_conflict = conflict_checkpoints
20934            .lock()
20935            .expect("conflict checkpoints")
20936            .last()
20937            .cloned()
20938            .expect("resolved conflict checkpoint");
20939        conflict
20940            .validate_successor(&resolved_conflict)
20941            .expect("bounded conflict resolution is monotonic");
20942        assert_eq!(resolved_conflict.state, JobState::Failed);
20943        assert!(resolved_conflict.publication_uncertain);
20944        assert_eq!(resolved_conflict.publication_absence_observations, 2);
20945        assert!(
20946            conflict_provider
20947                .publisher
20948                .lock()
20949                .expect("publisher")
20950                .publication_leases
20951                .is_empty()
20952        );
20953    }
20954
20955    #[test]
20956    fn restored_unarmed_publication_waits_for_the_original_process_lease() {
20957        let mut resume = durable_prepared_prepublication_resume();
20958        resume.publication_started_at_ms = 0;
20959        resume.publication_quiescence_deadline_ms = u64::MAX;
20960        let sink = CapturingCheckpointSink::default();
20961        let checkpoints = Arc::clone(&sink.checkpoints);
20962        let provider = GithubBuildProvider::with_artifact_store_and_clock(
20963            provider_config(all_authorized()),
20964            transport(FakeGhRunner::with([
20965                Ok(authenticated_user_row()),
20966                Ok(private_repository_row()),
20967            ])),
20968            FakePublisher {
20969                publication_lease_blocked: true,
20970                ..FakePublisher::default()
20971            },
20972            NoVerifiedArtifactStore,
20973            FixedClock(1_700_000_000_000),
20974        )
20975        .with_checkpoint_sink(sink);
20976        provider
20977            .restore_job_resumes(vec![resume])
20978            .expect("restore unarmed intent");
20979
20980        let error = provider
20981            .reconcile_restored_job("operation-1", &CancellationToken::new())
20982            .expect_err("live original owner must block takeover");
20983        assert!(matches!(
20984            error,
20985            RemoteBuildError::ProviderFailure {
20986                code,
20987                retryable: true,
20988                ..
20989            } if code == "publication_owner_active"
20990        ));
20991        assert!(checkpoints.lock().expect("checkpoints").is_empty());
20992        let state = provider.lock_state().expect("state");
20993        let record = state.jobs.get("operation-1").expect("restored job");
20994        assert_eq!(record.state, JobState::Created);
20995        assert_eq!(record.publication_absence_observations, 0);
20996        assert!(!record.publication_not_attempted);
20997    }
20998
20999    #[test]
21000    fn unsupported_publication_lease_never_retires_unarmed_intent() {
21001        let mut resume = durable_prepared_prepublication_resume();
21002        resume.publication_started_at_ms = 0;
21003        resume.publication_quiescence_deadline_ms = u64::MAX;
21004        resume.publication_process_fenced = false;
21005        resume.publication_lease_scope_sha256 = None;
21006        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21007            provider_config(all_authorized()),
21008            transport(FakeGhRunner::with([
21009                Ok(authenticated_user_row()),
21010                Ok(private_repository_row()),
21011            ])),
21012            FakePublisher {
21013                publication_lease_unsupported: true,
21014                ..FakePublisher::default()
21015            },
21016            NoVerifiedArtifactStore,
21017            FixedClock(1_700_000_000_000),
21018        );
21019        provider
21020            .restore_job_resumes(vec![resume])
21021            .expect("restore unsupported intent");
21022        assert!(matches!(
21023            provider.reconcile_restored_job("operation-1", &CancellationToken::new()),
21024            Err(RemoteBuildError::ProviderFailure {
21025                code,
21026                retryable: true,
21027                ..
21028            }) if code == "publication_lease_unavailable"
21029        ));
21030        let state = provider.lock_state().expect("state");
21031        let record = state.jobs.get("operation-1").expect("restored job");
21032        assert_eq!(record.state, JobState::Created);
21033        assert_eq!(record.publication_absence_observations, 0);
21034    }
21035
21036    #[test]
21037    fn absent_pre_push_ref_keeps_cancellation_fail_closed_and_non_recyclable() {
21038        let sink = CapturingCheckpointSink::default();
21039        let checkpoints = Arc::clone(&sink.checkpoints);
21040        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21041            provider_config(all_authorized()),
21042            transport(FakeGhRunner::with([
21043                Ok(authenticated_user_row()),
21044                Ok(private_repository_row()),
21045                Ok(authenticated_user_row()),
21046                Ok(private_repository_row()),
21047                Ok(authenticated_user_row()),
21048                Ok(private_repository_row()),
21049                Ok(Vec::new()),
21050                Ok(authenticated_user_row()),
21051                Ok(private_repository_row()),
21052            ])),
21053            FakePublisher::default(),
21054            NoVerifiedArtifactStore,
21055            FixedClock(1_700_000_000_000),
21056        )
21057        .with_checkpoint_sink(sink);
21058        provider
21059            .restore_job_resumes(vec![durable_prepared_prepublication_resume()])
21060            .expect("restore publication intent");
21061        poll_ready(provider.cancel(
21062            CancellationRequest {
21063                job_id: "operation-1".to_owned(),
21064                reason: "user-requested".to_owned(),
21065            },
21066            CancellationToken::new(),
21067        ))
21068        .expect("durable cancellation intent");
21069        assert_eq!(
21070            provider
21071                .reconcile_restored_job("operation-1", &CancellationToken::new())
21072                .expect("missing ref reconciliation"),
21073            GithubJobReconciliation::Missing
21074        );
21075        let latest = checkpoints
21076            .lock()
21077            .expect("checkpoints")
21078            .last()
21079            .cloned()
21080            .expect("latest checkpoint");
21081        assert!(latest.publication_uncertain);
21082        assert!(!latest.publication_absent);
21083        assert!(!latest.temporary_ref_deleted);
21084        assert_eq!(latest.state, JobState::Cancelling);
21085
21086        assert!(poll_ready(provider.submit(unsigned_request(), CancellationToken::new())).is_err());
21087    }
21088
21089    #[test]
21090    fn absent_pre_push_ref_does_not_claim_publication_absence() {
21091        let sink = CapturingCheckpointSink::default();
21092        let checkpoints = Arc::clone(&sink.checkpoints);
21093        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21094            provider_config(all_authorized()),
21095            transport(FakeGhRunner::with([
21096                Ok(authenticated_user_row()),
21097                Ok(private_repository_row()),
21098                Ok(authenticated_user_row()),
21099                Ok(private_repository_row()),
21100                Ok(authenticated_user_row()),
21101                Ok(private_repository_row()),
21102                Ok(Vec::new()),
21103                Ok(authenticated_user_row()),
21104                Ok(private_repository_row()),
21105            ])),
21106            FakePublisher::default(),
21107            NoVerifiedArtifactStore,
21108            FixedClock(1_700_000_000_000),
21109        )
21110        .with_checkpoint_sink(sink);
21111        provider
21112            .restore_job_resumes(vec![durable_prepared_prepublication_resume()])
21113            .expect("restore publication intent");
21114        assert_eq!(
21115            provider
21116                .reconcile_restored_job("operation-1", &CancellationToken::new())
21117                .expect("missing ref reconciliation"),
21118            GithubJobReconciliation::Missing
21119        );
21120        let latest = checkpoints
21121            .lock()
21122            .expect("checkpoints")
21123            .last()
21124            .cloned()
21125            .expect("latest checkpoint");
21126        assert_eq!(latest.state, JobState::Created);
21127        assert!(latest.publication_uncertain);
21128        assert!(!latest.publication_absent);
21129        assert!(!latest.temporary_ref_deleted);
21130    }
21131
21132    #[test]
21133    fn missing_pre_push_ref_can_be_reconciled_when_exact_ref_appears_later() {
21134        let sink = CapturingCheckpointSink::default();
21135        let checkpoints = Arc::clone(&sink.checkpoints);
21136        let responses = [
21137            Ok(authenticated_user_row()),
21138            Ok(private_repository_row()),
21139            Ok(authenticated_user_row()),
21140            Ok(private_repository_row()),
21141            Ok(authenticated_user_row()),
21142            Ok(private_repository_row()),
21143            Ok(Vec::new()),
21144            Ok(authenticated_user_row()),
21145            Ok(private_repository_row()),
21146            Ok(authenticated_user_row()),
21147            Ok(private_repository_row()),
21148            Ok(authenticated_user_row()),
21149            Ok(private_repository_row()),
21150            Ok(authenticated_user_row()),
21151            Ok(private_repository_row()),
21152            Ok(Vec::new()),
21153        ];
21154        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21155            provider_config(all_authorized()),
21156            transport(FakeGhRunner::with(responses)),
21157            FakePublisher::default(),
21158            NoVerifiedArtifactStore,
21159            FixedClock(1_700_000_000_000),
21160        )
21161        .with_checkpoint_sink(sink);
21162        provider
21163            .restore_job_resumes(vec![durable_prepared_prepublication_resume()])
21164            .expect("restore publication intent");
21165
21166        assert_eq!(
21167            provider
21168                .reconcile_restored_job("operation-1", &CancellationToken::new())
21169                .expect("initial missing reconciliation"),
21170            GithubJobReconciliation::Missing
21171        );
21172        let missing = checkpoints
21173            .lock()
21174            .expect("checkpoints")
21175            .last()
21176            .cloned()
21177            .expect("missing checkpoint");
21178        assert_eq!(missing.state, JobState::Created);
21179        assert!(missing.publication_uncertain);
21180
21181        provider.publisher.lock().expect("publisher").reconciliation = Some(
21182            TemporaryRefReconciliation::Exact(PublishedTemporaryRef::new(
21183                TemporaryGitRef::new(
21184                    provider_config(all_authorized())
21185                        .workflow
21186                        .temporary_branch_namespace(),
21187                    BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
21188                )
21189                .expect("temporary ref"),
21190                CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
21191            )),
21192        );
21193        assert_eq!(
21194            provider
21195                .reconcile_restored_job("operation-1", &CancellationToken::new())
21196                .expect("later exact reconciliation"),
21197            GithubJobReconciliation::Recovered
21198        );
21199
21200        let latest = checkpoints
21201            .lock()
21202            .expect("checkpoints")
21203            .last()
21204            .cloned()
21205            .expect("exact checkpoint");
21206        missing
21207            .validate_successor(&latest)
21208            .expect("exact evidence safely resolves publication uncertainty");
21209        assert_eq!(latest.state, JobState::Queued);
21210        assert_eq!(latest.dispatch_commit.as_deref(), Some(DISPATCH_SHA));
21211        assert!(!latest.publication_uncertain);
21212        assert!(
21213            provider
21214                .publisher
21215                .lock()
21216                .expect("publisher")
21217                .published
21218                .is_empty()
21219        );
21220    }
21221
21222    #[test]
21223    fn missing_prepared_ref_recovers_exact_run_history_without_duplicate_push() {
21224        let mut responses = Vec::new();
21225        for _ in 0..6 {
21226            responses.push(Ok(authenticated_user_row()));
21227            responses.push(Ok(private_repository_row()));
21228        }
21229        responses.insert(6, Ok(queued_run_row()));
21230        responses.insert(9, Ok(queued_run_row()));
21231        responses.push(Ok(queued_run_row()));
21232        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21233            provider_config(all_authorized()),
21234            transport(FakeGhRunner::with(responses)),
21235            FakePublisher::default(),
21236            NoVerifiedArtifactStore,
21237            FixedClock(1_700_000_000_000),
21238        );
21239        provider
21240            .restore_job_resumes(vec![durable_prepared_prepublication_resume()])
21241            .expect("restore prepared publication");
21242
21243        assert_eq!(
21244            provider
21245                .reconcile_restored_job("operation-1", &CancellationToken::new())
21246                .expect("run-history recovery"),
21247            GithubJobReconciliation::Recovered
21248        );
21249        let state = provider.lock_state().expect("state");
21250        let record = state.jobs.get("operation-1").expect("job");
21251        assert_eq!(
21252            record.dispatch_commit.as_ref().map(CommitSha::as_str),
21253            Some(DISPATCH_SHA)
21254        );
21255        assert!(record.run.is_some());
21256        assert_eq!(record.state, JobState::Queued);
21257        assert!(!record.publication_uncertain);
21258        assert!(
21259            provider
21260                .publisher
21261                .lock()
21262                .expect("publisher")
21263                .published
21264                .is_empty()
21265        );
21266    }
21267
21268    #[test]
21269    fn missing_prepared_ref_rejects_an_external_rerun_before_mapping() {
21270        let mut responses = Vec::new();
21271        for _ in 0..6 {
21272            responses.push(Ok(authenticated_user_row()));
21273            responses.push(Ok(private_repository_row()));
21274        }
21275        responses.insert(6, Ok(queued_run_attempt_row(2)));
21276        responses.insert(9, Ok(queued_run_attempt_row(2)));
21277        responses.push(Ok(queued_run_attempt_row(2)));
21278        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21279            provider_config(all_authorized()),
21280            transport(FakeGhRunner::with(responses)),
21281            FakePublisher::default(),
21282            NoVerifiedArtifactStore,
21283            FixedClock(1_700_000_000_000),
21284        );
21285        provider
21286            .restore_job_resumes(vec![durable_prepared_prepublication_resume()])
21287            .expect("restore prepared publication");
21288
21289        assert!(
21290            provider
21291                .reconcile_restored_job("operation-1", &CancellationToken::new())
21292                .is_err()
21293        );
21294        let state = provider.lock_state().expect("state");
21295        let record = state.jobs.get("operation-1").expect("job");
21296        assert!(record.dispatch_commit.is_none());
21297        assert!(record.run.is_none());
21298        assert!(record.publication_uncertain);
21299        assert!(
21300            provider
21301                .publisher
21302                .lock()
21303                .expect("publisher")
21304                .published
21305                .is_empty()
21306        );
21307    }
21308
21309    #[test]
21310    fn restored_unarmed_publication_is_retired_without_remote_reads_and_releases_capacity() {
21311        let base_config = provider_config(all_authorized());
21312        let principal = GithubPrincipalIdentityV1::User {
21313            id: AUTHENTICATED_USER_ID,
21314            login: "ShiroKSH".to_owned(),
21315        };
21316        let mut unarmed = durable_prepared_prepublication_resume();
21317        unarmed.publication_started_at_ms = 0;
21318        unarmed.publication_quiescence_deadline_ms = u64::MAX;
21319        let record = unarmed
21320            .into_record(&base_config, &principal, 991)
21321            .expect("unarmed prepared record");
21322        let mut config = base_config;
21323        config.max_jobs = 1;
21324        let resume = GithubJobResumeV1::from_record(&config, principal, 991, &record)
21325            .expect("capacity-bound unarmed resume");
21326        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21327            config,
21328            transport(FakeGhRunner::with([
21329                Ok(authenticated_user_row()),
21330                Ok(private_repository_row()),
21331            ])),
21332            FakePublisher {
21333                reconciliation: Some(TemporaryRefReconciliation::Conflict),
21334                ..FakePublisher::default()
21335            },
21336            NoVerifiedArtifactStore,
21337            FixedClock(1_700_000_000_000),
21338        );
21339        provider
21340            .restore_job_resumes(vec![resume])
21341            .expect("restore unarmed publication");
21342
21343        assert_eq!(
21344            provider
21345                .reconcile_restored_job("operation-1", &CancellationToken::new())
21346                .expect("retire unarmed intent"),
21347            GithubJobReconciliation::NotStarted
21348        );
21349        {
21350            let state = provider.lock_state().expect("state");
21351            let retired = state.jobs.get("operation-1").expect("retired job");
21352            assert_eq!(retired.state, JobState::Failed);
21353            assert!(retired.publication_not_attempted);
21354            assert!(record_is_recyclable(retired));
21355        }
21356
21357        let mut replacement = unsigned_request();
21358        replacement.operation_id = "operation-2".to_owned();
21359        let handle = poll_ready(provider.submit(replacement, CancellationToken::new()))
21360            .expect("unarmed intent releases bounded capacity");
21361        assert_eq!(handle.job_id, "operation-2");
21362        assert!(matches!(
21363            provider.publisher.lock().expect("publisher").reconciliation,
21364            Some(TemporaryRefReconciliation::Conflict)
21365        ));
21366    }
21367
21368    #[test]
21369    fn bounded_missing_prepared_publication_becomes_recyclable_without_guessing_once() {
21370        let base_config = provider_config(all_authorized());
21371        let principal = GithubPrincipalIdentityV1::User {
21372            id: AUTHENTICATED_USER_ID,
21373            login: "ShiroKSH".to_owned(),
21374        };
21375        let record = durable_prepared_prepublication_resume()
21376            .into_record(&base_config, &principal, 991)
21377            .expect("prepared record");
21378        let mut config = base_config;
21379        config.max_jobs = 1;
21380        let resume = GithubJobResumeV1::from_record(&config, principal.clone(), 991, &record)
21381            .expect("capacity-bound resume");
21382        let mut previous = resume.clone();
21383        let mut responses = vec![Ok(authenticated_user_row()), Ok(private_repository_row())];
21384        for _ in 0..3 {
21385            responses.push(Ok(authenticated_user_row()));
21386            responses.push(Ok(private_repository_row()));
21387            responses.push(Ok(authenticated_user_row()));
21388            responses.push(Ok(private_repository_row()));
21389            responses.push(Ok(Vec::new()));
21390            responses.push(Ok(authenticated_user_row()));
21391            responses.push(Ok(private_repository_row()));
21392        }
21393        let clock = MutableClock::new(1_700_000_000_000);
21394        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21395            config,
21396            transport(FakeGhRunner::with(responses)),
21397            FakePublisher::default(),
21398            NoVerifiedArtifactStore,
21399            clock.clone(),
21400        );
21401        provider
21402            .restore_job_resumes(vec![resume])
21403            .expect("restore prepared publication");
21404
21405        for attempt in 1..=3 {
21406            if attempt == 3 {
21407                clock.set(1_700_000_000_000 + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS);
21408            }
21409            assert_eq!(
21410                provider
21411                    .reconcile_restored_job("operation-1", &CancellationToken::new())
21412                    .expect("bounded missing reconciliation"),
21413                GithubJobReconciliation::Missing
21414            );
21415            let state = provider.lock_state().expect("state");
21416            let record = state.jobs.get("operation-1").expect("job");
21417            if attempt < 3 {
21418                assert_eq!(record.state, JobState::Created);
21419                assert!(!record.publication_absent);
21420                assert_eq!(
21421                    record.publication_absence_observations,
21422                    u8::try_from(attempt).unwrap_or(2)
21423                );
21424            } else {
21425                assert_eq!(record.state, JobState::Failed);
21426                assert!(record.publication_absent);
21427                assert!(record.temporary_ref_deleted);
21428            }
21429            let next =
21430                GithubJobResumeV1::from_record(&provider.config, principal.clone(), 991, record)
21431                    .expect("reconciliation checkpoint");
21432            previous
21433                .validate_successor(&next)
21434                .expect("bounded reconciliation advances monotonically");
21435            previous = next;
21436        }
21437
21438        let mut replacement = unsigned_request();
21439        replacement.operation_id = "operation-2".to_owned();
21440        let handle = poll_ready(provider.submit(replacement, CancellationToken::new()))
21441            .expect("strongly absent terminal record is recyclable");
21442        assert_eq!(handle.job_id, "operation-2");
21443    }
21444
21445    #[test]
21446    fn first_missing_observation_restarts_quiescence_after_a_delayed_push_checkpoint() {
21447        let mut responses = vec![Ok(authenticated_user_row()), Ok(private_repository_row())];
21448        for _ in 0..3 {
21449            responses.push(Ok(authenticated_user_row()));
21450            responses.push(Ok(private_repository_row()));
21451            responses.push(Ok(authenticated_user_row()));
21452            responses.push(Ok(private_repository_row()));
21453            responses.push(Ok(Vec::new()));
21454            responses.push(Ok(authenticated_user_row()));
21455            responses.push(Ok(private_repository_row()));
21456        }
21457        let delayed_now = 1_700_000_000_000 + 10 * GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS;
21458        let clock = MutableClock::new(delayed_now);
21459        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21460            provider_config(all_authorized()),
21461            transport(FakeGhRunner::with(responses)),
21462            FakePublisher::default(),
21463            NoVerifiedArtifactStore,
21464            clock.clone(),
21465        );
21466        provider
21467            .restore_job_resumes(vec![durable_prepared_prepublication_resume()])
21468            .expect("restore aged publication boundary");
21469
21470        for _ in 0..2 {
21471            assert_eq!(
21472                provider
21473                    .reconcile_restored_job("operation-1", &CancellationToken::new())
21474                    .expect("immediate missing observation"),
21475                GithubJobReconciliation::Missing
21476            );
21477        }
21478        {
21479            let state = provider.lock_state().expect("state");
21480            let record = state.jobs.get("operation-1").expect("job");
21481            assert_eq!(record.state, JobState::Created);
21482            assert_eq!(record.publication_absence_first_observed_at_ms, delayed_now);
21483            assert_eq!(
21484                record.publication_quiescence_deadline_ms,
21485                delayed_now + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS
21486            );
21487            assert_eq!(record.publication_absence_observations, 2);
21488            assert!(!record.publication_absent);
21489        }
21490
21491        clock.set(delayed_now + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS);
21492        assert_eq!(
21493            provider
21494                .reconcile_restored_job("operation-1", &CancellationToken::new())
21495                .expect("quiesced missing observation"),
21496            GithubJobReconciliation::Missing
21497        );
21498        let state = provider.lock_state().expect("state");
21499        let record = state.jobs.get("operation-1").expect("job");
21500        assert_eq!(record.state, JobState::Failed);
21501        assert!(record.publication_absent);
21502    }
21503
21504    #[test]
21505    fn unfenced_publication_never_uses_time_as_strong_absence_evidence() {
21506        let mut responses = vec![Ok(authenticated_user_row()), Ok(private_repository_row())];
21507        for _ in 0..2 {
21508            responses.push(Ok(authenticated_user_row()));
21509            responses.push(Ok(private_repository_row()));
21510            responses.push(Ok(authenticated_user_row()));
21511            responses.push(Ok(private_repository_row()));
21512            responses.push(Ok(Vec::new()));
21513            responses.push(Ok(authenticated_user_row()));
21514            responses.push(Ok(private_repository_row()));
21515        }
21516        let delayed_now = 1_700_000_000_000 + 10 * GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS;
21517        let clock = MutableClock::new(delayed_now);
21518        let provider = GithubBuildProvider::with_artifact_store_and_clock(
21519            provider_config(all_authorized()),
21520            transport(FakeGhRunner::with(responses)),
21521            FakePublisher::default(),
21522            NoVerifiedArtifactStore,
21523            clock.clone(),
21524        );
21525        let mut resume = durable_prepared_prepublication_resume();
21526        resume.publication_process_fenced = false;
21527        provider
21528            .restore_job_resumes(vec![resume])
21529            .expect("restore unfenced publication");
21530
21531        assert_eq!(
21532            provider
21533                .reconcile_restored_job("operation-1", &CancellationToken::new())
21534                .expect("first missing observation"),
21535            GithubJobReconciliation::Missing
21536        );
21537        clock.set(delayed_now + GITHUB_PUBLICATION_QUIESCENCE_WINDOW_MS);
21538        assert_eq!(
21539            provider
21540                .reconcile_restored_job("operation-1", &CancellationToken::new())
21541                .expect("post-quiescence missing observation"),
21542            GithubJobReconciliation::Missing
21543        );
21544        let state = provider.lock_state().expect("state");
21545        let record = state.jobs.get("operation-1").expect("job");
21546        assert_eq!(record.publication_absence_observations, 2);
21547        assert_eq!(record.state, JobState::Created);
21548        assert!(!record.publication_absent);
21549        assert!(!record_is_recyclable(record));
21550    }
21551
21552    #[test]
21553    #[allow(
21554        clippy::too_many_lines,
21555        reason = "one regression binds takeover lease, crash residue, recomputation, and no-fetch proof"
21556    )]
21557    fn git_reconciliation_recomputes_the_prepared_commit_without_fetching_remote_objects() {
21558        let directory = tempdir().expect("directory");
21559        let stale_lock = directory.path().join("operation-1.lock");
21560        let stale_index = directory.path().join("operation-1.index");
21561        let stale_attempt = directory.path().join(".rustferry-index-crash-residue");
21562        fs::write(&stale_lock, b"crash-owner").expect("stale legacy reservation lock");
21563        fs::write(&stale_index, b"partial-index").expect("stale legacy index");
21564        fs::create_dir(&stale_attempt).expect("stale attempt directory");
21565        fs::write(stale_attempt.join("index"), b"partial-index").expect("stale attempt index");
21566        fs::write(stale_attempt.join("index.lock"), b"crash-owner").expect("stale attempt lock");
21567        let workflow_path = ".github/workflows/rustferry-goal3-iphone.yml";
21568        let workflow = b"name: exact\n";
21569        let manifest = vec![b'm'; MAX_GIT_OUTPUT_BYTES + 1];
21570        let runner = FakeGitRunner::with(exact_reconciliation_git_responses(
21571            workflow_path,
21572            workflow,
21573            &manifest,
21574            DISPATCH_SHA,
21575        ));
21576        let mut publisher = GitTemporaryRefPublisher::new(
21577            runner,
21578            directory.path(),
21579            "origin",
21580            "signing",
21581            Duration::from_secs(30),
21582        )
21583        .expect("publisher");
21584        let namespace =
21585            TemporaryBranchNamespace::new("rustferry/goal3/builds").expect("temporary namespace");
21586        let temporary_ref = TemporaryGitRef::new(
21587            &namespace,
21588            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
21589        )
21590        .expect("temporary ref");
21591        let config = provider_config(all_authorized());
21592        let source_revision = CommitSha::new(SOURCE_SHA).expect("source revision");
21593        let expected_prepared_commit = CommitSha::new(DISPATCH_SHA).expect("prepared commit");
21594        let workflow_fingerprint = WorkflowFingerprint::for_workflow_bytes(workflow);
21595        assert_eq!(
21596            publisher.acquire_publication_lease(&TemporaryRefPublicationLeaseRequest {
21597                repository: &config.repository,
21598                operation_id: "operation-1",
21599                temporary_ref: &temporary_ref,
21600            }),
21601            Ok(TemporaryRefPublicationLease::Acquired)
21602        );
21603        let result = publisher
21604            .reconcile_temporary_ref(&TemporaryRefReconcileRequest {
21605                repository: &config.repository,
21606                source_repository: &config.source_repository,
21607                trusted_source_ref: config.workflow.trusted_source_ref(),
21608                source_revision: &source_revision,
21609                snapshot_source_ref: None,
21610                temporary_ref: &temporary_ref,
21611                workflow_path,
21612                workflow_bytes: workflow,
21613                workflow_fingerprint: &workflow_fingerprint,
21614                manifest_bytes: &manifest,
21615                operation_id: "operation-1",
21616                created_at_ms: 1_700_000_000_000,
21617                expected_prepared_commit: &expected_prepared_commit,
21618            })
21619            .expect("exact reconciliation");
21620        assert!(matches!(
21621            result,
21622            TemporaryRefReconciliation::Exact(publication)
21623                if publication.commit().as_str() == DISPATCH_SHA
21624                    && publication.temporary_ref() == &temporary_ref
21625        ));
21626        assert_eq!(
21627            fs::read(&stale_lock).expect("legacy lock retained"),
21628            b"crash-owner"
21629        );
21630        assert_eq!(
21631            fs::read(&stale_index).expect("legacy index retained"),
21632            b"partial-index"
21633        );
21634        assert_eq!(
21635            fs::read(stale_attempt.join("index")).expect("stale attempt retained"),
21636            b"partial-index"
21637        );
21638        let runner = publisher.into_runner();
21639        assert!(!runner.invocations.iter().any(|invocation| {
21640            invocation.arguments.first() == Some(&OsString::from("fetch"))
21641                && invocation
21642                    .arguments
21643                    .iter()
21644                    .any(|argument| argument == "refs/heads/rustferry/goal3/builds/operation-1")
21645        }));
21646        assert!(
21647            runner
21648                .invocations
21649                .iter()
21650                .filter(|invocation| matches!(
21651                    invocation
21652                        .arguments
21653                        .first()
21654                        .and_then(|argument| argument.to_str()),
21655                    Some("show" | "cat-file" | "merge-base")
21656                ))
21657                .all(|invocation| invocation.environment.is_empty())
21658        );
21659    }
21660
21661    #[test]
21662    fn git_reconciliation_rejects_exact_ref_when_source_is_not_trusted() {
21663        let directory = tempdir().expect("directory");
21664        let workflow_path = ".github/workflows/rustferry-goal3-iphone.yml";
21665        let workflow = b"name: exact\n";
21666        let manifest = b"{}\n";
21667        let mut responses =
21668            exact_reconciliation_git_responses(workflow_path, workflow, manifest, DISPATCH_SHA);
21669        responses[9] = Err(GitExecutionError::CommandFailed { exit_code: Some(1) });
21670        let runner = FakeGitRunner::with(responses);
21671        let mut publisher = GitTemporaryRefPublisher::new(
21672            runner,
21673            directory.path(),
21674            "origin",
21675            "signing",
21676            Duration::from_secs(30),
21677        )
21678        .expect("publisher");
21679        let config = provider_config(all_authorized());
21680        let temporary_ref = TemporaryGitRef::new(
21681            config.workflow.temporary_branch_namespace(),
21682            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
21683        )
21684        .expect("temporary ref");
21685        let source_revision = CommitSha::new(SOURCE_SHA).expect("source revision");
21686        let expected_prepared_commit = CommitSha::new(DISPATCH_SHA).expect("prepared commit");
21687        let workflow_fingerprint = WorkflowFingerprint::for_workflow_bytes(workflow);
21688
21689        assert_eq!(
21690            publisher.reconcile_temporary_ref(&TemporaryRefReconcileRequest {
21691                repository: &config.repository,
21692                source_repository: &config.source_repository,
21693                trusted_source_ref: config.workflow.trusted_source_ref(),
21694                source_revision: &source_revision,
21695                snapshot_source_ref: None,
21696                temporary_ref: &temporary_ref,
21697                workflow_path,
21698                workflow_bytes: workflow,
21699                workflow_fingerprint: &workflow_fingerprint,
21700                manifest_bytes: manifest,
21701                operation_id: "operation-1",
21702                created_at_ms: 1_700_000_000_000,
21703                expected_prepared_commit: &expected_prepared_commit,
21704            }),
21705            Err(TemporaryRefPublishError::SourceRevisionNotTrusted)
21706        );
21707        let runner = publisher.into_runner();
21708        assert!(!runner.invocations.iter().any(|invocation| {
21709            invocation.arguments.first() == Some(&OsString::from("ls-remote"))
21710                && invocation
21711                    .arguments
21712                    .iter()
21713                    .any(|argument| argument == "refs/heads/rustferry/goal3/builds/operation-1")
21714        }));
21715    }
21716
21717    #[test]
21718    fn git_reconciliation_rejects_a_ref_that_changes_during_content_verification() {
21719        let directory = tempdir().expect("directory");
21720        let workflow_path = ".github/workflows/rustferry-goal3-iphone.yml";
21721        let workflow = b"name: exact\n";
21722        let manifest = b"{}\n";
21723        let runner = FakeGitRunner::with(exact_reconciliation_git_responses(
21724            workflow_path,
21725            workflow,
21726            manifest,
21727            SOURCE_SHA,
21728        ));
21729        let mut publisher = GitTemporaryRefPublisher::new(
21730            runner,
21731            directory.path(),
21732            "origin",
21733            "signing",
21734            Duration::from_secs(30),
21735        )
21736        .expect("publisher");
21737        let namespace =
21738            TemporaryBranchNamespace::new("rustferry/goal3/builds").expect("temporary namespace");
21739        let temporary_ref = TemporaryGitRef::new(
21740            &namespace,
21741            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
21742        )
21743        .expect("temporary ref");
21744        let config = provider_config(all_authorized());
21745        let source_revision = CommitSha::new(SOURCE_SHA).expect("source revision");
21746        let expected_prepared_commit = CommitSha::new(DISPATCH_SHA).expect("prepared commit");
21747        let workflow_fingerprint = WorkflowFingerprint::for_workflow_bytes(workflow);
21748        assert_eq!(
21749            publisher
21750                .reconcile_temporary_ref(&TemporaryRefReconcileRequest {
21751                    repository: &config.repository,
21752                    source_repository: &config.source_repository,
21753                    trusted_source_ref: config.workflow.trusted_source_ref(),
21754                    source_revision: &source_revision,
21755                    snapshot_source_ref: None,
21756                    temporary_ref: &temporary_ref,
21757                    workflow_path,
21758                    workflow_bytes: workflow,
21759                    workflow_fingerprint: &workflow_fingerprint,
21760                    manifest_bytes: manifest,
21761                    operation_id: "operation-1",
21762                    created_at_ms: 1_700_000_000_000,
21763                    expected_prepared_commit: &expected_prepared_commit,
21764                })
21765                .expect("bounded reconciliation"),
21766            TemporaryRefReconciliation::Conflict
21767        );
21768    }
21769
21770    #[test]
21771    fn git_publication_lease_is_cross_instance_and_key_scoped() {
21772        let root = tempdir().expect("root");
21773        let isolation = root.path().join("isolation");
21774        create_test_private_directory(&isolation).expect("isolation directory");
21775        let mut first = GitTemporaryRefPublisher::new(
21776            FakeGitRunner::default(),
21777            &isolation,
21778            "origin",
21779            "signing",
21780            Duration::from_secs(30),
21781        )
21782        .expect("first publisher");
21783        let mut second = GitTemporaryRefPublisher::new(
21784            FakeGitRunner::default(),
21785            &isolation,
21786            "origin",
21787            "signing",
21788            Duration::from_secs(30),
21789        )
21790        .expect("second publisher");
21791        assert_eq!(
21792            first.publication_lease_scope_sha256(),
21793            second.publication_lease_scope_sha256()
21794        );
21795        let config = provider_config(all_authorized());
21796        let temporary_ref = TemporaryGitRef::new(
21797            config.workflow.temporary_branch_namespace(),
21798            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
21799        )
21800        .expect("temporary ref");
21801        let request = TemporaryRefPublicationLeaseRequest {
21802            repository: &config.repository,
21803            operation_id: "operation-1",
21804            temporary_ref: &temporary_ref,
21805        };
21806        assert_eq!(
21807            first.acquire_publication_lease(&request),
21808            Ok(TemporaryRefPublicationLease::Acquired)
21809        );
21810        assert_eq!(
21811            second.acquire_publication_lease(&request),
21812            Ok(TemporaryRefPublicationLease::HeldByOther)
21813        );
21814
21815        let independent_ref = TemporaryGitRef::new(
21816            config.workflow.temporary_branch_namespace(),
21817            BranchName::new("rustferry/goal3/builds/operation-2").expect("branch"),
21818        )
21819        .expect("temporary ref");
21820        assert_eq!(
21821            second.acquire_publication_lease(&TemporaryRefPublicationLeaseRequest {
21822                repository: &config.repository,
21823                operation_id: "operation-2",
21824                temporary_ref: &independent_ref,
21825            }),
21826            Ok(TemporaryRefPublicationLease::Acquired)
21827        );
21828        drop(first);
21829        assert_eq!(
21830            second.acquire_publication_lease(&request),
21831            Ok(TemporaryRefPublicationLease::Acquired)
21832        );
21833    }
21834
21835    #[test]
21836    fn git_snapshot_source_lease_is_cross_instance_and_exact_ref_scoped() {
21837        let root = tempdir().expect("root");
21838        let isolation = root.path().join("isolation");
21839        create_test_private_directory(&isolation).expect("isolation directory");
21840        let mut first = GitTemporaryRefPublisher::new(
21841            FakeGitRunner::default(),
21842            &isolation,
21843            "origin",
21844            "signing",
21845            Duration::from_secs(30),
21846        )
21847        .expect("first publisher");
21848        let mut second = GitTemporaryRefPublisher::new(
21849            FakeGitRunner::default(),
21850            &isolation,
21851            "origin",
21852            "signing",
21853            Duration::from_secs(30),
21854        )
21855        .expect("second publisher");
21856        let config = provider_config(all_authorized());
21857        let source_ref =
21858            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot source ref");
21859        let request = GitSnapshotSourcePublicationLeaseRequest {
21860            source_repository: &config.source_repository,
21861            source_ref: &source_ref,
21862        };
21863        assert_eq!(
21864            first.acquire_git_snapshot_source_lease(&request),
21865            Ok(TemporaryRefPublicationLease::Acquired)
21866        );
21867        assert_eq!(
21868            first.acquire_git_snapshot_source_lease(&request),
21869            Ok(TemporaryRefPublicationLease::AlreadyOwned)
21870        );
21871        assert_eq!(
21872            second.acquire_git_snapshot_source_lease(&request),
21873            Ok(TemporaryRefPublicationLease::HeldByOther)
21874        );
21875
21876        let other_source_ref = GitSnapshotSourceRef::for_operation("operation-2")
21877            .expect("independent snapshot source ref");
21878        assert_eq!(
21879            second.acquire_git_snapshot_source_lease(&GitSnapshotSourcePublicationLeaseRequest {
21880                source_repository: &config.source_repository,
21881                source_ref: &other_source_ref,
21882            },),
21883            Ok(TemporaryRefPublicationLease::Acquired)
21884        );
21885        assert_eq!(
21886            second.acquire_git_snapshot_source_lease(&GitSnapshotSourcePublicationLeaseRequest {
21887                source_repository: "https://github.com/shiroksh/other-public-source",
21888                source_ref: &source_ref,
21889            },),
21890            Ok(TemporaryRefPublicationLease::Acquired)
21891        );
21892        first.release_git_snapshot_source_lease(&request);
21893        assert_eq!(
21894            second.acquire_git_snapshot_source_lease(&request),
21895            Ok(TemporaryRefPublicationLease::Acquired)
21896        );
21897    }
21898
21899    #[test]
21900    fn git_publication_lease_scope_rejects_same_path_directory_replacement() {
21901        let root = tempdir().expect("root");
21902        let isolation = root.path().join("isolation");
21903        let moved = root.path().join("isolation-old");
21904        create_test_private_directory(&isolation).expect("isolation directory");
21905        let mut original = GitTemporaryRefPublisher::new(
21906            FakeGitRunner::default(),
21907            &isolation,
21908            "origin",
21909            "signing",
21910            Duration::from_secs(30),
21911        )
21912        .expect("original publisher");
21913        let original_scope = original
21914            .publication_lease_scope_sha256()
21915            .expect("scope")
21916            .to_owned();
21917        let config = provider_config(all_authorized());
21918        let temporary_ref = TemporaryGitRef::new(
21919            config.workflow.temporary_branch_namespace(),
21920            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
21921        )
21922        .expect("temporary ref");
21923
21924        #[cfg(windows)]
21925        {
21926            assert!(
21927                fs::rename(&isolation, &moved).is_err(),
21928                "retained directory handle must deny replacement before lease acquisition"
21929            );
21930            assert_eq!(
21931                original.publication_lease_scope_sha256(),
21932                Some(original_scope.as_str())
21933            );
21934            assert_eq!(
21935                original.acquire_publication_lease(&TemporaryRefPublicationLeaseRequest {
21936                    repository: &config.repository,
21937                    operation_id: "operation-1",
21938                    temporary_ref: &temporary_ref,
21939                }),
21940                Ok(TemporaryRefPublicationLease::Acquired)
21941            );
21942        }
21943
21944        #[cfg(not(windows))]
21945        {
21946            fs::rename(&isolation, &moved).expect("move original isolation directory");
21947            create_test_private_directory(&isolation).expect("replacement isolation directory");
21948            let replacement = GitTemporaryRefPublisher::new(
21949                FakeGitRunner::default(),
21950                &isolation,
21951                "origin",
21952                "signing",
21953                Duration::from_secs(30),
21954            )
21955            .expect("replacement publisher");
21956            assert_ne!(
21957                replacement.publication_lease_scope_sha256(),
21958                Some(original_scope.as_str())
21959            );
21960            assert_eq!(
21961                original.acquire_publication_lease(&TemporaryRefPublicationLeaseRequest {
21962                    repository: &config.repository,
21963                    operation_id: "operation-1",
21964                    temporary_ref: &temporary_ref,
21965                }),
21966                Err(TemporaryRefPublishError::LocalIsolationFailed)
21967            );
21968        }
21969    }
21970
21971    #[cfg(windows)]
21972    #[test]
21973    fn production_runner_writes_only_its_private_bare_object_database() {
21974        let git = PathBuf::from(r"C:\Program Files\Git\cmd\git.exe");
21975        if !git.is_file() {
21976            return;
21977        }
21978        let temporary = tempfile::tempdir().expect("fixture");
21979        let checkout = temporary.path().join("checkout");
21980        fs::create_dir(&checkout).expect("checkout directory");
21981        assert!(
21982            Command::new(&git)
21983                .args(["init", "--quiet"])
21984                .current_dir(&checkout)
21985                .status()
21986                .expect("git init")
21987                .success()
21988        );
21989        let caller_config = fs::read(checkout.join(".git/config")).expect("caller config");
21990        let caller_git = test_tree_snapshot(&checkout.join(".git"));
21991        let isolation = temporary.path().join("isolation");
21992        create_test_private_directory(&isolation).expect("private isolation");
21993        let mut runner = GitProcessRunner::new(&git, &isolation).expect("isolated runner");
21994
21995        let object = runner
21996            .execute(
21997                &GitInvocation::new(["hash-object", "-w", "--stdin"], Duration::from_secs(5))
21998                    .with_stdin(b"private object\n"),
21999            )
22000            .expect("private object write");
22001
22002        let object = parse_object_id(&object).expect("object ID");
22003        assert!(
22004            runner
22005                .repository
22006                .paths()
22007                .bare()
22008                .join("objects")
22009                .join(&object[..2])
22010                .join(&object[2..])
22011                .is_file()
22012        );
22013        assert_eq!(
22014            fs::read(checkout.join(".git/config")).expect("caller config after write"),
22015            caller_config
22016        );
22017        assert_eq!(test_tree_snapshot(&checkout.join(".git")), caller_git);
22018    }
22019
22020    #[test]
22021    fn git_executable_basename_is_platform_exact() {
22022        #[cfg(windows)]
22023        assert!(executable_basename_matches(
22024            Path::new(r"C:\Git\git.exe"),
22025            "git"
22026        ));
22027        #[cfg(not(windows))]
22028        assert!(executable_basename_matches(
22029            Path::new("/usr/bin/git"),
22030            "git"
22031        ));
22032        assert!(!executable_basename_matches(
22033            Path::new("/usr/bin/not-git"),
22034            "git"
22035        ));
22036    }
22037
22038    #[test]
22039    fn github_remote_identity_accepts_equivalent_https_and_ssh_transports() {
22040        let expected = "https://github.com/ShiroKSH/rust-and-iphone";
22041        for actual in [
22042            expected,
22043            "https://github.com/ShiroKSH/rust-and-iphone.git",
22044            "git@github.com:ShiroKSH/rust-and-iphone.git",
22045            "ssh://git@github.com/ShiroKSH/rust-and-iphone",
22046        ] {
22047            assert!(github_remote_matches(actual, expected), "{actual}");
22048        }
22049        for actual in [
22050            "https://github.com/attacker/rust-and-iphone",
22051            "git@github.com:attacker/rust-and-iphone.git",
22052            "ssh://git@github.example/ShiroKSH/rust-and-iphone",
22053            "https://github.com/ShiroKSH/rust-and-iphone/extra",
22054        ] {
22055            assert!(!github_remote_matches(actual, expected), "{actual}");
22056        }
22057    }
22058
22059    #[test]
22060    fn config_accepts_distinct_source_and_rejects_invalid_source_digest_and_worker_version() {
22061        let workflow = workflow_config();
22062        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
22063        let repository = Repository::new("ShiroKSH", "rustferry-signing").expect("repository");
22064        let split = GithubProviderConfig::new(
22065            repository.clone(),
22066            "https://github.com/shiroksh/rust-and-iphone",
22067            workflow.clone(),
22068            fingerprint.clone(),
22069            GithubPollingPolicy::secure_defaults(),
22070            all_authorized(),
22071            &Version::new(0, 1, 0),
22072            8,
22073        )
22074        .expect("distinct source and execution repositories");
22075        assert_eq!(split.repository(), &repository);
22076        assert_eq!(
22077            split.source_repository(),
22078            "https://github.com/shiroksh/rust-and-iphone"
22079        );
22080        assert_eq!(
22081            GithubProviderConfig::new(
22082                repository.clone(),
22083                "https://github.com/ShiroKSH/rust-and-iphone",
22084                workflow.clone(),
22085                fingerprint.clone(),
22086                GithubPollingPolicy::secure_defaults(),
22087                all_authorized(),
22088                &Version::new(0, 1, 0),
22089                8,
22090            ),
22091            Err(GithubProviderConfigError::InvalidSourceRepository)
22092        );
22093        assert_eq!(
22094            GithubProviderConfig::new(
22095                repository.clone(),
22096                "https://github.com/shiroksh/rust-and-iphone",
22097                workflow.clone(),
22098                WorkflowFingerprint::new("f".repeat(64)).expect("fingerprint"),
22099                GithubPollingPolicy::secure_defaults(),
22100                all_authorized(),
22101                &Version::new(0, 1, 0),
22102                8,
22103            ),
22104            Err(GithubProviderConfigError::WorkflowFingerprintMismatch)
22105        );
22106        assert_eq!(
22107            GithubProviderConfig::new(
22108                repository,
22109                "https://github.com/shiroksh/rust-and-iphone",
22110                workflow.clone(),
22111                WorkflowFingerprint::for_workflow(&generate_workflow(&workflow)),
22112                GithubPollingPolicy::secure_defaults(),
22113                all_authorized(),
22114                &Version::new(0, 2, 0),
22115                8,
22116            ),
22117            Err(GithubProviderConfigError::WorkerVersionMismatch)
22118        );
22119    }
22120
22121    #[test]
22122    fn provider_identity_binds_persisted_git_transports() {
22123        let base = same_repository_provider_config(all_authorized());
22124        let base_sha = provider_config_sha256(&base).expect("base identity");
22125        let https = GithubRemoteSnapshot::new(
22126            GithubGitEndpoint::parse("https://github.com/shiroksh/rust-and-iphone")
22127                .expect("HTTPS fetch"),
22128            GithubGitEndpoint::parse("https://github.com/shiroksh/rust-and-iphone")
22129                .expect("HTTPS push"),
22130        )
22131        .expect("HTTPS snapshot");
22132        let mixed = GithubRemoteSnapshot::new(
22133            GithubGitEndpoint::parse("https://github.com/shiroksh/rust-and-iphone")
22134                .expect("HTTPS fetch"),
22135            GithubGitEndpoint::parse("git@github.com:shiroksh/rust-and-iphone").expect("SSH push"),
22136        )
22137        .expect("mixed snapshot");
22138        #[cfg(unix)]
22139        let ssh = GithubRemoteSnapshot::new(
22140            GithubGitEndpoint::parse("git@github.com:shiroksh/rust-and-iphone").expect("SSH fetch"),
22141            GithubGitEndpoint::parse("git@github.com:shiroksh/rust-and-iphone").expect("SSH push"),
22142        )
22143        .expect("SSH snapshot");
22144        #[cfg(windows)]
22145        let https_sha = provider_config_sha256(
22146            &base
22147                .clone()
22148                .bind_git_endpoints(https.clone(), https.clone())
22149                .expect("HTTPS binding"),
22150        )
22151        .expect("HTTPS identity");
22152        #[cfg(unix)]
22153        assert_eq!(
22154            base.clone()
22155                .bind_git_endpoints(https.clone(), https.clone()),
22156            Err(GithubProviderConfigError::UnsupportedExecutionTransport)
22157        );
22158        #[cfg(unix)]
22159        assert_eq!(
22160            base.clone()
22161                .bind_git_endpoints(mixed.clone(), mixed.clone()),
22162            Err(GithubProviderConfigError::UnsupportedExecutionTransport)
22163        );
22164        #[cfg(windows)]
22165        let mixed_sha = provider_config_sha256(
22166            &base
22167                .bind_git_endpoints(mixed.clone(), mixed)
22168                .expect("mixed binding"),
22169        )
22170        .expect("mixed identity");
22171        #[cfg(unix)]
22172        let ssh_sha = provider_config_sha256(
22173            &base
22174                .bind_git_endpoints(ssh.clone(), ssh)
22175                .expect("SSH binding"),
22176        )
22177        .expect("SSH identity");
22178        #[cfg(windows)]
22179        assert_ne!(base_sha, https_sha);
22180        #[cfg(windows)]
22181        assert_ne!(https_sha, mixed_sha);
22182        #[cfg(unix)]
22183        assert_ne!(base_sha, ssh_sha);
22184    }
22185
22186    #[test]
22187    fn split_repository_artifact_context_keeps_execution_and_source_identities_separate() {
22188        let workflow = workflow_config();
22189        let fingerprint = WorkflowFingerprint::for_workflow(&generate_workflow(&workflow));
22190        let config = GithubProviderConfig::new(
22191            Repository::new("ShiroKSH", "rustferry-signing").expect("execution repository"),
22192            "https://github.com/shiroksh/rust-and-iphone",
22193            workflow,
22194            fingerprint,
22195            GithubPollingPolicy::new(3, Duration::from_millis(250)).expect("poll policy"),
22196            all_authorized(),
22197            &Version::new(0, 1, 0),
22198            8,
22199        )
22200        .expect("split repository config");
22201        let runner = FakeGhRunner::with([
22202            Ok(completed_run_row("success")),
22203            Ok(completed_run_row("success")),
22204        ]);
22205        let requests = Arc::clone(&runner.requests);
22206        let artifacts = CapturingArtifactStore::default();
22207        let contexts = Arc::clone(&artifacts.contexts);
22208        let provider = GithubBuildProvider::with_artifact_store_and_clock(
22209            config,
22210            transport(runner),
22211            FakePublisher::default(),
22212            artifacts,
22213            FixedClock(1_700_000_000_000),
22214        );
22215
22216        poll_ready(provider.submit(unsigned_request(), CancellationToken::new())).expect("submit");
22217        poll_ready(provider.events(
22218            EventRequest {
22219                job_id: "operation-1".to_owned(),
22220                after_sequence: None,
22221                limit: 100,
22222            },
22223            CancellationToken::new(),
22224        ))
22225        .expect("events");
22226
22227        let captured = contexts.lock().expect("contexts");
22228        let context = captured.first().expect("artifact context");
22229        assert_eq!(context.repository.owner(), "ShiroKSH");
22230        assert_eq!(context.repository.name(), "rustferry-signing");
22231        assert_eq!(context.execution_repository_id, None);
22232        assert_eq!(
22233            context.source_repository,
22234            "https://github.com/shiroksh/rust-and-iphone"
22235        );
22236        assert_eq!(
22237            context.request.source_repository.as_deref(),
22238            Some(context.source_repository.as_str())
22239        );
22240        assert_eq!(context.run.handle().head_sha(), &context.dispatch_revision);
22241        let requests = requests.lock().expect("requests");
22242        assert_eq!(requests.len(), 2);
22243        assert!(requests.iter().all(|request| {
22244            request
22245                .endpoint()
22246                .starts_with("/repos/ShiroKSH/rustferry-signing/actions/")
22247        }));
22248    }
22249
22250    #[test]
22251    fn production_doctor_rejects_trusted_tip_workflow_byte_mismatch() {
22252        let config = same_repository_provider_config(all_authorized());
22253        let workflow_path = config.workflow.filename().repository_path();
22254        let responses = [
22255            Ok(b"https://github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
22256            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
22257            Ok(format!(
22258                "{TRUSTED_TIP}\t{}\n",
22259                config.workflow.trusted_source_ref().as_str()
22260            )
22261            .into_bytes()),
22262            Ok(Vec::new()),
22263            Ok(b"name: unapproved\n".to_vec()),
22264        ];
22265        let directory = tempdir().expect("isolation directory");
22266        let mut publisher = GitTemporaryRefPublisher::new(
22267            FakeGitRunner::with(responses),
22268            directory.path(),
22269            "origin",
22270            "origin",
22271            Duration::from_secs(5),
22272        )
22273        .expect("publisher");
22274        let error = publisher
22275            .doctor(&TemporaryRefDoctorRequest {
22276                repository: &config.repository,
22277                source_repository: &config.source_repository,
22278                trusted_source_ref: config.workflow.trusted_source_ref(),
22279                temporary_branch_namespace: config.workflow.temporary_branch_namespace(),
22280                workflow_path: &workflow_path,
22281                workflow_fingerprint: &config.workflow_fingerprint,
22282            })
22283            .expect_err("unapproved trusted-tip workflow bytes");
22284        assert_eq!(error, TemporaryRefPublishError::WorkflowFingerprintMismatch);
22285    }
22286
22287    #[test]
22288    fn execution_workflow_verifier_reads_only_the_stable_execution_default_ref() {
22289        let config = workflow_dispatch_provider_config(all_authorized());
22290        let workflow = generate_workflow(&config.workflow);
22291        let source = GithubRemoteSnapshot::new(
22292            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rust-and-iphone.git")
22293                .expect("source fetch"),
22294            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rust-and-iphone.git")
22295                .expect("source push"),
22296        )
22297        .expect("source endpoints");
22298        let execution = GithubRemoteSnapshot::new(
22299            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rustferry-signing.git")
22300                .expect("execution fetch"),
22301            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rustferry-signing.git")
22302                .expect("execution push"),
22303        )
22304        .expect("execution endpoints");
22305        let default_ref = config.workflow.trusted_source_ref();
22306        let responses = [
22307            Ok(format!("{TRUSTED_TIP}\t{}\n", default_ref.as_str()).into_bytes()),
22308            Ok(Vec::new()),
22309            Ok(format!("{TRUSTED_TIP}\t{}\n", default_ref.as_str()).into_bytes()),
22310            Ok(workflow.yaml().as_bytes().to_vec()),
22311        ];
22312        let directory = tempdir().expect("isolation directory");
22313        let mut publisher = GitTemporaryRefPublisher::new_with_endpoints(
22314            FakeGitRunner::with(responses),
22315            directory.path(),
22316            &source,
22317            &execution,
22318            Duration::from_secs(5),
22319        )
22320        .expect("publisher");
22321
22322        publisher
22323            .verify_execution_workflow(&ExecutionWorkflowDoctorRequest {
22324                repository: &config.repository,
22325                default_branch_ref: default_ref,
22326                workflow_path: &config.workflow.filename().repository_path(),
22327                workflow_fingerprint: &config.workflow_fingerprint,
22328            })
22329            .expect("execution workflow proof");
22330
22331        let runner = publisher.into_runner();
22332        assert_eq!(runner.invocations.len(), 4);
22333        assert_eq!(
22334            runner
22335                .invocations
22336                .iter()
22337                .map(|invocation| invocation.network)
22338                .collect::<Vec<_>>(),
22339            [
22340                GitNetworkPolicy::HttpsWithCredentialManager,
22341                GitNetworkPolicy::HttpsWithCredentialManager,
22342                GitNetworkPolicy::HttpsWithCredentialManager,
22343                GitNetworkPolicy::Offline,
22344            ]
22345        );
22346        assert!(runner.invocations[..3].iter().all(|invocation| {
22347            invocation.arguments.iter().any(|argument| {
22348                argument == OsStr::new("https://github.com/shiroksh/rustferry-signing")
22349            }) && invocation.arguments.iter().all(|argument| {
22350                argument != OsStr::new("https://github.com/shiroksh/rust-and-iphone")
22351            })
22352        }));
22353        assert_eq!(
22354            runner.invocations[3].arguments.first(),
22355            Some(&OsString::from("show"))
22356        );
22357    }
22358
22359    #[test]
22360    fn execution_workflow_verifier_rejects_push_only_bytes() {
22361        let config = workflow_dispatch_provider_config(all_authorized());
22362        let source = GithubRemoteSnapshot::new(
22363            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rust-and-iphone.git")
22364                .expect("source fetch"),
22365            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rust-and-iphone.git")
22366                .expect("source push"),
22367        )
22368        .expect("source endpoints");
22369        let execution = GithubRemoteSnapshot::new(
22370            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rustferry-signing.git")
22371                .expect("execution fetch"),
22372            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rustferry-signing.git")
22373                .expect("execution push"),
22374        )
22375        .expect("execution endpoints");
22376        let default_ref = config.workflow.trusted_source_ref();
22377        let push_workflow = generate_workflow(&workflow_config());
22378        let responses = [
22379            Ok(format!("{TRUSTED_TIP}\t{}\n", default_ref.as_str()).into_bytes()),
22380            Ok(Vec::new()),
22381            Ok(format!("{TRUSTED_TIP}\t{}\n", default_ref.as_str()).into_bytes()),
22382            Ok(push_workflow.yaml().as_bytes().to_vec()),
22383        ];
22384        let directory = tempdir().expect("isolation directory");
22385        let mut publisher = GitTemporaryRefPublisher::new_with_endpoints(
22386            FakeGitRunner::with(responses),
22387            directory.path(),
22388            &source,
22389            &execution,
22390            Duration::from_secs(5),
22391        )
22392        .expect("publisher");
22393
22394        assert_eq!(
22395            publisher.verify_execution_workflow(&ExecutionWorkflowDoctorRequest {
22396                repository: &config.repository,
22397                default_branch_ref: default_ref,
22398                workflow_path: &config.workflow.filename().repository_path(),
22399                workflow_fingerprint: &config.workflow_fingerprint,
22400            }),
22401            Err(TemporaryRefPublishError::WorkflowFingerprintMismatch)
22402        );
22403    }
22404
22405    #[test]
22406    fn frozen_endpoints_skip_remote_config_and_select_exact_network_policies() {
22407        let config = same_repository_provider_config(all_authorized());
22408        let workflow_path = config.workflow.filename().repository_path();
22409        let workflow = generate_workflow(&config.workflow);
22410        let responses = [
22411            Ok(format!(
22412                "{TRUSTED_TIP}\t{}\n",
22413                config.workflow.trusted_source_ref().as_str()
22414            )
22415            .into_bytes()),
22416            Ok(Vec::new()),
22417            Ok(workflow.yaml().as_bytes().to_vec()),
22418            Ok(Vec::new()),
22419            Ok(Vec::new()),
22420            Ok(Vec::new()),
22421            Ok(Vec::new()),
22422        ];
22423        let endpoints = GithubRemoteSnapshot::new(
22424            GithubGitEndpoint::parse("https://github.com/ShiroKSH/rust-and-iphone.git")
22425                .expect("fetch endpoint"),
22426            GithubGitEndpoint::parse("git@github.com:ShiroKSH/rust-and-iphone.git")
22427                .expect("push endpoint"),
22428        )
22429        .expect("endpoint snapshot");
22430        let directory = tempdir().expect("isolation directory");
22431        let mut publisher = GitTemporaryRefPublisher::new_with_endpoints(
22432            FakeGitRunner::with(responses),
22433            directory.path(),
22434            &endpoints,
22435            &endpoints,
22436            Duration::from_secs(5),
22437        )
22438        .expect("publisher");
22439
22440        publisher
22441            .doctor(&TemporaryRefDoctorRequest {
22442                repository: &config.repository,
22443                source_repository: &config.source_repository,
22444                trusted_source_ref: config.workflow.trusted_source_ref(),
22445                temporary_branch_namespace: config.workflow.temporary_branch_namespace(),
22446                workflow_path: &workflow_path,
22447                workflow_fingerprint: &config.workflow_fingerprint,
22448            })
22449            .expect("frozen endpoint readiness");
22450
22451        let runner = publisher.into_runner();
22452        assert_eq!(runner.invocations.len(), 7);
22453        assert!(
22454            runner.invocations.iter().all(|invocation| {
22455                invocation.arguments.first() != Some(&OsString::from("remote"))
22456            })
22457        );
22458        assert_eq!(
22459            runner
22460                .invocations
22461                .iter()
22462                .map(|invocation| invocation.network)
22463                .collect::<Vec<_>>(),
22464            [
22465                GitNetworkPolicy::HttpsWithCredentialManager,
22466                GitNetworkPolicy::HttpsWithCredentialManager,
22467                GitNetworkPolicy::Offline,
22468                GitNetworkPolicy::HttpsWithCredentialManager,
22469                GitNetworkPolicy::GithubSsh,
22470                GitNetworkPolicy::GithubSsh,
22471                GitNetworkPolicy::GithubSsh,
22472            ]
22473        );
22474        assert!(runner.invocations[0].arguments.contains(&OsString::from(
22475            "https://github.com/shiroksh/rust-and-iphone"
22476        )));
22477        assert_eq!(
22478            runner.invocations[5].arguments.first(),
22479            Some(&OsString::from("push"))
22480        );
22481        assert!(
22482            runner.invocations[5]
22483                .arguments
22484                .contains(&OsString::from("--dry-run"))
22485        );
22486    }
22487
22488    #[test]
22489    fn git_publisher_rejects_a_mismatched_push_url_before_network_access() {
22490        let config = same_repository_provider_config(all_authorized());
22491        let workflow_path = config.workflow.filename().repository_path();
22492        let runner = FakeGitRunner::with([
22493            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
22494            Ok(b"git@github.com:attacker/rust-and-iphone.git\n".to_vec()),
22495        ]);
22496        let directory = tempdir().expect("isolation directory");
22497        let mut publisher = GitTemporaryRefPublisher::new(
22498            runner,
22499            directory.path(),
22500            "origin",
22501            "origin",
22502            Duration::from_secs(5),
22503        )
22504        .expect("publisher");
22505
22506        let error = publisher
22507            .doctor(&TemporaryRefDoctorRequest {
22508                repository: &config.repository,
22509                source_repository: &config.source_repository,
22510                trusted_source_ref: config.workflow.trusted_source_ref(),
22511                temporary_branch_namespace: config.workflow.temporary_branch_namespace(),
22512                workflow_path: &workflow_path,
22513                workflow_fingerprint: &config.workflow_fingerprint,
22514            })
22515            .expect_err("mismatched push URL");
22516        assert_eq!(error, TemporaryRefPublishError::RemoteMismatch);
22517        let runner = publisher.into_runner();
22518        assert_eq!(runner.invocations.len(), 2);
22519        assert!(runner.responses.is_empty());
22520    }
22521
22522    #[test]
22523    fn signed_push_doctor_accepts_approved_branch_only_workflow_without_registration() {
22524        let report = signed_doctor_report(
22525            Ok(signing_environment_row(false, true, true)),
22526            Ok(exact_signing_policy_row()),
22527            Ok(exact_signing_secret_rows()),
22528        );
22529        assert!(report.ready);
22530        assert!(report.checks.iter().any(|check| {
22531            check.code == "github.trusted_source" && check.status == ProviderCheckStatus::Ready
22532        }));
22533        assert!(
22534            report
22535                .checks
22536                .iter()
22537                .all(|check| check.code != "github.workflow_registration")
22538        );
22539    }
22540
22541    #[test]
22542    fn push_doctor_rejects_repository_scoped_credentials() {
22543        let provider = GithubBuildProvider::with_artifact_store_and_clock(
22544            provider_config(all_authorized()),
22545            transport(FakeGhRunner::with([
22546                Err(GhExecutionError::CommandFailed { exit_code: Some(1) }),
22547                Ok(b"991\tU2hpcm9LU0gvcnVzdGZlcnJ5LXNpZ25pbmc=\n".to_vec()),
22548                Ok(private_repository_row()),
22549                Ok(public_source_repository_row()),
22550            ])),
22551            FakePublisher::default(),
22552            DoctorArtifactStore,
22553            FixedClock(1_700_000_000_000),
22554        );
22555
22556        let report = poll_ready(provider.doctor(
22557            ProviderDoctorRequest {
22558                protocol_version: CURRENT_PROTOCOL_VERSION,
22559                operation_id: "push-repository-credential-doctor".to_owned(),
22560                require_signing: false,
22561            },
22562            CancellationToken::new(),
22563        ))
22564        .expect("doctor report");
22565        assert!(!report.ready);
22566        assert!(report.checks.iter().any(|check| {
22567            check.code == "github.authentication" && check.status == ProviderCheckStatus::Error
22568        }));
22569    }
22570
22571    #[test]
22572    fn workflow_dispatch_doctor_accepts_exact_repository_scoped_credentials() {
22573        let provider = GithubBuildProvider::with_artifact_store_and_clock(
22574            workflow_dispatch_provider_config(all_authorized()),
22575            transport(FakeGhRunner::with([
22576                Err(GhExecutionError::CommandFailed { exit_code: Some(1) }),
22577                Ok(b"991\tU2hpcm9LU0gvcnVzdGZlcnJ5LXNpZ25pbmc=\n".to_vec()),
22578                Ok(private_repository_row()),
22579                Ok(public_source_repository_row()),
22580                Ok(workflow_dispatch_registration_row()),
22581            ])),
22582            FakePublisher::default(),
22583            DoctorArtifactStore,
22584            FixedClock(1_700_000_000_000),
22585        );
22586
22587        let report = poll_ready(provider.doctor(
22588            ProviderDoctorRequest {
22589                protocol_version: CURRENT_PROTOCOL_VERSION,
22590                operation_id: "dispatch-repository-credential-doctor".to_owned(),
22591                require_signing: false,
22592            },
22593            CancellationToken::new(),
22594        ))
22595        .expect("doctor report");
22596        assert!(report.checks.iter().any(|check| {
22597            check.code == "github.authentication" && check.status == ProviderCheckStatus::Ready
22598        }));
22599        assert!(report.checks.iter().any(|check| {
22600            check.code == "github.workflow_dispatch_default_workflow"
22601                && check.status == ProviderCheckStatus::Ready
22602        }));
22603    }
22604
22605    #[test]
22606    fn provider_doctor_rejects_a_private_source_repository() {
22607        let provider = GithubBuildProvider::with_artifact_store_and_clock(
22608            provider_config(all_authorized()),
22609            transport(FakeGhRunner::with([
22610                Ok(authenticated_user_row()),
22611                Ok(b"991\tShiroKSH/rustferry-signing\ttrue\tfalse\tfalse\tmain\n".to_vec()),
22612                Ok(b"992\tShiroKSH/rust-and-iphone\ttrue\tfalse\tfalse\tmain\n".to_vec()),
22613            ])),
22614            FakePublisher::default(),
22615            DoctorArtifactStore,
22616            FixedClock(1_700_000_000_000),
22617        );
22618        let report = poll_ready(provider.doctor(
22619            ProviderDoctorRequest {
22620                protocol_version: CURRENT_PROTOCOL_VERSION,
22621                operation_id: "private-source-doctor".to_owned(),
22622                require_signing: false,
22623            },
22624            CancellationToken::new(),
22625        ))
22626        .expect("doctor report");
22627        assert!(!report.ready);
22628        assert!(report.checks.iter().any(|check| {
22629            check.code == "github.source_repository_visibility"
22630                && check.status == ProviderCheckStatus::Error
22631        }));
22632    }
22633
22634    #[test]
22635    fn signed_doctor_requires_distinct_stable_repository_ids() {
22636        let provider = GithubBuildProvider::with_artifact_store_and_clock(
22637            provider_config(all_authorized()),
22638            transport(FakeGhRunner::with([
22639                Ok(authenticated_user_row()),
22640                Ok(private_repository_row()),
22641                Ok(b"991\tShiroKSH/rust-and-iphone\tfalse\tfalse\tfalse\tmain\n".to_vec()),
22642                Ok(b"true\n".to_vec()),
22643                Ok(signing_environment_row(false, true, true)),
22644                Ok(exact_signing_policy_row()),
22645                Ok(exact_signing_secret_rows()),
22646            ])),
22647            FakePublisher::default(),
22648            DoctorArtifactStore,
22649            FixedClock(1_700_000_000_000),
22650        );
22651        let report = poll_ready(provider.doctor(
22652            ProviderDoctorRequest {
22653                protocol_version: CURRENT_PROTOCOL_VERSION,
22654                operation_id: "same-repository-id-doctor".to_owned(),
22655                require_signing: true,
22656            },
22657            CancellationToken::new(),
22658        ))
22659        .expect("doctor report");
22660
22661        assert!(!report.ready);
22662        assert!(report.checks.iter().any(|check| {
22663            check.code == "github.signing_repository_distinct"
22664                && check.status == ProviderCheckStatus::Error
22665        }));
22666    }
22667
22668    #[test]
22669    fn signed_doctor_requires_github_actions_to_be_enabled() {
22670        let report = signed_doctor_report_with_repository_policy(
22671            true,
22672            Ok(b"false\n".to_vec()),
22673            Ok(signing_environment_row(false, true, true)),
22674            Ok(exact_signing_policy_row()),
22675            Ok(exact_signing_secret_rows()),
22676        );
22677
22678        assert!(!report.ready);
22679        assert!(report.checks.iter().any(|check| {
22680            check.code == "github.actions.enabled" && check.status == ProviderCheckStatus::Error
22681        }));
22682    }
22683
22684    #[test]
22685    fn workflow_dispatch_signing_doctor_requires_an_active_exact_registration() {
22686        let active = workflow_dispatch_doctor_report(Ok(format!(
22687            "17\t{WORKFLOW_PATH_BASE64}\tactive\n"
22688        )
22689        .into_bytes()));
22690        assert!(active.ready);
22691        assert!(active.checks.iter().any(|check| {
22692            check.code == "github.workflow_registration"
22693                && check.status == ProviderCheckStatus::Ready
22694        }));
22695
22696        let inactive = workflow_dispatch_doctor_report(Ok(format!(
22697            "17\t{WORKFLOW_PATH_BASE64}\tdisabled_manually\n"
22698        )
22699        .into_bytes()));
22700        assert!(!inactive.ready);
22701        assert!(inactive.checks.iter().any(|check| {
22702            check.code == "github.workflow_registration"
22703                && check.status == ProviderCheckStatus::Error
22704        }));
22705    }
22706
22707    #[test]
22708    fn signed_doctor_accepts_attempt_scoped_cleanup_evidence_after_other_checks_are_ready() {
22709        let report = signed_doctor_report(
22710            Ok(signing_environment_row(false, true, true)),
22711            Ok(exact_signing_policy_row()),
22712            Ok(exact_signing_secret_rows()),
22713        );
22714
22715        assert!(report.ready);
22716        for code in [
22717            "github.signing_repository_visibility",
22718            "github.signing_repository_distinct",
22719            "github.actions.enabled",
22720            "github.signing_environment",
22721            "github.signing_branch_policy",
22722            "github.signing_secrets",
22723            "github.signing_environment.reviewers",
22724        ] {
22725            assert!(
22726                report.checks.iter().any(|check| {
22727                    check.code == code && check.status == ProviderCheckStatus::Ready
22728                })
22729            );
22730        }
22731        assert!(report.checks.iter().any(|check| {
22732            check.code == "github.signing_cleanup_evidence"
22733                && check.status == ProviderCheckStatus::Ready
22734        }));
22735    }
22736
22737    #[test]
22738    fn multi_profile_secret_metadata_requires_the_exact_configured_set() {
22739        let (_request, names) = multi_profile_request();
22740        let config = provider_config_with_secret_names(all_authorized(), names);
22741        let exact_rows = format!(
22742            "{CERTIFICATE_P12_BASE64}\n{CERTIFICATE_PASSWORD_BASE64}\n{PROVISIONING_PROFILE_BASE64}\n{WIDGET_PROFILE_BASE64}\n"
22743        )
22744        .into_bytes();
22745        let mut checks = Vec::new();
22746        append_signing_secret_check(
22747            &mut checks,
22748            &mut transport(FakeGhRunner::with([Ok(exact_rows)])),
22749            &config,
22750        );
22751        assert!(checks.iter().any(|check| {
22752            check.code == "github.signing_secrets" && check.status == ProviderCheckStatus::Ready
22753        }));
22754
22755        let mut checks = Vec::new();
22756        append_signing_secret_check(
22757            &mut checks,
22758            &mut transport(FakeGhRunner::with([Ok(exact_signing_secret_rows())])),
22759            &config,
22760        );
22761        assert!(checks.iter().any(|check| {
22762            check.code == "github.signing_secrets" && check.status == ProviderCheckStatus::Error
22763        }));
22764    }
22765
22766    #[test]
22767    fn signed_doctor_rejects_a_public_execution_repository() {
22768        let report = signed_doctor_report_with_visibility(
22769            false,
22770            Ok(signing_environment_row(false, true, true)),
22771            Ok(exact_signing_policy_row()),
22772            Ok(exact_signing_secret_rows()),
22773        );
22774
22775        assert!(!report.ready);
22776        assert!(report.checks.iter().any(|check| {
22777            check.code == "github.signing_repository_visibility"
22778                && check.status == ProviderCheckStatus::Error
22779        }));
22780    }
22781
22782    #[test]
22783    fn signed_doctor_requires_server_side_reviewer_approval() {
22784        let report = signed_doctor_report(
22785            Ok(signing_environment_row(false, true, false)),
22786            Ok(exact_signing_policy_row()),
22787            Ok(exact_signing_secret_rows()),
22788        );
22789
22790        assert!(!report.ready);
22791        assert!(report.checks.iter().any(|check| {
22792            check.code == "github.signing_environment.reviewers"
22793                && check.status == ProviderCheckStatus::Error
22794        }));
22795    }
22796
22797    #[test]
22798    fn signed_doctor_fails_closed_on_missing_or_mismatched_production_evidence() {
22799        let cases = [
22800            (
22801                "environment API unavailable",
22802                Err(GhExecutionError::CommandFailed {
22803                    exit_code: Some(1),
22804                }),
22805                Ok(exact_signing_policy_row()),
22806                Ok(exact_signing_secret_rows()),
22807                "github.signing_environment",
22808            ),
22809            (
22810                "environment policy flags",
22811                Ok(signing_environment_row(true, true, true)),
22812                Ok(exact_signing_policy_row()),
22813                Ok(exact_signing_secret_rows()),
22814                "github.signing_environment",
22815            ),
22816            (
22817                "deployment branch policy",
22818                Ok(signing_environment_row(false, true, true)),
22819                Ok(b"361471\tbWFpbg==\n".to_vec()),
22820                Ok(exact_signing_secret_rows()),
22821                "github.signing_branch_policy",
22822            ),
22823            (
22824                "unexpected deployment branch policy",
22825                Ok(signing_environment_row(false, true, true)),
22826                Ok(format!("361471\t{SIGNING_BRANCH_POLICY_BASE64}\n361472\tbWFpbg==\n")
22827                    .into_bytes()),
22828                Ok(exact_signing_secret_rows()),
22829                "github.signing_branch_policy",
22830            ),
22831            (
22832                "missing signing secret",
22833                Ok(signing_environment_row(false, true, true)),
22834                Ok(exact_signing_policy_row()),
22835                Ok(format!("{CERTIFICATE_P12_BASE64}\n{CERTIFICATE_PASSWORD_BASE64}\n")
22836                    .into_bytes()),
22837                "github.signing_secrets",
22838            ),
22839            (
22840                "unexpected signing secret",
22841                Ok(signing_environment_row(false, true, true)),
22842                Ok(exact_signing_policy_row()),
22843                Ok(format!(
22844                    "{CERTIFICATE_P12_BASE64}\n{CERTIFICATE_PASSWORD_BASE64}\n{PROVISIONING_PROFILE_BASE64}\nRVhUUkE=\n"
22845                )
22846                .into_bytes()),
22847                "github.signing_secrets",
22848            ),
22849        ];
22850
22851        for (name, environment, policies, secrets, expected_error) in cases {
22852            let report = signed_doctor_report(environment, policies, secrets);
22853            assert!(!report.ready, "accepted {name}");
22854            assert!(
22855                report.checks.iter().any(|check| {
22856                    check.code == expected_error && check.status == ProviderCheckStatus::Error
22857                }),
22858                "missing error for {name}"
22859            );
22860        }
22861    }
22862
22863    #[test]
22864    fn strict_dispatch_manifest_rejects_unknown_fields() {
22865        let config = provider_config(all_authorized());
22866        let temporary_ref = TemporaryGitRef::new(
22867            config.workflow.temporary_branch_namespace(),
22868            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
22869        )
22870        .expect("temporary ref");
22871        let manifest = GithubDispatchManifest::new(&config, &temporary_ref, valid_request());
22872        manifest.validate_for(&config).expect("valid manifest");
22873        assert!(github_remote_matches(
22874            &manifest.execution_repository,
22875            &repository_url(&config.repository)
22876        ));
22877        let mut wrong_execution = manifest.clone();
22878        wrong_execution.execution_repository =
22879            "https://github.com/shiroksh/other-signing".to_owned();
22880        assert!(wrong_execution.validate_for(&config).is_err());
22881        let mut value = serde_json::to_value(manifest).expect("manifest value");
22882        value
22883            .as_object_mut()
22884            .expect("object")
22885            .insert("future_shell".to_owned(), serde_json::json!("cargo build"));
22886        assert!(serde_json::from_value::<GithubDispatchManifest>(value).is_err());
22887    }
22888
22889    #[test]
22890    fn operation_ids_must_be_single_safe_ref_segments() {
22891        for invalid in [
22892            "",
22893            "-job",
22894            "job/child",
22895            "job:child",
22896            "job..child",
22897            "job.lock",
22898        ] {
22899            assert!(
22900                validate_ref_segment(invalid).is_err(),
22901                "accepted {invalid:?}"
22902            );
22903        }
22904        assert!(validate_ref_segment("operation-1.test_case").is_ok());
22905    }
22906
22907    #[test]
22908    fn unauthorized_git_publication_executes_nothing() {
22909        let directory = tempdir().expect("isolation directory");
22910        let runner = FakeGitRunner::default();
22911        let mut publisher = GitTemporaryRefPublisher::new(
22912            runner,
22913            directory.path(),
22914            "origin",
22915            "origin",
22916            Duration::from_secs(5),
22917        )
22918        .expect("publisher");
22919        let config = provider_config(all_authorized());
22920        let workflow = generate_workflow(&config.workflow);
22921        let temporary_ref = TemporaryGitRef::new(
22922            config.workflow.temporary_branch_namespace(),
22923            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
22924        )
22925        .expect("temporary ref");
22926        let error = publisher
22927            .publish(&TemporaryRefPublishRequest {
22928                repository: &config.repository,
22929                source_repository: &config.source_repository,
22930                trusted_source_ref: config.workflow.trusted_source_ref(),
22931                source_revision: &CommitSha::new(SOURCE_SHA).expect("source"),
22932                snapshot_source_ref: None,
22933                temporary_ref: &temporary_ref,
22934                workflow_path: workflow.path(),
22935                workflow_bytes: workflow.yaml().as_bytes(),
22936                workflow_fingerprint: &config.workflow_fingerprint,
22937                manifest_bytes: b"{}\n",
22938                operation_id: "operation-1",
22939                created_at_ms: 1_700_000_000_000,
22940                authorized: false,
22941            })
22942            .expect_err("authorization required");
22943        assert_eq!(error.error(), TemporaryRefPublishError::Unauthorized);
22944        assert!(publisher.into_runner().invocations.is_empty());
22945    }
22946
22947    #[test]
22948    fn git_snapshot_source_publication_is_create_only_on_the_full_custom_ref() {
22949        let directory = tempdir().expect("isolation directory");
22950        let mut publisher = GitTemporaryRefPublisher::new(
22951            FakeGitRunner::with(snapshot_source_mutation_git_responses(Some(SOURCE_SHA))),
22952            directory.path(),
22953            "source",
22954            "execution",
22955            Duration::from_secs(5),
22956        )
22957        .expect("publisher");
22958        let config = provider_config(all_authorized());
22959        let source_ref =
22960            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot source ref");
22961        let commit = GitSha1ObjectId::new(SOURCE_SHA).expect("snapshot commit");
22962        assert_eq!(
22963            publisher
22964                .publish_git_snapshot_source(&GitSnapshotSourcePublishRequest {
22965                    source_repository: &config.source_repository,
22966                    source_ref: &source_ref,
22967                    commit: &commit,
22968                    authorized: true,
22969                })
22970                .expect("snapshot source publication"),
22971            GitSnapshotSourcePublication::Exact
22972        );
22973        let runner = publisher.into_runner();
22974        assert!(runner.responses.is_empty());
22975        let push = runner
22976            .invocations
22977            .iter()
22978            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("push")))
22979            .expect("source push");
22980        assert!(push.arguments.contains(&OsString::from("--no-verify")));
22981        assert!(push.arguments.contains(&OsString::from("--atomic")));
22982        assert!(push.arguments.contains(&OsString::from(format!(
22983            "--force-with-lease={}:",
22984            source_ref.as_str()
22985        ))));
22986        assert!(push.arguments.contains(&OsString::from(format!(
22987            "{}:{}",
22988            commit.as_str(),
22989            source_ref.as_str()
22990        ))));
22991        assert!(
22992            push.arguments
22993                .contains(&OsString::from("git@github.com:shiroksh/rust-and-iphone"))
22994        );
22995        assert!(
22996            !push
22997                .arguments
22998                .iter()
22999                .any(|argument| { argument == "git@github.com:shiroksh/rustferry-signing" })
23000        );
23001    }
23002
23003    #[test]
23004    fn git_snapshot_source_deletion_uses_the_exact_expected_commit_lease() {
23005        let directory = tempdir().expect("isolation directory");
23006        let mut publisher = GitTemporaryRefPublisher::new(
23007            FakeGitRunner::with(snapshot_source_mutation_git_responses(None)),
23008            directory.path(),
23009            "source",
23010            "execution",
23011            Duration::from_secs(5),
23012        )
23013        .expect("publisher");
23014        let config = provider_config(all_authorized());
23015        let source_ref =
23016            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot source ref");
23017        let commit = GitSha1ObjectId::new(SOURCE_SHA).expect("snapshot commit");
23018        publisher
23019            .delete_git_snapshot_source(&GitSnapshotSourceDeleteRequest {
23020                source_repository: &config.source_repository,
23021                source_ref: &source_ref,
23022                expected_commit: &commit,
23023                authorized: true,
23024            })
23025            .expect("exact source deletion");
23026        let runner = publisher.into_runner();
23027        assert!(runner.responses.is_empty());
23028        let push = runner
23029            .invocations
23030            .iter()
23031            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("push")))
23032            .expect("source deletion push");
23033        assert!(push.arguments.contains(&OsString::from(format!(
23034            "--force-with-lease={}:{}",
23035            source_ref.as_str(),
23036            commit.as_str()
23037        ))));
23038        assert!(
23039            push.arguments
23040                .contains(&OsString::from(format!(":{}", source_ref.as_str())))
23041        );
23042        assert!(push.arguments.contains(&OsString::from("--no-verify")));
23043    }
23044
23045    #[test]
23046    fn git_snapshot_source_deletion_rejects_a_foreign_tip_without_claiming_absence() {
23047        let directory = tempdir().expect("isolation directory");
23048        let mut publisher = GitTemporaryRefPublisher::new(
23049            FakeGitRunner::with(snapshot_source_mutation_git_responses(Some(DISPATCH_SHA))),
23050            directory.path(),
23051            "source",
23052            "execution",
23053            Duration::from_secs(5),
23054        )
23055        .expect("publisher");
23056        let config = provider_config(all_authorized());
23057        let source_ref =
23058            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot source ref");
23059        let commit = GitSha1ObjectId::new(SOURCE_SHA).expect("snapshot commit");
23060        assert_eq!(
23061            publisher.delete_git_snapshot_source(&GitSnapshotSourceDeleteRequest {
23062                source_repository: &config.source_repository,
23063                source_ref: &source_ref,
23064                expected_commit: &commit,
23065                authorized: true,
23066            }),
23067            Err(TemporaryRefPublishError::GitSnapshotRefConflict)
23068        );
23069        let runner = publisher.into_runner();
23070        let push = runner
23071            .invocations
23072            .iter()
23073            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("push")))
23074            .expect("lease-protected deletion attempt");
23075        assert!(push.arguments.contains(&OsString::from(format!(
23076            "--force-with-lease={}:{}",
23077            source_ref.as_str(),
23078            commit.as_str()
23079        ))));
23080    }
23081
23082    #[test]
23083    fn git_prepare_binds_deterministic_commit_without_remote_mutation() {
23084        let full_trusted_ref = "refs/heads/goal3/macless-iphone-builds";
23085        let config = provider_config(all_authorized());
23086        let workflow = generate_workflow(&config.workflow);
23087        let responses = [
23088            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
23089            Ok(b"ssh://git@github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
23090            Ok(b"git@github.com:ShiroKSH/rustferry-signing.git\n".to_vec()),
23091            Ok(b"ssh://git@github.com/ShiroKSH/rustferry-signing\n".to_vec()),
23092            Ok(format!("{TRUSTED_TIP}\t{full_trusted_ref}\n").into_bytes()),
23093            Ok(Vec::new()),
23094            Ok(workflow.yaml().as_bytes().to_vec()),
23095            Ok(Vec::new()),
23096            Ok(Vec::new()),
23097            Ok(Vec::new()),
23098            Ok(Vec::new()),
23099            Ok(format!("{WORKFLOW_BLOB}\n").into_bytes()),
23100            Ok(format!("{MANIFEST_BLOB}\n").into_bytes()),
23101            Ok(Vec::new()),
23102            Ok(Vec::new()),
23103            Ok(format!("{TREE_SHA}\n").into_bytes()),
23104            Ok(format!("{DISPATCH_SHA}\n").into_bytes()),
23105        ];
23106        let directory = tempdir().expect("isolation directory");
23107        let mut publisher = GitTemporaryRefPublisher::new(
23108            FakeGitRunner::with(responses),
23109            directory.path(),
23110            "source",
23111            "execution",
23112            Duration::from_secs(5),
23113        )
23114        .expect("publisher");
23115        let temporary_ref = TemporaryGitRef::new(
23116            config.workflow.temporary_branch_namespace(),
23117            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23118        )
23119        .expect("temporary ref");
23120        let prepared = publisher
23121            .prepare(&TemporaryRefPublishRequest {
23122                repository: &config.repository,
23123                source_repository: &config.source_repository,
23124                trusted_source_ref: config.workflow.trusted_source_ref(),
23125                source_revision: &CommitSha::new(SOURCE_SHA).expect("source"),
23126                snapshot_source_ref: None,
23127                temporary_ref: &temporary_ref,
23128                workflow_path: workflow.path(),
23129                workflow_bytes: workflow.yaml().as_bytes(),
23130                workflow_fingerprint: &config.workflow_fingerprint,
23131                manifest_bytes: b"{\"schema_version\":1}\n",
23132                operation_id: "operation-1",
23133                created_at_ms: 1_700_000_000_000,
23134                authorized: true,
23135            })
23136            .expect("read-only preparation")
23137            .expect("prepared commit");
23138        assert_eq!(prepared.commit().as_str(), DISPATCH_SHA);
23139        let runner = publisher.into_runner();
23140        assert!(runner.responses.is_empty());
23141        assert!(!runner.invocations.iter().any(|invocation| {
23142            invocation
23143                .arguments
23144                .first()
23145                .and_then(|argument| argument.to_str())
23146                == Some("push")
23147        }));
23148    }
23149
23150    #[test]
23151    #[allow(
23152        clippy::too_many_lines,
23153        reason = "the regression proves split-repository custom-ref trust and the absence of ordinary ancestry fallback"
23154    )]
23155    fn git_snapshot_prepare_fetches_the_exact_custom_ref_from_the_source_repository() {
23156        let config = provider_config(all_authorized());
23157        let workflow = generate_workflow(&config.workflow);
23158        let directory = tempdir().expect("isolation directory");
23159        let mut publisher = GitTemporaryRefPublisher::new(
23160            FakeGitRunner::with(successful_snapshot_prepare_git_responses(
23161                workflow.yaml().as_bytes(),
23162                SOURCE_SHA,
23163            )),
23164            directory.path(),
23165            "source",
23166            "execution",
23167            Duration::from_secs(5),
23168        )
23169        .expect("publisher");
23170        let temporary_ref = TemporaryGitRef::new(
23171            config.workflow.temporary_branch_namespace(),
23172            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23173        )
23174        .expect("temporary ref");
23175        let source_revision = CommitSha::new(SOURCE_SHA).expect("source revision");
23176        let snapshot_ref =
23177            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot ref");
23178        let prepared = publisher
23179            .prepare(&TemporaryRefPublishRequest {
23180                repository: &config.repository,
23181                source_repository: &config.source_repository,
23182                trusted_source_ref: config.workflow.trusted_source_ref(),
23183                source_revision: &source_revision,
23184                snapshot_source_ref: Some(&snapshot_ref),
23185                temporary_ref: &temporary_ref,
23186                workflow_path: workflow.path(),
23187                workflow_bytes: workflow.yaml().as_bytes(),
23188                workflow_fingerprint: &config.workflow_fingerprint,
23189                manifest_bytes: b"{\"schema_version\":1}\n",
23190                operation_id: "operation-1",
23191                created_at_ms: 1_700_000_000_000,
23192                authorized: true,
23193            })
23194            .expect("snapshot dispatch preparation")
23195            .expect("prepared dispatch");
23196        assert_eq!(prepared.commit().as_str(), DISPATCH_SHA);
23197        let runner = publisher.into_runner();
23198        assert!(runner.responses.is_empty());
23199        assert!(!runner.invocations.iter().any(|invocation| {
23200            invocation.arguments.first() == Some(&OsString::from("merge-base"))
23201        }));
23202        assert_eq!(
23203            runner
23204                .invocations
23205                .iter()
23206                .filter(|invocation| {
23207                    invocation.arguments.first() == Some(&OsString::from("ls-remote"))
23208                        && invocation.arguments.last()
23209                            == Some(&OsString::from(snapshot_ref.as_str()))
23210                })
23211                .count(),
23212            2
23213        );
23214        let snapshot_fetch = runner
23215            .invocations
23216            .iter()
23217            .find(|invocation| {
23218                invocation.arguments.first() == Some(&OsString::from("fetch"))
23219                    && invocation.arguments.last() == Some(&OsString::from(snapshot_ref.as_str()))
23220            })
23221            .expect("snapshot source fetch");
23222        assert!(
23223            snapshot_fetch
23224                .arguments
23225                .contains(&OsString::from("git@github.com:shiroksh/rust-and-iphone"))
23226        );
23227        let temporary_ref_lookup = runner
23228            .invocations
23229            .iter()
23230            .find(|invocation| {
23231                invocation.arguments.first() == Some(&OsString::from("ls-remote"))
23232                    && invocation.arguments.last()
23233                        == Some(&OsString::from(
23234                            "refs/heads/rustferry/goal3/builds/operation-1",
23235                        ))
23236            })
23237            .expect("execution temporary-ref lookup");
23238        assert!(
23239            temporary_ref_lookup
23240                .arguments
23241                .contains(&OsString::from("git@github.com:shiroksh/rustferry-signing"))
23242        );
23243    }
23244
23245    #[test]
23246    fn git_snapshot_prepare_rejects_custom_ref_drift_before_object_writes() {
23247        let config = provider_config(all_authorized());
23248        let workflow = generate_workflow(&config.workflow);
23249        let directory = tempdir().expect("isolation directory");
23250        let mut publisher = GitTemporaryRefPublisher::new(
23251            FakeGitRunner::with(successful_snapshot_prepare_git_responses(
23252                workflow.yaml().as_bytes(),
23253                DISPATCH_SHA,
23254            )),
23255            directory.path(),
23256            "source",
23257            "execution",
23258            Duration::from_secs(5),
23259        )
23260        .expect("publisher");
23261        let temporary_ref = TemporaryGitRef::new(
23262            config.workflow.temporary_branch_namespace(),
23263            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23264        )
23265        .expect("temporary ref");
23266        let source_revision = CommitSha::new(SOURCE_SHA).expect("source revision");
23267        let snapshot_ref =
23268            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot ref");
23269        let error = publisher
23270            .prepare(&TemporaryRefPublishRequest {
23271                repository: &config.repository,
23272                source_repository: &config.source_repository,
23273                trusted_source_ref: config.workflow.trusted_source_ref(),
23274                source_revision: &source_revision,
23275                snapshot_source_ref: Some(&snapshot_ref),
23276                temporary_ref: &temporary_ref,
23277                workflow_path: workflow.path(),
23278                workflow_bytes: workflow.yaml().as_bytes(),
23279                workflow_fingerprint: &config.workflow_fingerprint,
23280                manifest_bytes: b"{\"schema_version\":1}\n",
23281                operation_id: "operation-1",
23282                created_at_ms: 1_700_000_000_000,
23283                authorized: true,
23284            })
23285            .expect_err("changed source ref must fail closed");
23286        assert_eq!(error, TemporaryRefPublishError::GitSnapshotRefConflict);
23287        let runner = publisher.into_runner();
23288        assert!(!runner.invocations.iter().any(|invocation| {
23289            matches!(
23290                invocation
23291                    .arguments
23292                    .first()
23293                    .and_then(|argument| argument.to_str()),
23294                Some("hash-object" | "update-index" | "write-tree" | "commit-tree" | "push")
23295            )
23296        }));
23297    }
23298
23299    #[test]
23300    fn snapshot_publish_prepared_rechecks_the_custom_ref_before_push() {
23301        let config = provider_config(all_authorized());
23302        let workflow = generate_workflow(&config.workflow);
23303        let mut responses =
23304            successful_snapshot_prepare_git_responses(workflow.yaml().as_bytes(), SOURCE_SHA);
23305        responses.extend(successful_snapshot_prepare_git_responses(
23306            workflow.yaml().as_bytes(),
23307            DISPATCH_SHA,
23308        ));
23309        let directory = tempdir().expect("isolation directory");
23310        let mut publisher = GitTemporaryRefPublisher::new(
23311            FakeGitRunner::with(responses),
23312            directory.path(),
23313            "source",
23314            "execution",
23315            Duration::from_secs(5),
23316        )
23317        .expect("publisher");
23318        let temporary_ref = TemporaryGitRef::new(
23319            config.workflow.temporary_branch_namespace(),
23320            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23321        )
23322        .expect("temporary ref");
23323        let source_revision = CommitSha::new(SOURCE_SHA).expect("source revision");
23324        let snapshot_ref =
23325            GitSnapshotSourceRef::for_operation("operation-1").expect("snapshot ref");
23326        let request = TemporaryRefPublishRequest {
23327            repository: &config.repository,
23328            source_repository: &config.source_repository,
23329            trusted_source_ref: config.workflow.trusted_source_ref(),
23330            source_revision: &source_revision,
23331            snapshot_source_ref: Some(&snapshot_ref),
23332            temporary_ref: &temporary_ref,
23333            workflow_path: workflow.path(),
23334            workflow_bytes: workflow.yaml().as_bytes(),
23335            workflow_fingerprint: &config.workflow_fingerprint,
23336            manifest_bytes: b"{\"schema_version\":1}\n",
23337            operation_id: "operation-1",
23338            created_at_ms: 1_700_000_000_000,
23339            authorized: true,
23340        };
23341        let prepared = publisher
23342            .prepare(&request)
23343            .expect("initial exact source check")
23344            .expect("prepared dispatch");
23345        let failure = publisher
23346            .publish_prepared(&request, Some(&prepared))
23347            .expect_err("source ref drift must block dispatch push");
23348        assert_eq!(
23349            failure.error(),
23350            TemporaryRefPublishError::GitSnapshotRefConflict
23351        );
23352        assert!(failure.possible_publication().is_none());
23353        let runner = publisher.into_runner();
23354        assert!(
23355            !runner.invocations.iter().any(|invocation| {
23356                invocation.arguments.first() == Some(&OsString::from("push"))
23357            })
23358        );
23359    }
23360
23361    #[test]
23362    #[allow(clippy::too_many_lines)]
23363    fn prepared_publications_do_not_share_mutable_cross_job_state() {
23364        let config = provider_config(all_authorized());
23365        let workflow = generate_workflow(&config.workflow);
23366        let temporary_ref_a = TemporaryGitRef::new(
23367            config.workflow.temporary_branch_namespace(),
23368            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch A"),
23369        )
23370        .expect("temporary ref A");
23371        let temporary_ref_b = TemporaryGitRef::new(
23372            config.workflow.temporary_branch_namespace(),
23373            BranchName::new("rustferry/goal3/builds/operation-2").expect("branch B"),
23374        )
23375        .expect("temporary ref B");
23376        let source_revision = CommitSha::new(SOURCE_SHA).expect("source");
23377        let mut responses = successful_prepare_git_responses(workflow.yaml().as_bytes());
23378        responses.extend(successful_prepare_git_responses(workflow.yaml().as_bytes()));
23379        responses.extend(successful_prepare_git_responses(workflow.yaml().as_bytes()));
23380        responses.push(Ok(Vec::new()));
23381        responses.push(Ok(format!(
23382            "{DISPATCH_SHA}\trefs/heads/{}\n",
23383            temporary_ref_a.branch().as_str()
23384        )
23385        .into_bytes()));
23386        let directory = tempdir().expect("isolation directory");
23387        let mut publisher = GitTemporaryRefPublisher::new(
23388            FakeGitRunner::with(responses),
23389            directory.path(),
23390            "source",
23391            "execution",
23392            Duration::from_secs(5),
23393        )
23394        .expect("publisher");
23395
23396        let prepared_a = publisher
23397            .prepare(&TemporaryRefPublishRequest {
23398                repository: &config.repository,
23399                source_repository: &config.source_repository,
23400                trusted_source_ref: config.workflow.trusted_source_ref(),
23401                source_revision: &source_revision,
23402                snapshot_source_ref: None,
23403                temporary_ref: &temporary_ref_a,
23404                workflow_path: workflow.path(),
23405                workflow_bytes: workflow.yaml().as_bytes(),
23406                workflow_fingerprint: &config.workflow_fingerprint,
23407                manifest_bytes: b"{\"schema_version\":1}\n",
23408                operation_id: "operation-1",
23409                created_at_ms: 1_700_000_000_000,
23410                authorized: true,
23411            })
23412            .expect("prepare A")
23413            .expect("prepared A");
23414        publisher
23415            .prepare(&TemporaryRefPublishRequest {
23416                repository: &config.repository,
23417                source_repository: &config.source_repository,
23418                trusted_source_ref: config.workflow.trusted_source_ref(),
23419                source_revision: &source_revision,
23420                snapshot_source_ref: None,
23421                temporary_ref: &temporary_ref_b,
23422                workflow_path: workflow.path(),
23423                workflow_bytes: workflow.yaml().as_bytes(),
23424                workflow_fingerprint: &config.workflow_fingerprint,
23425                manifest_bytes: b"{\"schema_version\":1}\n",
23426                operation_id: "operation-2",
23427                created_at_ms: 1_700_000_000_000,
23428                authorized: true,
23429            })
23430            .expect("interleaved prepare B")
23431            .expect("prepared B");
23432        let published_a = publisher
23433            .publish_prepared(
23434                &TemporaryRefPublishRequest {
23435                    repository: &config.repository,
23436                    source_repository: &config.source_repository,
23437                    trusted_source_ref: config.workflow.trusted_source_ref(),
23438                    source_revision: &source_revision,
23439                    snapshot_source_ref: None,
23440                    temporary_ref: &temporary_ref_a,
23441                    workflow_path: workflow.path(),
23442                    workflow_bytes: workflow.yaml().as_bytes(),
23443                    workflow_fingerprint: &config.workflow_fingerprint,
23444                    manifest_bytes: b"{\"schema_version\":1}\n",
23445                    operation_id: "operation-1",
23446                    created_at_ms: 1_700_000_000_000,
23447                    authorized: true,
23448                },
23449                Some(&prepared_a),
23450            )
23451            .expect("publish A after interleaved prepare B");
23452        assert_eq!(published_a, prepared_a);
23453        let runner = publisher.into_runner();
23454        assert!(runner.responses.is_empty());
23455        assert_eq!(
23456            runner
23457                .invocations
23458                .iter()
23459                .filter(|invocation| {
23460                    invocation
23461                        .arguments
23462                        .first()
23463                        .and_then(|argument| argument.to_str())
23464                        == Some("push")
23465                })
23466                .count(),
23467            1
23468        );
23469    }
23470
23471    #[test]
23472    fn publish_prepared_revalidates_source_trust_immediately_before_push() {
23473        let config = provider_config(all_authorized());
23474        let workflow = generate_workflow(&config.workflow);
23475        let temporary_ref = TemporaryGitRef::new(
23476            config.workflow.temporary_branch_namespace(),
23477            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23478        )
23479        .expect("temporary ref");
23480        let source_revision = CommitSha::new(SOURCE_SHA).expect("source");
23481        let mut responses = successful_prepare_git_responses(workflow.yaml().as_bytes());
23482        let mut revoked = successful_prepare_git_responses(workflow.yaml().as_bytes());
23483        revoked.truncate(10);
23484        revoked[9] = Err(GitExecutionError::CommandFailed { exit_code: Some(1) });
23485        responses.extend(revoked);
23486        let directory = tempdir().expect("isolation directory");
23487        let mut publisher = GitTemporaryRefPublisher::new(
23488            FakeGitRunner::with(responses),
23489            directory.path(),
23490            "source",
23491            "execution",
23492            Duration::from_secs(5),
23493        )
23494        .expect("publisher");
23495        let request = TemporaryRefPublishRequest {
23496            repository: &config.repository,
23497            source_repository: &config.source_repository,
23498            trusted_source_ref: config.workflow.trusted_source_ref(),
23499            source_revision: &source_revision,
23500            snapshot_source_ref: None,
23501            temporary_ref: &temporary_ref,
23502            workflow_path: workflow.path(),
23503            workflow_bytes: workflow.yaml().as_bytes(),
23504            workflow_fingerprint: &config.workflow_fingerprint,
23505            manifest_bytes: b"{\"schema_version\":1}\n",
23506            operation_id: "operation-1",
23507            created_at_ms: 1_700_000_000_000,
23508            authorized: true,
23509        };
23510        let prepared = publisher
23511            .prepare(&request)
23512            .expect("initial trust preflight")
23513            .expect("prepared publication");
23514        let failure = publisher
23515            .publish_prepared(&request, Some(&prepared))
23516            .expect_err("revoked source trust must block publication");
23517        assert_eq!(
23518            failure.error(),
23519            TemporaryRefPublishError::SourceRevisionNotTrusted
23520        );
23521        assert!(failure.possible_publication().is_none());
23522        let runner = publisher.into_runner();
23523        assert!(runner.responses.is_empty());
23524        assert!(!runner.invocations.iter().any(|invocation| {
23525            invocation
23526                .arguments
23527                .first()
23528                .and_then(|argument| argument.to_str())
23529                == Some("push")
23530        }));
23531    }
23532
23533    #[test]
23534    #[allow(clippy::too_many_lines)]
23535    fn git_publisher_creates_an_orphan_two_file_dispatch_commit() {
23536        let full_trusted_ref = "refs/heads/goal3/macless-iphone-builds";
23537        let full_temporary_ref = "refs/heads/rustferry/goal3/builds/operation-1";
23538        let config = provider_config(all_authorized());
23539        let workflow = generate_workflow(&config.workflow);
23540        let responses = [
23541            Ok(b"git@github.com:ShiroKSH/rust-and-iphone.git\n".to_vec()),
23542            Ok(b"ssh://git@github.com/ShiroKSH/rust-and-iphone\n".to_vec()),
23543            Ok(b"git@github.com:ShiroKSH/rustferry-signing.git\n".to_vec()),
23544            Ok(b"ssh://git@github.com/ShiroKSH/rustferry-signing\n".to_vec()),
23545            Ok(format!("{TRUSTED_TIP}\t{full_trusted_ref}\n").into_bytes()),
23546            Ok(Vec::new()),
23547            Ok(workflow.yaml().as_bytes().to_vec()),
23548            Ok(Vec::new()),
23549            Ok(Vec::new()),
23550            Ok(Vec::new()),
23551            Ok(Vec::new()),
23552            Ok(format!("{WORKFLOW_BLOB}\n").into_bytes()),
23553            Ok(format!("{MANIFEST_BLOB}\n").into_bytes()),
23554            Ok(Vec::new()),
23555            Ok(Vec::new()),
23556            Ok(format!("{TREE_SHA}\n").into_bytes()),
23557            Ok(format!("{DISPATCH_SHA}\n").into_bytes()),
23558            Ok(Vec::new()),
23559            Ok(format!("{DISPATCH_SHA}\t{full_temporary_ref}\n").into_bytes()),
23560        ];
23561        let directory = tempdir().expect("isolation directory");
23562        let runner = FakeGitRunner::with(responses);
23563        let mut publisher = GitTemporaryRefPublisher::new(
23564            runner,
23565            directory.path(),
23566            "source",
23567            "execution",
23568            Duration::from_secs(5),
23569        )
23570        .expect("publisher");
23571        let temporary_ref = TemporaryGitRef::new(
23572            config.workflow.temporary_branch_namespace(),
23573            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23574        )
23575        .expect("temporary ref");
23576        let publication = publisher
23577            .publish(&TemporaryRefPublishRequest {
23578                repository: &config.repository,
23579                source_repository: &config.source_repository,
23580                trusted_source_ref: config.workflow.trusted_source_ref(),
23581                source_revision: &CommitSha::new(SOURCE_SHA).expect("source"),
23582                snapshot_source_ref: None,
23583                temporary_ref: &temporary_ref,
23584                workflow_path: workflow.path(),
23585                workflow_bytes: workflow.yaml().as_bytes(),
23586                workflow_fingerprint: &config.workflow_fingerprint,
23587                manifest_bytes: b"{\"schema_version\":1}\n",
23588                operation_id: "operation-1",
23589                created_at_ms: 1_700_000_000_000,
23590                authorized: true,
23591            })
23592            .expect("publication");
23593        assert_eq!(publication.commit().as_str(), DISPATCH_SHA);
23594        let runner = publisher.into_runner();
23595        assert!(runner.responses.is_empty());
23596        assert!(runner.invocations.iter().all(|invocation| {
23597            !invocation.arguments.iter().any(|argument| {
23598                matches!(
23599                    argument.to_str(),
23600                    Some("checkout" | "switch" | "branch" | "commit")
23601                )
23602            })
23603        }));
23604        assert!(runner.invocations.iter().all(|invocation| {
23605            !matches!(
23606                invocation
23607                    .arguments
23608                    .first()
23609                    .and_then(|argument| argument.to_str()),
23610                Some("read-tree" | "ls-files")
23611            ) && !invocation
23612                .arguments
23613                .contains(&OsString::from("--force-remove"))
23614        }));
23615        let indexed_paths = runner
23616            .invocations
23617            .iter()
23618            .filter(|invocation| {
23619                invocation.arguments.first() == Some(&OsString::from("update-index"))
23620            })
23621            .filter_map(|invocation| invocation.arguments.last()?.to_str())
23622            .collect::<Vec<_>>();
23623        assert_eq!(
23624            indexed_paths,
23625            [
23626                ".github/workflows/rustferry-goal3-iphone.yml",
23627                DISPATCH_MANIFEST_PATH,
23628            ]
23629        );
23630        let push = runner
23631            .invocations
23632            .iter()
23633            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("push")))
23634            .expect("push invocation");
23635        assert!(push.arguments.contains(&OsString::from("--no-verify")));
23636        assert!(push.arguments.contains(&OsString::from(format!(
23637            "--force-with-lease={full_temporary_ref}:"
23638        ))));
23639        assert!(push.arguments.contains(&OsString::from(format!(
23640            "{DISPATCH_SHA}:{full_temporary_ref}"
23641        ))));
23642        assert!(
23643            push.arguments
23644                .contains(&OsString::from("git@github.com:shiroksh/rustferry-signing"))
23645        );
23646        let fetch = runner
23647            .invocations
23648            .iter()
23649            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("fetch")))
23650            .expect("trusted-ref fetch");
23651        assert!(
23652            fetch
23653                .arguments
23654                .contains(&OsString::from("git@github.com:shiroksh/rust-and-iphone"))
23655        );
23656        assert_eq!(
23657            runner
23658                .invocations
23659                .iter()
23660                .filter(|invocation| invocation.arguments.first()
23661                    == Some(&OsString::from("hash-object")))
23662                .count(),
23663            2
23664        );
23665        let commit = runner
23666            .invocations
23667            .iter()
23668            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("commit-tree")))
23669            .expect("commit-tree invocation");
23670        assert_eq!(
23671            commit.arguments,
23672            [OsString::from("commit-tree"), OsString::from(TREE_SHA)]
23673        );
23674        assert!(commit.stdin_len > 0);
23675        for expected in [
23676            (GitEnvironmentVariable::AuthorName, "ShiroKSH"),
23677            (
23678                GitEnvironmentVariable::AuthorEmail,
23679                "kushidashiro@gmail.com",
23680            ),
23681            (GitEnvironmentVariable::CommitterName, "ShiroKSH"),
23682            (
23683                GitEnvironmentVariable::CommitterEmail,
23684                "kushidashiro@gmail.com",
23685            ),
23686        ] {
23687            assert!(
23688                commit
23689                    .environment
23690                    .contains(&(expected.0, OsString::from(expected.1),))
23691            );
23692        }
23693        assert!(
23694            commit
23695                .environment
23696                .iter()
23697                .any(|(name, _)| *name == GitEnvironmentVariable::AuthorDate)
23698        );
23699    }
23700
23701    #[test]
23702    fn git_publisher_deletes_only_with_exact_expected_tip_lease() {
23703        let full_ref = "refs/heads/rustferry/goal3/builds/operation-1";
23704        let config = provider_config(all_authorized());
23705        let temporary_ref = TemporaryGitRef::new(
23706            config.workflow.temporary_branch_namespace(),
23707            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23708        )
23709        .expect("temporary ref");
23710        let responses = [
23711            Ok(b"git@github.com:ShiroKSH/rustferry-signing.git\n".to_vec()),
23712            Ok(b"ssh://git@github.com/ShiroKSH/rustferry-signing\n".to_vec()),
23713            Ok(Vec::new()),
23714            Ok(Vec::new()),
23715        ];
23716        let directory = tempdir().expect("isolation directory");
23717        let mut publisher = GitTemporaryRefPublisher::new(
23718            FakeGitRunner::with(responses),
23719            directory.path(),
23720            "origin",
23721            "origin",
23722            Duration::from_secs(5),
23723        )
23724        .expect("publisher");
23725        publisher
23726            .delete_temporary_ref(&TemporaryRefDeleteRequest {
23727                repository: &config.repository,
23728                source_repository: &config.source_repository,
23729                temporary_ref: &temporary_ref,
23730                expected_commit: &CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
23731                authorized: true,
23732            })
23733            .expect("lease-protected deletion");
23734        let runner = publisher.into_runner();
23735        let push = runner
23736            .invocations
23737            .iter()
23738            .find(|invocation| invocation.arguments.first() == Some(&OsString::from("push")))
23739            .expect("delete push");
23740        assert!(push.arguments.contains(&OsString::from(format!(
23741            "--force-with-lease={full_ref}:{DISPATCH_SHA}"
23742        ))));
23743        assert!(
23744            push.arguments
23745                .contains(&OsString::from(format!(":{full_ref}")))
23746        );
23747        assert!(push.arguments.contains(&OsString::from("--atomic")));
23748    }
23749
23750    #[test]
23751    fn git_publisher_reports_changed_tip_after_delete_lease_rejection() {
23752        let full_ref = "refs/heads/rustferry/goal3/builds/operation-1";
23753        let config = provider_config(all_authorized());
23754        let temporary_ref = TemporaryGitRef::new(
23755            config.workflow.temporary_branch_namespace(),
23756            BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
23757        )
23758        .expect("temporary ref");
23759        let responses = [
23760            Ok(b"git@github.com:ShiroKSH/rustferry-signing.git\n".to_vec()),
23761            Ok(b"ssh://git@github.com/ShiroKSH/rustferry-signing\n".to_vec()),
23762            Err(GitExecutionError::CommandFailed { exit_code: Some(1) }),
23763            Ok(format!("{TRUSTED_TIP}\t{full_ref}\n").into_bytes()),
23764        ];
23765        let directory = tempdir().expect("isolation directory");
23766        let mut publisher = GitTemporaryRefPublisher::new(
23767            FakeGitRunner::with(responses),
23768            directory.path(),
23769            "origin",
23770            "origin",
23771            Duration::from_secs(5),
23772        )
23773        .expect("publisher");
23774        assert_eq!(
23775            publisher.delete_temporary_ref(&TemporaryRefDeleteRequest {
23776                repository: &config.repository,
23777                source_repository: &config.source_repository,
23778                temporary_ref: &temporary_ref,
23779                expected_commit: &CommitSha::new(DISPATCH_SHA).expect("dispatch commit"),
23780                authorized: true,
23781            }),
23782            Err(TemporaryRefPublishError::TemporaryRefLeaseRejected)
23783        );
23784    }
23785
23786    #[test]
23787    fn provider_submit_publishes_exact_request_and_queues_without_fake_success() {
23788        let config = provider_config(all_authorized());
23789        let provider = GithubBuildProvider::with_artifact_store_and_clock(
23790            config.clone(),
23791            signed_transport(FakeGhRunner::default()),
23792            FakePublisher::default(),
23793            NoVerifiedArtifactStore,
23794            FixedClock(1_700_000_000_000),
23795        );
23796        let handle =
23797            poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
23798        assert_eq!(handle.job_id, "operation-1");
23799        assert_eq!(handle.state, JobState::Queued);
23800        let publisher = provider.publisher.lock().expect("publisher");
23801        let captured = publisher.published.first().expect("publication");
23802        assert_eq!(captured.source_revision, SOURCE_SHA);
23803        assert_eq!(captured.branch, "rustferry/goal3/builds/operation-1");
23804        assert_eq!(
23805            captured.workflow_path,
23806            ".github/workflows/rustferry-goal3-iphone.yml"
23807        );
23808        assert_eq!(
23809            captured.workflow_sha256,
23810            config.workflow_fingerprint.as_str()
23811        );
23812        assert_eq!(captured.manifest.request, valid_request());
23813        assert!(captured.authorized);
23814        assert!(publisher.publication_leases.is_empty());
23815        assert!(!provider.capabilities().artifact_download);
23816        let state = provider.state.lock().expect("state");
23817        let record = state.jobs.get("operation-1").expect("job record");
23818        assert_eq!(record.request, valid_request());
23819        assert_eq!(
23820            record.request_sha256,
23821            canonical_request_sha256(&record.request).expect("request hash")
23822        );
23823    }
23824
23825    #[test]
23826    fn signed_submission_rechecks_private_visibility_before_publication() {
23827        let runner = FakeGhRunner::with([
23828            Ok(public_source_repository_row()),
23829            Ok(b"991\tShiroKSH/rustferry-signing\tfalse\tfalse\tfalse\tmain\n".to_vec()),
23830        ]);
23831        let captured_requests = Arc::clone(&runner.requests);
23832        let provider = GithubBuildProvider::with_artifact_store_and_clock(
23833            provider_config(all_authorized()),
23834            transport(runner),
23835            FakePublisher::default(),
23836            NoVerifiedArtifactStore,
23837            FixedClock(1_700_000_000_000),
23838        );
23839
23840        let error = poll_ready(provider.submit(valid_request(), CancellationToken::new()))
23841            .expect_err("public signing repository must fail closed");
23842        assert!(matches!(
23843            error,
23844            RemoteBuildError::ProviderFailure { code, retryable: false, .. }
23845                if code == "public_signing_repository"
23846        ));
23847        assert_eq!(captured_requests.lock().expect("requests").len(), 2);
23848        assert!(
23849            provider
23850                .publisher
23851                .lock()
23852                .expect("publisher")
23853                .published
23854                .is_empty()
23855        );
23856        assert!(provider.state.lock().expect("state").jobs.is_empty());
23857    }
23858
23859    #[test]
23860    fn submission_requires_worker_supported_artifacts_and_bounded_profiles() {
23861        let config = provider_config(all_authorized());
23862        let provider = GithubBuildProvider::with_artifact_store_and_clock(
23863            config.clone(),
23864            signed_transport(FakeGhRunner::default()),
23865            FakePublisher::default(),
23866            NoVerifiedArtifactStore,
23867            FixedClock(1_700_000_000_000),
23868        );
23869        assert_eq!(
23870            provider.capabilities().artifact_types,
23871            BTreeSet::from([
23872                IosArtifactType::Ipa,
23873                IosArtifactType::AppBundle,
23874                IosArtifactType::Dsym,
23875                IosArtifactType::SigningReport,
23876                IosArtifactType::Xcarchive,
23877            ])
23878        );
23879        assert_eq!(
23880            provider.capabilities().signing_modes,
23881            BTreeSet::from([
23882                SigningMode::ManualDevelopment,
23883                SigningMode::UnsignedCompileOnly,
23884            ])
23885        );
23886
23887        let mut optional_artifacts = valid_request();
23888        optional_artifacts.requested_artifacts.extend([
23889            IosArtifactType::AppBundle,
23890            IosArtifactType::Dsym,
23891            IosArtifactType::Xcarchive,
23892        ]);
23893        validate_submission(&config, &optional_artifacts)
23894            .expect("provider accepts supported optional signed artifacts");
23895
23896        let mut missing_ipa = valid_request();
23897        missing_ipa
23898            .requested_artifacts
23899            .remove(&IosArtifactType::Ipa);
23900        assert_eq!(
23901            validate_submission(&config, &missing_ipa).expect_err("signed IPA is mandatory"),
23902            RemoteBuildError::InvalidEventPayload {
23903                event: "ios_device_build_request",
23904                reason: "signed builds must request the installable IPA",
23905            }
23906        );
23907
23908        let mut extra_artifact = valid_request();
23909        extra_artifact
23910            .requested_artifacts
23911            .insert(IosArtifactType::ProvisioningReport);
23912        assert_eq!(
23913            poll_ready(provider.submit(extra_artifact, CancellationToken::new()))
23914                .expect_err("worker does not produce provisioning reports"),
23915            unsupported(ProviderFeature::ArtifactType(
23916                IosArtifactType::ProvisioningReport
23917            ))
23918        );
23919
23920        let mut too_many_profiles = valid_request();
23921        for _ in 0..MAX_SIGNING_PROFILES {
23922            too_many_profiles
23923                .signing
23924                .provisioning
23925                .push(too_many_profiles.signing.provisioning[0].clone());
23926        }
23927        let error = poll_ready(provider.submit(too_many_profiles, CancellationToken::new()))
23928            .expect_err("more than three profiles must fail closed");
23929        assert!(matches!(
23930            error,
23931            RemoteBuildError::ProviderFailure { code, .. }
23932                if code == "profile_mapping_unsupported"
23933        ));
23934    }
23935
23936    #[test]
23937    fn submission_accepts_exact_static_multi_profile_map_and_rejects_role_swaps() {
23938        let (request, names) = multi_profile_request();
23939        let config = provider_config_with_secret_names(all_authorized(), names);
23940        validate_submission(&config, &request).expect("exact multi-profile map");
23941
23942        let mut swapped = request.clone();
23943        let first = swapped.signing.provisioning[0].profile.clone();
23944        swapped.signing.provisioning[0].profile = swapped.signing.provisioning[1].profile.clone();
23945        swapped.signing.provisioning[1].profile = first;
23946        let error = validate_submission(&config, &swapped).expect_err("profile roles are bound");
23947        assert!(matches!(
23948            error,
23949            RemoteBuildError::ProviderFailure { code, retryable: false, .. }
23950                if code == "signing_secret_reference_mismatch"
23951        ));
23952
23953        let legacy = provider_config(all_authorized());
23954        let error = validate_submission(&legacy, &request)
23955            .expect_err("legacy profile binding must remain single-profile only");
23956        assert!(matches!(
23957            error,
23958            RemoteBuildError::ProviderFailure { code, retryable: false, .. }
23959                if code == "signing_secret_reference_mismatch"
23960        ));
23961    }
23962
23963    #[test]
23964    fn submission_rejects_application_and_extension_identity_drift() {
23965        let (request, names) = multi_profile_request();
23966        let config = provider_config_with_secret_names(all_authorized(), names);
23967
23968        let mut app_bundle_drift = request.clone();
23969        app_bundle_drift.bundle_identifier = "com.example.renamed-app".to_owned();
23970        app_bundle_drift
23971            .signing
23972            .targets
23973            .iter_mut()
23974            .find(|target| target.kind == SigningTargetKind::Application)
23975            .expect("application target")
23976            .bundle_identifier =
23977            BundleIdentifier::new("com.example.renamed-app").expect("drifted app bundle");
23978        app_bundle_drift
23979            .validate()
23980            .expect("internally valid app bundle drift");
23981        assert_signing_reference_mismatch(&config, &app_bundle_drift);
23982
23983        let mut extension_bundle_drift = request.clone();
23984        extension_bundle_drift
23985            .signing
23986            .targets
23987            .iter_mut()
23988            .find(|target| target.kind == SigningTargetKind::Extension)
23989            .expect("extension target")
23990            .bundle_identifier = BundleIdentifier::new("com.example.app.renamed-widget")
23991            .expect("drifted extension bundle");
23992        extension_bundle_drift
23993            .product
23994            .nested_bundles
23995            .iter_mut()
23996            .find(|bundle| bundle.kind == UnsignedNestedBundleKind::AppExtension)
23997            .expect("extension bundle")
23998            .bundle_identifier = "com.example.app.renamed-widget".to_owned();
23999        extension_bundle_drift
24000            .validate()
24001            .expect("internally valid extension bundle drift");
24002        assert_signing_reference_mismatch(&config, &extension_bundle_drift);
24003
24004        let mut extension_name_drift = request.clone();
24005        extension_name_drift
24006            .signing
24007            .targets
24008            .iter_mut()
24009            .find(|target| target.kind == SigningTargetKind::Extension)
24010            .expect("extension target")
24011            .name = "RenamedWidget".to_owned();
24012        extension_name_drift
24013            .signing
24014            .provisioning
24015            .iter_mut()
24016            .find(|profile| profile.target == "Widget")
24017            .expect("extension profile")
24018            .target = "RenamedWidget".to_owned();
24019        extension_name_drift
24020            .signing
24021            .entitlements
24022            .iter_mut()
24023            .find(|entitlements| entitlements.target == "Widget")
24024            .expect("extension entitlements")
24025            .target = "RenamedWidget".to_owned();
24026        extension_name_drift
24027            .validate()
24028            .expect("internally valid extension name drift");
24029        assert_signing_reference_mismatch(&config, &extension_name_drift);
24030    }
24031
24032    #[test]
24033    fn submission_target_graph_match_is_order_insensitive_and_exact() {
24034        let (request, names) = multi_profile_request_with_framework();
24035        let config = provider_config_with_secret_names(all_authorized(), names);
24036        validate_submission(&config, &request).expect("exact full target graph");
24037
24038        let mut reordered = request.clone();
24039        reordered.signing.targets.reverse();
24040        reordered.validate().expect("valid reordered target graph");
24041        validate_submission(&config, &reordered).expect("target order is not identity");
24042
24043        let mut omitted = request.clone();
24044        omitted
24045            .signing
24046            .targets
24047            .retain(|target| target.name != "RuntimeBridge");
24048        omitted
24049            .product
24050            .nested_bundles
24051            .retain(|bundle| bundle.executable != "RuntimeBridge");
24052        omitted.validate().expect("valid omitted framework graph");
24053        assert_signing_reference_mismatch(&config, &omitted);
24054
24055        let mut extra = request.clone();
24056        extra.signing.targets.push(SigningTarget {
24057            name: "SupportKit".to_owned(),
24058            bundle_identifier: BundleIdentifier::new("com.example.app.support-kit")
24059                .expect("extra framework bundle"),
24060            kind: SigningTargetKind::Framework,
24061        });
24062        extra
24063            .product
24064            .nested_bundles
24065            .push(UnsignedNestedBundleExpectation {
24066                relative_path: "Frameworks/SupportKit.framework".to_owned(),
24067                bundle_identifier: "com.example.app.support-kit".to_owned(),
24068                executable: "SupportKit".to_owned(),
24069                kind: UnsignedNestedBundleKind::Framework,
24070            });
24071        extra
24072            .product
24073            .nested_bundles
24074            .sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
24075        extra.validate().expect("valid extra framework graph");
24076        assert_signing_reference_mismatch(&config, &extra);
24077    }
24078
24079    #[test]
24080    fn signed_secrets_are_exact_by_kind_and_role_while_unsigned_xcarchive_is_accepted() {
24081        let runner = FakeGhRunner::default();
24082        let captured_requests = Arc::clone(&runner.requests);
24083        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24084            provider_config(all_authorized()),
24085            transport(runner),
24086            FakePublisher::default(),
24087            NoVerifiedArtifactStore,
24088            FixedClock(1_700_000_000_000),
24089        );
24090
24091        let mut wrong_kind = valid_request();
24092        wrong_kind
24093            .signing
24094            .signing
24095            .as_mut()
24096            .expect("signing")
24097            .identity
24098            .private_key
24099            .reference = SecretReference::new(
24100            SecretReferenceKind::Environment,
24101            "RUSTFERRY_GOAL3_IOS_CERTIFICATE_P12",
24102        )
24103        .expect("environment reference");
24104        let error = poll_ready(provider.submit(wrong_kind, CancellationToken::new()))
24105            .expect_err("wrong secret namespace");
24106        assert!(matches!(
24107            error,
24108            RemoteBuildError::ProviderFailure { code, .. }
24109                if code == "signing_secret_reference_mismatch"
24110        ));
24111
24112        let mut swapped_roles = valid_request();
24113        let signing = swapped_roles.signing.signing.as_mut().expect("signing");
24114        signing.identity.private_key.reference = SecretReference::new(
24115            SecretReferenceKind::GithubActions,
24116            "RUSTFERRY_GOAL3_IOS_CERTIFICATE_PASSWORD",
24117        )
24118        .expect("password role");
24119        signing.password = Some(
24120            SecretReference::new(
24121                SecretReferenceKind::GithubActions,
24122                "RUSTFERRY_GOAL3_IOS_CERTIFICATE_P12",
24123            )
24124            .expect("certificate role"),
24125        );
24126        let error = poll_ready(provider.submit(swapped_roles, CancellationToken::new()))
24127            .expect_err("secret roles are not interchangeable");
24128        assert!(matches!(
24129            error,
24130            RemoteBuildError::ProviderFailure { code, .. }
24131                if code == "signing_secret_reference_mismatch"
24132        ));
24133
24134        let unsigned = unsigned_request();
24135        let handle = poll_ready(provider.submit(unsigned.clone(), CancellationToken::new()))
24136            .expect("unsigned xcarchive request");
24137        assert_eq!(handle.state, JobState::Queued);
24138        assert_eq!(
24139            provider
24140                .publisher
24141                .lock()
24142                .expect("publisher")
24143                .published
24144                .first()
24145                .expect("publication")
24146                .manifest
24147                .request,
24148            unsigned
24149        );
24150        assert!(captured_requests.lock().expect("requests").is_empty());
24151    }
24152
24153    #[test]
24154    fn failed_pre_push_submission_releases_job_reservation() {
24155        let publisher = FakePublisher {
24156            failure: Some(TemporaryRefPublishError::RemoteMismatch.into()),
24157            ..FakePublisher::default()
24158        };
24159        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24160            provider_config(all_authorized()),
24161            signed_transport(FakeGhRunner::with([
24162                Ok(public_source_repository_row()),
24163                Ok(private_repository_row()),
24164                Ok(signing_environment_row(false, true, true)),
24165                Ok(exact_signing_policy_row()),
24166                Ok(exact_signing_secret_rows()),
24167            ])),
24168            publisher,
24169            NoVerifiedArtifactStore,
24170            FixedClock(1_700_000_000_000),
24171        );
24172        poll_ready(provider.submit(valid_request(), CancellationToken::new()))
24173            .expect_err("first publication fails before push");
24174        let handle = poll_ready(provider.submit(valid_request(), CancellationToken::new()))
24175            .expect("reservation was released");
24176        assert_eq!(handle.job_id, "operation-1");
24177    }
24178
24179    #[test]
24180    fn cancellation_during_successful_push_returns_visible_handle() {
24181        let cancellation = CancellationToken::new();
24182        let publisher = FakePublisher {
24183            cancel_on_publish: Some(cancellation.clone()),
24184            ..FakePublisher::default()
24185        };
24186        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24187            provider_config(all_authorized()),
24188            signed_transport(FakeGhRunner::default()),
24189            publisher,
24190            NoVerifiedArtifactStore,
24191            FixedClock(1_700_000_000_000),
24192        );
24193        let handle = poll_ready(provider.submit(valid_request(), cancellation))
24194            .expect("published job remains visible");
24195        assert_eq!(handle.job_id, "operation-1");
24196        assert_eq!(handle.state, JobState::Cancelling);
24197        assert!(
24198            provider
24199                .state
24200                .lock()
24201                .expect("state")
24202                .jobs
24203                .contains_key("operation-1")
24204        );
24205    }
24206
24207    #[test]
24208    fn signed_submission_rejects_same_source_and_execution_repository() {
24209        let runner = FakeGhRunner::default();
24210        let captured_requests = Arc::clone(&runner.requests);
24211        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24212            same_repository_provider_config(all_authorized()),
24213            transport(runner),
24214            FakePublisher::default(),
24215            NoVerifiedArtifactStore,
24216            FixedClock(1_700_000_000_000),
24217        );
24218
24219        let error = poll_ready(provider.submit(valid_request(), CancellationToken::new()))
24220            .expect_err("same-repository signing must fail closed");
24221        assert!(matches!(
24222            error,
24223            RemoteBuildError::ProviderFailure { code, retryable: false, .. }
24224                if code == "separate_signing_repository_required"
24225        ));
24226        assert!(captured_requests.lock().expect("requests").is_empty());
24227        assert!(
24228            provider
24229                .publisher
24230                .lock()
24231                .expect("publisher")
24232                .published
24233                .is_empty()
24234        );
24235    }
24236
24237    #[test]
24238    fn signed_submission_rechecks_public_source_visibility_before_publication() {
24239        let runner = FakeGhRunner::with([Ok(private_source_repository_row())]);
24240        let captured_requests = Arc::clone(&runner.requests);
24241        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24242            provider_config(all_authorized()),
24243            transport(runner),
24244            FakePublisher::default(),
24245            NoVerifiedArtifactStore,
24246            FixedClock(1_700_000_000_000),
24247        );
24248
24249        let error = poll_ready(provider.submit(valid_request(), CancellationToken::new()))
24250            .expect_err("private source repository must fail closed");
24251        assert!(matches!(
24252            error,
24253            RemoteBuildError::ProviderFailure { code, retryable: false, .. }
24254                if code == "private_source_repository"
24255        ));
24256        assert_eq!(captured_requests.lock().expect("requests").len(), 1);
24257        assert!(
24258            provider
24259                .publisher
24260                .lock()
24261                .expect("publisher")
24262                .published
24263                .is_empty()
24264        );
24265    }
24266
24267    #[test]
24268    fn signed_submission_rechecks_required_environment_reviewers_before_publication() {
24269        let runner = FakeGhRunner::with([
24270            Ok(public_source_repository_row()),
24271            Ok(private_repository_row()),
24272            Ok(signing_environment_row(false, true, false)),
24273            Ok(exact_signing_policy_row()),
24274            Ok(exact_signing_secret_rows()),
24275        ]);
24276        let captured_requests = Arc::clone(&runner.requests);
24277        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24278            provider_config(all_authorized()),
24279            transport(runner),
24280            FakePublisher::default(),
24281            NoVerifiedArtifactStore,
24282            FixedClock(1_700_000_000_000),
24283        );
24284
24285        let error = poll_ready(provider.submit(valid_request(), CancellationToken::new()))
24286            .expect_err("missing deployment reviewer must fail closed");
24287        assert!(matches!(
24288            error,
24289            RemoteBuildError::ProviderFailure { code, retryable: false, .. }
24290                if code == "signing_environment_not_ready"
24291        ));
24292        assert_eq!(captured_requests.lock().expect("requests").len(), 5);
24293        assert!(
24294            provider
24295                .publisher
24296                .lock()
24297                .expect("publisher")
24298                .published
24299                .is_empty()
24300        );
24301    }
24302
24303    #[test]
24304    fn uncertain_push_requires_exact_reconciliation_before_normal_polling() {
24305        let publication = PublishedTemporaryRef::new(
24306            TemporaryGitRef::new(
24307                provider_config(all_authorized())
24308                    .workflow
24309                    .temporary_branch_namespace(),
24310                BranchName::new("rustferry/goal3/builds/operation-1").expect("branch"),
24311            )
24312            .expect("temporary ref"),
24313            CommitSha::new(DISPATCH_SHA).expect("dispatch SHA"),
24314        );
24315        let publisher = FakePublisher {
24316            prepared_publication: Some(publication.clone()),
24317            failure: Some(TemporaryRefPublishFailure::uncertain(
24318                TemporaryRefPublishError::Git(GitExecutionError::TimedOut),
24319                publication.clone(),
24320            )),
24321            reconciliation: Some(TemporaryRefReconciliation::Exact(publication)),
24322            ..FakePublisher::default()
24323        };
24324        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24325            provider_config(all_authorized()),
24326            transport(FakeGhRunner::with([Ok(Vec::new())])),
24327            publisher,
24328            NoVerifiedArtifactStore,
24329            FixedClock(1_700_000_000_000),
24330        );
24331        let handle = poll_ready(provider.submit(unsigned_request(), CancellationToken::new()))
24332            .expect("uncertain publication returns owned handle");
24333        assert_eq!(handle.state, JobState::Created);
24334        let state = provider.state.lock().expect("state");
24335        let record = state.jobs.get("operation-1").expect("durable record");
24336        assert!(record.publication_uncertain);
24337        assert!(record.dispatch_commit.is_none());
24338        assert!(record.publication_takeover_lease_owned);
24339        drop(state);
24340
24341        let error = poll_ready(provider.events(
24342            EventRequest {
24343                job_id: "operation-1".to_owned(),
24344                after_sequence: None,
24345                limit: 100,
24346            },
24347            CancellationToken::new(),
24348        ))
24349        .expect_err("ordinary polling must not adopt a possible push");
24350        assert!(matches!(
24351            error,
24352            RemoteBuildError::ProviderFailure {
24353                code,
24354                retryable: true,
24355                ..
24356            } if code == "publication_reconciliation_required"
24357        ));
24358
24359        assert_eq!(
24360            provider
24361                .reconcile_restored_job("operation-1", &CancellationToken::new())
24362                .expect("exact possible-push reconciliation"),
24363            GithubJobReconciliation::Recovered
24364        );
24365        let state = provider.state.lock().expect("state");
24366        let record = state.jobs.get("operation-1").expect("reconciled record");
24367        assert_eq!(record.state, JobState::Queued);
24368        assert!(!record.publication_uncertain);
24369        assert_eq!(
24370            record.dispatch_commit.as_ref().map(CommitSha::as_str),
24371            Some(DISPATCH_SHA)
24372        );
24373        assert!(!record.publication_takeover_lease_owned);
24374    }
24375
24376    #[test]
24377    fn publisher_binding_mismatch_fails_without_adopting_cleanup_ownership() {
24378        let config = provider_config(all_authorized());
24379        let returned_ref = TemporaryGitRef::new(
24380            config.workflow.temporary_branch_namespace(),
24381            BranchName::new("rustferry/goal3/builds/unexpected").expect("branch"),
24382        )
24383        .expect("temporary ref");
24384        let publisher = FakePublisher {
24385            returned_temporary_ref: Some(returned_ref.clone()),
24386            ..FakePublisher::default()
24387        };
24388        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24389            config,
24390            signed_transport(FakeGhRunner::default()),
24391            publisher,
24392            NoVerifiedArtifactStore,
24393            FixedClock(1_700_000_000_000),
24394        );
24395        let handle = poll_ready(provider.submit(valid_request(), CancellationToken::new()))
24396            .expect("possibly mutated ref retains a visible handle");
24397        assert_eq!(handle.state, JobState::Failed);
24398        let state = provider.state.lock().expect("state");
24399        let record = state.jobs.get("operation-1").expect("owned record");
24400        assert_ne!(record.temporary_ref, returned_ref);
24401        assert_eq!(
24402            record.temporary_ref.branch().as_str(),
24403            "rustferry/goal3/builds/operation-1"
24404        );
24405        assert_eq!(record.dispatch_commit, None);
24406        assert!(record.publication_uncertain);
24407    }
24408
24409    #[test]
24410    fn run_discovery_exhaustion_becomes_typed_cleanup_needed_failure() {
24411        let mut config = provider_config(all_authorized());
24412        config.poll_policy =
24413            GithubPollingPolicy::new(2, Duration::from_millis(250)).expect("policy");
24414        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24415            config,
24416            signed_transport(FakeGhRunner::with([Ok(Vec::new()), Ok(Vec::new())])),
24417            FakePublisher::default(),
24418            NoVerifiedArtifactStore,
24419            FixedClock(1_700_000_000_000),
24420        );
24421        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
24422        let events = || EventRequest {
24423            job_id: "operation-1".to_owned(),
24424            after_sequence: None,
24425            limit: 100,
24426        };
24427        let first = poll_ready(provider.events(events(), CancellationToken::new()))
24428            .expect("first discovery attempt");
24429        assert_eq!(first.state, JobState::Queued);
24430        let second = poll_ready(provider.events(events(), CancellationToken::new()))
24431            .expect("bounded discovery failure");
24432        assert_eq!(second.state, JobState::Failed);
24433        assert!(!second.complete);
24434        assert!(second.events.iter().any(|event| matches!(
24435            &event.kind,
24436            RemoteBuildEventKind::Warning { code, .. } if code == "github.cleanup_required"
24437        )));
24438        assert!(second.events.iter().any(|event| matches!(
24439            &event.kind,
24440            RemoteBuildEventKind::OperationFinished {
24441                success: false,
24442                error: Some(error),
24443                ..
24444            } if error.code == "github.run_discovery_exhausted"
24445        )));
24446    }
24447
24448    #[test]
24449    fn cleanup_uses_one_exact_lease_and_refuses_a_changed_tip() {
24450        let mut config = provider_config(all_authorized());
24451        config.poll_policy =
24452            GithubPollingPolicy::new(1, Duration::from_millis(250)).expect("policy");
24453        let runner = FakeGhRunner::with([Ok(Vec::new())]);
24454        let captured_requests = Arc::clone(&runner.requests);
24455        let publisher = FakePublisher {
24456            delete_failure: Some(TemporaryRefPublishError::TemporaryRefLeaseRejected),
24457            ..FakePublisher::default()
24458        };
24459        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24460            config,
24461            signed_transport(runner),
24462            publisher,
24463            NoVerifiedArtifactStore,
24464            FixedClock(1_700_000_000_000),
24465        );
24466        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
24467        let page = poll_ready(provider.events(
24468            EventRequest {
24469                job_id: "operation-1".to_owned(),
24470                after_sequence: None,
24471                limit: 100,
24472            },
24473            CancellationToken::new(),
24474        ))
24475        .expect("bounded discovery failure");
24476        assert_eq!(page.state, JobState::Failed);
24477        let request_count_before_cleanup = captured_requests.lock().expect("requests").len();
24478        let error = poll_ready(provider.cleanup(
24479            CleanupRequest {
24480                job_id: "operation-1".to_owned(),
24481                remove_artifacts: false,
24482            },
24483            CancellationToken::new(),
24484        ))
24485        .expect_err("changed remote ref must not be deleted");
24486        assert!(matches!(
24487            error,
24488            RemoteBuildError::ProviderFailure { code, .. }
24489                if code == "temporary_ref_tip_changed"
24490        ));
24491        assert_eq!(
24492            captured_requests.lock().expect("captured requests").len(),
24493            request_count_before_cleanup,
24494            "cleanup must not issue a GitHub DELETE after a tip mismatch"
24495        );
24496        let publisher = provider.publisher.lock().expect("publisher");
24497        let deletion = publisher.deletions.first().expect("delete lease");
24498        assert_eq!(deletion.branch, "rustferry/goal3/builds/operation-1");
24499        assert_eq!(deletion.expected_commit, DISPATCH_SHA);
24500        assert!(deletion.authorized);
24501    }
24502
24503    #[test]
24504    fn unsupported_artifact_removal_does_not_bind_cleanup_intent() {
24505        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24506            provider_config(all_authorized()),
24507            transport(FakeGhRunner::with([
24508                Ok(authenticated_user_row()),
24509                Ok(private_repository_row()),
24510                Ok(completed_run_row("success")),
24511                Ok(authenticated_user_row()),
24512                Ok(private_repository_row()),
24513                Ok(completed_run_row("success")),
24514                Ok(authenticated_user_row()),
24515                Ok(private_repository_row()),
24516                Ok(completed_run_row("success")),
24517                Ok(authenticated_user_row()),
24518                Ok(private_repository_row()),
24519                Ok(authenticated_user_row()),
24520                Ok(private_repository_row()),
24521                Ok(completed_run_row("success")),
24522                Ok(authenticated_user_row()),
24523                Ok(private_repository_row()),
24524            ])),
24525            FakePublisher::default(),
24526            NoVerifiedArtifactStore,
24527            FixedClock(1_700_000_000_000),
24528        );
24529        provider
24530            .restore_job_resumes(vec![durable_success_resume()])
24531            .expect("restore successful job");
24532
24533        let error = poll_ready(provider.cleanup(
24534            CleanupRequest {
24535                job_id: "operation-1".to_owned(),
24536                remove_artifacts: true,
24537            },
24538            CancellationToken::new(),
24539        ))
24540        .expect_err("unsupported removal must fail before durable intent");
24541        assert!(matches!(
24542            error,
24543            RemoteBuildError::ProviderFailure { code, .. }
24544                if code == "artifact_removal_unsupported"
24545        ));
24546        {
24547            let state = provider.state.lock().expect("state");
24548            let record = state.jobs.get("operation-1").expect("job");
24549            assert!(!record.cleanup_requested);
24550            assert!(!record.remove_artifacts_requested);
24551            assert!(!record.artifacts_removed);
24552            assert!(!record.temporary_ref_deleted);
24553        }
24554
24555        poll_ready(provider.cleanup(
24556            CleanupRequest {
24557                job_id: "operation-1".to_owned(),
24558                remove_artifacts: false,
24559            },
24560            CancellationToken::new(),
24561        ))
24562        .expect("ref-only retry remains available");
24563        assert_eq!(
24564            provider
24565                .publisher
24566                .lock()
24567                .expect("publisher")
24568                .deletions
24569                .len(),
24570            1
24571        );
24572    }
24573
24574    #[test]
24575    fn cleanup_revalidates_exact_run_attempt_before_binding_or_mutating() {
24576        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24577            provider_config(all_authorized()),
24578            transport(FakeGhRunner::with([
24579                Ok(authenticated_user_row()),
24580                Ok(private_repository_row()),
24581                Ok(completed_run_row("success")),
24582                Ok(authenticated_user_row()),
24583                Ok(private_repository_row()),
24584                Ok(queued_run_attempt_row(2)),
24585            ])),
24586            FakePublisher::default(),
24587            NoVerifiedArtifactStore,
24588            FixedClock(1_700_000_000_000),
24589        );
24590        provider
24591            .restore_job_resumes(vec![durable_success_resume()])
24592            .expect("restore successful attempt");
24593
24594        assert!(
24595            poll_ready(provider.cleanup(
24596                CleanupRequest {
24597                    job_id: "operation-1".to_owned(),
24598                    remove_artifacts: false,
24599                },
24600                CancellationToken::new(),
24601            ))
24602            .is_err()
24603        );
24604        let state = provider.lock_state().expect("state");
24605        let record = state.jobs.get("operation-1").expect("job");
24606        assert!(!record.cleanup_requested);
24607        assert!(!record.temporary_ref_deleted);
24608        assert!(
24609            provider
24610                .publisher
24611                .lock()
24612                .expect("publisher")
24613                .deletions
24614                .is_empty()
24615        );
24616    }
24617
24618    #[test]
24619    fn cleanup_never_confirms_after_a_rerun_starts_during_ref_deletion() {
24620        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24621            provider_config(all_authorized()),
24622            transport(FakeGhRunner::with([
24623                Ok(authenticated_user_row()),
24624                Ok(private_repository_row()),
24625                Ok(completed_run_row("success")),
24626                Ok(authenticated_user_row()),
24627                Ok(private_repository_row()),
24628                Ok(completed_run_row("success")),
24629                Ok(authenticated_user_row()),
24630                Ok(private_repository_row()),
24631                Ok(completed_run_row("success")),
24632                Ok(authenticated_user_row()),
24633                Ok(private_repository_row()),
24634                Ok(authenticated_user_row()),
24635                Ok(private_repository_row()),
24636                Ok(queued_run_attempt_row(2)),
24637            ])),
24638            FakePublisher::default(),
24639            NoVerifiedArtifactStore,
24640            FixedClock(1_700_000_000_000),
24641        );
24642        provider
24643            .restore_job_resumes(vec![durable_success_resume()])
24644            .expect("restore successful attempt");
24645
24646        let error = poll_ready(provider.cleanup(
24647            CleanupRequest {
24648                job_id: "operation-1".to_owned(),
24649                remove_artifacts: false,
24650            },
24651            CancellationToken::new(),
24652        ))
24653        .expect_err("rerun drift must prevent positive cleanup confirmation");
24654        assert!(
24655            matches!(
24656                &error,
24657                RemoteBuildError::ProviderFailure {
24658                    code,
24659                    retryable: true,
24660                    ..
24661                } if code == "cleanup_run_changed_after_mutation"
24662            ),
24663            "{error:?}"
24664        );
24665        let state = provider.lock_state().expect("state");
24666        let record = state.jobs.get("operation-1").expect("job");
24667        assert!(record.temporary_ref_deleted);
24668        assert_ne!(record.state, JobState::Cleaned);
24669        assert!(
24670            !record
24671                .events
24672                .iter()
24673                .any(|event| matches!(event.kind, RemoteBuildEventKind::CleanupFinished { .. }))
24674        );
24675        drop(state);
24676        assert_eq!(
24677            provider
24678                .publisher
24679                .lock()
24680                .expect("publisher")
24681                .deletions
24682                .len(),
24683            1
24684        );
24685    }
24686
24687    #[test]
24688    fn signed_cleanup_evidence_survives_a_distinct_later_attempt() {
24689        let mut responses = vec![
24690            Ok(authenticated_user_row()),
24691            Ok(private_repository_row()),
24692            Ok(queued_run_attempt_row(2)),
24693        ];
24694        for response in [
24695            authenticated_user_row(),
24696            private_repository_row(),
24697            queued_run_attempt_row(2),
24698            authenticated_user_row(),
24699            private_repository_row(),
24700            queued_run_attempt_row(2),
24701            authenticated_user_row(),
24702            private_repository_row(),
24703            authenticated_user_row(),
24704            private_repository_row(),
24705            queued_run_attempt_row(2),
24706            authenticated_user_row(),
24707            private_repository_row(),
24708        ] {
24709            responses.push(Ok(response));
24710        }
24711        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24712            provider_config(all_authorized()),
24713            transport(FakeGhRunner::with(responses)),
24714            FakePublisher::default(),
24715            NoVerifiedArtifactStore,
24716            FixedClock(1_700_000_000_000),
24717        );
24718        provider
24719            .restore_job_resumes(vec![durable_signed_success_resume()])
24720            .expect("restore attempt-one evidence while attempt two exists");
24721
24722        let confirmation = poll_ready(provider.cleanup(
24723            CleanupRequest {
24724                job_id: "operation-1".to_owned(),
24725                remove_artifacts: false,
24726            },
24727            CancellationToken::new(),
24728        ))
24729        .expect("attempt-one cleanup evidence remains valid");
24730
24731        assert!(confirmation.workspace_removed);
24732        assert!(confirmation.signing_material_removed);
24733        let state = provider.lock_state().expect("state");
24734        let record = state.jobs.get("operation-1").expect("job");
24735        assert_eq!(record.state, JobState::Cleaned);
24736        assert_eq!(
24737            record
24738                .run_snapshot
24739                .as_ref()
24740                .expect("bound attempt")
24741                .run_attempt(),
24742            1
24743        );
24744    }
24745
24746    #[test]
24747    fn signed_cleanup_evidence_is_bind_once_and_rejects_attempt_replay() {
24748        let resume = durable_signed_success_resume();
24749        let mut replay = resume.clone();
24750        replay
24751            .signed_cleanup_evidence
24752            .as_mut()
24753            .expect("evidence")
24754            .run
24755            .run_attempt = 2;
24756        assert!(
24757            replay
24758                .signed_cleanup_evidence
24759                .as_ref()
24760                .expect("evidence")
24761                .validate_for_resume(&replay)
24762                .is_err()
24763        );
24764        assert!(resume.validate_successor(&replay).is_err());
24765
24766        let mut removed = resume.clone();
24767        removed.signed_cleanup_evidence = None;
24768        assert!(resume.validate_successor(&removed).is_err());
24769    }
24770
24771    #[test]
24772    fn signed_cleanup_evidence_rejects_manifest_and_envelope_forgery() {
24773        let resume = durable_signed_success_resume();
24774        let mut candidates = Vec::new();
24775
24776        let mut candidate = resume.clone();
24777        candidate.manifests[0].operation_id = "other-operation".to_owned();
24778        candidates.push(candidate);
24779        let mut candidate = resume.clone();
24780        candidate.manifests[0].job_id = "other-job".to_owned();
24781        candidates.push(candidate);
24782        let mut candidate = resume.clone();
24783        candidate.manifests[0].source_repository =
24784            Some("https://github.com/other/source".to_owned());
24785        candidates.push(candidate);
24786        let mut candidate = resume.clone();
24787        candidate.manifests[0].source_sha256 = "9".repeat(64);
24788        candidates.push(candidate);
24789        let mut candidate = resume.clone();
24790        candidate.manifests[0]
24791            .artifacts
24792            .iter_mut()
24793            .find(|artifact| artifact.kind == ArtifactKind::SigningReport)
24794            .expect("signing report")
24795            .sha256 = "8".repeat(64);
24796        candidates.push(candidate);
24797        let mut candidate = resume.clone();
24798        candidate
24799            .signed_cleanup_evidence
24800            .as_mut()
24801            .expect("evidence")
24802            .execution_repository_id += 1;
24803        candidates.push(candidate);
24804        let mut candidate = resume.clone();
24805        candidate
24806            .signed_cleanup_evidence
24807            .as_mut()
24808            .expect("evidence")
24809            .dispatch_revision = SOURCE_SHA.to_owned();
24810        candidates.push(candidate);
24811        let mut candidate = resume.clone();
24812        candidate
24813            .signed_cleanup_evidence
24814            .as_mut()
24815            .expect("evidence")
24816            .compile_evidence_sha256 = "7".repeat(64);
24817        candidates.push(candidate);
24818
24819        for candidate in candidates {
24820            assert!(
24821                candidate
24822                    .signed_cleanup_evidence
24823                    .as_ref()
24824                    .expect("evidence")
24825                    .validate_for_resume(&candidate)
24826                    .is_err()
24827            );
24828        }
24829    }
24830
24831    #[test]
24832    fn failed_signed_attempt_restores_cleanup_failure_without_positive_evidence() {
24833        let config = provider_config(all_authorized());
24834        let principal = GithubPrincipalIdentityV1::User {
24835            id: AUTHENTICATED_USER_ID,
24836            login: "ShiroKSH".to_owned(),
24837        };
24838        let mut resume = durable_resume_with_run();
24839        resume.request = valid_request();
24840        resume.request_sha256 = canonical_request_sha256(&resume.request).expect("request digest");
24841        let run = resume.run.as_mut().expect("run");
24842        run.status = GithubRunStatusV1::Completed;
24843        run.conclusion = Some(GithubRunConclusionV1::Failure);
24844        resume.state = JobState::CleanupFailed;
24845        resume.cleanup_requested = true;
24846        resume.temporary_ref_deleted = true;
24847
24848        let record = resume
24849            .into_record(&config, &principal, 991)
24850            .expect("negative cleanup evidence remains restorable");
24851        assert_eq!(record.state, JobState::CleanupFailed);
24852        assert!(record.signed_cleanup_evidence.is_none());
24853    }
24854
24855    #[test]
24856    fn event_page_completes_only_after_closed_lifecycle_and_untruncated_delivery() {
24857        let mut config = provider_config(all_authorized());
24858        config.poll_policy =
24859            GithubPollingPolicy::new(1, Duration::from_millis(250)).expect("policy");
24860        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24861            config,
24862            signed_transport(FakeGhRunner::with([Ok(Vec::new())])),
24863            FakePublisher::default(),
24864            NoVerifiedArtifactStore,
24865            FixedClock(1_700_000_000_000),
24866        );
24867        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
24868        let failed = poll_ready(provider.events(
24869            EventRequest {
24870                job_id: "operation-1".to_owned(),
24871                after_sequence: None,
24872                limit: 100,
24873            },
24874            CancellationToken::new(),
24875        ))
24876        .expect("discovery failure");
24877        assert_eq!(failed.state, JobState::Failed);
24878        assert!(!failed.complete);
24879        poll_ready(provider.cleanup(
24880            CleanupRequest {
24881                job_id: "operation-1".to_owned(),
24882                remove_artifacts: false,
24883            },
24884            CancellationToken::new(),
24885        ))
24886        .expect("cleanup closes lifecycle");
24887        let truncated = poll_ready(provider.events(
24888            EventRequest {
24889                job_id: "operation-1".to_owned(),
24890                after_sequence: None,
24891                limit: 1,
24892            },
24893            CancellationToken::new(),
24894        ))
24895        .expect("truncated events");
24896        assert!(truncated.state.is_terminal());
24897        assert!(!truncated.complete);
24898        let complete = poll_ready(provider.events(
24899            EventRequest {
24900                job_id: "operation-1".to_owned(),
24901                after_sequence: None,
24902                limit: 100,
24903            },
24904            CancellationToken::new(),
24905        ))
24906        .expect("complete events");
24907        assert!(complete.state.is_terminal());
24908        assert!(complete.complete);
24909    }
24910
24911    #[test]
24912    fn closed_job_records_are_recycled_at_capacity() {
24913        let mut config = provider_config(all_authorized());
24914        config.max_jobs = 1;
24915        config.poll_policy =
24916            GithubPollingPolicy::new(1, Duration::from_millis(250)).expect("policy");
24917        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24918            config,
24919            signed_transport(FakeGhRunner::with([
24920                Ok(Vec::new()),
24921                Ok(public_source_repository_row()),
24922                Ok(private_repository_row()),
24923                Ok(signing_environment_row(false, true, true)),
24924                Ok(exact_signing_policy_row()),
24925                Ok(exact_signing_secret_rows()),
24926            ])),
24927            FakePublisher::default(),
24928            NoVerifiedArtifactStore,
24929            FixedClock(1_700_000_000_000),
24930        );
24931        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
24932        poll_ready(provider.events(
24933            EventRequest {
24934                job_id: "operation-1".to_owned(),
24935                after_sequence: None,
24936                limit: 100,
24937            },
24938            CancellationToken::new(),
24939        ))
24940        .expect("discovery failure");
24941        poll_ready(provider.cleanup(
24942            CleanupRequest {
24943                job_id: "operation-1".to_owned(),
24944                remove_artifacts: false,
24945            },
24946            CancellationToken::new(),
24947        ))
24948        .expect("closed old job");
24949
24950        let mut replacement = valid_request();
24951        replacement.operation_id = "operation-2".to_owned();
24952        let handle = poll_ready(provider.submit(replacement, CancellationToken::new()))
24953            .expect("closed record recycled");
24954        assert_eq!(handle.job_id, "operation-2");
24955        let state = provider.state.lock().expect("state");
24956        assert_eq!(state.jobs.len(), 1);
24957        assert!(state.jobs.contains_key("operation-2"));
24958    }
24959
24960    #[test]
24961    fn github_success_stays_pending_without_independent_artifact_verifier() {
24962        let responses = [
24963            Ok(completed_run_row("success")),
24964            Ok(completed_run_row("success")),
24965        ];
24966        let provider = GithubBuildProvider::with_artifact_store_and_clock(
24967            provider_config(all_authorized()),
24968            signed_transport(FakeGhRunner::with(responses)),
24969            FakePublisher::default(),
24970            NoVerifiedArtifactStore,
24971            FixedClock(1_700_000_000_000),
24972        );
24973        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
24974        let page = poll_ready(provider.events(
24975            EventRequest {
24976                job_id: "operation-1".to_owned(),
24977                after_sequence: None,
24978                limit: 100,
24979            },
24980            CancellationToken::new(),
24981        ))
24982        .expect("events");
24983        assert_eq!(page.state, JobState::Running);
24984        assert!(!page.complete);
24985        assert!(page.events.iter().any(|event| matches!(
24986            &event.kind,
24987            RemoteBuildEventKind::Warning { code, .. }
24988                if code == "github.artifact_verification_pending"
24989        )));
24990        assert!(!page.events.iter().any(|event| matches!(
24991            event.kind,
24992            RemoteBuildEventKind::OperationFinished { success: true, .. }
24993        )));
24994    }
24995
24996    #[test]
24997    fn signed_terminal_run_cannot_claim_signing_material_cleanup_without_worker_evidence() {
24998        let responses = [
24999            Ok(completed_run_row("success")),
25000            Ok(completed_run_row("success")),
25001            Ok(completed_run_row("success")),
25002            Ok(completed_run_row("success")),
25003            Ok(completed_run_row("success")),
25004        ];
25005        let provider = GithubBuildProvider::with_artifact_store_and_clock(
25006            provider_config(all_authorized()),
25007            signed_transport(FakeGhRunner::with(responses)),
25008            FakePublisher::default(),
25009            FailingArtifactStore,
25010            FixedClock(1_700_000_000_000),
25011        );
25012        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
25013        let error = poll_ready(provider.events(
25014            EventRequest {
25015                job_id: "operation-1".to_owned(),
25016                after_sequence: None,
25017                limit: 100,
25018            },
25019            CancellationToken::new(),
25020        ))
25021        .expect_err("artifact verification fails");
25022        assert!(matches!(
25023            error,
25024            RemoteBuildError::ProviderFailure { code, .. }
25025                if code == "artifact_fixture_rejected"
25026        ));
25027
25028        let confirmation = poll_ready(provider.cleanup(
25029            CleanupRequest {
25030                job_id: "operation-1".to_owned(),
25031                remove_artifacts: false,
25032            },
25033            CancellationToken::new(),
25034        ))
25035        .expect("terminal run cleanup");
25036        assert!(confirmation.workspace_removed);
25037        assert!(!confirmation.signing_material_removed);
25038        let state = provider.state.lock().expect("state");
25039        assert_eq!(
25040            state.jobs.get("operation-1").expect("job").state,
25041            JobState::CleanupFailed
25042        );
25043        drop(state);
25044        assert_eq!(
25045            provider
25046                .publisher
25047                .lock()
25048                .expect("publisher")
25049                .deletions
25050                .len(),
25051            1
25052        );
25053    }
25054
25055    #[test]
25056    fn unsigned_cleanup_failure_can_retry_to_positive_confirmation() {
25057        let mut resume = durable_resume_with_run();
25058        let run = resume.run.as_mut().expect("run");
25059        run.status = GithubRunStatusV1::Completed;
25060        run.conclusion = Some(GithubRunConclusionV1::Cancelled);
25061        resume.state = JobState::CleanupFailed;
25062        resume.cleanup_requested = true;
25063        resume.temporary_ref_deleted = true;
25064        let previous = resume.clone();
25065        let sink = CapturingCheckpointSink::default();
25066        let checkpoints = Arc::clone(&sink.checkpoints);
25067        let provider = GithubBuildProvider::with_artifact_store_and_clock(
25068            provider_config(all_authorized()),
25069            transport(FakeGhRunner::with([
25070                Ok(authenticated_user_row()),
25071                Ok(private_repository_row()),
25072                Ok(authenticated_user_row()),
25073                Ok(private_repository_row()),
25074                Ok(completed_run_row("cancelled")),
25075                Ok(authenticated_user_row()),
25076                Ok(private_repository_row()),
25077                Ok(completed_run_row("cancelled")),
25078                Ok(authenticated_user_row()),
25079                Ok(private_repository_row()),
25080                Ok(completed_run_row("cancelled")),
25081                Ok(authenticated_user_row()),
25082                Ok(private_repository_row()),
25083            ])),
25084            FakePublisher::default(),
25085            NoVerifiedArtifactStore,
25086            FixedClock(1_700_000_000_000),
25087        )
25088        .with_checkpoint_sink(sink);
25089        provider
25090            .restore_job_resumes(vec![resume])
25091            .expect("restore cleanup failure");
25092
25093        let confirmation = poll_ready(provider.cleanup(
25094            CleanupRequest {
25095                job_id: "operation-1".to_owned(),
25096                remove_artifacts: false,
25097            },
25098            CancellationToken::new(),
25099        ))
25100        .expect("retry unsigned cleanup");
25101        assert!(confirmation.workspace_removed);
25102        assert!(confirmation.signing_material_removed);
25103        let latest = checkpoints
25104            .lock()
25105            .expect("checkpoints")
25106            .last()
25107            .cloned()
25108            .expect("positive cleanup checkpoint");
25109        previous
25110            .validate_successor(&latest)
25111            .expect("cleanup retry remains a monotonic successor");
25112        assert_eq!(latest.state, JobState::Cleaned);
25113        assert!(matches!(
25114            latest.events[latest.events.len() - 2].kind,
25115            RemoteBuildEventKind::CleanupStarted
25116        ));
25117        assert!(matches!(
25118            latest.events.last().expect("finish event").kind,
25119            RemoteBuildEventKind::CleanupFinished { .. }
25120        ));
25121    }
25122
25123    #[test]
25124    fn failed_run_produces_typed_terminal_failure() {
25125        let responses = [
25126            Ok(completed_run_row("failure")),
25127            Ok(completed_run_row("failure")),
25128        ];
25129        let provider = GithubBuildProvider::with_artifact_store_and_clock(
25130            provider_config(all_authorized()),
25131            signed_transport(FakeGhRunner::with(responses)),
25132            FakePublisher::default(),
25133            NoVerifiedArtifactStore,
25134            FixedClock(1_700_000_000_000),
25135        );
25136        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
25137        let page = poll_ready(provider.events(
25138            EventRequest {
25139                job_id: "operation-1".to_owned(),
25140                after_sequence: None,
25141                limit: 100,
25142            },
25143            CancellationToken::new(),
25144        ))
25145        .expect("events");
25146        assert_eq!(page.state, JobState::Failed);
25147        assert!(page.events.iter().any(|event| matches!(
25148            &event.kind,
25149            RemoteBuildEventKind::OperationFinished {
25150                success: false,
25151                error: Some(error),
25152                ..
25153            } if error.code == "github.run_failed"
25154        )));
25155    }
25156
25157    #[test]
25158    fn cancellation_is_idempotent_before_run_mapping() {
25159        let provider = GithubBuildProvider::with_artifact_store_and_clock(
25160            provider_config(all_authorized()),
25161            signed_transport(FakeGhRunner::default()),
25162            FakePublisher::default(),
25163            NoVerifiedArtifactStore,
25164            FixedClock(1_700_000_000_000),
25165        );
25166        poll_ready(provider.submit(valid_request(), CancellationToken::new())).expect("submit");
25167        let request = CancellationRequest {
25168            job_id: "operation-1".to_owned(),
25169            reason: "user_requested".to_owned(),
25170        };
25171        let first = poll_ready(provider.cancel(request.clone(), CancellationToken::new()))
25172            .expect("first cancellation");
25173        let second = poll_ready(provider.cancel(request, CancellationToken::new()))
25174            .expect("second cancellation");
25175        assert!(first.accepted);
25176        assert!(!second.accepted);
25177        assert_eq!(second.state, JobState::Cancelling);
25178    }
25179
25180    #[test]
25181    fn unconfigured_artifact_download_is_typed_unsupported() {
25182        let provider = GithubBuildProvider::with_artifact_store_and_clock(
25183            provider_config(all_authorized()),
25184            transport(FakeGhRunner::default()),
25185            FakePublisher::default(),
25186            NoVerifiedArtifactStore,
25187            FixedClock(1_700_000_000_000),
25188        );
25189        let destination = rustferry_remote::ProtocolPath::new(
25190            rustferry_remote::ProtocolPathSemantics::ClientAbsolute,
25191            "/tmp/application.ipa",
25192        )
25193        .expect("destination");
25194        let error = poll_ready(provider.download_artifact(
25195            ArtifactDownloadRequest {
25196                job_id: "operation-1".to_owned(),
25197                artifact_id: "ipa".to_owned(),
25198                destination,
25199            },
25200            CancellationToken::new(),
25201        ))
25202        .expect_err("download unsupported");
25203        assert_eq!(error, unsupported(ProviderFeature::ArtifactDownload));
25204    }
25205}