diff --git a/.github/workflows/publish.dispatch.yml b/.github/workflows/publish.dispatch.yml new file mode 100644 index 00000000..7e196900 --- /dev/null +++ b/.github/workflows/publish.dispatch.yml @@ -0,0 +1,68 @@ +name: Publish NPM (Manual) + +on: + workflow_dispatch: + inputs: + release_tag: + type: string + required: true + description: Release Tag to Publish + +jobs: + validate_tag: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + id: validate-release + with: + script: | + const tag = core.getInput('release_tag', { required: true }); + + let exhausted = false; + let page = 1; + while (!exhausted) { + const releases = octokit.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + page, + per_page: 100, + }); + + const matchingRelease = releases.find(r => r.tag_name === tag); + if (matchingRelease) { + core.setOutput('hasRelease', true); + core.setOutput('isPrerelease', release.prerelease); + return; + } + + if (releases.length < 100) { + exhausted = true; + } else { + page++ + } + } + + core.setOutput('hasRelease', false); + core.setOutput('isPrerelease', false); + + - name: Abort + if: ${{ !steps.validate-release.outputs.hasRelease }} + run: | + { + echo "Tag ${{ github.event.inputs.release_tag }} not found." + exit 1 + } + + - name: Print Output + run: | + { + echo "Has Release: ${{ steps.validate-release.outputs.hasRelease }}" + echo "Is Prerelease: ${{ steps.validate-release.outputs.isPrerelease }}" + } + + # publish_npm: + # needs: validate_tag + # uses: ./.github/workflows/publish.reusable.yml + # with: + # release-tag: ${{ github.event.inputs.release_tag }} + # secrets: inherit diff --git a/.github/workflows/publish.reusable.yml b/.github/workflows/publish.reusable.yml new file mode 100644 index 00000000..64c2d3a0 --- /dev/null +++ b/.github/workflows/publish.reusable.yml @@ -0,0 +1,60 @@ +name: Publish to NPM & Brew + +on: + pull_request: + branches: + - main + - supabase-community:main + +jobs: + publish: + name: Publish All the Things + runs-on: ubuntu-latest + + # todo: add secrets + permissions: + contents: write + # ? what's this?! required for executing the node script? + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + registry-url: "https://registry.npmjs.org" + + - name: Generate Packages + run: node packages/@pglt/pglt/scripts/generate-packages.mjs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: "0.1.0" + PRERELEASE: ${{ env.PRERELEASE }} + + - name: Verify NPM TOKEN exists + run: | + if [ -z "${{ secrets.NPM_TOKEN }}" ]; then + echo "Secret is not defined" + exit 1 + else + echo "Secret is defined" + fi + + - name: Publish npm packages as nightly + if: false + run: | + for package in packages/@pglt/*; do + npm publish $package --tag nightly --access public --provenance + done + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # + + - name: Publish npm packages as latest + if: true + run: | + for package in packages/@pglt/*; do + npm publish $package --tag latest --access public --provenance + done + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish.trigger.yml b/.github/workflows/publish.trigger.yml new file mode 100644 index 00000000..9f162e7b --- /dev/null +++ b/.github/workflows/publish.trigger.yml @@ -0,0 +1,13 @@ +name: Publish NPM (Automatic) + +on: + release: + types: [released, prereleased] + +jobs: + publish_npm: + uses: ./.github/workflows/publish.reusable.yml + with: + release-tag: ${{ github.event.release.tag_name }} + is-prerelease: ${{ github.event.release.prerelease }} + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4bc1b90..882dd01d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,7 @@ jobs: fail_on_unmatched_files: true draft: true - - name: ✅ Output Link to Worflow Summary + - name: ✅ Output Link to Workflow Summary run: | { echo "# 🚀 Release completed!" diff --git a/packages/@pglt/pglt/bin/pglt b/packages/@pglt/pglt/bin/pglt new file mode 100644 index 00000000..0dd9f286 --- /dev/null +++ b/packages/@pglt/pglt/bin/pglt @@ -0,0 +1,46 @@ +#!/usr/bin/env node +const { platform, arch, env } = process; + +/** + * platform and arch are values injected into the node runtime. + * We use the values documented on https://nodejs.org. + */ +const PLATFORMS = { + win32: { + x64: "@pglt/testrelease-cli-x86_64-windows-msvc/pglt.exe", + arm64: "@pglt/testrelease-cli-aarch64-windows-msvc/pglt.exe", + }, + darwin: { + x64: "@pglt/testrelease-cli-x86_64-apple-darwin/pglt", + arm64: "@pglt/testrelease-cli-aarch64-apple-darwin/pglt", + }, + linux: { + x64: "@pglt/testrelease-cli-x86_64-linux-gnu/pglt", + arm64: "@pglt/testrelease-cli-aarch64-linux-gnu/pglt", + }, +}; + +const binPath = env.PGLT_BINARY || PLATFORMS?.[platform]?.[arch]; + +if (binPath) { + const result = require("child_process").spawnSync( + require.resolve(binPath), + process.argv.slice(2), + { + shell: false, + stdio: "inherit", + env, + } + ); + + if (result.error) { + throw result.error; + } + + process.exitCode = result.status; +} else { + console.error( + "The pglt CLI package doesn't ship with prebuilt binaries for your platform yet. Please file an issue in the main repository." + ); + process.exitCode = 1; +} diff --git a/packages/@pglt/pglt/package.json b/packages/@pglt/pglt/package.json new file mode 100644 index 00000000..1f3a41ab --- /dev/null +++ b/packages/@pglt/pglt/package.json @@ -0,0 +1,40 @@ +{ + "name": "pglt", + "version": "", + "bin": { + "pglt": "bin/pglt" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/supabase/postgres_lsp.git", + "directory": "packages/@pglt/pglt" + }, + "author": "Supabase Community", + "contributors": [ + { + "name": "Philipp Steinrötter", + "url": "https://github.com/psteinroe" + }, + { + "name": "Julian Domke", + "url": "https://github.com/juleswritescode" + } + ], + "license": "MIT or Apache-2.0", + "description": "A collection of language tools and a Language Server Protocol (LSP) implementation for Postgres, focusing on developer experience and reliable SQL tooling.", + "files": [ + "bin/pglt", + "schema.json" + ], + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@pglt/testrelease-cli-aarch64-apple-darwin": "", + "@pglt/testrelease-cli-aarch64-windows-msvc": "", + "@pglt/testrelease-cli-aarch64-linux-gnu": "", + "@pglt/testrelease-cli-x86_64-apple-darwin": "", + "@pglt/testrelease-cli-x86_64-windows-msvc": "", + "@pglt/testrelease-cli-x86_64-linux-gnu": "" + } +} diff --git a/packages/@pglt/pglt/scripts/generate-packages.mjs b/packages/@pglt/pglt/scripts/generate-packages.mjs new file mode 100644 index 00000000..2d1b89af --- /dev/null +++ b/packages/@pglt/pglt/scripts/generate-packages.mjs @@ -0,0 +1,205 @@ +import assert from "node:assert"; +import * as fs from "node:fs"; +import { pipeline } from "node:stream"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +const streamPipeline = promisify(pipeline); + +const CLI_ROOT = resolve(fileURLToPath(import.meta.url), "../.."); +const PACKAGES_PGLT_ROOT = resolve(CLI_ROOT, ".."); +const PGLT_ROOT = resolve(PACKAGES_PGLT_ROOT, "../.."); +const MANIFEST_PATH = resolve(CLI_ROOT, "package.json"); +const SUPPORTED_PLATFORMS = [ + "pc-windows-msvc", + "apple-darwin", + "unknown-linux-gnu", +]; +const SUPPORTED_ARCHITECTURES = ["x86_64", "aarch64"]; + +async function downloadSchema(releaseTag, githubToken) { + const assetUrl = `https://github.com/supabase-community/postgres_lsp/releases/download/${releaseTag}/schema.json`; + + const response = await fetch(assetUrl, { + headers: { + Authorization: `token ${githubToken}`, + Accept: `application/octet-stream`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to Fetch Asset from ${assetUrl}`); + } + + // download to root. + const fileStream = fs.createWriteStream(resolve(PGLT_ROOT, "schema.json")); + + await streamPipeline(response.body, fileStream); + + console.log(`Downloaded schema for ${releaseTag}`); +} + +async function downloadBinary(platform, arch, os, releaseTag, githubToken) { + const buildName = getBuildName(platform, arch); + + const assetUrl = `https://github.com/supabase-community/postgres_lsp/releases/download/${releaseTag}/${buildName}`; + + const response = await fetch(assetUrl, { + headers: { + Authorization: `token ${githubToken}`, + Accept: `application/octet-stream`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to Fetch Asset from ${assetUrl}. Reason: ${error}`); + } + + // just download to root. + const fileStream = fs.createWriteStream(getBinarySource(platform, arch, os)); + + await streamPipeline(response.body, fileStream); + + console.log(`Downloaded asset for ${buildName} (v${releaseTag})`); +} + +async function overwriteManifestVersions(releaseTag, isPrerelease) { + const version = getVersion(releaseTag, isPrerelease); + + const manifestClone = structuredClone(rootManifest); + + manifestClone.version = version; + for (const key in manifestClone.optionalDependencies) { + manifestClone.optionalDependencies[key] = version; + } + + fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifestClone, null, 2)); +} + +function copyBinaryToNativePackage(platform, arch, os) { + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_PGLT_ROOT, buildName); + const packageName = getPackageName(platform, arch); + + // "unknow-linux-gnu", "apple-darwin" – take linux, apple, windows + + // Update the package.json manifest + const { version, license, repository, engines } = rootManifest; + + const manifest = JSON.stringify( + { + name: packageName, + version, + license, + repository, + engines, + os: [os], + cpu: [arch], + libc: (() => { + switch (os) { + case "linux": + return "gnu"; + case "windows": + return "msvc"; + default: + return undefined; + } + })(), + }, + null, + 2 + ); + + const manifestPath = resolve(packageRoot, "package.json"); + console.info(`Update manifest ${manifestPath}`); + fs.writeFileSync(manifestPath, manifest); + + // Copy the CLI binary + const binarySource = getBinarySource(platform, arch, os); + const ext = getBinaryExt(os); + const binaryTarget = resolve(packageRoot, `pglt${ext}`); + + if (!fs.existsSync(binarySource)) { + console.error( + `Source for binary for ${buildName} not found at: ${binarySource}` + ); + process.exit(1); + } + + console.info(`Copy binary ${binaryTarget}`); + fs.copyFileSync(binarySource, binaryTarget); + fs.chmodSync(binaryTarget, 0o755); +} + +function copySchemaToNativePackage(platform, arch) { + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_PGLT_ROOT, buildName); + + const schemaSrc = resolve(PGLT_ROOT, `schema.json`); + const schemaTarget = resolve(packageRoot, `schema.json`); + + if (!fs.existsSync(schemaSrc)) { + console.error(`schema.json not found at: ${schemaSrc}`); + process.exit(1); + } + + console.info(`Copying schema.json`); + fs.copyFileSync(schemaSrc, schemaTarget); + fs.chmodSync(schemaTarget, 0o666); +} + +const rootManifest = JSON.parse( + fs.readFileSync(MANIFEST_PATH).toString("utf-8") +); + +function getBinaryExt(os) { + return os === "windows" ? ".exe" : ""; +} + +function getBinarySource(platform, arch, os) { + const ext = getBinaryExt(os); + return resolve(PGLT_ROOT, `${getBuildName(platform, arch)}${ext}`); +} + +function getBuildName(platform, arch) { + return `pglt_${arch}-${platform}`; +} + +function getPackageName(platform, arch) { + // trim the "unknown" from linux and the "pc" from windows + const name = platform.split("-").slice(-2).join("-"); + return `@pglt/testrelease-cli-${arch}-${name}`; +} + +function getOs(platform) { + return platform.split("-").find((_, idx) => idx === 1); +} + +function getVersion(releaseTag, isPrerelease) { + return releaseTag + (isPrerelease ? "-rc" : ""); +} + +(async function main() { + const githubToken = process.env.GITHUB_TOKEN; + let releaseTag = process.env.RELEASE_TAG; + assert(githubToken, "GITHUB_TOKEN not defined!"); + assert(releaseTag, "RELEASE_TAG not defined!"); + + const isPrerelease = process.env.PRERELEASE === "true"; + + await downloadSchema(releaseTag, githubToken); + overwriteManifestVersions(releaseTag, isPrerelease); + + for (const platform of SUPPORTED_PLATFORMS) { + const os = getOs(platform); + + for (const arch of SUPPORTED_ARCHITECTURES) { + await downloadBinary(platform, arch, os, releaseTag, githubToken); + copyBinaryToNativePackage(platform, arch, os); + copySchemaToNativePackage(platform, arch); + } + } + + process.exit(0); +})(); diff --git a/packages/@pglt/pglt_aarch64-apple-darwin/package.json b/packages/@pglt/pglt_aarch64-apple-darwin/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/@pglt/pglt_aarch64-apple-darwin/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/@pglt/pglt_aarch64-pc-windows-msvc/package.json b/packages/@pglt/pglt_aarch64-pc-windows-msvc/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/@pglt/pglt_aarch64-pc-windows-msvc/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/@pglt/pglt_aarch64-unknown-linux-gnu/package.json b/packages/@pglt/pglt_aarch64-unknown-linux-gnu/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/@pglt/pglt_aarch64-unknown-linux-gnu/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/@pglt/pglt_x86_64-apple-darwin/package.json b/packages/@pglt/pglt_x86_64-apple-darwin/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/@pglt/pglt_x86_64-apple-darwin/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/@pglt/pglt_x86_64-pc-windows-msvc/package.json b/packages/@pglt/pglt_x86_64-pc-windows-msvc/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/@pglt/pglt_x86_64-pc-windows-msvc/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/@pglt/pglt_x86_64-unknown-linux-gnu/package.json b/packages/@pglt/pglt_x86_64-unknown-linux-gnu/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/@pglt/pglt_x86_64-unknown-linux-gnu/package.json @@ -0,0 +1 @@ +{}