mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 12:53:12 +00:00
Mutable tags and the dependency-review v5 branch executed inside every consumer, including OIDC deploy jobs. SHA pins with version comments match the policy scanner and let Renovate advance them.
307 lines
12 KiB
YAML
307 lines
12 KiB
YAML
name: CI — Mobile iOS
|
|
|
|
# Reusable CI counterpart to cd-mobile-ios.yaml. Verifies a React Native iOS
|
|
# app before merge: dependency install, typecheck, optional lint, optional unit
|
|
# tests, and a compile that is neither signed nor uploaded.
|
|
#
|
|
# Emits the single `ci / ci` status context required by the org branch-
|
|
# protection rulesets. As in ci-python-app.yaml, `ci` is an aggregator job
|
|
# gated on `needs`, so a caller job keyed `ci` reports `ci / ci` and that one
|
|
# context is red whenever any job below it failed.
|
|
#
|
|
# Input names mirror cd-mobile-ios.yaml wherever the same concept exists:
|
|
# node-version, ruby-version, working-directory, cache-dependency-path,
|
|
# fastlane-lane.
|
|
#
|
|
# Runner split. The JS checks run on ubuntu-latest; only the native compile
|
|
# runs on macOS, because only that step needs Xcode and CocoaPods. The macOS
|
|
# runner is billed at a multiple of the Linux rate, so putting `npm ci` +
|
|
# typecheck + tests on Linux keeps the expensive runner to the one job that
|
|
# genuinely requires it. `macos-26` matches cd-mobile-ios.yaml's runner, so CI
|
|
# compiles on the same Xcode image the deploy builds on.
|
|
#
|
|
# The compile is a `xcodebuild build`, not `archive` + `export`: it passes
|
|
# CODE_SIGNING_ALLOWED=NO / CODE_SIGNING_REQUIRED=NO / CODE_SIGN_IDENTITY="",
|
|
# and there is no App Store Connect or fastlane match step anywhere in this
|
|
# file. It therefore needs no signing certificates and no secrets, which is why
|
|
# `workflow_call` here declares none.
|
|
#
|
|
# run-lint and run-tests default to FALSE. The only current caller of
|
|
# cd-mobile-ios (proposal-system, working-directory `mobile`) declares exactly
|
|
# five npm scripts — start, ios, android, typecheck, postinstall — so a lint or
|
|
# test script cannot be assumed to exist. Turn them on per repo once the
|
|
# scripts are there.
|
|
#
|
|
# Caller example:
|
|
# jobs:
|
|
# ci:
|
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@<full-commit-sha> # v1.0.4
|
|
# with:
|
|
# working-directory: mobile
|
|
# cache-dependency-path: mobile/package-lock.json
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
node-version:
|
|
description: "Node.js version to use"
|
|
type: string
|
|
default: "24"
|
|
ruby-version:
|
|
description: "Ruby version for CocoaPods / Fastlane"
|
|
type: string
|
|
default: "3.3"
|
|
working-directory:
|
|
description: "Directory containing the mobile project"
|
|
type: string
|
|
default: "."
|
|
cache-dependency-path:
|
|
description: "Path to package-lock.json for npm cache"
|
|
type: string
|
|
default: "package-lock.json"
|
|
run-typecheck:
|
|
description: "Run the typecheck npm script"
|
|
type: boolean
|
|
default: true
|
|
typecheck-script:
|
|
description: "npm script name for the TypeScript check"
|
|
type: string
|
|
default: "typecheck"
|
|
run-lint:
|
|
description: "Run the lint npm script. The script must exist in package.json."
|
|
type: boolean
|
|
default: false
|
|
lint-script:
|
|
description: "npm script name for the linter"
|
|
type: string
|
|
default: "lint"
|
|
run-tests:
|
|
description: "Run the test npm script. The script must exist in package.json."
|
|
type: boolean
|
|
default: false
|
|
test-script:
|
|
description: "npm script name for the unit tests"
|
|
type: string
|
|
default: "test"
|
|
run-ios-build:
|
|
description: "Compile the iOS app without signing it"
|
|
type: boolean
|
|
default: true
|
|
fastlane-lane:
|
|
description: "Fastlane lane to run instead of xcodebuild. Empty means call xcodebuild directly. The lane must not sign or upload."
|
|
type: string
|
|
default: ""
|
|
xcode-workspace:
|
|
description: "Path to the .xcworkspace, relative to working-directory. Empty means auto-detect the one under ios/."
|
|
type: string
|
|
default: ""
|
|
xcode-scheme:
|
|
description: "Xcode scheme to build. Empty means the workspace file name without its extension."
|
|
type: string
|
|
default: ""
|
|
xcode-configuration:
|
|
description: "Xcode build configuration"
|
|
type: string
|
|
default: "Debug"
|
|
js-timeout-minutes:
|
|
description: "Timeout in minutes for the JavaScript checks job"
|
|
type: number
|
|
default: 15
|
|
ios-timeout-minutes:
|
|
description: "Timeout in minutes for the iOS compile job"
|
|
type: number
|
|
default: 45
|
|
|
|
# Read-only: this workflow builds and tests, it publishes nothing and assumes
|
|
# no cloud role. No `id-token` — nothing here mints an OIDC token.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
js:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: ${{ inputs.js-timeout-minutes }}
|
|
# cancel-in-progress is TRUE: superseding a push should abandon the older
|
|
# CI run, which produces no external side effects.
|
|
#
|
|
# The trailing segment is the job id written out literally, NOT
|
|
# `${{ github.job }}`. In a called workflow that expression evaluates to the
|
|
# CALLER's job id, so every job here would resolve to the same group and,
|
|
# with cancel-in-progress on, cancel its own siblings. Observed live in
|
|
# pr-reviewer: `lint` was cancelled one second in by a sibling and the
|
|
# aggregator failed on the cancelled dependency.
|
|
concurrency:
|
|
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-js
|
|
cancel-in-progress: true
|
|
defaults:
|
|
run:
|
|
working-directory: ${{ inputs.working-directory }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: ${{ inputs.node-version }}
|
|
cache: npm
|
|
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
|
|
|
- name: Install JS dependencies
|
|
run: npm ci
|
|
|
|
- name: Verify the requested npm scripts exist
|
|
env:
|
|
RUN_TYPECHECK: ${{ inputs.run-typecheck }}
|
|
TYPECHECK_SCRIPT: ${{ inputs.typecheck-script }}
|
|
RUN_LINT: ${{ inputs.run-lint }}
|
|
LINT_SCRIPT: ${{ inputs.lint-script }}
|
|
RUN_TESTS: ${{ inputs.run-tests }}
|
|
TEST_SCRIPT: ${{ inputs.test-script }}
|
|
run: |
|
|
node <<'NODE'
|
|
const { readFileSync } = require("node:fs");
|
|
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
const wanted = [
|
|
[process.env.RUN_TYPECHECK, process.env.TYPECHECK_SCRIPT],
|
|
[process.env.RUN_LINT, process.env.LINT_SCRIPT],
|
|
[process.env.RUN_TESTS, process.env.TEST_SCRIPT],
|
|
];
|
|
const missing = wanted
|
|
.filter(([enabled, name]) => enabled === "true" && name)
|
|
.map(([, name]) => name)
|
|
.filter((name) => !pkg.scripts?.[name]);
|
|
|
|
if (missing.length > 0) {
|
|
console.error(`Enabled but missing from package.json scripts: ${missing.join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
console.log("All enabled npm scripts are present.");
|
|
NODE
|
|
|
|
- name: Typecheck
|
|
if: ${{ inputs.run-typecheck }}
|
|
env:
|
|
TYPECHECK_SCRIPT: ${{ inputs.typecheck-script }}
|
|
run: npm run "${TYPECHECK_SCRIPT}"
|
|
|
|
- name: Lint
|
|
if: ${{ inputs.run-lint }}
|
|
env:
|
|
LINT_SCRIPT: ${{ inputs.lint-script }}
|
|
run: npm run "${LINT_SCRIPT}"
|
|
|
|
- name: Unit tests
|
|
if: ${{ inputs.run-tests }}
|
|
env:
|
|
TEST_SCRIPT: ${{ inputs.test-script }}
|
|
run: npm run "${TEST_SCRIPT}"
|
|
|
|
ios-build:
|
|
if: ${{ inputs.run-ios-build }}
|
|
runs-on: macos-26
|
|
timeout-minutes: ${{ inputs.ios-timeout-minutes }}
|
|
concurrency:
|
|
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-ios-build
|
|
cancel-in-progress: true
|
|
defaults:
|
|
run:
|
|
working-directory: ${{ inputs.working-directory }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: ${{ inputs.node-version }}
|
|
cache: npm
|
|
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
|
|
|
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
with:
|
|
ruby-version: ${{ inputs.ruby-version }}
|
|
bundler-cache: true
|
|
working-directory: ${{ inputs.working-directory }}
|
|
|
|
- name: Install JS dependencies
|
|
run: npm ci
|
|
|
|
- name: Install CocoaPods
|
|
run: bundle exec pod install --project-directory=ios
|
|
|
|
- name: Point the Xcode build phase at the runner's node
|
|
# React Native's "Bundle React Native code and images" build phase
|
|
# resolves node through .xcode.env.local. cd-mobile-ios.yaml gets this
|
|
# from the repo's Fastfile; the xcodebuild path below has no Fastfile
|
|
# step, so write it here.
|
|
run: |
|
|
set -euo pipefail
|
|
node_path="$(command -v node)"
|
|
printf 'export NODE_BINARY=%s\n' "${node_path}" > ios/.xcode.env.local
|
|
echo "NODE_BINARY set to ${node_path}"
|
|
|
|
- name: Build without signing
|
|
if: ${{ inputs.fastlane-lane == '' }}
|
|
env:
|
|
XCODE_WORKSPACE: ${{ inputs.xcode-workspace }}
|
|
XCODE_SCHEME: ${{ inputs.xcode-scheme }}
|
|
XCODE_CONFIGURATION: ${{ inputs.xcode-configuration }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workspace="${XCODE_WORKSPACE}"
|
|
if [ -z "${workspace}" ]; then
|
|
workspace="$(find ios -maxdepth 1 -name '*.xcworkspace' | head -n 1)"
|
|
fi
|
|
|
|
if [ -z "${workspace}" ] || [ ! -d "${workspace}" ]; then
|
|
echo "::error::No .xcworkspace found. Set xcode-workspace explicitly."
|
|
exit 1
|
|
fi
|
|
|
|
scheme="${XCODE_SCHEME}"
|
|
if [ -z "${scheme}" ]; then
|
|
scheme="$(basename "${workspace}" .xcworkspace)"
|
|
fi
|
|
|
|
echo "Building workspace ${workspace}, scheme ${scheme}, configuration ${XCODE_CONFIGURATION}."
|
|
|
|
# `build`, not `archive`: no .ipa is produced and nothing is exported.
|
|
# The three CODE_SIGN* settings turn signing off entirely, so this
|
|
# needs no certificates and cannot upload anything.
|
|
xcodebuild build \
|
|
-workspace "${workspace}" \
|
|
-scheme "${scheme}" \
|
|
-configuration "${XCODE_CONFIGURATION}" \
|
|
-destination 'generic/platform=iOS' \
|
|
-derivedDataPath "${RUNNER_TEMP}/DerivedData" \
|
|
CODE_SIGNING_ALLOWED=NO \
|
|
CODE_SIGNING_REQUIRED=NO \
|
|
CODE_SIGN_IDENTITY=""
|
|
|
|
- name: Build via Fastlane
|
|
if: ${{ inputs.fastlane-lane != '' }}
|
|
env:
|
|
FASTLANE_LANE: ${{ inputs.fastlane-lane }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Deliberately word-split: `fastlane-lane` carries a platform and a
|
|
# lane ("ios build") that fastlane expects as two separate argv
|
|
# entries, exactly as cd-mobile-ios.yaml passes it. Quoting would
|
|
# send one argument and fastlane would not find the lane.
|
|
# shellcheck disable=SC2086
|
|
bundle exec fastlane ${FASTLANE_LANE}
|
|
|
|
ci:
|
|
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
|
|
needs: [js, ios-build]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-ci
|
|
cancel-in-progress: true
|
|
steps:
|
|
- name: Require all jobs to have succeeded
|
|
run: |
|
|
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
|
|
echo "A required CI job failed or was cancelled."
|
|
exit 1
|
|
fi
|
|
echo "All CI jobs passed."
|