Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .taskcluster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,24 @@ tasks:
name: bugbug ui build
description: bugbug ui build

- $mergeDeep:
- { $eval: default_task_definition }
- taskId: { $eval: as_slugid("hackbot_ui_tests_task") }
payload:
image: node:lts
command:
- "/bin/sh"
- "-cxe"
- "git clone --quiet ${repository} /bugbug &&
cd /bugbug &&
git -c advice.detachedHead=false checkout ${head_rev} &&
cd services/hackbot-ui &&
npm ci --no-progress &&
npm test"
metadata:
name: bugbug hackbot-ui tests
description: bugbug hackbot-ui tests

- $if: 'tasks_for == "github-push" && head_branch[:10] == "refs/tags/"'
then:
$mergeDeep:
Expand All @@ -273,6 +291,7 @@ tasks:
- { $eval: as_slugid("http_tests_task") }
- { $eval: as_slugid("libs_tests_task") }
- { $eval: as_slugid("frontend_build") }
- { $eval: as_slugid("hackbot_ui_tests_task") }
- { $eval: as_slugid("packaging_test_task") }
- { $eval: as_slugid("version_check_task") }
- { $eval: as_slugid("integration_test") }
Expand Down Expand Up @@ -305,6 +324,7 @@ tasks:
- { $eval: as_slugid("http_tests_task") }
- { $eval: as_slugid("libs_tests_task") }
- { $eval: as_slugid("frontend_build") }
- { $eval: as_slugid("hackbot_ui_tests_task") }
- { $eval: as_slugid("packaging_test_task") }
scopes:
- secrets:get:project/bugbug/deploy
Expand Down
23 changes: 12 additions & 11 deletions services/hackbot-ui/components/TriggerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";

import { AGENTS, type AgentValue } from "@/lib/agents";
import { parseBugId } from "@/lib/bugzilla";
import { saveRun } from "@/lib/store";
import type { RunRef } from "@/lib/types";

