gowalk-cicd 1.0.32 → 1.0.34

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 CHANGED
@@ -572,6 +572,7 @@ common ones:
572
572
  | `bundle-id` | Override the auto-detected bundle identifier |
573
573
  | `team-id` | Override the auto-detected team ID |
574
574
  | `app-store-apple-id` | Numeric ASC app ID (override auto-lookup) |
575
+ | `certificate-cap-policy` | Full-cap behavior: `fail` preserves identities; default `revoke-oldest` rotates. |
575
576
  | `run-tests` | `false` to skip the simulator test stage |
576
577
  | `uses-non-exempt-encryption` | Value for `ITSAppUsesNonExemptEncryption` |
577
578
  | `archive` | `false` to build-only (PR runs without secrets) |
@@ -591,6 +592,12 @@ inside GitHub Actions.
591
592
  Once the app exists, all subsequent builds and uploads are fully automated via
592
593
  the ASC API key.
593
594
 
595
+ For accounts where automatic certificate revocation is not acceptable, pass
596
+ `certificate-cap-policy: 'fail'`. The action may add a certificate when Apple has a free
597
+ slot, but a full-cap response aborts without listing or revoking existing identities. If
598
+ signing preparation fails after creating a certificate, the default-branch workflow first
599
+ commits any completed cache files, then re-raises the failure so the private key is not lost.
600
+
594
601
  ## Troubleshooting
595
602
 
596
603
  **"No app found for bundle ID"** — the app record doesn't exist yet. Run the
@@ -1 +1 @@
1
- 1.0.32
1
+ 1.0.34
package/action/action.yml CHANGED
@@ -53,6 +53,13 @@ inputs:
53
53
  compatibility — passing a value emits a notice.
54
54
  required: false
55
55
  default: ""
56
+ certificate-cap-policy:
57
+ description: >
58
+ Behavior when Apple reports the two-certificate Distribution cap.
59
+ 'revoke-oldest' preserves historical behavior; 'fail' aborts without
60
+ listing or revoking any existing certificate.
61
+ required: false
62
+ default: "revoke-oldest"
56
63
  upload:
57
64
  description: "Set to 'false' to stop after producing the IPA"
58
65
  required: false
@@ -362,10 +369,19 @@ runs:
362
369
  WORKSPACE: ${{ inputs.workspace || env.CFG_WORKSPACE }}
363
370
  TEAM_ID: ${{ inputs.team-id || env.CFG_TEAM_ID }}
364
371
  CREDS_DIR: ${{ github.workspace }}/creds
365
- run: python3 "${{ github.action_path }}/scripts/prepare_signing.py"
372
+ CERTIFICATE_CAP_POLICY: ${{ inputs.certificate-cap-policy }}
373
+ run: |
374
+ set +e
375
+ python3 "${{ github.action_path }}/scripts/prepare_signing.py"
376
+ signing_exit=$?
377
+ set -e
378
+ echo "SIGNING_PREP_EXIT=$signing_exit" >> "$GITHUB_ENV"
379
+ if [ "$signing_exit" -ne 0 ]; then
380
+ echo "::error::Signing preparation failed; preserving any generated cache before failing."
381
+ fi
366
382
 
367
383
  # DESIGN DECISION (intentional, not a bug):
368
- # The next four steps stage + commit-back the Apple Distribution
384
+ # The next five steps stage + commit-back the Apple Distribution
369
385
  # code-signing identity (.p12 + provisioning profiles) AND any
370
386
  # autoupdate of the vendored app-ci actions into the consumer
371
387
  # repo's default branch. This is a deliberate trade-off chosen over
@@ -386,7 +402,8 @@ runs:
386
402
  # alone without redundant retries: (1) copy bot scripts to RUNNER_TEMP
387
403
  # so autoupdate's `npx` cannot mid-run swap script bytes under us,
388
404
  # (2) stage cert files, (3) check for newer app-ci on npm and
