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
This commit is contained in:
Miles Malerba 2025-04-30 23:12:06 +00:00 committed by Andrew Kushnir
parent 88858118ea
commit 8828a84ecf
10 changed files with 271 additions and 147 deletions

44
.github/workflows/update-cdk-apis.yml vendored Normal file
View file

@ -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 }}

View file

@ -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`);
}

View file

@ -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<string[]>
*/
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<string>
*/
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);
});
});
}
}

View file

@ -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.

View file

@ -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);
});

View file

@ -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<string>
*/
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<string[]>
*/
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);
});
});
}

View file

@ -0,0 +1,4 @@
{
"branchName": "refs/heads/main",
"sha": "9500b84652d34db69e545a1b24b2728ea86f0328"
}

View file

@ -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",
],
)

View file

@ -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",
],
),
)