Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions .github/workflows/npm-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ on:
required: false
default: false
type: boolean
auth_method:
description: Authentication method (oidc uses GitHub OIDC trusted publisher; manual_otp uses NPM_TOKEN secret + 2FA OTP)
required: false
default: oidc
type: choice
options:
- oidc
- manual_otp
npm_otp:
description: 2FA OTP code (required when auth_method=manual_otp). Code is masked in workflow logs.
required: false
type: string

permissions:
contents: read
Expand Down Expand Up @@ -68,6 +80,26 @@ jobs:
echo "published=false" >> "$GITHUB_OUTPUT"
fi

- name: Configure manual OTP auth
if: github.event_name == 'workflow_dispatch' && inputs.auth_method == 'manual_otp'
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_OTP: ${{ inputs.npm_otp }}
run: |
set -euo pipefail
if [ -z "${NPM_TOKEN}" ]; then
echo "::error::auth_method=manual_otp requires NPM_TOKEN to be configured as a secret on the production environment."
exit 1
fi
if [ -z "${NPM_OTP}" ]; then
echo "::error::auth_method=manual_otp requires npm_otp input to be supplied with the current 2FA OTP code."
exit 1
fi
# Override actions/setup-node OIDC NODE_AUTH_TOKEN with the legacy token.
echo "NODE_AUTH_TOKEN=${NPM_TOKEN}" >> "$GITHUB_ENV"
echo "NPM_OTP_CONFIGURED=true" >> "$GITHUB_ENV"
echo "Using manual_otp auth: NPM_TOKEN secret + 2FA OTP."

- name: Dry run publish
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
env:
Expand All @@ -76,16 +108,27 @@ jobs:

- name: Publish package
if: steps.package.outputs.published != 'true' && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
run: npm publish --access public --tag "${{ github.event_name == 'workflow_dispatch' && inputs.npm_tag || 'latest' }}"
env:
NPM_OTP: ${{ env.NPM_OTP_CONFIGURED == 'true' && inputs.npm_otp || '' }}
run: |
set -euo pipefail
TAG="${{ github.event_name == 'workflow_dispatch' && inputs.npm_tag || 'latest' }}"
if [ -n "${NPM_OTP}" ]; then
npm publish --access public --tag "${TAG}" --otp="${NPM_OTP}"
else
npm publish --access public --tag "${TAG}"
fi

- name: Publish scoped alias
if: steps.package.outputs.published != 'true' && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
env:
ALIAS_SCOPE: "@echohello"
NPM_OTP: ${{ env.NPM_OTP_CONFIGURED == 'true' && inputs.npm_otp || '' }}
run: |
set -euo pipefail
VERSION=$(node --print "require('./package.json').version")
ALIAS_NAME="${ALIAS_SCOPE}/skillet"
TAG="${{ github.event_name == 'workflow_dispatch' && inputs.npm_tag || 'latest' }}"

# Skip if the alias version is already published
if npm view "${ALIAS_NAME}@${VERSION}" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
Expand All @@ -106,4 +149,8 @@ jobs:
"

cd "$ALIAS_DIR"
npm publish --access public --tag "${{ github.event_name == 'workflow_dispatch' && inputs.npm_tag || 'latest' }}"
if [ -n "${NPM_OTP}" ]; then
npm publish --access public --tag "${TAG}" --otp="${NPM_OTP}"
else
npm publish --access public --tag "${TAG}"
fi
13 changes: 13 additions & 0 deletions docs/distribution/npm.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ Manual dispatch inputs:
- `npm_tag`: npm dist-tag, defaults to `latest`
- `dry_run`: when true, runs `npm publish --dry-run` and skips the real publish step
- When `dry_run` is true, the workflow uses the provided `npm_tag`, or falls back to `dry-run` if you leave it at `latest`.
- `auth_method`: `oidc` (default, uses GitHub OIDC trusted publisher) or `manual_otp` (uses a legacy `NPM_TOKEN` secret plus a one-time password from the dispatch input). Use `manual_otp` when the OIDC integration is failing or for a one-off bootstrap publish.
- `npm_otp`: required when `auth_method=manual_otp`. Provide the current 2FA OTP code from your authenticator. The value is masked in workflow logs but still surfaces in the dispatch payload; rotate the OTP after each dispatch.

### Manual OTP bootstrap publish

When the OIDC trusted publisher is misbehaving (for example, returning `404 'getskillet is not in this registry'` during automated release publishes), fall back to manual OTP:

1. Add `NPM_TOKEN` as an environment secret on the GitHub `production` environment (Settings → Environments → production → Add secret). Use a legacy automation token from `npmjs.com → Access Tokens → Automation` with 2FA-required publish scope.
2. Trigger `npm-publish.yaml` via `workflow_dispatch` with `auth_method=manual_otp` and `npm_otp=<6-digit code>`.
3. The workflow writes `NODE_AUTH_TOKEN` from the secret to `$GITHUB_ENV`, which overrides the `actions/setup-node` OIDC value for the publish steps. Both `getskillet` and `@echohello/skillet` are published with `--otp` against the same code.
4. Rotate the OTP after the run completes and consider rotating the `NPM_TOKEN` secret if it was exposed in any logs.

This path is intentionally heavier than OIDC (manual step + secret rotation). It exists to unblock a release when the trusted publisher configuration is broken; it should not be the default.

Local command:

Expand Down
Loading