mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 15:13:12 +00:00
ci-python-app.yaml and ci-mobile-ios.yaml built their concurrency group
from ${{ github.job }}. In a called workflow that expression evaluates to
the caller's job id, not the job's own id, so every job in the reusable
resolved to the same group. With cancel-in-progress: true they cancelled
each other.
Observed in pr-reviewer after repinning it off a ref that predates the
concurrency blocks: one run, ci / lint cancelled 1s after start by a
sibling, ci / subproject-tests succeeded, and the aggregator failed on the
cancelled dependency.
Replaces the expression with the job id written out literally in all 7
groups, and records the reason at the first block in each file.
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> # main
|
|
# 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@v7
|
|
|
|
- uses: actions/setup-node@v7
|
|
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@v7
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: ${{ inputs.node-version }}
|
|
cache: npm
|
|
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
|
|
|
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
|
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."
|