feat(release): report endpoint build timings

This commit is contained in:
tianma-if committed 2026-08-26 00:19:09 +08:00
1 parent 5ab291cbcd
commit b7e7e6e986
5 files changed
+738

No files matched your search

+239
View File
@@ -0,0 +1,239 @@
name: Report Release build timings
on:
workflow_dispatch:
inputs:
release_tag:
description: "Formal vX.Y.Z Release"
required: true
type: string
release_sha:
description: "Release commit SHA"
required: true
type: string
issue_number:
description: "Related Issue receiving the timing report"
required: true
type: string
desktop_run_id:
description: "Draft desktop preparation Run ID"
required: true
type: string
desktop_mode:
description: "Desktop release plan"
required: true
type: choice
options: [rebuild, reuse]
mobile_run_id:
description: "Draft Android preparation Run ID"
required: true
type: string
mobile_mode:
description: "Android release plan"
required: true
type: choice
options: [rebuild, reuse]
docker_run_id:
description: "Draft GHCR preparation Run ID"
required: true
type: string
permissions:
actions: read
contents: read
issues: write
concurrency:
group: release-build-timings-${{ inputs.release_tag }}
cancel-in-progress: false
jobs:
report:
if: github.repository == 'tianma-if/edgeever'
name: Collect all Release endpoint timings
runs-on: ubuntu-latest
timeout-minutes: 50
env:
GH_TOKEN: ${{ github.token }}
TCR_IMAGE: ccr.ccs.tencentyun.com/edgeever/edgeever
steps:
- name: Check out timing reporter
uses: actions/checkout@v5
- name: Resolve Release and post-publication workflow runs
id: release
env:
RELEASE_SHA: ${{ inputs.release_sha }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
release_json="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease,publishedAt,targetCommitish,url)"
test "$(jq -r '.isDraft' <<<"$release_json")" = "false"
test "$(jq -r '.isPrerelease' <<<"$release_json")" = "false"
test "$(jq -r '.targetCommitish' <<<"$release_json")" = "$RELEASE_SHA"
find_release_run() {
local workflow="$1"
local deadline=$((SECONDS + 120))
while ((SECONDS < deadline)); do
run_id="$(
gh run list \
--repo "$GITHUB_REPOSITORY" \
--workflow "$workflow" \
--event release \
--limit 20 \
--json databaseId,displayTitle,headSha \
--jq ".[] | select(.displayTitle == \"$RELEASE_TAG\" and .headSha == \"$RELEASE_SHA\") | .databaseId" |
head -n 1
)"
if [[ -n "$run_id" ]]; then
printf '%s' "$run_id"
return 0
fi
sleep 5
done
return 1
}
demo_run_id="$(find_release_run deploy-demo.yml)"
tcr_run_id="$(find_release_run docker-tcr-mirror.yml)"
{
echo "published_at=$(jq -r '.publishedAt' <<<"$release_json")"
echo "release_url=$(jq -r '.url' <<<"$release_json")"
echo "demo_run_id=$demo_run_id"
echo "tcr_run_id=$tcr_run_id"
} >> "$GITHUB_OUTPUT"
- name: Wait for Cloudflare deployment and TCR source sync
id: post_release
env:
DEMO_RUN_ID: ${{ steps.release.outputs.demo_run_id }}
TCR_RUN_ID: ${{ steps.release.outputs.tcr_run_id }}
run: |
wait_for_run() {
local run_id="$1"
while true; do
run_json="$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json status,conclusion,url)"
status="$(jq -r '.status' <<<"$run_json")"
if [[ "$status" = "completed" ]]; then
jq -r '.conclusion' <<<"$run_json"
return 0
fi
sleep 10
done
}
echo "demo_conclusion=$(wait_for_run "$DEMO_RUN_ID")" >> "$GITHUB_OUTPUT"
echo "tcr_source_conclusion=$(wait_for_run "$TCR_RUN_ID")" >> "$GITHUB_OUTPUT"
- name: Observe public TCR Release readiness
id: tcr
env:
RELEASE_SHA: ${{ inputs.release_sha }}
RELEASE_TAG: ${{ inputs.release_tag }}
SOURCE_CONCLUSION: ${{ steps.post_release.outputs.tcr_source_conclusion }}
run: |
status="failure"
ready_at=""
if [[ "$SOURCE_CONCLUSION" = "success" ]]; then
for attempt in $(seq 1 180); do
if raw_manifest="$(docker buildx imagetools inspect --raw "${TCR_IMAGE}:${RELEASE_TAG}" 2>/dev/null)" &&
jq -e '([.manifests[].platform.architecture] | index("amd64") != null) and ([.manifests[].platform.architecture] | index("arm64") != null)' <<<"$raw_manifest" >/dev/null; then
metadata="$(docker buildx imagetools inspect "${TCR_IMAGE}:${RELEASE_TAG}" --format '{{json .Image}}' 2>/dev/null || true)"
if jq -e --arg revision "$RELEASE_SHA" 'length >= 2 and all(.[]; .config.Labels["org.opencontainers.image.revision"] == $revision)' <<<"$metadata" >/dev/null 2>&1; then
status="success"
ready_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
break
fi
fi
sleep 15
done
if [[ "$status" != "success" ]]; then
status="timeout"
fi
fi
{
echo "status=$status"
echo "ready_at=$ready_at"
} >> "$GITHUB_OUTPUT"
- name: Download exact Actions timing data
env:
DEMO_RUN_ID: ${{ steps.release.outputs.demo_run_id }}
DESKTOP_RUN_ID: ${{ inputs.desktop_run_id }}
DOCKER_RUN_ID: ${{ inputs.docker_run_id }}
MOBILE_RUN_ID: ${{ inputs.mobile_run_id }}
TCR_RUN_ID: ${{ steps.release.outputs.tcr_run_id }}
run: |
collect_run() {
local name="$1"
local run_id="$2"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}" > "$RUNNER_TEMP/${name}-run.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/${name}-jobs.json"
jq -s '{run: .[0], jobs: .[1].jobs}' \
"$RUNNER_TEMP/${name}-run.json" \
"$RUNNER_TEMP/${name}-jobs.json" \
> "$RUNNER_TEMP/${name}.json"
}
collect_run desktop "$DESKTOP_RUN_ID"
collect_run mobile "$MOBILE_RUN_ID"
collect_run docker "$DOCKER_RUN_ID"
collect_run cloudflare "$DEMO_RUN_ID"
collect_run tcr-source "$TCR_RUN_ID"
- name: Generate unified Release timing report
env:
RELEASE_URL: ${{ steps.release.outputs.release_url }}
TCR_READY_AT: ${{ steps.tcr.outputs.ready_at }}
TCR_STATUS: ${{ steps.tcr.outputs.status }}
run: |
node scripts/report-release-timings.mjs \
--tag "${{ inputs.release_tag }}" \
--sha "${{ inputs.release_sha }}" \
--published-at "${{ steps.release.outputs.published_at }}" \
--release-url "$RELEASE_URL" \
--desktop "$RUNNER_TEMP/desktop.json" \
--desktop-mode "${{ inputs.desktop_mode }}" \
--mobile "$RUNNER_TEMP/mobile.json" \
--mobile-mode "${{ inputs.mobile_mode }}" \
--docker "$RUNNER_TEMP/docker.json" \
--cloudflare "$RUNNER_TEMP/cloudflare.json" \
--tcr-source "$RUNNER_TEMP/tcr-source.json" \
--tcr-ready-at "$TCR_READY_AT" \
--tcr-status "$TCR_STATUS" \
--json "$RUNNER_TEMP/release-build-timings.json" \
--markdown "$RUNNER_TEMP/release-build-timings.md"
cat "$RUNNER_TEMP/release-build-timings.md" >> "$GITHUB_STEP_SUMMARY"
- name: Return timing report to the related Issue
env:
ISSUE_NUMBER: ${{ inputs.issue_number }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
marker="<!-- edgeever-release-build-timings:${RELEASE_TAG} -->"
comment_file="$RUNNER_TEMP/release-build-timings-comment.md"
printf '%s\n\n' "$marker" > "$comment_file"
cat "$RUNNER_TEMP/release-build-timings.md" >> "$comment_file"
comment_id="$(
gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/comments?per_page=100" \
--paginate \
--jq ".[] | select(.body | startswith(\"$marker\")) | .id" |
head -n 1
)"
if [[ -n "$comment_id" ]]; then
jq -n --rawfile body "$comment_file" '{body: $body}' |
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" --input - >/dev/null
else
gh issue comment "$ISSUE_NUMBER" \
--repo "$GITHUB_REPOSITORY" \
--body-file "$comment_file"
fi
- name: Upload machine-readable Release timing report
uses: actions/upload-artifact@v7
with:
name: edgeever-${{ inputs.release_tag }}-build-timings
path: ${{ runner.temp }}/release-build-timings.json
if-no-files-found: error
retention-days: 90
+33
View File
@@ -27,6 +27,7 @@ export const RELEASE_WORKFLOWS = {
androidPlaySignature: "android-play-signature-audit.yml",
docker: "docker-image.yml",
demo: "deploy-demo.yml",
timings: "release-timings.yml",
};
export const RELEASE_VALIDATIONS = [
@@ -1178,6 +1179,38 @@ const releaseMain = async (options) => {
"--body",
`Released in [${tag}](${releaseUrl}).\n\nRequired local validations, Draft asset and image preparation, and post-publication audits passed.`,
]);
const timingDispatch = run("gh", [
"workflow",
"run",
RELEASE_WORKFLOWS.timings,
"--repo",
options.repository,
"--ref",
"main",
"-f",
`release_tag=${tag}`,
"-f",
`release_sha=${releaseSha}`,
"-f",
`issue_number=${issueNumber}`,
"-f",
`desktop_run_id=${desktopRunId}`,
"-f",
`desktop_mode=${desktopPlan.rebuild ? "rebuild" : "reuse"}`,
"-f",
`mobile_run_id=${mobileRunId}`,
"-f",
`mobile_mode=${mobilePlan.rebuild ? "rebuild" : "reuse"}`,
"-f",
`docker_run_id=${dockerRunId}`,
], { allowFailure: true });
if (timingDispatch.status === 0) {
console.log(
`[release] endpoint timing report continues in background: https://github.com/${options.repository}/actions/workflows/${RELEASE_WORKFLOWS.timings}`,
);
} else {
console.warn("[release] failed to dispatch the non-blocking endpoint timing report");
}
run("gh", [
"issue",
"close",
+9
View File
@@ -21,6 +21,15 @@ describe("release automation", () => {
expect(RELEASE_WORKFLOWS.docker).toBe("docker-image.yml");
});
test("dispatches a unified post-release endpoint timing report", () => {
expect(RELEASE_WORKFLOWS.timings).toBe("release-timings.yml");
const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8");
expect(releaseSource).toContain("desktop_run_id=\${desktopRunId}");
expect(releaseSource).toContain("mobile_run_id=\${mobileRunId}");
expect(releaseSource).toContain("docker_run_id=\${dockerRunId}");
expect(releaseSource).toContain("endpoint timing report continues in background");
});
test("blocks publication until the Draft Android APK passes the Play signature gate", () => {
const releaseSource = readFileSync(new URL("./release.mjs", import.meta.url), "utf8");
const signatureGate = releaseSource.indexOf('label: "Draft Android Play signature gate"');
+292
View File
@@ -0,0 +1,292 @@
import { readFileSync, writeFileSync } from "node:fs";
import { durationMs, formatDuration } from "./report-native-build-timings.mjs";
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
const timestamp = (value) => {
if (!value) return null;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
};
const runDurationMs = (run) =>
durationMs(run.run_started_at ?? run.created_at, run.updated_at);
const findJob = (payload, name) =>
payload.jobs.find((candidate) => candidate.name === name) ?? null;
const findStep = (job, name) =>
job?.steps?.find((candidate) => candidate.name === name) ?? null;
const jobDurationMs = (job) =>
job ? durationMs(job.started_at, job.completed_at) : null;
const stepDurationMs = (step) =>
step ? durationMs(step.started_at, step.completed_at) : null;
const resultOf = (candidate) =>
candidate?.conclusion ?? candidate?.status ?? "unknown";
const escapeCell = (value) => String(value ?? "—").replaceAll("|", "\\|");
const runLink = (url) => (url ? `[Run](${url})` : "—");
function componentRow({ target, mode, candidate, duration, detail, url }) {
return {
target,
mode,
result: resultOf(candidate),
durationMs: duration,
detail,
url,
};
}
function nativeRows(payload, platform, requestedMode) {
if (platform === "desktop") {
const architectureJobs = ["macOS arm64", "macOS x64"]
.map((name) => findJob(payload, name))
.filter(Boolean);
if (architectureJobs.length > 0) {
return architectureJobs.map((job) => {
const packageStep = findStep(job, "Package desktop installer");
return componentRow({
target: job.name,
mode: "rebuild",
candidate: job,
duration: jobDurationMs(job),
detail: `package + notarize ${formatDuration(stepDurationMs(packageStep))}`,
url: job.html_url ?? payload.run.html_url,
});
});
}
const reuseJob = findJob(payload, "Reuse macOS release assets");
if (reuseJob) {
return [
componentRow({
target: "macOS arm64 + x64",
mode: "reuse",
candidate: reuseJob,
duration: jobDurationMs(reuseJob),
detail: "copied previously verified assets",
url: reuseJob.html_url ?? payload.run.html_url,
}),
];
}
const planJob = findJob(payload, "Plan desktop release asset");
return [
componentRow({
target: "macOS arm64 + x64",
mode: "already prepared",
candidate: planJob ?? payload.run,
duration: jobDurationMs(planJob) ?? runDurationMs(payload.run),
detail: `requested ${requestedMode}; Draft assets were already ready`,
url: planJob?.html_url ?? payload.run.html_url,
}),
];
}
const buildJob = findJob(payload, "Build Android packages");
if (buildJob) {
const packageStep = findStep(
buildJob,
"Build signed release APK for GitHub Release",
);
return [
componentRow({
target: "Android arm64",
mode: "rebuild",
candidate: buildJob,
duration: jobDurationMs(buildJob),
detail: `APK build ${formatDuration(stepDurationMs(packageStep))}`,
url: buildJob.html_url ?? payload.run.html_url,
}),
];
}
const reuseJob = findJob(payload, "Reuse Android release APK");
if (reuseJob) {
return [
componentRow({
target: "Android arm64",
mode: "reuse",
candidate: reuseJob,
duration: jobDurationMs(reuseJob),
detail: "copied previously verified Play-signed APK",
url: reuseJob.html_url ?? payload.run.html_url,
}),
];
}
const planJob = findJob(payload, "Plan Android release asset");
return [
componentRow({
target: "Android arm64",
mode: "already prepared",
candidate: planJob ?? payload.run,
duration: jobDurationMs(planJob) ?? runDurationMs(payload.run),
detail: `requested ${requestedMode}; Draft APK was already ready`,
url: planJob?.html_url ?? payload.run.html_url,
}),
];
}
export function createReleaseTimingReport({
release,
desktop,
desktopMode,
mobile,
mobileMode,
docker,
cloudflare,
tcrSource,
tcrReadyAt,
tcrStatus,
}) {
const cloudflareJob = findJob(cloudflare, "Build and deploy Demo Worker");
const cloudflareStep = findStep(cloudflareJob, "Deploy Demo Worker");
const dockerJob = findJob(docker, "Publish official multi-platform image");
const dockerStep = findStep(dockerJob, "Build and publish image");
const tcrSourceJob = findJob(
tcrSource,
"Trigger asynchronous Tencent-side image build",
);
const tcrStart = tcrSource.run.run_started_at ?? tcrSource.run.created_at;
const tcrDuration = durationMs(tcrStart, tcrReadyAt);
const rows = [
componentRow({
target: "Cloudflare Demo Worker",
mode: "deploy",
candidate: cloudflareJob ?? cloudflare.run,
duration: jobDurationMs(cloudflareJob) ?? runDurationMs(cloudflare.run),
detail: `deploy step ${formatDuration(stepDurationMs(cloudflareStep))}`,
url: cloudflareJob?.html_url ?? cloudflare.run.html_url,
}),
componentRow({
target: "Docker GHCR (amd64 + arm64)",
mode: "build + push",
candidate: dockerJob ?? docker.run,
duration: jobDurationMs(dockerJob) ?? runDurationMs(docker.run),
detail: `image build/push ${formatDuration(stepDurationMs(dockerStep))}`,
url: dockerJob?.html_url ?? docker.run.html_url,
}),
{
target: "Docker TCR (amd64 + arm64)",
mode: "Tencent-side build",
result: tcrStatus,
durationMs: tcrDuration,
detail: `release source sync ${formatDuration(jobDurationMs(tcrSourceJob) ?? runDurationMs(tcrSource.run))}; duration is end-to-end until the public TCR tag was observed`,
url: tcrSourceJob?.html_url ?? tcrSource.run.html_url,
},
...nativeRows(desktop, "desktop", desktopMode),
...nativeRows(mobile, "android", mobileMode),
];
const publishedAt = timestamp(release.publishedAt);
const endpointReadyTimes = [cloudflare.run.updated_at, tcrReadyAt]
.map(timestamp)
.filter((value) => value !== null);
const allEndpointsReady =
resultOf(cloudflareJob ?? cloudflare.run) === "success" &&
tcrStatus === "success" &&
endpointReadyTimes.length === 2;
const allEndpointsReadyAt = allEndpointsReady
? Math.max(...endpointReadyTimes)
: null;
return {
schemaVersion: 1,
release,
generatedAt: new Date().toISOString(),
allEndpointsReady,
allEndpointsReadyDurationMs:
publishedAt !== null &&
allEndpointsReadyAt !== null &&
allEndpointsReadyAt >= publishedAt
? allEndpointsReadyAt - publishedAt
: null,
rows,
};
}
export function renderReleaseTimingMarkdown(report) {
const readinessSummary = report.allEndpointsReady
? `All post-publication endpoints were observed after **${formatDuration(report.allEndpointsReadyDurationMs)}**.`
: "At least one post-publication endpoint failed or was not observed before the reporting deadline.";
const lines = [
`## Release build timings · ${report.release.tag}`,
"",
`${readinessSummary} Client and GHCR assets are prepared in the Draft phase and therefore report their actual preparation jobs rather than time after publication.`,
"",
"| Target | Mode | Result | Duration | Detail | Workflow |",
"| --- | --- | --- | ---: | --- | --- |",
];
for (const row of report.rows) {
lines.push(
`| ${escapeCell(row.target)} | ${escapeCell(row.mode)} | ${escapeCell(row.result)} | ${formatDuration(row.durationMs)} | ${escapeCell(row.detail)} | ${runLink(row.url)} |`,
);
}
lines.push(
"",
"> Durations use GitHub Actions job/step timestamps. TCR is measured end-to-end from the release-triggered source sync until the matching public multi-architecture tag and Git revision become visible.",
"",
);
return lines.join("\n");
}
function parseArguments(argv) {
const options = {};
for (let index = 0; index < argv.length; index += 2) {
const key = argv[index];
const value = argv[index + 1];
if (!key?.startsWith("--") || value === undefined) {
throw new Error(`Invalid argument near ${key ?? "end"}`);
}
options[key.slice(2)] = value;
}
for (const required of [
"tag",
"sha",
"published-at",
"desktop",
"desktop-mode",
"mobile",
"mobile-mode",
"docker",
"cloudflare",
"tcr-source",
"tcr-status",
"json",
"markdown",
]) {
if (!options[required]) throw new Error(`Missing --${required}`);
}
return options;
}
if (import.meta.main) {
const options = parseArguments(process.argv.slice(2));
const report = createReleaseTimingReport({
release: {
tag: options.tag,
sha: options.sha,
publishedAt: options["published-at"],
url: options["release-url"] ?? null,
},
desktop: readJson(options.desktop),
desktopMode: options["desktop-mode"],
mobile: readJson(options.mobile),
mobileMode: options["mobile-mode"],
docker: readJson(options.docker),
cloudflare: readJson(options.cloudflare),
tcrSource: readJson(options["tcr-source"]),
tcrReadyAt: options["tcr-ready-at"] || null,
tcrStatus: options["tcr-status"],
});
writeFileSync(options.json, `${JSON.stringify(report, null, 2)}\n`);
writeFileSync(options.markdown, renderReleaseTimingMarkdown(report));
}
+165
View File
@@ -0,0 +1,165 @@
import { describe, expect, test } from "bun:test";
import {
createReleaseTimingReport,
renderReleaseTimingMarkdown,
} from "./report-release-timings.mjs";
const step = (name, start, seconds) => ({
name,
conclusion: "success",
started_at: start,
completed_at: new Date(Date.parse(start) + seconds * 1000).toISOString(),
});
const job = (name, start, seconds, steps = []) => ({
name,
conclusion: "success",
started_at: start,
completed_at: new Date(Date.parse(start) + seconds * 1000).toISOString(),
html_url: `https://example.test/jobs/${encodeURIComponent(name)}`,
steps,
});
const payload = (name, start, seconds, jobs) => ({
run: {
name,
conclusion: "success",
created_at: start,
run_started_at: start,
updated_at: new Date(Date.parse(start) + seconds * 1000).toISOString(),
html_url: `https://example.test/runs/${encodeURIComponent(name)}`,
},
jobs,
});
describe("release timing report", () => {
test("combines Cloudflare, both registries, and rebuilt clients", () => {
const start = "2026-08-25T00:00:00Z";
const report = createReleaseTimingReport({
release: { tag: "v1.41.0", sha: "abc", publishedAt: start },
desktop: payload("Desktop", start, 600, [
job("macOS arm64", start, 480, [
step("Package desktop installer", start, 180),
]),
job("macOS x64", start, 600, [
step("Package desktop installer", start, 300),
]),
]),
desktopMode: "rebuild",
mobile: payload("Mobile", start, 540, [
job("Build Android packages", start, 540, [
step("Build signed release APK for GitHub Release", start, 420),
]),
]),
mobileMode: "rebuild",
docker: payload("Docker", start, 300, [
job("Publish official multi-platform image", start, 300, [
step("Build and publish image", start, 240),
]),
]),
cloudflare: payload("Cloudflare", start, 120, [
job("Build and deploy Demo Worker", start, 120, [
step("Deploy Demo Worker", start, 60),
]),
]),
tcrSource: payload("TCR", start, 20, [
job("Trigger asynchronous Tencent-side image build", start, 20),
]),
tcrReadyAt: "2026-08-25T00:10:00Z",
tcrStatus: "success",
});
expect(report.allEndpointsReady).toBe(true);
expect(report.allEndpointsReadyDurationMs).toBe(600_000);
expect(report.rows.map((row) => row.target)).toEqual([
"Cloudflare Demo Worker",
"Docker GHCR (amd64 + arm64)",
"Docker TCR (amd64 + arm64)",
"macOS arm64",
"macOS x64",
"Android arm64",
]);
const markdown = renderReleaseTimingMarkdown(report);
expect(markdown).toContain(
"All post-publication endpoints were observed after **10m 00s**",
);
expect(markdown).toContain(
"| Docker TCR (amd64 + arm64) | Tencent-side build | success | 10m 00s |",
);
expect(markdown).toContain(
"| macOS x64 | rebuild | success | 10m 00s | package + notarize 5m 00s |",
);
});
test("reports reused native assets without pretending they were rebuilt", () => {
const start = "2026-08-25T00:00:00Z";
const common = {
release: { tag: "v1.41.1", sha: "def", publishedAt: start },
docker: payload("Docker", start, 60, [
job("Publish official multi-platform image", start, 60),
]),
cloudflare: payload("Cloudflare", start, 60, [
job("Build and deploy Demo Worker", start, 60),
]),
tcrSource: payload("TCR", start, 15, [
job("Trigger asynchronous Tencent-side image build", start, 15),
]),
tcrReadyAt: "2026-08-25T00:02:00Z",
tcrStatus: "success",
};
const report = createReleaseTimingReport({
...common,
desktop: payload("Desktop", start, 40, [
job("Reuse macOS release assets", start, 40),
]),
desktopMode: "reuse",
mobile: payload("Mobile", start, 30, [
job("Reuse Android release APK", start, 30),
]),
mobileMode: "reuse",
});
expect(report.rows.at(-2)).toMatchObject({
target: "macOS arm64 + x64",
mode: "reuse",
durationMs: 40_000,
});
expect(report.rows.at(-1)).toMatchObject({
target: "Android arm64",
mode: "reuse",
durationMs: 30_000,
});
});
test("does not claim all endpoints are ready when TCR times out", () => {
const start = "2026-08-25T00:00:00Z";
const report = createReleaseTimingReport({
release: { tag: "v1.41.2", sha: "ghi", publishedAt: start },
desktop: payload("Desktop", start, 40, [
job("Reuse macOS release assets", start, 40),
]),
desktopMode: "reuse",
mobile: payload("Mobile", start, 30, [
job("Reuse Android release APK", start, 30),
]),
mobileMode: "reuse",
docker: payload("Docker", start, 60, [
job("Publish official multi-platform image", start, 60),
]),
cloudflare: payload("Cloudflare", start, 60, [
job("Build and deploy Demo Worker", start, 60),
]),
tcrSource: payload("TCR", start, 15, [
job("Trigger asynchronous Tencent-side image build", start, 15),
]),
tcrReadyAt: null,
tcrStatus: "timeout",
});
expect(report.allEndpointsReady).toBe(false);
expect(report.allEndpointsReadyDurationMs).toBeNull();
expect(renderReleaseTimingMarkdown(report)).toContain(
"At least one post-publication endpoint failed or was not observed",
);
});
});