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<> "$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