Files
pleno-vue/tests/unit/mobile-store-test-gate.spec.js
T
Jeppe B 7782d93fe9 Gate mobile releases behind explicit phased rollout (#212)
Require explicit mobile-v* tags or manual dispatch, gate exact tested master SHAs, and default Google Play production submissions to an initial 1% in-progress rollout.
2026-07-22 18:51:40 +02:00

224 lines
7.3 KiB
JavaScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
evaluateStoreGate,
expectedStoreGateJobs,
verifyStoreTestGate,
} from "../../scripts/mobile/verify-store-test-gate.mjs";
const sha = "a".repeat(40);
const successfulJobs = (platform) =>
expectedStoreGateJobs(platform).map((name, index) => ({
id: index + 1,
name,
run_attempt: 1,
status: "completed",
conclusion: "success",
}));
describe("mobile store test gates", () => {
it("maps Android to every Chromium mobile role shard", () => {
expect(expectedStoreGateJobs("android")).toEqual([
"E2E-full-Chromium-mobile-superuser-shard-1-of-2",
"E2E-full-Chromium-mobile-superuser-shard-2-of-2",
"E2E-full-Chromium-mobile-admin-shard-1-of-2",
"E2E-full-Chromium-mobile-admin-shard-2-of-2",
"E2E-full-Chromium-mobile-customer-shard-1-of-1",
"E2E-full-Chromium-mobile-subuser-shard-1-of-1",
]);
});
it("maps Apple to every WebKit mobile role shard", () => {
expect(expectedStoreGateJobs("apple")).toEqual([
"E2E-full-WebKit-mobile-superuser-shard-1-of-2",
"E2E-full-WebKit-mobile-superuser-shard-2-of-2",
"E2E-full-WebKit-mobile-admin-shard-1-of-2",
"E2E-full-WebKit-mobile-admin-shard-2-of-2",
"E2E-full-WebKit-mobile-customer-shard-1-of-1",
"E2E-full-WebKit-mobile-subuser-shard-1-of-1",
]);
});
it("rejects a missing, skipped, or failed required slice", () => {
const jobs = successfulJobs("android");
jobs.splice(3, 1);
jobs[3] = { ...jobs[3], conclusion: "failure" };
expect(evaluateStoreGate("android", jobs)).toMatchObject({
passed: false,
failures: [
"E2E-full-Chromium-mobile-admin-shard-2-of-2:missing",
"E2E-full-Chromium-mobile-customer-shard-1-of-1:completed/failure",
],
});
});
it("uses the newest execution of a rerun job while carrying over successful jobs from earlier attempts", () => {
const jobs = successfulJobs("android");
jobs.push({ ...jobs[0], id: 100, run_attempt: 2 });
expect(evaluateStoreGate("android", jobs)).toMatchObject({ passed: true, failures: [] });
jobs.push({ ...jobs[0], id: 101, run_attempt: 3, conclusion: "failure" });
expect(evaluateStoreGate("android", jobs)).toMatchObject({
passed: false,
failures: ["E2E-full-Chromium-mobile-superuser-shard-1-of-2:completed/failure"],
});
});
it("accepts only a completed push run for the exact source SHA", async () => {
const responses = [
{
id: 123,
name: "Automated Tests",
head_sha: sha,
head_branch: "master",
event: "push",
status: "completed",
conclusion: "success",
},
{ jobs: successfulJobs("apple") },
];
const paths = [];
const fetchImpl = async (url) => {
paths.push(new URL(url).pathname + new URL(url).search);
return {
ok: true,
json: async () => responses.shift(),
};
};
await expect(
verifyStoreTestGate({
platform: "apple",
sourceSha: sha,
runId: "123",
defaultBranch: "master",
repository: "owner/repo",
token: "test-token",
fetchImpl,
})
).resolves.toMatchObject({ passed: true, runId: 123, sourceSha: sha });
expect(paths).toEqual([
"/repos/owner/repo/actions/runs/123",
"/repos/owner/repo/actions/runs/123/jobs?filter=all&per_page=100&page=1",
]);
});
it("accepts carried-over successful jobs after a partial rerun", async () => {
const jobs = successfulJobs("apple");
jobs.push({ ...jobs[0], id: 100, run_attempt: 2 });
const responses = [
{
id: 123,
name: "Automated Tests",
head_sha: sha,
head_branch: "master",
event: "push",
status: "completed",
conclusion: "success",
},
{ jobs },
];
const paths = [];
const fetchImpl = async (url) => {
paths.push(new URL(url).pathname + new URL(url).search);
return { ok: true, json: async () => responses.shift() };
};
await expect(
verifyStoreTestGate({
platform: "apple",
sourceSha: sha,
runId: "123",
defaultBranch: "master",
repository: "owner/repo",
token: "test-token",
fetchImpl,
})
).resolves.toMatchObject({ passed: true });
expect(paths[1]).toContain("filter=all");
});
it("rejects a completed test run whose overall conclusion failed", async () => {
const fetchImpl = async () => ({
ok: true,
json: async () => ({
id: 123,
name: "Automated Tests",
head_sha: sha,
head_branch: "master",
event: "push",
status: "completed",
conclusion: "failure",
}),
});
await expect(
verifyStoreTestGate({
platform: "android",
sourceSha: sha,
runId: "123",
defaultBranch: "master",
repository: "owner/repo",
token: "test-token",
fetchImpl,
})
).rejects.toThrow("did not succeed (failure)");
});
it("fails closed when the run belongs to another branch", async () => {
const fetchImpl = async () => ({
ok: true,
json: async () => ({
id: 123,
name: "Automated Tests",
head_sha: sha,
head_branch: "dev",
event: "push",
status: "completed",
conclusion: "success",
}),
});
await expect(
verifyStoreTestGate({
platform: "android",
sourceSha: sha,
runId: "123",
defaultBranch: "master",
repository: "owner/repo",
token: "test-token",
fetchImpl,
})
).rejects.toThrow("not a push run for master");
});
it("resolves current master before any manual or tag Play upload", () => {
const source = readFileSync(join(process.cwd(), ".github/workflows/mobile-artifacts.yml"), "utf8");
expect(source).toContain('tags:\n - "mobile-v*"');
expect(source).not.toContain("workflow_run:");
expect(source).not.toContain("github.event.workflow_run");
expect(source).toContain("UPLOAD_TO_PLAY:");
expect(source).toContain("PLAY_STORE_RELEASE_STATUS: ${{ inputs.android_release_status || 'inProgress' }}");
expect(source).toContain('PLAY_STORE_USER_FRACTION: "0.01"');
expect(source).not.toContain("- completed");
expect(source).toContain("git/ref/heads/$DEFAULT_BRANCH");
expect(source).toContain('[[ "$latest_sha" != "$EXPECTED_SHA" && "$UPLOAD_TO_PLAY" == "true" ]]');
expect(source).toContain('echo "source_sha=$latest_sha" >> "$GITHUB_OUTPUT"');
expect(source).toContain("STORE_SOURCE_SHA: ${{ steps.release-guard.outputs.source_sha }}");
expect(source).toContain("Recheck live master before Play upload");
expect(source).toContain("Record immutable Android artifact proof");
expect(source).toContain("Record Google Play submission proof");
expect(source).toContain("platform=android applicationId=%s version=%s buildNumber=%s");
expect(source).toContain("signingIdentityRef=%s storeSubmissionId=%s");
expect(source).toContain('[[ "$live_master_sha" == "$EXPECTED_SHA" ]]');
expect(source.indexOf("Recheck live master before Play upload")).toBeLessThan(
source.indexOf("Upload Android App Bundle to Google Play")
);
});
});