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.
@@ -1 +1 @@
1
- 1.0.162
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.162
1
+ 1.0.164
@@ -1 +1 @@
1
- 1.0.162
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
- set -- --nginx -d "$domain" --non-interactive --agree-tos --email "$email" --redirect
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.162",
3
+ "version": "1.0.164",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
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",