gowalk-cicd 1.0.162 → 1.0.164
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/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.164
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* Close a timed-out iOS upload only after the original owner and Apple both prove completion. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const github = require('./mobile_release_api.cjs');
|
|
6
|
+
|
|
7
|
+
const RECEIPT = 'gowalk-cicd/apple-build-upload-receipt.v1';
|
|
8
|
+
const OWNER = 'gowalk-cicd/mobile-upload-owner.v1';
|
|
9
|
+
|
|
10
|
+
function refuse(code) { throw Error(`apple_upload_reconcile_${code}`); }
|
|
11
|
+
|
|
12
|
+
function readReceipt(path) {
|
|
13
|
+
let data;
|
|
14
|
+
try { data = JSON.parse(fs.readFileSync(path, 'utf8')); } catch { refuse('receipt_unavailable'); }
|
|
15
|
+
const id = value => typeof value === 'string' && /^[A-Za-z0-9-]{1,64}$/.test(value);
|
|
16
|
+
if (data?.schema !== RECEIPT || data.stage !== 'processing' ||
|
|
17
|
+
data.failure?.category !== 'processing_pending' ||
|
|
18
|
+
!/^\d+$/.test(data.app_id || '') || !id(data.upload_id) || !id(data.file_id) ||
|
|
19
|
+
!/^[a-z0-9.]+$/.test(data.bundle_id || '') || !/^\d+$/.test(data.cfBundleVersion || '') ||
|
|
20
|
+
!/^\d+(\.\d+){1,2}$/.test(data.cfBundleShortVersionString || '') ||
|
|
21
|
+
!/^[a-f0-9]{64}$/.test(data.sha256 || '') || data.fileName !== `${data.sha256}.ipa` ||
|
|
22
|
+
!Number.isSafeInteger(data.fileSize) || data.fileSize <= 0) refuse('receipt_invalid');
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function verifyOwner(deploymentId, runId, attempt, api = github) {
|
|
27
|
+
if (!Number.isSafeInteger(deploymentId) || deploymentId <= 0 ||
|
|
28
|
+
!Number.isSafeInteger(runId) || runId <= 0 ||
|
|
29
|
+
!Number.isSafeInteger(attempt) || attempt <= 0) refuse('input_invalid');
|
|
30
|
+
const deployment = await api.request('GET', `/deployments/${deploymentId}`);
|
|
31
|
+
const owner = deployment.payload || {};
|
|
32
|
+
if (deployment.id !== deploymentId || deployment.task !== 'mobile-upload' ||
|
|
33
|
+
!/^mobile-ios-[a-f0-9]{64}$/.test(deployment.environment || '') ||
|
|
34
|
+
owner.schema !== OWNER || owner.platform !== 'ios' ||
|
|
35
|
+
owner.fingerprint !== deployment.environment.slice('mobile-ios-'.length) ||
|
|
36
|
+
owner.run_id !== runId || owner.run_attempt !== attempt ||
|
|
37
|
+
!/^[a-f0-9]{40}$/.test(owner.source_sha || '') || deployment.sha !== owner.source_sha) {
|
|
38
|
+
refuse('owner_invalid');
|
|
39
|
+
}
|
|
40
|
+
const run = await api.request('GET', `/actions/runs/${runId}/attempts/${attempt}`);
|
|
41
|
+
if (run.head_sha !== owner.source_sha || run.path !== '.github/workflows/deploy.yml' ||
|
|
42
|
+
run.event !== 'push' || run.head_branch !== 'main' ||
|
|
43
|
+
run.status !== 'completed' || run.conclusion !== 'failure') refuse('run_invalid');
|
|
44
|
+
const statuses = await api.request('GET', `/deployments/${deploymentId}/statuses?per_page=1`);
|
|
45
|
+
const latest = statuses[0] || {};
|
|
46
|
+
if (latest.state === 'success' && latest.description === 'uploaded') return 'reused';
|
|
47
|
+
if (latest.state !== 'in_progress' || latest.description !== 'uploading') refuse('phase_invalid');
|
|
48
|
+
return 'pending';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function appleGet(path) {
|
|
52
|
+
if (!path.startsWith('/') || path.includes('..')) refuse('path_invalid');
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
const encoded = value => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
55
|
+
const header = encoded({alg: 'ES256', kid: process.env.ASC_KEY_ID, typ: 'JWT'});
|
|
56
|
+
const payload = encoded({iss: process.env.ASC_ISSUER_ID, iat: now, exp: now + 1200,
|
|
57
|
+
aud: 'appstoreconnect-v1'});
|
|
58
|
+
let signature;
|
|
59
|
+
try {
|
|
60
|
+
signature = crypto.sign('sha256', Buffer.from(`${header}.${payload}`),
|
|
61
|
+
{key: process.env.ASC_KEY_P8, dsaEncoding: 'ieee-p1363'}).toString('base64url');
|
|
62
|
+
} catch { refuse('apple_key_invalid'); }
|
|
63
|
+
const response = await fetch(`https://api.appstoreconnect.apple.com/v1${path}`, {
|
|
64
|
+
headers: {Authorization: `Bearer ${header}.${payload}.${signature}`, Accept: 'application/json'},
|
|
65
|
+
signal: AbortSignal.timeout(30000),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) refuse(`apple_http_${response.status}`);
|
|
68
|
+
try { return await response.json(); } catch { refuse('apple_response_invalid'); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function verifyApple(receipt, get = appleGet) {
|
|
72
|
+
const upload = (await get(`/buildUploads/${receipt.upload_id}?include=build`)).data;
|
|
73
|
+
const attributes = upload?.attributes || {};
|
|
74
|
+
const buildId = upload?.relationships?.build?.data?.id;
|
|
75
|
+
if (upload?.id !== receipt.upload_id || attributes.state?.state !== 'COMPLETE' ||
|
|
76
|
+
attributes.cfBundleVersion !== receipt.cfBundleVersion ||
|
|
77
|
+
attributes.cfBundleShortVersionString !== receipt.cfBundleShortVersionString ||
|
|
78
|
+
!/^[A-Za-z0-9-]{1,64}$/.test(buildId || '')) refuse('upload_unverified');
|
|
79
|
+
const file = (await get(`/buildUploadFiles/${receipt.file_id}`)).data;
|
|
80
|
+
if (file?.id !== receipt.file_id || file.attributes?.assetType !== 'ASSET' ||
|
|
81
|
+
file.attributes.fileName !== receipt.fileName ||
|
|
82
|
+
file.attributes.fileSize !== receipt.fileSize ||
|
|
83
|
+
file.attributes.assetDeliveryState?.state !== 'COMPLETE') refuse('file_unverified');
|
|
84
|
+
const build = (await get(`/builds/${buildId}`)).data;
|
|
85
|
+
if (build?.id !== buildId || build.attributes?.processingState !== 'VALID' ||
|
|
86
|
+
build.attributes?.buildAudienceType !== 'APP_STORE_ELIGIBLE' ||
|
|
87
|
+
build.attributes?.version !== receipt.cfBundleVersion) refuse('build_unverified');
|
|
88
|
+
const app = (await get(`/builds/${buildId}/app`)).data;
|
|
89
|
+
if (app?.id !== receipt.app_id || app.attributes?.bundleId !== receipt.bundle_id) refuse('app_unverified');
|
|
90
|
+
return {build_id: buildId, app_id: app.id, version: receipt.cfBundleShortVersionString,
|
|
91
|
+
build: receipt.cfBundleVersion, upload_id: receipt.upload_id};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function reconcile(deploymentId, runId, attempt, path, api = github, get = appleGet) {
|
|
95
|
+
const receipt = readReceipt(path);
|
|
96
|
+
const phase = await verifyOwner(deploymentId, runId, attempt, api);
|
|
97
|
+
const proof = await verifyApple(receipt, get);
|
|
98
|
+
if (phase === 'pending') await api.status(deploymentId, 'uploaded', 'success');
|
|
99
|
+
const statuses = await api.request('GET', `/deployments/${deploymentId}/statuses?per_page=1`);
|
|
100
|
+
if (statuses[0]?.state !== 'success' || statuses[0]?.description !== 'uploaded') {
|
|
101
|
+
refuse('write_unconfirmed');
|
|
102
|
+
}
|
|
103
|
+
return {schema: 'gowalk-cicd/apple-upload-reconciled.v1', state: phase === 'pending' ? 'landed' : 'reused',
|
|
104
|
+
deployment_id: deploymentId, ...proof};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const [deployment, run, attempt, path] = process.argv.slice(2);
|
|
109
|
+
const result = await reconcile(Number(deployment), Number(run), Number(attempt), path);
|
|
110
|
+
console.log(JSON.stringify(result));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (require.main === module) main().catch(error => {
|
|
114
|
+
const code = /^apple_upload_reconcile_[a-z0-9_]+$/.test(error.message) ? error.message :
|
|
115
|
+
'apple_upload_reconcile_unavailable';
|
|
116
|
+
console.error('::error title=apple_upload_reconcile_refused::' + JSON.stringify({code}));
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
});
|
|
119
|
+
module.exports = {readReceipt, verifyOwner, appleGet, verifyApple, reconcile};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.164
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.164
|
|
@@ -111,7 +111,11 @@ certificate_present() {
|
|
|
111
111
|
|
|
112
112
|
request_certificate() {
|
|
113
113
|
local domain="$1" email="$2" attempt="$3" account
|
|
114
|
-
|
|
114
|
+
# Certbot tells the CA to validate one second after reloading nginx by default. A host with
|
|
115
|
+
# hundreds of vhosts and certificates takes longer to load a new configuration, so the CA
|
|
116
|
+
# reached old workers still serving the token-gated edge and HTTP-01 failed with 403
|
|
117
|
+
# (2026-09-24). Certbot also stores this wait for renewals of the certificate.
|
|
118
|
+
set -- --nginx --nginx-sleep-seconds 10 -d "$domain" --non-interactive --agree-tos --email "$email" --redirect
|
|
115
119
|
account="$(select_certbot_account)" || return $?
|
|
116
120
|
if [ -n "$account" ]; then
|
|
117
121
|
log "using an existing Let's Encrypt account"
|
package/package.json
CHANGED
package/remedies.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"schema": "gowalk-cicd/apple-build-upload-failed.v2",
|
|
4
|
+
"code": "processing_pending",
|
|
5
|
+
"fixed_in": "1.0.164",
|
|
6
|
+
"remedy": "Keep the original IPA and apple-build-upload receipt. If Apple later validates the exact upload, run reconcile_apple_upload.cjs in an authorized GitHub Actions recovery job with the original deployment, run, attempt and artifact receipt. It proves the failed owner, completed file, valid build and matching app before marking the deployment uploaded. Rerun the whole original deploy workflow after the readback; never upload a second binary or hand-edit the deployment receipt."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
|
4
10
|
"code": "mobile_release_phase_owner_mismatch",
|