Skip to content

Publishing Packages

Introduction

As a maintainer, there are many options available to you on how you setup your publishing workflow. This may be manual, semi-automated, or fully automated.

With the rise of supply-chain attacks, it is more important than ever to ensure that your publishing workflow is secure. Maintainers need to take extra care to ensure malicious code and people cannot compromise their packages via the publishing workflow.

In this document, we will cover a typical workflow with security in mind, and some recipes for more advanced workflows.

Prerequisites

Enforcing 2FA

Before anything, it is important to enable 2FA in your various accounts and use a secure authentication method.

In your npm account, navigate to the Account page and ensure you have 2FA enabled. Ideally, use a security key, otherwise use an authenticator app.

Similarly, in your GitHub account, navigate to Settings > Password and authentication and ensure you have 2FA enabled. Again, ideally use a security key, otherwise use an authenticator app.

We also highly recommend you manage your passwords with a password manager, and ideally generate them.

NOTE

A security key isn't necessarily a physical key (e.g. YubiKey), it can also be something your OS provides, like Windows Hello or macOS Touch ID.

Setting up the GitHub repository

Next, we need to ensure your GitHub repository is setup securely.

Actions Settings

Navigate to Settings > Actions > General and configure:

  • ✓ Require actions to be pinned to a full-length commit SHA
  • ✓ Require approval for first-time contributors
  • ✓ Set default workflow permissions to "Read repository contents and packages permissions"

These settings ensure that outside contributors cannot run arbitrary code in your workflows without your approval first, and that all actions used in your workflows are pinned to a specific commit SHA, preventing supply-chain attacks through action updates.

NOTE

If your repository is part of an organization, we highly recommend you make these changes to the organisation as a whole rather than this specific repository. This will ensure all repositories in the organisation have a consistent security measures in place.

Branch Protection

Navigate to Settings > Branches and configure:

  • ✓ Create a ruleset for your default branch (e.g. main)
  • ✓ Require a pull request before merging
  • ✓ Require approvals (set to at least 1)
  • ✓ Dismiss stale pull request approvals when new commits are pushed
  • ✓ Require approval of the most recent reviewable push

This ensures that nobody can push directly to the main branch, and that all changes are reviewed before being merged.

Secrets & Variables

Navigate to Settings > Secrets & Variables > Actions:

  • Remove all unnecessary repository secrets (e.g. npm tokens, as OIDC can be used instead)

Setting up Trusted Publishing

Now we need to configure trusted publishing for our npm package. This will ensure that only workflows we trust can publish new versions of our package.

Navigate to your npm package page, and click on the "Settings" tab. You will now see a "Trusted Publishing" section.

In this "Trusted Publishing" section, setup a trusted publisher for the workflow you're about to create with the following settings:

  • Organisation and repository are the ones you're publishing from
  • Workflow filename is the name of the workflow file you'll create (e.g. publish.yml)

While you're in there, also check the box for "Require two-factor authentication and disallow tokens (recommended)". This will ensure that manual publishing must use 2FA.

TIP

It can be a slow job opening all of your packages individually and changing these settings. To assist with this, you can use the open-packages-on-npm tool in your local repository to open the package(s) on npm, each in a new tab. You can then use this userscript to quickly update the trusted publisher settings on each page.

Standard Workflow

Next, we need to create the GitHub workflow that will handle the publishing.

For this, let's use a template from the setup-publish repository:

publish.yml

In this file, you'll see we have the following jobs:

  • Test (runs tests)
  • Build (builds the package)
  • Publish (publishes the already built package)

There are a few important things to note about this workflow:

  • The package is built in a separate job to publishing, ensuring the publish permissions are not exposed to runtime build code
  • All actions are pinned to a full-length commit SHA
  • Install is done with --ignore-scripts to avoid running any lifecycle scripts that may be malicious

When updating the workflow, keep these constraints in mind to ensure the security of your publishing process.

TIP

It is also worth setting ignore-scripts=true in your project's .npmrc file so this applies to all installs, not just in the workflow. Similarly, we highly recommend you do this for your local user using npm config set -g ignore-scripts true.

Creating a Release

To publish a new version of your package, you can now create a tag against main:

