mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-08-03 07:23:30 +02:00
CI builder progress view enhancements
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+71
-24
@@ -425,30 +425,77 @@ export default function RepoPage() {
|
||||
<div className="max-w-2xl space-y-4">
|
||||
{showCiCard ? (
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900/40 p-4 space-y-2 text-sm">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-slate-500">CI</span>
|
||||
<span className="text-white font-medium">{build.status}</span>
|
||||
{build.githubRunId ? (
|
||||
<a
|
||||
className="text-cyan-400 hover:underline text-xs"
|
||||
href={`https://github.com/${MESH_FORGE_ACTIONS_REPO}/actions/runs/${build.githubRunId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View run on GitHub
|
||||
</a>
|
||||
) : build.status === "failed" ? (
|
||||
<a
|
||||
className="text-cyan-400 hover:underline text-xs"
|
||||
href={meshForgeWorkflowUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="No run ID — usually the workflow never started (e.g. dispatch rejected). Open the Mesh Forge workflow to fix YAML or inspect recent runs."
|
||||
>
|
||||
Mesh Forge workflow on GitHub
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{build.githubRunId || build.status === "failed" ? (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{build.githubRunId ? (
|
||||
<a
|
||||
className="text-cyan-400 hover:underline text-xs"
|
||||
href={`https://github.com/${MESH_FORGE_ACTIONS_REPO}/actions/runs/${build.githubRunId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View run on GitHub
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
className="text-cyan-400 hover:underline text-xs"
|
||||
href={meshForgeWorkflowUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="No run ID — usually the workflow never started (e.g. dispatch rejected). Open the Mesh Forge workflow to fix YAML or inspect recent runs."
|
||||
>
|
||||
Mesh Forge workflow on GitHub
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{!isFlashView && buildInProgress && !build.githubRunId ? (
|
||||
<p className="text-xs text-slate-400">Waiting for workflow to start…</p>
|
||||
) : null}
|
||||
{isFlashView && buildInProgress ? (
|
||||
<div className="space-y-2 pt-1">
|
||||
{(() => {
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
className={`relative h-2.5 w-full overflow-hidden rounded-full bg-slate-800/90 ${
|
||||
pct === null ? "animate-pulse" : ""
|
||||
}`}
|
||||
>
|
||||
{pct !== null ? (
|
||||
<div
|
||||
className="h-full rounded-full bg-linear-to-r from-cyan-600 to-emerald-500 transition-[width] duration-500 ease-out shadow-[0_0_12px_rgba(34,211,238,0.35)]"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="ci-progress-shimmer-x top-0 h-full w-[42%] rounded-full bg-linear-to-r from-cyan-700/85 via-emerald-400/95 to-cyan-700/85 shadow-[0_0_10px_rgba(52,211,153,0.4)]" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">
|
||||
{hasSteps ? (
|
||||
<>
|
||||
Step {step} of {total}
|
||||
{label ? ` · ${label}` : ""}
|
||||
</>
|
||||
) : (
|
||||
<>Waiting for CI progress…</>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
) : null}
|
||||
{build.status === "failed" && build.errorSummary ? (
|
||||
<div className="space-y-2 text-xs">
|
||||
{(() => {
|
||||
|
||||
Reference in New Issue
Block a user