首页
/ Angular adev 仓库如何自动同步 Aria、CDK 与 CLI 文档资产:update-cross-repo-docs 脚本解析

Angular adev 仓库如何自动同步 Aria、CDK 与 CLI 文档资产:update-cross-repo-docs 脚本解析

2026-09-05 11:57:27作者:胡易黎Nicole

本文围绕 adev/scripts/update-cross-repo-docs 目录下的跨仓库文档资产更新脚本展开,说明它如何从 angular/aria-buildsangular/cdk-buildsangular/cli-builds 三个下游构建仓库拉取 JSON 文档资产,落地到 adev(即 angular.dev 站点)的内容目录中。读完后你将理解该脚本的完整工作流:分支与 SHA 的版本比对、增量变更检测、临时仓库克隆与文件替换策略,以及 _build-info.json 作为同步状态锚点的设计原理。

一、脚本职责:为 angular.dev 维护三类跨仓库文档资产

根据 README 的描述,该脚本负责从其他仓库更新 angular.dev 所需的文档资产,覆盖三类内容:

资产类型 来源仓库 资产子路径 落地目录 用途
Aria API JSON angular/aria-builds _adev_assets adev/src/content/aria angular.dev 的 API 文档页
CDK API JSON angular/cdk-builds _adev_assets adev/src/content/cdk angular.dev 的 API 文档页
CLI Help JSON angular/cli-builds help adev/src/content/cli angular.dev 的 CLI 帮助页

这三个目录中存放的实际内容可在仓库中直接看到:

  • adev/src/content/aria 下包含 aria-accordion.jsonaria-combobox.jsonaria-grid.jsonaria-listbox.jsonaria-menu.jsonaria-tabs.jsonaria-toolbar.jsonaria-tree.json 等逐组件的 API JSON 文件;
  • adev/src/content/cdk 下包含 cdk_drag_drop.jsoncdk_testing.jsoncdk_testing_protractor.json 等 CDK 包的 API JSON 文件;
  • adev/src/content/cli 下包含 build.jsonserve.jsongenerate.jsonupdate.jsonextract-i18n.json 等与 CLI 子命令一一对应的帮助 JSON 文件。

每个目录中还存在一个同名的 _build-info.json 文件,它是整个同步机制的版本锚点,后文会详细展开。

二、主入口:三个仓库的批量更新编排

主入口文件 index.mjs 的逻辑非常直接——依次对三个下游仓库调用同一个 updateAssets 函数:

async function main() {
  await updateAssets({
    repo: 'angular/aria-builds',
    assetsPath: '_adev_assets',
    destPath: join(import.meta.dirname, '../../src/content/aria'),
  });

  await updateAssets({
    repo: 'angular/cdk-builds',
    assetsPath: '_adev_assets',
    destPath: join(import.meta.dirname, '../../src/content/cdk'),
  });

  await updateAssets({
    repo: 'angular/cli-builds',
    assetsPath: 'help',
    destPath: join(import.meta.dirname, '../../src/content/cli'),
  });

  console.log('\n-----------------------------------------------');
  console.log('Change list');
  console.log('-----------------------------------------------\n');
  execSync(`git status --porcelain`, {stdio: 'inherit'});
}

要点说明:

  1. 参数三元组updateAssets 接收 repo(下游 GitHub 仓库全名)、assetsPath(下游仓库中资产所在的子路径)、destPath(本仓库中的落地目录)。注意 Aria 与 CDK 仓库的资产都放在 _adev_assets 目录下,而 CLI 仓库放在 help 目录下,这与三个构建仓库各自不同的产物布局相对应。
  2. import.meta.dirname:入口脚本用 Node.js 的 ESM 内置变量 import.meta.dirname 定位自身目录,再以相对路径 ../../src/content/... 计算目标目录,保证无论从工作目录何处执行,落地路径都锚定在仓库内的 adev/src/content/ 下。
  3. 变更清单输出:全部更新完成后,脚本执行 git status --porcelain 打印改动文件清单(第 35 行),方便 CI 环境或人工快速核对本次同步实际改动了哪些文件。
  4. 失败即退出main().catch(...) 中捕获异常后 process.exit(1),确保任何一个仓库更新失败时整个任务以非零状态码结束,便于流水线感知失败。

三、核心流程:updateAssets 的版本锚点与增量同步

真正的同步逻辑全部实现在 update-assets.mjs 中的 updateAssets 函数。整个流程可以拆解为六个阶段。

3.1 读取并校验 _build-info.json

const buildInfoPath = join(destPath, '_build-info.json');
if (!existsSync(buildInfoPath)) {
  throw new Error(`${buildInfoPath} does not exist.`);
}

