diff --git a/.github/workflows/android-main.yml b/.github/workflows/android-main.yml new file mode 100644 index 00000000..d3fb2d0d --- /dev/null +++ b/.github/workflows/android-main.yml @@ -0,0 +1,42 @@ +name: Android main verification + +on: + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + evaluate-main: + name: Evaluate main gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + package-manager-cache: false + + - run: npm install --global npm@11.19.0 + - run: npm ci --ignore-scripts + + - id: main + name: Evaluate main gate + continue-on-error: true + run: npm run verify:android-gates -- --gate main --run-id "${{ github.run_id }}-main" + + - name: Upload Main Gate evidence + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: android-main-evidence + path: artifacts/mobile-evidence/android-gates + if-no-files-found: error + retention-days: 30 + + - name: Enforce main verdict + if: ${{ steps.main.outcome == 'failure' }} + run: exit 1 diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 829062f5..594b0081 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -49,8 +49,7 @@ "minSdkVersion": 30 } } - ], - "expo-network" + ] ], "experiments": { "typedRoutes": true diff --git a/apps/mobile/tests/scaffold.test.mjs b/apps/mobile/tests/scaffold.test.mjs index 58d043e9..7dfef646 100644 --- a/apps/mobile/tests/scaffold.test.mjs +++ b/apps/mobile/tests/scaffold.test.mjs @@ -68,6 +68,10 @@ test("the Android build enables SQLCipher and excludes all app data from backup" assert.equal(packageManifest.dependencies["expo-secure-store"], "57.0.2"); assert.equal(packageManifest.dependencies["expo-sqlite"], "57.0.2"); assert.equal(packageManifest.dependencies["expo-network"], "57.0.1"); + assert.ok( + !appManifest.expo.plugins.flat().includes("expo-network"), + "expo-network has no app.plugin.js; listing it in plugins loads build/Network.js under Node and fails type stripping", + ); }); test("SQLCipher is proven in memory before a persistent database is opened", () => { diff --git a/scripts/android-gates-workflow.test.mjs b/scripts/android-gates-workflow.test.mjs index 791bf09a..e873612f 100644 --- a/scripts/android-gates-workflow.test.mjs +++ b/scripts/android-gates-workflow.test.mjs @@ -19,8 +19,9 @@ async function workflowFiles() { } test("workflows invoke every Android gate without a release promotion", async () => { - const [build, candidate, publicRelease, release] = await Promise.all([ + const [build, main, candidate, publicRelease, release] = await Promise.all([ workflow("build.yml"), + workflow("android-main.yml"), workflow("android-candidate.yml"), workflow("android-public-release.yml"), workflow("release.yml"), @@ -29,10 +30,18 @@ test("workflows invoke every Android gate without a release promotion", async () assert.match(build.source, /--gate change/); assert.match(build.source, /ANDROID_GATE_FRAGMENT: "1"/); assert.doesNotMatch(build.source, /--gate main/); + assert.match(main.source, /--gate main/); + assert.equal(main.value.jobs["main-gate"], undefined); + assert.ok(main.value.jobs["evaluate-main"]); assert.match(candidate.source, /--gate candidate/); assert.match(publicRelease.source, /--gate public-release/); assert.doesNotMatch(release.source, /verify:android-gates|android-public-release/i); - for (const source of [build.source, candidate.source, publicRelease.source]) { + for (const source of [ + build.source, + main.source, + candidate.source, + publicRelease.source, + ]) { assert.doesNotMatch(source, /firebase\s+test\s+lab|test-lab/i); assert.doesNotMatch(source, HOSTED_EMU_FORBIDDEN); }