From 5facc6d0ec6cc130bff2faf79f56bb4ff5411550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADnez?= Date: Sun, 30 Aug 2026 13:12:29 +0200 Subject: [PATCH 1/2] npm-publish.yml: workflow reutilizable con trusted publishing (OIDC), idempotente; el composite queda deprecado --- .github/workflows/npm-publish.yml | 121 ++++++++++++++++++++++++++++++ README.md | 57 +++++++++----- 2 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/npm-publish.yml diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..85255c6 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,121 @@ +# Publica uno o varios paquetes npm desde el ref que lo llama, con trusted +# publishing (OIDC): sin NPM_TOKEN, con provenance, y solo lo que pasa la +# verificación. Idempotente: una versión que ya está en npm se salta, así que +# un job puede relanzarse tras un fallo sin quemar versiones. +# +# Requisitos en el repo que lo llama: +# - `permissions: { contents: read, id-token: write }` en el job `uses:`. +# - Un entorno (por defecto `npm-publish`) con revisores requeridos. +# - Cada paquete con trusted publisher configurado en npmjs.com apuntando al +# workflow llamador (fichero del caller, no este). La primera versión de un +# paquete nuevo se publica a mano: npm exige que exista antes de fiarse. +name: npm publish (reusable) + +on: + workflow_call: + inputs: + packages: + description: Directorios de paquete separados por espacios (`.` para un solo paquete). + type: string + default: '.' + package-manager: + description: npm o pnpm. + type: string + default: npm + node-version: + type: string + default: '22' + verify: + description: Script que debe pasar antes de publicar (typecheck, tests, build…). Vacío para omitir. + type: string + default: verify + build: + description: Script de build si `verify` no lo incluye. Vacío para omitir. + type: string + default: '' + require-tag: + description: Exigir que el ref sea un tag v igual a package.json (solo con un paquete). + type: boolean + default: false + environment: + description: Entorno de GitHub que gatea la publicación. + type: string + default: npm-publish + access: + description: public o restricted; vacío respeta publishConfig.access de cada paquete. + type: string + default: '' + outputs: + published: + description: Lista `nombre@versión` de lo publicado en esta ejecución (vacía si todo estaba ya en npm). + value: ${{ jobs.publish.outputs.published }} + +permissions: {} + +jobs: + publish: + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + permissions: + contents: read + id-token: write + outputs: + published: ${{ steps.publish.outputs.published }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + if: inputs.package-manager == 'pnpm' + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ inputs.node-version }} + registry-url: https://registry.npmjs.org + cache: ${{ inputs.package-manager }} + + # Trusted publishing necesita npm >= 11.5.1; fijado a major para que un + # release de npm sin revisar no cambie cómo se publica. + - run: npm install -g npm@11 + + - name: Install + run: | + if [ "${{ inputs.package-manager }}" = pnpm ]; then pnpm install --frozen-lockfile; else npm ci; fi + + - name: Verify + if: inputs.verify != '' + run: ${{ inputs.package-manager }} run ${{ inputs.verify }} + + - name: Build + if: inputs.build != '' + run: ${{ inputs.package-manager }} run ${{ inputs.build }} + + - name: Check the tag matches the package version + if: inputs.require-tag + run: | + case "$GITHUB_REF" in refs/tags/v*) ;; *) echo "::error::Se publica solo desde un tag v* (ref: $GITHUB_REF)."; exit 1 ;; esac + set -- ${{ inputs.packages }} + [ "$#" -eq 1 ] || { echo "::error::require-tag solo vale con un paquete."; exit 1; } + pkg="$(node -p "require('./$1/package.json').version")" + [ "${GITHUB_REF_NAME#v}" = "$pkg" ] || { echo "::error::El tag $GITHUB_REF_NAME no coincide con la versión $pkg."; exit 1; } + + # Sin token: el intercambio OIDC ocurre dentro de npm publish. Lo que ya + # está en npm se salta; npm no permite sobrescribir y este paso tampoco. + - name: Publish + id: publish + run: | + published="" + for dir in ${{ inputs.packages }}; do + name="$(node -p "require('./$dir/package.json').name")" + version="$(node -p "require('./$dir/package.json').version")" + private="$(node -p "require('./$dir/package.json').private === true")" + if [ "$private" = true ]; then echo "$name es private: se omite."; continue; fi + if npm view "$name@$version" version >/dev/null 2>&1; then + echo "$name@$version ya está en npm: se omite (reintento)." + continue + fi + access=""; [ -n "${{ inputs.access }}" ] && access="--access ${{ inputs.access }}" + (cd "$dir" && npm publish --provenance $access) + published="$published $name@$version" + done + echo "published=${published# }" >> "$GITHUB_OUTPUT" + echo "Publicado:${published:- nada nuevo}" diff --git a/README.md b/README.md index ba16c72..b7542de 100644 --- a/README.md +++ b/README.md @@ -100,35 +100,54 @@ Bundle multiple OpenAPI YAML files into one using Redocly CLI. --- -### `npm-publish` +### `npm-publish` (composite) — deprecado -**Composite action** (use inside a job, not as a standalone workflow) +Usa `NPM_TOKEN`, sube versión en cada ejecución y sigue aunque fallen los tests. No lo uses en repos nuevos: el sustituto es el workflow reutilizable `npm-publish.yml` de abajo. -Regenerate code, build, bump version, and publish to npm. +--- -**Inputs:** -- `generate-script` (optional) - npm script to run for code generation -- `build-script` (optional, default: `build`) - npm script to build -- `skip-tests` (optional, default: `false`) - Skip running tests -- `npm-token` (required) - npm authentication token +## 🌐 Reusable Workflows -**Outputs:** -- `published` - Whether package was published -- `version` - New version number +### `npm-publish.yml` -**Example:** +Publica uno o varios paquetes con **trusted publishing de npm (OIDC)**: sin `NPM_TOKEN`, con provenance, solo lo que pasa la verificación, e idempotente (lo que ya está en npm se salta, así que se puede relanzar). + +**Inputs:** `packages` (default `.`; varios separados por espacio), `package-manager` (`npm`|`pnpm`), `node-version` (`22`), `verify` (script, default `verify`), `build` (script opcional), `require-tag` (el ref debe ser `v` = package.json), `environment` (default `npm-publish`), `access` (`public`|`restricted`; vacío respeta `publishConfig`). + +**Output:** `published` — lista `nombre@versión` publicada. + +**Requisitos en el repo llamador:** entorno `npm-publish` con revisores; trusted publisher en npmjs.com por paquete apuntando al fichero del workflow llamador; la primera versión de un paquete nuevo se publica a mano (npm exige que exista). ```yaml -- uses: beel-es/github-actions/npm-publish@main - with: - generate-script: 'generate' - build-script: 'build' - npm-token: ${{ secrets.NPM_TOKEN }} +# .github/workflows/publish.yml +on: + push: + tags: ['v*'] +permissions: + contents: read + id-token: write +jobs: + publish: + uses: beel-es/github-actions/.github/workflows/npm-publish.yml@main + with: + require-tag: true ``` ---- +Monorepo con pnpm y changesets (publica lo que `master` diga que toca): + +```yaml +on: + push: + branches: [master] + paths: ['packages/*/package.json'] +jobs: + publish: + uses: beel-es/github-actions/.github/workflows/npm-publish.yml@main + with: + package-manager: pnpm + packages: packages/brand packages/surface +``` -## 🌐 Reusable Workflows ### `sync-from-openapi.yml` From b6ff41048f6f9d16aa6676a3c4576e08fd461c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADnez?= Date: Sun, 30 Aug 2026 13:14:26 +0200 Subject: [PATCH 2/2] feat: reusable npm-publish.yml with trusted publishing (OIDC); deprecate the token-based composite --- .github/workflows/npm-publish.yml | 86 ++++++++++++++++++------------- README.md | 45 +++++++++++++++- 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 85255c6..fcd3289 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,53 +1,53 @@ -# Publica uno o varios paquetes npm desde el ref que lo llama, con trusted -# publishing (OIDC): sin NPM_TOKEN, con provenance, y solo lo que pasa la -# verificación. Idempotente: una versión que ya está en npm se salta, así que -# un job puede relanzarse tras un fallo sin quemar versiones. +# Publishes one or more npm packages from the calling ref with npm trusted +# publishing (OIDC): no long-lived token, provenance attached, and only what +# passes verification. Idempotent: a version already on npm is skipped, so a +# failed job can be re-run without burning a version number. # -# Requisitos en el repo que lo llama: -# - `permissions: { contents: read, id-token: write }` en el job `uses:`. -# - Un entorno (por defecto `npm-publish`) con revisores requeridos. -# - Cada paquete con trusted publisher configurado en npmjs.com apuntando al -# workflow llamador (fichero del caller, no este). La primera versión de un -# paquete nuevo se publica a mano: npm exige que exista antes de fiarse. +# The calling repository needs: +# - `permissions: { contents: read, id-token: write }` on the `uses:` job. +# - A GitHub environment (default `npm-publish`) with required reviewers. +# - A trusted publisher on npmjs.com for each package, pointing at the +# caller's workflow file. A brand-new package is published by hand once: +# npm only trusts a publisher for a package that already exists. name: npm publish (reusable) on: workflow_call: inputs: packages: - description: Directorios de paquete separados por espacios (`.` para un solo paquete). + description: Package directories, space separated (`.` for a single package). type: string default: '.' package-manager: - description: npm o pnpm. + description: npm or pnpm. type: string default: npm node-version: type: string default: '22' verify: - description: Script que debe pasar antes de publicar (typecheck, tests, build…). Vacío para omitir. + description: Script that must pass before publishing (typecheck, tests, build…). Empty to skip. type: string default: verify build: - description: Script de build si `verify` no lo incluye. Vacío para omitir. + description: Build script when `verify` does not build. Empty to skip. type: string default: '' require-tag: - description: Exigir que el ref sea un tag v igual a package.json (solo con un paquete). + description: Require the ref to be a `v` tag equal to package.json (single package only). type: boolean default: false environment: - description: Entorno de GitHub que gatea la publicación. + description: GitHub environment that gates the publish. type: string default: npm-publish access: - description: public o restricted; vacío respeta publishConfig.access de cada paquete. + description: public or restricted; empty keeps each package's publishConfig. type: string default: '' outputs: published: - description: Lista `nombre@versión` de lo publicado en esta ejecución (vacía si todo estaba ya en npm). + description: Space-separated `name@version` list published by this run (empty if everything was already on npm). value: ${{ jobs.publish.outputs.published }} permissions: {} @@ -73,49 +73,61 @@ jobs: registry-url: https://registry.npmjs.org cache: ${{ inputs.package-manager }} - # Trusted publishing necesita npm >= 11.5.1; fijado a major para que un - # release de npm sin revisar no cambie cómo se publica. + # Trusted publishing needs npm >= 11.5.1; pinned to a major so an + # unreviewed npm release cannot change how a publish behaves. - run: npm install -g npm@11 - name: Install + env: + PM: ${{ inputs.package-manager }} run: | - if [ "${{ inputs.package-manager }}" = pnpm ]; then pnpm install --frozen-lockfile; else npm ci; fi + if [ "$PM" = pnpm ]; then pnpm install --frozen-lockfile; else npm ci; fi - name: Verify if: inputs.verify != '' - run: ${{ inputs.package-manager }} run ${{ inputs.verify }} + env: + PM: ${{ inputs.package-manager }} + SCRIPT: ${{ inputs.verify }} + run: "$PM" run "$SCRIPT" - name: Build if: inputs.build != '' - run: ${{ inputs.package-manager }} run ${{ inputs.build }} + env: + PM: ${{ inputs.package-manager }} + SCRIPT: ${{ inputs.build }} + run: "$PM" run "$SCRIPT" - name: Check the tag matches the package version if: inputs.require-tag + env: + PACKAGES: ${{ inputs.packages }} run: | - case "$GITHUB_REF" in refs/tags/v*) ;; *) echo "::error::Se publica solo desde un tag v* (ref: $GITHUB_REF)."; exit 1 ;; esac - set -- ${{ inputs.packages }} - [ "$#" -eq 1 ] || { echo "::error::require-tag solo vale con un paquete."; exit 1; } + case "$GITHUB_REF" in refs/tags/v*) ;; *) echo "::error::Publish only runs from a v* tag (got $GITHUB_REF)."; exit 1 ;; esac + set -- $PACKAGES + [ "$#" -eq 1 ] || { echo "::error::require-tag works with a single package."; exit 1; } pkg="$(node -p "require('./$1/package.json').version")" - [ "${GITHUB_REF_NAME#v}" = "$pkg" ] || { echo "::error::El tag $GITHUB_REF_NAME no coincide con la versión $pkg."; exit 1; } + [ "${GITHUB_REF_NAME#v}" = "$pkg" ] || { echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $pkg."; exit 1; } - # Sin token: el intercambio OIDC ocurre dentro de npm publish. Lo que ya - # está en npm se salta; npm no permite sobrescribir y este paso tampoco. + # No token: the OIDC exchange happens inside npm publish. Versions already + # on npm are skipped; npm never overwrites and neither does this step. - name: Publish id: publish + env: + PACKAGES: ${{ inputs.packages }} + ACCESS: ${{ inputs.access }} run: | published="" - for dir in ${{ inputs.packages }}; do + for dir in $PACKAGES; do name="$(node -p "require('./$dir/package.json').name")" version="$(node -p "require('./$dir/package.json').version")" - private="$(node -p "require('./$dir/package.json').private === true")" - if [ "$private" = true ]; then echo "$name es private: se omite."; continue; fi + if [ "$(node -p "require('./$dir/package.json').private === true")" = true ]; then + echo "$name is private: skipped."; continue + fi if npm view "$name@$version" version >/dev/null 2>&1; then - echo "$name@$version ya está en npm: se omite (reintento)." - continue + echo "$name@$version is already on npm: skipped (retry)."; continue fi - access=""; [ -n "${{ inputs.access }}" ] && access="--access ${{ inputs.access }}" - (cd "$dir" && npm publish --provenance $access) + if [ -n "$ACCESS" ]; then (cd "$dir" && npm publish --provenance --access "$ACCESS"); else (cd "$dir" && npm publish --provenance); fi published="$published $name@$version" done echo "published=${published# }" >> "$GITHUB_OUTPUT" - echo "Publicado:${published:- nada nuevo}" + echo "Published:${published:- nothing new}" diff --git a/README.md b/README.md index b7542de..0f9634a 100644 --- a/README.md +++ b/README.md @@ -100,14 +100,55 @@ Bundle multiple OpenAPI YAML files into one using Redocly CLI. --- -### `npm-publish` (composite) — deprecado +### `npm-publish` (composite) — deprecated -Usa `NPM_TOKEN`, sube versión en cada ejecución y sigue aunque fallen los tests. No lo uses en repos nuevos: el sustituto es el workflow reutilizable `npm-publish.yml` de abajo. +Uses a long-lived `NPM_TOKEN`, bumps the version on every run and continues when tests fail. Do not use it in new repositories; use the reusable workflow `npm-publish.yml` below instead. --- ## 🌐 Reusable Workflows +### `npm-publish.yml` + +Publishes one or more packages with **npm trusted publishing (OIDC)**: no `NPM_TOKEN`, provenance attached, only what passes verification, and idempotent (versions already on npm are skipped, so a run can be retried). + +**Inputs:** `packages` (default `.`; several, space separated), `package-manager` (`npm`|`pnpm`), `node-version` (`22`), `verify` (script, default `verify`), `build` (optional script), `require-tag` (the ref must be `v` = package.json), `environment` (default `npm-publish`), `access` (`public`|`restricted`; empty keeps `publishConfig`). + +**Output:** `published` — `name@version` list published by the run. + +**Caller requirements:** a `npm-publish` environment with required reviewers; a trusted publisher per package on npmjs.com pointing at the caller's workflow file; the first version of a new package is published by hand (npm requires the package to exist). + +```yaml +# .github/workflows/publish.yml +on: + push: + tags: ['v*'] +permissions: + contents: read + id-token: write +jobs: + publish: + uses: beel-es/github-actions/.github/workflows/npm-publish.yml@main + with: + require-tag: true +``` + +pnpm monorepo with changesets (publishes whatever the default branch says is due): + +```yaml +on: + push: + branches: [main] + paths: ['packages/*/package.json'] +jobs: + publish: + uses: beel-es/github-actions/.github/workflows/npm-publish.yml@main + with: + package-manager: pnpm + packages: packages/a packages/b +``` + + ### `npm-publish.yml` Publica uno o varios paquetes con **trusted publishing de npm (OIDC)**: sin `NPM_TOKEN`, con provenance, solo lo que pasa la verificación, e idempotente (lo que ya está en npm se salta, así que se puede relanzar).