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
43 changes: 43 additions & 0 deletions .github/workflows/release-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,46 @@ jobs:
run: pnpm exec bun apps/cli/scripts/update-scoop.ts --version "${VERSION}" --name "${SCOOP_NAME}"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}

publish-scoop-main:
needs: publish-scoop
# Stable channel only — beta lives in our own bucket, never PR'd upstream.
if: ${{ !inputs.dry_run && inputs.scoop_name == 'supabase' }}
runs-on: ubuntu-latest
# Best-effort: the upstream excavator bot will catch us up within a few
# hours regardless, so a failure here should not fail the release.
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Setup
uses: ./.github/actions/setup

- name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: cli-build-${{ inputs.shell }}-${{ inputs.version }}

- name: Generate scoop-main fork token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
scoop-main

- name: Configure git for fork push
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh auth setup-git
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Open PR against ScoopInstaller/Main
run: pnpm exec bun apps/cli/scripts/update-scoop-main.ts --version ${{ inputs.version }}
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"src/**/*.e2e.test.ts"
],
"ignore": [
"scripts/*.ts",
"scripts/**/*.ts",
"tests/**/*.ts"
],
"ignoreBinaries": [
Expand Down
83 changes: 83 additions & 0 deletions apps/cli/scripts/lib/scoop-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Shared Scoop manifest builder used by both update-scoop.ts (our own
// supabase/scoop-bucket) and update-scoop-main.ts (PR to upstream
// ScoopInstaller/Main). Producing the same JSON from one place keeps
// the two buckets from drifting in URL format, hashes, or arch list.

export interface BuildScoopManifestOptions {
version: string;
repo: string;
checksums: Map<string, string>;
local?: boolean;
distDir?: string;
}

export interface BuildScoopManifestResult {
manifest: object;
json: string;
}

const BIN_ENTRY = "supabase.exe";

export function buildScoopManifest(opts: BuildScoopManifestOptions): BuildScoopManifestResult {
const { version, repo, checksums, local = false, distDir } = opts;

if (local && !distDir) {
throw new Error("distDir is required when local=true");
}

const baseUrl = local
? `file:///${distDir!.replace(/\\/g, "/")}`
: `https://github.com/${repo}/releases/download/v${version}`;

const sha = (file: string): string => {
const hash = checksums.get(file);
if (!hash) throw new Error(`Checksum not found for ${file}`);
return hash;
};

const manifest = {
version,
description: "Supabase CLI",
homepage: "https://supabase.com",
license: "MIT",
architecture: {
"64bit": {
url: `${baseUrl}/supabase_${version}_windows_amd64.zip`,
hash: sha(`supabase_${version}_windows_amd64.zip`),
bin: [BIN_ENTRY],
},
arm64: {
url: `${baseUrl}/supabase_${version}_windows_arm64.zip`,
hash: sha(`supabase_${version}_windows_arm64.zip`),
bin: [BIN_ENTRY],
},
},
checkver: {
github: `https://github.com/${repo}`,
},
autoupdate: {
architecture: {
"64bit": {
url: `https://github.com/${repo}/releases/download/v$version/supabase_$version_windows_amd64.zip`,
},
arm64: {
url: `https://github.com/${repo}/releases/download/v$version/supabase_$version_windows_arm64.zip`,
},
},
},
};

const json = `${JSON.stringify(manifest, null, 4)}\n`;
return { manifest, json };
}

export async function readChecksums(path: string): Promise<Map<string, string>> {
const { readFile } = await import("node:fs/promises");
const text = await readFile(path, "utf-8");
const checksums = new Map<string, string>();
for (const line of text.trim().split("\n")) {
const [hash, file] = line.split(/\s+/) as [string, string];
checksums.set(file, hash);
}
return checksums;
}
108 changes: 108 additions & 0 deletions apps/cli/scripts/update-scoop-main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { $ } from "bun";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";
import { parseArgs } from "node:util";

import { buildScoopManifest, readChecksums } from "./lib/scoop-manifest.ts";

const { values } = parseArgs({
options: {
version: { type: "string" },
repo: { type: "string", default: "supabase/cli" },
fork: { type: "string", default: "supabase/scoop-main" },
upstream: { type: "string", default: "ScoopInstaller/Main" },
"upstream-branch": { type: "string", default: "master" },
local: { type: "boolean", default: false },
"dry-run": { type: "boolean", default: false },
},
});

const version = values.version;
if (!version) {
console.error(
"Usage: bun run scripts/update-scoop-main.ts --version <version> [--repo <owner/repo>] [--fork <owner/repo>] [--upstream <owner/repo>] [--upstream-branch <branch>] [--local] [--dry-run]",
);
process.exit(1);
}

const repo = values.repo!;
const fork = values.fork!;
const upstream = values.upstream!;
const upstreamBranch = values["upstream-branch"]!;
const local = values.local!;
const dryRun = values["dry-run"]!;

const root = path.resolve(import.meta.dir, "../../..");
const distDir = path.join(root, "dist");

const checksums = await readChecksums(path.join(distDir, "checksums.txt"));
const { json: manifestJson } = buildScoopManifest({
version,
repo,
checksums,
local,
distDir,
});

console.log(`Built scoop manifest for ${repo}@${version}`);

if (local || dryRun) {
console.log(manifestJson);
process.exit(0);
}

const branch = `supabase-${version}`;
const manifestPathInRepo = "bucket/supabase.json";

const tmpDir = await mkdtemp(path.join(tmpdir(), "scoop-main-"));
try {
await $`gh repo clone ${fork} ${tmpDir}`;

// Sync fork's master with upstream so the PR diff is clean.
await $`git -C ${tmpDir} remote add upstream https://github.com/${upstream}.git`;
await $`git -C ${tmpDir} fetch upstream ${upstreamBranch}`;
await $`git -C ${tmpDir} checkout ${upstreamBranch}`;
await $`git -C ${tmpDir} reset --hard upstream/${upstreamBranch}`;
await $`git -C ${tmpDir} push origin ${upstreamBranch} --force-with-lease`;

// Branch off the synced base.
await $`git -C ${tmpDir} checkout -B ${branch}`;

await writeFile(path.join(tmpDir, manifestPathInRepo), manifestJson);

// If the manifest is already current upstream (e.g. the excavator bot
// landed this version first), bail out cleanly.
const diff = await $`git -C ${tmpDir} status --porcelain ${manifestPathInRepo}`.text();
if (diff.trim() === "") {
console.log(`${upstream}/${manifestPathInRepo} already at ${version}; nothing to do.`);
process.exit(0);
}

await $`git -C ${tmpDir} add ${manifestPathInRepo}`;
await $`git -C ${tmpDir} commit -m ${`supabase: Update to version ${version}`}`;
await $`git -C ${tmpDir} push origin ${branch} --force-with-lease`;

// Open a PR upstream. If one already exists for this head ref, gh
// will print an error — treat that as success.
const forkOwner = fork.split("/")[0];
const title = `supabase@${version}: Update to ${version}`;
const body = `Bumps the \`supabase\` manifest to v${version}.\n\nSee https://github.com/${repo}/releases/tag/v${version}.`;

const pr =
await $`gh pr create --repo ${upstream} --base ${upstreamBranch} --head ${forkOwner}:${branch} --title ${title} --body ${body}`.nothrow();
if (pr.exitCode !== 0) {
const stderr = pr.stderr.toString();
if (stderr.includes("already exists")) {
console.log(`PR for ${forkOwner}:${branch} already open; skipping.`);
} else {
console.error(stderr);
process.exit(pr.exitCode);
}
} else {
console.log(pr.stdout.toString());
}
} finally {
await rm(tmpDir, { recursive: true });
}
62 changes: 10 additions & 52 deletions apps/cli/scripts/update-scoop.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { $ } from "bun";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";
import { parseArgs } from "node:util";

import { buildScoopManifest, readChecksums } from "./lib/scoop-manifest.ts";

const { values } = parseArgs({
options: {
version: { type: "string" },
Expand Down Expand Up @@ -35,63 +37,19 @@ const dryRun = values["dry-run"]!;
// beta can coexist in the same bucket. Matches the Go CLI's historical
// scoop-bucket layout (`supabase.json` and `supabase-beta.json` both shim
// `supabase.exe`).
const binEntry = "supabase.exe";
const root = path.resolve(import.meta.dir, "../../..");
const distDir = path.join(root, "dist");

// Parse checksums
const checksums = new Map<string, string>();
const checksumsText = await readFile(path.join(distDir, "checksums.txt"), "utf-8");
for (const line of checksumsText.trim().split("\n")) {
const [hash, file] = line.split(/\s+/) as [string, string];
checksums.set(file, hash);
}

function sha(file: string): string {
const hash = checksums.get(file);
if (!hash) throw new Error(`Checksum not found for ${file}`);
return hash;
}

// Scoop supports file:// URLs for local testing
const baseUrl = local
? `file:///${distDir.replace(/\\/g, "/")}`
: `https://github.com/${repo}/releases/download/v${version}`;

const manifest = {
const checksums = await readChecksums(path.join(distDir, "checksums.txt"));
const { json: manifestJson } = buildScoopManifest({
version,
description: "Supabase CLI",
homepage: "https://supabase.com",
license: "MIT",
architecture: {
"64bit": {
url: `${baseUrl}/supabase_${version}_windows_amd64.zip`,
hash: sha(`supabase_${version}_windows_amd64.zip`),
bin: [binEntry],
},
arm64: {
url: `${baseUrl}/supabase_${version}_windows_arm64.zip`,
hash: sha(`supabase_${version}_windows_arm64.zip`),
bin: [binEntry],
},
},
checkver: {
github: `https://github.com/${repo}`,
},
autoupdate: {
architecture: {
"64bit": {
url: `https://github.com/${repo}/releases/download/v$version/supabase_$version_windows_amd64.zip`,
},
arm64: {
url: `https://github.com/${repo}/releases/download/v$version/supabase_$version_windows_arm64.zip`,
},
},
},
};
repo,
checksums,
local,
distDir,
});

const manifestFileName = `${name}.json`;
const manifestJson = `${JSON.stringify(manifest, null, 4)}\n`;
const manifestOut = path.join(distDir, manifestFileName);
await writeFile(manifestOut, manifestJson);
console.log(`Manifest written to ${manifestOut}`);
Expand Down
Loading