gowalk-cicd 1.0.184 → 1.0.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/action/scripts/apple_upload_handoff.cjs +4 -9
- package/action/scripts/apple_upload_handoff_history.cjs +29 -0
- package/action/scripts/apple_upload_proof.cjs +1 -1
- package/action/scripts/apple_upload_proof.py +2 -0
- package/action/scripts/apple_upload_recovery.cjs +14 -7
- package/action/scripts/apple_upload_recovery_evidence.cjs +38 -0
- package/docs/CONFIGURATION.md +5 -1
- package/package.json +1 -1
- package/remedies.json +2 -2
- package/src/config/workflows.mjs +12 -6
- package/templates/apple-upload-recovery.yml +96 -0
- package/templates/history.json.gz +0 -0
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ Writes three core things into your repo:
|
|
|
25
25
|
|
|
26
26
|
It also writes:
|
|
27
27
|
|
|
28
|
+
- `.github/workflows/apple-upload-recovery.yml` — explicit verification-only recovery for a retained iOS upload;
|
|
29
|
+
it checks the original producer and Apple build before completing that deployment, without rebuilding or uploading
|
|
28
30
|
- `.github/workflows/mobile-candidate.yml` — shipping binaries for ready task PRs
|
|
29
31
|
(dispatch-only while candidate artifacts are off)
|
|
30
32
|
- `.github/workflows/warm-android-cache.yml` — only while Android candidates are enabled: after this
|
|
@@ -1225,7 +1227,10 @@ commits completed part transfers with `uploaded: true`, following Apple's
|
|
|
1225
1227
|
The IPA endpoint rejected an optional `sourceFileChecksums` SHA_256 object even though
|
|
1226
1228
|
the generic schema lists that algorithm; the uploader no longer sends that attribute.
|
|
1227
1229
|
A completed file is reused only when its content-derived filename and size match, and
|
|
1228
|
-
any returned file checksum is verified.
|
|
1230
|
+
any nonnull returned whole-file checksum is verified. An explicit
|
|
1231
|
+
`sourceFileChecksums.file: null` means that the optional whole-file checksum is absent;
|
|
1232
|
+
a composite checksum does not substitute for a whole-IPA digest.
|
|
1233
|
+
New receipts retain locally computed SHA-256 and
|
|
1229
1234
|
MD5 so Apple's returned checksum can be compared with the exact transferred bytes.
|
|
1230
1235
|
|
|
1231
1236
|
An upload summary can remain `PROCESSING` after Apple exposes a valid eligible build.
|
|
@@ -1248,8 +1253,11 @@ different bytes under the same number. Build numbers come from `/builds`,
|
|
|
1248
1253
|
`/preReleaseVersions` **and** `/apps/{id}/buildUploads`, so an unfinished or refused
|
|
1249
1254
|
reservation is never reused; delete such a reservation only after reading it back.
|
|
1250
1255
|
|
|
1251
|
-
When an iOS
|
|
1252
|
-
`Retain Apple verification handoff` succeeded, version 1.0.
|
|
1256
|
+
When an iOS deployment failed or was cancelled **after** both `Build and transfer iOS binary`
|
|
1257
|
+
and `Retain Apple verification handoff` succeeded, version 1.0.185 provides the managed
|
|
1258
|
+
`apple-upload-recovery.yml` workflow. Dispatch it on the repository's default branch with the
|
|
1259
|
+
original `producer-run`, `producer-attempt`, `deployment-id` and `handoff-artifact-id` inputs.
|
|
1260
|
+
It calls the released helper with those four validated identifiers:
|
|
1253
1261
|
|
|
1254
1262
|
```bash
|
|
1255
1263
|
node .github/actions/swift-app/scripts/apple_upload_recovery.cjs \
|
|
@@ -1296,7 +1304,8 @@ digest option: it downloads the exact original artifact, verifies GitHub's archi
|
|
|
1296
1304
|
and accepts only its bounded `apple-verification.json` member. It authenticates the current
|
|
1297
1305
|
dispatch separately from the original run, attempt, source, repository, branch and deployment.
|
|
1298
1306
|
|
|
1299
|
-
Recovery requires a terminal
|
|
1307
|
+
Recovery requires a terminal original run: either cancelled with its cancelled iOS producer,
|
|
1308
|
+
or failed with a completed successful or failed iOS producer. Both paths require those two
|
|
1300
1309
|
successful steps in order. A later completed attempt may exist; an active latest attempt
|
|
1301
1310
|
refuses, including a retry observed immediately before writing the receipt. Fresh Apple
|
|
1302
1311
|
readback must prove the exact completed upload and file, matching bytes, eligible `VALID`
|
|
@@ -1306,10 +1315,13 @@ and the old handoff's metadata and release notes are never replayed. Missing, ex
|
|
|
1306
1315
|
mismatched evidence remains refused; do not relabel a receipt or impersonate the original
|
|
1307
1316
|
`GITHUB_*` environment. Normal delivery still refuses cancelled producers.
|
|
1308
1317
|
|
|
1318
|
+
The managed workflow retains `apple-recovered-<recovery-run>-<recovery-attempt>` with the
|
|
1319
|
+
unchanged `apple-verification.json`, its `.verified.json` proof and derived `.upload-receipt.json`.
|
|
1320
|
+
The proof identifies the original producer and the current recovery caller separately.
|
|
1309
1321
|
Retain the recovery proof, then rerun the **whole original deployment workflow**. Its
|
|
1310
1322
|
upload-once admission reuses the confirmed iOS receipt and skips the iOS binary. Follow that
|
|
1311
1323
|
new attempt and any unrelated remaining jobs to completion; recovery does not change the
|
|
1312
|
-
original cancelled attempt's conclusion. Read current store state for remaining submission
|
|
1324
|
+
original failed or cancelled attempt's conclusion. Read current store state for remaining submission
|
|
1313
1325
|
work without changing an already accepted submission. This exceptional command adds no
|
|
1314
1326
|
normal delivery job, polling or latency.
|
|
1315
1327
|
|
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const {createHash} = require('node:crypto');
|
|
5
5
|
const {join} = require('node:path');
|
|
6
6
|
const github = require('./mobile_release_api.cjs');
|
|
7
|
+
const {historicalProducer, uniqueArtifact} = require('./apple_upload_handoff_history.cjs');
|
|
7
8
|
const SCHEMA = 'gowalk-cicd/apple-upload-handoff.v1';
|
|
8
9
|
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
9
10
|
const id = value => typeof value === 'string' && /^[A-Za-z0-9-]{1,64}$/.test(value);
|
|
@@ -64,26 +65,20 @@ async function verifyProducer(value, artifactId, api = github, recoverCancelled
|
|
|
64
65
|
run.path !== '.github/workflows/deploy.yml' || run.event !== o.event ||
|
|
65
66
|
`refs/heads/${run.head_branch}` !== o.ref || run.repository?.full_name !== o.repository ||
|
|
66
67
|
run.head_repository?.full_name !== o.repository) fail('producer_run');
|
|
67
|
-
if (recoverCancelled)
|
|
68
|
-
const latest = await api.request('GET', `/actions/runs/${o.run_id}`);
|
|
69
|
-
if (run.status !== 'completed' || run.conclusion !== 'cancelled' || latest.id !== o.run_id ||
|
|
70
|
-
latest.status !== 'completed' || !positive(latest.run_attempt) || latest.run_attempt < o.run_attempt ||
|
|
71
|
-
latest.head_sha !== o.source_sha || latest.path !== run.path || latest.event !== o.event ||
|
|
72
|
-
latest.head_branch !== run.head_branch || latest.repository?.full_name !== o.repository ||
|
|
73
|
-
latest.head_repository?.full_name !== o.repository) fail('cancelled_producer_run');
|
|
74
|
-
}
|
|
68
|
+
if (recoverCancelled) await historicalProducer(o, run, api, fail);
|
|
75
69
|
const result = await api.request('GET', `/actions/runs/${o.run_id}/attempts/${o.run_attempt}/jobs?per_page=100`);
|
|
76
70
|
if (!Array.isArray(result.jobs) || result.jobs.length >= 100) fail('producer_jobs');
|
|
77
71
|
const jobs = result.jobs.filter(job => job.name === 'iOS TestFlight' || job.name?.endsWith('/ iOS TestFlight'));
|
|
78
72
|
// A later symbol-retention failure does not undo these exact completed steps.
|
|
79
73
|
// The producer's original failure remains red; only Apple delivery is reconciled here.
|
|
80
|
-
const conclusions = recoverCancelled ? ['cancelled'] : ['success', 'failure'];
|
|
74
|
+
const conclusions = recoverCancelled && run.conclusion === 'cancelled' ? ['cancelled'] : ['success', 'failure'];
|
|
81
75
|
if (jobs.length !== 1 || jobs[0].status !== 'completed' || !conclusions.includes(jobs[0].conclusion) ||
|
|
82
76
|
jobs[0].head_sha !== o.source_sha || !transferred(jobs[0])) fail('producer_job');
|
|
83
77
|
const artifact = await api.request('GET', `/actions/artifacts/${artifactId}`);
|
|
84
78
|
if (artifact.id !== artifactId || artifact.expired !== false || artifact.workflow_run?.id !== o.run_id ||
|
|
85
79
|
artifact.workflow_run?.head_sha !== o.source_sha ||
|
|
86
80
|
artifact.name !== `apple-verification-${o.run_id}-${o.run_attempt}`) fail('producer_artifact');
|
|
81
|
+
if (recoverCancelled) await uniqueArtifact(o, artifact, api, fail);
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
async function phase(value, api = github) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Recheck one terminal historical producer and its unique retained handoff. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
4
|
+
|
|
5
|
+
async function historicalProducer(owner, run, api, fail) {
|
|
6
|
+
const latest = await api.request('GET', `/actions/runs/${owner.run_id}`);
|
|
7
|
+
const terminal = ['success', 'failure', 'cancelled', 'timed_out', 'action_required',
|
|
8
|
+
'neutral', 'skipped', 'stale', 'startup_failure'];
|
|
9
|
+
if (run.status !== 'completed' || !['cancelled', 'failure'].includes(run.conclusion) ||
|
|
10
|
+
latest.id !== owner.run_id || latest.status !== 'completed' || !terminal.includes(latest.conclusion) ||
|
|
11
|
+
!positive(latest.run_attempt) || latest.run_attempt < owner.run_attempt ||
|
|
12
|
+
latest.run_attempt === owner.run_attempt && latest.conclusion !== run.conclusion ||
|
|
13
|
+
latest.head_sha !== owner.source_sha || latest.path !== run.path || latest.event !== owner.event ||
|
|
14
|
+
latest.head_branch !== run.head_branch || latest.repository?.full_name !== owner.repository ||
|
|
15
|
+
latest.head_repository?.full_name !== owner.repository) fail('historical_producer_run');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function uniqueArtifact(owner, artifact, api, fail) {
|
|
19
|
+
const result = await api.request('GET', `/actions/runs/${owner.run_id}/artifacts?per_page=100`);
|
|
20
|
+
if (!Array.isArray(result.artifacts) || result.artifacts.length >= 100 ||
|
|
21
|
+
!Number.isSafeInteger(result.total_count) || result.total_count !== result.artifacts.length) fail('producer_artifact');
|
|
22
|
+
const selected = result.artifacts.filter(row => row.name === artifact.name);
|
|
23
|
+
if (selected.length !== 1 || selected[0].id !== artifact.id || selected[0].expired !== false ||
|
|
24
|
+
selected[0].workflow_run?.id !== owner.run_id || selected[0].workflow_run?.head_sha !== owner.source_sha ||
|
|
25
|
+
!/^sha256:[a-f0-9]{64}$/.test(artifact.digest || '') || selected[0].digest !== artifact.digest ||
|
|
26
|
+
!positive(artifact.size_in_bytes) || selected[0].size_in_bytes !== artifact.size_in_bytes) fail('producer_artifact');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {historicalProducer, uniqueArtifact};
|
|
@@ -25,7 +25,7 @@ function checksum(attributes, receipt, fail) {
|
|
|
25
25
|
fail('file_checksum_unverified');
|
|
26
26
|
}
|
|
27
27
|
const file = checksums?.file;
|
|
28
|
-
if (file === undefined) return;
|
|
28
|
+
if (file === undefined || file === null) return;
|
|
29
29
|
const digest = file?.algorithm === 'SHA_256' ? receipt.sha256 :
|
|
30
30
|
file?.algorithm === 'MD5' ? receipt.md5 : null;
|
|
31
31
|
const width = file?.algorithm === 'SHA_256' ? 64 : 32;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Explicit Actions recovery of a
|
|
1
|
+
/* Explicit Actions recovery of a terminal producer's authenticated handoff; never upload or metadata. */
|
|
2
2
|
'use strict';
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const {createHash} = require('node:crypto');
|
|
@@ -6,6 +6,7 @@ const {inflateRawSync} = require('node:zlib');
|
|
|
6
6
|
const github = require('./mobile_release_api.cjs');
|
|
7
7
|
const handoff = require('./apple_upload_handoff.cjs');
|
|
8
8
|
const verifier = require('./apple_upload_verify.cjs');
|
|
9
|
+
const {pinnedArtifact, evidence} = require('./apple_upload_recovery_evidence.cjs');
|
|
9
10
|
const LIMIT = 2 * 1024 * 1024;
|
|
10
11
|
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
11
12
|
const fail = code => handoff.fail(`recovery_${code}`);
|
|
@@ -83,7 +84,8 @@ async function current(env, api) {
|
|
|
83
84
|
run.repository?.full_name !== repository || run.head_repository?.full_name !== repository ||
|
|
84
85
|
!/^\.github\/workflows\/[\w.-]+\.ya?ml$/.test(run.path || '') ||
|
|
85
86
|
env.GITHUB_WORKFLOW_REF !== `${repository}/${run.path}@${env.GITHUB_REF}`) fail('caller_unverified');
|
|
86
|
-
return {run_id: runId, run_attempt: attempt, repository, ref: env.GITHUB_REF
|
|
87
|
+
return {run_id: runId, run_attempt: attempt, repository, ref: env.GITHUB_REF,
|
|
88
|
+
source_sha: env.GITHUB_SHA, workflow_path: run.path, event: run.event};
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
async function recover({runId, attempt, deploymentId, artifactId}, dependencies = {}) {
|
|
@@ -91,7 +93,7 @@ async function recover({runId, attempt, deploymentId, artifactId}, dependencies
|
|
|
91
93
|
const env = dependencies.env || process.env, api = dependencies.api || github;
|
|
92
94
|
const caller = await current(env, api);
|
|
93
95
|
if (runId === caller.run_id) fail('caller_invalid');
|
|
94
|
-
const artifact = await api.request('GET', `/actions/artifacts/${artifactId}`);
|
|
96
|
+
const artifact = structuredClone(await api.request('GET', `/actions/artifacts/${artifactId}`));
|
|
95
97
|
if (artifact.id !== artifactId || artifact.expired !== false || artifact.workflow_run?.id !== runId ||
|
|
96
98
|
artifact.name !== `apple-verification-${runId}-${attempt}`) fail('artifact_identity');
|
|
97
99
|
const bytes = await download(artifact, env, dependencies.fetch || fetch);
|
|
@@ -101,20 +103,25 @@ async function recover({runId, attempt, deploymentId, artifactId}, dependencies
|
|
|
101
103
|
if (owner.repository !== caller.repository || owner.ref !== caller.ref || owner.run_id !== runId ||
|
|
102
104
|
owner.run_attempt !== attempt || owner.deployment_id !== deploymentId ||
|
|
103
105
|
owner.source_sha !== artifact.workflow_run?.head_sha) fail('handoff_owner');
|
|
104
|
-
const result = await verifier.verify(value, artifactId, 900,
|
|
105
|
-
|
|
106
|
+
const result = await verifier.verify(value, artifactId, 900,
|
|
107
|
+
{...dependencies, api: pinnedArtifact(api, artifact), recoverCancelled: true});
|
|
108
|
+
const proof = {...result, schema: 'gowalk-cicd/apple-upload-recovered.v1', producer_run: runId,
|
|
106
109
|
repository: owner.repository, producer_source_sha: owner.source_sha,
|
|
107
110
|
producer_ref: owner.ref, producer_event: owner.event,
|
|
108
111
|
artifact_id: artifactId, artifact_digest: artifact.digest,
|
|
109
112
|
handoff_sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
110
113
|
ipa_sha256: value.receipt.sha256, file_id: value.receipt.file_id,
|
|
111
|
-
recovery_run: caller.run_id, recovery_attempt: caller.run_attempt
|
|
114
|
+
recovery_run: caller.run_id, recovery_attempt: caller.run_attempt,
|
|
115
|
+
recovery_source_sha: caller.source_sha, recovery_ref: caller.ref,
|
|
116
|
+
recovery_event: caller.event, recovery_workflow_path: caller.workflow_path};
|
|
117
|
+
if (dependencies.writeEvidence === true) Object.assign(proof, evidence(bytes, value, proof, env));
|
|
118
|
+
return proof;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
async function main() {
|
|
115
122
|
if (process.argv.length !== 6) fail('arguments');
|
|
116
123
|
const [runId, attempt, deploymentId, artifactId] = process.argv.slice(2).map(Number);
|
|
117
|
-
console.log(JSON.stringify(await recover({runId, attempt, deploymentId, artifactId})));
|
|
124
|
+
console.log(JSON.stringify(await recover({runId, attempt, deploymentId, artifactId}, {writeEvidence: true})));
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
if (require.main === module) main().catch(error => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Preserve the exact authenticated handoff and record separately owned verification evidence. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const {join} = require('node:path');
|
|
5
|
+
const handoff = require('./apple_upload_handoff.cjs');
|
|
6
|
+
const fail = code => handoff.fail(`recovery_${code}`);
|
|
7
|
+
|
|
8
|
+
function pinnedArtifact(api, original) {
|
|
9
|
+
return {...api, request: async (method, path, ...args) => {
|
|
10
|
+
const result = await api.request(method, path, ...args);
|
|
11
|
+
if (path === `/actions/artifacts/${original.id}` && (result.id !== original.id ||
|
|
12
|
+
result.digest !== original.digest || result.size_in_bytes !== original.size_in_bytes ||
|
|
13
|
+
result.expired !== false || result.name !== original.name ||
|
|
14
|
+
result.workflow_run?.id !== original.workflow_run.id ||
|
|
15
|
+
result.workflow_run?.head_sha !== original.workflow_run.head_sha)) fail('artifact_changed');
|
|
16
|
+
return result;
|
|
17
|
+
}};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function evidence(bytes, value, proof, env) {
|
|
21
|
+
if (!env.RUNNER_TEMP || /[\r\n]/.test(env.RUNNER_TEMP)) fail('evidence_path');
|
|
22
|
+
const directory = join(env.RUNNER_TEMP, 'apple-upload-recovery');
|
|
23
|
+
fs.mkdirSync(directory, {recursive: true, mode: 0o700});
|
|
24
|
+
const original = join(directory, 'apple-verification.json');
|
|
25
|
+
const receipt = `${original}.upload-receipt.json`, verified = `${original}.verified.json`;
|
|
26
|
+
fs.writeFileSync(original, bytes, {mode: 0o600});
|
|
27
|
+
if (!fs.readFileSync(original).equals(bytes)) fail('evidence_identity');
|
|
28
|
+
fs.writeFileSync(receipt, JSON.stringify({...value.receipt, stage: 'complete', state: 'COMPLETE',
|
|
29
|
+
build_id: proof.build_id, upload_resource_state: proof.upload_resource_state,
|
|
30
|
+
processing_seconds: proof.processing_seconds}, null, 2) + '\n', {mode: 0o600});
|
|
31
|
+
fs.writeFileSync(verified, JSON.stringify(proof, null, 2) + '\n', {mode: 0o600});
|
|
32
|
+
if (env.GITHUB_OUTPUT) fs.appendFileSync(env.GITHUB_OUTPUT,
|
|
33
|
+
`verified=true\nbuild-id=${proof.build_id}\nartifact-directory=${directory}\n` +
|
|
34
|
+
`handoff-path=${original}\nreceipt-path=${receipt}\nverified-proof-path=${verified}\n`);
|
|
35
|
+
return {artifact_directory: directory, handoff_path: original, receipt_path: receipt, verified_proof_path: verified};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {pinnedArtifact, evidence};
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -62,7 +62,11 @@ Compose service must pass `BACKEND_PUBLIC_BASE_URL` into its container and use i
|
|
|
62
62
|
The same named repository inputs reach the feature-branch backend preview in `deploy.yml`.
|
|
63
63
|
|
|
64
64
|
The supported managed files are `deploy.yml`, `mobile-candidate.yml`,
|
|
65
|
-
`warm-android-cache.yml`, `deploy-web.yml` and `deploy-backend.yml`.
|
|
65
|
+
`apple-upload-recovery.yml`, `warm-android-cache.yml`, `deploy-web.yml` and `deploy-backend.yml`.
|
|
66
|
+
The installer adds the dispatch-only `apple-upload-recovery.yml` when the rendered mobile
|
|
67
|
+
deployment supports iOS; explicit Android-only deployment excludes it. It preserves recovery
|
|
68
|
+
workflow overrides and removes only an unmodified generated copy when iOS is no longer supported.
|
|
69
|
+
The installer no longer adds
|
|
66
70
|
`deploy-web.yml`, and adds `warm-android-cache.yml` only while candidate artifacts are enabled
|
|
67
71
|
and `mobile.platforms` is not `ios`;
|
|
68
72
|
it removes such a file that is still the unmodified generated one with no overrides,
|
package/package.json
CHANGED
package/remedies.json
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
{
|
|
27
27
|
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
|
28
28
|
"code": "mobile_release_upload_unconfirmed_read_back_retained_binary_before_retry",
|
|
29
|
-
"fixed_in": "1.0.
|
|
30
|
-
"remedy": "Preserve the original upload owner and artifacts. If
|
|
29
|
+
"fixed_in": "1.0.185",
|
|
30
|
+
"remedy": "Preserve the original upload owner and artifacts. If the original iOS deployment failed or was cancelled after Build and transfer iOS binary and Retain Apple verification handoff succeeded in order, dispatch the managed apple-upload-recovery.yml on the default branch with original producer-run, producer-attempt, deployment-id and handoff-artifact-id. It authenticates the original artifact and unchanged handoff, terminal original and latest run, owner, and fresh Apple upload/file/build/app evidence. A null optional file checksum is absent; present whole-file checksums remain strictly verified. It never rebuilds, uploads, replays metadata or rewrites the original handoff. Retain the original handoff and derived completion proof with distinct current recovery caller, then rerun the whole original deployment: admission reuses the uploaded receipt without another iOS binary. The old failure or cancellation remains historical evidence; follow the new attempt to completion. Missing or mismatched evidence remains refused."
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
package/src/config/workflows.mjs
CHANGED
|
@@ -36,11 +36,11 @@ function render(source, normalized, file) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const WARM = 'warm-android-cache.yml';
|
|
39
|
-
const
|
|
39
|
+
const RECOVERY = 'apple-upload-recovery.yml';
|
|
40
|
+
const MANAGED = ['deploy.yml', 'mobile-candidate.yml', 'deploy-web.yml', 'deploy-backend.yml', WARM, RECOVERY];
|
|
40
41
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// Anything customized stays and keeps upgrading.
|
|
42
|
+
// Retire generated workflows only while their locked bytes are unchanged and they have no overrides.
|
|
43
|
+
// App-owned customizations stay and keep upgrading when their automatic use is disabled.
|
|
44
44
|
function retired(root, file, state) {
|
|
45
45
|
if (state.migrating || state.config.workflows[file]?.overrides?.length) return false;
|
|
46
46
|
const previous = load(root, `.github/workflows/${file}`);
|
|
@@ -87,8 +87,14 @@ export function planWorkflows(root, templateDir, files, version) {
|
|
|
87
87
|
if (!MANAGED.includes(file)) throw new MigrationRepair('unknown_managed_workflow', CONFIG);
|
|
88
88
|
}
|
|
89
89
|
const removed = selected.filter(file => file === 'deploy-web.yml' && retired(root, file, state));
|
|
90
|
-
const writes = selected.filter(file => !removed.includes(file) && file
|
|
90
|
+
const writes = selected.filter(file => !removed.includes(file) && ![WARM, RECOVERY].includes(file))
|
|
91
91
|
.map(file => upgradeWorkflow(root, file, templateDir, state));
|
|
92
|
+
const recovery = selected.includes('deploy.yml') && state.config.mobile?.platforms !== 'android';
|
|
93
|
+
if (recovery || (selected.includes(RECOVERY) && !retired(root, RECOVERY, state))) {
|
|
94
|
+
writes.push(upgradeWorkflow(root, RECOVERY, templateDir, state));
|
|
95
|
+
} else if (selected.includes(RECOVERY)) {
|
|
96
|
+
removed.push(RECOVERY);
|
|
97
|
+
}
|
|
92
98
|
// Only verified candidate keys can request trusted post-release dependency priming on either OS.
|
|
93
99
|
const candidates = selected.includes('mobile-candidate.yml')
|
|
94
100
|
&& state.config.mobile?.['candidate-artifacts'] !== false;
|
|
@@ -106,7 +112,7 @@ export function planWorkflows(root, templateDir, files, version) {
|
|
|
106
112
|
for (const [file, entry] of Object.entries(state.config.workflows)) {
|
|
107
113
|
for (const change of entry.overrides) assertReferences(change.value, file, change.path);
|
|
108
114
|
}
|
|
109
|
-
writes.push(...planValidation(root, [...selected, WARM]));
|
|
115
|
+
writes.push(...planValidation(root, [...selected, WARM, RECOVERY]));
|
|
110
116
|
state.lock.package = version;
|
|
111
117
|
const config = state.text && equal(readYaml(state.text, CONFIG).value, state.config)
|
|
112
118
|
? state.text
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
name: Recover Apple upload
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
producer-run:
|
|
6
|
+
description: Original Mobile Deploy run ID
|
|
7
|
+
required: true
|
|
8
|
+
type: string
|
|
9
|
+
producer-attempt:
|
|
10
|
+
description: Original Mobile Deploy run attempt
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
deployment-id:
|
|
14
|
+
description: Original iOS release deployment ID
|
|
15
|
+
required: true
|
|
16
|
+
type: string
|
|
17
|
+
handoff-artifact-id:
|
|
18
|
+
description: Original Apple verification handoff artifact ID
|
|
19
|
+
required: true
|
|
20
|
+
type: string
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
actions: read
|
|
25
|
+
deployments: write
|
|
26
|
+
|
|
27
|
+
# Serialize the original owner update with ordinary Mobile Deploy; recovery never uploads.
|
|
28
|
+
concurrency:
|
|
29
|
+
group: mobile-deploy-${{ github.ref }}
|
|
30
|
+
cancel-in-progress: false
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
recover:
|
|
34
|
+
name: Verify the original Apple upload
|
|
35
|
+
if: >-
|
|
36
|
+
${{ github.event_name == 'workflow_dispatch'
|
|
37
|
+
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
38
|
+
runs-on: ubuntu-24.04
|
|
39
|
+
timeout-minutes: 20
|
|
40
|
+
env:
|
|
41
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
42
|
+
APPLE_RECOVERY_PRODUCER_RUN: ${{ inputs.producer-run }}
|
|
43
|
+
APPLE_RECOVERY_PRODUCER_ATTEMPT: ${{ inputs.producer-attempt }}
|
|
44
|
+
APPLE_RECOVERY_DEPLOYMENT_ID: ${{ inputs.deployment-id }}
|
|
45
|
+
APPLE_RECOVERY_HANDOFF_ARTIFACT_ID: ${{ inputs.handoff-artifact-id }}
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
48
|
+
- name: Validate original upload identifiers
|
|
49
|
+
shell: bash
|
|
50
|
+
run: |
|
|
51
|
+
for value in "$APPLE_RECOVERY_PRODUCER_RUN" "$APPLE_RECOVERY_PRODUCER_ATTEMPT" \
|
|
52
|
+
"$APPLE_RECOVERY_DEPLOYMENT_ID" "$APPLE_RECOVERY_HANDOFF_ARTIFACT_ID"; do
|
|
53
|
+
if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then
|
|
54
|
+
echo "::error title=apple_upload_recovery_failed::invalid_recovery_identifiers"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
- name: Prepare Apple API access
|
|
59
|
+
env:
|
|
60
|
+
CI_ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
|
61
|
+
CI_ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
62
|
+
CI_ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
63
|
+
run: |
|
|
64
|
+
umask 077
|
|
65
|
+
mkdir -p creds
|
|
66
|
+
if [ -n "${CI_ASC_KEY_P8:-}" ]; then
|
|
67
|
+
: "${CI_ASC_KEY_ID:?ASC_KEY_ID secret is required with ASC_KEY_P8}"
|
|
68
|
+
: "${CI_ASC_ISSUER_ID:?ASC_ISSUER_ID secret is required with ASC_KEY_P8}"
|
|
69
|
+
printf '%s' "$CI_ASC_KEY_P8" > "creds/AuthKey_${CI_ASC_KEY_ID}_Issuer_${CI_ASC_ISSUER_ID}.p8"
|
|
70
|
+
fi
|
|
71
|
+
python3 -m pip install --quiet --break-system-packages PyJWT cryptography requests
|
|
72
|
+
PYTHONPATH=.github/actions/swift-app/scripts python3 - <<'PY'
|
|
73
|
+
import os
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
from read_config import emit_credentials
|
|
76
|
+
emit_credentials(Path(os.environ['GITHUB_ENV']), Path(os.environ['GITHUB_WORKSPACE']))
|
|
77
|
+
PY
|
|
78
|
+
- name: Verify the original upload and reconcile its owner
|
|
79
|
+
id: recover
|
|
80
|
+
run: |
|
|
81
|
+
node .github/actions/swift-app/scripts/apple_upload_recovery.cjs \
|
|
82
|
+
"$APPLE_RECOVERY_PRODUCER_RUN" "$APPLE_RECOVERY_PRODUCER_ATTEMPT" \
|
|
83
|
+
"$APPLE_RECOVERY_DEPLOYMENT_ID" "$APPLE_RECOVERY_HANDOFF_ARTIFACT_ID"
|
|
84
|
+
- name: Retain verified Apple recovery proof
|
|
85
|
+
if: ${{ steps.recover.outputs.verified == 'true' }}
|
|
86
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
87
|
+
env:
|
|
88
|
+
NODE_OPTIONS: --require "${{ github.workspace }}/.github/actions/android-app/scripts/artifact_env.cjs"
|
|
89
|
+
with:
|
|
90
|
+
name: apple-recovered-${{ github.run_id }}-${{ github.run_attempt }}
|
|
91
|
+
path: |
|
|
92
|
+
${{ runner.temp }}/apple-upload-recovery/apple-verification.json
|
|
93
|
+
${{ runner.temp }}/apple-upload-recovery/apple-verification.json.verified.json
|
|
94
|
+
${{ runner.temp }}/apple-upload-recovery/apple-verification.json.upload-receipt.json
|
|
95
|
+
if-no-files-found: error
|
|
96
|
+
retention-days: 7
|
|
Binary file
|