Skip to content
Open
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
54 changes: 54 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Publish to npm
on:
workflow_call:
secrets:
NPM_PUBLISH:
required: true

permissions:
id-token: write
contents: read

jobs:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely will like to pin dependencies here as our tokens are used in this workflow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing that in a couple minutes !

install:
runs-on: ubuntu-latest
environment: publish
permissions:
contents: read
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v6
Comment on lines +20 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
steps:
- name: Checkout
uses: actions/checkout@v6
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@v6

Obviously we need to use egress-policy: block once the allowedlist is clear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely! Nothing against that - in the contrary it will be way better.

As this is not yet setup, I only included the base workflow


- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Version
if: github.event_name == 'release' && github.event.action == 'created'
run: |
VERSION=${{ github.event.release.tag_name }}
VERSION=${VERSION:1}
CURRENT_VERSION=$(npm pkg get version | tr -d '"')
if [ "$CURRENT_VERSION" != "$VERSION" ]; then
npm version $VERSION --no-git-tag-version
else
echo "Version already set to $VERSION, skipping npm version command"
fi

- name: Wait for 2FA
uses: step-security/wait-for-secrets@v1
id: wait-for-secrets
with:
secrets: |
OTP:
name: 'OTP to publish package'
description: 'NPM 2FA'

- name: publish
run: |
export NODE_AUTH_TOKEN=${{ secrets.NPM_PUBLISH }}
npm publish --otp ${{ steps.wait-for-secrets.outputs.OTP }} --access public
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
# ci-workflows
Repository containing shared set of reusable workflows for the Expressjs organization
# Shared GitHub Workflows

This repository contains reusable GitHub Actions workflows for the Express.js organization shared across multiple repositories.

The purpose of this repository is to centralize common automation logic, reduce duplication, and ensure consistent CI/CD practices.

Each workflow is designed to be generic and reusable. Detailed documentation for individual workflows is provided in dedicated files inside the `docs` folder.

## Available workflows

- [Publish](./docs/publish.md)
122 changes: 122 additions & 0 deletions docs/publish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Publish

This workflow publishes a Node.js package to the npm registry.
It is designed to be reused across repositories via `workflow_call`.

[workflow definition](../.github/workflows/publish.yml)

## Purpose

The workflow provides a standardized and secure way to publish packages to npm, including:

- Node.js setup
- Optional version alignment with a GitHub release tag
- Support for npm 2FA using a one-time password (OTP)

## Trigger

This workflow is triggered using `workflow_call` from another repository.

## Required secrets

| Secret name | Description | Required |
|-----------------|--------------------------------------|----------|
| `NPM_PUBLISH` | npm token with publish permissions | Yes |

## Permissions

The workflow requires the following permissions:

- `contents: read` – to read repository contents
- `id-token: write` – to support secure authentication flows

## npm token requirements

For security reasons, the npm token used by this workflow must follow these rules:

- Use a **granular npm token** scoped only to the package(s) being published
- The token must have **publish-only permissions**
- The token should be **added shortly before each publish**
- The token must be **revoked immediately after the deployment completes**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this needs to be automated (high chances we forget as local logout) but the CLI requires the ID: npm token revoke <token-id> so in that case we need to add an additional secret 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be great to not need a specific PAT token for that :) I am still hoping that we can have tokens that are deleted automatically after a short time or after a number of usage (1 would be perfect).

For this workflows, I am not sure about the case to have a token to delete a token, and then delete the token used to delete the token 😅


## Environment protection

This workflow is executed in the `publish` environment.

Using a dedicated environment allows:

- Restricting access to sensitive secrets
- Enforcing manual approvals before publishing
- Applying environment-specific security rules

Secrets required for publishing must be scoped to this environment.

## Environment secret setup

To configure the `publish` environment and its secrets in GitHub:

1. Go to the repository **Settings**
2. Navigate to **Environments**
3. Click **New environment**
4. Create an environment named `publish`
5. Under **Environment secrets**, add:
- `NPM_PUBLISH` with the npm publish token
6. Configure **Deployment branch restrictions**
7. (Optional) Configure:
- Required reviewers
- Wait timers

Only workflows explicitly targeting `environment: publish` will be able to access these secrets.

## Behavior
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some repos (probably more in the future) that might not work with this approach as they require a build step before release like codemod: (https://github.com/expressjs/codemod/blob/35e5d273b5530b4a1e2352cc849612ee39d929b6/package.json#L29)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of allowing the call of the action with a specific npm script to run for that. Would you have anything against ? Or do you see a specific way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t need to worry about codemod, because it has a different setup for publishing (see https://github.com/expressjs/codemod/blob/main/.github/workflows/publish.yml and expressjs/codemod#100), which we’re going to start using soon.


1. **Checkout repository**
- Retrieves the package source code.

2. **Setup Node.js**
- Uses Node.js version `24`
- Configures the npm registry to `https://registry.npmjs.org`

3. **Version synchronization (optional)**
- If triggered by a GitHub release creation:
- Extracts the version from the release tag (expects `vX.Y.Z`)
- Updates `package.json` if the version differs

4. **Wait for npm 2FA**
- Pauses execution until a one-time password (OTP) is provided
- Required when npm 2FA is enabled for publishing

5. **Publish**
- Publishes the package to npm using:
- The provided npm token
- The supplied OTP
- Public access

## Example usage

```yaml
name: Publish package to npm
on:
release:
types: [created]

permissions:
id-token: write
contents: read

jobs:
publish:
uses: expressjs/ci-workflows/.github/workflows/release.yml@main
secrets:
NPM_PUBLISH: ${{ secrets.NPM_PUBLISH }}
```

## Security overview

## npm token configuration and lifecycle management

Securing the npm publishing process is a critical aspect of modernizing Express’ supply-chain security. Publishing credentials represent a high-value attack surface, particularly when used in automated CI/CD environments.

To mitigate this risk, npm tokens used for publication must follow a strict lifecycle and configuration model. Granular tokens are scoped to a single package, enforce mandatory two-factor authentication, and are issued only for the shortest valid duration required to complete a release. Tokens are added immediately before publication and revoked as soon as the release process completes, ensuring no long-lived credentials persist beyond their intended use.

This approach significantly reduces the blast radius of potential CI compromises and aligns Express’ release process with modern supply-chain security best practices, while maintaining a practical and auditable workflow for maintainers.