389
- # stage refreshed action files, (4) commit + push whatever is staged.
405
+ # stage refreshed action files, (4) commit + push whatever is staged,
406
+ # (5) re-raise a deferred prepare failure after preserving its cache.
390
407
 
391
408
  # Stage scripts to RUNNER_TEMP so the autoupdate step's `npx` (which
392
409
  # overwrites .github/actions/swift-app/ — the same dir as ${action_path}
@@ -494,6 +511,13 @@ runs:
494
511
  GITHUB_REF_NAME: ${{ github.ref_name }}
495
512
  run: bash "$BOT_SCRIPTS_DIR/commit_bot_changes.sh"
496
513
 
514
+ - name: Re-raise signing preparation failure
515
+ if: ${{ inputs.archive == 'true' && env.SIGNING_PREP_EXIT != '0' }}
516
+ shell: bash
517
+ run: |
518
+ echo "::error::Signing preparation failed with exit $SIGNING_PREP_EXIT."
519
+ exit "$SIGNING_PREP_EXIT"
520
+
497
521
  - name: Stamp Info.plist versions (CFBundleVersion + CFBundleShortVersionString)
498
522
  if: ${{ inputs.archive == 'true' }}
499
523
  shell: bash
@@ -3,7 +3,7 @@
3
3
  Cert-creation primitives for the iOS native TestFlight action.
4
4
 
5
5
  Owns the cryptographic legwork (RSA key + CSR), Apple's per-team cert
6
- cap rotation (revoke OLDEST on 409), and PKCS12 serialisation. Split
6
+ cap policy (fail closed or revoke OLDEST on 409), and PKCS12 serialisation. Split
7
7
  from ``prepare_signing.py`` so the orchestrator there stays focused on
8
8
  the load-or-regen control flow and the file count stays under the
9
9
  project's per-file limits.
@@ -36,6 +36,16 @@ CERT_REVOKE_PROPAGATION_DELAY_SEC = float(
36
36
  CERT_POST_RETRIES_AFTER_REVOKE = int(
37
37
  os.getenv("CERT_POST_RETRIES_AFTER_REVOKE", "2")
38
38
  )
39
+ CERTIFICATE_CAP_POLICIES = frozenset({"revoke-oldest", "fail"})
40
+
41
+
42
+ def certificate_cap_policy() -> str:
43
+ """Return the configured full-cap behavior before any provider call."""
44
+ policy = os.getenv("CERTIFICATE_CAP_POLICY", "revoke-oldest").strip().lower()
45
+ if policy not in CERTIFICATE_CAP_POLICIES:
46
+ allowed = ", ".join(sorted(CERTIFICATE_CAP_POLICIES))
47
+ raise SystemExit(f"invalid CERTIFICATE_CAP_POLICY {policy!r}; use {allowed}")
48
+ return policy
39
49
 
40
50
 
41
51
  def generate_key_and_csr() -> tuple[rsa.RSAPrivateKey, bytes]:
@@ -137,6 +147,7 @@ def create_distribution_cert(token: str, csr_b64: str) -> tuple[str, bytes]:
137
147
  backoff to absorb Apple's revoke -> create propagation lag (see
138
148
  :func:`_post_with_revoke_backoff`).
