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
85 changes: 59 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
outputs:
should_publish: ${{ steps.release_metadata.outputs.should_publish }}
tag: ${{ steps.release_metadata.outputs.tag }}
dist_tag: ${{ steps.release_metadata.outputs.dist_tag }}
prerelease: ${{ steps.release_metadata.outputs.prerelease }}
tarball_name: ${{ steps.release_metadata.outputs.tarball_name }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
Expand All @@ -23,6 +25,10 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4
with:
# Keep checkout fast: we should only need to scroll back a few
# commits for release notes. If the release commit is older than
# this depth, release:metadata will emit empty release notes.
fetch-depth: 10
persist-credentials: false

- name: Setup Node.js
Expand All @@ -37,7 +43,17 @@ jobs:
- name: Read release metadata
id: release_metadata
run: |
npm run --silent release:metadata >> "${GITHUB_OUTPUT}"
release_metadata_json="$(npm run --silent release:metadata)"
jq -r '
"version=\(.version)",
"tag=\(.tag)",
"dist_tag=\(.distTag)",
"prerelease=\(.prerelease)",
"package_spec=\(.packageSpec)",
"tarball_name=\(.tarballName)",
"should_publish=\(.shouldPublish)"
' <<< "${release_metadata_json}" >> "${GITHUB_OUTPUT}"
jq -r '.releaseNotes' <<< "${release_metadata_json}" > ./release-notes.md

- name: Log publish decision
run: |
Expand All @@ -62,22 +78,23 @@ jobs:
name: npmDist-tarball
path: ./${{ steps.release_metadata.outputs.tarball_name }}

publish-release:
name: Publish, tag, and create release
- name: Upload release notes
if: steps.release_metadata.outputs.should_publish == 'true'
uses: actions/upload-artifact@v4
with:
name: release-notes
path: ./release-notes.md

publish-npm:
name: Publish npm package
needs: check-publish
# 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-js' && github.ref_name == '17.x.x' && needs.check-publish.outputs.should_publish == 'true' && needs.check-publish.result == 'success' }}
runs-on: ubuntu-latest
environment: release
permissions:
contents: write # for creating/pushing tag and creating GitHub release
id-token: write # for npm trusted publishing via OIDC
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
Expand All @@ -89,34 +106,50 @@ jobs:
name: npmDist-tarball
path: ./artifacts

- name: Verify package tarball is present
- name: Dry-run npm publish
run: |
tarball="./artifacts/${{ needs.check-publish.outputs.tarball_name }}"
if [ ! -f "${tarball}" ]; then
echo "::error::Expected package tarball ${tarball} is missing."
exit 1
if [ -n "${{ needs.check-publish.outputs.dist_tag }}" ]; then
npm publish --provenance --tag "${{ needs.check-publish.outputs.dist_tag }}" "./artifacts/${{ needs.check-publish.outputs.tarball_name }}"
else
npm publish --provenance "./artifacts/${{ needs.check-publish.outputs.tarball_name }}"
fi

- name: Create release if needed
create-release:
name: Create release
needs: check-publish
# 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-js' && github.ref_name == '17.x.x' && needs.check-publish.outputs.should_publish == 'true' && needs.check-publish.result == 'success' }}
runs-on: ubuntu-latest
environment: release
permissions:
contents: write # for creating GitHub release
steps:
- name: Download release notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: ./artifacts

- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
release_notes_file="./artifacts/release-notes.md"
gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}" \
--jq '.commit.message | split("\n")[1:] | join("\n") | ltrimstr("\n")' \
> "${release_notes_file}"
if [ ! -s "${release_notes_file}" ]; then
printf '## Release %s\n' "${{ needs.check-publish.outputs.tag }}" > "${release_notes_file}"
if gh release view "${{ needs.check-publish.outputs.tag }}" > /dev/null 2>&1; then
echo "GitHub release ${{ needs.check-publish.outputs.tag }} already exists. Skipping release creation."
exit 0
fi

if gh release view "${{ needs.check-publish.outputs.tag }}" > /dev/null 2>&1; then
echo "GitHub release ${{ needs.check-publish.outputs.tag }} already exists."
release_notes_file="./artifacts/release-notes.md"

if [ "${{ needs.check-publish.outputs.prerelease }}" = "true" ]; then
gh release create "${{ needs.check-publish.outputs.tag }}" \
--target "${GITHUB_SHA}" \
--title "${{ needs.check-publish.outputs.tag }}" \
--notes-file "${release_notes_file}" \
--prerelease
else
gh release create "${{ needs.check-publish.outputs.tag }}" \
--target "${GITHUB_SHA}" \
--title "${{ needs.check-publish.outputs.tag }}" \
--notes-file "${release_notes_file}"
fi

