Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0e2e126
fix(cli): push oauth server enabled and path together (CLI-2691) (#7367)
7ttp Oct 28, 2026
c725074
chore(stack): bump storage to v1.84.34-r0 (#7373)
supabase-cli-releaser[bot] Oct 28, 2026
7916c98
chore(stack): bump postgres to 17.11.0.008-r0 (#7375)
supabase-cli-releaser[bot] Oct 28, 2026
5d5f526
chore(stack): add postgres 17.11.0.008-orioledb-r0 (#7376)
supabase-cli-releaser[bot] Oct 28, 2026
61c822d
chore(stack): bump storage to v1.84.35-r0 (#7381)
supabase-cli-releaser[bot] Oct 28, 2026
0cc65ea
chore(stack): bump storage to v1.84.36-r0 (#7383)
supabase-cli-releaser[bot] Oct 28, 2026
480345c
chore(stack): bump realtime to v2.149.2-r0 (#7389)
supabase-cli-releaser[bot] Oct 29, 2026
bff634c
fix(cli): let `link` work without secret key access (CLI-2274, CLI-26…
7ttp Oct 29, 2026
b0f1016
feat(config): publish the config schemas as draft-07 under supabase.c…
Coly010 Oct 29, 2026
097b7f2
feat(stack): add the remaining gateway routes and forwarded headers (…
7ttp Oct 29, 2026
96781a4
ci(cli): authenticate mirror detect job to ECR Public (#7390)
avallete Oct 29, 2026
fd2e017
fix(cli): recover start and db start from a stopped database containe…
Coly010 Oct 29, 2026
f8bcf89
ci(cli): add a report-only CLI benchmark for pull requests (#7363)
Prashansa-K Oct 29, 2026
4834c89
chore(stack): bump postgres to 15.19.0.011-r0 (#7392)
supabase-cli-releaser[bot] Oct 29, 2026
6bcada7
chore(stack): bump postgres to 17.11.0.011-r0 (#7391)
supabase-cli-releaser[bot] Oct 29, 2026
3a9e1fb
chore(stack): add postgres 17.11.0.011-orioledb-r0 (#7393)
supabase-cli-releaser[bot] Oct 29, 2026
b73be06
fix(cli): lint config-exposed schemas in local db advisors (CLI-2824)…
mdonnalley Oct 29, 2026
74d4774
chore: sync API types from infrastructure (#7400)
supabase-cli-releaser[bot] Oct 29, 2026
56569b5
Merge remote-tracking branch 'origin/next' into sync/develop-into-next
github-actions[bot] Oct 29, 2026
60bb734
chore(repo): fix the develop-into-next merge
github-actions[bot] Oct 29, 2026
c7e287b
chore(repo): repair the develop-into-next sync
github-actions[bot] Oct 29, 2026
bcd3ec9
fix(cli): stop `db reset` failing at random (CLI-2312, CLI-2563) (#7380)
7ttp Oct 29, 2026
db33a57
test(cli): confirm consent-gated commands in live tests
Coly010 Oct 29, 2026
465b1d8
chore(repo): merge develop into sync/develop-into-next
Coly010 Oct 29, 2026
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
4 changes: 1 addition & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ packages/api/src/generated/* linguist-generated=true
packages/api/scripts/openapi-source.json linguist-generated=true
packages/stack/src/functions/generated/* text eol=lf linguist-generated=true
apps/cli/src/shared/feedback/database.types.ts linguist-generated=true
apps/docs/public/cli/config.schema.json linguist-generated=true
apps/docs/public/cli/project-config.schema.json linguist-generated=true
apps/docs/public/cli/supabase-lock/v1.schema.json linguist-generated=true
apps/docs/public/schemas/** linguist-generated=true

# Lockfiles. Collapsed rather than hidden -- a dependency change is still one
# click away in review, which is the same treatment GitHub gives package-lock.json.
Expand Down
69 changes: 69 additions & 0 deletions .github/scripts/cli-bench/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/** Small GitHub REST helpers shared by the CLI benchmark scripts. */

export function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}

/** A GitHub request that did not succeed, with the HTTP status and GitHub's own message. */
export class GithubRequestError extends Error {
constructor(
readonly status: number,
readonly detail: string,
url: string,
) {
super(`GitHub request failed (${status}) for ${url}`);
this.name = "GithubRequestError";
}
}

export async function githubFetch(
url: string,
token: string,
init: RequestInit = {},
): Promise<Response> {
const response = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
},
});
if (!response.ok) {
const payload = (await response.json().catch(() => ({}))) as { message?: unknown };
const detail = typeof payload.message === "string" ? payload.message : response.statusText;
throw new GithubRequestError(response.status, detail, url);
}
return response;
}

export interface IssueComment {
id: number;
body: string;
authorLogin: string;
}

/** Every comment on an issue or pull request, oldest first. */
export async function listIssueComments(
token: string,
base: string,
issue: number,
): Promise<Array<IssueComment>> {
const comments: Array<IssueComment> = [];
for (let page = 1; ; page++) {
const batch = (await (
await githubFetch(`${base}/issues/${issue}/comments?per_page=100&page=${page}`, token)
).json()) as Array<{ id: number; body: string | null; user: { login: string } | null }>;
comments.push(
...batch.map((entry) => ({
id: entry.id,
body: entry.body ?? "",
authorLogin: entry.user?.login ?? "",
})),
);
if (batch.length < 100) return comments;
}
}
114 changes: 114 additions & 0 deletions .github/scripts/cli-bench/history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { describe, expect, test } from "bun:test";
import {
decodeHistory,
headlineOf,
makeEntry,
MAX_PREVIOUS_RUNS,
mergeHistory,
renderHistory,
type RunEntry,
} from "./history.ts";

const entry = (n: number, overrides: Partial<RunEntry> = {}): RunEntry => ({
sha: n.toString(16).padStart(7, "0"),
at: "2026-10-28T09:05Z",
headline: "no changes found",
url: `https://github.com/o/r/actions/runs/${n}`,
...overrides,
});

describe("headlineOf", () => {
test("reads the title of the rendered report", () => {
expect(headlineOf("<!-- cli-bench:pr -->\n\n## CLI benchmark: 2 things to check\n\nbody")).toBe(
"2 things to check",
);
});

test("falls back when the report has no title", () => {
expect(headlineOf("nothing here")).toBe("unknown");
});
});

describe("makeEntry", () => {
test("shortens the commit and rounds the time down to the minute in UTC", () => {
const made = makeEntry({
sha: "0123456789abcdef0123456789abcdef01234567",
now: new Date("2026-10-28T09:05:59Z"),
headline: "no changes found",
url: "https://github.com/o/r/actions/runs/9/attempts/2",
});

expect(made).toMatchObject({ sha: "0123456", at: "2026-10-28T09:05Z" });
});

test("refuses a run link that is not a workflow run", () => {
const made = makeEntry({
sha: "abcdef1",
now: new Date(),
headline: "h",
url: "https://evil.test/x",
});

expect(made).toBeUndefined();
});
});

describe("mergeHistory", () => {
test("puts the new run first", () => {
expect(mergeHistory([entry(1)], entry(2)).map((e) => e.sha)).toEqual([
entry(2).sha,
entry(1).sha,
]);
});

test("keeps the current run plus at most 20 earlier ones, dropping the oldest", () => {
const previous = Array.from({ length: 30 }, (_, i) => entry(i + 1));

const merged = mergeHistory(previous, entry(100));

expect(merged).toHaveLength(MAX_PREVIOUS_RUNS + 1);
expect(merged[0]).toEqual(entry(100));
expect(merged.at(-1)).toEqual(entry(MAX_PREVIOUS_RUNS));
});
});

describe("renderHistory and decodeHistory", () => {
test("round-trips the entries through the hidden marker", () => {
const entries = [entry(3), entry(2), entry(1)];

expect(decodeHistory(`body\n${renderHistory(entries, (t) => t)}\n`)).toEqual(entries);
});

test("lists earlier runs newest first and leaves the current run out of the list", () => {
const rendered = renderHistory(
[entry(3), entry(2, { headline: "1 thing to check" })],
(t) => t,
);

expect(rendered).toContain("<summary>Previous runs</summary>");
expect(rendered).toContain(
"- `0000002` · 2026-10-28 09:05 UTC · 1 thing to check · [run](https://github.com/o/r/actions/runs/2)",
);
expect(rendered).not.toContain("0000003` ·");
});

test("shows no list for a first run", () => {
expect(renderHistory([entry(1)], (t) => t)).not.toContain("Previous runs");
});

test.each([
["no marker", "plain comment"],
["not base64 json", "<!-- cli-bench:history bm90IGpzb24= -->"],
["wrong shape", `<!-- cli-bench:history ${Buffer.from('[{"sha":1}]').toString("base64")} -->`],
[
"a forged run link",
`<!-- cli-bench:history ${Buffer.from(JSON.stringify([entry(1, { url: "https://evil.test/x" })])).toString("base64")} -->`,
],
[
"too many entries",
`<!-- cli-bench:history ${Buffer.from(JSON.stringify(Array.from({ length: 22 }, (_, i) => entry(i + 1)))).toString("base64")} -->`,
],
])("treats %s as empty history", (_name, body) => {
expect(decodeHistory(body)).toEqual([]);
});
});
106 changes: 106 additions & 0 deletions .github/scripts/cli-bench/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Run history kept inside the benchmark comment. The newest run is the comment itself; earlier
* runs sit in a collapsed list, backed by a hidden marker so the next update can carry them on.
* The marker is read back only from the workflow bot's own comment, and anything that does not
* decode cleanly counts as no history.
*/

/** Earlier runs kept below the current one. */
export const MAX_PREVIOUS_RUNS = 20;
const HISTORY_MARKER = /<!-- cli-bench:history ([A-Za-z0-9+/=]+) -->/u;
const MAX_ENCODED_CHARS = 30_000;
const MAX_HEADLINE_CHARS = 100;
const SHA = /^[0-9a-f]{7,40}$/u;
const AT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/u;
const RUN_URL = /^https:\/\/[^\s)<>]+\/actions\/runs\/\d+(?:\/attempts\/\d+)?$/u;

export interface RunEntry {
/** Short commit SHA. */
sha: string;
/** UTC time to the minute, `YYYY-MM-DDTHH:MMZ`. */
at: string;
headline: string;
url: string;
}

const isEntry = (value: unknown): value is RunEntry => {
if (typeof value !== "object" || value === null) return false;
const { sha, at, headline, url } = value as Record<string, unknown>;
return (
typeof sha === "string" &&
SHA.test(sha) &&
typeof at === "string" &&
AT.test(at) &&
typeof headline === "string" &&
headline.length <= MAX_HEADLINE_CHARS &&
!/[\r\n]/u.test(headline) &&
typeof url === "string" &&
RUN_URL.test(url)
);
};

/** The title the renderer puts after `## CLI benchmark:`, flattened to one bounded line. */
export function headlineOf(markdown: string): string {
const title = /^## CLI benchmark: (.+)$/mu.exec(markdown)?.[1] ?? "unknown";
return title.replaceAll(/\s+/gu, " ").trim().slice(0, MAX_HEADLINE_CHARS);
}

/** An entry for a run, or undefined when the commit or run link is not in the expected shape. */
export function makeEntry(input: {
sha: string;
now: Date;
headline: string;
url: string;
}): RunEntry | undefined {
const entry = {
sha: input.sha.slice(0, 7),
at: `${input.now.toISOString().slice(0, 16)}Z`,
headline: input.headline,
url: input.url,
};
return isEntry(entry) ? entry : undefined;
}

/** The runs recorded in a comment body, newest first; empty when the marker is absent or invalid. */
export function decodeHistory(body: string): Array<RunEntry> {
const encoded = HISTORY_MARKER.exec(body)?.[1];
if (encoded === undefined || encoded.length > MAX_ENCODED_CHARS) return [];
try {
const value: unknown = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
return Array.isArray(value) && value.length <= MAX_PREVIOUS_RUNS + 1 && value.every(isEntry)
? value
: [];
} catch {
return [];
}
}

/** The new run first, then the earlier ones, with at most {@link MAX_PREVIOUS_RUNS} behind it. */
export function mergeHistory(
previous: ReadonlyArray<RunEntry>,
current: RunEntry,
): Array<RunEntry> {
return [current, ...previous].slice(0, MAX_PREVIOUS_RUNS + 1);
}

const day = (at: string) => `${at.slice(0, 10)} ${at.slice(11, 16)} UTC`;

/**
* The hidden marker for `entries` (current run first), preceded by a collapsed list of the
* earlier ones when there are any. `clean` makes a headline safe to render.
*/
export function renderHistory(
entries: ReadonlyArray<RunEntry>,
clean: (text: string) => string,
): string {
const marker = `<!-- cli-bench:history ${Buffer.from(JSON.stringify(entries)).toString("base64")} -->`;
const earlier = entries.slice(1);
if (earlier.length === 0) return marker;
const lines = earlier.map(
(entry) =>
`- \`${entry.sha}\` · ${day(entry.at)} · ${clean(entry.headline)} · [run](${entry.url})`,
);
return ["<details><summary>Previous runs</summary>", "", ...lines, "", "</details>", marker].join(
"\n",
);
}
Loading
Loading