chore(repo): refactor publish.yml for PR releases (#26550)

Redo of #26509, with more guards for unexpected missing/relative values
within full releases in GitHub Actions.

---

Refactors our publish workflow to support PR releases, in addition to
our previous triggers.

**Tests:**

---

- Example of failure on non-PR release (comment skipped):
https://github.com/nrwl/nx/actions/runs/9480869812

---

- Example of failure on PR release (comment created on PR):
  - https://github.com/nrwl/nx/actions/runs/9480852880
  - https://github.com/nrwl/nx/pull/26515#issuecomment-2162646682

---

- Example of dry-run of full release (`workflow_dispatch` with no PR
number provided): https://github.com/nrwl/nx/actions/runs/9497871483

---

- Real PR release created here:

| Release details | 📑 |
  | ------------- | ------------- |
| **Published version** |
[0.0.0-pr-26515-856ef7f](https://www.npmjs.com/package/nx/v/0.0.0-pr-26515-856ef7f)
|
  | **Triggered by** | @JamesHenry |
| **Branch** |
[JamesHenry-patch-1](https://github.com/nrwl/nx/tree/JamesHenry-patch-1)
|
| **Commit** |
[856ef7f](856ef7f353)
|
| **Workflow run** |
[9497298216](https://github.com/nrwl/nx/actions/runs/9497298216) |

---------

Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
This commit is contained in:
James Henry 2024-06-14 15:07:06 +04:00 committed by GitHub
parent e22021da4e
commit 42749b8225
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 488 additions and 83 deletions

View File

@ -4,6 +4,8 @@
<!-- Please make sure that your commit message follows our format --> <!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` --> <!-- Example: `fix(nx): must begin with lowercase` -->
<!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. -->
## Current Behavior ## Current Behavior
<!-- This is the behavior we have today --> <!-- This is the behavior we have today -->

View File

@ -1,19 +1,122 @@
name: publish name: publish
on: on:
# Automated schedule - canary releases from master
schedule: schedule:
- cron: "0 3 * * 2-6" # Tuesdays - Saturdays, at 3am UTC - cron: "0 3 * * 2-6" # Tuesdays - Saturdays, at 3am UTC
# Manual trigger - PR releases or dry-runs (based on workflow inputs)
workflow_dispatch: workflow_dispatch:
inputs:
pr:
description: "If set, a real release will be created for the branch associated with the given PR number. If blank, a dry-run of the currently selected branch will be performed."
required: false
type: number
release: release:
types: [ published ] types: [ published ]
# Dynamically generate the display name for the GitHub UI based on the event type and inputs
run-name: ${{ github.event.inputs.pr && format('PR Release for {0}', github.event.inputs.pr) || github.event_name == 'schedule' && 'Canary Release' || github.event_name == 'workflow_dispatch' && !github.event.inputs.pr && 'Release Dry-Run' || github.ref_name }}
env: env:
DEBUG: napi:* DEBUG: napi:*
NX_RUN_GROUP: ${{ github.run_id }}-${{ github.run_attempt }} NX_RUN_GROUP: ${{ github.run_id }}-${{ github.run_attempt }}
CYPRESS_INSTALL_BINARY: 0 CYPRESS_INSTALL_BINARY: 0
NODE_VERSION: 18
PNPM_VERSION: 8.15.7 # Aligned with root package.json (pnpm/action-setup will helpfully error if out of sync)
jobs: jobs:
# We first need to determine the version we are releasing, and if we need a custom repo or ref to use for the git checkout in subsequent steps.
# These values depend upon the event type that triggered the workflow:
#
# - schedule:
# - We are running a canary release which always comes from the master branch, we can use default ref resolution
# in actions/checkout. The exact version will be generated within scripts/nx-release.ts.
#
# - release:
# - We are running a full release which is based on the tag that triggered the release event, we can use default
# ref resolution in actions/checkout. The exact version will be generated within scripts/nx-release.ts.
#
# - workflow_dispatch:
# - We are either running a dry-run on the current branch, in which case the version will be statica and we can use
# default ref resolution in actions/checkout, or we are creating a PR release for the given PR number, in which case
# we should generate an applicable version number within publish-resolve-data.js and use a custom ref of the PR branch name.
resolve-required-data:
name: Resolve Required Data
if: ${{ github.repository_owner == 'nrwl' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.script.outputs.version }}
dry_run_flag: ${{ steps.script.outputs.dry_run_flag }}
success_comment: ${{ steps.script.outputs.success_comment }}
publish_branch: ${{ steps.script.outputs.publish_branch }}
ref: ${{ steps.script.outputs.ref }}
repo: ${{ steps.script.outputs.repo }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
# Default checkout on the triggering branch so that the latest publish-resolve-data.js script is available
- uses: actions/checkout@v4
# Set up pnpm and node so that we can verify our setup and that the NPM_TOKEN secret will work later
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
check-latest: true
cache: 'pnpm'
# Ensure that the NPM_TOKEN secret is still valid before wasting any time deriving data or building projects
- name: Check NPM Credentials
run: npm whoami && echo "NPM credentials are valid" || (echo "NPM credentials are invalid or have expired." && exit 1)
- name: Resolve and set checkout and version data to use for release
id: script
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ github.event.inputs.pr }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const script = require('${{ github.workspace }}/scripts/publish-resolve-data.js');
await script({ github, context, core });
- name: (PR Release Only) Check out latest master
if: ${{ steps.script.outputs.ref != '' }}
uses: actions/checkout@v4
with:
# Check out the latest master branch to get its copy of nx-release.ts
repository: nrwl/nx
ref: master
path: latest-master-checkout
- name: (PR Release Only) Check out PR branch
if: ${{ steps.script.outputs.ref != '' }}
uses: actions/checkout@v4
with:
# Check out the PR branch to get its copy of nx-release.ts
repository: ${{ steps.script.outputs.repo }}
ref: ${{ steps.script.outputs.ref }}
path: pr-branch-checkout
- name: (PR Release Only) Ensure that nx-release.ts has not changed in the PR being released
if: ${{ steps.script.outputs.ref != '' }}
env:
FILE_TO_COMPARE: "scripts/nx-release.ts"
run: |
if ! cmp -s "latest-master-checkout/${{ env.FILE_TO_COMPARE }}" "pr-branch-checkout/${{ env.FILE_TO_COMPARE }}"; then
echo "🛑 Error: The file ${{ env.FILE_TO_COMPARE }} is different on the ${{ steps.script.outputs.ref }} branch on ${{ steps.script.outputs.repo }} vs latest master on nrwl/nx, cancelling workflow."
exit 1
else
echo "✅ The file ${{ env.FILE_TO_COMPARE }} is identical between the ${{ steps.script.outputs.ref }} branch on ${{ steps.script.outputs.repo }} and latest master on nrwl/nx."
fi
build: build:
needs: [resolve-required-data]
if: ${{ github.repository_owner == 'nrwl' }} if: ${{ github.repository_owner == 'nrwl' }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -100,16 +203,19 @@ jobs:
runs-on: ${{ matrix.settings.host }} runs-on: ${{ matrix.settings.host }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with: with:
version: 8 repository: ${{ needs.resolve-required-data.outputs.repo }}
ref: ${{ needs.resolve-required-data.outputs.ref }}
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
if: ${{ !matrix.settings.docker }} if: ${{ !matrix.settings.docker }}
with: with:
node-version: 18 node-version: ${{ env.NODE_VERSION }}
check-latest: true check-latest: true
cache: 'pnpm' cache: 'pnpm'
@ -120,7 +226,7 @@ jobs:
targets: ${{ matrix.settings.target }} targets: ${{ matrix.settings.target }}
- name: Cache cargo - name: Cache cargo
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.cargo/registry/index/ ~/.cargo/registry/index/
@ -129,30 +235,36 @@ jobs:
.cargo-cache .cargo-cache
target/ target/
key: ${{ matrix.settings.target }}-cargo-registry key: ${{ matrix.settings.target }}-cargo-registry
- uses: goto-bus-stop/setup-zig@v2 - uses: goto-bus-stop/setup-zig@v2
if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }}
with: with:
version: 0.10.0 version: 0.10.0
- name: Setup toolchain - name: Setup toolchain
run: ${{ matrix.settings.setup }} run: ${{ matrix.settings.setup }}
if: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }}
shell: bash shell: bash
- name: Setup node x86 - name: Setup node x86
if: matrix.settings.target == 'i686-pc-windows-msvc' if: matrix.settings.target == 'i686-pc-windows-msvc'
run: yarn config set supportedArchitectures.cpu "ia32" run: yarn config set supportedArchitectures.cpu "ia32"
shell: bash shell: bash
- name: Install dependencies - name: Install dependencies
if: ${{ !matrix.settings.docker }} if: ${{ !matrix.settings.docker }}
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
timeout-minutes: 30 timeout-minutes: 30
- name: Setup node x86 - name: Setup node x86
uses: actions/setup-node@v4 uses: actions/setup-node@v4
if: matrix.settings.target == 'i686-pc-windows-msvc' if: matrix.settings.target == 'i686-pc-windows-msvc'
with: with:
node-version: 18 node-version: ${{ env.NODE_VERSION }}
check-latest: true check-latest: true
cache: pnpm cache: pnpm
architecture: x86 architecture: x86
- name: Build in docker - name: Build in docker
uses: addnab/docker-run-action@v3 uses: addnab/docker-run-action@v3
if: ${{ matrix.settings.docker }} if: ${{ matrix.settings.docker }}
@ -160,28 +272,35 @@ jobs:
image: ${{ matrix.settings.docker }} image: ${{ matrix.settings.docker }}
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build
run: ${{ matrix.settings.build }} run: ${{ matrix.settings.build }}
- name: Build - name: Build
run: ${{ matrix.settings.build }} run: ${{ matrix.settings.build }}
if: ${{ !matrix.settings.docker }} if: ${{ !matrix.settings.docker }}
shell: bash shell: bash
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: bindings-${{ matrix.settings.target }} name: bindings-${{ matrix.settings.target }}
path: packages/**/*.node path: packages/**/*.node
if-no-files-found: error if-no-files-found: error
build-freebsd: build-freebsd:
needs: [resolve-required-data]
if: ${{ github.repository_owner == 'nrwl' }} if: ${{ github.repository_owner == 'nrwl' }}
runs-on: macos-13-large runs-on: macos-13-large
name: Build FreeBSD name: Build FreeBSD
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
if: ${{ github.event_name != 'schedule' }} if: ${{ github.event_name != 'schedule' && !github.event.inputs.pr }}
with:
repository: ${{ needs.resolve-required-data.outputs.repo }}
ref: ${{ needs.resolve-required-data.outputs.ref }}
- name: Build - name: Build
id: build id: build
if: ${{ github.event_name != 'schedule' }} if: ${{ github.event_name != 'schedule' && !github.event.inputs.pr }}
uses: cross-platform-actions/action@v0.22.0 uses: cross-platform-actions/action@v0.22.0
env: env:
DEBUG: napi:* DEBUG: napi:*
@ -223,9 +342,10 @@ jobs:
echo "KILL ALL NODE PROCESSES" echo "KILL ALL NODE PROCESSES"
killall node || true killall node || true
echo "COMPLETE" echo "COMPLETE"
- name: Upload artifact - name: Upload artifact
if: ${{ github.event_name != 'schedule' }} if: ${{ github.event_name != 'schedule' && !github.event.inputs.pr }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: bindings-freebsd name: bindings-freebsd
path: packages/**/*.node path: packages/**/*.node
@ -238,7 +358,9 @@ jobs:
permissions: permissions:
id-token: write id-token: write
contents: write contents: write
pull-requests: write
needs: needs:
- resolve-required-data
- build-freebsd - build-freebsd
- build - build
env: env:
@ -247,45 +369,92 @@ jobs:
NPM_CONFIG_PROVENANCE: true NPM_CONFIG_PROVENANCE: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with: with:
version: 8 repository: ${{ needs.resolve-required-data.outputs.repo }}
ref: ${{ needs.resolve-required-data.outputs.ref }}
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
check-latest: true check-latest: true
cache: 'pnpm' cache: 'pnpm'
- name: Check NPM Credentials
run: npm whoami && echo "NPM credentials are valid" || (echo "NPM credentials are invalid or have expired." && exit 1)
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
path: artifacts path: artifacts
# This command will appropriately fail if no artifacts are available
- name: List artifacts - name: List artifacts
run: ls -R artifacts run: ls -R artifacts
shell: bash shell: bash
- name: Publish - name: Publish
env:
VERSION: ${{ needs.resolve-required-data.outputs.version }}
DRY_RUN: ${{ needs.resolve-required-data.outputs.dry_run_flag }}
PUBLISH_BRANCH: ${{ needs.resolve-required-data.outputs.publish_branch }}
run: | run: |
git checkout -b publish/$GITHUB_REF_NAME echo ""
# If triggered by the cron, create a canary release # Create and check out the publish branch
if [ "${{ github.event_name }}" = "schedule" ]; then git checkout -b $PUBLISH_BRANCH
VERSION="canary" echo ""
else echo "Version set to: $VERSION"
# Otherwise, use the tag name (if triggered via release), or explicit version (if triggered via workflow_dispatch) echo "DRY_RUN set to: $DRY_RUN"
VERSION="${GITHUB_REF_NAME}" echo ""
fi
# If triggered via workflow_dispatch, perform a dry-run
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
DRY_RUN="--dry-run"
else
DRY_RUN=""
fi
pnpm nx-release --local=false $VERSION $DRY_RUN pnpm nx-release --local=false $VERSION $DRY_RUN
- name: (Stable Release Only) Trigger Docs Release - name: (Stable Release Only) Trigger Docs Release
# Publish docs only on a full release # Publish docs only on a full release
if: ${{ !github.event.release.prerelease && github.event_name == 'release' }} if: ${{ !github.event.release.prerelease && github.event_name == 'release' }}
run: npx ts-node ./scripts/release-docs.ts run: npx ts-node ./scripts/release-docs.ts
- name: (PR Release Only) Create comment for successful PR release
if: success() && github.event.inputs.pr
uses: actions/github-script@v7
env:
SUCCESS_COMMENT: ${{ needs.resolve-required-data.outputs.success_comment }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const successComment = JSON.parse(process.env.SUCCESS_COMMENT);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.inputs.pr }},
body: successComment
});
pr_failure_comment:
# Run this job if it is a PR release, running on the nrwl origin, and any of the required jobs failed
if: ${{ github.repository_owner == 'nrwl' && github.event.inputs.pr && always() && contains(needs.*.result, 'failure') }}
needs: [resolve-required-data, build, build-freebsd, publish]
name: (PR Release Failure Only) Create comment for failed PR release
runs-on: ubuntu-latest
steps:
- name: Create comment for failed PR release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# This script is intentionally kept inline (and e.g. not generated in publish-resolve-data.js)
# to ensure that an error within the data generation itself is not missed.
script: |
const message = `
Failed to publish a PR release of this pull request, triggered by @${{ github.triggering_actor }}.
See the failed workflow run at: https://github.com/nrwl/nx/actions/runs/${{ github.run_id }}
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.inputs.pr }},
body: message
});

