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
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
"$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
"changelog": ["@changesets/changelog-github", {"repo": "EventSource/eventsource"}],
"commit": false,
"format": "oxfmt",
"fixed": [],
"linked": [],
"access": "public",
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/changeset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ name: Changeset

on:
pull_request:
branches: [main]
# Keep in sync with the release workflow: backports to a `vN` maintenance branch
# need a changeset just as much as changes to `main` do.
branches: [main, 'v[0-9]+']

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
82 changes: 73 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ name: Release

on:
push:
branches: [main]
# `v[0-9]+` are the maintenance branches (`v4` today) that supported older majors
# are patched from. The archived `v1.x`/`v2.x` branches predate the convention and
# deliberately do not match.
branches: [main, 'v[0-9]+']
workflow_dispatch:

concurrency:
Expand All @@ -14,14 +17,29 @@ jobs:
name: 'Release: Version PR or publish to npm'
# `workflow_dispatch` can be triggered on any ref, and npm's trusted publisher
# config pins the workflow file but not the branch. Without this, dispatching
# from a feature branch could version and publish from an unreviewed ref.
if: github.ref == 'refs/heads/main'
# from a feature branch could version and publish from an unreviewed ref. The
# `Resolve release channel` step tightens this to the exact branch names, since
# expressions here cannot match a pattern.
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v')
runs-on: ubuntu-latest
permissions:
contents: write # to push the version commit and tag, and create the GitHub release
pull-requests: write # to open and update the "Version Packages" pull request
id-token: write # to authenticate with npm through OIDC (trusted publishing)
steps:
# Runs before checkout so an unexpected ref never gets as far as building.
- name: Resolve release channel
id: channel
run: |
if [ "$GITHUB_REF_NAME" = 'main' ]; then
echo 'npm-tag=latest' >> "$GITHUB_OUTPUT"
elif [[ "$GITHUB_REF_NAME" =~ ^v[0-9]+$ ]]; then
echo "npm-tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
else
echo "::error::Refusing to release from '$GITHUB_REF_NAME'. Releases run from 'main' or a 'vN' maintenance branch."
exit 1
fi

- uses: actions/checkout@v7
with:
fetch-depth: 0
Expand All @@ -39,11 +57,57 @@ jobs:
- run: npm ci

- name: Create version pull request or publish to npm
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
id: changesets
uses: changesets/action@22ccf9aa43179fe9e27dc62e575971d28cce197c # v2.0.0
with:
version: npm run version:packages
publish: npm run release
commit: 'chore(release): version packages'
title: 'chore(release): version packages'
version-script: npm run version:packages
# `--tag` keeps backports off the `latest` dist-tag: `main` publishes to
# `latest`, `v4` publishes to `v4`, and so on.
#
# The action discovers what was published by reading the file named in the
# `CHANGESETS_OUTPUT` environment variable, which it expects the script to
# pass through to the changesets CLI. `npm run` forwards the environment, so
# this works as-is - but don't wrap it in anything that drops env vars.
publish-script: npm run release -- --tag ${{ steps.channel.outputs.npm-tag }}
commit-message: 'chore(release): version packages'
pr-title: 'chore(release): version packages'

# `changesets/action` calls the create-release API without `make_latest`, and that
# parameter defaults to true - so a backport would silently take the "Latest" badge
# off the current major. Clear it on everything we just published, then assert the
# repository's latest release is not one of them.
#
# This corrects the flag rather than never setting it: there are a few seconds
# between the action creating the release and this step running. Closing that
# window would mean turning off `create-github-releases` and rebuilding the
# changelog-derived release notes by hand, which risks the worse failure of
# publishing to npm with no GitHub release at all. The assertion below is what
# makes the end state guaranteed - if the demotion does not stick, the job fails
# loudly instead of leaving the badge on the wrong major.
#
# `make_latest` is a string enum ("true" / "false" / "legacy"), so it goes through
# `gh api -f` rather than `gh release edit --latest=false`, which would depend on
# how the runner's gh build handles negated boolean flags.
- name: Keep backport releases off "Latest"
if: steps.channel.outputs.npm-tag != 'latest' && steps.changesets.outputs.published == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.published-packages }}
run: |
# Single-package repo, so changesets tags releases `v<version>`.
mapfile -t tags < <(jq -r '.[] | "v\(.version)"' <<< "$PUBLISHED_PACKAGES")

for tag in "${tags[@]}"; do
id=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$tag" --jq '.id')
echo "Clearing latest flag on $tag (release $id)"
gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$id" -f make_latest=false >/dev/null
done

latest=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null || echo '<none>')
echo "Latest release is now $latest"
for tag in "${tags[@]}"; do
if [ "$latest" = "$tag" ]; then
echo "::error::$tag is still flagged as the latest release."
exit 1
fi
done
20 changes: 19 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,25 @@ Changes that do not affect the published package - docs, tests, CI, internal ref

Maintainers only. When changesets land on `main`, the release workflow opens a "Version Packages" pull request that applies the version bump and updates the changelog. Merging that pull request publishes to npm, pushes the git tag and creates the GitHub release.

Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) over OIDC, so there is no npm token to rotate. The trusted publisher is configured on npm against the `release.yml` workflow in this repository - renaming that file will break publishing until the npm setting is updated to match.
Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) over OIDC, so there is no npm token to rotate. The trusted publisher is configured on npm against the `release.yml` workflow in this repository - renaming that file will break publishing until the npm setting is updated to match. The npm configuration pins the workflow file but not the branch, which is what lets the same workflow release from maintenance branches.

### Releasing a fix for an older major

Older majors that are still supported are patched from a long-lived `vN` branch - currently just `v4`, for the 4.x line. It sits at the latest release of that major and carries its own changesets configuration, so the release workflow behaves there exactly as it does on `main`:

1. Branch off the maintenance branch, e.g. `git switch -c fix/some-backport v4`.
2. Apply the fix and add a changeset (`npm run changeset`). Use `patch` or `minor` - a backport must never be a `major`, since that would collide with a version that already exists on a newer line.
3. Open the pull request **against the maintenance branch**, not `main`.
4. Merging it opens a "Version Packages" pull request against that same branch. Merging that one publishes.

Two things differ from a release off `main`:

- The npm dist-tag matches the branch, so a 4.x release publishes under `v4` rather than `latest`. Users on that line install it with `npm install eventsource@v4`, and `npm install eventsource` keeps resolving to the current major.
- The GitHub release is demoted from "Latest" after publishing, so the newest major keeps that badge.

Both are handled by the release workflow; there is nothing to pass by hand. If the fix also applies to the current major, land it on `main` separately - nothing is merged forward automatically.

The workflow's branch filter accepts any `vN` branch, so a `v5` branch can be cut the same way once `main` moves on to 6.x. The archived `v1.x` and `v2.x` branches predate the convention, do not match the filter, and are not released from. 3.x is no longer supported - see [SECURITY.md](./SECURITY.md).

# How to file a security issue

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
| ------- | ------------------ |
| 5.x.x | :white_check_mark: |
| 4.x.x | :white_check_mark: |
| < 3.0 | :x: |
| < 4.0 | :x: |

## Reporting a Vulnerability

Expand Down
Loading