From 6e0083b7b667a32cdcbeccf7936b2ababd926edc Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Wed, 6 May 2026 00:09:53 +0800 Subject: [PATCH] feat: add OmniRoute vendored release workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/omniroute-artifacts.yaml | 242 +++++++++++++++ .gitmodules | 3 + README.md | 21 +- .../omniroute/scripts/build-artifacts.mjs | 283 ++++++++++++++++++ .../scripts/build-artifacts.test.mjs | 44 +++ packages/omniroute/scripts/verify-startup.mjs | 184 ++++++++++++ packages/omniroute/upstream | 1 + scripts/github-release.test.mjs | 59 ++++ scripts/publication.test.mjs | 74 +++++ 9 files changed, 907 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/omniroute-artifacts.yaml create mode 100644 packages/omniroute/scripts/build-artifacts.mjs create mode 100644 packages/omniroute/scripts/build-artifacts.test.mjs create mode 100644 packages/omniroute/scripts/verify-startup.mjs create mode 160000 packages/omniroute/upstream diff --git a/.github/workflows/omniroute-artifacts.yaml b/.github/workflows/omniroute-artifacts.yaml new file mode 100644 index 0000000..0ed59d6 --- /dev/null +++ b/.github/workflows/omniroute-artifacts.yaml @@ -0,0 +1,242 @@ +name: Build OmniRoute artifacts + +on: + workflow_dispatch: + inputs: + publish_to_azure: + description: Publish built artifacts to Azure Storage after all builds succeed + required: false + type: boolean + default: false + schedule: + - cron: "23 3 * * *" + push: + branches: + - main + paths: + - ".github/workflows/omniroute-artifacts.yaml" + - ".gitmodules" + - "README.md" + - "scripts/**" + - "packages/omniroute/**" + pull_request: + branches: + - main + paths: + - ".github/workflows/omniroute-artifacts.yaml" + - ".gitmodules" + - "README.md" + - "scripts/**" + - "packages/omniroute/**" + +permissions: + contents: read + +jobs: + prepare_release: + name: prepare-release + runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Resolve release version + id: version + run: node ./scripts/versioning.mjs >> "$GITHUB_OUTPUT" + + build: + name: ${{ matrix.name }} + needs: prepare_release + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux x64 + runner: ubuntu-22.04 + artifact_name: omniroute-linux-amd64 + platform: linux + arch: amd64 + - name: macOS x64 + runner: macos-13 + artifact_name: omniroute-macos-amd64 + platform: macos + arch: amd64 + - name: macOS arm64 + runner: macos-14 + artifact_name: omniroute-macos-arm64 + platform: macos + arch: arm64 + - name: Windows x64 + runner: windows-latest + artifact_name: omniroute-windows-amd64 + platform: windows + arch: amd64 + + env: + CI: true + VERSION: ${{ needs.prepare_release.outputs.version }} + BUILD_ARTIFACTS_PLATFORM: ${{ matrix.platform }} + ARCH: ${{ matrix.arch }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: packages/omniroute/upstream/package-lock.json + + - name: Build artifacts + run: node ./packages/omniroute/scripts/build-artifacts.mjs + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }} + path: artifacts/omniroute/* + if-no-files-found: error + + verify: + name: ${{ format('verify-{0}', matrix.name) }} + needs: build + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux x64 + runner: ubuntu-22.04 + artifact_name: omniroute-linux-amd64 + - name: macOS x64 + runner: macos-13 + artifact_name: omniroute-macos-amd64 + - name: macOS arm64 + runner: macos-14 + artifact_name: omniroute-macos-arm64 + - name: Windows x64 + runner: windows-latest + artifact_name: omniroute-windows-amd64 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: packages/omniroute/upstream/.node-version + + - name: Download artifacts + uses: actions/download-artifact@v5 + with: + name: ${{ matrix.artifact_name }} + path: downloaded/omniroute + + - name: Verify downloaded release + run: node ./packages/omniroute/scripts/verify-startup.mjs + + publish_azure: + name: publish-azure + needs: + - prepare_release + - build + - verify + if: >- + ${{ + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_to_azure == 'true') + }} + runs-on: ubuntu-22.04 + concurrency: + group: vendered-azure-publication + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Download build artifacts + uses: actions/download-artifact@v5 + with: + pattern: omniroute-* + path: downloaded + + - name: Validate Azure SAS secret + shell: bash + env: + AZURE_STORAGE_CONTAINER_SAS_URL: ${{ secrets.VENDORED_AZURE_CONTAINER_SAS_URL }} + run: | + if [[ -z "${AZURE_STORAGE_CONTAINER_SAS_URL}" ]]; then + echo "Missing GitHub secret: VENDORED_AZURE_CONTAINER_SAS_URL" >&2 + exit 1 + fi + + - name: Publish vendored artifacts to Azure Storage + env: + AZURE_STORAGE_CONTAINER_SAS_URL: ${{ secrets.VENDORED_AZURE_CONTAINER_SAS_URL }} + run: node ./scripts/publish-to-azure.mjs --artifacts-dir downloaded --publish-result artifacts/publish-result.json + + - name: Update version index + env: + AZURE_STORAGE_CONTAINER_SAS_URL: ${{ secrets.VENDORED_AZURE_CONTAINER_SAS_URL }} + run: node ./scripts/update-version-index.mjs --publish-result artifacts/publish-result.json + + publish_github_release: + name: publish-github-release + needs: + - prepare_release + - build + - verify + if: >- + ${{ + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_to_azure == 'true') + }} + runs-on: ubuntu-22.04 + concurrency: + group: ${{ format('vendered-github-release-{0}', needs.prepare_release.outputs.tag) }} + cancel-in-progress: false + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Download build artifacts + uses: actions/download-artifact@v5 + with: + pattern: omniroute-* + path: downloaded + + - name: Create GitHub release and upload build archives + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + node ./scripts/github-release.mjs + --artifacts-dir downloaded + --tag "${{ needs.prepare_release.outputs.tag }}" + --name "${{ needs.prepare_release.outputs.version }}" + --target-commitish "${{ github.sha }}" diff --git a/.gitmodules b/.gitmodules index 39d5046..b1985d9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "code-server"] path = packages/code-server/upstream url = https://github.com/coder/code-server.git +[submodule "packages/omniroute/upstream"] + path = packages/omniroute/upstream + url = https://github.com/diegosouzapw/OmniRoute.git diff --git a/README.md b/README.md index c5f0410..3adaefe 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,18 @@ This repository stores vendored build inputs and CI automation. - `packages/code-server/` contains the vendored code-server integration. - `packages/code-server/upstream/` is a Git submodule pointing to `https://github.com/coder/code-server.git`. +- `packages/omniroute/` contains the vendored OmniRoute integration. +- `packages/omniroute/upstream/` is a Git submodule pointing to `https://github.com/diegosouzapw/OmniRoute.git`. - `.github/workflows/code-server-artifacts.yaml` builds code-server artifacts on Linux, macOS, and Windows, validates startup on each runner, uploads the outputs to GitHub Actions artifacts, and publishes successful `main` branch pushes into Azure Storage and a GitHub Release in parallel. +- `.github/workflows/omniroute-artifacts.yaml` builds OmniRoute artifacts on Linux x64, macOS x64, macOS arm64, and Windows x64, validates packaged entrypoints on each runner, and only publishes on `main` pushes or explicit manual dispatch. - `packages/code-server/scripts/build-artifacts.mjs` and `packages/code-server/scripts/verify-startup.mjs` are the Node entrypoints for the build and post-build verification flow. +- `packages/omniroute/scripts/build-artifacts.mjs` and `packages/omniroute/scripts/verify-startup.mjs` are the OmniRoute package-local build and packaged-entry verification entrypoints. ## Azure publication -The publication jobs in `.github/workflows/code-server-artifacts.yaml` run after the per-platform build and verification jobs succeed. They publish automatically on `push` to `main`, and they can also be triggered manually with `workflow_dispatch` by setting `publish_to_azure=true`. -Because the SAS publication scripts only use repository files plus downloaded build artifacts, the publish job uses a standalone Node 22 runtime and does not need the `packages/code-server/upstream/` submodule checkout. +The publication jobs in `.github/workflows/code-server-artifacts.yaml` and `.github/workflows/omniroute-artifacts.yaml` run after the per-platform build and verification jobs succeed. They publish automatically on `push` to `main`, and they can also be triggered manually with `workflow_dispatch` by setting `publish_to_azure=true`. +The OmniRoute workflow also has a daily schedule, but scheduled runs stop after build and verification so publication remains explicit. +Because the SAS publication scripts only use repository files plus downloaded build artifacts, the publish jobs use a standalone Node 22 runtime and do not need the package submodule checkout. ## Release versioning @@ -55,7 +60,13 @@ For `code-server`, the initial contract is: - archive blob key: `packages/code-server/versions//-/code-server---.` - metadata blob key: `packages/code-server/versions//-/metadata.json` -`packages/code-server/scripts/build-artifacts.mjs` emits normalized `metadata.json` with: +For `omniroute`, the vendored contract is: + +- `packageId`: `omniroute` +- archive blob key: `packages/omniroute/versions//-/omniroute---.` +- metadata blob key: `packages/omniroute/versions//-/metadata.json` + +`packages/code-server/scripts/build-artifacts.mjs` and `packages/omniroute/scripts/build-artifacts.mjs` emit normalized `metadata.json` with: - `schemaVersion` - `packageId` @@ -66,13 +77,15 @@ For `code-server`, the initial contract is: - `extra` - `artifacts[]` with `kind`, `fileName`, `blobKey`, and integrity fields when available +OmniRoute uses `extra.standaloneBundle = true` and `extra.packagedEntrypoint = "bin/omniroute.mjs"` so downstream publication records can identify the packaged entrypoint contract. + If required metadata is missing, or any declared artifact file does not exist, publication fails before `index.json` is updated. `scripts/publish-to-azure.mjs` and `scripts/update-version-index.mjs` both require `AZURE_STORAGE_CONTAINER_SAS_URL` in the environment. The GitHub workflow maps that from `secrets.VENDORED_AZURE_CONTAINER_SAS_URL`. ## GitHub Release publication -When publication is enabled, the workflow also creates or updates a repository release tagged with `v` and uploads the generated `.tar.gz` and `.zip` build archives. This runs in parallel with Azure publication and uses the workflow's built-in `GITHUB_TOKEN`, so no extra secret is required beyond the Azure SAS URL. +When publication is enabled, each workflow creates or updates the same repository release tagged with `v` and uploads its generated `.tar.gz` and `.zip` build archives. Asset names stay package-specific so OmniRoute and `code-server` can append to the same vendored release/tag without deleting one another's archives. This runs in parallel with Azure publication and uses the workflow's built-in `GITHUB_TOKEN`, so no extra secret is required beyond the Azure SAS URL. ### `index.json` semantics diff --git a/packages/omniroute/scripts/build-artifacts.mjs b/packages/omniroute/scripts/build-artifacts.mjs new file mode 100644 index 0000000..7bf276a --- /dev/null +++ b/packages/omniroute/scripts/build-artifacts.mjs @@ -0,0 +1,283 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto" +import { spawn } from "node:child_process" +import { access, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises" +import path from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" + +import { PUBLICATION_SCHEMA_VERSION, buildBlobKey } from "../../../scripts/publication.mjs" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const packageRoot = path.resolve(__dirname, "..") +const root = path.resolve(packageRoot, "../..") +const upstreamRoot = path.join(packageRoot, "upstream") +const artifactsDir = path.join(root, process.env.ARTIFACTS_OUTPUT_DIR || path.join("artifacts", "omniroute")) +const releaseWorkspace = path.join(root, "release", "omniroute") +const packageId = "omniroute" +const platform = normalizePlatform(process.env.BUILD_ARTIFACTS_PLATFORM || process.platform) +const arch = normalizeArch(process.env.ARCH || process.arch) + +if (isMainModule()) { + main().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : String(error)) + process.exitCode = 1 + }) +} + +async function main() { + process.chdir(root) + await access(upstreamRoot) + + const upstreamVersion = await readUpstreamVersion() + const version = process.env.VERSION || upstreamVersion + + await run("git", ["submodule", "update", "--init", "--recursive"], { cwd: root }) + await rm(artifactsDir, { recursive: true, force: true }) + await rm(releaseWorkspace, { recursive: true, force: true }) + await mkdir(artifactsDir, { recursive: true }) + await mkdir(releaseWorkspace, { recursive: true }) + + await run("npm", ["ci", "--no-audit", "--no-fund"], { + cwd: upstreamRoot, + env: withBuildEnv(process.env, version), + }) + await run("npm", ["run", "build:cli"], { + cwd: upstreamRoot, + env: withBuildEnv(process.env, version), + }) + await run("npm", ["run", "check:pack-artifact"], { + cwd: upstreamRoot, + env: withBuildEnv(process.env, version), + }) + + const releaseRoot = await stageReleaseTree(version) + const artifacts = await createArchive(version, releaseRoot) + await writeMetadata(version, upstreamVersion, artifacts) +} + +async function stageReleaseTree(version) { + const releaseRoot = path.join(releaseWorkspace, `${packageId}-${version}-${platform}-${arch}`) + await rm(releaseRoot, { recursive: true, force: true }) + await mkdir(releaseRoot, { recursive: true }) + + const manifest = JSON.parse(await readFile(path.join(upstreamRoot, "package.json"), "utf8")) + const publishPaths = new Set([...(Array.isArray(manifest.files) ? manifest.files : []), "package.json", "package-lock.json", ".node-version"]) + + for (const relativePath of [...publishPaths].sort()) { + const sourcePath = path.join(upstreamRoot, relativePath) + if (!(await exists(sourcePath))) { + continue + } + + const destinationPath = path.join(releaseRoot, relativePath) + await mkdir(path.dirname(destinationPath), { recursive: true }) + await cp(sourcePath, destinationPath, { + recursive: true, + force: true, + }) + } + + const stagedManifestPath = path.join(releaseRoot, "package.json") + const stagedManifest = JSON.parse(await readFile(stagedManifestPath, "utf8")) + stagedManifest.version = version + await writeFile(stagedManifestPath, `${JSON.stringify(stagedManifest, null, 2)}\n`) + + await access(path.join(releaseRoot, "app", "server.js")) + await access(path.join(releaseRoot, "bin", "omniroute.mjs")) + + return releaseRoot +} + +async function createArchive(version, releaseRoot) { + const archiveBaseName = `${packageId}-${version}-${platform}-${arch}` + const archivePath = + platform === "windows" + ? path.join(artifactsDir, `${archiveBaseName}.zip`) + : path.join(artifactsDir, `${archiveBaseName}.tar.gz`) + + if (platform === "windows") { + await run("powershell.exe", [ + "-NoLogo", + "-NoProfile", + "-Command", + `Compress-Archive -Path '${escapePowerShell(releaseRoot.replaceAll("/", "\\"))}' -DestinationPath '${escapePowerShell(archivePath.replaceAll("/", "\\"))}' -Force`, + ]) + } else { + await run("tar", ["-czf", archivePath, "-C", path.dirname(releaseRoot), path.basename(releaseRoot)]) + } + + const archiveStats = await stat(archivePath) + + return [ + { + kind: "archive", + fileName: path.basename(archivePath), + blobKey: buildBlobKey( + { + packageId, + version, + platform, + arch, + }, + path.basename(archivePath), + ), + sizeBytes: archiveStats.size, + sha256: await calculateSha256(archivePath), + }, + ] +} + +async function writeMetadata(version, upstreamVersion, artifacts) { + const revision = (await readGitOutput(["rev-parse", "HEAD"], upstreamRoot)).trim() + const metadataFileName = "metadata.json" + await writeFile( + path.join(artifactsDir, metadataFileName), + `${JSON.stringify(createMetadataPayload({ version, upstreamVersion, sourceRevision: revision, artifacts }), null, 2)}\n`, + ) +} + +export function createMetadataPayload({ version, upstreamVersion, sourceRevision, artifacts }) { + return { + schemaVersion: PUBLICATION_SCHEMA_VERSION, + packageId, + version, + platform, + arch, + sourceRevision, + extra: { + standaloneBundle: true, + packagedEntrypoint: "bin/omniroute.mjs", + upstreamVersion, + }, + artifacts: [ + ...artifacts, + { + kind: "metadata", + fileName: "metadata.json", + blobKey: buildBlobKey( + { + packageId, + version, + platform, + arch, + }, + "metadata.json", + ), + }, + ], + } +} + +async function readUpstreamVersion() { + const packageJson = JSON.parse(await readFile(path.join(upstreamRoot, "package.json"), "utf8")) + return packageJson.version +} + +function withBuildEnv(env, version) { + return { + ...env, + CI: "true", + npm_config_fund: "false", + npm_config_audit: "false", + VERSION: env.VERSION || version, + } +} + +function normalizePlatform(value) { + switch (String(value).toLowerCase()) { + case "darwin": + case "macos": + return "macos" + case "win32": + case "windows": + case "windows_nt": + return "windows" + default: + return "linux" + } +} + +function normalizeArch(value) { + switch (String(value).toLowerCase()) { + case "x64": + return "amd64" + case "aarch64": + return "arm64" + default: + return String(value).toLowerCase() + } +} + +async function exists(targetPath) { + try { + await access(targetPath) + return true + } catch { + return false + } +} + +function getCommand(command) { + if (process.platform === "win32" && command === "npm") { + return "npm.cmd" + } + return command +} + +function run(command, args, options = {}) { + const finalCommand = getCommand(command) + return new Promise((resolve, reject) => { + const child = spawn(finalCommand, args, { + cwd: options.cwd || root, + env: options.env || process.env, + stdio: "inherit", + }) + + child.on("error", reject) + child.on("exit", (code) => { + if (code === 0) { + resolve() + return + } + reject(new Error(`${finalCommand} ${args.join(" ")} exited with code ${code}`)) + }) + }) +} + +function readGitOutput(args, cwd) { + return new Promise((resolve, reject) => { + const child = spawn("git", args, { + cwd, + env: process.env, + stdio: ["ignore", "pipe", "inherit"], + }) + + let output = "" + child.stdout.on("data", (chunk) => { + output += chunk.toString() + }) + child.on("error", reject) + child.on("exit", (code) => { + if (code === 0) { + resolve(output) + return + } + reject(new Error(`git ${args.join(" ")} exited with code ${code}`)) + }) + }) +} + +function escapePowerShell(value) { + return value.replaceAll("'", "''") +} + +async function calculateSha256(filePath) { + const contents = await readFile(filePath) + return createHash("sha256").update(contents).digest("hex") +} + +function isMainModule() { + return process.argv[1] != null && import.meta.url === pathToFileURL(process.argv[1]).href +} diff --git a/packages/omniroute/scripts/build-artifacts.test.mjs b/packages/omniroute/scripts/build-artifacts.test.mjs new file mode 100644 index 0000000..add5ac6 --- /dev/null +++ b/packages/omniroute/scripts/build-artifacts.test.mjs @@ -0,0 +1,44 @@ +import test from "node:test" +import assert from "node:assert/strict" + +import { createMetadataPayload } from "./build-artifacts.mjs" +import { buildBlobKey } from "../../../scripts/publication.mjs" + +test("createMetadataPayload emits vendored OmniRoute publication metadata", () => { + const metadata = createMetadataPayload({ + version: "2026.0505.0001", + upstreamVersion: "3.7.4", + sourceRevision: "abc123", + artifacts: [ + { + kind: "archive", + fileName: "omniroute-2026.0505.0001-linux-amd64.tar.gz", + blobKey: buildBlobKey( + { + packageId: "omniroute", + version: "2026.0505.0001", + platform: "linux", + arch: "amd64", + }, + "omniroute-2026.0505.0001-linux-amd64.tar.gz", + ), + sizeBytes: 123, + sha256: "a".repeat(64), + }, + ], + }) + + assert.equal(metadata.packageId, "omniroute") + assert.equal(metadata.version, "2026.0505.0001") + assert.equal(metadata.sourceRevision, "abc123") + assert.deepEqual(metadata.extra, { + standaloneBundle: true, + packagedEntrypoint: "bin/omniroute.mjs", + upstreamVersion: "3.7.4", + }) + assert.deepEqual(metadata.artifacts.at(-1), { + kind: "metadata", + fileName: "metadata.json", + blobKey: "packages/omniroute/versions/2026.0505.0001/linux-amd64/metadata.json", + }) +}) diff --git a/packages/omniroute/scripts/verify-startup.mjs b/packages/omniroute/scripts/verify-startup.mjs new file mode 100644 index 0000000..c85d2a3 --- /dev/null +++ b/packages/omniroute/scripts/verify-startup.mjs @@ -0,0 +1,184 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process" +import { access, mkdtemp, readFile, readdir, rm } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const packageRoot = path.resolve(__dirname, "..") +const root = path.resolve(packageRoot, "../..") +const downloadedDir = path.resolve(root, process.env.ARTIFACTS_DOWNLOAD_DIR || path.join("downloaded", "omniroute")) + +main().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : String(error)) + process.exitCode = 1 +}) + +async function main() { + const metadataPath = await findFile(downloadedDir, (entryPath) => path.basename(entryPath) === "metadata.json") + if (!metadataPath) { + throw new Error(`No metadata.json found under ${downloadedDir}`) + } + + const metadata = JSON.parse(await readFile(metadataPath, "utf8")) + if (metadata.packageId !== "omniroute") { + throw new Error(`Expected omniroute metadata, received ${String(metadata.packageId)}`) + } + + const archiveDescriptor = Array.isArray(metadata.artifacts) + ? metadata.artifacts.find((artifact) => artifact?.kind === "archive") + : null + if (!archiveDescriptor?.fileName) { + throw new Error(`Metadata ${metadataPath} does not declare an archive artifact`) + } + + const archivePath = path.join(path.dirname(metadataPath), archiveDescriptor.fileName) + await access(archivePath) + + const tempDirectory = await mkdtemp(path.join(os.tmpdir(), "vendored-omniroute-verify-")) + + try { + await extractArchive(archivePath, tempDirectory) + const releaseRoot = await findReleaseRoot(tempDirectory) + await access(path.join(releaseRoot, "app", "server.js")) + await access(path.join(releaseRoot, "bin", "omniroute.mjs")) + + const version = await runAndCapture( + process.execPath, + [path.join("bin", "omniroute.mjs"), "--version"], + { + cwd: releaseRoot, + env: { + ...process.env, + HOME: path.join(tempDirectory, "home"), + USERPROFILE: path.join(tempDirectory, "home"), + APPDATA: path.join(tempDirectory, "appdata"), + DATA_DIR: path.join(tempDirectory, "data"), + OMNIROUTE_MEMORY_MB: "256", + }, + }, + ) + + if (version.trim() !== metadata.version) { + throw new Error(`Packaged OmniRoute version mismatch: expected ${metadata.version}, received ${version.trim()}`) + } + + console.log(`Verified OmniRoute package ${metadata.version}`) + } finally { + await rm(tempDirectory, { recursive: true, force: true }) + } +} + +async function extractArchive(archivePath, destinationDir) { + if (archivePath.endsWith(".tar.gz")) { + await run("tar", ["-xzf", archivePath, "-C", destinationDir]) + return + } + + if (!archivePath.endsWith(".zip")) { + throw new Error(`Unsupported archive format: ${archivePath}`) + } + + await run("powershell.exe", [ + "-NoLogo", + "-NoProfile", + "-Command", + `Expand-Archive -Path '${escapePowerShell(archivePath.replaceAll("/", "\\"))}' -DestinationPath '${escapePowerShell(destinationDir.replaceAll("/", "\\"))}' -Force`, + ]) +} + +async function findReleaseRoot(rootDir) { + const entries = await readdir(rootDir, { withFileTypes: true }) + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue + } + + const candidate = path.join(rootDir, entry.name) + if (await exists(path.join(candidate, "bin", "omniroute.mjs"))) { + return candidate + } + } + + throw new Error(`Unable to find extracted OmniRoute release root in ${rootDir}`) +} + +async function findFile(rootDir, predicate) { + const entries = await readdir(rootDir, { withFileTypes: true }) + + for (const entry of entries) { + const entryPath = path.join(rootDir, entry.name) + if (entry.isFile() && predicate(entryPath)) { + return entryPath + } + + if (entry.isDirectory()) { + const nested = await findFile(entryPath, predicate) + if (nested) { + return nested + } + } + } + + return null +} + +async function exists(targetPath) { + try { + await access(targetPath) + return true + } catch { + return false + } +} + +function run(command, args, options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd || root, + env: options.env || process.env, + stdio: "inherit", + }) + + child.on("error", reject) + child.on("exit", (code) => { + if (code === 0) { + resolve() + return + } + reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`)) + }) + }) +} + +function runAndCapture(command, args, options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd || root, + env: options.env || process.env, + stdio: ["ignore", "pipe", "inherit"], + }) + + let output = "" + child.stdout.on("data", (chunk) => { + output += chunk.toString() + }) + + child.on("error", reject) + child.on("exit", (code) => { + if (code === 0) { + resolve(output) + return + } + reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`)) + }) + }) +} + +function escapePowerShell(value) { + return value.replaceAll("'", "''") +} diff --git a/packages/omniroute/upstream b/packages/omniroute/upstream new file mode 160000 index 0000000..99c6dc7 --- /dev/null +++ b/packages/omniroute/upstream @@ -0,0 +1 @@ +Subproject commit 99c6dc7fd69523e9f2da974826fcd31a8a351163 diff --git a/scripts/github-release.test.mjs b/scripts/github-release.test.mjs index 934197f..4f3c52d 100644 --- a/scripts/github-release.test.mjs +++ b/scripts/github-release.test.mjs @@ -149,3 +149,62 @@ test("publishGitHubRelease replaces existing assets on rerun", async () => { await rm(tempDirectory, { recursive: true, force: true }) } }) + +test("publishGitHubRelease preserves unrelated existing assets on shared vendored tags", async () => { + const tempDirectory = await mkdtemp(path.join(os.tmpdir(), "vendored-release-shared-")) + + try { + await writeFile(path.join(tempDirectory, "omniroute-2026.0505.0001-linux-amd64.tar.gz"), "linux") + + const requests = [] + const fetchImpl = async (url, options) => { + requests.push({ url, options }) + + if (String(url).includes("/releases/tags/")) { + return Response.json({ + url: "https://api.github.com/repos/newbe36524/vendered/releases/1", + upload_url: "https://uploads.github.com/repos/newbe36524/vendered/releases/1/assets{?name,label}", + assets: [ + { + name: "code-server-2026.0505.0001-linux-amd64.tar.gz", + url: "https://api.github.com/repos/newbe36524/vendered/releases/assets/10", + }, + ], + }) + } + + if (String(url).endsWith("/releases/1")) { + return Response.json({ + url: "https://api.github.com/repos/newbe36524/vendered/releases/1", + upload_url: "https://uploads.github.com/repos/newbe36524/vendered/releases/1/assets{?name,label}", + assets: [ + { + name: "code-server-2026.0505.0001-linux-amd64.tar.gz", + url: "https://api.github.com/repos/newbe36524/vendered/releases/assets/10", + }, + ], + }) + } + + if (String(url).startsWith("https://uploads.github.com/")) { + return Response.json({ ok: true }) + } + + throw new Error(`Unexpected request: ${url}`) + } + + await publishGitHubRelease({ + artifactsDir: tempDirectory, + repository: "newbe36524/vendered", + token: "test-token", + tagName: "v2026.0505.0001", + releaseName: "2026.0505.0001", + targetCommitish: "abc123", + fetchImpl, + }) + + assert.deepEqual(requests.map((request) => request.options.method), ["GET", "PATCH", "POST"]) + } finally { + await rm(tempDirectory, { recursive: true, force: true }) + } +}) diff --git a/scripts/publication.test.mjs b/scripts/publication.test.mjs index 0c6397a..4562576 100644 --- a/scripts/publication.test.mjs +++ b/scripts/publication.test.mjs @@ -304,6 +304,80 @@ test("mergeVersionIndex replaces repeated publish entries without duplicating ve assert.equal(mergedIndex.packages["code-server"].versions["1.2.2"].sourceRevision, "olderrev") }) +test("mergeVersionIndex adds OmniRoute entries without clobbering code-server versions", () => { + const currentIndex = createEmptyVersionIndex("2026-05-04T00:00:00.000Z") + currentIndex.packages["code-server"] = { + packageId: "code-server", + versions: { + "1.2.3": { + packageId: "code-server", + version: "1.2.3", + publishedAt: "2026-05-04T00:00:00.000Z", + sourceRevision: "coderev", + extra: { slimArtifact: true }, + artifacts: [ + { + kind: "archive", + fileName: "code-server-1.2.3-linux-amd64.tar.gz", + blobKey: buildBlobKey( + { + packageId: "code-server", + version: "1.2.3", + platform: "linux", + arch: "amd64", + }, + "code-server-1.2.3-linux-amd64.tar.gz", + ), + platform: "linux", + arch: "amd64", + }, + ], + }, + }, + } + + const publishResult = { + schemaVersion: 1, + generatedAt: "2026-05-05T00:00:00.000Z", + entries: [ + { + packageId: "omniroute", + version: "2026.0505.0001", + publishedAt: "2026-05-05T00:00:00.000Z", + sourceRevision: "omnirev", + extra: { + standaloneBundle: true, + packagedEntrypoint: "bin/omniroute.mjs", + }, + artifacts: [ + { + kind: "archive", + fileName: "omniroute-2026.0505.0001-linux-amd64.tar.gz", + blobKey: buildBlobKey( + { + packageId: "omniroute", + version: "2026.0505.0001", + platform: "linux", + arch: "amd64", + }, + "omniroute-2026.0505.0001-linux-amd64.tar.gz", + ), + platform: "linux", + arch: "amd64", + }, + ], + }, + ], + } + + const mergedIndex = mergeVersionIndex(currentIndex, publishResult, { + generatedAt: "2026-05-05T00:00:00.000Z", + }) + + assert.equal(mergedIndex.packages["code-server"].versions["1.2.3"].sourceRevision, "coderev") + assert.equal(mergedIndex.packages["omniroute"].versions["2026.0505.0001"].sourceRevision, "omnirev") +}) + test("loadPublishInputs fails when metadata is incomplete", async () => { const tempDirectory = await mkdtemp(path.join(os.tmpdir(), "vendored-publication-test-"))