fix: publish lazycodex as release alias

This commit is contained in:
YeonGyu-Kim
2026-05-30 23:34:13 +09:00
parent d508b9b52b
commit 090e780974
4 changed files with 132 additions and 129 deletions
+33 -74
View File
@@ -22,8 +22,8 @@ on:
required: false
type: boolean
default: false
publish_lazycodex:
description: "Publish lazycodex npm alias and sync Codex marketplace"
sync_lazycodex_marketplace:
description: "Sync the LazyCodex Codex marketplace repository"
required: false
type: boolean
default: false
@@ -35,9 +35,6 @@ permissions:
id-token: write
actions: write
env:
LAZYCODEX_VERSION: "0.1.0"
jobs:
test:
runs-on: ubuntu-latest
@@ -81,7 +78,7 @@ jobs:
contents: read
steps:
- name: Require LazyCodex sync token
if: inputs.publish_lazycodex == true
if: inputs.sync_lazycodex_marketplace == true
env:
LAZYCODEX_SYNC_TOKEN: ${{ secrets.LAZYCODEX_SYNC_TOKEN }}
run: |
@@ -94,7 +91,6 @@ jobs:
env:
REPO: code-yeongyu/oh-my-openagent
WORKFLOW_FILE: publish.yml
PUBLISH_LAZYCODEX: ${{ inputs.publish_lazycodex }}
run: |
OIDC_TOKEN=$(curl -sH "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org" \
@@ -106,10 +102,7 @@ jobs:
fi
PLATFORMS=(darwin-arm64 darwin-x64 darwin-x64-baseline linux-x64 linux-x64-baseline linux-arm64 linux-x64-musl linux-x64-musl-baseline linux-arm64-musl windows-x64 windows-x64-baseline)
ALL_PACKAGES=(oh-my-opencode oh-my-openagent)
if [ "${PUBLISH_LAZYCODEX}" = "true" ]; then
ALL_PACKAGES+=(lazycodex)
fi
ALL_PACKAGES=(oh-my-opencode oh-my-openagent lazycodex)
for plat in "${PLATFORMS[@]}"; do
ALL_PACKAGES+=("oh-my-opencode-${plat}")
ALL_PACKAGES+=("oh-my-openagent-${plat}")
@@ -135,45 +128,20 @@ jobs:
done
if [ ${#FAILED[@]} -gt 0 ]; then
HARD_FAILED=()
SOFT_FAILED=()
for pkg in "${FAILED[@]}"; do
if [ "${pkg}" = "lazycodex" ]; then
SOFT_FAILED+=("${pkg}")
else
HARD_FAILED+=("${pkg}")
fi
done
if [ ${#HARD_FAILED[@]} -gt 0 ]; then
{
echo
echo "::error::Trusted publisher not configured for ${#HARD_FAILED[@]} required package(s)."
echo "::error::Configure each below at the URL with these values:"
echo "::error:: Provider: GitHub Actions"
echo "::error:: Organization: code-yeongyu"
echo "::error:: Repository: ${REPO}"
echo "::error:: Workflow filename: ${WORKFLOW_FILE}"
echo
for pkg in "${HARD_FAILED[@]}"; do
echo "::error:: https://www.npmjs.com/package/${pkg}/access"
done
} >&2
exit 1
fi
if [ ${#SOFT_FAILED[@]} -gt 0 ]; then
{
echo
echo "::warning::Trusted publisher is not configured for lazycodex yet."
echo "::warning::First publish can require a one-time manual npm publish to claim the name."
echo "::warning::After claiming, configure GitHub Actions trusted publishing at:"
echo "::warning:: https://www.npmjs.com/package/lazycodex/access"
echo "::warning:: Organization: code-yeongyu"
echo "::warning:: Repository: ${REPO}"
echo "::warning:: Workflow filename: ${WORKFLOW_FILE}"
}
fi
{
echo
echo "::error::Trusted publisher not configured for ${#FAILED[@]} required package(s)."
echo "::error::Configure each below at the URL with these values:"
echo "::error:: Provider: GitHub Actions"
echo "::error:: Organization: code-yeongyu"
echo "::error:: Repository: ${REPO}"
echo "::error:: Workflow filename: ${WORKFLOW_FILE}"
echo
for pkg in "${FAILED[@]}"; do
echo "::error:: https://www.npmjs.com/package/${pkg}/access"
done
} >&2
exit 1
fi
echo
@@ -313,13 +281,14 @@ jobs:
fi
- name: Check if lazycodex already published
if: inputs.publish_lazycodex == true
id: check-lazycodex
env:
VERSION: ${{ needs.release-metadata.outputs.version }}
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/lazycodex/${LAZYCODEX_VERSION}")
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/lazycodex/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "✓ lazycodex@${LAZYCODEX_VERSION} already published"
echo "✓ lazycodex@${VERSION} already published"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
@@ -328,7 +297,7 @@ jobs:
if: >-
steps.check.outputs.skip != 'true' ||
steps.check-openagent.outputs.skip != 'true' ||
(inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true')
steps.check-lazycodex.outputs.skip != 'true'
env:
VERSION: ${{ needs.release-metadata.outputs.version }}
run: |
@@ -346,14 +315,14 @@ jobs:
if: >-
steps.check.outputs.skip != 'true' ||
steps.check-openagent.outputs.skip != 'true' ||
(inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true')
steps.check-lazycodex.outputs.skip != 'true'
run: bun run build
- name: Strip token auth from .npmrc to force OIDC
if: >-
steps.check.outputs.skip != 'true' ||
steps.check-openagent.outputs.skip != 'true' ||
(inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true')
steps.check-lazycodex.outputs.skip != 'true'
run: |
for f in .npmrc "$HOME/.npmrc"; do
if [ -f "$f" ]; then
@@ -372,7 +341,7 @@ jobs:
if [ -n "$DIST_TAG" ]; then
npm publish --access public --provenance --tag "$DIST_TAG" --loglevel verbose
else
npm publish --access public --provenance --loglevel verbose
npm publish --access public --provenance --tag latest --loglevel verbose
fi
- name: Publish oh-my-openagent
@@ -405,36 +374,26 @@ jobs:
git checkout -- package.json
- name: Publish lazycodex
if: inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'
continue-on-error: true
if: steps.check-lazycodex.outputs.skip != 'true'
env:
OMO_VERSION: ${{ needs.release-metadata.outputs.version }}
DIST_TAG: ${{ needs.release-metadata.outputs.dist_tag }}
NPM_CONFIG_PROVENANCE: true
run: |
jq --arg lazycodex_version "$LAZYCODEX_VERSION" --arg omo_version "$OMO_VERSION" '
jq --arg omo_version "$OMO_VERSION" '
.name = "lazycodex" |
.version = $lazycodex_version |
.version = $omo_version |
.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $omo_version) | from_entries)
' package.json > tmp.json && mv tmp.json package.json
set +e
if [ -n "$DIST_TAG" ]; then
npm publish --access public --provenance --tag "$DIST_TAG" --loglevel verbose
else
npm publish --access public --provenance --loglevel verbose
fi
PUBLISH_STATUS=$?
set -e
if [ "$PUBLISH_STATUS" -ne 0 ]; then
echo "::warning::lazycodex publish failed. If this is the first publish, claim it once manually from a trusted environment (with NPM_AUTH_TOKEN) using npm publish."
echo "::warning::Then configure GitHub Actions trusted publishing at https://www.npmjs.com/package/lazycodex/access with org=code-yeongyu repo=oh-my-openagent workflow=publish.yml."
exit 0
npm publish --access public --provenance --tag latest --loglevel verbose
fi
- name: Restore package.json after lazycodex publish attempt
if: always() && inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'
if: always() && steps.check-lazycodex.outputs.skip != 'true'
run: |
git checkout -- package.json
@@ -572,7 +531,7 @@ jobs:
fi
- name: Checkout LazyCodex marketplace
if: inputs.publish_lazycodex == true
if: inputs.sync_lazycodex_marketplace == true
uses: actions/checkout@v4
with:
repository: code-yeongyu/lazycodex
@@ -581,7 +540,7 @@ jobs:
fetch-depth: 0
- name: Sync LazyCodex Codex marketplace
if: inputs.publish_lazycodex == true
if: inputs.sync_lazycodex_marketplace == true
env:
VERSION: ${{ needs.release-metadata.outputs.version }}
run: |
+5 -34
View File
@@ -6,7 +6,6 @@ import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { basename } from "node:path";
import { fileURLToPath } from "node:url";
import {
getPlatformPackageCandidates,
getBinaryPath,
@@ -97,10 +96,13 @@ function getWrapperPackageName() {
* (e.g. `lazycodex` defaults to the Codex install flow).
* @returns {string}
*/
function getInvocationName() {
function getInvocationName(wrapperPackageName) {
if (process.env.OMO_INVOCATION_NAME) {
return process.env.OMO_INVOCATION_NAME;
}
if (getPackageBareName(wrapperPackageName) === "lazycodex") {
return "lazycodex";
}
const argv1 = process.argv[1] ?? "";
if (!argv1) {
return "oh-my-opencode";
@@ -108,42 +110,11 @@ function getInvocationName() {
return basename(argv1, ".js").replace(/\.exe$/, "");
}
function shouldRunBundledLazyCodexCli(packageName, invocationName) {
return getPackageBareName(packageName) === "lazycodex" && invocationName === "lazycodex";
}
function runBundledLazyCodexCli(invocationName) {
const cliPath = fileURLToPath(new URL("../dist/cli/index.js", import.meta.url));
const result = spawnSync("bun", [cliPath, ...process.argv.slice(2)], {
stdio: "inherit",
env: {
...process.env,
OMO_INVOCATION_NAME: invocationName,
},
});
if (result.error) {
console.error("\nlazycodex: Failed to execute bundled CLI with Bun.");
console.error("Install Bun or run through `bunx lazycodex`.");
console.error(`Error: ${result.error.message}\n`);
process.exit(2);
}
if (result.signal) {
process.exit(getSignalExitCode(result.signal));
}
process.exit(result.status ?? 1);
}
function main() {
const { platform, arch } = process;
const libcFamily = getLibcFamily();
const wrapperPackageName = getWrapperPackageName();
const invocationName = getInvocationName();
if (shouldRunBundledLazyCodexCli(wrapperPackageName, invocationName)) {
runBundledLazyCodexCli(invocationName);
}
const invocationName = getInvocationName(wrapperPackageName);
const packageBaseName = resolvePlatformPackageBaseName(wrapperPackageName);
const avx2Supported = supportsAvx2();
+71 -9
View File
@@ -1,9 +1,12 @@
/// <reference types="bun-types" />
import { afterEach, describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { chmod, cp, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { chmod, cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { getPlatformPackageCandidates } from "./platform.js";
const testRoots: string[] = [];
@@ -12,7 +15,7 @@ afterEach(async () => {
});
describe("lazycodex bin wrapper", () => {
test("runs the bundled Bun CLI instead of stale OMO platform binaries", async () => {
test("runs the platform binary so npx lazycodex does not require Bun", async () => {
// #given
const fixture = await createLazyCodexFixture();
const nodePath = Bun.which("node") ?? "node";
@@ -23,7 +26,7 @@ describe("lazycodex bin wrapper", () => {
env: {
...process.env,
CAPTURE_DIR: fixture.captureDir,
PATH: `${fixture.fakeBinDir}:${process.env.PATH ?? ""}`,
PATH: fixture.fakeBinDir,
},
});
@@ -31,13 +34,12 @@ describe("lazycodex bin wrapper", () => {
expect(result.status).toBe(23);
expect((await readFile(join(fixture.captureDir, "env"), "utf8")).trim()).toBe("lazycodex");
expect((await readFile(join(fixture.captureDir, "args"), "utf8")).trim().split("\n")).toEqual([
await realpath(fixture.bundledCli),
"install",
"--no-tui",
]);
});
test("runs the bundled Bun CLI when published under an npm scope", async () => {
test("runs the platform binary when published under an npm scope", async () => {
// #given
const fixture = await createLazyCodexFixture({ packageName: "@code-yeongyu/lazycodex" });
const nodePath = Bun.which("node") ?? "node";
@@ -48,7 +50,31 @@ describe("lazycodex bin wrapper", () => {
env: {
...process.env,
CAPTURE_DIR: fixture.captureDir,
PATH: `${fixture.fakeBinDir}:${process.env.PATH ?? ""}`,
PATH: fixture.fakeBinDir,
},
});
// #then
expect(result.status).toBe(23);
expect((await readFile(join(fixture.captureDir, "env"), "utf8")).trim()).toBe("lazycodex");
expect((await readFile(join(fixture.captureDir, "args"), "utf8")).trim().split("\n")).toEqual([
"install",
"--no-tui",
]);
});
test("routes npm shim execution from the lazycodex package to the Codex installer", async () => {
// #given
const fixture = await createLazyCodexFixture({ wrapperFileName: "oh-my-opencode.js" });
const nodePath = Bun.which("node") ?? "node";
// #when
const result = spawnSync(nodePath, [fixture.wrapperBin, "install", "--no-tui"], {
encoding: "utf8",
env: {
...process.env,
CAPTURE_DIR: fixture.captureDir,
PATH: fixture.fakeBinDir,
},
});
@@ -56,14 +82,13 @@ describe("lazycodex bin wrapper", () => {
expect(result.status).toBe(23);
expect((await readFile(join(fixture.captureDir, "env"), "utf8")).trim()).toBe("lazycodex");
expect((await readFile(join(fixture.captureDir, "args"), "utf8")).trim().split("\n")).toEqual([
await realpath(fixture.bundledCli),
"install",
"--no-tui",
]);
});
});
async function createLazyCodexFixture(options: { packageName?: string } = {}) {
async function createLazyCodexFixture(options: { packageName?: string; wrapperFileName?: string } = {}) {
const root = await mkdtemp(join(tmpdir(), "lazycodex-bin-wrapper-"));
testRoots.push(root);
@@ -76,10 +101,16 @@ async function createLazyCodexFixture(options: { packageName?: string } = {}) {
await mkdir(fakeBinDir, { recursive: true });
await mkdir(captureDir, { recursive: true });
await cp(fileURLToPath(new URL("./oh-my-opencode.js", import.meta.url)), join(binDir, "lazycodex"));
const wrapperFileName = options.wrapperFileName ?? "lazycodex";
const wrapperBin = join(binDir, wrapperFileName);
await cp(fileURLToPath(new URL("./oh-my-opencode.js", import.meta.url)), wrapperBin);
if (wrapperFileName !== "lazycodex") {
await symlink(wrapperFileName, join(binDir, "lazycodex"));
}
await cp(fileURLToPath(new URL("./platform.js", import.meta.url)), join(binDir, "platform.js"));
await writeFile(join(root, "package.json"), JSON.stringify({ name: options.packageName ?? "lazycodex", type: "module" }));
await writeFile(distCli, "#!/usr/bin/env bun\n");
await writePlatformPackages(root);
const fakeBun = join(fakeBinDir, "bun");
await writeFile(
@@ -99,5 +130,36 @@ async function createLazyCodexFixture(options: { packageName?: string } = {}) {
captureDir,
fakeBinDir,
lazycodexBin: join(binDir, "lazycodex"),
wrapperBin,
};
}
async function writePlatformPackages(root: string): Promise<void> {
const packages = getPlatformPackageCandidates({
platform: process.platform,
arch: process.arch,
libcFamily: process.platform === "linux" ? "glibc" : undefined,
packageBaseName: "oh-my-opencode",
});
for (const packageName of packages) {
const binaryPath = join(root, "node_modules", packageName, "bin", process.platform === "win32" ? "oh-my-opencode.exe" : "oh-my-opencode");
await mkdir(dirname(binaryPath), { recursive: true });
await writeFile(
binaryPath,
[
"#!/bin/sh",
"printf '%s\\n' \"$OMO_INVOCATION_NAME\" > \"$CAPTURE_DIR/env\"",
"printf '%s\\n' \"$@\" > \"$CAPTURE_DIR/args\"",
"exit 23",
"",
].join("\n"),
);
await chmod(binaryPath, 0o755);
}
if (process.platform === "linux") {
const detectLibcPath = join(root, "node_modules", "detect-libc", "index.js");
await mkdir(dirname(detectLibcPath), { recursive: true });
await writeFile(detectLibcPath, 'exports.familySync = () => "glibc";\n');
}
}
+23 -12
View File
@@ -155,7 +155,7 @@ describe("test workflows", () => {
expect(marketplacePushSkipsWhenClean, "marketplace sync must skip push when rerun has no changes").toBe(true)
})
test("keeps LazyCodex deployment behind an explicit publish flag", () => {
test("publishes the LazyCodex npm alias on every release while keeping marketplace sync explicit", () => {
// #given
const workflow = readFileSync(publishWorkflowPath, "utf8")
@@ -163,8 +163,8 @@ describe("test workflows", () => {
const keepsCodexPluginVersionIndependent =
!workflow.includes("jq --arg v \"$VERSION\" '.version = $v' packages/omo-codex/plugin/.codex-plugin/plugin.json") &&
!workflow.includes("jq --arg v \"$VERSION\" '.version = $v' packages/omo-codex/plugin/package.json")
const flagDefaultsOff = workflow.includes("publish_lazycodex:") &&
workflow.includes('description: "Publish lazycodex npm alias and sync Codex marketplace"') &&
const flagDefaultsOff = workflow.includes("sync_lazycodex_marketplace:") &&
workflow.includes('description: "Sync the LazyCodex Codex marketplace repository"') &&
workflow.includes("default: false")
const syncsLazycodexMarketplace = workflow.includes("bun run script/sync-lazycodex-marketplace.ts")
const syncBuildsMcpDists =
@@ -175,10 +175,18 @@ describe("test workflows", () => {
workflow.includes("bun run --cwd packages/omo-codex/plugin build") &&
workflow.indexOf("bun run --cwd packages/omo-codex/plugin build") < workflow.indexOf("bun run script/sync-lazycodex-marketplace.ts")
const pushesLazycodexMarketplace = workflow.includes("code-yeongyu/lazycodex")
const gatesLazycodexNpmPublish = workflow.includes("name: Publish lazycodex") &&
workflow.includes("if: inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'")
const publishLazycodexStep = workflow.slice(
workflow.indexOf("name: Publish lazycodex"),
workflow.indexOf("name: Restore package.json after lazycodex publish attempt"),
)
const alwaysChecksLazycodexNpm = workflow.includes("name: Check if lazycodex already published") &&
workflow.includes('https://registry.npmjs.org/lazycodex/${VERSION}')
const publishesLazycodexNpm = publishLazycodexStep.includes("name: Publish lazycodex") &&
publishLazycodexStep.includes("if: steps.check-lazycodex.outputs.skip != 'true'") &&
publishLazycodexStep.includes("npm publish --access public --provenance --tag latest --loglevel verbose") &&
!publishLazycodexStep.includes("continue-on-error: true")
const gatesLazycodexMarketplaceSync = workflow.includes("name: Sync LazyCodex Codex marketplace") &&
workflow.includes("if: inputs.publish_lazycodex == true")
workflow.includes("if: inputs.sync_lazycodex_marketplace == true")
const tokenRequirementBeforePublish = workflow.indexOf("name: Require LazyCodex sync token") <
workflow.indexOf("publish-main:")
const requiresLazycodexSyncToken = workflow.includes("LAZYCODEX_SYNC_TOKEN: ${{ secrets.LAZYCODEX_SYNC_TOKEN }}") &&
@@ -187,13 +195,14 @@ describe("test workflows", () => {
// #then
expect(keepsCodexPluginVersionIndependent, "LazyCodex plugin metadata must keep its own 0.1.0 version").toBe(true)
expect(flagDefaultsOff, "LazyCodex deployment must default to disabled").toBe(true)
expect(flagDefaultsOff, "LazyCodex marketplace sync must default to disabled").toBe(true)
expect(syncsLazycodexMarketplace, "release must sync the LazyCodex marketplace bundle").toBe(true)
expect(syncBuildsMcpDists, "release must build bundled MCP dists before LazyCodex marketplace sync").toBe(true)
expect(syncBuildsCodexPlugin, "release must build the aggregate Codex plugin before LazyCodex marketplace sync").toBe(true)
expect(pushesLazycodexMarketplace, "release must target the LazyCodex repository").toBe(true)
expect(gatesLazycodexNpmPublish, "lazycodex npm publish must require publish_lazycodex=true").toBe(true)
expect(gatesLazycodexMarketplaceSync, "LazyCodex marketplace push must require publish_lazycodex=true").toBe(true)
expect(alwaysChecksLazycodexNpm, "release must always check lazycodex using the release version").toBe(true)
expect(publishesLazycodexNpm, "lazycodex npm publish must be part of the normal release, tag stable releases as latest, and fail loudly").toBe(true)
expect(gatesLazycodexMarketplaceSync, "LazyCodex marketplace push must require sync_lazycodex_marketplace=true").toBe(true)
expect(requiresLazycodexSyncToken, "release must require a cross-repo token for LazyCodex push").toBe(true)
})
@@ -203,8 +212,10 @@ describe("test workflows", () => {
const platformResolver = readFileSync(new URL("../bin/platform.js", import.meta.url), "utf8")
// #when
const lazycodexStepPinsWrapperVersion = workflow.includes('LAZYCODEX_VERSION: "0.1.0"') &&
workflow.includes(".version = $lazycodex_version |")
const lazycodexStepUsesReleaseVersion =
!workflow.includes('LAZYCODEX_VERSION: "0.1.0"') &&
workflow.includes(".name = \"lazycodex\" |") &&
workflow.includes(".version = $omo_version |")
const lazycodexStepUsesOmoPlatformVersion = workflow.includes(
".optionalDependencies = (.optionalDependencies | to_entries | map(.value = $omo_version) | from_entries)",
)
@@ -213,7 +224,7 @@ describe("test workflows", () => {
platformResolver.includes("lazycodex") && platformResolver.includes("oh-my-opencode")
// #then
expect(lazycodexStepPinsWrapperVersion, "lazycodex publish step must pin wrapper metadata to 0.1.0").toBe(true)
expect(lazycodexStepUsesReleaseVersion, "lazycodex publish step must use the release version so unpublished versions are not reused").toBe(true)
expect(lazycodexStepUsesOmoPlatformVersion, "lazycodex must depend on the matching OMO platform packages").toBe(true)
expect(lazycodexStepDoesNotRenameOptionalDeps, "lazycodex publish step must keep optionalDependencies on published platform packages").toBe(true)
expect(shimMapsLazycodexToPublishedPlatformFamily, "platform resolver must map lazycodex to the real published platform package family").toBe(true)