Skip to content
Closed
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
97 changes: 97 additions & 0 deletions .github/workflows/reusable-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Reusable Publish Release

on:
workflow_call:
secrets:
wombot-token:
description: 'The Wombat release-backed publish token'
required: true

permissions: {}

jobs:
# Job 1: Build and Verify the release assets.
build:
name: Build & Verify
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
is-release-commit: ${{ steps.check-commit.outputs.is-release }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@main

- name: Install node modules
run: pnpm install --frozen-lockfile

- name: Check if commit is a release commit
id: check-commit
run: |
COMMIT_MSG=$(git log -1 --pretty=%B)
if [[ "$COMMIT_MSG" =~ ^release:\ v[0-9] || "$COMMIT_MSG" =~ ^Bump\ version\ to\ \"v[0-9] ]]; then
echo "is-release=true" >> $GITHUB_OUTPUT
else
echo "is-release=false" >> $GITHUB_OUTPUT
fi

- name: Get version from package.json
id: get-version
if: steps.check-commit.outputs.is-release == 'true'
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Setup Bazel
if: steps.check-commit.outputs.is-release == 'true'
uses: angular/dev-infra/github-actions/bazel/setup@main

- name: Build release packages
if: steps.check-commit.outputs.is-release == 'true'
run: pnpm ng-dev release build --json > built_packages.json

- name: Run release prechecks
if: steps.check-commit.outputs.is-release == 'true'
run: pnpm ng-dev release precheck < built_packages.json

- name: Archive built packages
if: steps.check-commit.outputs.is-release == 'true'
uses: actions/upload-artifact@v4
with:
name: release-packages
path: dist/packages-dist/
retention-days: 1

# Job 2: The Gatekeeper.
# This job enforces the Org-level environment protection rules (Dual Approval).
approve:
name: Await Dual Approval
needs: build
if: needs.build.outputs.is-release-commit == 'true'
runs-on: ubuntu-latest
environment: npm-publish # Enforces required reviewers & prevent self-approval
steps:
- name: Approved
run: echo "Release approved by second party."

# Job 3: Publish to Wombat.
publish:
name: Publish to Wombat (NPM)
needs: [build, approve]
runs-on: ubuntu-latest
permissions:
contents: write # Required for ng-dev to create GitHub releases/tags
id-token: write # Required to generate NPM/Sigstore provenance metadata
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@main

- name: Download built packages
uses: actions/download-artifact@v4
with:
name: release-packages
path: dist/packages-dist/

- name: Run ng-dev CI publish
env:
WOMBOT_TOKEN: ${{ secrets.wombot-token }}
run: pnpm ng-dev release publish-ci --built-packages-dir=dist/packages-dist/ --expected-sha=${{ github.sha }}
2 changes: 2 additions & 0 deletions ng-dev/release/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {ReleaseInfoCommandModule} from './info/cli.js';
import {ReleaseNotesCommandModule} from './notes/cli.js';
import {ReleasePrecheckCommandModule} from './precheck/cli.js';
import {ReleasePublishCommandModule} from './publish/cli.js';
import {ReleasePublishCiCommandModule} from './publish/cli-ci.js';
import {ReleasePublishSnapshotsCommandModule} from './snapshot-publish/cli.js';
import {BuildEnvStampCommand} from './stamping/cli.js';
import {ReleaseNpmDistTagCommand} from './npm-dist-tag/cli.js';
Expand All @@ -23,6 +24,7 @@ export function buildReleaseParser(localYargs: Argv) {
.strict()
.demandCommand()
.command(ReleasePublishCommandModule)
.command(ReleasePublishCiCommandModule)
.command(ReleaseBuildCommandModule)
.command(ReleaseInfoCommandModule)
.command(ReleaseNpmDistTagCommand)
Expand Down
9 changes: 9 additions & 0 deletions ng-dev/release/publish/actions-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/

import type {PullRequest} from './actions.js';

/** Error that will be thrown if the user manually aborted a release action. */
export class UserAbortedReleaseActionError extends Error {}

/** Error that will be thrown if the action has been aborted due to a fatal error. */
export class FatalReleaseActionError extends Error {}

/** Error that will be thrown if the stage-only phase is completed successfully. */
export class StageOnlySuccessError extends Error {
constructor(public pullRequest: PullRequest) {
super('Stage-only phase completed successfully.');
}
}
27 changes: 25 additions & 2 deletions ng-dev/release/publish/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ import {ActiveReleaseTrains} from '../versioning/active-release-trains.js';
import {createExperimentalSemver} from '../versioning/experimental-versions.js';
import {NpmCommand} from '../versioning/npm-command.js';
import {getReleaseTagForVersion} from '../versioning/version-tags.js';
import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions-error.js';
import {
FatalReleaseActionError,
StageOnlySuccessError,
UserAbortedReleaseActionError,
} from './actions-error.js';
import {
analyzeAndExtendBuiltPackagesWithInfo,
assertIntegrityOfBuiltPackages,
Expand Down Expand Up @@ -78,7 +82,15 @@ export interface ReleaseActionConstructor<T extends ReleaseAction = ReleaseActio
/** Whether the release action is currently active. */
isActive(active: ActiveReleaseTrains, config: ReleaseConfig): Promise<boolean>;
/** Constructs a release action. */
new (...args: [ActiveReleaseTrains, AuthenticatedGitClient, ReleaseConfig, string]): T;
new (
...args: [
active: ActiveReleaseTrains,
git: AuthenticatedGitClient,
config: ReleaseConfig,
projectDir: string,
stageOnly?: boolean,
]
): T;
}

/**
Expand Down Expand Up @@ -106,6 +118,7 @@ export abstract class ReleaseAction {
protected git: AuthenticatedGitClient,
protected config: ReleaseConfig,
protected projectDir: string,
protected stageOnly = false,
) {}

/**
Expand Down Expand Up @@ -519,6 +532,16 @@ export abstract class ReleaseAction {

Log.info(green(' ✓ Release staging pull request has been created.'));

if (this.stageOnly) {
for (const pkg of builtPackagesWithInfo) {
if (existsSync(pkg.outputPath)) {
await fs.rm(pkg.outputPath, {recursive: true, force: true});
Log.info(`Cleaned up built package directory: ${pkg.outputPath}`);
}
}
throw new StageOnlySuccessError(pullRequest);
}

return {releaseNotes, pullRequest, builtPackagesWithInfo};
}

Expand Down
73 changes: 73 additions & 0 deletions ng-dev/release/publish/cli-ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license
* Copyright Google LLC
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Argv, Arguments, CommandModule} from 'yargs';

import {assertValidGithubConfig, getConfig} from '../../utils/config.js';
import {addGithubTokenOption} from '../../utils/git/github-yargs.js';
import {assertValidReleaseConfig} from '../config/index.js';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client.js';
import {PublishCiTool} from './index-ci.js';
import {green, Log} from '../../utils/logging.js';

export interface ReleasePublishCiOptions {
builtPackagesDir: string;
expectedSha: string;
dryRun?: boolean;
}

function builder(argv: Argv): Argv<ReleasePublishCiOptions> {
return addGithubTokenOption(argv)
.option('built-packages-dir' as 'builtPackagesDir', {
type: 'string',
demandOption: true,
description: 'Path to the directory containing pre-built packages.',
})
.option('expected-sha' as 'expectedSha', {
type: 'string',
demandOption: true,
description: 'The expected Git SHA of the release commit.',
})
.option('dry-run' as 'dryRun', {
type: 'boolean',
default: false,
description:
'Run the publish command in dry-run mode, skipping tag/release creation and NPM publishing.',
});
}

async function handler(flags: Arguments<ReleasePublishCiOptions>) {
const git = await AuthenticatedGitClient.get();
const config = await getConfig();
assertValidReleaseConfig(config);
assertValidGithubConfig(config);

const tool = new PublishCiTool(config, git, git.baseDir, flags);

try {
await tool.run();
Log.info(green('Release CI publish completed successfully.'));
} catch (e) {
if (e instanceof Error) {
Log.error(`Release CI publish failed: ${e.message}`);
if (e.stack) {
Log.debug(e.stack);
}
} else {
Log.error(`Release CI publish failed with unknown error: ${e}`);
}
process.exitCode = 1;
}
}

export const ReleasePublishCiCommandModule: CommandModule<{}, ReleasePublishCiOptions> = {
builder,
handler,
command: 'publish-ci',
describe: 'Publish a release from CI.',
};
17 changes: 13 additions & 4 deletions ng-dev/release/publish/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@ import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client.j
import {green, Log, yellow} from '../../utils/logging.js';

/** Command line options for publishing a release. */
export interface ReleasePublishOptions extends ReleaseToolFlags {}
export interface ReleasePublishOptions extends ReleaseToolFlags {
stageOnly?: boolean;
}

/** Yargs command builder for configuring the `ng-dev release publish` command. */
function builder(argv: Argv): Argv<ReleasePublishOptions> {
return addGithubTokenOption(argv).option('publishRegistry', {
type: 'string',
});
return addGithubTokenOption(argv)
.option('publishRegistry', {
type: 'string',
})
.option('stage-only', {
type: 'boolean',
default: false,
description:
'Only stage the release (bump version, generate changelog, build, precheck, create PR) and exit.',
});
}

/** Yargs command handler for staging a release. */
Expand Down
Loading
Loading