assert(process.env.GITHUB_REF);
const currentBranch = process.env.GITHUB_REF;

const {sha: storedSha, branchName: storedBranch} = JSON.parse(
  await readFile(buildInfoPath, 'utf-8'),
);

每个目标目录中的 _build-info.json 记录了“上一次成功同步时对应的下游分支与提交”,例如当前仓库中 adev/src/content/aria/_build-info.json 的内容为:

{
  "branchName": "refs/heads/main",
  "sha": "d2a5d8578e039301b04c1d28f09d90e73cfd36d8"
}

该文件在每次同步结束时被写回(见 3.7 节),因此它既充当增量比对的基线,也充当同步状态的水位线。脚本首先要求该文件存在,然后从环境变量 GITHUB_REF 读取当前运行所在的分支引用(这要求脚本在具备 GitHub Actions 环境的 CI 中运行),并校验其中存储的 SHA 与分支名格式:

  • shaRegex = /^[0-9a-f]{40}$/i:40 位十六进制的完整提交哈希;
  • branchRegex = /^(?!.*\.\.)[a-zA-Z0-9/_.-]+$/:合法的分支名,且显式排除包含 .. 的输入(防止路径/命令注入类风险)。

3.2 解析下游最新 SHA 及分支回退策略

assert(process.env.ANGULAR_READONLY_GITHUB_TOKEN);
const githubApi = new GithubClient(
  repo,
  process.env.ANGULAR_READONLY_GITHUB_TOKEN,
  'ADEV_Cross_Repo_Docs_Update',
);

let downstreamBranch = currentBranch;
let latestSha = await githubApi.getShaForBranch(currentBranch);
if (
  latestSha.includes('No commit') &&
  currentBranch !== 'refs/heads/main' &&
  currentBranch !== storedBranch
) {
  // 某些情况下(例如为特性新建分支时),
  // 该分支可能不存在于下游仓库。
  // 例如异常的 minor 发布(FW 20.3.x 对 Components: 20.2.x)。
  // 此时回退到最后一次已知的分支。
  latestSha = await githubApi.getShaForBranch(storedBranch);
  downstreamBranch = storedBranch;
}

这里的回退逻辑值得注意:Angular 框架(FW)与 Components/CDK 等仓库的分支名并不总是对齐的。当本仓库处于一个特性分支(如 FW 的 20.3.x 维护分支),而下游仓库根本没有同名分支时,查询会返回 “No commit” 之类的占位结果。此时脚本不再强求同名分支,而是回退到 _build-info.json 中记录的 storedBranch(上一次成功同步的分支)取最新 SHA,并把本次比对所用的分支记录为 downstreamBranch。这保证在主干之外的维护线/特性线上同步不会直接失败。

3.3 通过 GitHub API 比对两个 SHA 之间的变更文件

console.log(`Comparing ${storedSha}...${latestSha}.`);
const affectedFiles = await githubApi.getAffectedFiles(storedSha, latestSha);
const changedFiles = affectedFiles.filter((file) => file.startsWith(`${assetsPath}/`));

getAffectedFiles 调用 GitHub REST API 的 compare 端点,返回 storedShalatestSha 之间所有发生变化的文件名,脚本再按 assetsPath/ 前缀过滤,只关心资产目录内的变更。这一步决定了“是否需要执行文件替换”——如果资产目录没有任何变化,脚本只输出一行日志并直接跳到写回水位线,不做任何文件操作。

3.4 在临时 Git 仓库中检出资产并确定“文件实际变更的 SHA”

当检测到资产有变更时,脚本在系统临时目录中初始化一个一次性 Git 仓库来取文件,而不是把下游仓库整体克隆进工作区:

const temporaryDir = await realpath(await mkdtemp(join(tmpdir(), 'update-assets-')));

try {
  const execOptions = {cwd: temporaryDir, stdio: 'inherit'};
  execFileSync('git', ['init'], execOptions);
  execFileSync('git', ['remote', 'add', 'origin', `https://github.com/${repo}.git`], execOptions);
  // fetch a commit
  execFileSync('git', ['fetch', 'origin', latestSha], execOptions);
  // reset this repository's main branch to the commit of interest
  execFileSync('git', ['reset', '--hard', 'FETCH_HEAD'], execOptions);
  // get sha when files where changed
  shaWhenFilesChanged = execFileSync('git', ['rev-list', '-1', latestSha, `${assetsPath}/`], {
    encoding: 'utf8',
    cwd: temporaryDir,
    stdio: ['ignore', 'pipe', 'ignore'],
  }).trim();
  // ...
} finally {
  await rm(temporaryDir, {force: true, recursive: true});
}

