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 concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write id-token: write actions: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install dependencies run: bun install env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi" - name: Run mock-heavy tests (isolated) run: | # These files use mock.module() which pollutes module cache # Run them in separate processes to prevent cross-file contamination bun test src/plugin-handlers bun test src/hooks/atlas bun test src/hooks/compaction-context-injector bun test src/features/tmux-subagent bun test src/cli/doctor/formatter.test.ts bun test src/cli/doctor/format-default.test.ts bun test src/tools/call-omo-agent/sync-executor.test.ts bun test src/tools/call-omo-agent/session-creator.test.ts bun test src/tools/session-manager bun test src/features/opencode-skill-loader/loader.test.ts bun test src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts bun test src/hooks/anthropic-context-window-limit-recovery/executor.test.ts # src/shared mock-heavy files (mock.module pollutes connected-providers-cache and legacy-plugin-warning) bun test src/shared/model-capabilities.test.ts bun test src/shared/log-legacy-plugin-startup-warning.test.ts bun test src/shared/model-error-classifier.test.ts bun test src/shared/opencode-message-dir.test.ts # session-recovery mock isolation (recover-tool-result-missing mocks ./storage) bun test src/hooks/session-recovery/recover-tool-result-missing.test.ts # legacy-plugin-toast mock isolation (hook.test.ts mocks ./auto-migrate) bun test src/hooks/legacy-plugin-toast/hook.test.ts - name: Run remaining tests run: | # Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files # that were already run in isolation above. # Excluded from src/shared: model-capabilities, log-legacy-plugin-startup-warning, model-error-classifier, opencode-message-dir # Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts # Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts, session-manager (all) # Excluded from src/hooks/anthropic-context-window-limit-recovery: recovery-hook.test.ts, executor.test.ts # Build src/shared file list excluding mock-heavy files already run in isolation SHARED_FILES=$(find src/shared -name '*.test.ts' \ ! -name 'model-capabilities.test.ts' \ ! -name 'log-legacy-plugin-startup-warning.test.ts' \ ! -name 'model-error-classifier.test.ts' \ ! -name 'opencode-message-dir.test.ts' \ | sort | tr '\n' ' ') bun test bin script src/config src/mcp src/index.test.ts \ src/agents $SHARED_FILES \ src/cli/run src/cli/config-manager src/cli/mcp-oauth \ src/cli/index.test.ts src/cli/install.test.ts src/cli/model-fallback.test.ts \ src/cli/config-manager.test.ts \ src/cli/doctor/runner.test.ts src/cli/doctor/checks \ src/tools/ast-grep src/tools/background-task src/tools/delegate-task \ src/tools/glob src/tools/grep src/tools/interactive-bash \ src/tools/look-at src/tools/lsp \ src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \ src/tools/call-omo-agent/background-agent-executor.test.ts \ src/tools/call-omo-agent/background-executor.test.ts \ src/tools/call-omo-agent/subagent-session-creator.test.ts \ src/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.test.ts src/hooks/anthropic-context-window-limit-recovery/parser.test.ts src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts src/hooks/anthropic-context-window-limit-recovery/storage.test.ts \ src/hooks/session-recovery/detect-error-type.test.ts src/hooks/session-recovery/index.test.ts src/hooks/session-recovery/recover-empty-content-message-sdk.test.ts src/hooks/session-recovery/resume.test.ts src/hooks/session-recovery/storage \ src/hooks/legacy-plugin-toast/auto-migrate.test.ts \ src/hooks/claude-code-compatibility \ src/hooks/context-injection \ src/hooks/provider-toast \ src/hooks/session-notification \ src/hooks/sisyphus \ src/hooks/todo-continuation-enforcer \ src/features/background-agent \ src/features/builtin-commands \ src/features/builtin-skills \ src/features/claude-code-session-state \ src/features/hook-message-injector \ src/features/opencode-skill-loader/config-source-discovery.test.ts \ src/features/opencode-skill-loader/merger.test.ts \ src/features/opencode-skill-loader/skill-content.test.ts \ src/features/opencode-skill-loader/blocking.test.ts \ src/features/opencode-skill-loader/async-loader.test.ts \ src/features/skill-mcp-manager typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install dependencies run: bun install env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi" - name: Type check run: bun run typecheck publish-main: runs-on: ubuntu-latest needs: [test, typecheck] if: github.repository == 'code-yeongyu/oh-my-openagent' outputs: version: ${{ steps.version.outputs.version }} dist_tag: ${{ steps.version.outputs.dist_tag }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - run: git fetch --force --tags - uses: oven-sh/setup-bun@v2 with: bun-version: latest - uses: actions/setup-node@v4 with: node-version: "24" registry-url: "https://registry.npmjs.org" - name: Install dependencies run: bun install env: BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi" - 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" - name: Check if already published id: check env: VERSION: ${{ steps.version.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: Update version if: steps.check.outputs.skip != 'true' env: VERSION: ${{ steps.version.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 jq --arg v "$VERSION" '.version = $v' "packages/${platform}/package.json" > tmp.json mv tmp.json "packages/${platform}/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' run: | bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi bunx tsc --emitDeclarationOnly bun run build:schema - name: Publish oh-my-opencode if: steps.check.outputs.skip != 'true' env: DIST_TAG: ${{ steps.version.outputs.dist_tag }} NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} NPM_CONFIG_PROVENANCE: true run: | if [ -n "$DIST_TAG" ]; then npm publish --access public --provenance --tag "$DIST_TAG" else npm publish --access public --provenance fi - name: Check if oh-my-openagent already published id: check-openagent env: VERSION: ${{ steps.version.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: Publish oh-my-openagent if: steps.check-openagent.outputs.skip != 'true' env: VERSION: ${{ steps.version.outputs.version }} DIST_TAG: ${{ steps.version.outputs.dist_tag }} NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 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" else npm publish --access public --provenance fi - name: Restore package.json if: always() && steps.check-openagent.outputs.skip != 'true' run: | git checkout -- package.json publish-platform: needs: publish-main if: inputs.skip_platform != true uses: ./.github/workflows/publish-platform.yml with: version: ${{ needs.publish-main.outputs.version }} dist_tag: ${{ needs.publish-main.outputs.dist_tag }} secrets: inherit release: runs-on: ubuntu-latest needs: [publish-main, publish-platform] if: always() && 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: latest - name: Install dependencies run: bun install env: BUN_INSTALL_ALLOW_SCRIPTS: "@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: Apply release version to source tree env: VERSION: ${{ needs.publish-main.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 jq --arg v "$VERSION" '.version = $v' "packages/${platform}/package.json" > tmp.json mv tmp.json "packages/${platform}/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 env: VERSION: ${{ needs.publish-main.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/*/package.json git diff --cached --quiet || git commit -m "release: v${VERSION}" - name: Create release tag env: VERSION: ${{ needs.publish-main.outputs.version }} run: | if git rev-parse "v${VERSION}" >/dev/null 2>&1; then echo "::error::Tag v${VERSION} already exists" exit 1 fi git tag "v${VERSION}" - name: Push release state env: VERSION: ${{ needs.publish-main.outputs.version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git push origin HEAD git push origin "v${VERSION}" - name: Create GitHub release env: VERSION: ${{ needs.publish-main.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.publish-main.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"