diff --git a/.github/workflows/build-cli-artifacts.yml b/.github/workflows/build-cli-artifacts.yml new file mode 100644 index 0000000000..2b01361340 --- /dev/null +++ b/.github/workflows/build-cli-artifacts.yml @@ -0,0 +1,88 @@ +name: Build CLI Artifacts + +on: + workflow_call: + inputs: + version: + description: CLI package version to build + required: true + type: string + shell: + description: CLI shell to package as the shipped supabase binary + required: true + type: string + ref: + description: Optional git ref or SHA to check out before building + required: false + type: string + default: "" + secrets: + SENTRY_DSN: + required: false + POSTHOG_API_KEY: + required: false + POSTHOG_ENDPOINT: + required: false + +permissions: + contents: read + +jobs: + build: + name: Build CLI artifacts + runs-on: blacksmith-32vcpu-ubuntu-2404 + env: + BUN_SHELL: ${{ inputs.shell }} + VERSION: ${{ inputs.version }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + POSTHOG_ENDPOINT: ${{ secrets.POSTHOG_ENDPOINT }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: Setup Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: apps/cli-go/go.mod + cache: true + cache-dependency-path: apps/cli-go/go.sum + + - name: Pre-download Go modules + working-directory: apps/cli-go + run: go mod download -x + + - name: Install nfpm + run: | + echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list + sudo apt-get update + sudo apt-get install -y nfpm + + - name: Sync versions + run: pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${VERSION}" + + - name: Build selected shell + run: pnpm exec bun apps/cli/scripts/build.ts --version "${VERSION}" --shell "${BUN_SHELL}" + + - name: Verify build artifacts + run: | + for pkg in cli-darwin-arm64 cli-darwin-x64 cli-linux-arm64 cli-linux-arm64-musl cli-linux-x64 cli-linux-x64-musl cli-windows-arm64 cli-windows-x64; do + echo "Checking packages/$pkg/bin/..." + ls -la "packages/$pkg/bin/" + done + echo "Checking dist/..." + ls -la dist/ + + - name: Upload build artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: cli-build-${{ inputs.shell }}-${{ inputs.version }} + path: | + packages/cli-*/bin/ + dist/ diff --git a/.github/workflows/publish-preview-cli-packages.yml b/.github/workflows/publish-preview-cli-packages.yml new file mode 100644 index 0000000000..40b6382a4a --- /dev/null +++ b/.github/workflows/publish-preview-cli-packages.yml @@ -0,0 +1,214 @@ +name: Publish Preview CLI Packages + +on: + workflow_run: + workflows: + - Test + types: + - completed + +permissions: + actions: read + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha || github.run_id }} + cancel-in-progress: true + +jobs: + resolve: + if: >- + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + name: Resolve preview build context + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + outputs: + should_build: ${{ steps.context.outputs.should_build }} + pr_number: ${{ steps.context.outputs.pr_number }} + pr_head_sha: ${{ steps.context.outputs.pr_head_sha }} + preview_version: ${{ steps.context.outputs.preview_version }} + steps: + - name: Resolve PR context + id: context + run: | + set -euo pipefail + + should_build=false + pr_number="$(jq -r '.workflow_run.pull_requests[0].number // ""' "$GITHUB_EVENT_PATH")" + pr_head_sha="$(jq -r '.workflow_run.head_sha // ""' "$GITHUB_EVENT_PATH")" + pr_head_branch="$(jq -r '.workflow_run.head_branch // ""' "$GITHUB_EVENT_PATH")" + + if [[ -z "${pr_head_sha}" ]]; then + echo "Workflow run has no head SHA; skipping." + echo "should_build=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ -z "${pr_number}" && -n "${pr_head_branch}" ]]; then + pr_number="$( + gh pr list \ + --repo "${REPOSITORY}" \ + --head "${pr_head_branch}" \ + --state open \ + --json number,headRefOid \ + --jq 'map(select(.headRefOid == "'"${pr_head_sha}"'")) | .[0].number // ""' + )" + fi + + if [[ -z "${pr_number}" ]]; then + echo "Test run is not associated with an open pull request; skipping." + echo "should_build=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")" + current_head_sha="$(jq -r '.head.sha' <<< "${pr_json}")" + state="$(jq -r '.state' <<< "${pr_json}")" + draft="$(jq -r '.draft' <<< "${pr_json}")" + head_repo="$(jq -r '.head.repo.full_name' <<< "${pr_json}")" + base_repo="$(jq -r '.base.repo.full_name' <<< "${pr_json}")" + + if [[ "${state}" != "open" ]]; then + echo "PR #${pr_number} is ${state}; skipping." + echo "should_build=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "${draft}" == "true" ]]; then + echo "PR #${pr_number} is draft; skipping." + echo "should_build=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "${head_repo}" != "${base_repo}" ]]; then + echo "PR #${pr_number} comes from fork ${head_repo}; skipping." + echo "should_build=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "${pr_head_sha}" != "${current_head_sha}" ]]; then + echo "Test SHA ${pr_head_sha} is stale; current PR head is ${current_head_sha}. Skipping." + echo "should_build=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + preview_version="0.0.0-pr.${pr_number}" + should_build=true + + { + echo "should_build=${should_build}" + echo "pr_number=${pr_number}" + echo "pr_head_sha=${pr_head_sha}" + echo "preview_version=${preview_version}" + } >> "$GITHUB_OUTPUT" + + build: + needs: resolve + if: needs.resolve.outputs.should_build == 'true' + name: Build preview CLI packages + uses: ./.github/workflows/build-cli-artifacts.yml + with: + version: ${{ needs.resolve.outputs.preview_version }} + shell: legacy + ref: ${{ needs.resolve.outputs.pr_head_sha }} + + publish: + needs: [resolve, build] + if: needs.resolve.outputs.should_build == 'true' && needs.build.result == 'success' + name: Publish preview package + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: write + pull-requests: read + env: + GH_TOKEN: ${{ github.token }} + PREVIEW_VERSION: ${{ needs.resolve.outputs.preview_version }} + PR_HEAD_SHA: ${{ needs.resolve.outputs.pr_head_sha }} + PR_NUMBER: ${{ needs.resolve.outputs.pr_number }} + REPOSITORY: ${{ github.repository }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.resolve.outputs.pr_head_sha }} + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: Download preview build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: cli-build-legacy-${{ needs.resolve.outputs.preview_version }} + + - name: Prepare package files + run: | + set -euo pipefail + pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${PREVIEW_VERSION}" + pnpm --dir apps/cli build:shim + find packages -path '*/bin/supabase*' -type f -exec chmod +x {} + + + - name: Publish preview package + run: | + pnpm exec pkg-pr-new publish \ + --pnpm \ + --bin \ + --comment=off \ + --json pkg-pr-new.json \ + --no-template \ + './packages/cli-darwin-arm64' \ + './packages/cli-darwin-x64' \ + './packages/cli-linux-arm64' \ + './packages/cli-linux-arm64-musl' \ + './packages/cli-linux-x64' \ + './packages/cli-linux-x64-musl' \ + './packages/cli-windows-arm64' \ + './packages/cli-windows-x64' \ + './apps/cli' + + - name: Smoke test preview command + run: | + set -euo pipefail + preview_url="https://pkg.pr.new/supabase@${PR_NUMBER}" + npx --yes "${preview_url}" --version + + - name: Update PR comment + run: | + set -euo pipefail + preview_url="https://pkg.pr.new/supabase@${PR_NUMBER}" + short_sha="${PR_HEAD_SHA:0:7}" + marker="" + cat > comment.md < comment.json + comment_id="$( + gh api "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --paginate \ + --jq '.[] | select(.body | contains("'"${marker}"'")) | .id' \ + | head -n1 + )" + + if [[ -n "${comment_id}" ]]; then + gh api --method PATCH "repos/${REPOSITORY}/issues/comments/${comment_id}" --input comment.json >/dev/null + else + gh api --method POST "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" --input comment.json >/dev/null + fi diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml index 4c9fe3b8f7..a428dde730 100644 --- a/.github/workflows/release-shared.yml +++ b/.github/workflows/release-shared.yml @@ -53,75 +53,27 @@ on: required: false jobs: build: - runs-on: blacksmith-32vcpu-ubuntu-2404 - env: - BUN_SHELL: ${{ inputs.shell }} - VERSION: ${{ inputs.version }} + name: Build CLI artifacts + uses: ./.github/workflows/build-cli-artifacts.yml + with: + version: ${{ inputs.version }} + shell: ${{ inputs.shell }} + secrets: SENTRY_DSN: ${{ secrets.SENTRY_DSN }} POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} POSTHOG_ENDPOINT: ${{ secrets.POSTHOG_ENDPOINT }} - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup - uses: ./.github/actions/setup - - - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: apps/cli-go/go.mod - cache: true - cache-dependency-path: apps/cli-go/go.sum - - - name: Pre-download Go modules - working-directory: apps/cli-go - run: go mod download -x - - - name: Install nfpm - run: | - echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list - sudo apt-get update - sudo apt-get install -y nfpm - - - name: Sync versions - run: pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${VERSION}" - - - name: Build selected shell - run: pnpm exec bun apps/cli/scripts/build.ts --version "${VERSION}" --shell "${BUN_SHELL}" - - - name: Verify build artifacts - run: | - for pkg in cli-darwin-arm64 cli-darwin-x64 cli-linux-arm64 cli-linux-arm64-musl cli-linux-x64 cli-linux-x64-musl cli-windows-arm64 cli-windows-x64; do - echo "Checking packages/$pkg/bin/..." - ls -la "packages/$pkg/bin/" - done - echo "Checking dist/..." - ls -la dist/ - - - name: Upload build artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: cli-build-${{ inputs.shell }}-${{ inputs.version }} - path: | - packages/cli-*/bin/ - dist/ smoke-test: needs: build strategy: fail-fast: false - # macos-15-intel is the slowest smoke leg and the only one not on - # Blacksmith (Blacksmith macOS is ARM-only). Drop it from the matrix - # on prereleases (PR smoke + develop -> beta) so beta wall-clock isn't - # gated by it; stable releases on main still run the full matrix. - # The matrix list is built via fromJSON because GitHub Actions does - # not allow the `matrix` context in a job-level `if:` (matrix - # expansion happens after job conditions are evaluated). matrix: - runner: ${{ fromJSON(inputs.prerelease && '["blacksmith-8vcpu-ubuntu-2404","blacksmith-6vcpu-macos-latest","blacksmith-8vcpu-windows-2025"]' || '["blacksmith-8vcpu-ubuntu-2404","blacksmith-6vcpu-macos-latest","macos-15-intel","blacksmith-8vcpu-windows-2025"]') }} + runner: + - blacksmith-8vcpu-ubuntu-2404 + - blacksmith-6vcpu-macos-latest + - macos-15-intel + - blacksmith-8vcpu-windows-2025 + - windows-11-arm runs-on: ${{ matrix.runner }} env: NPM_TAG: ${{ inputs.npm_tag }} diff --git a/.github/workflows/release-smoke-test.yml b/.github/workflows/release-smoke-test.yml new file mode 100644 index 0000000000..038092a77c --- /dev/null +++ b/.github/workflows/release-smoke-test.yml @@ -0,0 +1,47 @@ +name: Release Smoke Test + +on: + workflow_dispatch: + inputs: + version: + description: Version to build and smoke test + required: false + type: string + default: 0.0.0-smoke + shell: + description: CLI shell to package as the shipped supabase binary + required: false + type: choice + options: + - legacy + - next + default: legacy + npm_tag: + description: npm tag to use for local package smoke tests + required: false + type: choice + options: + - latest + - alpha + - beta + default: beta + +permissions: + # release-shared.yml declares privileged publish jobs. They are gated by + # dry_run here, but GitHub validates nested-workflow permissions at startup. + contents: write + id-token: write + +jobs: + smoke: + name: Run release smoke tests + uses: ./.github/workflows/release-shared.yml + with: + version: ${{ inputs.version }} + shell: ${{ inputs.shell }} + npm_tag: ${{ inputs.npm_tag }} + channel: beta + prerelease: true + dry_run: true + secrets: + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/.github/workflows/smoke-test-pr.yml b/.github/workflows/smoke-test-pr.yml deleted file mode 100644 index 4e936867e2..0000000000 --- a/.github/workflows/smoke-test-pr.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Smoke Test (PR) - -on: - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - branches: - - develop - # Trigger on any change that could affect the build phase or how the - # built artifacts behave at runtime. Anything in this list is something - # `release-shared.yml`'s build/smoke jobs need to re-validate. - paths: - - "apps/cli/scripts/build.ts" - - "apps/cli/scripts/sync-versions.ts" - - "apps/cli/src/**" - - "apps/cli/package.json" - - "apps/cli-go/**" - - "packages/cli-*/**" - - "package.json" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - - ".github/actions/setup/**" - -permissions: - # release-shared.yml's `publish` job declares `contents: write` and - # `id-token: write`. Even though that job is gated by `!inputs.dry_run` - # and never runs here, GitHub validates nested-workflow permissions at - # startup and rejects the run if the caller grants less than any - # nested job requests. Granting the superset here is safe because (a) - # `dry_run: true` short-circuits the privileged jobs at runtime and - # (b) for fork PRs GitHub still issues a read-only GITHUB_TOKEN - # regardless of the declared scope. - contents: write - id-token: write - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -jobs: - smoke: - if: github.event.pull_request.draft == false - uses: ./.github/workflows/release-shared.yml - with: - # PR-scoped version so concurrent PRs don't collide on the build artifact - # name (release-shared.yml uploads `cli-build-${shell}-${version}`). - version: 0.0.0-pr-${{ github.event.pull_request.number }} - shell: legacy - npm_tag: latest - channel: beta - prerelease: true - dry_run: true - # release-shared.yml's publish/homebrew/scoop jobs reference - # `secrets.APP_ID` and `secrets.GH_APP_PRIVATE_KEY`. They are gated by - # `!inputs.dry_run` and never execute here, but GitHub validates secret - # references at startup, so the called workflow needs the secrets bag - # propagated even when the jobs that use them are skipped. - secrets: - GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/apps/cli/tests/smoke-test-windows.ts b/apps/cli/tests/smoke-test-windows.ts index 1e5c171628..8b4c26d63c 100644 --- a/apps/cli/tests/smoke-test-windows.ts +++ b/apps/cli/tests/smoke-test-windows.ts @@ -1,4 +1,6 @@ import { $ } from "bun"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; @@ -19,6 +21,10 @@ if (tag !== "latest" && tag !== "alpha" && tag !== "beta") { } const root = path.resolve(import.meta.dir, "../../.."); +async function gitBashPath(filePath: string) { + return process.platform === "win32" ? (await $`cygpath -u ${filePath}`.text()).trim() : filePath; +} + interface TestResult { name: string; status: "pass" | "fail"; @@ -32,9 +38,11 @@ console.log(`\n${"=".repeat(60)}`); console.log("Native binary tests"); console.log("=".repeat(60)); +const arch = process.arch === "arm64" ? "arm64" : "x64"; + { - const name = "native-windows-x64"; - const binPath = path.join(root, "packages", "cli-windows-x64", "bin", "supabase.exe"); + const name = `native-windows-${arch}`; + const binPath = path.join(root, "packages", `cli-windows-${arch}`, "bin", "supabase.exe"); console.log(`[${name}] Running ${binPath} --version...`); try { @@ -51,6 +59,38 @@ console.log("=".repeat(60)); } } +// --- Release tarball --- + +console.log(`\n${"=".repeat(60)}`); +console.log("Release tarball test"); +console.log("=".repeat(60)); + +{ + const archiveArch = arch === "arm64" ? "arm64" : "amd64"; + const name = `windows-${archiveArch}-tarball`; + const archivePath = path.join(root, "dist", `supabase_${version}_windows_${archiveArch}.tar.gz`); + const extractDir = await mkdtemp(path.join(tmpdir(), "supabase-windows-tarball-")); + + console.log(`[${name}] Extracting ${archivePath}...`); + try { + await $`tar -xzf ${await gitBashPath(archivePath)} -C ${await gitBashPath(extractDir)}`; + const binPath = path.join(extractDir, "supabase.exe"); + const output = await $`${binPath} --version`.text(); + const trimmed = output.trim(); + const shellCheck = await verifyExpectedShell(binPath); + const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + + console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); + console.log(`[${name}] ${shellCheck.detail}`); + results.push({ name, status: passed ? "pass" : "fail" }); + } catch (e) { + console.error(`[${name}] Error: ${e}`); + results.push({ name, status: "fail" }); + } finally { + await rm(extractDir, { recursive: true, force: true }); + } +} + // --- Scoop --- console.log(`\n${"=".repeat(60)}`); diff --git a/package.json b/package.json index 7845c78455..9f1662441e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@swc-node/register": "catalog:", "@swc/core": "catalog:", "nx": "catalog:", + "pkg-pr-new": "0.0.75", "verdaccio": "^6.7.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef026fe268..0467e6880a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: nx: specifier: 'catalog:' version: 22.7.5(@swc-node/register@1.11.1(@swc/core@1.15.40)(@swc/types@0.1.26)(typescript@6.0.3))(@swc/core@1.15.40) + pkg-pr-new: + specifier: 0.0.75 + version: 0.0.75 verdaccio: specifier: ^6.7.2 version: 6.7.2(typanion@3.14.0) @@ -5253,6 +5256,10 @@ packages: resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} engines: {node: '>=4'} + pkg-pr-new@0.0.75: + resolution: {integrity: sha512-u9mdErTewKSMsr+ceCt8VcNuNP0ro5AXiPXhUVApuEyqr2Zlvt+DdCFBcm+yGWN8mhOdZJ27meIDbnoZgfzpOw==} + hasBin: true + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -11177,6 +11184,8 @@ snapshots: find-up: 2.1.0 load-json-file: 4.0.0 + pkg-pr-new@0.0.75: {} + postcss@8.4.31: dependencies: nanoid: 3.3.12