流程要点:

  1. mkdtemp 创建临时目录并 realpath 解析:先 realpath 是为了消除 macOS 上 /var/private/var 这类符号链接差异带来的路径不一致问题(mkdtemp 返回的路径可能与 git 内部使用的工作目录不一致,进而引发 cwd 校验失败)。
  2. 浅取目标提交git fetch origin <latestSha> 只拉取指定提交而非完整历史,配合后续的 git reset --hard FETCH_HEAD,把临时仓库的主干定位到目标 SHA 上。
  3. git rev-list -1 <latestSha> <assetsPath>/:这是整个流程里最巧妙的一步——它找出“最近一次实际改动过 assetsPath/ 目录下文件的提交”。用这个 SHA(记为 shaWhenFilesChanged)而非 latestSha 作为写回水位线,可以让水位线精确对应“内容真正发生过变化的那一次提交”。从源码结构看,这样即使下游仓库在资产目录之外持续有大量提交,_build-info.json 里的 SHA 也始终指向与已落地资产内容严格对应的版本,比对窗口更小、日志更可解释。
  4. finally 保证清理:无论后续复制是否成功,临时目录都会被 rm -rf 删除,不污染系统。

3.5 先删后拷的原子化文件替换

替换落地目录中的 JSON 文件采用“先全部删除旧文件,再全部复制新文件”的策略:

// 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);

两个细节值得说明:

  • _build-info.json 不会被误删:删除逻辑只针对 destPath 目录下已存在的 .json 文件。虽然 _build-info.json 也是 .json,但它随后会被重写(见 3.7 节),且 adev/src/content/cli/BUILD.bazel 中的 filegroup 明确将其从资产 glob 中排除(注释写明 “Exclude _build-info.json as it is not a help entry”),因此它不参与文档渲染,只承担水位线职责。
  • COPYFILE_FICLONE 拷贝模式copyFile(src, dest, fsConstants.COPYFILE_FICLONE) 优先走文件系统的 clone(如 reflink),在同盘复制时几乎是零拷贝;不支持时回退为普通复制。对每个资产文件做并行拷贝,用 Promise.allSettled 而非 Promise.all,避免单个文件失败导致剩余任务被取消。

3.6 无变更路径

} else {
  console.log(`No '${assetsPath}/**' files changed between ${storedSha} and ${latestSha}.`);
}

若两个 SHA 之间资产目录无变化,脚本不做任何文件替换,只打印一条明确的“无变更”日志,然后继续执行水位线写回。

3.7 写回 _build-info.json

// Write SHA to file.
await writeFile(
  buildInfoPath,
  JSON.stringify(
    {
      branchName: downstreamBranch,
      sha: shaWhenFilesChanged ?? storedSha,
    },
    undefined,
    2,
  ),
);

写回规则:

  • branchName 取本次实际用于解析 SHA 的分支 downstreamBranch(正常情况即当前分支,回退情况下是 storedBranch);
  • sha 取“文件实际变更的提交” shaWhenFilesChanged;若本次没有变更则为 undefined,用空值合并回退到 storedSha

这样 _build-info.json 的语义始终自洽:它所指向的下游版本,就是本目录中已落地资产内容的来源版本。

四、GithubClient:基于 Node.js 内置 https 的最小 GitHub API 客户端

github-client.mjs 没有引入任何第三方 SDK,完全基于 node:https 手写,提供了两个能力:

const GITHUB_API = 'https://api.github.com/repos/';
const SHA_REGEX = /^[0-9a-f]{40}$/i;
const BRANCH_REGEX = /^(?!.*\.\.)[a-zA-Z0-9/_.-]+$/;

export class GithubClient {
  constructor(repo, token, ua) {
    this.#token = token;
    this.#ua = ua;
    this.#api = posix.join(GITHUB_API, repo);
  }

  async getAffectedFiles(baseSha, headSha) { /* ... */ }

  async getShaForBranch(branch) { /* ... */ }
}
  • getShaForBranch(branch):请求 GET /repos/{owner}/{repo}/commits/{branch},并通过请求头 Accept: application/vnd.github.VERSION.sha 让 GitHub 直接返回纯 SHA 文本(而非完整提交 JSON),减少解析成本;分支名先经 BRANCH_REGEX 校验,防止非法分支名拼入 URL。
  • getAffectedFiles(baseSha, headSha):请求 GET /repos/{owner}/{repo}/compare/{baseSha}...{headSha},解析响应中的 files 数组并提取 filename 列表;两个 SHA 都先做 40 位十六进制校验。
  • #httpGet 私有方法:统一注入 Authorization: token <token>User-Agent 请求头。User-Agent 是 GitHub REST API 的强制要求,脚本将其固定为 ADEV_Cross_Repo_Docs_Update,便于在 API 审计中识别该调用方。
  • 凭据使用只读 Tokenupdate-assets.mjs 中使用的环境变量是 ANGULAR_READONLY_GITHUB_TOKEN,即整个同步过程只需要下游仓库的只读访问权,最小化凭据权限面。