Expand Down Expand Up @@ -55,8 +56,8 @@ export function TriggerForm() {

const inputs: Record<string, unknown> = {};

const parsedBugId = bugId.trim() ? Number.parseInt(bugId, 10) : Number.NaN;
const hasBugId = Number.isInteger(parsedBugId) && parsedBugId > 0;
const parsedBugId = parseBugId(bugId);
const hasBugId = parsedBugId !== null;
const hasBugData = isReproAgent && bugData.trim().length > 0;

if (isBuildRepairAgent) {
Expand Down Expand Up @@ -109,13 +110,15 @@ export function TriggerForm() {
inputs.test_scope = testScope.trim();
} else if (!isReproAgent) {
if (!hasBugId) {
setError("Enter a valid Bugzilla bug ID.");
setError("Enter a valid Bugzilla bug ID or bug URL.");
return;
}
inputs.bug_id = parsedBugId;
} else {
if (!hasBugId && !hasBugData) {
setError("Provide a Bugzilla bug ID or paste report text.");
setError(
"Provide a Bugzilla bug ID (or bug URL) or paste report text."
);
return;
}
if (hasBugId) inputs.bug_id = parsedBugId;
Expand Down Expand Up @@ -188,13 +191,12 @@ export function TriggerForm() {
<div className="field">
<label htmlFor="bugId">
{isReproAgent
? "Bugzilla bug ID (optional if report text provided)"
: "Bugzilla bug ID *"}
? "Bugzilla bug ID or URL (optional if report text provided)"
: "Bugzilla bug ID or URL *"}
</label>
<input
id="bugId"
inputMode="numeric"
placeholder="e.g. 1846789"
placeholder="e.g. 1846789 or https://bugzilla.mozilla.org/show_bug.cgi?id=1846789"
value={bugId}
onChange={(e) => setBugId(e.target.value)}
required={!isReproAgent}
Expand All @@ -220,11 +222,10 @@ export function TriggerForm() {
{isBuildRepairAgent && (
<>
<div className="field">
<label htmlFor="bugId">Bugzilla bug ID (optional)</label>
<label htmlFor="bugId">Bugzilla bug ID or URL (optional)</label>
<input
id="bugId"
inputMode="numeric"
placeholder="e.g. 1846789"
placeholder="e.g. 1846789 or https://bugzilla.mozilla.org/show_bug.cgi?id=1846789"
value={bugId}
onChange={(e) => setBugId(e.target.value)}
/>
Expand Down
83 changes: 83 additions & 0 deletions services/hackbot-ui/lib/bugzilla.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import { test } from "node:test";

import { parseBugId } from "./bugzilla.ts";

test("accepts a bare bug ID", () => {
assert.equal(parseBugId("2058177"), 2058177);
assert.equal(parseBugId(" 2058177 "), 2058177);
});

test("accepts a show_bug.cgi URL", () => {
assert.equal(
parseBugId("https://bugzilla.mozilla.org/show_bug.cgi?id=2058177"),
2058177
);
assert.equal(
parseBugId("http://bugzilla.mozilla.org/show_bug.cgi?id=2058177"),
2058177
);
assert.equal(
parseBugId("//bugzilla.mozilla.org/show_bug.cgi?id=2058177"),
2058177
);
assert.equal(
parseBugId("bugzilla.mozilla.org/show_bug.cgi?id=2058177"),
2058177
);
});

test("ignores extra query parameters and fragments", () => {
assert.equal(
parseBugId(
"https://bugzilla.mozilla.org/show_bug.cgi?id=2058177&comment=5#c5"
),
2058177
);
});

test("accepts the short bugzilla.mozilla.org/<id> form", () => {
assert.equal(parseBugId("https://bugzilla.mozilla.org/2058177"), 2058177);
assert.equal(parseBugId("bugzilla.mozilla.org/2058177/"), 2058177);
});

test("accepts the staging Bugzilla hosts", () => {
assert.equal(
parseBugId("https://bugzilla.allizom.org/show_bug.cgi?id=2058177"),
2058177
);
assert.equal(
parseBugId("https://BUGZILLA-DEV.allizom.org/show_bug.cgi?id=2058177"),
2058177
);
});

test("rejects a non-Bugzilla host carrying an id parameter", () => {
for (const value of [
"https://example.com/?id=2058177",
"https://example.com/show_bug.cgi?id=2058177",
"https://bugzilla.mozilla.org.evil.example/show_bug.cgi?id=2058177",
"https://example.com/2058177",
"//example.com/show_bug.cgi?id=2058177",
]) {
assert.equal(parseBugId(value), null, `expected null for ${value}`);
}
});

test("rejects input without a usable bug ID", () => {
for (const value of [
"",
" ",
"0",
"-1",
"12abc",
"abc",
"https://bugzilla.mozilla.org/show_bug.cgi?id=abc",
"https://bugzilla.mozilla.org/show_bug.cgi",
"https://example.com/foo",
"https://bugzilla.mozilla.org/describecomponents.cgi",
"http://",
]) {
assert.equal(parseBugId(value), null, `expected null for ${value}`);
}
});
38 changes: 38 additions & 0 deletions services/hackbot-ui/lib/bugzilla.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const BUGZILLA_HOSTS = [
"bugzilla.mozilla.org",
"bugzilla.allizom.org",
"bugzilla-dev.allizom.org",
];

/**
* Extract a bug ID from user input: either a bare ID ("1846789") or a Bugzilla
* URL ("https://bugzilla.mozilla.org/show_bug.cgi?id=1846789").
* Returns null when the input does not contain a usable bug ID.
*/
export function parseBugId(value: string): number | null {
const trimmed = value.trim();
if (!trimmed) return null;

let candidate = trimmed;
if (trimmed.includes("/")) {
// Accept URLs with no scheme ("bugzilla.mozilla.org/…") and
// protocol-relative ones ("//bugzilla.mozilla.org/…") as well.
const withScheme = /^https?:\/\//i.test(trimmed)
? trimmed
: `https://${trimmed.replace(/^\/+/, "")}`;
let url: URL;
try {
url = new URL(withScheme);
} catch {
return null;
}
if (!BUGZILLA_HOSTS.includes(url.hostname.toLowerCase())) return null;
// show_bug.cgi?id=123, or the short form bugzilla.mozilla.org/123
candidate =
url.searchParams.get("id") ?? url.pathname.replace(/^\/+|\/+$/g, "");
}

if (!/^\d+$/.test(candidate)) return null;
const parsed = Number.parseInt(candidate, 10);
return parsed > 0 ? parsed : null;
}
Loading