diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad09384..3621a7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,6 +246,7 @@ jobs: publish-mobile-update: name: Publish compatible mobile update if: github.event_name == 'push' && github.ref == 'refs/heads/main' + continue-on-error: true needs: [lint, check, build, test, test-integration, test-e2e] runs-on: [self-hosted, evoo-ci, Linux] timeout-minutes: 20 @@ -344,23 +345,30 @@ jobs: fi echo "publish=$publish" >> "$GITHUB_OUTPUT" - - name: Require Expo automation token + - name: Check Expo automation token + id: expo_token if: steps.ota.outputs.publish == 'true' env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} run: | 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 + echo "ready=true" >> "$GITHUB_OUTPUT" - 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 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }} - 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 run: | set -euo pipefail @@ -377,25 +385,25 @@ jobs: mkdir -p "$store" echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV" - 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: node-version: 24 cache: pnpm - 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 with: eas-version: latest packager: pnpm token: ${{ secrets.EXPO_TOKEN }} - 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 - 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 - 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 run: | set -euo pipefail diff --git a/infra/updater/src/ci-workflow.test.ts b/infra/updater/src/ci-workflow.test.ts new file mode 100644 index 0000000..ddbaae1 --- /dev/null +++ b/infra/updater/src/ci-workflow.test.ts @@ -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; +}; + +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'"); + }); +});