五、落地资产如何被 adev 站点管线消费

这些 JSON 并不是终点。从 adev/src/assets/BUILD.bazel 的构建配置可以看到,三个目录的资产被分别作为 Bazel 依赖注入到 adev 站点的构建产物中:

"adev/src/content/cli/cli_docs_html": "cli/",
"adev/src/content/aria/aria_docs_html": "api/",
"adev/src/content/cdk/cdk_docs_html": "api/",

即 CLI 帮助 JSON 渲染后的 HTML 落到站点的 cli/ 路由,Aria 与 CDK 的 API JSON 渲染后落到 api/ 路由。以 adev/src/content/cli/BUILD.bazel 为例,其规则为:

load("//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html.bzl", "render_api_to_html")

package(default_visibility = ["//visibility:public"])

exports_files(["_build-info.json"])

filegroup(
    name = "cli",
    srcs = glob(
        ["*.json"],
        exclude = [
            # Exclude _build-info.json as it is not a help entry.
            "_build-info.json",
        ],
    ),
)

render_api_to_html(
    name = "cli_docs",
    srcs = [
        ":cli",
    ],
)

这印证了两点:_build-info.jsonexports_files 显式导出但不进入 render_api_to_html 的输入(仅作为同步元数据被脚本读取);渲染规则来自 adev/shared-docs/pipeline/api-gen/rendering,说明 JSON 到 HTML 的转换是 adev 共享文档管线的一部分。也就是说,本脚本保证“数据新鲜”,Bazel 管线保证“数据可渲染”,二者职责分离。

六、运行前提与操作方式

综合 index.mjsupdate-assets.mjs 的断言与调用,运行该脚本需要满足以下前提:

  1. 环境变量 GITHUB_REF:指明当前运行所在的分支引用(如 refs/heads/main)。脚本 assert 该变量存在,说明它设计为在 GitHub Actions 等 CI 环境中运行,而不是本地手工裸跑。
  2. 环境变量 ANGULAR_READONLY_GITHUB_TOKEN:一个具备目标仓库(angular/aria-buildsangular/cdk-buildsangular/cli-builds)只读访问权的 GitHub Token,用于调用 compare 与 commits 两个 REST 端点。
  3. Git 可用:脚本依赖系统 PATH 中的 git 命令完成临时仓库的 init/fetch/reset/rev-list 操作。
  4. 三个目标目录均已有 _build-info.json:缺失会直接抛错终止,因为水位线是增量比对的基础。

满足上述条件后,脚本执行方式为直接以 Node.js 运行入口文件(脚本内部无额外构建步骤):

node adev/scripts/update-cross-repo-docs/index.mjs

执行后控制台会依次输出三个仓库的处理段落(Processing: <repo>)、比对区间(Comparing <storedSha>...<latestSha>)、变更文件列表或“无变更”提示,最后以 git status --porcelain 汇总本次在 adev 仓库内产生的全部文件变更,便于在 PR 中直接审阅。

七、小结

update-cross-repo-docs 脚本是 angular.dev 站点保持文档资产与上游构建产物同步的核心机制。它的工程价值体现在几处细节设计上:

  • 水位线锚定内容变更_build-info.json 记录的不是“上次检查时的分支头”,而是“资产内容实际发生变更的提交”(git rev-list -1 求解),使增量比对窗口始终最小、可解释;
  • 分支不对称容错:针对 Angular 各仓库分支名不对齐的场景(如异常 minor 发布周期),提供基于历史水位线的分支回退,避免同步任务在维护分支上直接失败;
  • 隔离式文件获取:用临时目录 + 单提交 fetch 的方式取文件,不向工作区引入下游仓库的完整历史;
  • 最小权限与零依赖 API 客户端:只读 Token + node:https 手写客户端,把攻击面与依赖面都压到最低。

对于维护 adev 站点内容的开发者而言,当 adev/src/content/ariaadev/src/content/cdkadev/src/content/cli 下的 JSON 资产需要刷新时,理解这套“水位线 + 增量比对 + 临时检出 + 先删后拷”的流程,是排查同步失败(如 SHA 校验报错、分支回退日志)与确认资产版本来源的关键。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384