Skip to content

Commit 00b033b

Browse files
committed
internal: add 17.x.x publish automation
1 parent 879d43f commit 00b033b

File tree

7 files changed

+389
-26
lines changed

7 files changed

+389
-26
lines changed

.github/CONTRIBUTING.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,28 +86,18 @@ Feel free to reach out via the [graphql-js channel](https://discord.com/channels
8686

8787
## Release on NPM
8888

89-
_Only core contributors may release to NPM._
90-
91-
To release a new version on NPM, first ensure all tests pass with `npm test`,
92-
then use `npm version patch|minor|major` in order to increment the version in
93-
package.json and tag and commit a release. Then `git push && git push --tags`
94-
to sync this change with source control. Then `npm publish npmDist` to actually
95-
publish the release to NPM.
96-
Once published, add [release notes](https://github.com/graphql/graphql-js/releases).
97-
Use [semver](https://semver.org/) to determine which version part to increment.
98-
99-
Example for a patch release:
100-
101-
```sh
102-
npm ci
103-
npm test
104-
npm version patch --ignore-scripts=false
105-
git push --follow-tags
106-
cd npmDist && npm publish
107-
npm run changelog
89+
Releases on `17.x.x` are managed by local scripts and GitHub Actions:
90+
91+
```bash
92+
git switch 17.x.x
93+
git switch -c <my_release_branch>
94+
export GH_TOKEN=<token> # required to build changelog via GitHub API requests
95+
npm run release:prepare -- 17.x.x patch
10896
```
10997

110-
Then upload the changelog to [https://github.com/graphql/graphql-js/releases](https://github.com/graphql/graphql-js/releases).
98+
Push `<my_release_branch>`, open a PR from `<my_release_branch>` to `17.x.x`, wait for CI to pass, and merge.
99+
100+
After merge, GitHub Actions runs the publish flow automatically. It is currently configured as a dry-run for testing.
111101

112102
## License
113103

.github/workflows/release.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Release
2+
on:
3+
push:
4+
branches:
5+
- 17.x.x
6+
permissions: {}
7+
jobs:
8+
check-publish:
9+
name: Check for publish need and prepare artifacts
10+
# Keep this guard on every job for defense-in-depth in case job dependencies are refactored.
11+
if: ${{ !github.event.repository.fork && github.repository == 'graphql/graphql-js' && github.ref_name == '17.x.x' }}
12+
runs-on: ubuntu-latest
13+
outputs:
14+
should_publish: ${{ steps.release_metadata.outputs.should_publish }}
15+
tag: ${{ steps.release_metadata.outputs.tag }}
16+
tarball_name: ${{ steps.release_metadata.outputs.tarball_name }}
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref_name }}
19+
cancel-in-progress: true
20+
permissions:
21+
contents: read # for actions/checkout
22+
steps:
23+
- name: Checkout repo
24+
uses: actions/checkout@v4
25+
with:
26+
persist-credentials: false
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
cache: npm
32+
node-version-file: '.node-version'
33+
34+
- name: Install Dependencies
35+
run: npm ci --ignore-scripts
36+
37+
- name: Read release metadata
38+
id: release_metadata
39+
run: |
40+
npm run --silent release:metadata >> "${GITHUB_OUTPUT}"
41+
42+
- name: Log publish decision
43+
run: |
44+
if [ "${{ steps.release_metadata.outputs.should_publish }}" = "true" ]; then
45+
echo "${{ steps.release_metadata.outputs.package_spec }} is not published yet."
46+
else
47+
echo "${{ steps.release_metadata.outputs.package_spec }} is already published."
48+
fi
49+
50+
- name: Build NPM package
51+
if: steps.release_metadata.outputs.should_publish == 'true'
52+
run: npm run build:npm
53+
54+
- name: Pack npmDist package
55+
if: steps.release_metadata.outputs.should_publish == 'true'
56+
run: npm pack ./npmDist --pack-destination . > /dev/null
57+
58+
- name: Upload npm package tarball
59+
if: steps.release_metadata.outputs.should_publish == 'true'
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: npmDist-tarball
63+
path: ./${{ steps.release_metadata.outputs.tarball_name }}
64+
65+
publish-release:
66+
name: Publish, tag, and create release
67+
needs: check-publish
68+
# Keep this guard on every job for defense-in-depth in case job dependencies are refactored.
69+
if: ${{ !github.event.repository.fork && github.repository == 'graphql/graphql-js' && github.ref_name == '17.x.x' && needs.check-publish.outputs.should_publish == 'true' && needs.check-publish.result == 'success' }}
70+
runs-on: ubuntu-latest
71+
environment: release
72+
permissions:
73+
contents: write # for creating/pushing tag and creating GitHub release
74+
id-token: write # for npm trusted publishing via OIDC
75+
steps:
76+
- name: Checkout repo
77+
uses: actions/checkout@v4
78+
with:
79+
persist-credentials: false
80+
81+
- name: Setup Node.js
82+
uses: actions/setup-node@v4
83+
with:
84+
node-version-file: '.node-version'
85+
86+
- name: Download npmDist package
87+
uses: actions/download-artifact@v4
88+
with:
89+
name: npmDist-tarball
90+
path: ./artifacts
91+
92+
- name: Verify package tarball is present
93+
run: |
94+
tarball="./artifacts/${{ needs.check-publish.outputs.tarball_name }}"
95+
if [ ! -f "${tarball}" ]; then
96+
echo "::error::Expected package tarball ${tarball} is missing."
97+
exit 1
98+
fi
99+
100+
- name: Create release if needed
101+
env:
102+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103+
run: |
104+
release_notes_file="./artifacts/release-notes.md"
105+
gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}" --jq '.commit.message' > "${release_notes_file}"
106+
if [ ! -s "${release_notes_file}" ]; then
107+
printf '## Release %s\n' "${{ needs.check-publish.outputs.tag }}" > "${release_notes_file}"
108+
fi
109+
110+
if gh release view "${{ needs.check-publish.outputs.tag }}" > /dev/null 2>&1; then
111+
echo "GitHub release ${{ needs.check-publish.outputs.tag }} already exists."
112+
else
113+
gh release create "${{ needs.check-publish.outputs.tag }}" \
114+
--target "${GITHUB_SHA}" \
115+
--title "${{ needs.check-publish.outputs.tag }}" \
116+
--notes-file "${release_notes_file}"
117+
fi
118+
119+
- name: Dry-run npm publish
120+
run: npm publish --provenance --dry-run "./artifacts/${{ needs.check-publish.outputs.tarball_name }}"

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"version": "node --import ./resources/register-ts-node.js resources/gen-version.ts && npm test && git add src/version.ts",
3434
"fuzzonly": "mocha --full-trace src/**/__tests__/**/*-fuzz.ts",
3535
"changelog": "node --import ./resources/register-ts-node.js resources/gen-changelog.ts",
36+
"release:prepare": "node --import ./resources/register-ts-node.js resources/release-prepare.ts",
37+
"release:metadata": "node --import ./resources/register-ts-node.js resources/release-metadata.ts",
3638
"benchmark": "node --import ./resources/register-ts-node.js resources/benchmark.ts",
3739
"test": "npm run lint && npm run check && npm run testonly:cover && npm run prettier:check && npm run check:spelling && npm run check:integrations",
3840
"lint": "eslint --cache --max-warnings 0 .",

resources/gen-changelog.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,37 @@ const { githubOrg, githubRepo } = repoURLMatch.groups;
5555

5656
process.stdout.write(await genChangeLog());
5757

58+
function parseFromRevArg(rawArgs: ReadonlyArray<string>): string | null {
59+
if (rawArgs.length === 0) {
60+
return null;
61+
}
62+
63+
if (rawArgs.length === 1 && rawArgs[0].trim() !== '') {
64+
return rawArgs[0];
65+
}
66+
67+
throw new Error(
68+
'Usage: npm run changelog [-- <fromRev>]\n' +
69+
'Example: npm run changelog -- d41f59bbfdfc207712a2fc3778934694a3410ddf',
70+
);
71+
}
72+
5873
async function genChangeLog(): Promise<string> {
5974
const { version } = packageJSON;
6075
const releaseTag = `v${version}`;
76+
const releaseTagExists = git().tagExists(releaseTag);
6177
let tag: string | null = null;
62-
let commitsList: Array<string>;
63-
try {
64-
commitsList = git().revList('--reverse', `${releaseTag}..`);
65-
} catch {
78+
let baseRef = parseFromRevArg(process.argv.slice(2));
79+
if (releaseTagExists) {
80+
baseRef ??= releaseTag;
81+
tag = releaseTag;
82+
} else {
6683
const parentPackageJSON = git().catFile('blob', 'HEAD~1:package.json');
6784
const parentVersion = JSON.parse(parentPackageJSON).version;
68-
commitsList = git().revList('--reverse', `v${parentVersion}..HEAD~1`);
69-
tag = releaseTag;
85+
baseRef = `v${parentVersion}`;
7086
}
87+
const commitsRange = releaseTagExists ? `${baseRef}..` : `${baseRef}..HEAD~1`;
88+
const commitsList = git().revList('--reverse', commitsRange);
7189

7290
const allPRs = await getPRsInfo(commitsList);
7391
const date = git().log('-1', '--format=%cd', '--date=short');

resources/release-metadata.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { npm, readPackageJSON } from './utils.js';
2+
3+
try {
4+
const packageJSON = readPackageJSON();
5+
const { version } = packageJSON;
6+
7+
if (typeof version !== 'string' || version === '') {
8+
throw new Error('package.json is missing a valid "version" field.');
9+
}
10+
11+
const tag = `v${version}`;
12+
const packageSpec = `graphql@${version}`;
13+
const tarballName = `graphql-${version}.tgz`;
14+
15+
const versionsJSON = npm().view('graphql', 'versions', '--json');
16+
const parsedVersions = JSON.parse(versionsJSON) as unknown;
17+
const versions = Array.isArray(parsedVersions)
18+
? parsedVersions
19+
: [parsedVersions];
20+
const shouldPublish = versions.includes(version) ? 'false' : 'true';
21+
22+
process.stdout.write(`version=${version}\n`);
23+
process.stdout.write(`tag=${tag}\n`);
24+
process.stdout.write(`package_spec=${packageSpec}\n`);
25+
process.stdout.write(`tarball_name=${tarballName}\n`);
26+
process.stdout.write(`should_publish=${shouldPublish}\n`);
27+
} catch (error) {
28+
const message = error instanceof Error ? error.message : String(error);
29+
process.stderr.write(message + '\n');
30+
process.exit(1);
31+
}

0 commit comments

Comments
 (0)