From 9389e51c10c506caa55f204527452a9e29b0e438 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 15:55:43 -0400 Subject: [PATCH 1/2] feat(workflows): add release and ci-mobile-ios reusable workflows release.yaml is workflow_call-only. It normalises and validates a `version` input against MAJOR.MINOR.PATCH, skips every mutating step when the tag or a Release for it already exists, creates an annotated tag with `git tag -a` and publishes a GitHub Release with `gh release create --verify-tag`. Top-level permissions grant `contents: write` only; no id-token is requested. The previous-tag lookup and `--generate-notes` both work in a repo with no tags. ci-mobile-ios.yaml is workflow_call-only and pairs with cd-mobile-ios.yaml, reusing its node-version, ruby-version, working-directory, cache-dependency-path and fastlane-lane input names. Job `js` runs npm ci, typecheck, optional lint and optional tests on ubuntu-latest. Job `ios-build` runs pod install and `xcodebuild build` on macos-26 with CODE_SIGNING_ALLOWED=NO, CODE_SIGNING_REQUIRED=NO and CODE_SIGN_IDENTITY=""; it declares no secrets and performs no upload. Job `ci` aggregates both via `needs` so a caller job keyed `ci` reports `ci / ci`. All three jobs carry job-level concurrency with cancel-in-progress: true. Top-level permissions are `contents: read`. The Fastlane branch carries a per-line `# shellcheck disable=SC2086` because fastlane requires the platform and lane as two argv entries. --- .github/workflows/ci-mobile-ios.yaml | 301 +++++++++++++++++++++++++++ .github/workflows/release.yaml | 279 +++++++++++++++++++++++++ 2 files changed, 580 insertions(+) create mode 100644 .github/workflows/ci-mobile-ios.yaml create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/ci-mobile-ios.yaml b/.github/workflows/ci-mobile-ios.yaml new file mode 100644 index 0000000..db07e6b --- /dev/null +++ b/.github/workflows/ci-mobile-ios.yaml @@ -0,0 +1,301 @@ +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@ # 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. `github.job` is in the + # key so the three jobs here do not serialise against each other. + concurrency: + group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-${{ github.job }} + 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 }}-${{ github.job }} + 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 }}-${{ github.job }} + 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." diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..504cf53 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,279 @@ +name: Release — Tag and GitHub Release + +# Reusable release workflow: creates an annotated git tag at a commit and +# publishes a GitHub Release pointing at it. +# +# Version derivation is an INPUT, not read from a manifest and not computed. +# That follows what the org's repos actually contain: +# - 3 of 23 non-archived repos carry any tag at all; every existing tag is +# `vMAJOR.MINOR.PATCH`, which is why `tag-prefix` defaults to "v". +# - `package.json` "version" is not maintained as a release marker where it +# exists: seahaven-door-unlock-api sits at "1.0.0" while its tags reach +# v3.0.0, and payments-dashboard sits at "1.0.0" with no tags at all. +# - No repo has a VERSION file, and the repos that tag are Python/SAM, +# TypeScript/CDK and Python-desktop, so there is no one manifest to read. +# - The single repo that derives a version from a file +# (afterhours-shift-manager, from CHANGELOG.md) does it with two +# repo-local parser scripts under `scripts/`, which a reusable workflow +# cannot assume exist. +# An explicit input is therefore the only derivation that works unchanged for +# every repo here. A repo that does maintain a machine-readable version can +# still pass it: `version: ${{ needs.x.outputs.version }}`. +# +# Re-running on a version that is already released is a no-op, not a failure: +# the tag and the Release are both checked first, and either one being present +# skips every mutating step. This mirrors afterhours-shift-manager's release +# job, which likewise no-ops when the Release already exists. +# +# Safe in a repo with no releases yet: the "previous tag" lookup tolerates zero +# tags, and `gh release create --generate-notes` falls back to the full commit +# history when there is no earlier release to diff against. +# +# Caller example: +# jobs: +# release: +# uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@ # main +# with: +# version: ${{ inputs.version }} + +on: + workflow_call: + inputs: + version: + description: 'Version to release, e.g. "1.4.0". A leading "v" is accepted and stripped.' + type: string + required: true + tag-prefix: + description: "Prefix placed in front of the version to form the tag name" + type: string + default: "v" + target: + description: "Commit-ish to tag. Empty means the commit the workflow was triggered on." + type: string + default: "" + notes-file: + description: "Path to a file whose contents become the release notes. Empty means use generate-notes." + type: string + default: "" + generate-notes: + description: "Let GitHub generate release notes from commits when notes-file is empty" + type: boolean + default: true + title: + description: "Release title. Empty means use the tag name." + type: string + default: "" + draft: + description: "Publish the Release as a draft" + type: boolean + default: false + prerelease: + description: "Mark the Release as a prerelease" + type: boolean + default: false + timeout-minutes: + description: "Job timeout in minutes" + type: number + default: 10 + outputs: + tag: + description: "The tag name that was created, or that already existed" + value: ${{ jobs.release.outputs.tag }} + version: + description: "The normalised version, without the tag prefix" + value: ${{ jobs.release.outputs.version }} + released: + description: '"true" when this run created the tag and Release, "false" when it skipped' + value: ${{ jobs.release.outputs.released }} + url: + description: "URL of the Release this run created. Empty when the run skipped." + value: ${{ jobs.release.outputs.url }} + +# Only `contents: write` — needed to push the tag and publish the Release. +# Nothing here mints an OIDC token, so `id-token` is deliberately not granted. +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout-minutes }} + # Serialise per tag so two runs cannot race to create the same release. + # version is required and tag-prefix always defaults, so the group is never + # empty. cancel-in-progress is FALSE on purpose: unlike CI, aborting midway + # can leave a pushed tag with no Release attached to it. + concurrency: + group: release-${{ github.repository }}-${{ inputs.tag-prefix }}${{ inputs.version }} + cancel-in-progress: false + outputs: + tag: ${{ steps.resolve.outputs.tag }} + version: ${{ steps.resolve.outputs.version }} + released: ${{ steps.publish.outputs.released || 'false' }} + url: ${{ steps.publish.outputs.url }} + steps: + - uses: actions/checkout@v7 + with: + # Full history + tags: the existing-tag guard reads local refs. + fetch-depth: 0 + fetch-tags: true + ref: ${{ inputs.target }} + + - name: Resolve and validate version + id: resolve + # Inputs are passed through env, never interpolated into the script + # body, so a caller cannot inject shell into this step. + env: + RAW_VERSION: ${{ inputs.version }} + TAG_PREFIX: ${{ inputs.tag-prefix }} + run: | + set -euo pipefail + + version="${RAW_VERSION#v}" + + if ! printf '%s' "${version}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then + echo "::error::version '${RAW_VERSION}' is not MAJOR.MINOR.PATCH with an optional -prerelease/+build suffix." + exit 1 + fi + + tag="${TAG_PREFIX}${version}" + + { + echo "version=${version}" + echo "tag=${tag}" + } >> "${GITHUB_OUTPUT}" + + echo "Resolved ${RAW_VERSION} to tag ${tag}." + + - name: Check whether the tag or Release already exists + id: guard + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.resolve.outputs.tag }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + skip=false + reason="" + + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + skip=true + reason="tag ${TAG} already exists locally" + elif [ -n "$(git ls-remote --tags origin "refs/tags/${TAG}")" ]; then + skip=true + reason="tag ${TAG} already exists on the remote" + elif gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then + skip=true + reason="a Release for ${TAG} is already published" + fi + + echo "skip=${skip}" >> "${GITHUB_OUTPUT}" + + if [ "${skip}" = "true" ]; then + echo "::notice::Skipping — ${reason}." + else + echo "No existing tag or Release for ${TAG}." + fi + + - name: Report the previous tag + if: ${{ steps.guard.outputs.skip == 'false' }} + run: | + set -euo pipefail + + previous="$(git tag -l --sort=-v:refname | head -n 1)" + + if [ -z "${previous}" ]; then + echo "No previous tag in this repository — this is the first release." + else + echo "Previous tag: ${previous}" + fi + + - name: Resolve release notes + id: notes + if: ${{ steps.guard.outputs.skip == 'false' }} + env: + NOTES_FILE: ${{ inputs.notes-file }} + GENERATE_NOTES: ${{ inputs.generate-notes }} + run: | + set -euo pipefail + + if [ -n "${NOTES_FILE}" ]; then + if [ ! -f "${NOTES_FILE}" ]; then + echo "::error::notes-file '${NOTES_FILE}' does not exist." + exit 1 + fi + echo "mode=file" >> "${GITHUB_OUTPUT}" + echo "Using release notes from ${NOTES_FILE}." + elif [ "${GENERATE_NOTES}" = "true" ]; then + echo "mode=generate" >> "${GITHUB_OUTPUT}" + echo "Release notes will be generated from commit history." + else + echo "mode=empty" >> "${GITHUB_OUTPUT}" + echo "Publishing with empty release notes." + fi + + - name: Create and push the annotated tag + if: ${{ steps.guard.outputs.skip == 'false' }} + env: + TAG: ${{ steps.resolve.outputs.tag }} + run: | + set -euo pipefail + + # -a makes this an annotated tag: it is a real tag object carrying a + # tagger, a date and a message, unlike a lightweight ref. + git -c user.name='github-actions[bot]' \ + -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ + tag -a "${TAG}" -m "${TAG}" "${GITHUB_SHA}" + + git push origin "refs/tags/${TAG}" + + echo "Pushed annotated tag ${TAG} at ${GITHUB_SHA}." + + - name: Publish the GitHub Release + id: publish + if: ${{ steps.guard.outputs.skip == 'false' }} + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TAG: ${{ steps.resolve.outputs.tag }} + TITLE: ${{ inputs.title }} + NOTES_MODE: ${{ steps.notes.outputs.mode }} + NOTES_FILE: ${{ inputs.notes-file }} + DRAFT: ${{ inputs.draft }} + PRERELEASE: ${{ inputs.prerelease }} + run: | + set -euo pipefail + + args=(release create "${TAG}" --repo "${REPO}" --verify-tag) + args+=(--title "${TITLE:-${TAG}}") + + case "${NOTES_MODE}" in + file) args+=(--notes-file "${NOTES_FILE}") ;; + generate) args+=(--generate-notes) ;; + *) args+=(--notes "") ;; + esac + + if [ "${DRAFT}" = "true" ]; then + args+=(--draft) + fi + + if [ "${PRERELEASE}" = "true" ]; then + args+=(--prerelease) + fi + + gh "${args[@]}" + + url="$(gh release view "${TAG}" --repo "${REPO}" --json url --jq .url)" + + { + echo "released=true" + echo "url=${url}" + } >> "${GITHUB_OUTPUT}" + + echo "Published ${url}" + + - name: Report a skipped run + if: ${{ steps.guard.outputs.skip == 'true' }} + env: + TAG: ${{ steps.resolve.outputs.tag }} + run: echo "${TAG} was already released. Nothing to do." From ead0b6cf6c6c53bc1423a3cdf05c5683a1f111d9 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 15:57:17 -0400 Subject: [PATCH 2/2] feat(workflow-templates): add release and ci-mobile-ios templates, pass node-version on ci-python release.yml / release.properties.json and ci-mobile-ios.yml / ci-mobile-ios.properties.json are new caller templates for the two reusable workflows added in 9389e51. Both pin the reusable to 9389e51, the commit that introduces the workflow files. release.yml triggers on workflow_dispatch with a required `version` input and declares `permissions: contents: write`, which the reusable needs to push the tag and publish the Release. ci-mobile-ios.yml triggers on pull_request and keys its job `ci` so the check context resolves to `ci / ci`. ci-python.yml now passes `node-version: "24"`. Its target, ci-python-sam.yaml, declares that input at line 34 with default "24"; the ci-static, ci-typescript-frontend, ci-node, cdk-deploy and mobile-ios-deploy templates already pass the same value. Both new .properties.json files carry the same five keys as the thirteen existing ones: categories, description, filePatterns, iconName, name. --- .../ci-mobile-ios.properties.json | 7 ++++++ workflow-templates/ci-mobile-ios.yml | 21 ++++++++++++++++ workflow-templates/ci-python.yml | 4 ++++ workflow-templates/release.properties.json | 7 ++++++ workflow-templates/release.yml | 24 +++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 workflow-templates/ci-mobile-ios.properties.json create mode 100644 workflow-templates/ci-mobile-ios.yml create mode 100644 workflow-templates/release.properties.json create mode 100644 workflow-templates/release.yml diff --git a/workflow-templates/ci-mobile-ios.properties.json b/workflow-templates/ci-mobile-ios.properties.json new file mode 100644 index 0000000..b27a105 --- /dev/null +++ b/workflow-templates/ci-mobile-ios.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — CI (Mobile / iOS)", + "description": "Runs npm ci, typecheck, optional lint and optional unit tests on Linux, then compiles the iOS app on macOS with signing disabled and no upload, via the org reusable ci-mobile-ios workflow. The pre-merge counterpart to the iOS TestFlight deploy.", + "iconName": "octicon-checklist", + "categories": ["Mobile", "TypeScript", "JavaScript", "Continuous integration"], + "filePatterns": ["metro\\.config\\.[cm]?js$", "app\\.json$", "Podfile$"] +} diff --git a/workflow-templates/ci-mobile-ios.yml b/workflow-templates/ci-mobile-ios.yml new file mode 100644 index 0000000..01c348c --- /dev/null +++ b/workflow-templates/ci-mobile-ios.yml @@ -0,0 +1,21 @@ +name: CI (Mobile / iOS) +on: + pull_request: + branches: [main] + +jobs: + ci: + # Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the + # check context resolves to the required `ci / ci`. + uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@9389e51c10c506caa55f204527452a9e29b0e438 # main + with: + # Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also + # the reusable workflow's default — passed explicitly to pin against drift. + node-version: "24" + # Set these two to the app directory when the mobile project is not at + # the repo root, matching the values passed to cd-mobile-ios. + working-directory: "." + cache-dependency-path: "package-lock.json" + # Off by default: enable once package.json actually declares the script. + run-lint: false + run-tests: false diff --git a/workflow-templates/ci-python.yml b/workflow-templates/ci-python.yml index 9ae9f03..a12000f 100644 --- a/workflow-templates/ci-python.yml +++ b/workflow-templates/ci-python.yml @@ -8,3 +8,7 @@ jobs: uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@555d07c3a240689a81668026787eba089df4c975 # main with: run-tests: true + # ci-python-sam.yaml declares a `node-version` input (default "24") that + # feeds the CDK CLI when run-cdk-synth is enabled. Passed explicitly so + # the pin does not drift from local dev (Node 24 / npm 11). + node-version: "24" diff --git a/workflow-templates/release.properties.json b/workflow-templates/release.properties.json new file mode 100644 index 0000000..b430506 --- /dev/null +++ b/workflow-templates/release.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — Release (tag + GitHub Release)", + "description": "Creates an annotated v-prefixed git tag and publishes a GitHub Release for a version supplied on manual dispatch, using the org reusable release workflow. Re-running on a version that is already tagged or released is a no-op.", + "iconName": "octicon-tag", + "categories": ["Deployment", "Utilities"], + "filePatterns": ["README\\.md$"] +} diff --git a/workflow-templates/release.yml b/workflow-templates/release.yml new file mode 100644 index 0000000..1dab305 --- /dev/null +++ b/workflow-templates/release.yml @@ -0,0 +1,24 @@ +name: Release +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release, e.g. 1.4.0 (a leading "v" is accepted)' + type: string + required: true + +permissions: + # The reusable workflow pushes an annotated tag and publishes a Release. + contents: write + +jobs: + release: + uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@9389e51c10c506caa55f204527452a9e29b0e438 # main + with: + version: ${{ inputs.version }} + # Tags in this org are v-prefixed MAJOR.MINOR.PATCH; "v" is the default + # and is passed explicitly so a repo can see what it is producing. + tag-prefix: "v" + # Notes come from GitHub's commit-history generator. Set notes-file + # instead to publish the contents of a file as the release notes. + generate-notes: true