ci: soft-fail mobile OTA when EXPO_TOKEN is missing (#124)

* ci: soft-fail mobile OTA when EXPO_TOKEN is missing

OTA publish is an infra helper; missing Expo credentials should not
fail the ci workflow or block evo-deploy after green Product CI.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* test(updater): narrow optional workflow jobs and format the OTA assertion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mFCmHk4zPyWpD9yY2C3yJ

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-03 23:04:17 +02:00 committed by GitHub
parent d912712668
commit 0d1939c775
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 10 deletions

View File

@ -246,6 +246,7 @@ jobs:
publish-mobile-update: publish-mobile-update:
name: Publish compatible mobile update name: Publish compatible mobile update
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
continue-on-error: true
needs: [lint, check, build, test, test-integration, test-e2e] needs: [lint, check, build, test, test-integration, test-e2e]
runs-on: [self-hosted, evoo-ci, Linux] runs-on: [self-hosted, evoo-ci, Linux]
timeout-minutes: 20 timeout-minutes: 20
@ -344,23 +345,30 @@ jobs:
fi fi
echo "publish=$publish" >> "$GITHUB_OUTPUT" echo "publish=$publish" >> "$GITHUB_OUTPUT"
- name: Require Expo automation token - name: Check Expo automation token
id: expo_token
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true'
env: env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: | run: |
if [[ -z "$EXPO_TOKEN" ]]; then if [[ -z "$EXPO_TOKEN" ]]; then
echo "Add an EXPO_TOKEN repository secret with access to the inbox-zero/rakazo Expo project." >&2 {
exit 1 echo "### Mobile OTA skipped"
echo
echo "No \`EXPO_TOKEN\` repository secret is configured, so CI will not publish a production update. Add a token with access to the inbox-zero/rakazo Expo project when OTA from main is desired."
} >> "$GITHUB_STEP_SUMMARY"
echo "ready=false" >> "$GITHUB_OUTPUT"
exit 0
fi fi
echo "ready=true" >> "$GITHUB_OUTPUT"
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
id: pnpm-setup id: pnpm-setup
with: with:
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }}
- name: Isolate pnpm store and PATH - name: Isolate pnpm store and PATH
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
@ -377,25 +385,25 @@ jobs:
mkdir -p "$store" mkdir -p "$store"
echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV"
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
- name: Set up Expo - name: Set up Expo
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # v9 uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # v9
with: with:
eas-version: latest eas-version: latest
packager: pnpm packager: pnpm
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies - name: Install dependencies
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Verify mobile project - name: Verify mobile project
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
run: pnpm --filter @rakazo/mobile check run: pnpm --filter @rakazo/mobile check
- name: Publish production update - name: Publish production update
if: steps.ota.outputs.publish == 'true' if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
working-directory: apps/mobile working-directory: apps/mobile
run: | run: |
set -euo pipefail set -euo pipefail

View File

@ -0,0 +1,47 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const workflowText = readFileSync(
path.resolve(import.meta.dirname, "../../../.github/workflows/ci.yml"),
"utf8",
);
interface WorkflowJob {
if?: string;
needs?: string[];
"continue-on-error"?: boolean;
}
const workflow = parse(workflowText) as {
jobs: Record<string, WorkflowJob>;
};
describe("ci workflow merge gate", () => {
it("aggregates only product jobs in Product CI", () => {
const productGate = workflow.jobs["product-gate"];
expect(productGate?.needs).toEqual([
"lint",
"check",
"build",
"test",
"test-integration",
"test-e2e",
]);
expect(productGate?.if).toBe("always()");
});
it("soft-fails mobile OTA so missing Expo credentials do not fail the workflow", () => {
const mobile = workflow.jobs["publish-mobile-update"];
expect(mobile?.["continue-on-error"]).toBe(true);
expect(workflowText).toContain('echo "ready=false" >> "$GITHUB_OUTPUT"');
expect(workflowText).not.toMatch(
/Check Expo automation token[\s\S]*?exit 1[\s\S]*?Require Expo/,
);
expect(workflowText).not.toContain(
"Add an EXPO_TOKEN repository secret with access to the inbox-zero/rakazo Expo project.",
);
expect(workflowText).toContain("steps.expo_token.outputs.ready == 'true'");
});
});