bash
git tag v1.0.0
git push origin v1.0.0

Then in the GitHub UI, navigate to the "Releases" page and click "Draft a new release".

You can now choose the tag you just pushed, and click "Generate release notes" to automatically generate the changelog from your commit history.

Finally, click "Publish release" to trigger the workflow and publish your package.

Alternative Workflows

The @e18e/setup-publish CLI can help you set up the above workflow and a few other useful workflows in your repository.

You can use it with npx:

bash
npx @e18e/setup-publish

This CLI uses a set of community defined workflow templates to help guide you in setting up your own workflow.

Using changesets for versioning and changelog generation

If you'd prefer to use changesets to manage your versioning and changelog generation, you can use the changesets template.

This template differs from the basic workflow in a few ways:

  • Releases are created by merging a generated changesets release pull request to main
  • All other merged pull requests will automatically update the release pull request
  • The changelog is generated by changesets, and included in the release pull request
  • You no longer need to manually create releases or tags in GitHub

Using changelogithub for changelog generation

You can also use changelogithub to generate your changelog from commit messages. You can set this up using the changelogithub template.

In this workflow:

  • You must still push tags manually
  • Any time a tag is pushed, a GitHub release is created with a changelog generated from commit messages
  • Any time a tag is pushed, the package is published

Maintenance & Tooling

Managing dependency updates

We also highly recommend setting up dependabot or renovate to keep your dependencies up to date.

This will ensure that any security vulnerabilities in your dependencies are addressed promptly.

Keeping actions up to date

It is important to keep your GitHub actions up to date, and always reference them using a full-length commit SHA.

To assist with switching to using full-length commit SHAs, you can use the actions-up CLI.

bash
npx actions-up

This will update your workflow files to use the latest commit SHA for each action.

Once you've made the switch, you can then use dependabot, renovate or actions-up itself to keep your actions up to date.

Linting workflows for issues and vulnerabilities

You can also use zizmor to lint your GitHub workflows for common issues. This will find things like template injection vulnerabilities, and excessive permission scopes.

For example, to lint your publish.yml workflow:

bash
zizmor .github/workflows/publish.yml

Validating package configuration

Use the publint tool to lint your package for common publishing issues. This will find things like missing files, incorrect package.json fields, and much more.

You can view the full list of rules in the publint documentation.

To run this, simply execute:

bash
npx publint

Visualising dependency changes

You can use multiocular to visualise changes in your dependencies between versions.

This will give you a visualisation of what code has changed when a dependency updated, which can help you identify potential security issues and breaking changes.

Further Security

Use an environment with required reviewers

It is possible to specify an environment in your workflow:

publish.yml
yaml
jobs:
  publish:
    environment: production

In GitHub, you can then configure this environment to require manual approval before the job can proceed. This ensures that even if you manage to trigger the workflow, a human still needs to review and approve the job before it can publish.

IMPORTANT

If you do this, ensure that you set the environment in your npm trusted publishing settings too.

Use hardware security keys

Physical security keys (like YubiKey) provide strong 2FA protection and are generally much more secure than using an authenticator app or SMS.

Use protection rules in GitHub

We already noted that main should have a protection rule to prevent unreviewed changes. It would also be a good idea to add similar rules to any other long-lived branches you may have, and to all tags.

Use immutable releases

Enable immutable releases on GitHub and this will prevent any changes to tags or GitHub releases after they are created. This will ensure that once a release is created, it cannot be modified or deleted.

Sole Maintainer Considerations

There's currently a feature request and ongoing discussion about supporting 2FA for workflow approvals on GitHub.

Until this is supported, using trusted publishing can actually be less secure for solo-maintainers than publishing locally with 2FA enabled.

If your GitHub token leaks somehow, an attack could publish through your trusted workflow the same way a legitimate release would.

Once environment approvals support 2FA, this will no longer be a concern as the attacker would need to pass the 2FA check to approve the workflow.

For these reasons, if you're a solo maintainer, you may want to consider publishing locally with 2FA enabled until this feature is available. Having said that, please ensure you still follow all other security recommendations in this document.

Released under the MIT License. (0d8c024a)