diff --git a/github-actions/slash-commands/main.js b/github-actions/slash-commands/main.js index db24192ec..dd61fb352 100644 --- a/github-actions/slash-commands/main.js +++ b/github-actions/slash-commands/main.js @@ -51666,12 +51666,40 @@ Ran at: ${now} } }); +// ng-dev/utils/git/graphql-queries.js +var require_graphql_queries = __commonJS({ + "ng-dev/utils/git/graphql-queries.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.findOwnedForksOfRepoQuery = void 0; + var typed_graphqlify_1 = require_dist(); + exports2.findOwnedForksOfRepoQuery = (0, typed_graphqlify_1.params)({ + $owner: "String!", + $name: "String!" + }, { + repository: (0, typed_graphqlify_1.params)({ owner: "$owner", name: "$name" }, { + forks: (0, typed_graphqlify_1.params)({ affiliations: "OWNER", first: 1 }, { + nodes: [ + { + owner: { + login: typed_graphqlify_1.types.string + }, + name: typed_graphqlify_1.types.string + } + ] + }) + }) + }); + } +}); + // ng-dev/utils/git/authenticated-git-client.js var require_authenticated_git_client = __commonJS({ "ng-dev/utils/git/authenticated-git-client.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); exports2.AuthenticatedGitClient = void 0; + var graphql_queries_1 = require_graphql_queries(); var console_1 = require_console(); var git_client_1 = require_git_client(); var github_12 = require_github2(); @@ -51682,6 +51710,7 @@ var require_authenticated_git_client = __commonJS({ this.githubToken = githubToken; this._githubTokenRegex = new RegExp(this.githubToken, "g"); this._cachedOauthScopes = null; + this._cachedForkRepo = null; this.github = new github_12.AuthenticatedGithubClient(this.githubToken); } sanitizeConsoleOutput(value) { @@ -51706,6 +51735,19 @@ Alternatively, a new token can be created at: ${github_urls_1.GITHUB_TOKEN_GENER `; return { error }; } + async getForkOfAuthenticatedUser() { + if (this._cachedForkRepo !== null) { + return this._cachedForkRepo; + } + const { owner, name } = this.remoteConfig; + const result = await this.github.graphql(graphql_queries_1.findOwnedForksOfRepoQuery, { owner, name }); + const forks = result.repository.forks.nodes; + if (forks.length === 0) { + throw Error(`Unable to find fork for currently authenticated user. Please ensure you created a fork of: ${owner}/${name}.`); + } + const fork = forks[0]; + return this._cachedForkRepo = { owner: fork.owner.login, name: fork.name }; + } _fetchAuthScopesForToken() { if (this._cachedOauthScopes !== null) { return this._cachedOauthScopes; diff --git a/ng-dev/misc/cli.ts b/ng-dev/misc/cli.ts index 64b2b785a..f1e0d00e1 100644 --- a/ng-dev/misc/cli.ts +++ b/ng-dev/misc/cli.ts @@ -8,8 +8,13 @@ import * as yargs from 'yargs'; import {BuildAndLinkCommandModule} from './build-and-link/cli'; +import {UpdateYarnCommandModule} from './update-yarn/cli'; /** Build the parser for the misc commands. */ export function buildMiscParser(localYargs: yargs.Argv) { - return localYargs.help().strict().command(BuildAndLinkCommandModule); + return localYargs + .help() + .strict() + .command(BuildAndLinkCommandModule) + .command(UpdateYarnCommandModule); } diff --git a/ng-dev/misc/update-yarn/cli.ts b/ng-dev/misc/update-yarn/cli.ts new file mode 100644 index 000000000..3e8eeb2be --- /dev/null +++ b/ng-dev/misc/update-yarn/cli.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 {readdirSync, unlinkSync} from 'fs'; +import {join} from 'path'; +import {Argv, CommandModule} from 'yargs'; +import {spawnSync} from '../../utils/child-process'; + +import {error, info, red} from '../../utils/console'; +import {Spinner} from '../../utils/spinner'; +import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; +import {addGithubTokenOption} from '../../utils/git/github-yargs'; + +async function builder(yargs: Argv) { + return addGithubTokenOption(yargs); +} + +/** Environment object enabling the usage of yarn-path to determine the new version. */ +const useYarnPathEnv = { + ...process.env, + YARN_IGNORE_PATH: '0', +}; + +/** Environment object to prevent running husky workflow. */ +const skipHuskyEnv = { + ...process.env, + HUSKY: '0', +}; + +async function handler() { + /** Directory where node binary are globally installed. */ + const npmBinDir = spawnSync('npm', ['bin', '--global', 'yarn']).stdout.trim(); + /** The full path to the globally installed yarn binary. */ + const yarnBin = `${npmBinDir}/yarn`; + /** Instance of the local git client. */ + const git = AuthenticatedGitClient.get(); + /** The main branch name of the repository. */ + const mainBranchName = git.mainBranchName; + /** The original branch or ref before the command was invoked. */ + const originalBranchOrRef = git.getCurrentBranchOrRevision(); + + if (git.hasUncommittedChanges()) { + error(red('Found changes in the local repository. Make sure there are no uncommitted files.')); + process.exitCode = 1; + return; + } + + /** A spinner instance. */ + const spinner = new Spinner(''); + try { + spinner.update(`Fetching the latest primary branch from upstream: "${mainBranchName}"`); + git.run(['fetch', '-q', git.getRepoGitUrl(), mainBranchName]); + git.checkout('FETCH_HEAD', false); + + spinner.update('Removing previous yarn version.'); + const yarnReleasesDir = join(git.baseDir, '.yarn/releases'); + readdirSync(yarnReleasesDir).forEach((file) => unlinkSync(join(yarnReleasesDir, file))); + + spinner.update('Updating yarn version.'); + spawnSync(yarnBin, ['policies', 'set-version', 'latest']); + + spinner.update('Confirming the version of yarn was updated.'); + const newYarnVersion = spawnSync(yarnBin, ['-v'], {env: useYarnPathEnv}).stdout.trim(); + if (git.run(['status', '--porcelain']).stdout.length === 0) { + spinner.complete(); + error(red('Yarn already up to date')); + process.exitCode = 0; + return; + } + /** The title for the PR. */ + const title = `build: update to yarn v${newYarnVersion}`; + /** The body for the PR. */ + const body = `Update to the latest version of yarn, ${newYarnVersion}.`; + /** The commit message for the change. */ + const commitMessage = `${title}\n\n${body}`; + /** The name of the branch to use on remote. */ + const branchName = `yarn-update-v${newYarnVersion}`; + /** The name of the owner for remote branch on Github. */ + const {owner: localOwner} = await git.getForkOfAuthenticatedUser(); + + spinner.update('Staging yarn vendoring files and creating commit'); + git.run(['add', '.yarn/releases/**', '.yarnrc']); + git.run(['commit', '-q', '--no-verify', '-m', commitMessage], {env: skipHuskyEnv}); + + spinner.update('Pushing commit changes to github.'); + git.run(['push', '-q', 'origin', '--force-with-lease', `HEAD:refs/heads/${branchName}`]); + + spinner.update('Creating a PR for the changes.'); + const {number} = ( + await git.github.pulls.create({ + ...git.remoteParams, + title, + body, + base: mainBranchName, + head: `${localOwner}:${branchName}`, + }) + ).data; + + spinner.complete(); + info(`Created PR #${number} to update to yarn v${newYarnVersion}`); + } catch (e) { + spinner.complete(); + error(red('Aborted yarn update do to errors:')); + error(e); + process.exitCode = 1; + git.checkout(originalBranchOrRef, true); + } finally { + git.checkout(originalBranchOrRef, true); + } +} + +/** CLI command module. */ +export const UpdateYarnCommandModule: CommandModule = { + builder, + handler, + command: 'update-yarn', + describe: 'Automatically update the vendored yarn version in the repository and create a PR', +}; diff --git a/ng-dev/release/publish/actions.ts b/ng-dev/release/publish/actions.ts index f222aa5e3..3b82eca26 100644 --- a/ng-dev/release/publish/actions.ts +++ b/ng-dev/release/publish/actions.ts @@ -29,7 +29,6 @@ import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions- import {getCommitMessageForRelease, getReleaseNoteCherryPickCommitMessage} from './commit-message'; import {githubReleaseBodyLimit, waitForPullRequestInterval} from './constants'; import {invokeReleaseBuildCommand, invokeYarnInstallCommand} from './external-commands'; -import {findOwnedForksOfRepoQuery} from './graphql-queries'; import {getPullRequestState} from './pull-request-state'; import {getReleaseTagForVersion} from '../versioning/version-tags'; import {GithubApiRequestError} from '../../utils/git/github'; diff --git a/ng-dev/release/publish/graphql-queries.ts b/ng-dev/release/publish/graphql-queries.ts deleted file mode 100644 index 0df0ab660..000000000 --- a/ng-dev/release/publish/graphql-queries.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * 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 {params, types} from 'typed-graphqlify'; - -/** - * Graphql Github API query that can be used to find forks of a given repository - * that are owned by the current viewer authenticated with the Github API. - */ -export const findOwnedForksOfRepoQuery = params( - { - $owner: 'String!', - $name: 'String!', - }, - { - repository: params( - {owner: '$owner', name: '$name'}, - { - forks: params( - {affiliations: 'OWNER', first: 1}, - { - nodes: [ - { - owner: { - login: types.string, - }, - name: types.string, - }, - ], - }, - ), - }, - ), - }, -); diff --git a/tools/local-actions/changelog/main.js b/tools/local-actions/changelog/main.js index 46fee831b..4250a100a 100644 --- a/tools/local-actions/changelog/main.js +++ b/tools/local-actions/changelog/main.js @@ -52454,12 +52454,40 @@ var require_release_notes = __commonJS({ } }); +// ng-dev/utils/git/graphql-queries.js +var require_graphql_queries = __commonJS({ + "ng-dev/utils/git/graphql-queries.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.findOwnedForksOfRepoQuery = void 0; + var typed_graphqlify_1 = require_dist(); + exports2.findOwnedForksOfRepoQuery = (0, typed_graphqlify_1.params)({ + $owner: "String!", + $name: "String!" + }, { + repository: (0, typed_graphqlify_1.params)({ owner: "$owner", name: "$name" }, { + forks: (0, typed_graphqlify_1.params)({ affiliations: "OWNER", first: 1 }, { + nodes: [ + { + owner: { + login: typed_graphqlify_1.types.string + }, + name: typed_graphqlify_1.types.string + } + ] + }) + }) + }); + } +}); + // ng-dev/utils/git/authenticated-git-client.js var require_authenticated_git_client = __commonJS({ "ng-dev/utils/git/authenticated-git-client.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); exports2.AuthenticatedGitClient = void 0; + var graphql_queries_1 = require_graphql_queries(); var console_1 = require_console(); var git_client_1 = require_git_client(); var github_12 = require_github2(); @@ -52470,6 +52498,7 @@ var require_authenticated_git_client = __commonJS({ this.githubToken = githubToken; this._githubTokenRegex = new RegExp(this.githubToken, "g"); this._cachedOauthScopes = null; + this._cachedForkRepo = null; this.github = new github_12.AuthenticatedGithubClient(this.githubToken); } sanitizeConsoleOutput(value) { @@ -52494,6 +52523,19 @@ Alternatively, a new token can be created at: ${github_urls_1.GITHUB_TOKEN_GENER `; return { error }; } + async getForkOfAuthenticatedUser() { + if (this._cachedForkRepo !== null) { + return this._cachedForkRepo; + } + const { owner, name } = this.remoteConfig; + const result = await this.github.graphql(graphql_queries_1.findOwnedForksOfRepoQuery, { owner, name }); + const forks = result.repository.forks.nodes; + if (forks.length === 0) { + throw Error(`Unable to find fork for currently authenticated user. Please ensure you created a fork of: ${owner}/${name}.`); + } + const fork = forks[0]; + return this._cachedForkRepo = { owner: fork.owner.login, name: fork.name }; + } _fetchAuthScopesForToken() { if (this._cachedOauthScopes !== null) { return this._cachedOauthScopes;