mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(release): resolve beta smoke workflow run
This commit is contained in:
@@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Release/beta smoke: resolve the dispatched Telegram beta E2E run from `gh run list` when `gh workflow run` returns no run URL, so the maintainer helper does not fail immediately after dispatch. Thanks @vincentkoc.
|
||||
- Media/images: keep HEIC/HEIF attachments fail-closed when optional Sharp conversion is unavailable instead of sending originals that still need conversion. Thanks @vincentkoc.
|
||||
- Telegram/streaming: sanitize tool-progress draft preview backticks before shared compaction, so long backtick-heavy progress text still renders inside the safe code-formatted preview instead of collapsing to an ellipsis.
|
||||
- UI/chat: remove the unsupported `line-clamp` declaration from the chat queue text rule to eliminate Firefox console noise without changing visible truncation behavior. Thanks @ZanderH-code.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
interface Options {
|
||||
beta: string;
|
||||
@@ -101,6 +102,8 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
const TELEGRAM_BETA_WORKFLOW_FILE = "npm-telegram-beta-e2e.yml";
|
||||
|
||||
function resolveBetaVersion(beta: string): string {
|
||||
const value = beta.trim().replace(/^openclaw@/, "");
|
||||
if (/^\d{4}\.\d+\.\d+-beta\.\d+$/u.test(value)) {
|
||||
@@ -160,13 +163,92 @@ function ghJson(repo: string, pathSuffix: string): unknown {
|
||||
return JSON.parse(run("gh", ["api", `repos/${repo}/${pathSuffix}`], { capture: true }));
|
||||
}
|
||||
|
||||
function dispatchTelegram(options: Options, packageSpec: string): string {
|
||||
export function parseWorkflowRunIdFromOutput(output: string): string | undefined {
|
||||
return /\/actions\/runs\/(\d+)/u.exec(output)?.[1];
|
||||
}
|
||||
|
||||
type WorkflowRunListEntry = {
|
||||
createdAt?: string;
|
||||
databaseId?: number | string;
|
||||
};
|
||||
|
||||
function normalizeRunId(value: unknown): string | undefined {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function selectNewestDispatchedRunId(params: {
|
||||
beforeIds: ReadonlySet<string>;
|
||||
runs: readonly WorkflowRunListEntry[];
|
||||
}): string | undefined {
|
||||
return params.runs
|
||||
.filter((entry) => {
|
||||
const id = normalizeRunId(entry.databaseId);
|
||||
return id !== undefined && !params.beforeIds.has(id);
|
||||
})
|
||||
.toSorted((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""))
|
||||
.map((entry) => normalizeRunId(entry.databaseId))
|
||||
.find((id): id is string => id !== undefined);
|
||||
}
|
||||
|
||||
function listWorkflowDispatchRuns(repo: string, workflow: string): WorkflowRunListEntry[] {
|
||||
return JSON.parse(
|
||||
run(
|
||||
"gh",
|
||||
[
|
||||
"run",
|
||||
"list",
|
||||
"--repo",
|
||||
repo,
|
||||
"--workflow",
|
||||
workflow,
|
||||
"--event",
|
||||
"workflow_dispatch",
|
||||
"--limit",
|
||||
"50",
|
||||
"--json",
|
||||
"databaseId,createdAt",
|
||||
],
|
||||
{ capture: true },
|
||||
),
|
||||
) as WorkflowRunListEntry[];
|
||||
}
|
||||
|
||||
async function findDispatchedWorkflowRunId(params: {
|
||||
beforeIds: ReadonlySet<string>;
|
||||
repo: string;
|
||||
workflow: string;
|
||||
}): Promise<string> {
|
||||
for (let attempt = 0; attempt < 60; attempt++) {
|
||||
const runId = selectNewestDispatchedRunId({
|
||||
beforeIds: params.beforeIds,
|
||||
runs: listWorkflowDispatchRuns(params.repo, params.workflow),
|
||||
});
|
||||
if (runId) {
|
||||
return runId;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
||||
}
|
||||
throw new Error(`could not find dispatched run for ${params.workflow}`);
|
||||
}
|
||||
|
||||
async function dispatchTelegram(options: Options, packageSpec: string): Promise<string> {
|
||||
const beforeIds = new Set(
|
||||
listWorkflowDispatchRuns(options.repo, TELEGRAM_BETA_WORKFLOW_FILE)
|
||||
.map((entry) => normalizeRunId(entry.databaseId))
|
||||
.filter((id): id is string => id !== undefined),
|
||||
);
|
||||
const output = run(
|
||||
"gh",
|
||||
[
|
||||
"workflow",
|
||||
"run",
|
||||
"NPM Telegram Beta E2E",
|
||||
TELEGRAM_BETA_WORKFLOW_FILE,
|
||||
"--repo",
|
||||
options.repo,
|
||||
"--ref",
|
||||
@@ -180,11 +262,15 @@ function dispatchTelegram(options: Options, packageSpec: string): string {
|
||||
],
|
||||
{ capture: true },
|
||||
);
|
||||
const runId = /\/actions\/runs\/(\d+)/u.exec(output)?.[1];
|
||||
if (!runId) {
|
||||
throw new Error(`could not parse workflow run id from gh output:\n${output}`);
|
||||
const runId = parseWorkflowRunIdFromOutput(output);
|
||||
if (runId) {
|
||||
return runId;
|
||||
}
|
||||
return runId;
|
||||
return await findDispatchedWorkflowRunId({
|
||||
beforeIds,
|
||||
repo: options.repo,
|
||||
workflow: TELEGRAM_BETA_WORKFLOW_FILE,
|
||||
});
|
||||
}
|
||||
|
||||
async function pollRun(repo: string, runId: string): Promise<void> {
|
||||
@@ -266,7 +352,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (!options.skipTelegram) {
|
||||
const runId = dispatchTelegram(options, packageSpec);
|
||||
const runId = await dispatchTelegram(options, packageSpec);
|
||||
await pollRun(options.repo, runId);
|
||||
const artifactDir = downloadTelegramArtifact(options.repo, runId);
|
||||
const report = findFile(artifactDir, "telegram-qa-report.md");
|
||||
@@ -277,7 +363,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
await main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
30
test/scripts/release-beta-smoke.test.ts
Normal file
30
test/scripts/release-beta-smoke.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseWorkflowRunIdFromOutput,
|
||||
selectNewestDispatchedRunId,
|
||||
} from "../../scripts/release-beta-smoke.ts";
|
||||
|
||||
describe("release-beta-smoke", () => {
|
||||
it("parses workflow run urls when gh includes them in dispatch output", () => {
|
||||
expect(
|
||||
parseWorkflowRunIdFromOutput(
|
||||
"Dispatched: https://github.com/openclaw/openclaw/actions/runs/1234567890",
|
||||
),
|
||||
).toBe("1234567890");
|
||||
});
|
||||
|
||||
it("selects the newest workflow_dispatch run not present before dispatch", () => {
|
||||
const beforeIds = new Set(["100", "101"]);
|
||||
|
||||
expect(
|
||||
selectNewestDispatchedRunId({
|
||||
beforeIds,
|
||||
runs: [
|
||||
{ databaseId: 100, createdAt: "2026-05-04T10:00:00Z" },
|
||||
{ databaseId: 102, createdAt: "2026-05-04T10:01:00Z" },
|
||||
{ databaseId: 103, createdAt: "2026-05-04T10:02:00Z" },
|
||||
],
|
||||
}),
|
||||
).toBe("103");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user