fix(release): bound cross-os fetch bodies #62
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Plugin NPM Release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - ".github/workflows/plugin-npm-release.yml" | |
| - "extensions/**" | |
| - "package.json" | |
| - "scripts/lib/plugin-npm-package-manifest.mjs" | |
| - "scripts/lib/plugin-npm-release.ts" | |
| - "scripts/plugin-npm-publish.sh" | |
| - "scripts/plugin-npm-release-check.ts" | |
| - "scripts/plugin-npm-release-plan.ts" | |
| - "scripts/verify-plugin-npm-published-runtime.mjs" | |
| workflow_dispatch: | |
| inputs: | |
| publish_scope: | |
| description: Publish the selected plugins or all publishable plugins from the ref | |
| required: true | |
| default: selected | |
| type: choice | |
| options: | |
| - selected | |
| - all-publishable | |
| ref: | |
| description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from | |
| required: true | |
| type: string | |
| plugins: | |
| description: Comma-separated plugin package names to publish when publish_scope=selected | |
| required: false | |
| type: string | |
| release_publish_run_id: | |
| description: Approved OpenClaw Release Publish workflow run id | |
| required: false | |
| type: string | |
| concurrency: | |
| group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }} | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| NODE_VERSION: "24.15.0" | |
| jobs: | |
| preview_plugins_npm: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| ref_revision: ${{ steps.ref.outputs.sha }} | |
| has_candidates: ${{ steps.plan.outputs.has_candidates }} | |
| candidate_count: ${{ steps.plan.outputs.candidate_count }} | |
| matrix: ${{ steps.plan.outputs.matrix }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }} | |
| fetch-depth: 0 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| install-bun: "false" | |
| - name: Resolve checked-out ref | |
| id: ref | |
| run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Validate ref is on a trusted publish branch | |
| env: | |
| WORKFLOW_REF: ${{ github.ref }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin \ | |
| +refs/heads/main:refs/remotes/origin/main \ | |
| '+refs/heads/release/*:refs/remotes/origin/release/*' | |
| if git merge-base --is-ancestor HEAD origin/main; then | |
| exit 0 | |
| fi | |
| while IFS= read -r release_ref; do | |
| if git merge-base --is-ancestor HEAD "${release_ref}"; then | |
| exit 0 | |
| fi | |
| done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release) | |
| if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then | |
| alpha_branch="${WORKFLOW_REF#refs/heads/}" | |
| git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}" | |
| if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then | |
| exit 0 | |
| fi | |
| fi | |
| echo "Plugin npm publishes must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2 | |
| exit 1 | |
| - name: Validate publishable plugin metadata | |
| env: | |
| PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }} | |
| RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }} | |
| BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }} | |
| HEAD_REF: ${{ steps.ref.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${PUBLISH_SCOPE}" ]]; then | |
| release_args=(--selection-mode "${PUBLISH_SCOPE}") | |
| if [[ -n "${RELEASE_PLUGINS}" ]]; then | |
| release_args+=(--plugins "${RELEASE_PLUGINS}") | |
| fi | |
| pnpm release:plugins:npm:check -- "${release_args[@]}" | |
| elif [[ -n "${BASE_REF}" ]]; then | |
| pnpm release:plugins:npm:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" | |
| else | |
| pnpm release:plugins:npm:check | |
| fi | |
| - name: Resolve plugin release plan | |
| id: plan | |
| env: | |
| PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }} | |
| RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }} | |
| BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }} | |
| HEAD_REF: ${{ steps.ref.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .local | |
| if [[ -n "${PUBLISH_SCOPE}" ]]; then | |
| plan_args=(--selection-mode "${PUBLISH_SCOPE}") | |
| if [[ -n "${RELEASE_PLUGINS}" ]]; then | |
| plan_args+=(--plugins "${RELEASE_PLUGINS}") | |
| fi | |
| node --import tsx scripts/plugin-npm-release-plan.ts "${plan_args[@]}" > .local/plugin-npm-release-plan.json | |
| elif [[ -n "${BASE_REF}" ]]; then | |
| node --import tsx scripts/plugin-npm-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-npm-release-plan.json | |
| else | |
| node --import tsx scripts/plugin-npm-release-plan.ts > .local/plugin-npm-release-plan.json | |
| fi | |
| cat .local/plugin-npm-release-plan.json | |
| candidate_count="$(jq -r '.candidates | length' .local/plugin-npm-release-plan.json)" | |
| has_candidates="false" | |
| if [[ "${candidate_count}" != "0" ]]; then | |
| has_candidates="true" | |
| fi | |
| matrix_json="$(jq -c '.candidates' .local/plugin-npm-release-plan.json)" | |
| { | |
| echo "candidate_count=${candidate_count}" | |
| echo "has_candidates=${has_candidates}" | |
| echo "matrix=${matrix_json}" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Plugin release candidates:" | |
| jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-npm-release-plan.json | |
| echo "Already published / skipped:" | |
| jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-npm-release-plan.json | |
| - name: Validate Tideclaw alpha plugin channels | |
| if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/') | |
| run: | | |
| set -euo pipefail | |
| invalid="$( | |
| jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-npm-release-plan.json | |
| )" | |
| if [[ -n "${invalid}" ]]; then | |
| echo "Tideclaw alpha plugin npm publishes may only publish alpha plugin versions." >&2 | |
| printf '%s\n' "${invalid}" >&2 | |
| exit 1 | |
| fi | |
| validate_release_publish_approval: | |
| name: Validate release publish approval | |
| needs: preview_plugins_npm | |
| if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Validate release publish approval run | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }} | |
| EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then | |
| if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then | |
| echo "Plugin npm publish dispatched by another workflow must include release_publish_run_id." >&2 | |
| exit 1 | |
| fi | |
| echo "Direct Plugin NPM Release dispatch; relying on this workflow's npm-release environment approval." | |
| exit 0 | |
| fi | |
| if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then | |
| echo "Plugin npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2 | |
| exit 1 | |
| fi | |
| RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)" | |
| printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);' | |
| preview_plugin_pack: | |
| needs: preview_plugins_npm | |
| if: needs.preview_plugins_npm.outputs.has_candidates == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }} | |
| fetch-depth: 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| install-bun: "false" | |
| - name: Preview publish command | |
| run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}" | |
| - name: Preview npm pack contents | |
| run: bash scripts/plugin-npm-publish.sh --pack-dry-run "${{ matrix.plugin.packageDir }}" | |
| publish_plugins_npm: | |
| needs: [preview_plugins_npm, preview_plugin_pack, validate_release_publish_approval] | |
| if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true' | |
| runs-on: ubuntu-latest | |
| environment: npm-release | |
| permissions: | |
| actions: read | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }} | |
| fetch-depth: 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| install-bun: "false" | |
| - name: Ensure version is not already published | |
| env: | |
| PACKAGE_NAME: ${{ matrix.plugin.packageName }} | |
| PACKAGE_VERSION: ${{ matrix.plugin.version }} | |
| run: | | |
| set -euo pipefail | |
| if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then | |
| echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm." | |
| exit 1 | |
| fi | |
| - name: Publish | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}" | |
| - name: Verify published runtime | |
| env: | |
| PACKAGE_NAME: ${{ matrix.plugin.packageName }} | |
| PACKAGE_VERSION: ${{ matrix.plugin.version }} | |
| run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}" |