name: publish run-name: "${{ format('release {0}', inputs.version || inputs.bump) }}" on: workflow_dispatch: inputs: bump: description: "Bump major, minor, or patch" required: true type: choice default: patch options: - patch - minor - major version: description: "Override version (e.g., 3.0.0-beta.6). Takes precedence over bump." required: false type: string skip_platform: description: "Skip platform binary packages" required: false type: boolean default: false sync_lazycodex_marketplace: description: "Sync the LazyCodex Codex marketplace repository" required: false type: boolean default: false publish_lazycodex: description: "Publish the lazycodex npm alias" required: false type: boolean default: true concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write id-token: write actions: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: "24" - name: Build lsp-tools-mcp submodule run: npm ci && npm run build working-directory: packages/lsp-tools-mcp - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.12" - name: Install dependencies run: bun install --frozen-lockfile env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/cli @ast-grep/napi" - name: Run tests run: bun test typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: "24" - name: Build lsp-tools-mcp submodule run: npm ci && npm run build working-directory: packages/lsp-tools-mcp - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.12" - name: Install dependencies run: bun install --frozen-lockfile env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/cli @ast-grep/napi" - name: Type check run: bun run typecheck preflight-trust: runs-on: ubuntu-latest if: github.repository == 'code-yeongyu/oh-my-openagent' permissions: id-token: write contents: read steps: - name: Require LazyCodex sync token if: inputs.sync_lazycodex_marketplace == true env: LAZYCODEX_SYNC_TOKEN: ${{ secrets.LAZYCODEX_SYNC_TOKEN }} run: | if [ -z "$LAZYCODEX_SYNC_TOKEN" ]; then echo "::error::LAZYCODEX_SYNC_TOKEN is required to push the Codex marketplace bundle to code-yeongyu/lazycodex." exit 1 fi - name: Verify trusted publisher for release packages 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" \ | jq -r '.value // empty') if [ -z "${OIDC_TOKEN}" ]; then echo "::error::Failed to acquire GitHub OIDC token" exit 1 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 for plat in "${PLATFORMS[@]}"; do ALL_PACKAGES+=("oh-my-opencode-${plat}") ALL_PACKAGES+=("oh-my-openagent-${plat}") done FAILED=() for pkg in "${ALL_PACKAGES[@]}"; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST \ "https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/${pkg}" \ -H "Authorization: Bearer ${OIDC_TOKEN}" \ -H "Content-Type: application/json" \ -d '{}') # npm returns 200 or 201 when trusted publisher is configured (token issued). # 404 means the package has no trusted publisher mapping for this workflow. if [ "${STATUS}" -ge 200 ] && [ "${STATUS}" -lt 300 ]; then echo "OK ${pkg}" else echo "FAIL ${pkg} (HTTP ${STATUS})" FAILED+=("${pkg}") fi done if [ ${#FAILED[@]} -gt 0 ]; then { 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 echo "All ${#ALL_PACKAGES[@]} packages have trusted publisher configured." release-metadata: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} dist_tag: ${{ steps.version.outputs.dist_tag }} steps: - name: Calculate version id: version env: RAW_VERSION: ${{ inputs.version }} BUMP: ${{ inputs.bump }} run: | VERSION="$RAW_VERSION" if [ -z "$VERSION" ]; then PREV=$(curl -s https://registry.npmjs.org/oh-my-opencode/latest | jq -r '.version // "0.0.0"') BASE="${PREV%%-*}" IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE" case "$BUMP" in major) VERSION="$((MAJOR+1)).0.0" ;; minor) VERSION="${MAJOR}.$((MINOR+1)).0" ;; *) VERSION="${MAJOR}.${MINOR}.$((PATCH+1))" ;; esac fi if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]]; then echo "::error::Invalid version: $VERSION" exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" if [[ "$VERSION" == *"-"* ]]; then DIST_TAG=$(printf '%s' "$VERSION" | cut -d'-' -f2 | cut -d'.' -f1) if ! [[ "$DIST_TAG" =~ ^[a-z][a-z0-9-]*$ ]]; then echo "::error::Invalid dist_tag: $DIST_TAG" exit 1 fi echo "dist_tag=${DIST_TAG:-next}" >> "$GITHUB_OUTPUT" else echo "dist_tag=" >> "$GITHUB_OUTPUT" fi echo "Version: $VERSION" publish-main: runs-on: ubuntu-latest needs: [test, typecheck, preflight-trust, release-metadata, publish-platform] if: >- always() && github.repository == 'code-yeongyu/oh-my-openagent' && needs.test.result == 'success' && needs.typecheck.result == 'success' && needs.preflight-trust.result == 'success' && needs.release-metadata.result == 'success' && (inputs.skip_platform == true || needs.publish-platform.result == 'success') steps: - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive - run: git fetch --force --tags - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.12" - uses: actions/setup-node@v6 with: node-version: "24" registry-url: "https://registry.npmjs.org" - name: Upgrade npm for trusted publishing (>=11.5.1) run: npm install -g npm@latest - name: Install dependencies run: bun install --frozen-lockfile env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/cli @ast-grep/napi" - name: Verify platform packages are published env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | 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) FAILED=() for platform in "${PLATFORMS[@]}"; do for family in oh-my-opencode oh-my-openagent; do pkg="${family}-${platform}" STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/${pkg}/${VERSION}") if [ "$STATUS" = "200" ]; then echo "OK ${pkg}@${VERSION}" else echo "MISS ${pkg}@${VERSION} (HTTP ${STATUS})" FAILED+=("${pkg}") fi done done if [ ${#FAILED[@]} -gt 0 ]; then echo "::error::Missing platform package(s); refusing to publish wrappers." for pkg in "${FAILED[@]}"; do echo "::error:: ${pkg}@${VERSION}" done exit 1 fi - name: Check if already published id: check env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-opencode/${VERSION}") if [ "$STATUS" = "200" ]; then echo "skip=true" >> "$GITHUB_OUTPUT" echo "✓ oh-my-opencode@${VERSION} already published" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Check if oh-my-openagent already published id: check-openagent env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-openagent/${VERSION}") if [ "$STATUS" = "200" ]; then echo "skip=true" >> "$GITHUB_OUTPUT" echo "✓ oh-my-openagent@${VERSION} already published" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Check if lazycodex already published id: check-lazycodex if: inputs.publish_lazycodex == true env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | 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@${VERSION} already published" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Update version if: >- steps.check.outputs.skip != 'true' || steps.check-openagent.outputs.skip != 'true' || (inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true') env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json for platform in 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; do package_dir="packages/oh-my-opencode-${platform}" jq --arg v "$VERSION" '.version = $v' "${package_dir}/package.json" > tmp.json mv tmp.json "${package_dir}/package.json" done jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json - name: Build main package if: >- steps.check.outputs.skip != 'true' || steps.check-openagent.outputs.skip != 'true' || (inputs.publish_lazycodex == 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') run: | for f in .npmrc "$HOME/.npmrc"; do if [ -f "$f" ]; then sed -i.bak '/_authToken/d' "$f" rm -f "$f.bak" echo "Cleaned $f" fi done - name: Publish oh-my-opencode if: steps.check.outputs.skip != 'true' env: DIST_TAG: ${{ needs.release-metadata.outputs.dist_tag }} NPM_CONFIG_PROVENANCE: true run: | if [ -n "$DIST_TAG" ]; then npm publish --access public --provenance --tag "$DIST_TAG" --loglevel verbose else npm publish --access public --provenance --tag latest --loglevel verbose fi - name: Publish oh-my-openagent if: steps.check-openagent.outputs.skip != 'true' env: VERSION: ${{ needs.release-metadata.outputs.version }} DIST_TAG: ${{ needs.release-metadata.outputs.dist_tag }} NPM_CONFIG_PROVENANCE: true run: | # Update package name, version, and optionalDependencies for oh-my-openagent jq --arg v "$VERSION" ' .name = "oh-my-openagent" | .version = $v | .optionalDependencies = ( .optionalDependencies | to_entries | map(.key = (.key | sub("^oh-my-opencode-"; "oh-my-openagent-")) | .value = $v) | from_entries ) ' package.json > tmp.json && mv tmp.json package.json if [ -n "$DIST_TAG" ]; then npm publish --access public --provenance --tag "$DIST_TAG" --loglevel verbose else npm publish --access public --provenance --loglevel verbose fi - name: Restore package.json if: always() && steps.check-openagent.outputs.skip != 'true' run: | git checkout -- package.json - name: Publish lazycodex if: inputs.publish_lazycodex == true && 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 omo_version "$OMO_VERSION" ' .name = "lazycodex" | .version = $omo_version | .optionalDependencies = ( .optionalDependencies | to_entries | map(.key = (.key | sub("^oh-my-opencode-"; "oh-my-openagent-")) | .value = $omo_version) | from_entries ) ' package.json > tmp.json && mv tmp.json package.json if [ -n "$DIST_TAG" ]; then npm publish --access public --provenance --tag "$DIST_TAG" --loglevel verbose else 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' run: | git checkout -- package.json publish-platform: needs: [test, typecheck, preflight-trust, release-metadata] if: >- always() && github.repository == 'code-yeongyu/oh-my-openagent' && inputs.skip_platform != true && needs.test.result == 'success' && needs.typecheck.result == 'success' && needs.preflight-trust.result == 'success' && needs.release-metadata.result == 'success' uses: ./.github/workflows/publish-platform.yml with: version: ${{ needs.release-metadata.outputs.version }} dist_tag: ${{ needs.release-metadata.outputs.dist_tag }} secrets: inherit release: runs-on: ubuntu-latest needs: [release-metadata, publish-main, publish-platform] if: >- always() && needs.release-metadata.result == 'success' && needs.publish-main.result == 'success' && (inputs.skip_platform == true || needs.publish-platform.result == 'success') steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - run: git fetch --force --tags - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.12" - name: Install dependencies run: bun install --frozen-lockfile env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/cli @ast-grep/napi" - name: Generate changelog run: | bun run script/generate-changelog.ts > /tmp/changelog.md cat /tmp/changelog.md env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Resolve release state id: release-state env: VERSION: ${{ needs.release-metadata.outputs.version }} RELEASE_REF: ${{ github.ref_name }} run: | if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then echo "tag_exists=true" >> "$GITHUB_OUTPUT" echo "release_commit_exists=true" >> "$GITHUB_OUTPUT" git checkout --detach "v${VERSION}" exit 0 fi echo "tag_exists=false" >> "$GITHUB_OUTPUT" RELEASE_SHA="" if [ -n "$RELEASE_REF" ]; then RELEASE_SHA=$(git rev-list --max-count=1 --grep="^release: v${VERSION}$" "origin/${RELEASE_REF}" 2>/dev/null || true) fi if [ -n "$RELEASE_SHA" ]; then echo "release_commit_exists=true" >> "$GITHUB_OUTPUT" git checkout --detach "$RELEASE_SHA" else echo "release_commit_exists=false" >> "$GITHUB_OUTPUT" fi - name: Apply release version to source tree if: steps.release-state.outputs.release_commit_exists != 'true' env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json for platform in 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; do package_dir="packages/oh-my-opencode-${platform}" jq --arg v "$VERSION" '.version = $v' "${package_dir}/package.json" > tmp.json mv tmp.json "${package_dir}/package.json" done jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json - name: Commit version bump if: steps.release-state.outputs.release_commit_exists != 'true' env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" git add package.json packages/oh-my-opencode-*/package.json git diff --cached --quiet || git commit -m "release: v${VERSION}" - name: Create release tag if: steps.release-state.outputs.tag_exists != 'true' env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then echo "Release tag v${VERSION} already exists locally" else git tag "v${VERSION}" fi - name: Push release state if: steps.release-state.outputs.tag_exists != 'true' env: VERSION: ${{ needs.release-metadata.outputs.version }} RELEASE_REF: ${{ github.ref_name }} RELEASE_COMMIT_EXISTS: ${{ steps.release-state.outputs.release_commit_exists }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ "$RELEASE_COMMIT_EXISTS" != "true" ]; then if [ -z "$RELEASE_REF" ]; then echo "::error::Cannot push release commit because github.ref_name is empty." exit 1 fi git push origin "HEAD:${RELEASE_REF}" else echo "Release commit already exists on origin/${RELEASE_REF}" fi if git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" >/dev/null 2>&1; then echo "Release tag v${VERSION} already exists on origin" else git push origin "v${VERSION}" fi - name: Checkout LazyCodex marketplace if: inputs.sync_lazycodex_marketplace == true uses: actions/checkout@v4 with: repository: code-yeongyu/lazycodex path: lazycodex-marketplace token: ${{ secrets.LAZYCODEX_SYNC_TOKEN }} fetch-depth: 0 - name: Sync LazyCodex Codex marketplace if: inputs.sync_lazycodex_marketplace == true env: VERSION: ${{ needs.release-metadata.outputs.version }} run: | npm --prefix packages/omo-codex/plugin ci bun run --cwd packages/omo-codex/plugin build bun run build:ast-grep-mcp bun run build:lsp-tools-mcp bun run script/sync-lazycodex-marketplace.ts "$GITHUB_WORKSPACE" "$GITHUB_WORKSPACE/lazycodex-marketplace" cd "$GITHUB_WORKSPACE/lazycodex-marketplace" git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" git add .agents/plugins/marketplace.json plugins/omo if git diff --cached --quiet; then echo "LazyCodex marketplace already up to date" else git commit -m "chore: sync Codex marketplace v${VERSION}" git push origin HEAD:main fi - name: Create GitHub release env: VERSION: ${{ needs.release-metadata.outputs.version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release view "v${VERSION}" >/dev/null 2>&1 || \ gh release create "v${VERSION}" --title "v${VERSION}" --notes-file /tmp/changelog.md - name: Delete draft release run: gh release delete next --yes 2>/dev/null || true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Merge to master continue-on-error: true env: VERSION: ${{ needs.release-metadata.outputs.version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git stash --include-untracked || true git checkout master git reset --hard "v${VERSION}" git push -f origin master || echo "::warning::Failed to push to master"