View File

@ -355,3 +355,7 @@ Closes #157
To simplify and automate the process of committing with this format, To simplify and automate the process of committing with this format,
**Nx is a [Commitizen](https://github.com/commitizen/cz-cli) friendly repository**, just do `git add` and **Nx is a [Commitizen](https://github.com/commitizen/cz-cli) friendly repository**, just do `git add` and
execute `pnpm commit`. execute `pnpm commit`.
#### PR releases
If you are working on a particularly complex change or feature addition, you can request a dedicated Nx release for the associated pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate.

View File

@ -130,62 +130,60 @@ const VALID_AUTHORS_FOR_LATEST = [
const distTag = determineDistTag(options.version); const distTag = determineDistTag(options.version);
if (options.dryRun) { // If publishing locally, force all projects to not be private first
console.warn('Not Publishing because --dryRun was passed'); if (options.local) {
} else { console.log(
// If publishing locally, force all projects to not be private first chalk.dim`\n Publishing locally, so setting all packages with existing nx-release-publish targets to not be private. If you have created a new private package and you want it to be published, you will need to manually configure the "nx-release-publish" target using executor "@nx/js:release-publish"`
if (options.local) { );
console.log( const projectGraph = await createProjectGraphAsync();
chalk.dim`\n Publishing locally, so setting all packages with existing nx-release-publish targets to not be private. If you have created a new private package and you want it to be published, you will need to manually configure the "nx-release-publish" target using executor "@nx/js:release-publish"` for (const proj of Object.values(projectGraph.nodes)) {
); if (proj.data.targets?.['nx-release-publish']) {
const projectGraph = await createProjectGraphAsync(); const packageJsonPath = join(
for (const proj of Object.values(projectGraph.nodes)) { workspaceRoot,
if (proj.data.targets?.['nx-release-publish']) { proj.data.targets?.['nx-release-publish']?.options.packageRoot,
const packageJsonPath = join( 'package.json'
workspaceRoot,
proj.data.targets?.['nx-release-publish']?.options.packageRoot,
'package.json'
);
try {
const packageJson = require(packageJsonPath);
if (packageJson.private) {
console.log(
'- Publishing private package locally:',
packageJson.name
);
writeFileSync(
packageJsonPath,
JSON.stringify({ ...packageJson, private: false })
);
}
} catch {}
}
}
}
if (!options.local && (!distTag || distTag === 'latest')) {
// We are only expecting non-local latest releases to be performed within publish.yml on GitHub
const author = process.env.GITHUB_ACTOR ?? '';
if (!VALID_AUTHORS_FOR_LATEST.includes(author)) {
throw new Error(
`The GitHub user "${author}" is not allowed to publish to "latest". Please request one of the following users to carry out the release: ${VALID_AUTHORS_FOR_LATEST.join(
', '
)}`
); );
try {
const packageJson = require(packageJsonPath);
if (packageJson.private) {
console.log(
'- Publishing private package locally:',
packageJson.name
);
writeFileSync(
packageJsonPath,
JSON.stringify({ ...packageJson, private: false })
);
}
} catch {}
} }
} }
}
// Run with dynamic output-style so that we have more minimal logs by default but still always see errors if (!options.local && (!distTag || distTag === 'latest')) {
let publishCommand = `pnpm nx release publish --registry=${getRegistry()} --tag=${distTag} --output-style=dynamic --parallel=8`; // We are only expecting non-local latest releases to be performed within publish.yml on GitHub
if (options.dryRun) { const author = process.env.GITHUB_ACTOR ?? '';
publishCommand += ' --dry-run'; if (!VALID_AUTHORS_FOR_LATEST.includes(author)) {
throw new Error(
`The GitHub user "${author}" is not allowed to publish to "latest". Please request one of the following users to carry out the release: ${VALID_AUTHORS_FOR_LATEST.join(
', '
)}`
);
} }
console.log(`\n> ${publishCommand}`); }
execSync(publishCommand, {
stdio: [0, 1, 2],
maxBuffer: LARGE_BUFFER,
});
// Run with dynamic output-style so that we have more minimal logs by default but still always see errors
let publishCommand = `pnpm nx release publish --registry=${getRegistry()} --tag=${distTag} --output-style=dynamic --parallel=8`;
if (options.dryRun) {
publishCommand += ' --dry-run';
}
console.log(`\n> ${publishCommand}`);
execSync(publishCommand, {
stdio: [0, 1, 2],
maxBuffer: LARGE_BUFFER,
});
if (!options.dryRun) {
let version; let version;
if (['minor', 'major', 'patch'].includes(options.version)) { if (['minor', 'major', 'patch'].includes(options.version)) {
version = execSync(`npm view nx@${distTag} version`).toString().trim(); version = execSync(`npm view nx@${distTag} version`).toString().trim();
@ -241,7 +239,16 @@ function parseArgs() {
description: description:
'The version to publish. This does not need to be passed and can be inferred.', 'The version to publish. This does not need to be passed and can be inferred.',
default: 'minor', default: 'minor',
coerce: (version) => { coerce: (version: string) => {
const isGithubActions = !!process.env.GITHUB_ACTIONS;
if (isGithubActions && isRelativeVersionKeyword(version)) {
// Print error rather than throw to avoid yargs noise in this specifically handled case
console.error(
'Error: The release script was triggered in a GitHub Actions workflow, but a relative version keyword was provided. This is an unexpected combination.'
);
process.exit(1);
}
if (version !== 'canary') { if (version !== 'canary') {
return version; return version;
} }
@ -358,12 +365,17 @@ function getRegistry() {
function determineDistTag( function determineDistTag(
newVersion: string newVersion: string
): 'latest' | 'next' | 'previous' | 'canary' { ): 'latest' | 'next' | 'previous' | 'canary' | 'pull-request' {
// Special case of canary // Special case of canary
if (newVersion.includes('canary')) { if (newVersion.includes('canary')) {
return 'canary'; return 'canary';
} }
// Special case of PR release
if (newVersion.startsWith('0.0.0-pr-')) {
return 'pull-request';
}
// For a relative version keyword, it cannot be previous // For a relative version keyword, it cannot be previous
if (isRelativeVersionKeyword(newVersion)) { if (isRelativeVersionKeyword(newVersion)) {
const prereleaseKeywords: ReleaseType[] = [ const prereleaseKeywords: ReleaseType[] = [

View File

@ -0,0 +1,218 @@
// @ts-check
/**
* This function is invoked by the publish.yml GitHub Action workflow and contains all of the dynamic logic needed
* for the various workflow trigger types. This avoids the need for the logic to be stored in fragile inline
* shell commands.
*
* @typedef {'--dry-run' | ''} DryRunFlag
*
* @typedef {{
* version: string;
* dry_run_flag: DryRunFlag;
* success_comment: string;
* publish_branch: string;
* repo: string;
* ref: string;
* }} PublishResolveData
*
* Partial from https://github.com/actions/toolkit/blob/c6b487124a61d7dc6c7bd6ea0208368af3513a6e/packages/github/src/context.ts
* @typedef {{
* actor: string;
* runId: number;
* repo: { owner: string; repo: string };
* }} GitHubContext
*
* @param {{
* github: import('octokit/dist-types').Octokit;
* context: GitHubContext;
* core: import('@actions/core');
* }} param
*/
module.exports = async ({ github, context, core }) => {
const data = await getPublishResolveData({ github, context });
// Ensure that certain outputs are always set
if (!data.version) {
throw new Error('The "version" to release could not be determined');
}
if (!data.publish_branch) {
throw new Error('The "publish_branch" could not be determined');
}
// Set the outputs to be consumed in later steps
core.setOutput('version', data.version);
core.setOutput('dry_run_flag', data.dry_run_flag);
core.setOutput('success_comment', JSON.stringify(data.success_comment)); // Escape the multi-line string
core.setOutput('publish_branch', data.publish_branch);
core.setOutput('ref', data.ref);
core.setOutput('repo', data.repo);
};
/**
* @param {{
* github: import('octokit/dist-types').Octokit;
* context: GitHubContext;
* }} param
*
* @returns {Promise<PublishResolveData>}
*/
async function getPublishResolveData({ github, context }) {
// We use empty strings as default values so that we can let the `actions/checkout` action apply its default resolution
const DEFAULT_REF = '';
const DEFAULT_REPO = '';
/**
* "The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown
* on GitHub. For example, feature-branch-1."
*
* Source: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
*/
const refName = process.env.GITHUB_REF_NAME;
if (!refName) {
throw new Error('The github ref name could not be determined');
}
const DEFAULT_PUBLISH_BRANCH = `publish/${refName}`;
/** @type {DryRunFlag} */
const DRY_RUN_DISABLED = '';
/** @type {DryRunFlag} */
const DRY_RUN_ENABLED = '--dry-run';
switch (process.env.GITHUB_EVENT_NAME) {
case 'schedule': {
const data = {
version: 'canary',
dry_run_flag: DRY_RUN_DISABLED,
success_comment: '',
publish_branch: DEFAULT_PUBLISH_BRANCH,
// In this case the default checkout logic should use the default (master) branch
repo: DEFAULT_REPO,
ref: DEFAULT_REF,
};
console.log('"schedule" trigger detected', { data });
return data;
}
case 'release': {
const data = {
version: refName,
dry_run_flag: DRY_RUN_DISABLED,
success_comment: '',
publish_branch: DEFAULT_PUBLISH_BRANCH,
// In this case the default checkout logic should use the tag that triggered the release event
ref: DEFAULT_REF,
repo: DEFAULT_REPO,
};
console.log('"release" trigger detected', { data });
return data;
}
case 'workflow_dispatch': {
const prNumber = process.env.PR_NUMBER;
if (!prNumber) {
const data = {
version: '0.0.0-dry-run.0',
dry_run_flag: DRY_RUN_ENABLED,
success_comment: '',
publish_branch: DEFAULT_PUBLISH_BRANCH,
// In this case the default checkout logic should use the branch/tag selected when triggering the workflow
repo: DEFAULT_REPO,
ref: DEFAULT_REF,
};
console.log(
'"workflow_dispatch" trigger detected, no PR number provided',
{ data }
);
return data;
}
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(prNumber),
});
if (!pr?.data?.head?.repo) {
throw new Error(
`The PR data for PR number ${prNumber} is missing the head branch information`
);
}
const fullSHA = pr.data.head.sha;
const shortSHA = fullSHA.slice(0, 7);
const version = `0.0.0-pr-${prNumber}-${shortSHA}`;
const repo = pr.data.head.repo.full_name;
const ref = pr.data.head.ref;
const data = {
version,
dry_run_flag: DRY_RUN_DISABLED,
success_comment: getSuccessCommentForPR({
context,
version,
repo,
ref,
pr_short_sha: shortSHA,
pr_full_sha: fullSHA,
}),
// Custom publish branch name for PRs
publish_branch: `publish/pr-${prNumber}`,
// In this case we instruct the checkout action what repo and ref to use
repo,
ref,
};
console.log(
`"workflow_dispatch" trigger detected, PR number ${prNumber} provided`,
{ data }
);
console.log(`Owner: ${context.repo.owner}`);
console.log(`Repo: ${context.repo.repo}`);
console.log(`Fork repo:`, pr.data.head.repo.full_name);
console.log(`Fetched PR details: ${pr.data.head.ref}`);
console.log(`Full PR SHA: ${pr.data.head.sha}`);
return data;
}
default:
throw new Error(
`The publish.yml workflow was triggered by an unexpected event: "${process.env.GITHUB_EVENT_NAME}"`
);
}
}
function getSuccessCommentForPR({
context,
version,
repo,
ref,
pr_short_sha,
pr_full_sha,
}) {
return `## 🐳 We have a release for that!
This PR has a release associated with it. You can try it out using this command:
\`\`\`bash
npx create-nx-workspace@${version} my-workspace
\`\`\`
Or just copy this version and use it in your own command:
\`\`\`bash
${version}
\`\`\`
| Release details | 📑 |
| ------------- | ------------- |
| **Published version** | [${version}](https://www.npmjs.com/package/nx/v/${version}) |
| **Triggered by** | @${context.actor} |
| **Branch** | [${ref}](https://github.com/${repo}/tree/${ref}) |
| **Commit** | [${pr_short_sha}](https://github.com/${repo}/commit/${pr_full_sha}) |
| **Workflow run** | [${context.runId}](https://github.com/nrwl/nx/actions/runs/${context.runId}) |
To request a new release for this pull request, mention someone from the Nx team or the \`@nrwl/nx-pipelines-reviewers\`.
`;
}