139
149
  """
150
+ cap_policy = certificate_cap_policy()
140
151
  body = {
141
152
  "data": {
142
153
  "type": "certificates",
@@ -150,6 +161,11 @@ def create_distribution_cert(token: str, csr_b64: str) -> tuple[str, bytes]:
150
161
  "POST", "/certificates", token, json_body=body, allow_status={409}
151
162
  )
152
163
  if resp.status_code == 409:
164
+ if cap_policy == "fail":
165
+ raise SystemExit(
166
+ "distribution certificate cap reached; policy=fail preserves all "
167
+ "existing certificates and refuses automatic revocation"
168
+ )
153
169
  # Revoke the OLDEST cert, NOT the newest. The newest cert signed
154
170
  # the previous build that may still be in ASC processing — revoking
155
171
  # it during processing yields ITMS-90035 on the prior build.
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Distribution-certificate cap policy and action wiring tests."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import unittest
8
+ from pathlib import Path
9
+ from unittest import mock
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+
13
+ import cert_factory # noqa: E402
14
+
15
+
16
+ ACTION_YAML = Path(__file__).resolve().parents[1] / "action.yml"
17
+
18
+
19
+ class CertificateCapPolicyTests(unittest.TestCase):
20
+ def test_invalid_policy_fails_before_provider_call(self):
21
+ with mock.patch.dict(os.environ, {"CERTIFICATE_CAP_POLICY": "rotate-all"}, clear=True):
22
+ with mock.patch.object(cert_factory, "request") as request:
23
+ with self.assertRaisesRegex(SystemExit, "invalid CERTIFICATE_CAP_POLICY"):
24
+ cert_factory.create_distribution_cert("token", "csr")
25
+ request.assert_not_called()
26
+
27
+ def test_fail_policy_preserves_every_certificate_on_cap(self):
28
+ response = mock.MagicMock(status_code=409)
29
+ with mock.patch.dict(os.environ, {"CERTIFICATE_CAP_POLICY": "fail"}, clear=True):
30
+ with mock.patch.object(cert_factory, "request", return_value=response) as request:
31
+ with mock.patch.object(cert_factory, "oldest_distribution_cert_id") as oldest:
32
+ with self.assertRaisesRegex(SystemExit, "refuses automatic revocation"):
33
+ cert_factory.create_distribution_cert("token", "csr")
34
+ request.assert_called_once()
35
+ oldest.assert_not_called()
36
+
37
+ def test_default_policy_keeps_backward_compatible_rotation(self):
38
+ capped = mock.MagicMock(status_code=409)
39
+ created = mock.MagicMock()
40
+ created.json.return_value = {
41
+ "data": {"id": "NEW", "attributes": {"certificateContent": "Y2VydA=="}}
42
+ }
43
+ with mock.patch.dict(os.environ, {}, clear=True):
44
+ with mock.patch.object(
45
+ cert_factory, "request", side_effect=[capped, mock.MagicMock()]
46
+ ) as request:
47
+ with mock.patch.object(cert_factory, "oldest_distribution_cert_id", return_value="OLD"):
48
+ with mock.patch.object(cert_factory, "_post_with_revoke_backoff", return_value=created):
49
+ cert_id, cert_der = cert_factory.create_distribution_cert("token", "csr")
50
+ self.assertEqual((cert_id, cert_der), ("NEW", b"cert"))
51
+ request.assert_any_call("DELETE", "/certificates/OLD", "token")
52
+
53
+
54
+ class CertificateCapActionWiringTests(unittest.TestCase):
55
+ def test_action_exposes_policy_and_persists_partial_cache_before_failure(self):
56
+ source = ACTION_YAML.read_text(encoding="utf-8")
57
+ self.assertRegex(
58
+ source,
59
+ r"(?m)^ certificate-cap-policy:\n(?: .*\n)*? default: \"revoke-oldest\"$",
60
+ )
61
+ self.assertIn("CERTIFICATE_CAP_POLICY: ${{ inputs.certificate-cap-policy }}", source)
62
+ self.assertIn("SIGNING_PREP_EXIT=$signing_exit", source)
63
+ self.assertIn("env.SIGNING_PREP_EXIT != '0'", source)
64
+ commit_at = source.index("- name: Commit + push staged bot changes")
65
+ failure_at = source.index("- name: Re-raise signing preparation failure")
66
+ self.assertLess(commit_at, failure_at)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ unittest.main()
@@ -1 +1 @@
1
- 1.0.32
1
+ 1.0.34
@@ -1 +1 @@
1
- 1.0.32
1
+ 1.0.34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {