Skip to content
Draft
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
161 changes: 161 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: Release
on:
push:
branches:
- main
permissions: {}
jobs:
build:
name: Check version and build package
# Keep this guard on every job for defense-in-depth in case job dependencies are refactored.
if: ${{ !github.event.repository.fork && github.repository == 'graphql/graphql-relay-js' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.metadata.outputs.version }}
dist_tag: ${{ steps.metadata.outputs.dist_tag }}
should_publish: ${{ steps.metadata.outputs.should_publish }}
permissions:
contents: read # for actions/checkout
pull-requests: read # for gen-changelog.js
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
# gen-changelog.js shells out to 'git rev-list' against release tags,
# so it needs full history rather than the default shallow clone.
fetch-depth: 0
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v4
Comment on lines +22 to +30

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These versions are a little old, I think v7 is latest?

with:
cache: npm
node-version-file: '.node-version'

- name: Read release metadata
id: metadata
run: |
version="$(node -p 'require("./package.json").version')"
echo "version=${version}" >> "${GITHUB_OUTPUT}"

# A prerelease must never take the 'latest' dist-tag.
case "${version}" in
*-*) dist_tag='next' ;;
*) dist_tag='latest' ;;
esac
echo "dist_tag=${dist_tag}" >> "${GITHUB_OUTPUT}"

# Publishing is driven entirely by whether this version is already on
# npm, so ordinary merges to main are a no-op and re-runs are safe.
# 'npm view pkg versions' returns a bare string when only one version
# exists, hence the array coercion.
versions_json="$(npm view graphql-relay versions --json)"
if jq -e --arg v "${version}" \
'(if type == "array" then . else [.] end) | index($v)' \
> /dev/null <<< "${versions_json}"
then
echo "graphql-relay@${version} is already published. Nothing to do."
echo "should_publish=false" >> "${GITHUB_OUTPUT}"
else
echo "graphql-relay@${version} is not published yet."
echo "should_publish=true" >> "${GITHUB_OUTPUT}"
fi

- name: Install Dependencies
if: steps.metadata.outputs.should_publish == 'true'
run: npm ci

- name: Run tests
if: steps.metadata.outputs.should_publish == 'true'
run: npm run test

- name: Build npm package
if: steps.metadata.outputs.should_publish == 'true'
run: npm run build

- name: Upload npmDist package
if: steps.metadata.outputs.should_publish == 'true'
uses: actions/upload-artifact@v4
with:
name: npmDist
path: ./npmDist

# Generated here rather than in the publish job so that a PR missing its
# 'PR: *' label fails the release before anything reaches npm. Needs the
# dependencies installed above, since resources/utils.js requires prettier.
- name: Generate release notes
if: steps.metadata.outputs.should_publish == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node resources/gen-changelog.js > ./release-notes.md

- name: Upload release notes
if: steps.metadata.outputs.should_publish == 'true'
uses: actions/upload-artifact@v4
with:
name: release-notes
path: ./release-notes.md

publish:
name: Publish to npm and create release
needs: build
# Keep this guard on every job for defense-in-depth in case job dependencies are refactored.
if: ${{ !github.event.repository.fork && github.repository == 'graphql/graphql-relay-js' && needs.build.outputs.should_publish == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write # for creating the tag and GitHub release
id-token: write # for npm trusted publishing via OIDC
steps:
# This job holds the OIDC token, so it deliberately never runs 'npm ci':
# no dependency lifecycle scripts execute while the token is available.
# It only publishes the artifact the build job already produced.
- name: Checkout repo
uses: actions/checkout@v4
with:
persist-credentials: false

# Trusted publishing needs npm >= 11.5.1, which is newer than the npm
# bundled with some Node releases. '.node-version' is pinned accordingly.
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'

- name: Download npmDist package
uses: actions/download-artifact@v4
with:
name: npmDist
path: ./npmDist

- name: Download release notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .

- name: Publish to npm
working-directory: ./npmDist
env:
DIST_TAG: ${{ needs.build.outputs.dist_tag }}
run: npm publish --provenance --tag "${DIST_TAG}"

- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.build.outputs.version }}
DIST_TAG: ${{ needs.build.outputs.dist_tag }}
run: |
tag="v${VERSION}"

latest_flag='--latest=false'
if [ "${DIST_TAG}" = 'latest' ]; then
latest_flag='--latest'
fi

# '--target' creates the tag at the commit that was actually built,
# so a tag can never disagree with what was published.
gh release create "${tag}" \
--target "${GITHUB_SHA}" \
--title "${tag}" \
--notes-file ./release-notes.md \
"${latest_flag}"
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v24
57 changes: 40 additions & 17 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,52 @@
## Publishing

This package uses a custom publish flow.
## Pull requests

**IMPORTANT**: Do not push directly to `main` - every change must go through a PR
otherwise changelog generation will fail.

Every PR must carry exactly one `PR: *` label before it is merged. Changelog
generation reads these labels and throws if a merged PR is missing one:

- `PR: breaking change 💥`
- `PR: feature 🚀`
- `PR: bug fix 🐞`
- `PR: docs 📝`
- `PR: polish 💅`
- `PR: internal 🏠`
- `PR: dependency 📦`

## Publishing

Releases are cut by the `Release` workflow. Merging to `main` is the only gate:
the workflow checks whether the version in `package.json` is already on npm and
publishes it if it is not. Ordinary merges are therefore a no-op, and a release
is just a merged version bump.

Publishing uses [trusted publishing][], so no npm token is involved. The workflow
builds the package, publishes it with provenance, creates the `vX.Y.Z` tag, and
opens a GitHub release.

To cut a release, open a PR that bumps the version and merge it:

```sh
git checkout -b release-VERSION_NUMBER
npm install
npm version patch # or minor or major
git push --follow-tags
npm run build
cd npmDist
npm publish --tag=next
npm version VERSION_NUMBER --no-git-tag-version
git commit --all --message VERSION_NUMBER
```

Then test it by installing `graphql-relay@next` from npm...
Label the PR `PR: internal 🏠` and merge once CI is green. That is the whole
release - do not tag or run `npm publish` by hand.

All good? Publish:
A version containing a `-` is treated as a prerelease and published under the
`next` dist-tag instead of `latest`, so a release can be rehearsed with a version
like `0.11.1-rc.0`.

```sh
npm dist-tags add graphql-relay@VERSION_NUMBER latest
```
Release notes are generated from the `PR: *` labels by
`resources/gen-changelog.js` and attached to the GitHub release - there is no
`CHANGELOG.md` file. A merged PR missing its label fails the release before
anything reaches npm, so add the label and re-run.

Finally generate the CHANGELOG:
If a publish fails partway through, re-run the workflow. It re-checks npm before
doing anything, so re-runs are safe.

```sh
node resources/gen-changelog.js
```
[trusted publishing]: https://docs.npmjs.com/trusted-publishers
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"unbase",
"unbased",

// Used by the release workflow
"OIDC",

// Different words used inside tests

"corellian",
Expand Down
Loading