- name: Dry-run npm publish
run: npm publish --provenance --dry-run "./artifacts/${{ needs.check-publish.outputs.tarball_name }}"
11 changes: 3 additions & 8 deletions resources/gen-changelog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { git, readPackageJSON } from './utils.js';
import { git, readPackageJSON, readPackageJSONAtRef } from './utils.js';

const packageJSON = readPackageJSON();
const labelsConfig: { [label: string]: { section: string; fold?: boolean } } = {
Expand Down Expand Up @@ -70,11 +70,6 @@ function parseFromRevArg(rawArgs: ReadonlyArray<string>): string | null {
);
}

function readVersionFromPackageJSONAtRef(ref: string): string {
const packageJSONAtRef = git().catFile('blob', `${ref}:package.json`);
return JSON.parse(packageJSONAtRef).version;
}

function resolveChangelogRangeConfig(
workingTreeVersion: string,
fromRev: string | null,
Expand All @@ -98,7 +93,7 @@ function resolveChangelogRangeConfig(
};
}

const headVersion = readVersionFromPackageJSONAtRef('HEAD');
const headVersion = readPackageJSONAtRef('HEAD').version;
const headReleaseTag = `v${headVersion}`;

// Supported scenario 2: release preparation started
Expand All @@ -116,7 +111,7 @@ function resolveChangelogRangeConfig(
// - release preparation committed
// - working-tree version tag equal to HEAD version tag, both not yet created
// - HEAD~1 version tag exists
const parentVersion = readVersionFromPackageJSONAtRef('HEAD~1');
const parentVersion = readPackageJSONAtRef('HEAD~1').version;
const parentTag = `v${parentVersion}`;
const parentTagExists = git().tagExists(parentTag);
if (workingTreeReleaseTag === headReleaseTag && parentTagExists) {
Expand Down
65 changes: 57 additions & 8 deletions resources/release-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import { npm, readPackageJSON } from './utils.js';
import { git, npm, readPackageJSON, readPackageJSONAtRef } from './utils.js';

interface ReleaseMetadata {
version: string;
tag: string;
distTag: string;
prerelease: boolean;
releaseNotes: string;
packageSpec: string;
tarballName: string;
shouldPublish: boolean;
}

try {
const packageJSON = readPackageJSON();
const { version } = packageJSON;
const { version, publishConfig } = packageJSON;

if (typeof version !== 'string' || version === '') {
throw new Error('package.json is missing a valid "version" field.');
}

const tag = `v${version}`;
const distTag = publishConfig?.tag ?? '';
const prerelease = distTag === 'alpha';
const releaseCommitSha = findReleaseCommitSha(version);
const releaseNotes =
releaseCommitSha == null
? ''
: git().log('-1', '--format=%b', releaseCommitSha).trim();
const packageSpec = `graphql@${version}`;
const tarballName = `graphql-${version}.tgz`;

Expand All @@ -17,15 +35,46 @@ try {
const versions = Array.isArray(parsedVersions)
? parsedVersions
: [parsedVersions];
const shouldPublish = versions.includes(version) ? 'false' : 'true';
const shouldPublish = !versions.includes(version);
const releaseMetadata: ReleaseMetadata = {
version,
tag,
distTag,
prerelease,
releaseNotes,
packageSpec,
tarballName,
shouldPublish,
};

process.stdout.write(`version=${version}\n`);
process.stdout.write(`tag=${tag}\n`);
process.stdout.write(`package_spec=${packageSpec}\n`);
process.stdout.write(`tarball_name=${tarballName}\n`);
process.stdout.write(`should_publish=${shouldPublish}\n`);
process.stdout.write(JSON.stringify(releaseMetadata) + '\n');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(message + '\n');
process.exit(1);
}

function findReleaseCommitSha(version: string): string | null {
const commitsTouchingPackageJSON = git().revList(
'--first-parent',
'--reverse',
'HEAD',
'--',
'package.json',
);

let previousVersion: string | null = null;
for (const commit of commitsTouchingPackageJSON) {
const versionAtCommit = readPackageJSONAtRef(commit).version;
if (versionAtCommit === version && previousVersion !== version) {
return commit;
}
previousVersion = versionAtCommit;
}

process.stderr.write(
`Warning: Unable to find commit introducing version ${version} in fetched history. ` +
`Release notes will be empty for this run.\n`,
);
return null;
}
5 changes: 5 additions & 0 deletions resources/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ export function readPackageJSON(
return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
}

export function readPackageJSONAtRef(ref: string): PackageJSON {
const packageJSONAtRef = git().catFile('blob', `${ref}:package.json`);
return JSON.parse(packageJSONAtRef);
}

export function readTSConfig(overrides?: any): ts.CompilerOptions {
const tsConfigPath = localRepoPath('tsconfig.json');

Expand Down
Loading