name: Publish on: push: branches: [main] paths: - "src/**" - "tools/cli/**" - "package.json" workflow_dispatch: inputs: channel: description: "Publish channel" required: true default: "latest" type: choice options: - latest - next bump: description: "Version bump type (latest channel only)" required: false default: "patch" type: choice options: - patch - minor - major concurrency: group: publish cancel-in-progress: ${{ github.event_name == 'push' }} permissions: id-token: write contents: write jobs: publish: if: github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node uses: actions/setup-node@v4 with: node-version-file: ".nvmrc" cache: "npm" registry-url: "https://registry.npmjs.org" - name: Debug trusted publishing identity run: | echo "GitHub workflow context:" echo " repository: ${{ github.repository }}" echo " repository_owner: ${{ github.repository_owner }}" echo " ref: ${{ github.ref }}" echo " event_name: ${{ github.event_name }}" echo " workflow: ${{ github.workflow }}" echo " workflow_ref: ${{ github.workflow_ref }}" echo " actor: ${{ github.actor }}" echo " selected_channel: ${{ inputs.channel || 'n/a' }}" echo " selected_bump: ${{ inputs.bump || 'n/a' }}" echo " node_auth_token_present: $([ -n \"$NODE_AUTH_TOKEN\" ] && echo yes || echo no)" WORKFLOW_FILE=$(node -e " const ref = process.argv[1] || ''; const match = ref.match(/\.github\/workflows\/([^@]+)@/); process.stdout.write(match ? match[1] : ''); " "${{ github.workflow_ref }}") echo " workflow_filename_for_npm: ${WORKFLOW_FILE:-unknown}" echo "OIDC claims (sanitized):" RESPONSE=$(curl -fsS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org") ID_TOKEN=$(node -e " const fs = require('fs'); const data = JSON.parse(fs.readFileSync(0, 'utf8')); process.stdout.write(data.value || ''); " <<<"$RESPONSE") node -e " const token = process.argv[1]; if (!token) { console.log(JSON.stringify({ error: 'missing_id_token' }, null, 2)); process.exit(0); } const payloadPart = token.split('.')[1] || ''; const padded = payloadPart.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (payloadPart.length % 4)) % 4); const claims = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')); const out = { iss: claims.iss, sub: claims.sub, aud: claims.aud, repository: claims.repository, repository_owner: claims.repository_owner, workflow: claims.workflow, workflow_ref: claims.workflow_ref, job_workflow_ref: claims.job_workflow_ref, ref: claims.ref, environment: claims.environment || null, runner_environment: claims.runner_environment || null, }; console.log(JSON.stringify(out, null, 2)); " "$ID_TOKEN" - name: Configure git user if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Derive next prerelease version if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.channel == 'next') run: | NEXT_VER=$(npm view bmad-method@next version 2>/dev/null || echo "") LATEST_VER=$(npm view bmad-method@latest version 2>/dev/null || echo "") # Determine the best base version for the next prerelease. BASE=$(node -e " const semver = require('semver'); const next = process.argv[1] || null; const latest = process.argv[2] || null; if (!next && !latest) process.exit(0); if (!next) { console.log(latest); process.exit(0); } if (!latest) { console.log(next); process.exit(0); } const nextBase = next.replace(/-next\.\d+$/, ''); console.log(semver.gt(latest, nextBase) ? latest : next); " "$NEXT_VER" "$LATEST_VER") if [ -n "$BASE" ]; then npm version "$BASE" --no-git-tag-version --allow-same-version fi npm version prerelease --preid=next --no-git-tag-version - name: Bump stable version if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' run: 'npm version ${{ inputs.bump }} -m "chore(release): v%s [skip ci]"' - name: Debug publish target and registry state run: | echo "Local package target:" node -e " const pkg = require('./package.json'); console.log(JSON.stringify({ name: pkg.name, version: pkg.version }, null, 2)); " echo "Registry package view (bmad-method):" npm view bmad-method name version dist-tags --json || true - name: Publish prerelease to npm if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.channel == 'next') run: npm publish --tag next --provenance env: NODE_AUTH_TOKEN: "" - name: Publish stable release to npm if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' run: npm publish --tag latest --provenance env: NODE_AUTH_TOKEN: "" - name: Print npm debug logs if: always() run: | LOG_DIR="$HOME/.npm/_logs" echo "npm log directory: $LOG_DIR" ls -la "$LOG_DIR" || true found=0 for file in "$LOG_DIR"/*-debug-0.log; do [ -e "$file" ] || continue found=1 echo "::group::npm-debug $(basename "$file")" cat "$file" echo "::endgroup::" done if [ "$found" -eq 0 ]; then echo "No npm *-debug-0.log files found." fi - name: Push version commit and tag if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' run: git push origin main --follow-tags - name: Create GitHub Release if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' run: | TAG="v$(node -p 'require("./package.json").version')" gh release create "$TAG" --generate-notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Notify Discord if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' continue-on-error: true run: | set -o pipefail source .github/scripts/discord-helpers.sh [ -z "$WEBHOOK" ] && exit 0 VERSION=$(node -p 'require("./package.json").version') RELEASE_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/v${VERSION}" MSG=$(printf '📦 **[bmad-method v%s released](<%s>)**' "$VERSION" "$RELEASE_URL" | esc) jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @- env: WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}