Files
pleno-vue/.github/workflows/ios-device-debug.yml
T
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b6f9b5a3a4 Bump actions/setup-node from 5 to 7 (#228)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5
to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add cache-primary-key and cache-matched-key as outputs by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Remove dummy NODE_AUTH_TOKEN export by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li>
<li>Only use <code>mirrorToken</code> in <code>getManifest</code> if
it's provided by <a
href="https://github.com/deiga"><code>@​deiga</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>Add documentation for publishing to npm with Trusted Publisher
(OIDC) by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li>docs: Update restore-only cache documentation by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li>
<li>docs: Update caching recommendations to mitigate cache poisoning
risks by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li><a href="https://github.com/deiga"><code>@​deiga</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p>
<h2>v6.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update <code>@​actions/cache</code> to 5.1.0 and add security
overrides for undici and fast-xml-parser by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p>
<h2>v6.4.0</h2>
<h2>What's Changed</h2>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade <a
href="https://github.com/actions"><code>@​actions</code></a>
dependencies by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
<li>Update Node.js versions in versions.yml and bump package to v6.4.0
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p>
<h2>v6.3.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support parsing <code>devEngines</code> field by <a
href="https://github.com/susnux"><code>@​susnux</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-node/commit/820762786026740c76f36085b0efc47a31fe5020"><code>8207627</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-node/issues/1574">#1574</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4"><code>04be95c</code></a>
Add cache-primary-key and cache-matched-key as outputs (<a
href="https://redirect.github.com/actions/setup-node/issues/1577">#1577</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/7c2c68d20d402ed6a201ada70a81341941093140"><code>7c2c68d</code></a>
docs: Update caching recommendations to mitigate cache poisoning risks
(<a
href="https://redirect.github.com/actions/setup-node/issues/1567">#1567</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/6a61c0375d66246de94630495909f12cf8dac84d"><code>6a61c03</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-node/issues/1569">#1569</a>
from jasongin/update-actions-cache-5.1.0</li>
<li><a
href="https://github.com/actions/setup-node/commit/30eb73b41ded577900c1ebf968ef95cdf8f7434f"><code>30eb73b</code></a>
Resolve high-severity audit issues</li>
<li><a
href="https://github.com/actions/setup-node/commit/4e1a87a501d0302f99e30e2748568adcb388d09f"><code>4e1a87a</code></a>
Update dist</li>
<li><a
href="https://github.com/actions/setup-node/commit/360237f0c01778d0c17291f75c56d6feae4f7574"><code>360237f</code></a>
Strict equality</li>
<li><a
href="https://github.com/actions/setup-node/commit/4f8aac5beb2f0854bc79651567a18c67eb0b9de3"><code>4f8aac5</code></a>
Bump <code>@​actions/cache</code> to 5.1.0, log cache write denied</li>
<li><a
href="https://github.com/actions/setup-node/commit/f4a67bbeca970f103397d3d2b9462cf787cd2980"><code>f4a67bb</code></a>
Only use <code>mirrorToken</code> in <code>getManifest</code> if it's
provided (<a
href="https://redirect.github.com/actions/setup-node/issues/1548">#1548</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/0355742c943ddb13ca8a6b700f824231caa91e75"><code>0355742</code></a>
Remove dummy NODE_AUTH_TOKEN export (<a
href="https://redirect.github.com/actions/setup-node/issues/1558">#1558</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/v5...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=5&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 12:55:41 +02:00

625 lines
28 KiB
YAML

name: iOS Device Debug IPA
on:
workflow_dispatch:
inputs:
source_ref:
description: Same-repository branch, tag, or commit to build
required: true
default: master
type: string
expected_sha:
description: Full 40-character SHA that source_ref must resolve to
required: true
type: string
confirmation:
description: Type SIGN IOS DEBUG IPA
required: true
type: string
permissions:
contents: read
concurrency:
group: ios-device-debug-${{ github.run_id }}
cancel-in-progress: false
jobs:
resolve:
name: Resolve and verify source
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
source_sha: ${{ steps.resolve.outputs.source_sha }}
steps:
- name: Validate dispatch confirmation
shell: bash
env:
CONFIRMATION: ${{ inputs.confirmation }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
SOURCE_REF: ${{ inputs.source_ref }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_REF" != "refs/heads/master" ]]; then
echo "The signing workflow must be dispatched from the master workflow ref" >&2
exit 1
fi
if [[ "$CONFIRMATION" != "SIGN IOS DEBUG IPA" ]]; then
echo "confirmation must exactly match SIGN IOS DEBUG IPA" >&2
exit 1
fi
if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "expected_sha must be a full 40-character commit SHA" >&2
exit 1
fi
if [[ -z "$SOURCE_REF" || "$SOURCE_REF" =~ [[:space:]] ]]; then
echo "source_ref must be non-empty and contain no whitespace" >&2
exit 1
fi
if [[ "$SOURCE_REF" == refs/pull/* || "$SOURCE_REF" == pull/* ]]; then
echo "Pull-request refs are not eligible for device-debug signing" >&2
exit 1
fi
- name: Checkout same-repository history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve immutable commit
id: resolve
shell: bash
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail
expected_sha="${EXPECTED_SHA,,}"
if [[ "$SOURCE_REF" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
candidate="$SOURCE_REF"
elif [[ "$SOURCE_REF" == refs/heads/* ]]; then
candidate="refs/remotes/origin/${SOURCE_REF#refs/heads/}"
elif [[ "$SOURCE_REF" == refs/tags/* ]]; then
candidate="$SOURCE_REF"
elif git show-ref --verify --quiet "refs/remotes/origin/$SOURCE_REF"; then
candidate="refs/remotes/origin/$SOURCE_REF"
elif git show-ref --verify --quiet "refs/tags/$SOURCE_REF"; then
candidate="refs/tags/$SOURCE_REF"
else
echo "source_ref does not identify a same-repository branch, tag, or fetched commit" >&2
exit 1
fi
source_sha="$(git rev-parse --verify "${candidate}^{commit}" 2>/dev/null || true)"
source_sha="${source_sha,,}"
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "source_ref could not be resolved to a commit" >&2
exit 1
fi
if [[ "$source_sha" != "$expected_sha" ]]; then
echo "source_ref resolved to a SHA different from expected_sha" >&2
exit 1
fi
reachable=false
while IFS= read -r repository_ref; do
if git merge-base --is-ancestor "$source_sha" "$repository_ref" 2>/dev/null; then
reachable=true
break
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin refs/tags)
if [[ "$reachable" != true ]]; then
echo "The requested commit is not reachable from a same-repository branch or tag" >&2
exit 1
fi
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "Resolved source_ref to $source_sha" >> "$GITHUB_STEP_SUMMARY"
build:
name: Build development-signed IPA
needs: resolve
runs-on: macos-26
timeout-minutes: 90
environment:
name: mobile-device-debug
env:
IOS_PROJECT_PATH: ios/App/App.xcodeproj
IOS_SCHEME: App
IOS_DEBUG_BUNDLE_ID: ${{ vars.IOS_DEBUG_BUNDLE_ID || 'io.truckwash.app.debug' }}
IOS_DEBUG_API_URL: ${{ vars.IOS_DEBUG_API_URL || 'https://api-v2.truckwash.io/master/api' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
MOBILE_VERSION_NAME: 0.0.${{ github.run_number }}
RESOLVED_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
steps:
- name: Checkout resolved source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve.outputs.source_sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify runner and resolve build number
shell: bash
run: |
set -euo pipefail
xcode_version_output="$(xcodebuild -version)"
IFS= read -r xcode_version <<< "$xcode_version_output"
xcode_major="$(awk '{split($2, version, "."); print version[1]}' <<< "$xcode_version")"
if [[ ! "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 26 )); then
echo "Xcode 26 or newer is required; found $xcode_version" >&2
exit 1
fi
build_number="$((10#$GITHUB_RUN_NUMBER * 100 + 10#$GITHUB_RUN_ATTEMPT))"
if [[ ! "$build_number" =~ ^[1-9][0-9]{0,17}$ ]]; then
echo "Derived build number is outside Apple's supported integer format" >&2
exit 1
fi
echo "MOBILE_VERSION_CODE=$build_number" >> "$GITHUB_ENV"
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Verify stable API and Capacitor iOS CORS
shell: bash
run: |
set -euo pipefail
api_base="${IOS_DEBUG_API_URL%/}"
curl --fail --silent --show-error --location \
--connect-timeout 10 --max-time 20 \
--header 'Accept: application/json' \
--output /dev/null \
"$api_base/ping"
cors_headers="$RUNNER_TEMP/ios-debug-cors-headers.txt"
cors_body="$RUNNER_TEMP/ios-debug-cors-body.txt"
cors_status="$(curl --silent --show-error \
--connect-timeout 10 --max-time 20 \
--request OPTIONS \
--header 'Origin: capacitor://localhost' \
--header 'Access-Control-Request-Method: POST' \
--header 'Access-Control-Request-Headers: authorization,content-type' \
--dump-header "$cors_headers" \
--output "$cors_body" \
--write-out '%{http_code}' \
"$api_base/ping")"
if [[ ! "$cors_status" =~ ^2[0-9][0-9]$ ]]; then
echo "Stable API rejected the Capacitor iOS CORS preflight with HTTP $cors_status" >&2
sed -n '1,20p' "$cors_body" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-origin:[[:space:]]*capacitor://localhost[[:space:]]*$' "$cors_headers"; then
echo "Stable API did not allow the exact capacitor://localhost origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-credentials:[[:space:]]*true[[:space:]]*$' "$cors_headers"; then
echo "Stable API did not allow credentialed Capacitor requests" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-methods:.*[[:space:],]POST([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow POST from the Capacitor origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Authorization([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow the Authorization header from the Capacitor origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Content-Type([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow the Content-Type header from the Capacitor origin" >&2
exit 1
fi
- name: Build stable production web payload
env:
RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}
VITE_API_URL: ${{ env.IOS_DEBUG_API_URL }}
VITE_RELEASE_MANAGER_CONTROL_API_URL: ${{ env.IOS_DEBUG_API_URL }}
VITE_RELEASE_PUBLIC_GATEWAY_API_URL: https://api-v2.truckwash.io
run: |
npm run build
node -e "const manifest = require('./dist/release-manifest.json'); if (manifest.commit_sha !== process.env.RESOLVED_SOURCE_SHA) { throw new Error('Web release manifest source SHA mismatch'); }"
- name: Sync and validate iOS shell
shell: bash
run: |
set -euo pipefail
npx cap sync ios
npm run mobile:permissions:check
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
echo "Unexpected Xcode shell-script build phase detected" >&2
exit 1
fi
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
- name: Validate native Debug and Release settings
shell: bash
run: |
set -euo pipefail
debug_settings="$RUNNER_TEMP/ios-debug-build-settings.txt"
release_settings="$RUNNER_TEMP/ios-release-build-settings.txt"
xcodebuild -showBuildSettings \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Debug \
CODE_SIGNING_ALLOWED=NO > "$debug_settings"
xcodebuild -showBuildSettings \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Release \
CODE_SIGNING_ALLOWED=NO > "$release_settings"
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app\.debug$' "$debug_settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash Debug$' "$debug_settings"
grep -Eq '^[[:space:]]*PRODUCT_NAME = TruckWashDebug$' "$debug_settings"
grep -Eq '^[[:space:]]*CAPACITOR_DEBUG = true$' "$debug_settings"
grep -Eq '^[[:space:]]*DEBUG_INFORMATION_FORMAT = dwarf-with-dsym$' "$debug_settings"
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$debug_settings"
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app$' "$release_settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$release_settings"
grep -Eq '^[[:space:]]*PRODUCT_NAME = App$' "$release_settings"
- name: Install and validate Apple development signing assets
shell: bash
env:
IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}
IOS_DEBUG_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DEBUG_CERTIFICATE_PASSWORD }}
IOS_DEBUG_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_DEBUG_PROVISION_PROFILE_BASE64 }}
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
run: |
set -euo pipefail
node scripts/mobile/check-ios-debug-signing-env.mjs
certificate_path="$RUNNER_TEMP/ios-debug-development.p12"
profile_path="$RUNNER_TEMP/ios-debug-development.mobileprovision"
profile_plist="$RUNNER_TEMP/ios-debug-development-profile.plist"
keychain_path="$RUNNER_TEMP/ios-debug-signing.keychain-db"
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
echo "::add-mask::$keychain_password"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_CERTIFICATE_BASE64.replace(/\\s/g, ''), 'base64'))" "$certificate_path"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_PROVISION_PROFILE_BASE64.replace(/\\s/g, ''), 'base64'))" "$profile_path"
chmod 600 "$certificate_path" "$profile_path"
security cms -D -i "$profile_path" > "$profile_plist"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" \
-P "$IOS_DEBUG_CERTIFICATE_PASSWORD" \
-A \
-t cert \
-f pkcs12 \
-k "$keychain_path"
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
security set-key-partition-list \
-S apple-tool:,apple: \
-s \
-k "$keychain_password" \
"$keychain_path"
signing_identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Development/ {print $2; exit}')"
if [[ ! "$signing_identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
echo "The PKCS#12 file does not contain a valid Apple Development signing identity" >&2
exit 1
fi
IOS_SIGNING_IDENTITY_SHA="$signing_identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime
import hashlib
import os
import plistlib
import re
import sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
profile = plistlib.load(handle)
team_id = os.environ["APPLE_TEAM_ID"]
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
entitlements = profile.get("Entitlements", {})
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
provisioned = set(profile.get("ProvisionedDevices", []))
expiration = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if expiration and expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
checks = {
"profile team identifier": team_id in profile.get("TeamIdentifier", []),
"application identifier": entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
"entitlement team identifier": entitlements.get("com.apple.developer.team-identifier") == team_id,
"development entitlement": entitlements.get("get-task-allow") is True,
"profile expiration": expiration is not None and expiration > now,
"registered devices": bool(allowed) and allowed <= provisioned,
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
"developer certificate": bool(profile.get("DeveloperCertificates")),
"profile UUID": isinstance(profile.get("UUID"), str) and re.fullmatch(r"[0-9A-Fa-f-]{36}", profile["UUID"]) is not None,
"safe profile name": isinstance(profile.get("Name"), str) and not any(char in profile["Name"] for char in "\r\n"),
}
identity_sha = os.environ["IOS_SIGNING_IDENTITY_SHA"].upper()
certificate_hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
checks["certificate belongs to profile"] = identity_sha in certificate_hashes
failures = [label for label, passed in checks.items() if not passed]
if failures:
print("Development provisioning profile validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
sys.exit(1)
PY
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
profile_expiration="$(PROFILE_PLIST="$profile_plist" python3 - <<'PY'
import datetime
import os
import plistlib
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
expiration = plistlib.load(handle)["ExpirationDate"]
if expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
print(expiration.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"))
PY
)"
profile_install_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
profile_install_path="$profile_install_dir/$profile_uuid.mobileprovision"
mkdir -p "$profile_install_dir"
cp "$profile_path" "$profile_install_path"
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "IOS_PROFILE_INSTALL_PATH=$profile_install_path" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
echo "IOS_PROFILE_EXPIRATION=$profile_expiration" >> "$GITHUB_ENV"
- name: Archive Debug app with Apple Development signing
shell: bash
run: |
set -euo pipefail
archive_path="$RUNNER_TEMP/TruckWashDebug.xcarchive"
xcodebuild \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Debug \
-destination "generic/platform=iOS" \
-archivePath "$archive_path" \
archive \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Development" \
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" \
PRODUCT_BUNDLE_IDENTIFIER="$IOS_DEBUG_BUNDLE_ID" \
MARKETING_VERSION="$MOBILE_VERSION_NAME" \
CURRENT_PROJECT_VERSION="$MOBILE_VERSION_CODE" \
DEBUG_INFORMATION_FORMAT="dwarf-with-dsym" \
ONLY_ACTIVE_ARCH=NO
echo "IOS_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
- name: Export development IPA
shell: bash
run: |
set -euo pipefail
export_options="$RUNNER_TEMP/ios-debug-ExportOptions.plist"
export_path="$RUNNER_TEMP/ios-debug-export"
EXPORT_OPTIONS="$export_options" python3 <<'PY'
import os
import plistlib
options = {
"method": "development",
"signingStyle": "manual",
"teamID": os.environ["APPLE_TEAM_ID"],
"provisioningProfiles": {
os.environ["IOS_DEBUG_BUNDLE_ID"]: os.environ["IOS_PROFILE_NAME"],
},
"stripSwiftSymbols": True,
"manageAppVersionAndBuildNumber": False,
}
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle:
plistlib.dump(options, handle)
PY
xcodebuild \
-exportArchive \
-archivePath "$IOS_ARCHIVE_PATH" \
-exportPath "$export_path" \
-exportOptionsPlist "$export_options"
shopt -s nullglob
ipa_files=("$export_path"/*.ipa)
if [[ ${#ipa_files[@]} -ne 1 ]]; then
echo "Expected exactly one exported IPA; found ${#ipa_files[@]}" >&2
exit 1
fi
echo "IOS_EXPORTED_IPA=${ipa_files[0]}" >> "$GITHUB_ENV"
- name: Validate exported IPA and embedded signature
shell: bash
env:
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
run: |
set -euo pipefail
inspect_dir="$RUNNER_TEMP/ios-debug-inspect"
mkdir -p "$inspect_dir"
unzip -q "$IOS_EXPORTED_IPA" -d "$inspect_dir"
shopt -s nullglob
app_bundles=("$inspect_dir"/Payload/*.app)
if [[ ${#app_bundles[@]} -ne 1 ]]; then
echo "Expected exactly one Payload app; found ${#app_bundles[@]}" >&2
exit 1
fi
app_path="${app_bundles[0]}"
app_info="$app_path/Info.plist"
embedded_profile="$RUNNER_TEMP/ios-debug-embedded-profile.plist"
signature_entitlements="$RUNNER_TEMP/ios-debug-signature-entitlements.plist"
signature_details="$RUNNER_TEMP/ios-debug-signature-details.txt"
security cms -D -i "$app_path/embedded.mobileprovision" > "$embedded_profile"
codesign --verify --deep --strict "$app_path"
codesign -d --entitlements :- "$app_path" > "$signature_entitlements"
codesign -dvv "$app_path" > /dev/null 2> "$signature_details"
grep -Fq "TeamIdentifier=$APPLE_TEAM_ID" "$signature_details"
grep -Eq '^Authority=Apple Development:' "$signature_details"
executable_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_info")"
architectures="$(lipo -archs "$app_path/$executable_name")"
if [[ " $architectures " != *" arm64 "* ]]; then
echo "Exported executable does not contain arm64" >&2
exit 1
fi
APP_INFO="$app_info" PROFILE_PLIST="$embedded_profile" SIGNATURE_ENTITLEMENTS="$signature_entitlements" python3 <<'PY'
import datetime
import os
import plistlib
import sys
def load(path):
with open(path, "rb") as handle:
return plistlib.load(handle)
info = load(os.environ["APP_INFO"])
profile = load(os.environ["PROFILE_PLIST"])
signature = load(os.environ["SIGNATURE_ENTITLEMENTS"])
profile_entitlements = profile.get("Entitlements", {})
team_id = os.environ["APPLE_TEAM_ID"]
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
provisioned = set(profile.get("ProvisionedDevices", []))
expiration = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if expiration and expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
checks = {
"bundle identifier": info.get("CFBundleIdentifier") == bundle_id,
"display name": info.get("CFBundleDisplayName") == "Truck Wash Debug",
"debug executable": info.get("CFBundleExecutable") == "TruckWashDebug",
"marketing version": info.get("CFBundleShortVersionString") == os.environ["MOBILE_VERSION_NAME"],
"build number": info.get("CFBundleVersion") == os.environ["MOBILE_VERSION_CODE"],
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
"profile UUID": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
"profile team": team_id in profile.get("TeamIdentifier", []),
"profile application identifier": profile_entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
"development profile": profile_entitlements.get("get-task-allow") is True,
"signature application identifier": signature.get("application-identifier") == f"{team_id}.{bundle_id}",
"signature team identifier": signature.get("com.apple.developer.team-identifier") == team_id,
"debuggable signature": signature.get("get-task-allow") is True,
"profile expiration": expiration is not None and expiration > now,
"registered devices": bool(allowed) and allowed <= provisioned,
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
}
failures = [label for label, passed in checks.items() if not passed]
if failures:
print("Exported development IPA validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
sys.exit(1)
PY
- name: Assemble debug artifact
shell: bash
env:
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail
short_sha="${RESOLVED_SOURCE_SHA:0:12}"
artifact_name="truck-wash-debug-${MOBILE_VERSION_NAME}-${short_sha}"
artifact_dir="$RUNNER_TEMP/device-debug-artifact"
ipa_filename="$artifact_name.ipa"
dsym_filename="$artifact_name.dSYM.zip"
mkdir -p "$artifact_dir"
cp "$IOS_EXPORTED_IPA" "$artifact_dir/$ipa_filename"
shopt -s nullglob
dsym_bundles=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
if [[ ${#dsym_bundles[@]} -eq 0 ]]; then
echo "The Debug archive did not contain any dSYM bundles" >&2
exit 1
fi
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact_dir/$dsym_filename"
capacitor_version="$(node -p "require('./node_modules/@capacitor/core/package.json').version")"
BUILT_AT_UTC="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
CAPACITOR_VERSION="$capacitor_version" \
DSYM_FILENAME="$dsym_filename" \
IPA_FILENAME="$ipa_filename" \
MANIFEST_PATH="$artifact_dir/manifest.json" \
python3 <<'PY'
import json
import os
manifest = {
"schema_version": 1,
"repository": os.environ["GITHUB_REPOSITORY"],
"source_ref": os.environ["SOURCE_REF"],
"source_sha": os.environ["RESOLVED_SOURCE_SHA"],
"workflow_run": int(os.environ["GITHUB_RUN_NUMBER"]),
"workflow_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
"built_at_utc": os.environ["BUILT_AT_UTC"],
"api_url": os.environ["IOS_DEBUG_API_URL"],
"release_manager_control_api_url": os.environ["IOS_DEBUG_API_URL"],
"bundle_id": os.environ["IOS_DEBUG_BUNDLE_ID"],
"display_name": "Truck Wash Debug",
"executable_name": "TruckWashDebug",
"version": os.environ["MOBILE_VERSION_NAME"],
"build": os.environ["MOBILE_VERSION_CODE"],
"minimum_ios": "15.0",
"capacitor_version": os.environ["CAPACITOR_VERSION"],
"xcode_version": os.environ["XCODE_VERSION"],
"signing_method": "development",
"profile_expiration_utc": os.environ["IOS_PROFILE_EXPIRATION"],
"ipa_filename": os.environ["IPA_FILENAME"],
"dsym_filename": os.environ["DSYM_FILENAME"],
}
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
PY
(
cd "$artifact_dir"
shasum -a 256 "$ipa_filename" "$dsym_filename" manifest.json > SHA256SUMS
)
echo "IOS_DEBUG_ARTIFACT_DIR=$artifact_dir" >> "$GITHUB_ENV"
echo "IOS_DEBUG_ARTIFACT_NAME=$artifact_name" >> "$GITHUB_ENV"
- name: Upload device-debug artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.IOS_DEBUG_ARTIFACT_NAME }}
path: ${{ env.IOS_DEBUG_ARTIFACT_DIR }}
if-no-files-found: error
retention-days: 7
- name: Clean up Apple signing assets
if: always()
shell: bash
run: |
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
else
security delete-keychain "$RUNNER_TEMP/ios-debug-signing.keychain-db" || true
fi
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then
rm -f "$IOS_PROFILE_INSTALL_PATH"
fi
rm -f \
"$RUNNER_TEMP/ios-debug-development.p12" \
"$RUNNER_TEMP/ios-debug-development.mobileprovision" \
"$RUNNER_TEMP/ios-debug-development-profile.plist" \
"$RUNNER_TEMP/ios-debug-embedded-profile.plist" \
"$RUNNER_TEMP/ios-debug-signature-entitlements.plist" \
"$RUNNER_TEMP/ios-debug-signature-details.txt"