diff --git a/.github/workflows/custom_build.yml b/.github/workflows/custom_build.yml index 8fcf420..ff96028 100644 --- a/.github/workflows/custom_build.yml +++ b/.github/workflows/custom_build.yml @@ -35,6 +35,7 @@ jobs: CONVEX_URL: ${{ inputs.convex_url }} REPO_BUILD_ID: ${{ inputs.repo_build_id }} CONVEX_BUILD_TOKEN: ${{ secrets.CONVEX_BUILD_TOKEN }} + CI_PROGRESS_TOTAL: "5" steps: - uses: actions/checkout@v4 @@ -46,6 +47,13 @@ jobs: -H "Authorization: Bearer $CONVEX_BUILD_TOKEN" \ -d "{\"repo_build_id\":\"$REPO_BUILD_ID\",\"state\":\"running\",\"github_run_id\":${{ github.run_id }}}" + - name: CI progress — build started + shell: bash + env: + STEP_INDEX: 1 + LABEL: Build started on Actions + run: python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" + - name: Set up Python uses: actions/setup-python@v5 with: @@ -56,6 +64,13 @@ jobs: with: bun-version: latest + - name: CI progress — tooling ready + shell: bash + env: + STEP_INDEX: 2 + LABEL: Runner tooling ready + run: python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" + - name: Download source archive shell: bash env: @@ -63,6 +78,7 @@ jobs: REPO: ${{ inputs.repo }} REF: ${{ inputs.ref }} run: | + STEP_INDEX=3 LABEL="Downloading source archive" python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" ENC_REF=$(python3 -c "import urllib.parse,os; print(urllib.parse.quote(os.environ['REF'], safe=''))") curl -fsSL -o /tmp/src.zip "https://codeload.github.com/${OWNER}/${REPO}/zip/${ENC_REF}" @@ -74,6 +90,7 @@ jobs: R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} shell: bash run: | + STEP_INDEX=4 LABEL="Building firmware (PlatformIO)" python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" set -e sudo apt-get update -qq && sudo apt-get install -y -qq unzip unzip -q /tmp/src.zip -d /tmp/src @@ -107,6 +124,7 @@ jobs: tar -czf "/tmp/$ARTIFACT_NAME" -C "$STAGE" . OBJECT_PATH="${R2_BUCKET_NAME}/${ARTIFACT_NAME}" bunx wrangler r2 object put "$OBJECT_PATH" --file "/tmp/$ARTIFACT_NAME" --remote + STEP_INDEX=5 LABEL="Firmware bundle uploaded" python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" echo "r2_key=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT" - name: Notify Convex — success diff --git a/.github/workflows/custom_build_test.yml b/.github/workflows/custom_build_test.yml index 8fcf420..ff96028 100644 --- a/.github/workflows/custom_build_test.yml +++ b/.github/workflows/custom_build_test.yml @@ -35,6 +35,7 @@ jobs: CONVEX_URL: ${{ inputs.convex_url }} REPO_BUILD_ID: ${{ inputs.repo_build_id }} CONVEX_BUILD_TOKEN: ${{ secrets.CONVEX_BUILD_TOKEN }} + CI_PROGRESS_TOTAL: "5" steps: - uses: actions/checkout@v4 @@ -46,6 +47,13 @@ jobs: -H "Authorization: Bearer $CONVEX_BUILD_TOKEN" \ -d "{\"repo_build_id\":\"$REPO_BUILD_ID\",\"state\":\"running\",\"github_run_id\":${{ github.run_id }}}" + - name: CI progress — build started + shell: bash + env: + STEP_INDEX: 1 + LABEL: Build started on Actions + run: python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" + - name: Set up Python uses: actions/setup-python@v5 with: @@ -56,6 +64,13 @@ jobs: with: bun-version: latest + - name: CI progress — tooling ready + shell: bash + env: + STEP_INDEX: 2 + LABEL: Runner tooling ready + run: python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" + - name: Download source archive shell: bash env: @@ -63,6 +78,7 @@ jobs: REPO: ${{ inputs.repo }} REF: ${{ inputs.ref }} run: | + STEP_INDEX=3 LABEL="Downloading source archive" python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" ENC_REF=$(python3 -c "import urllib.parse,os; print(urllib.parse.quote(os.environ['REF'], safe=''))") curl -fsSL -o /tmp/src.zip "https://codeload.github.com/${OWNER}/${REPO}/zip/${ENC_REF}" @@ -74,6 +90,7 @@ jobs: R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} shell: bash run: | + STEP_INDEX=4 LABEL="Building firmware (PlatformIO)" python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" set -e sudo apt-get update -qq && sudo apt-get install -y -qq unzip unzip -q /tmp/src.zip -d /tmp/src @@ -107,6 +124,7 @@ jobs: tar -czf "/tmp/$ARTIFACT_NAME" -C "$STAGE" . OBJECT_PATH="${R2_BUCKET_NAME}/${ARTIFACT_NAME}" bunx wrangler r2 object put "$OBJECT_PATH" --file "/tmp/$ARTIFACT_NAME" --remote + STEP_INDEX=5 LABEL="Firmware bundle uploaded" python3 "${{ github.workspace }}/scripts/report-convex-ci-progress.py" echo "r2_key=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT" - name: Notify Convex — success diff --git a/convex/http.ts b/convex/http.ts index 41be2a1..6371c78 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -56,6 +56,40 @@ http.route({ }), }) +http.route({ + path: "/ingest-repo-build-progress", + method: "POST", + handler: httpAction(async (ctx, request) => { + if (!verifyBearer(request)) { + return new Response("Unauthorized", { status: 401 }) + } + const payload = (await request.json()) as { + repo_build_id?: string + step_index?: number + step_total?: number + label?: string + } + + if ( + !payload.repo_build_id || + typeof payload.step_index !== "number" || + typeof payload.step_total !== "number" || + typeof payload.label !== "string" + ) { + return new Response("Missing repo_build_id, step_index, step_total, or label", { status: 400 }) + } + + await ctx.runMutation(internal.repoBuilds.patchCiProgress, { + buildId: payload.repo_build_id as Id<"repoBuilds">, + stepIndex: payload.step_index, + stepTotal: payload.step_total, + label: payload.label, + }) + + return new Response(null, { status: 200 }) + }), +}) + /** Legacy: old workflows posting build_id + state */ http.route({ path: "/github-webhook", diff --git a/convex/repoBuilds.ts b/convex/repoBuilds.ts index 9a51600..0fef469 100644 --- a/convex/repoBuilds.ts +++ b/convex/repoBuilds.ts @@ -91,11 +91,41 @@ export const patchFromWebhook = internalMutation({ if (args.errorSummary !== undefined) patch.errorSummary = args.errorSummary if (args.status === "succeeded" || args.status === "failed") { patch.completedAt = Date.now() + patch.ciProgressStep = undefined + patch.ciProgressTotal = undefined + patch.ciProgressLabel = undefined } await ctx.db.patch(args.buildId, patch) }, }) +export const patchCiProgress = internalMutation({ + args: { + buildId: v.id("repoBuilds"), + stepIndex: v.number(), + stepTotal: v.number(), + label: v.string(), + }, + handler: async (ctx, args) => { + const doc = await ctx.db.get(args.buildId) + if (!doc) { + return + } + if (doc.status === "succeeded" || doc.status === "failed") { + return + } + if (args.stepTotal < 1 || args.stepIndex < 1 || args.stepIndex > args.stepTotal) { + return + } + await ctx.db.patch(args.buildId, { + ciProgressStep: args.stepIndex, + ciProgressTotal: args.stepTotal, + ciProgressLabel: args.label, + updatedAt: Date.now(), + }) + }, +}) + export const logBuildDispatchError = internalMutation({ args: { buildId: v.id("repoBuilds"), message: v.string() }, handler: async (ctx, args) => { @@ -104,6 +134,9 @@ export const logBuildDispatchError = internalMutation({ errorSummary: args.message, updatedAt: Date.now(), completedAt: Date.now(), + ciProgressStep: undefined, + ciProgressTotal: undefined, + ciProgressLabel: undefined, }) }, }) diff --git a/convex/schema.ts b/convex/schema.ts index 2298742..fa42500 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -49,6 +49,10 @@ export const repoBuildsFields = { githubRunId: v.optional(v.number()), r2ObjectKey: v.optional(v.string()), errorSummary: v.optional(v.string()), + /** CI workflow progress (1-based step / total + label), pushed via /ingest-repo-build-progress. */ + ciProgressStep: v.optional(v.number()), + ciProgressTotal: v.optional(v.number()), + ciProgressLabel: v.optional(v.string()), } export const deviceReportFields = { diff --git a/scripts/report-convex-ci-progress.py b/scripts/report-convex-ci-progress.py new file mode 100644 index 0000000..68830d4 --- /dev/null +++ b/scripts/report-convex-ci-progress.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""POST CI step progress to Convex (used by custom_build*.yml). Env: REPO_BUILD_ID, CONVEX_URL, CONVEX_BUILD_TOKEN, STEP_INDEX, CI_PROGRESS_TOTAL, LABEL.""" +from __future__ import annotations + +import json +import os +import sys +import urllib.error +import urllib.request + + +def main() -> None: + try: + repo_build_id = os.environ["REPO_BUILD_ID"] + convex_url = os.environ["CONVEX_URL"].rstrip("/") + token = os.environ["CONVEX_BUILD_TOKEN"] + step_index = int(os.environ["STEP_INDEX"]) + step_total = int(os.environ["CI_PROGRESS_TOTAL"]) + label = os.environ["LABEL"] + except KeyError as e: + print(f"missing env: {e}", file=sys.stderr) + sys.exit(1) + + if step_total < 1 or step_index < 1 or step_index > step_total: + print("invalid STEP_INDEX / CI_PROGRESS_TOTAL", file=sys.stderr) + sys.exit(1) + + body = { + "repo_build_id": repo_build_id, + "step_index": step_index, + "step_total": step_total, + "label": label, + } + url = f"{convex_url}/ingest-repo-build-progress" + req = urllib.request.Request( + url, + data=json.dumps(body).encode(), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + method="POST", + ) + try: + urllib.request.urlopen(req) + except urllib.error.HTTPError as e: + print(e.read().decode() or str(e), file=sys.stderr) + raise SystemExit(1) from e + + +if __name__ == "__main__": + main() diff --git a/src/index.css b/src/index.css index 1674fb7..369ebca 100644 --- a/src/index.css +++ b/src/index.css @@ -96,3 +96,17 @@ padding: 1rem; overflow-x: auto; } + +@keyframes ci-progress-shimmer-x { + 0% { + left: -45%; + } + 100% { + left: 105%; + } +} + +.ci-progress-shimmer-x { + position: absolute; + animation: ci-progress-shimmer-x 1.45s ease-in-out infinite; +} diff --git a/src/pages/RepoPage.tsx b/src/pages/RepoPage.tsx index 492ec9d..77caab9 100644 --- a/src/pages/RepoPage.tsx +++ b/src/pages/RepoPage.tsx @@ -425,30 +425,77 @@ export default function RepoPage() {
{showCiCard ? (
-
- CI - {build.status} - {build.githubRunId ? ( - - View run on GitHub - - ) : build.status === "failed" ? ( - - Mesh Forge workflow on GitHub - - ) : null} -
+ {build.githubRunId || build.status === "failed" ? ( +
+ {build.githubRunId ? ( + + View run on GitHub + + ) : ( + + Mesh Forge workflow on GitHub + + )} +
+ ) : null} + {!isFlashView && buildInProgress && !build.githubRunId ? ( +

Waiting for workflow to start…

+ ) : null} + {isFlashView && buildInProgress ? ( +
+ {(() => { + const step = build.ciProgressStep + const total = build.ciProgressTotal + const label = build.ciProgressLabel + const hasSteps = + typeof step === "number" && + typeof total === "number" && + total > 0 && + step >= 1 && + step <= total + const pct = hasSteps ? Math.min(100, Math.round((step / total) * 100)) : null + return ( + <> +
+ {pct !== null ? ( +
+ ) : ( +
+ )} +
+

+ {hasSteps ? ( + <> + Step {step} of {total} + {label ? ` · ${label}` : ""} + + ) : ( + <>Waiting for CI progress… + )} +

+ + ) + })()} +
+ ) : null} {build.status === "failed" && build.errorSummary ? (
{(() => {