diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a56efa..d4c7d80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,13 +3,24 @@ name: Release on: push: branches: [main] + # Retries npm publish for a release whose tag already exists. Keep this in + # release.yml: npm trusted publishing is bound to this workflow file. + workflow_dispatch: + inputs: + tag: + description: 'Existing release tag to publish, e.g. deploygate--v1.5.2' + required: true + type: string jobs: release-please: + if: ${{ github.event_name == 'push' }} runs-on: ubuntu-latest permissions: contents: write - id-token: write + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: actions/create-github-app-token@v3 id: app-token @@ -55,24 +66,72 @@ jobs: git push origin "HEAD:${BRANCH}" fi - - if: ${{ steps.release.outputs.release_created == 'true' }} - uses: actions/checkout@v7 + publish: + needs: release-please + if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.release-please.result == 'success' && needs.release-please.outputs.release_created == 'true')) }} + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + env: + TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }} + steps: + - run: | + if ! printf '%s' "$TAG" | grep -Eq '^deploygate--v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Invalid release tag (expected deploygate--vX.Y.Z)" + exit 1 + fi + + - uses: actions/checkout@v7 + with: + ref: refs/tags/${{ env.TAG }} + fetch-depth: 0 - - if: ${{ steps.release.outputs.release_created == 'true' }} + # Only publish commits that went through main, so a manual run cannot + # publish a tag pointing at unreviewed code. + - run: | + if ! git merge-base --is-ancestor HEAD origin/main; then + echo "::error::${TAG} is not reachable from main; refusing to publish" + exit 1 + fi + + - id: check + run: | + VERSION="${TAG#deploygate--v}" + PKG_VERSION=$(node -p "require('./package.json').version") + if [ "$PKG_VERSION" != "$VERSION" ]; then + echo "::error::package.json version ${PKG_VERSION} does not match tag ${TAG}" + exit 1 + fi + # Only an explicit E404 means "not published yet". Any other registry + # error stops the run instead of attempting a publish. + if PUBLISHED=$(npm view "@deploygate/mcp@${VERSION}" version 2>"$RUNNER_TEMP/npm-view.err"); then + if [ "$PUBLISHED" = "$VERSION" ]; then + echo "::notice::@deploygate/mcp@${VERSION} is already on npm; skipping publish" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + elif ! grep -q E404 "$RUNNER_TEMP/npm-view.err"; then + cat "$RUNNER_TEMP/npm-view.err" + echo "::error::Could not check npm for @deploygate/mcp@${VERSION}" + exit 1 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + + - if: ${{ steps.check.outputs.skip == 'false' }} uses: actions/setup-node@v7 with: node-version: 24 cache: npm registry-url: 'https://registry.npmjs.org' - - if: ${{ steps.release.outputs.release_created == 'true' }} + - if: ${{ steps.check.outputs.skip == 'false' }} run: npm install -g npm@latest - - if: ${{ steps.release.outputs.release_created == 'true' }} + - if: ${{ steps.check.outputs.skip == 'false' }} run: npm ci - - if: ${{ steps.release.outputs.release_created == 'true' }} - run: npm run bundle - - - if: ${{ steps.release.outputs.release_created == 'true' }} + # No `npm run bundle` here: the plugin/scripts/bundle.js committed at the + # tag is published as-is, so npm and git ship the same file. + - if: ${{ steps.check.outputs.skip == 'false' }} run: npm publish diff --git a/README.md b/README.md index 1564c6c..e0b372f 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,13 @@ End users can pin to a tag with: Without a pin, `claude plugin install` follows `main`. Users can fetch the latest published tag with `claude plugin update deploygate`. +### Retrying a failed npm publish + +If the tag `deploygate--vX.Y.Z` and its GitHub Release exist but `npm publish` failed, do not delete the tag: that breaks release-please's baseline. + +- If the failure was transient or outside the repository (an npm outage, a registry or trusted-publisher setting), fix it and go to Actions → Release → Run workflow, then enter the tag. The workflow checks out the tag and publishes the files committed there, including `plugin/scripts/bundle.js`. It refuses tags that are not reachable from `main`, and does nothing if that version is already on npm. +- If the failure was caused by the tagged files themselves (for example a broken `package-lock.json`), a retry fails again because the tag cannot change. Merge a `fix:` PR to `main` and ship it as the next release instead. + ## Support & Project Status This plugin is open-source software provided **as-is on a best-effort