mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
Add a fastlane pipeline that releases HealthChat to TestFlight (#2)
element-x-ios had no release machinery at all: no fastlane, no Gemfile, and none of its workflows built or uploaded anything. Upstream ships via Xcode Cloud, which needs Element's private Enterprise submodule and so is unavailable here, while the legacy element-ios fork already proves a fastlane pipeline against the same Apple team, certificate and App Store Connect key. Tagging healthchat-v<version> now builds a signed App Store IPA on the org's Mac Studio and uploads it to TestFlight. Release notes come from a new HEALTHCHAT_CHANGES.md, because upstream's own tooling owns CHANGES.md. The lane deliberately does not pin Xcode. The runner carries a single globally selected version, and naming an absent one does not fail fast — it sends xcodes into a multi-GB authenticated download that no CI secret can satisfy. Each run logs xcodebuild -version instead, which is the only record of what compiled a build. Everything here is fork-owned, since upstream has no fastlane directory, so none of it can conflict on rebase.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
name: Release HealthChat
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'healthchat-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Marketing version (e.g. 26.7.4). Must be 1-3 dot-separated integers — Apple rejects anything else.'
|
||||
required: true
|
||||
type: string
|
||||
release_notes_override:
|
||||
description: 'Optional release notes. If empty, falls back to the HEALTHCHAT_CHANGES.md section for the given version.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
# Minimum needed for actions/checkout to clone an internal repo. The workflow does not push,
|
||||
# comment, or create releases — everything else authenticates with the ASC key and .p12.
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-healthchat-${{ github.event.inputs.version || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build & upload to TestFlight
|
||||
runs-on: [self-hosted, macOS]
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# This repo's only submodule is element-hq's private Enterprise repo, which Novaloop
|
||||
# cannot clone. Checking out submodules fails the job on its very first step.
|
||||
submodules: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Record toolchain
|
||||
# HealthChat: nothing pins Xcode (the runner carries a single globally-selected one), so this
|
||||
# log line is the only record of which compiler produced a given TestFlight build.
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: Resolve marketing version
|
||||
id: version
|
||||
env:
|
||||
# Read via env, never `${{ }}` inside the script: the input is attacker-controlled text
|
||||
# that GitHub would splice into the shell before the regex below could reject it.
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
VERSION="$INPUT_VERSION"
|
||||
else
|
||||
VERSION="${GITHUB_REF_NAME#healthchat-v}"
|
||||
fi
|
||||
# Apple caps CFBundleShortVersionString at three integers. The Fastfile guards this too,
|
||||
# but failing here costs seconds instead of a signing run.
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
|
||||
echo "::error::Invalid marketing version '$VERSION' — expected 1-3 dot-separated integers, e.g. 26.7.4"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved marketing version: $VERSION"
|
||||
|
||||
- name: Extract release notes
|
||||
id: notes
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
OVERRIDE: ${{ github.event.inputs.release_notes_override }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "$OVERRIDE" ]]; then
|
||||
NOTES="$OVERRIDE"
|
||||
echo "Using release notes from the workflow input"
|
||||
elif [[ ! -f HEALTHCHAT_CHANGES.md ]]; then
|
||||
echo "::error::HEALTHCHAT_CHANGES.md is missing — it is the release-notes source"
|
||||
exit 1
|
||||
else
|
||||
NOTES=$(awk -v ver="$VERSION" '
|
||||
/^## Changes in / {
|
||||
if (found) { exit }
|
||||
if ($0 ~ "## Changes in " ver "( |$|\\()") { found = 1; next }
|
||||
}
|
||||
found { print }
|
||||
' HEALTHCHAT_CHANGES.md | sed '/^[[:space:]]*$/{N;/^\n$/D;}')
|
||||
if [[ -z "$NOTES" ]]; then
|
||||
NOTES="HealthChat ${VERSION} — automated build from ${GITHUB_REF_NAME}."
|
||||
echo "::warning::No HEALTHCHAT_CHANGES.md section found for $VERSION — using fallback"
|
||||
fi
|
||||
fi
|
||||
{
|
||||
echo 'notes<<NOTES_EOF'
|
||||
echo "$NOTES"
|
||||
echo 'NOTES_EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Bundle install
|
||||
run: |
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Import signing certificate
|
||||
uses: apple-actions/import-codesign-certs@b610f78488812c1e56b20e6df63ec42d833f2d14 # v6.0.0
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
- name: Build & upload to TestFlight
|
||||
run: bundle exec fastlane release_testflight
|
||||
env:
|
||||
# Passed via env rather than lane options so multi-line notes containing quotes,
|
||||
# $, or backticks survive without shell escaping.
|
||||
HEALTHCHAT_MARKETING_VERSION: ${{ steps.version.outputs.version }}
|
||||
HEALTHCHAT_RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
|
||||
APPSTORECONNECT_KEY_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ID }}
|
||||
APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ISSUER_ID }}
|
||||
APPSTORECONNECT_KEY_CONTENT: ${{ secrets.HEALTHCHAT_ASC_KEY_CONTENT }}
|
||||
TEAM_ID: '6G37K72W82'
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: healthchat-${{ steps.version.outputs.version }}
|
||||
path: |
|
||||
out/ElementX.ipa
|
||||
out/ElementX.app.dSYM.zip
|
||||
build/Logs/Gym/*.log
|
||||
retention-days: 90
|
||||
if-no-files-found: warn
|
||||
Reference in New Issue
Block a user