Skip to content
Open
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
6 changes: 5 additions & 1 deletion .github/local-actions/branch-manager/main.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .ng-dev/github.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const github = {
name: 'dev-infra',
mainBranchName: 'main',
useNgDevAuthService: true,
mergeMode: 'caretaker-only',
};
7 changes: 6 additions & 1 deletion ng-dev/caretaker/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import {Argv} from 'yargs';
import {assertValidCaretakerConfig, assertValidGithubConfig, getConfig} from '../utils/config.js';
import {CheckModule} from './check/cli.js';
import {HandoffModule} from './handoff/cli.js';
import {MergeModeModule} from './merge-mode/cli.js';

/** Build the parser for the caretaker commands. */
export function buildCaretakerParser(argv: Argv) {
return argv.middleware(caretakerCommandCanRun, false).command(CheckModule).command(HandoffModule);
return argv
.middleware(caretakerCommandCanRun, false)
.command(MergeModeModule)
.command(CheckModule)
.command(HandoffModule);
}

function caretakerCommandCanRun() {
Expand Down
12 changes: 7 additions & 5 deletions ng-dev/caretaker/handoff/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
*/

import {Argv, CommandModule} from 'yargs';

import {addGithubTokenOption} from '../../utils/git/github-yargs.js';

import {updateCaretakerTeamViaPrompt} from './update-github-team.js';

export interface CaretakerHandoffOptions {}
import {assertValidGithubConfig, getConfig} from '../../utils/config.js';
import {verifyMergeMode} from './verify-merge-mode.js';

/** Builds the command. */
function builder(argv: Argv) {
Expand All @@ -21,11 +19,15 @@ function builder(argv: Argv) {

/** Handles the command. */
async function handler() {
const {mergeMode} = (await getConfig([assertValidGithubConfig])).github;
if (!(await verifyMergeMode(mergeMode))) {
return;
}
await updateCaretakerTeamViaPrompt();
}

/** yargs command module for assisting in handing off caretaker. */
export const HandoffModule: CommandModule<{}, CaretakerHandoffOptions> = {
export const HandoffModule: CommandModule<{}, {}> = {
handler,
builder,
command: 'handoff',
Expand Down
9 changes: 6 additions & 3 deletions ng-dev/caretaker/handoff/update-github-team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export async function updateCaretakerTeamViaPrompt() {
const config = await getConfig([assertValidCaretakerConfig, assertValidGithubConfig]);
/** The github team name for the caretaker group. */
const caretakerGroup = `${config.github.name}-caretaker`;
/** The github team name for the group containing the repository releaser. */
const releaserGroup = `${config.github.name}-releaser`;
/** The github team name for the group containing all possible caretakers. */
const caretakerGroupRoster = `${config.github.name}-caretaker-roster`;
/** The github team name for the group containing all possible emea caretakers. */
Expand Down Expand Up @@ -74,7 +76,8 @@ export async function updateCaretakerTeamViaPrompt() {
}

try {
await setCaretakerGroup(caretakerGroup, Array.from(selected));
await setGithubTeam(caretakerGroup, Array.from(selected));
await setGithubTeam(releaserGroup, Array.from(selected));
} catch {
return Log.error(' ✘ Failed to update caretaker group.');
}
Expand All @@ -98,7 +101,7 @@ async function getGroupMembers(group: string) {
}
}

async function setCaretakerGroup(group: string, members: string[]) {
async function setGithubTeam(group: string, members: string[]) {
/** The authenticated GitClient instance. */
const git = await AuthenticatedGitClient.get();
/** The full name of the group <org>/<group name>. */
Expand Down Expand Up @@ -127,7 +130,7 @@ async function setCaretakerGroup(group: string, members: string[]) {
});
};

Log.debug(`Caretaker Group: ${fullSlug}`);
Log.debug(`Github Team: ${fullSlug}`);
Log.debug(`Current Membership: ${current.join(', ')}`);
Log.debug(`New Membership: ${members.join(', ')}`);
Log.debug(`Removed: ${removed.join(', ')}`);
Expand Down
40 changes: 40 additions & 0 deletions ng-dev/caretaker/handoff/verify-merge-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @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 {RepositoryMergeModes} from '../../utils/config';
import {bold, green, Log, red} from '../../utils/logging';
import {Prompt} from '../../utils/prompt';
import {getCurrentMergeMode, setRepoMergeMode} from '../../utils/git/repository-merge-mode';

export async function verifyMergeMode(expectedMode: RepositoryMergeModes): Promise<boolean> {
const mode = await getCurrentMergeMode();
if (mode === expectedMode) {
return true;
}

Log.info(`The repository merge-mode is currently ${bold(mode)} and must be reset before handoff`);
if (
await Prompt.confirm({
message: `Would you like to reset this to ${expectedMode}`,
default: true,
})
) {
try {
await setRepoMergeMode(expectedMode);
Log.info(`${green('✔')} Successfuly set merge-mode to ${expectedMode}`);
return true;
} catch (err) {
Log.info(`${red('✘')} Failed to update merge-mode`);
Log.info(err);
return false;
}
}
// User chose not to reset merge-mode
Log.info('Aborting...');
return false;
}
47 changes: 47 additions & 0 deletions ng-dev/caretaker/merge-mode/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @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 {Log} from '../../utils/logging';
import {addGithubTokenOption} from '../../utils/git/github-yargs';
import {setMergeModeRelease} from './release';
import {resetMergeMode} from './reset';
import {getCurrentMergeMode} from '../../utils/git/repository-merge-mode';

interface Options {
mode?: string;
}

async function setMergeModeBuilder(argv: Argv): Promise<Argv<Options>> {
return addGithubTokenOption(argv).positional('mode', {
type: 'string',
choices: ['release', 'reset'],
});
}

async function setMergeModeHandler({mode}: Arguments<Options>) {
if (mode === undefined) {
const currentMode = await getCurrentMergeMode();
Log.info(`Repository merge-mode is currently set to: ${currentMode}`);
return;
}
if (mode === 'reset') {
return await resetMergeMode();
}
if (mode === 'release') {
return await setMergeModeRelease();
}
Log.error(`Unable to set the merge mode to the provided mode: ${mode}`);
}

export const MergeModeModule: CommandModule<{}, {}> = {
builder: setMergeModeBuilder,
handler: setMergeModeHandler,
command: ['merge-mode [mode]'],
describe: 'Set the repository merge mode',
};
93 changes: 93 additions & 0 deletions ng-dev/caretaker/merge-mode/release.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @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 {assertValidGithubConfig, getConfig} from '../../utils/config';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client';
import {setRepoMergeMode} from '../../utils/git/repository-merge-mode';
import {green, Log, red, bold} from '../../utils/logging';

export async function setMergeModeRelease() {
try {
await setRepoReleaserTeamToOnlyCurrentUser();
await setRepoMergeMode('release');
Log.info(green(' ✔ Repository is set for release'));
} catch (err) {
Log.error(' ✘ Failed to setup of repository for release');
if (err instanceof Error) {
Log.info(err.message);
Log.debug(err.stack);
return;
}
Log.info(err);
}
}

async function setRepoReleaserTeamToOnlyCurrentUser() {
/** The authenticated GitClient instance. */
const git = await AuthenticatedGitClient.get();
/** Caretaker specific configuration. */
const config = await getConfig([assertValidGithubConfig]);
/** The github team name for the group containing the repository releaser. */
const group = `${config.github.name}-releaser`;
/** The user currently authenticated to github. */
const login = await git.github.users.getAuthenticated().then(({data}) => data.login);
/**
* The membership role, if any for the current user in the releaser group. A maintainer role
* is required to be able to modify the membership of the group.
*/
const membership = await git.github.teams
.getMembershipForUserInOrg({
org: git.remoteConfig.owner,
team_slug: group,
username: login,
})
.then(
({data}) => data.role,
() => undefined,
);

if (membership !== 'maintainer') {
Log.info(`Unable to update membership in ${bold(group)}`);
Log.info(`Please reach out to dev-infra for assistance.`);
throw '';
}

/** A list of the current members of the releaser group */
const members = new Set(
await git.github.teams
.listMembersInOrg({
org: git.remoteConfig.owner,
team_slug: group,
})
.then(({data}) => data.map(({login}) => login)),
);

Log.debug(`Current membership for ${group}:`);
for (const member of members) {
Log.debug(` - ${member}`);
}

// Do not remove the current user.
members.delete(login);

await Promise.all(
Array.from(members).map(async (username: string) => {
// This check should be unnecessary, but is put in place as a second guard
// against accidently locking all non-admins out of updating the group.
if (username === login) {
return;
}
await git.github.teams.removeMembershipForUserInOrg({
org: git.remoteConfig.owner,
team_slug: group,
username,
});
Log.debug(`Removed ${username} from ${group}.`);
}),
);
}
29 changes: 29 additions & 0 deletions ng-dev/caretaker/merge-mode/reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @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 {assertValidGithubConfig, getConfig} from '../../utils/config';
import {setRepoMergeMode} from '../../utils/git/repository-merge-mode';
import {green, Log, red} from '../../utils/logging';

export async function resetMergeMode() {
try {
const {
github: {mergeMode},
} = await getConfig([assertValidGithubConfig]);
await setRepoMergeMode(mergeMode);
Log.info(`${green('✔')} Repository has been reset to the normal mode: ${mergeMode}`);
} catch (err) {
Log.info(`${red('✘')} Failed to reset the merge mode of the repository`);
if (err instanceof Error) {
Log.info(err.message);
Log.debug(err.stack);
return;
}
Log.info(err);
}
}
7 changes: 6 additions & 1 deletion ng-dev/pr/merge/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@

beforeEach(() => {
api = new GithubClient();
githubConfig = {owner: 'angular', name: 'dev-infra-test', mainBranchName: 'master'};
githubConfig = {
mergeMode: 'caretaker-only',
owner: 'angular',
name: 'dev-infra-test',
mainBranchName: 'master',

Check notice on line 42 in ng-dev/pr/merge/integration.spec.ts

View check run for this annotation

In Solidarity / Inclusive Language

Match Found

Please consider an alternative to `master`. Possibilities include: `primary`, `main`, `leader`, `active`, `writer`
Raw output
/master/gi
};
releaseConfig = {
representativeNpmPackage: '@angular/dev-infra-test-pkg',
npmPackages: [{name: '@angular/dev-infra-test-pkg'}],
Expand Down
7 changes: 6 additions & 1 deletion ng-dev/release/notes/changelog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ describe('Changelog', () => {
beforeEach(() => {
cleanTestTmpDir();
gitClient = getMockGitClient(
{owner: 'angular', name: 'dev-infra-test', mainBranchName: 'main'},
{
mergeMode: 'team-only',
owner: 'angular',
name: 'dev-infra-test',
mainBranchName: 'main',
},
/* useSandboxGitClient */ false,
);
spyOn(GitClient, 'get').and.resolveTo(gitClient);
Expand Down
17 changes: 17 additions & 0 deletions ng-dev/release/publish/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {GithubConfig} from '../../utils/config.js';
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client.js';
import {getCurrentMergeMode} from '../../utils/git/repository-merge-mode.js';
import {ReleaseConfig} from '../config/index.js';
import {ActiveReleaseTrains} from '../versioning/active-release-trains.js';
import {NpmCommand} from '../versioning/npm-command.js';
Expand Down Expand Up @@ -50,6 +51,7 @@ export class ReleaseTool {
const nextBranchName = getNextBranchName(this._github);

if (
!(await this._verifyInReleaseMergeMode()) ||
!(await this._verifyNoUncommittedChanges()) ||
!(await this._verifyRunningFromNextBranch(nextBranchName)) ||
!(await this._verifyNoShallowRepository()) ||
Expand Down Expand Up @@ -144,6 +146,21 @@ export class ReleaseTool {
return true;
}

/**
* Verifies that there are no uncommitted changes in the project.
* @returns a boolean indicating success or failure.
*/
private async _verifyInReleaseMergeMode(): Promise<boolean> {
const mode = await getCurrentMergeMode();
if (mode !== 'release') {
Log.error(` ✘ The repository merge-mode is set to ${mode} but must be set to release`);
Log.error(' prior to publishing releases. You can set merge-mode for release using:');
Log.error(' ng-dev caretaker merge-mode release');
return false;
}
return true;
}

/**
* Verifies that the local repository is not configured as shallow.
* @returns a boolean indicating success or failure.
Expand Down
1 change: 1 addition & 0 deletions ng-dev/release/publish/test/release-notes/context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const defaultContextData: RenderContextData = {
hiddenScopes: undefined,
groupOrder: undefined,
github: {
mergeMode: 'caretaker-only',
name: 'repoName',
owner: 'repoOwner',
mainBranchName: 'master',
Expand Down
7 changes: 6 additions & 1 deletion ng-dev/release/publish/test/release-notes/generation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ describe('release notes generation', () => {
representativeNpmPackage: 'test-pkg',
buildPackages: async () => [],
};
githubConfig = {owner: 'angular', name: 'dev-infra-test', mainBranchName: 'main'};
githubConfig = {
mergeMode: 'caretaker-only',
owner: 'angular',
name: 'dev-infra-test',
mainBranchName: 'main',
};
setConfig({github: githubConfig, release: releaseConfig});
client = getMockGitClient(githubConfig, /* useSandboxGitClient */ true);

Expand Down
Loading
Loading