Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
bc6a21c
Harden Apple release provenance and recovery
PrzemyslawKlys Aug 9, 2026
420b54a
Harden Apple release integrity and source trust
PrzemyslawKlys Aug 9, 2026
d7c749b
Close Apple release integrity gaps
PrzemyslawKlys Aug 9, 2026
ba5bd05
Close remaining Apple release integrity gaps
PrzemyslawKlys Aug 9, 2026
0b12cda
Harden Apple source and upload recovery edges
PrzemyslawKlys Aug 9, 2026
d1f629e
Close final Apple source and adoption gaps
PrzemyslawKlys Aug 9, 2026
8551d5c
Close Apple resume and package trust gaps
PrzemyslawKlys Aug 9, 2026
f7b9809
Close Apple source and recovery integrity gaps
PrzemyslawKlys Aug 9, 2026
f121c58
Harden exact Apple archive provenance
PrzemyslawKlys Aug 9, 2026
3138332
Complete immutable Apple release inputs
PrzemyslawKlys Aug 9, 2026
993cb27
Bind complete Apple mutation policy
PrzemyslawKlys Aug 9, 2026
fa63c89
Complete immutable Apple release execution inputs
PrzemyslawKlys Aug 9, 2026
1f5d833
Harden Apple export and source provenance
PrzemyslawKlys Aug 9, 2026
6037e21
Harden Apple notarization and screenshot replacement integrity
PrzemyslawKlys Aug 9, 2026
c14bf20
Close remaining Apple release integrity gaps
PrzemyslawKlys Aug 9, 2026
c3e9b81
Close remaining Apple exact-source integrity gaps
PrzemyslawKlys Aug 9, 2026
2f9fcf4
Harden Apple source and upload integrity
PrzemyslawKlys Aug 9, 2026
d7dfa7d
Harden Swift package source validation
PrzemyslawKlys Aug 9, 2026
5e38b11
Harden Apple release plan integrity
PrzemyslawKlys Aug 10, 2026
5ffa3ed
Harden Xcode and Swift package source trust
PrzemyslawKlys Aug 10, 2026
7eb947e
Harden Apple source trust parsing
PrzemyslawKlys Aug 10, 2026
aec8c98
Fix Apple release integrity edge cases
PrzemyslawKlys Aug 10, 2026
d33be5d
Harden Apple source and upload provenance
PrzemyslawKlys Aug 10, 2026
fb38cb7
Harden Apple release evidence and dependency trust
PrzemyslawKlys Aug 10, 2026
f86683b
Harden Apple recovery authority and package trust
PrzemyslawKlys Aug 10, 2026
0701f65
Close Apple exact-build trust gaps
PrzemyslawKlys Aug 10, 2026
8753c34
Close final Apple merge blockers
PrzemyslawKlys Aug 10, 2026
2af953e
Harden exact Apple build input boundaries
PrzemyslawKlys Aug 10, 2026
770513c
Seal exact Apple execution inputs
PrzemyslawKlys Aug 10, 2026
006b374
Close final Apple exact-source merge gaps
PrzemyslawKlys Aug 10, 2026
d5ed1de
Harden Apple source and mutation evidence
PrzemyslawKlys Aug 10, 2026
ed54709
Harden Apple source tools and artifact rollback
PrzemyslawKlys Aug 10, 2026
968806f
Reject indirect Swift manifest execution
PrzemyslawKlys Aug 10, 2026
1d8860c
Seal Apple source and notarization evidence
PrzemyslawKlys Aug 10, 2026
26af8e8
Complete Apple source trust validation
PrzemyslawKlys Aug 10, 2026
52a3b19
Close final Apple source and export trust gaps
PrzemyslawKlys Aug 10, 2026
20214f6
Close Apple source and screenshot mutation gaps
PrzemyslawKlys Aug 10, 2026
51527d0
Close Apple source parser input gaps
PrzemyslawKlys Aug 10, 2026
cfb57a7
Close Apple compiler input integrity gaps
PrzemyslawKlys Aug 10, 2026
ec53de7
Close Apple preprocessor plugin and version atomicity gaps
PrzemyslawKlys Aug 10, 2026
2c5df34
Close Apple config export and stapler integrity gaps
PrzemyslawKlys Aug 11, 2026
594fb61
Close Apple compiler and module source-trust gaps
PrzemyslawKlys Aug 11, 2026
98e9666
Bind Apple composite inputs and nested screenshots
PrzemyslawKlys Aug 11, 2026
5962cb1
Bind Apple archive and rollback integrity
PrzemyslawKlys Aug 11, 2026
903718e
Harden final Apple source and notarization boundaries
PrzemyslawKlys Aug 11, 2026
8338243
Bind Swift modules and direct exports at source boundaries
PrzemyslawKlys Aug 11, 2026
88d769b
Close Apple source trust and producer boundary gaps
PrzemyslawKlys Aug 11, 2026
f5b7150
Close remaining Apple input and screenshot approval gaps
PrzemyslawKlys Aug 11, 2026
a24d671
Close Apple source and producer boundary gaps
PrzemyslawKlys Aug 11, 2026
0ba66e7
Close Apple source and screenshot mutation gaps
PrzemyslawKlys Aug 11, 2026
7f997be
Close Apple plugin, assembler, and plan gaps
PrzemyslawKlys Aug 11, 2026
f9dd3a3
Close Apple publication and source trust races
PrzemyslawKlys Aug 11, 2026
7f7575e
Close Apple source and artifact review gaps
PrzemyslawKlys Aug 11, 2026
c4d5bd4
Close Apple preprocessing and source snapshot gaps
PrzemyslawKlys Aug 11, 2026
71940ce
Close Apple source and package cache races
PrzemyslawKlys Aug 11, 2026
5005bfe
Close Apple package producer boundaries
PrzemyslawKlys Aug 11, 2026
9c9180a
Close Apple source verification gaps
PrzemyslawKlys Aug 11, 2026
c4aa559
Fix remaining Apple release integrity gaps
PrzemyslawKlys Aug 11, 2026
bb55fc5
Bind ThinLTO index inputs to exact source
PrzemyslawKlys Aug 11, 2026
054398f
Preserve exact source and private snapshot identity
PrzemyslawKlys Aug 11, 2026
98131ce
Harden Apple source input binding
PrzemyslawKlys Aug 11, 2026
c35d536
Serialize Apple receipt journal updates
PrzemyslawKlys Aug 11, 2026
47345a4
Require complete screenshot approval sets
PrzemyslawKlys Aug 11, 2026
99d3bcf
Harden receipt leases and backup cleanup
PrzemyslawKlys Aug 11, 2026
e0bf8ff
Fix Apple output and filtered source integrity
PrzemyslawKlys Aug 11, 2026
bc6650f
Harden Apple filesystem and snapshot integrity
PrzemyslawKlys Aug 11, 2026
b3f25f8
Harden Apple archive and package boundaries
PrzemyslawKlys Aug 11, 2026
c49bfc1
Harden Apple archive recovery boundaries
PrzemyslawKlys Aug 12, 2026
c781c5e
Close Swift linker and screenshot cleanup gaps
PrzemyslawKlys Aug 12, 2026
01bb37d
Bind Clang offload inputs to exact source
PrzemyslawKlys Aug 12, 2026
9f3c5b3
Harden screenshot snapshots and Metal input trust
PrzemyslawKlys Aug 12, 2026
bb68a9b
Bind approved screenshots and package identities
PrzemyslawKlys Aug 12, 2026
cb578e9
Harden notarization submission continuity
PrzemyslawKlys Aug 12, 2026
9584a74
Harden Apple remote mutation recovery
PrzemyslawKlys Aug 12, 2026
ec93e1e
Close Apple mutation ambiguity gaps
PrzemyslawKlys Aug 12, 2026
2370bc4
Close Apple recovery and path isolation gaps
PrzemyslawKlys Aug 12, 2026
ccb6c48
Close Apple source trust and recovery gaps
PrzemyslawKlys Aug 12, 2026
bb1689a
Complete Apple release input isolation
PrzemyslawKlys Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sourceRoot)) {
$sourceRoot = [IO.Path]::GetFullPath($sourceRoot)

if (-not [string]::IsNullOrWhiteSpace($SourceCommit)) {
if ($SourceCommit -notmatch '^[0-9A-Fa-f]{40}$') { throw 'source-commit must be an exact 40-character commit SHA.' }
if ($SourceCommit -notmatch '^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$') { throw 'source-commit must be a full SHA-1 or SHA-256 Git commit object id.' }
$actualCommit = (& $GitPath -C $sourceRoot rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or -not $actualCommit.Equals($SourceCommit, [StringComparison]::OrdinalIgnoreCase)) {
throw "Checked-out source '$actualCommit' does not match source-commit '$SourceCommit'."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ if (-not [string]::IsNullOrWhiteSpace($env:INPUT_MARKETING_VERSION)) {
$arguments += @('--apple-version', $env:INPUT_MARKETING_VERSION)
}
if (-not [string]::IsNullOrWhiteSpace($env:INPUT_SOURCE_COMMIT)) {
if ($env:INPUT_SOURCE_COMMIT -notmatch '^[0-9A-Fa-f]{40}$') {
throw 'source-commit must be an exact 40-character commit SHA.'
if ($env:INPUT_SOURCE_COMMIT -notmatch '^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$') {
throw 'source-commit must be a full SHA-1 or SHA-256 Git commit object id.'
}
$arguments += @('--apple-source-commit', $env:INPUT_SOURCE_COMMIT)
}
Expand Down
56 changes: 54 additions & 2 deletions Docs/PSPublishModule.AppleRelease.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ submitting a version.
"Automation": {
"WriteReceipt": true,
"ReceiptPath": "build/powerforge/apple/release-receipt.json",
"ReceiptHistoryPath": "build/powerforge/apple/receipts",
"PlanReceiptPath": "build/powerforge/apple/release-plan.json",
"LockPath": "build/powerforge/apple/release.lock",
"VersionSourcePath": "project.yml",
Expand Down Expand Up @@ -254,8 +255,8 @@ Use the same entry point for each transition:
| `Doctor` | Reads release state plus local topology, embedded-product evidence, metadata ownership, App Review details, age rating, pricing, availability, accessibility, encryption, monetization, webhook coverage, and TestFlight feedback. It does not mutate Apple state. |
| `Version` | Updates the configured XcodeGen version source and chooses one build number above both local state and every configured App Store Connect platform. |
| `Archive` | Creates signed archives without uploading. |
| `Upload` | Archives, uploads, waits for processing, and resumes an exact remote build when possible. |
| `UploadExisting` | Uploads existing archives and uses the same resume/wait behavior. |
| `Upload` | Archives, uploads, waits for processing, and resumes an exact remote build only when an immutable receipt binds it to the same source commit and archive SHA-256. |
| `UploadExisting` | Uploads existing archives and uses the same provenance-bound resume and wait behavior. |
| `Prepare` | Creates/updates versions, metadata, app information, build selection, and readiness. |
| `Screenshots` | Validates and syncs configured screenshot sets as a separate, deliberate transition. |
| `TestFlight` | Assigns the processed build to configured groups and testers. |
Expand Down Expand Up @@ -310,6 +311,57 @@ uses a separate plan receipt, checks the exact version/build remotely, and stops
Screenshot replacement is opt-in during `Advance`. Keep `SyncScreenshots=false` when the
protected `powerforge-apple-screenshots.yml` lane owns capture, approval, and immediate sync.

### Receipts and recovery

Each executed Apple action writes an atomic latest receipt and one immutable attempt
receipt under `ReceiptHistoryPath`. The history is append-only: a later `Status` or
`Doctor` run does not erase the upload or notarization evidence needed by a retry.
Receipts contain a canonical hash and previous-receipt hash; PowerForge rejects changed,
missing, forked, linked, or otherwise incomplete history before it trusts that evidence.
Canonical hashes are derived from the JSON properties actually stored, so adding an
optional receipt field in a later PowerForge version does not invalidate old evidence.
The first schema-v4 write also preserves a schema-v3 latest receipt in history before
replacing it.

Mutating actions validate the complete journal and append a `Started` checkpoint before
their first side effect. Upload and direct notarization append `UploadAttested` or
`NotarizationAttested` immediately after the external tool succeeds, before processing
polls, stapling, readiness checks, or cleanup can fail. A final `Completed` receipt then
records the reconciled outcome. This means a terminated process can be resumed from the
last durable fact without guessing whether the external mutation happened.

For App Store uploads, the original attempt records the exact source commit, archive
path, archive SHA-256, build id or build-upload id, and attestation attempt id. A matching
version/build in App Store Connect is not enough by itself. If no retained attempt proves
that PowerForge uploaded that binary from the current source, the action stops instead
of silently treating an unrelated binary as the current release.

Receipt hashes detect corruption and preserve continuity, but receipt files owned by the
same local account are not treated as operator authority. Resuming an upload or accepted
notarization in a later process therefore requires explicit adoption and confirmation,
even when the matching receipt is present. This prevents a writable local journal from
silently authorizing a remote release mutation.

Run real publication through `scripts/Invoke-PinnedPowerForge.ps1`; it supplies the exact
consumer commit automatically. Direct CLI callers should pass the same full 40-character
SHA-1 or 64-character SHA-256 Git object ID with `--apple-source-commit`. If an older binary
must be recovered and independent evidence has already established its identity, adoption
is available as an explicit exception:

```text
powerforge apple-release Upload --config powerforge.release.json --apple-source-commit <exact-commit> --apple-adopt-existing-build --plan --summary --output json
powerforge apple-release Upload --config powerforge.release.json --apple-source-commit <exact-commit> --apple-adopt-existing-build --confirm-apple-action --summary --output json
```

Adoption records the deliberate recovery decision. When no matching upload attestation
exists, it also emits `APPLE_BUILD_ADOPTED_WITHOUT_UPLOAD_ATTESTATION`; when attestation
does exist, it is retained as continuity evidence rather than being promoted to authority.
Prefer a new build number and a fresh upload whenever that is possible.

`CleanupAfterProcessing` removes only artifacts older than `ArtifactRetentionDays` after
the remote build is valid. It deliberately retains the current archive and export so a
successful upload does not immediately destroy its local evidence.

## Commercial and compliance governance

Use one checked-in governance file per App Store Connect app. The file declares only
Expand Down
45 changes: 26 additions & 19 deletions PSPublishModule/Cmdlets/InvokePowerForgeReleaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text.Json;
using System.Text.Json.Serialization;
using PowerForge;
using PowerForge.ConsoleShared;

Expand Down Expand Up @@ -256,13 +254,33 @@ public sealed partial class InvokePowerForgeReleaseCommand : PSCmdlet
[Parameter]
public PowerForgeAppleReleaseAction AppleAction { get; set; } = PowerForgeAppleReleaseAction.Configured;

/// <summary>Overrides the Apple marketing version selected for this operation.</summary>
[Parameter]
public string? AppleVersion { get; set; }

/// <summary>Binds Apple release evidence to a full 40-character SHA-1 or 64-character SHA-256 source commit.</summary>
[Parameter]
public string? AppleSourceCommit { get; set; }

/// <summary>Requires the persisted Apple plan receipt to match this exact SHA-256.</summary>
[Parameter]
public string? AppleExpectedPlanSha256 { get; set; }

/// <summary>
/// Explicitly confirms a risky Apple screenshot replacement, review submission, or public release action.
/// </summary>
[Parameter]
public SwitchParameter ConfirmAppleAction { get; set; }

/// <summary>Forces exact remote-build reuse on this run.</summary>
/// <summary>
/// Explicitly adopts a verified remote Apple build or accepted notarization operation.
/// Local receipts provide continuity evidence but never authorize cross-process recovery by themselves;
/// this switch requires ConfirmAppleAction and is intended only for deliberate recovery.
/// </summary>
[Parameter]
public SwitchParameter AdoptExistingAppleBuild { get; set; }

/// <summary>Enables recovery discovery; deliberate cross-process reuse also requires AdoptExistingAppleBuild and ConfirmAppleAction.</summary>
[Parameter]
public SwitchParameter AppleResume { get; set; }

Expand Down Expand Up @@ -663,22 +681,7 @@ private static IEnumerable<string> EnumerateSelfAndParents(string startDirectory
}

private static PowerForgeReleaseSpec LoadConfig(string configPath)
{
var json = File.ReadAllText(configPath);
var options = new JsonSerializerOptions
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true
};
options.Converters.Add(new JsonStringEnumConverter());

var spec = JsonSerializer.Deserialize<PowerForgeReleaseSpec>(json, options);
if (spec is null)
throw new InvalidOperationException($"Unable to deserialize unified release config: {configPath}");

return spec;
}
=> PowerForgeReleaseService.LoadConfiguration(configPath);

private static bool? ResolveRequestedFlag(IDictionary<string, object>? boundParameters, string parameterName)
{
Expand Down Expand Up @@ -811,7 +814,11 @@ private PowerForgeReleaseInvocationOptions BuildInvocationOptions(IDictionary<st
PackageSignStore = NormalizeNullable(PackageSignStore),
PackageSignTimestampUrl = NormalizeNullable(PackageSignTimestampUrl),
AppleAction = AppleAction,
AppleMarketingVersion = NormalizeNullable(AppleVersion),
AppleSourceCommit = NormalizeNullable(AppleSourceCommit),
AppleExpectedPlanSha256 = NormalizeNullable(AppleExpectedPlanSha256),
AppleActionConfirmed = ConfirmAppleAction.IsPresent,
AppleAdoptExistingBuild = AdoptExistingAppleBuild.IsPresent,
AppleResume = ResolveMutuallyExclusiveFlag(
boundParameters,
nameof(AppleResume),
Expand Down
8 changes: 8 additions & 0 deletions PSPublishModule/Cmdlets/PowerForgeReleaseInvocationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,16 @@ internal sealed class PowerForgeReleaseInvocationOptions

public PowerForgeAppleReleaseAction AppleAction { get; set; } = PowerForgeAppleReleaseAction.Configured;

public string? AppleMarketingVersion { get; set; }

public string? AppleSourceCommit { get; set; }

public string? AppleExpectedPlanSha256 { get; set; }

public bool AppleActionConfirmed { get; set; }

public bool AppleAdoptExistingBuild { get; set; }

public bool? AppleResume { get; set; }

public bool? AppleWaitForProcessing { get; set; }
Expand Down
11 changes: 11 additions & 0 deletions PSPublishModule/Cmdlets/PowerForgeReleaseRequestMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ internal static PowerForgeReleaseRequest Build(
request.EnableSigning = ChooseBool(request.EnableSigning, options.EnableSigning);
request.AppleResume = ChooseBool(request.AppleResume, options.AppleResume);
request.AppleWaitForProcessing = ChooseBool(request.AppleWaitForProcessing, options.AppleWaitForProcessing);
request.AppleAdoptExistingBuild = request.AppleAdoptExistingBuild || options.AppleAdoptExistingBuild;
request.AppleMarketingVersion = ChooseString(request.AppleMarketingVersion, options.AppleMarketingVersion);
request.AppleSourceCommit = ChooseString(request.AppleSourceCommit, options.AppleSourceCommit);
Comment thread
PrzemyslawKlys marked this conversation as resolved.
request.RequireImmutableAppleSourceSnapshot = request.RequireImmutableAppleSourceSnapshot ||
!string.IsNullOrWhiteSpace(request.AppleSourceCommit);
request.AppleExpectedPlanSha256 = ChooseString(request.AppleExpectedPlanSha256, options.AppleExpectedPlanSha256);

request.SkipWorkspaceValidation = request.SkipWorkspaceValidation || options.SkipWorkspaceValidation;
request.SkipRestore = request.SkipRestore || options.SkipRestore;
Expand Down Expand Up @@ -208,7 +214,12 @@ private static PowerForgeReleaseRequest Clone(PowerForgeReleaseRequest? source)
ToolOutputs = source.ToolOutputs.ToArray(),
SkipToolOutputs = source.SkipToolOutputs.ToArray(),
AppleAction = source.AppleAction,
AppleMarketingVersion = source.AppleMarketingVersion,
AppleSourceCommit = source.AppleSourceCommit,
RequireImmutableAppleSourceSnapshot = source.RequireImmutableAppleSourceSnapshot,
AppleExpectedPlanSha256 = source.AppleExpectedPlanSha256,
AppleActionConfirmed = source.AppleActionConfirmed,
AppleAdoptExistingBuild = source.AppleAdoptExistingBuild,
AppleResume = source.AppleResume,
AppleWaitForProcessing = source.AppleWaitForProcessing,
AppleProcessingTimeoutSeconds = source.AppleProcessingTimeoutSeconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public sealed class SyncAppStoreConnectScreenshotsCommand : AsyncPSCmdlet

/// <summary>Exact source commit expected by the reviewed approval manifest.</summary>
[Parameter]
[ValidatePattern("^[0-9A-Fa-f]{40}$")]
[ValidatePattern("^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$")]
public string? SourceCommit { get; set; }

/// <summary>Syncs local screenshot folders to App Store Connect screenshot sets.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public sealed class TestAppStoreConnectScreenshotSyncConfigCommand : PSCmdlet

/// <summary>Exact source commit expected by the reviewed approval manifest.</summary>
[Parameter]
[ValidatePattern("^[0-9A-Fa-f]{40}$")]
[ValidatePattern("^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$")]
public string? SourceCommit { get; set; }

/// <summary>Validates the screenshot sync configuration.</summary>
Expand Down
2 changes: 2 additions & 0 deletions PowerForge.Cli/AppleReleaseCliSummary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal sealed class AppleReleaseCliPlanSummary

public bool Resume { get; set; }

public bool AdoptExistingBuild { get; set; }

public bool WaitForProcessing { get; set; }

public int ProcessingTimeoutSeconds { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions PowerForge.Cli/Program.Command.AppleRelease.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ internal static partial class Program
"Usage: powerforge apple-release <Status|Doctor|Version|Archive|Upload|UploadExisting|Prepare|Screenshots|TestFlight|Advance|SubmitTestFlightReview|SubmitAppReview|Release|Cleanup> " +
"[--config <release.json>] [--plan] [--validate] [--confirm-apple-action] " +
"[--apple-version <marketing-version-or-X-pattern>] [--apple-source-commit <sha>] [--apple-expected-plan-sha256 <sha256>] " +
"[--apple-adopt-existing-build] " +
"[--apple-resume|--no-apple-resume] [--apple-wait|--no-apple-wait] " +
"[--apple-timeout-seconds <seconds>] [--apple-poll-seconds <seconds>] " +
"[--target <Name[,Name...]>] [--summary] [--output json]";
Expand Down Expand Up @@ -73,6 +74,7 @@ private static void ValidateAppleReleaseArguments(string[] argv)
"--dry-run",
"--validate",
"--confirm-apple-action",
"--apple-adopt-existing-build",
"--apple-resume",
"--no-apple-resume",
"--apple-wait",
Expand Down
4 changes: 2 additions & 2 deletions PowerForge.Cli/Program.Command.AppleScreenshots.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ static string RequiredString(JsonElement value, string name)
var captureRunId = RequiredString(root, "captureRunId");

var sourceCommit = RequiredString(root, "sourceCommit").ToLowerInvariant();
if (sourceCommit.Length != 40 || !sourceCommit.All(Uri.IsHexDigit))
throw new InvalidOperationException("Screenshot capture provenance SourceCommit must be an exact 40-character Git commit SHA.");
if (!GitObjectId.IsFull(sourceCommit))
throw new InvalidOperationException("Screenshot capture provenance SourceCommit must be a full SHA-1 or SHA-256 Git commit object id.");

if (!root.TryGetProperty("screenshots", out var screenshotsElement) || screenshotsElement.ValueKind != JsonValueKind.Array)
throw new InvalidOperationException("Screenshot capture provenance must contain an exact screenshots inventory.");
Expand Down
Loading
Loading