Skip to content

ci: replace Create iOS Certificates placeholder with the real workflow - #585

Merged
ignaciosantise merged 3 commits into
mainfrom
ci/create-ios-certs
Aug 7, 2026
Merged

ignaciosantise merged 3 commits into
mainfrom
ci/create-ios-certs

Conversation

@ignaciosantise

Copy link
Copy Markdown
Collaborator

Extracted from #510 so the cert workflow can be dispatched from main (that PR won't merge soon, and I need to mint certs now).

main currently has a placeholder create-ios-certs.yaml (added in #522) that only exists to register the workflow name for "Use workflow from". This replaces it with the real thing.

What's here

Only the three files the workflow needs to run, plus the README section:

  • .github/workflows/create-ios-certs.yaml — placeholder → real workflow. workflow_dispatch with bundle-id / certs-repo / match-types inputs; loops the types and writes a compare URL per type to the job summary.
  • scripts/create-certificates.sh — adds --no-pr (pre-creates the certs branch from master over SSH, so no GitHub token is needed; a teammate opens/merges the PR in reown-com/mobile-match) and picks App Store Connect API-key auth automatically when APPLE_KEY_ID / APPLE_ISSUER_ID / APPLE_KEY_CONTENT are set, falling back to interactive Apple ID auth locally.
  • fastlane/Fastfile — new create_certs lane: match(readonly: false) with API-key auth, no Apple ID / 2FA, so it runs unattended.
  • README.md — "Creating Certificates for a New App" now leads with the CI path; the script is documented as the local fallback.

The merchant POS app itself, release-merchant-pos.yaml, and the release runbook all stay out of this PR.

One deliberate difference from #510

create_certs calls setup_ci() first. release_testflight already does; without it match on a macOS runner tries to import the .p12 into the login keychain and fails with SecKeychainItemImport: User interaction is not allowed.

Notes for review

  • No new secrets. APPLE_KEY_ID, APPLE_ISSUER_ID, APPLE_KEY_CONTENT, MATCH_KEYCHAIN_PASSWORD and MATCH_SSH_KEY are already repo-level (used by release-ios-base.yaml), and macos-latest-xlarge is already in use.
  • Prerequisite, called out in the workflow header: the App ID and App Store Connect app record must be created manually first. match does not create the identifier, and fastlane produce can't authenticate with the API key.
  • The branch is pre-created from master before match runs, on purpose — pointed at a non-existent branch, match makes an orphan branch with only the new files (unmergeable) and re-mints a duplicate certificate because it can't see the existing one.

Verification

bash -n on the script, ruby -c on the Fastfile, and YAML.load_file on the workflow all pass; bundle exec fastlane lanes lists ios create_certs. Not dispatched yet — I'll run it manually once this is on main.

🤖 Generated with Claude Code

The workflow was authored in #510 (merchant POS app), which won't merge soon.
It's app-agnostic infra — bundle id is a workflow_dispatch input — so extract
it to main along with the two things it needs to run:

- scripts/create-certificates.sh: --no-pr mode (pre-creates the certs branch
  from master over SSH, no GitHub token) and automatic App Store Connect
  API-key auth when APPLE_KEY_* are set.
- fastlane create_certs lane: match with API-key auth, no Apple ID / 2FA.

The lane calls setup_ci() so match has a temporary keychain to import the
.p12 into — release_testflight already does this; without it match fails on
a macOS runner with "User interaction is not allowed".

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 15:49
@vercel

vercel Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
pos-demo Ignored Ignored Preview Aug 7, 2026 4:39pm
react-native-appkit Ignored Ignored Preview Aug 7, 2026 4:39pm
react-native-walletkit Ignored Ignored Preview Aug 7, 2026 4:39pm

Request Review

@claude

claude Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Claude finished @ignaciosantise's task in 3m 27s —— View job


Code Review

  • Gather context
  • Read changed files
  • Run breaking changes subagent → no breaking changes found
  • Run data classification subagent → no data classification issues found
  • Post review findings

Found 3 issue(s)

Issue 1: Expression injection via ${{ inputs.* }} directly in run: shell

ID: create-ios-certs-workflow-input-injection-a3f1
File: .github/workflows/create-ios-certs.yaml:64
Severity: MEDIUM
Category: security

Context:

  • Pattern: ${{ inputs.match-types }}, ${{ inputs.bundle-id }}, and ${{ inputs.certs-repo }} are interpolated directly by the GitHub Actions expression engine before the shell interpreter sees the script (lines 64, 66, 68, 74–78).
  • Risk: Any workflow dispatcher with write access can inject arbitrary shell commands — e.g. match-types set to appstore"; rm -rf /; echo " would execute before the script's own logic.
  • Impact: Code execution on the macOS runner with full access to the loaded SSH key (MATCH_SSH_KEY) and Apple API credentials.
  • Trigger: Any repo member who can trigger workflow_dispatch.

Recommendation: Map inputs to env vars before the run: block so the shell only ever sees $VAR-style references, which are safe from injection:

- name: Create certificates
  env:
    APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
    APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
    APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
    MATCH_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
    INPUT_MATCH_TYPES: ${{ inputs.match-types }}
    INPUT_BUNDLE_ID: ${{ inputs.bundle-id }}
    INPUT_CERTS_REPO: ${{ inputs.certs-repo }}
  run: |
    IFS=',' read -ra TYPES <<< "$INPUT_MATCH_TYPES"
    ...
    ./scripts/create-certificates.sh "$INPUT_CERTS_REPO" "$INPUT_BUNDLE_ID" "" "$type" --no-pr
    branch="certs/add-${INPUT_BUNDLE_ID}-${type}"
    compare="https://github.com/${INPUT_CERTS_REPO}/compare/master...${branch}?expand=1"

Fix this →


Issue 2: cleanup_branch not invoked on unexpected exits / signals

ID: create-certs-cleanup-no-trap-b7c2
File: scripts/create-certificates.sh:61
Severity: LOW
Category: code_quality

Context:

  • Pattern: cleanup_branch is defined at line 61 and called only within explicit if ! ...; then ... cleanup_branch; exit 1; fi guards. set -e at line 2 exits on unhandled errors, but without a trap, job cancellation (SIGTERM from a runner timeout or manual cancel) leaves the pre-created branch dangling in the certs repo.
  • Risk: A stale certs/add-<bundle-id>-<type> branch in reown-com/mobile-match blocks the next run (the existing-branch guard at line 78/103 exits 1 on branch-already-exists).
  • Trigger: Runner timeout, workflow_dispatch cancel mid-run.

Recommendation:

# after BRANCH_NAME is set (~line 49)
trap 'cleanup_branch' ERR INT TERM
# clear the trap on clean exit so it doesn't fire at the end
trap - EXIT

Or more idiomatically, set a trap cleanup_branch EXIT and clear it just before exit 0.


Issue 3: Third-party action pinned to a mutable tag, not a commit SHA

ID: create-ios-certs-unpinned-action-d4e8
File: .github/workflows/create-ios-certs.yaml:44
Severity: LOW
Category: security

Context:

  • Pattern: webfactory/ssh-agent@v0.9.0 (line 44) is a mutable tag — the maintainer can push a new commit to that tag without changing the reference.
  • Risk: Supply-chain compromise: a tag force-push could inject malicious code that exfiltrates MATCH_SSH_KEY or the Apple API credentials.
  • Impact: Full access to the match certificates repo and Apple developer account on the next workflow run.
  • Trigger: Tag is force-pushed by the maintainer or the repo is compromised.

Recommendation: Pin to a commit SHA:

uses: webfactory/ssh-agent@dc588b651fe13675575265b1f7e3e8f56e1f67c7  # v0.9.0

The same applies to ruby/setup-ruby@v1 (line 49).

Breaking changes: None — the placeholder-to-real-workflow swap is purely additive; no callers of the old workflow_dispatch stub existed.
Data classification: No issues — all credentials use ${{ secrets.* }} and are never echoed.

Comment thread .github/workflows/create-ios-certs.yaml Outdated
Comment thread scripts/create-certificates.sh
Comment thread .github/workflows/create-ios-certs.yaml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the existing placeholder Create iOS Certificates workflow on main with the real, dispatchable workflow and the minimal supporting code needed to mint iOS signing certs/profiles via Fastlane Match (CI-first), plus updated docs.

Changes:

  • Replace .github/workflows/create-ios-certs.yaml placeholder with a real workflow_dispatch workflow that loops match types and posts compare links in the job summary.
  • Extend scripts/create-certificates.sh to support --no-pr (SSH-only branch push) and auto-select App Store Connect API-key auth when APPLE_KEY_* env vars are present.
  • Add fastlane ios create_certs lane and update README guidance to prefer the CI path.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
.github/workflows/create-ios-certs.yaml Real CI workflow to create certs/profiles on macOS runner, looping match types and emitting compare links.
scripts/create-certificates.sh Adds --no-pr mode and API-key auth path; handles branch pre-creation and local PR creation/merge.
fastlane/Fastfile Adds ios create_certs lane using setup_ci() + API-key auth for unattended Match runs.
README.md Updates certificate creation docs to lead with CI workflow and document local fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/create-certificates.sh
Comment thread scripts/create-certificates.sh
Comment thread .github/workflows/create-ios-certs.yaml
Comment thread README.md Outdated
- Workflow: pass bundle-id / certs-repo / match-types through env instead of
  interpolating ${{ inputs.* }} into the run: body, so an input can't break out
  of the command; skip empty entries from a doubled comma.
- Workflow: reword the job summary. It read as a failure ("Action required"),
  but not opening PRs is deliberate — there is no write token for the certs
  repo, by design — so say so and frame the links as the expected next step.
- cleanup_branch: only delete the branch while it's still identical to master.
  Once match has pushed, the branch is the sole record of a certificate Apple
  has already issued; deleting it stranded that cert and removed the
  "branch already exists" guard, so the retry could mint a duplicate against
  Apple's distribution-cert cap.
- Two unreachable error handlers: under set -e a failing command substitution
  aborts at the assignment, so `if [ -z "$MASTER_SHA" ]` and `if [ $? -ne 0 ]`
  after `gh pr create` never ran. Worse, 2>&1 captured gh's error into PR_URL,
  so a failed PR creation exited silently with no instructions. The PR path now
  reports the error and says not to re-run, since the certs already exist.
- README: note that gh is only needed for local runs; CI uses --no-pr.

Co-Authored-By: Claude <noreply@anthropic.com>
@ignaciosantise

Copy link
Copy Markdown
Collaborator Author

Pushed b67fa03 addressing the review. Notes on what I took and what I didn't:

Applied

  • Expression injection (create-ios-certs.yaml:64) — done, inputs now go through INPUT_* env vars. Verified a appstore" ; echo PWNED ; echo " payload stays a literal string. Worth noting the severity is lower than MEDIUM in practice: workflow_dispatch requires write access, and anyone with write access can already push their own workflow and dispatch that. The real win is robustness against odd characters in a bundle id.
  • Unreachable error handlers (Copilot, create-certificates.sh:90 and :175) — the best findings here, and both confirmed: bash -c 'set -e; X=$(false); echo REACHED' prints nothing. So if [ -z "$MASTER_SHA" ] and if [ $? -ne 0 ] were dead code. The gh pr create one was the worse of the two — 2>&1 captured gh's error into $PR_URL, so a failed PR creation exited silently, leaving a branch full of certs and no instructions. Both pre-existing on main, fixed here since I'm in the file.
  • Empty match-type entries (Copilot, :75) — guard added, though the stated trigger is wrong: IFS=',' read -ra T <<< "appstore," yields 1 field, not 2. A trailing comma was always safe. A doubled comma (appstore,,development) does produce an empty field, which is what the guard actually covers.
  • README gh note (Copilot) — clarified that gh is only needed for local runs.

Not applied

  • trap for cleanup_branch — declining as written. trap - EXIT clears a trap that was never set, and trap cleanup_branch EXIT would double-fire alongside the existing if ! ...; then cleanup_branch; exit 1; fi handlers. More importantly it's actively harmful: a TERM trap firing after match has pushed would delete the branch holding freshly-minted certs.

    The finding did point at something real, just at the wrong site — the unconditional cleanup_branch calls after a match failure. Fixed properly instead: cleanup now compares the branch and master SHAs via git ls-remote and only deletes while they're identical, i.e. while match has pushed nothing. Once match has committed, the branch is the only record of a certificate Apple has already issued, and deleting it both stranded that cert and removed the "branch already exists" guard that stops a retry from minting a duplicate. Exercised all three cases against a scratch remote: never-pushed → no-op, unchanged-from-master → deleted, ahead-of-master → preserved with a warning.

  • SHA-pinning the actions — not in this PR. webfactory/ssh-agent@v0.9.0 is already used by release-ios-base.yaml with the same MATCH_SSH_KEY, so pinning it in one file reduces the attack surface by zero; a tag force-push still lands via the release path. That's a repo-wide sweep (currently 4 SHA-pinned vs. most on tags) and belongs in its own PR. ruby/setup-ruby@v1 is a floating major by design — pinning it means chasing patches by hand.

Also changed, from running it for real: the job summary was reworded. It led with "⚠️ Action required" and read like a failure, when not opening PRs is the deliberate design (no write token for the certs repo). It now says so explicitly and frames the compare links as the expected next step.

Drop comments that restate the code (`# Parse flags`, `# Local path`, the stale
`# 4.` / `# 5/6.` step numbers left over from the old numbering) and condense the
ones worth keeping. The load-bearing rationale stays: why the branch is cut from
master first, why cleanup won't delete a branch match has pushed to, why the two
`set -e` workarounds exist, and why inputs go through env.

Co-Authored-By: Claude <noreply@anthropic.com>
@ignaciosantise
ignaciosantise merged commit 842db3e into main Aug 7, 2026
8 checks passed
@ignaciosantise
ignaciosantise deleted the ci/create-ios-certs branch August 7, 2026 17:49
ignaciosantise added a commit that referenced this pull request Aug 7, 2026
#585 extracted the cert-creation workflow to main, so this branch no longer
needs its own copies. Resolved create-ios-certs.yaml, create-certificates.sh,
fastlane/Fastfile and README.md to main's versions — those carry the review
fixes this branch predates, notably the setup_ci() call in create_certs
without which match fails to import the .p12 on a macOS runner.

release-merchant-pos.yaml resolves the other way: main has only the #522
placeholder stub, the real workflow lives here.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants