## Summary - Consolidate the intended changes from #194–#199 onto current master. - Upgrade checkout 7.0.1, upload-artifact 7.0.1, setup-android 4.0.1, setup-java 5.6.0, github-script 9.0.0, and Fastlane 2.237.0. - Pin every upgraded workflow action to its verified immutable commit SHA. - Exclude the abandoned dependency-aware test-graph ancestor entirely. ## Verification - All five action families matched live upstream tag commits (`github-script` uses the peeled annotated-tag commit). - Workflow YAML parse passed. - AI workflow generated-output check passed. - 22 focused mobile/Playwright workflow unit tests passed. - `git diff --check` passed. - Ruby/Bundler is unavailable locally; Linux/macOS Fastlane resolution remains a required CI gate.
118 lines
5.9 KiB
YAML
118 lines
5.9 KiB
YAML
name: iOS Credential Health
|
|
|
|
on:
|
|
schedule:
|
|
- cron: "17 6 * * 1"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ios-credential-health
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
gate:
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
enabled: ${{ steps.gate.outputs.enabled }}
|
|
steps:
|
|
- id: gate
|
|
env:
|
|
ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
|
|
run: |
|
|
enabled=false
|
|
[[ "$ENABLED" == true ]] && enabled=true
|
|
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
|
|
if [[ "$enabled" != true ]]; then
|
|
echo "App Store automation is disabled; credential health did not access its environment." >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
validate:
|
|
needs: gate
|
|
if: needs.gate.outputs.enabled == 'true'
|
|
runs-on: macos-15
|
|
timeout-minutes: 15
|
|
environment: app-store-signing
|
|
env:
|
|
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
|
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
|
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
|
|
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
|
|
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
|
|
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
with:
|
|
node-version: 22
|
|
|
|
- name: Validate API key and app access
|
|
run: node scripts/mobile/app-store-connect.mjs verify-credentials
|
|
|
|
- name: Validate certificate and profile identity and expiry
|
|
shell: bash
|
|
env:
|
|
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
|
|
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
|
|
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
cert_p12="$RUNNER_TEMP/distribution.p12"
|
|
cert_pem="$RUNNER_TEMP/distribution.pem"
|
|
cert_der="$RUNNER_TEMP/distribution.der"
|
|
profile="$RUNNER_TEMP/distribution.mobileprovision"
|
|
profile_plist="$RUNNER_TEMP/distribution-profile.plist"
|
|
keychain="$RUNNER_TEMP/credential-health.keychain-db"
|
|
keychain_password="$(openssl rand -hex 24)"
|
|
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$cert_p12"
|
|
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile"
|
|
chmod 600 "$cert_p12" "$profile"
|
|
security create-keychain -p "$keychain_password" "$keychain"
|
|
security unlock-keychain -p "$keychain_password" "$keychain"
|
|
security import "$cert_p12" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
|
|
security list-keychains -d user -s "$keychain"
|
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
|
|
security find-identity -v -p codesigning "$keychain" | grep -q 'Apple Distribution' || {
|
|
echo "Distribution P12 does not contain a usable private signing identity." >&2
|
|
exit 1
|
|
}
|
|
openssl pkcs12 -in "$cert_p12" -clcerts -nokeys -passin env:IOS_DISTRIBUTION_CERTIFICATE_PASSWORD -out "$cert_pem"
|
|
openssl x509 -in "$cert_pem" -noout -subject -issuer -dates
|
|
openssl x509 -in "$cert_pem" -checkend 2592000 -noout || { echo "Distribution certificate expires within 30 days." >&2; exit 1; }
|
|
openssl x509 -in "$cert_pem" -outform DER -out "$cert_der"
|
|
security cms -D -i "$profile" > "$profile_plist"
|
|
CERT_DER="$cert_der" PROFILE_PLIST="$profile_plist" python3 <<'PY'
|
|
import datetime, hashlib, os, plistlib, sys
|
|
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
|
|
with open(os.environ["CERT_DER"], "rb") as handle: cert_sha = hashlib.sha1(handle.read()).hexdigest().upper()
|
|
expiration = profile.get("ExpirationDate")
|
|
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
|
warning = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
|
|
ent = profile.get("Entitlements", {})
|
|
checks = {
|
|
"team": os.environ["APPLE_TEAM_ID"] in profile.get("TeamIdentifier", []),
|
|
"bundle": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
|
|
"distribution": ent.get("get-task-allow") is False and not profile.get("ProvisionedDevices"),
|
|
"profile expiry beyond 30 days": expiration is not None and expiration > warning,
|
|
"certificate belongs to profile": cert_sha in {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])},
|
|
}
|
|
failed = [name for name, ok in checks.items() if not ok]
|
|
if failed:
|
|
print("Credential health failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f"Provisioning profile is healthy through {expiration.isoformat()}.")
|
|
PY
|
|
|
|
- name: Clean temporary credential files
|
|
if: always()
|
|
run: |
|
|
security delete-keychain "$RUNNER_TEMP/credential-health.keychain-db" 2>/dev/null || true
|
|
rm -f "$RUNNER_TEMP"/distribution.{p12,pem,der,mobileprovision} "$RUNNER_TEMP/distribution-profile.plist"
|