Files
oh-my-opencode/script/publish-workflow.test.ts
T

222 lines
12 KiB
TypeScript
Raw Normal View History

/// <reference types="bun-types" />
2026-04-04 01:51:48 +09:00
import { describe, expect, test } from "bun:test"
2026-05-30 19:12:06 +09:00
import { readFileSync, readdirSync } from "node:fs"
const ciWorkflowPath = new URL("../.github/workflows/ci.yml", import.meta.url)
const publishWorkflowPath = new URL("../.github/workflows/publish.yml", import.meta.url)
const publishPlatformWorkflowPath = new URL("../.github/workflows/publish-platform.yml", import.meta.url)
const workflowsDir = new URL("../.github/workflows/", import.meta.url)
const pinnedBunVersion = 'bun-version: "1.3.12"'
const workflowPaths = readdirSync(workflowsDir)
.filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
.map((name) => new URL(name, workflowsDir))
2026-04-04 01:51:48 +09:00
2026-05-15 11:41:35 +09:00
const workflowChecks = [
{
2026-05-30 19:12:06 +09:00
path: ciWorkflowPath,
2026-05-15 11:41:35 +09:00
testRuns: [
2026-05-15 16:26:57 +09:00
"run: bun test",
2026-05-15 11:41:35 +09:00
"run: bun test src/shared/dist-bundle-bun-globals.test.ts",
],
},
{
2026-05-30 19:12:06 +09:00
path: publishWorkflowPath,
2026-05-15 16:26:57 +09:00
testRuns: ["run: bun test"],
2026-05-15 11:41:35 +09:00
},
2026-04-04 01:51:48 +09:00
]
2026-05-30 19:12:06 +09:00
function sliceWorkflowSection(workflow: string, startMarker: string, endMarker: string): string {
const start = workflow.indexOf(startMarker)
const end = workflow.indexOf(endMarker, start)
if (start < 0 || end < 0 || end <= start) {
throw new Error(`missing workflow section between ${startMarker} and ${endMarker}`)
}
return workflow.slice(start, end)
}
2026-04-04 01:51:48 +09:00
describe("test workflows", () => {
2026-04-06 18:45:33 +09:00
test("use pure bun test for workflows", () => {
2026-05-15 11:41:35 +09:00
for (const workflowCheck of workflowChecks) {
// #given
2026-05-15 11:41:35 +09:00
const workflow = readFileSync(workflowCheck.path, "utf8")
2026-04-04 01:51:48 +09:00
2026-05-15 11:41:35 +09:00
for (const testRun of workflowCheck.testRuns) {
expect(workflow).toContain(testRun)
}
2026-04-04 01:51:48 +09:00
}
})
2026-05-30 19:12:06 +09:00
test("exercise root checks across linux macos and windows", () => {
// #given
const workflow = readFileSync(ciWorkflowPath, "utf8")
// #when
const hasCrossOsMatrix = workflow.includes("os: [ubuntu-latest, macos-latest, windows-latest]")
const hasMatrixRunner = workflow.includes("runs-on: ${{ matrix.os }}")
// #then
expect(hasCrossOsMatrix, "CI root checks must cover Linux, macOS, and Windows").toBe(true)
expect(hasMatrixRunner, "CI root checks must run on the selected matrix OS").toBe(true)
})
test("runs codex compatibility checks on every supported os", () => {
// #given
const workflow = readFileSync(ciWorkflowPath, "utf8")
// #when
const hasCodexMatrixJob = workflow.includes("codex-compatibility:")
const hasCodexCommand = workflow.includes("run: bun run test:codex")
const buildNeedsCodexMatrix = workflow.includes("needs: [test, typecheck, codex-compatibility]")
// #then
expect(hasCodexMatrixJob, "CI must expose a Codex compatibility matrix job").toBe(true)
expect(hasCodexCommand, "Codex compatibility job must run the shared Codex test script").toBe(true)
expect(buildNeedsCodexMatrix, "Build must wait for Codex compatibility checks").toBe(true)
})
test("pins every workflow Bun setup to the tested runtime", () => {
for (const workflowPath of workflowPaths) {
// #given
const workflow = readFileSync(workflowPath, "utf8")
const bunVersionLines = workflow.match(/bun-version: .*/g) ?? []
// #when
const unpinnedBunLines = bunVersionLines.filter((line) => line !== pinnedBunVersion)
// #then
expect(unpinnedBunLines, `${workflowPath.pathname} must pin Bun to 1.3.12`).toEqual([])
}
})
test("publishes platform packages before installable wrappers", () => {
// #given
const workflow = readFileSync(publishWorkflowPath, "utf8")
// #when
const computesReleaseMetadata = workflow.includes("release-metadata:") &&
workflow.includes("outputs:") &&
workflow.includes("version: ${{ steps.version.outputs.version }}") &&
workflow.includes("dist_tag: ${{ steps.version.outputs.dist_tag }}")
const computesVersionOnce = (workflow.match(/id: version/g) ?? []).length === 1
const platformUsesMetadata = workflow.includes("version: ${{ needs.release-metadata.outputs.version }}") &&
workflow.includes("dist_tag: ${{ needs.release-metadata.outputs.dist_tag }}")
const mainWaitsForPlatform = workflow.includes("needs: [test, typecheck, preflight-trust, release-metadata, publish-platform]") &&
workflow.includes("inputs.skip_platform == true || needs.publish-platform.result == 'success'")
const releaseUsesMetadata = workflow.includes("VERSION: ${{ needs.release-metadata.outputs.version }}")
const wrappersVerifyPlatformPackages = workflow.includes("name: Verify platform packages are published") &&
workflow.includes("Missing platform package(s); refusing to publish wrappers.")
// #then
expect(computesReleaseMetadata, "release metadata must be a first-class job output").toBe(true)
expect(computesVersionOnce, "version and dist tag must be computed exactly once").toBe(true)
expect(platformUsesMetadata, "platform workflow must consume the shared release metadata").toBe(true)
expect(mainWaitsForPlatform, "wrapper publish must wait for platform success unless pre-published platforms are explicitly verified").toBe(true)
expect(releaseUsesMetadata, "release tail must use the shared release metadata").toBe(true)
expect(wrappersVerifyPlatformPackages, "wrappers must verify matching platform binaries exist before npm publish").toBe(true)
})
test("fails when a required platform artifact is missing", () => {
// #given
const workflow = readFileSync(publishPlatformWorkflowPath, "utf8")
// #when
const downloadStep = sliceWorkflowSection(
workflow,
" - name: Download artifact",
" - name: Extract artifact",
)
const downloadsWhenPublishNeeded = downloadStep.includes("if: steps.check.outputs.skip_all != 'true'")
const suppressesDownloadFailure = downloadStep.includes("continue-on-error: true")
// #then
expect(downloadsWhenPublishNeeded, "publish job must download artifacts for packages that still need publishing").toBe(true)
expect(suppressesDownloadFailure, "missing required artifacts must fail the reusable publish workflow").toBe(false)
})
test("keeps the release tail safe to rerun after a tag exists", () => {
// #given
const workflow = readFileSync(publishWorkflowPath, "utf8")
// #when
const hasExistingTagResolver = workflow.includes("id: release-state") &&
workflow.includes("tag_exists=true") &&
workflow.includes('git checkout --detach "v${VERSION}"')
const tagStepSkipsExistingTag = workflow.includes("if: steps.release-state.outputs.tag_exists != 'true'")
const noHardExistingTagFailure = !workflow.includes("Tag v${VERSION} already exists")
const pushSkipsExistingRemoteTag = workflow.includes("Release tag v${VERSION} already exists on origin")
const marketplacePushSkipsWhenClean = workflow.includes("LazyCodex marketplace already up to date")
// #then
expect(hasExistingTagResolver, "release must detect and reuse an existing tag on rerun").toBe(true)
expect(tagStepSkipsExistingTag, "tag creation must skip when the release tag already exists").toBe(true)
expect(noHardExistingTagFailure, "existing tags must not make reruns fail").toBe(true)
expect(pushSkipsExistingRemoteTag, "tag push must be skip-if-exists").toBe(true)
expect(marketplacePushSkipsWhenClean, "marketplace sync must skip push when rerun has no changes").toBe(true)
})
test("keeps LazyCodex deployment behind an explicit publish flag", () => {
// #given
const workflow = readFileSync(publishWorkflowPath, "utf8")
// #when
2026-05-30 21:32:57 +09:00
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")
2026-05-30 19:12:06 +09:00
const flagDefaultsOff = workflow.includes("publish_lazycodex:") &&
workflow.includes('description: "Publish lazycodex npm alias and sync Codex marketplace"') &&
workflow.includes("default: false")
const syncsLazycodexMarketplace = workflow.includes("bun run script/sync-lazycodex-marketplace.ts")
const syncBuildsMcpDists =
workflow.includes("bun run build:ast-grep-mcp") &&
workflow.includes("bun run build:lsp-tools-mcp") &&
workflow.indexOf("bun run build:lsp-tools-mcp") < workflow.indexOf("bun run script/sync-lazycodex-marketplace.ts")
const syncBuildsCodexPlugin =
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 gatesLazycodexMarketplaceSync = workflow.includes("name: Sync LazyCodex Codex marketplace") &&
workflow.includes("if: inputs.publish_lazycodex == 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 }}") &&
workflow.includes("token: ${{ secrets.LAZYCODEX_SYNC_TOKEN }}") &&
tokenRequirementBeforePublish
// #then
2026-05-30 21:32:57 +09:00
expect(keepsCodexPluginVersionIndependent, "LazyCodex plugin metadata must keep its own 0.1.0 version").toBe(true)
2026-05-30 19:12:06 +09:00
expect(flagDefaultsOff, "LazyCodex deployment 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(requiresLazycodexSyncToken, "release must require a cross-repo token for LazyCodex push").toBe(true)
})
test("keeps lazycodex platform dependencies aligned with shim resolution", () => {
// #given
const workflow = readFileSync(publishWorkflowPath, "utf8")
const platformResolver = readFileSync(new URL("../bin/platform.js", import.meta.url), "utf8")
// #when
2026-05-30 21:32:57 +09:00
const lazycodexStepPinsWrapperVersion = workflow.includes('LAZYCODEX_VERSION: "0.1.0"') &&
workflow.includes(".version = $lazycodex_version |")
const lazycodexStepUsesOmoPlatformVersion = workflow.includes(
".optionalDependencies = (.optionalDependencies | to_entries | map(.value = $omo_version) | from_entries)",
)
2026-05-30 19:12:06 +09:00
const lazycodexStepDoesNotRenameOptionalDeps = !workflow.includes('sub("^oh-my-opencode-"; "lazycodex-")')
const shimMapsLazycodexToPublishedPlatformFamily =
platformResolver.includes("lazycodex") && platformResolver.includes("oh-my-opencode")
// #then
2026-05-30 21:32:57 +09:00
expect(lazycodexStepPinsWrapperVersion, "lazycodex publish step must pin wrapper metadata to 0.1.0").toBe(true)
expect(lazycodexStepUsesOmoPlatformVersion, "lazycodex must depend on the matching OMO platform packages").toBe(true)
2026-05-30 19:12:06 +09:00
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)
})
2026-04-04 01:51:48 +09:00
})