From 8828a84ecf45aa9237fef142f71bf63cad1b2bdf Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 30 Apr 2025 23:12:06 +0000 Subject: [PATCH] ci: add a script to copy cdk api files to adev (#61081) Refactors the update-cli-help script into a generic script to copy json assets, and uses the shared code to also copy the CDK apis PR Close #61081 --- .github/workflows/update-cdk-apis.yml | 44 +++++ adev/scripts/shared/copy-json-assets.mjs | 90 ++++++++++ adev/scripts/shared/github-client.mjs | 77 +++++++++ adev/scripts/update-cdk-apis/README.md | 3 + adev/scripts/update-cdk-apis/index.mjs | 35 ++++ adev/scripts/update-cli-help/index.mjs | 154 ++---------------- adev/src/content/cdk/_build-info.json | 4 + adev/src/content/cli/BUILD.bazel | 2 +- adev/src/content/cli/help/BUILD.bazel | 9 +- .../{build-info.json => _build-info.json} | 0 10 files changed, 271 insertions(+), 147 deletions(-) create mode 100644 .github/workflows/update-cdk-apis.yml create mode 100644 adev/scripts/shared/copy-json-assets.mjs create mode 100644 adev/scripts/shared/github-client.mjs create mode 100644 adev/scripts/update-cdk-apis/README.md create mode 100644 adev/scripts/update-cdk-apis/index.mjs create mode 100644 adev/src/content/cdk/_build-info.json rename adev/src/content/cli/help/{build-info.json => _build-info.json} (100%) diff --git a/.github/workflows/update-cdk-apis.yml b/.github/workflows/update-cdk-apis.yml new file mode 100644 index 00000000000..0907c7ca59f --- /dev/null +++ b/.github/workflows/update-cdk-apis.yml @@ -0,0 +1,44 @@ +name: Update ADEV Angular CDK apis + +on: + workflow_dispatch: + inputs: {} + push: + branches: + - 'main' + - '[0-9]+.[0-9]+.x' + +# Declare default permissions as read only. +permissions: + contents: read + +jobs: + update_cli_help: + name: Update Angular CDK apis (if necessary) + if: github.repository == 'angular/angular' + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Setting `persist-credentials: false` prevents the github-action account from being the + # account that is attempted to be used for authentication, instead the remote is set to + # an authenticated URL. + persist-credentials: false + # This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync. + fetch-depth: 0 + - name: Generate CDK apis + run: node adev/scripts/update-cdk-apis/index.mjs + env: + ANGULAR_CDK_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CDK_BUILDS_READONLY_GITHUB_TOKEN }} + - name: Create a PR (if necessary) + uses: angular/dev-infra/github-actions/create-pr-for-changes@14c3d6bd2fa5c3231be7bd4b3c0bba68c9d79e94 + with: + branch-prefix: update-cdk-apis + pr-title: 'docs: update Angular CDK apis [${{github.ref_name}}]' + pr-description: | + Updated Angular CDK api files. + pr-labels: | + action: review + area: docs + angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }} diff --git a/adev/scripts/shared/copy-json-assets.mjs b/adev/scripts/shared/copy-json-assets.mjs new file mode 100644 index 00000000000..ef62017b2ee --- /dev/null +++ b/adev/scripts/shared/copy-json-assets.mjs @@ -0,0 +1,90 @@ +/** + * @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.dev/license + */ + +//tslint:disable:no-console +import {execSync} from 'node:child_process'; +import {existsSync, constants as fsConstants} from 'node:fs'; +import {copyFile, mkdtemp, readdir, readFile, realpath, unlink, writeFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; + +export async function copyJsonAssets({repo, githubApi, assetsPath, destPath}) { + const buildInfoPath = join(destPath, '_build-info.json'); + if (!existsSync(buildInfoPath)) { + throw new Error(`${buildInfoPath} does not exist.`); + } + + const branch = process.env.GITHUB_REF; + const {sha: currentSha} = JSON.parse(await readFile(buildInfoPath, 'utf-8')); + const latestSha = await githubApi.getShaForBranch(branch); + + console.log(`Comparing ${currentSha}...${latestSha}.`); + const affectedFiles = await githubApi.getAffectedFiles(currentSha, latestSha); + const changedFiles = affectedFiles.filter((file) => file.startsWith(`${assetsPath}/`)); + + if (changedFiles.length === 0) { + console.log(`No '${assetsPath}/**' files changed between ${currentSha} and ${latestSha}.`); + return; + } + + console.log( + `The below files changed between ${currentSha} and ${latestSha}:\n` + + changedFiles.map((f) => '* ' + f).join('\n'), + ); + + const temporaryDir = await realpath(await mkdtemp(join(tmpdir(), 'copy-json-assets-'))); + const execOptions = {cwd: temporaryDir, stdio: 'inherit'}; + execSync('git init', execOptions); + execSync(`git remote add origin https://github.com/${repo}.git`, execOptions); + // fetch a commit + execSync(`git fetch origin ${latestSha}`, execOptions); + // reset this repository's main branch to the commit of interest + execSync('git reset --hard FETCH_HEAD', execOptions); + // get sha when files where changed + const shaWhenFilesChanged = execSync(`git rev-list -1 ${latestSha} "${assetsPath}/"`, { + encoding: 'utf8', + cwd: temporaryDir, + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + + // Delete existing asset files. + const apiFilesUnlink = (await readdir(destPath)) + .filter((f) => f.endsWith('.json')) + .map((f) => unlink(join(destPath, f))); + + await Promise.allSettled(apiFilesUnlink); + + // Copy new asset files + const tempAssetsDir = join(temporaryDir, assetsPath); + const assetFilesCopy = (await readdir(tempAssetsDir)).map((f) => { + const src = join(tempAssetsDir, f); + const dest = join(destPath, f); + + return copyFile(src, dest, fsConstants.COPYFILE_FICLONE); + }); + + await Promise.allSettled(assetFilesCopy); + + // Write SHA to file. + await writeFile( + buildInfoPath, + JSON.stringify( + { + branchName: branch, + sha: shaWhenFilesChanged, + }, + undefined, + 2, + ), + ); + + console.log('\nChanges: '); + execSync(`git status --porcelain`, {stdio: 'inherit'}); + + console.log(`Successfully updated asset files in '${destPath}'.\n`); +} diff --git a/adev/scripts/shared/github-client.mjs b/adev/scripts/shared/github-client.mjs new file mode 100644 index 00000000000..4e1fcfac5d8 --- /dev/null +++ b/adev/scripts/shared/github-client.mjs @@ -0,0 +1,77 @@ +/** + * @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.dev/license + */ + +import {get} from 'node:https'; +import {posix} from 'node:path'; + +const GITHUB_API = 'https://api.github.com/repos/'; + +export class GithubClient { + #token; + #ua; + #api; + + constructor(repo, token, ua) { + this.#token = token; + this.#ua = ua; + this.#api = posix.join(GITHUB_API, repo); + } + + /** + * Get the affected files. + * + * @param {string} baseSha + * @param {string} headSha + * @returns Promise + */ + async getAffectedFiles(baseSha, headSha) { + const {files} = JSON.parse(await this.#httpGet(`${this.#api}/compare/${baseSha}...${headSha}`)); + return files.map((f) => f.filename); + } + + /** + * Get SHA of a branch. + * + * @param {string} branch + * @returns Promise + */ + async getShaForBranch(branch) { + const sha = await this.#httpGet(`${this.#api}/commits/${branch}`, { + headers: {Accept: 'application/vnd.github.VERSION.sha'}, + }); + + if (!sha) { + throw new Error(`Unable to extract the SHA for '${branch}'.`); + } + + return sha; + } + + #httpGet(url, options = {}) { + options.headers ??= {}; + options.headers['Authorization'] = `token ${this.#token}`; + // User agent is required + // https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#user-agent-required + options.headers['User-Agent'] = this.#ua; + + return new Promise((resolve, reject) => { + get(url, options, (res) => { + let data = ''; + res + .on('data', (chunk) => { + data += chunk; + }) + .on('end', () => { + resolve(data); + }); + }).on('error', (e) => { + reject(e); + }); + }); + } +} diff --git a/adev/scripts/update-cdk-apis/README.md b/adev/scripts/update-cdk-apis/README.md new file mode 100644 index 00000000000..bcebde73992 --- /dev/null +++ b/adev/scripts/update-cdk-apis/README.md @@ -0,0 +1,3 @@ +# Generating data for `angular.dev/api` CDK packages + +This script updates the Angular CDK api JSON files stored in `adev/src/content/cdk`. This files are used to generate the [angular.dev api](https://angular.dev/api) pages for the CDK packages. diff --git a/adev/scripts/update-cdk-apis/index.mjs b/adev/scripts/update-cdk-apis/index.mjs new file mode 100644 index 00000000000..905db31466d --- /dev/null +++ b/adev/scripts/update-cdk-apis/index.mjs @@ -0,0 +1,35 @@ +/** + * @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.dev/license + */ + +import {dirname, resolve as resolvePath} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {copyJsonAssets} from '../shared/copy-json-assets.mjs'; +import {GithubClient} from '../shared/github-client.mjs'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); + +const CDK_BUILDS_REPO = 'angular/cdk-builds'; +const CDK_APIS_CONTENT_PATH = resolvePath(scriptDir, '../../src/content/cdk'); + +async function main() { + await copyJsonAssets({ + repo: CDK_BUILDS_REPO, + assetsPath: '_adev_assets', + destPath: CDK_APIS_CONTENT_PATH, + githubApi: new GithubClient( + CDK_BUILDS_REPO, + process.env.ANGULAR_CDK_BUILDS_READONLY_GITHUB_TOKEN, + 'ADEV_Angular_CDK_Sources_Update', + ), + }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/adev/scripts/update-cli-help/index.mjs b/adev/scripts/update-cli-help/index.mjs index e36fc9344d0..042812fb185 100644 --- a/adev/scripts/update-cli-help/index.mjs +++ b/adev/scripts/update-cli-help/index.mjs @@ -6,154 +6,26 @@ * found in the LICENSE file at https://angular.dev/license */ -//tslint:disable:no-console -import {execSync} from 'node:child_process'; -import {readFile, writeFile, readdir, mkdtemp, realpath, copyFile, unlink} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {get} from 'node:https'; -import {dirname, resolve as resolvePath, posix, join} from 'node:path'; +import {dirname, resolve as resolvePath} from 'node:path'; import {fileURLToPath} from 'node:url'; -import {existsSync, constants as fsConstants} from 'node:fs'; - -const GITHUB_API = 'https://api.github.com/repos/'; -const CLI_BUILDS_REPO = 'angular/cli-builds'; -const GITHUB_API_CLI_BUILDS = posix.join(GITHUB_API, CLI_BUILDS_REPO); +import {copyJsonAssets} from '../shared/copy-json-assets.mjs'; +import {GithubClient} from '../shared/github-client.mjs'; const scriptDir = dirname(fileURLToPath(import.meta.url)); + +const CLI_BUILDS_REPO = 'angular/cli-builds'; const CLI_HELP_CONTENT_PATH = resolvePath(scriptDir, '../../src/content/cli/help'); -const CLI_SHA_PATH = join(CLI_HELP_CONTENT_PATH, 'build-info.json'); async function main() { - if (!existsSync(CLI_SHA_PATH)) { - throw new Error(`${CLI_SHA_PATH} does not exist.`); - } - - const branch = process.env.GITHUB_REF; - const {sha: currentSha} = JSON.parse(await readFile(CLI_SHA_PATH, 'utf-8')); - const latestSha = await getShaFromCliBuilds(branch); - - console.log(`Comparing ${currentSha}...${latestSha}.`); - const affectedFiles = await getAffectedFiles(currentSha, latestSha); - const changedHelpFiles = affectedFiles.filter((file) => file.startsWith('help/')); - - if (changedHelpFiles.length === 0) { - console.log(`No 'help/**' files changed between ${currentSha} and ${latestSha}.`); - - return; - } - - console.log( - `The below help files changed between ${currentSha} and ${latestSha}:\n` + - changedHelpFiles.map((f) => '* ' + f).join('\n'), - ); - - const temporaryDir = await realpath(await mkdtemp(join(tmpdir(), 'cli-src-'))); - const execOptions = {cwd: temporaryDir, stdio: 'inherit'}; - execSync('git init', execOptions); - execSync('git remote add origin https://github.com/angular/cli-builds.git', execOptions); - // fetch a commit - execSync(`git fetch origin ${latestSha}`, execOptions); - // reset this repository's main branch to the commit of interest - execSync('git reset --hard FETCH_HEAD', execOptions); - // get sha when files where changed - const shaWhenFilesChanged = execSync(`git rev-list -1 ${latestSha} "help/"`, { - encoding: 'utf8', - cwd: temporaryDir, - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - - // Delete existing JSON help files. - const helpFilesUnlink = (await readdir(CLI_HELP_CONTENT_PATH)) - .filter((f) => f.endsWith('.json')) - .map((f) => unlink(join(CLI_HELP_CONTENT_PATH, f))); - - await Promise.allSettled(helpFilesUnlink); - - // Copy new help files - const tempHelpDir = join(temporaryDir, 'help'); - const helpFilesCopy = (await readdir(tempHelpDir)).map((f) => { - const src = join(tempHelpDir, f); - const dest = join(CLI_HELP_CONTENT_PATH, f); - - return copyFile(src, dest, fsConstants.COPYFILE_FICLONE); - }); - - await Promise.allSettled(helpFilesCopy); - - // Write SHA to file. - await writeFile( - CLI_SHA_PATH, - JSON.stringify( - { - branchName: branch, - sha: shaWhenFilesChanged, - }, - undefined, - 2, + await copyJsonAssets({ + repo: CLI_BUILDS_REPO, + assetsPath: 'help', + destPath: CLI_HELP_CONTENT_PATH, + githubApi: new GithubClient( + CLI_BUILDS_REPO, + process.env.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN, + 'ADEV_Angular_CLI_Sources_Update', ), - ); - - console.log('\nChanges: '); - execSync(`git status --porcelain`, {stdio: 'inherit'}); - - console.log(`Successfully updated help files in '${CLI_HELP_CONTENT_PATH}'.\n`); -} - -/** - * Get SHA of a branch. - * - * @param {string} branch - * @param {string} headSha - * @returns Promise - */ -async function getShaFromCliBuilds(branch) { - const sha = await httpGet(`${GITHUB_API_CLI_BUILDS}/commits/${branch}`, { - headers: {Accept: 'application/vnd.github.VERSION.sha'}, - }); - - if (!sha) { - throw new Error(`Unable to extract the SHA for '${branch}'.`); - } - - return sha; -} - -/** - * Get the affected files. - * - * @param {string} baseSha - * @param {string} headSha - * @returns Promise - */ -async function getAffectedFiles(baseSha, headSha) { - const {files} = JSON.parse( - await httpGet(`${GITHUB_API_CLI_BUILDS}/compare/${baseSha}...${headSha}`), - ); - return files.map((f) => f.filename); -} - -function httpGet(url, options = {}) { - options.headers ??= {}; - options.headers[ - 'Authorization' - ] = `token ${process.env.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN}`; - // User agent is required - // https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#user-agent-required - options.headers['User-Agent'] = `ADEV_Angular_CLI_Sources_Update`; - - return new Promise((resolve, reject) => { - get(url, options, (res) => { - let data = ''; - res - .on('data', (chunk) => { - data += chunk; - }) - .on('end', () => { - resolve(data); - }); - }).on('error', (e) => { - reject(e); - }); }); } diff --git a/adev/src/content/cdk/_build-info.json b/adev/src/content/cdk/_build-info.json new file mode 100644 index 00000000000..100ce82d18b --- /dev/null +++ b/adev/src/content/cdk/_build-info.json @@ -0,0 +1,4 @@ +{ + "branchName": "refs/heads/main", + "sha": "9500b84652d34db69e545a1b24b2728ea86f0328" +} diff --git a/adev/src/content/cli/BUILD.bazel b/adev/src/content/cli/BUILD.bazel index 1e2db063cf4..b899cc30b5a 100644 --- a/adev/src/content/cli/BUILD.bazel +++ b/adev/src/content/cli/BUILD.bazel @@ -9,6 +9,6 @@ filegroup( ], ) + [ "//adev/src/content/cli/help", - "//adev/src/content/cli/help:build-info.json", + "//adev/src/content/cli/help:_build-info.json", ], ) diff --git a/adev/src/content/cli/help/BUILD.bazel b/adev/src/content/cli/help/BUILD.bazel index 68ed9cdae7c..f7a6207855f 100644 --- a/adev/src/content/cli/help/BUILD.bazel +++ b/adev/src/content/cli/help/BUILD.bazel @@ -2,16 +2,15 @@ load("//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html.bzl", "re package(default_visibility = ["//visibility:public"]) -exports_files(["build-info.json"]) +exports_files(["_build-info.json"]) filegroup( name = "help", srcs = glob( - ["*"], + ["*.json"], exclude = [ - # Exlucde build-info.json as it is not a help entry. - "build-info.json", - "BUILD.bazel", + # Exlucde _build-info.json as it is not a help entry. + "_*.json", ], ), ) diff --git a/adev/src/content/cli/help/build-info.json b/adev/src/content/cli/help/_build-info.json similarity index 100% rename from adev/src/content/cli/help/build-info.json rename to adev/src/content/cli/help/_build-info.json