add caching to the build to speed up incremental changes
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
@@ -80,8 +80,18 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
|
||||
- name: restore package-docs cache
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-
|
||||
|
||||
- name: build API reference (beta)
|
||||
run: yarn backstage-repo-tools package-docs
|
||||
continue-on-error: true
|
||||
|
||||
- name: upload API reference (beta)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -91,6 +101,14 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
|
||||
- name: save package-docs cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
if: always()
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
|
||||
|
||||
- name: microsite yarn install
|
||||
run: yarn install --immutable
|
||||
working-directory: microsite
|
||||
@@ -147,8 +165,18 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
|
||||
- name: restore package-docs cache
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-
|
||||
|
||||
- name: build API reference (beta)
|
||||
run: yarn backstage-repo-tools package-docs
|
||||
continue-on-error: true
|
||||
|
||||
- name: upload API reference (beta)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -158,6 +186,14 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
|
||||
- name: save package-docs cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
if: always()
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-${{ github.run_id }}
|
||||
|
||||
# Also build and upload storybook
|
||||
- name: storybook yarn install
|
||||
run: yarn install --immutable
|
||||
|
||||
@@ -85,8 +85,18 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
|
||||
- name: restore package-docs cache
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-
|
||||
|
||||
- name: build API reference (beta)
|
||||
run: yarn backstage-repo-tools package-docs
|
||||
continue-on-error: true
|
||||
|
||||
- name: upload API reference (beta)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -94,6 +104,14 @@ jobs:
|
||||
name: stable-reference-beta
|
||||
path: type-docs/
|
||||
|
||||
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
|
||||
- name: save package-docs cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
if: always()
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
|
||||
|
||||
- name: microsite yarn install
|
||||
run: yarn install --immutable
|
||||
working-directory: microsite
|
||||
@@ -149,8 +167,18 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
|
||||
- name: restore package-docs cache
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-
|
||||
|
||||
- name: build API reference (beta)
|
||||
run: yarn backstage-repo-tools package-docs
|
||||
continue-on-error: true
|
||||
|
||||
- name: upload API reference (beta)
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -158,6 +186,14 @@ jobs:
|
||||
name: next-reference-beta
|
||||
path: type-docs/
|
||||
|
||||
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
|
||||
- name: save package-docs cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
if: always()
|
||||
with:
|
||||
path: .cache/package-docs
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-${{ github.run_id }}
|
||||
|
||||
# Also build and upload storybook
|
||||
- name: storybook yarn install
|
||||
run: yarn install --immutable
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"commander": "^12.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^8.0.3",
|
||||
"globby": "^11.0.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"just-diff": "^6.0.2",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { readFile, writeFile, cp } from 'fs/promises';
|
||||
import globby from 'globby';
|
||||
import workerPath, { dirname } from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { Lockfile } from '@backstage/cli-node';
|
||||
import { paths as cliPaths } from '../../lib/paths';
|
||||
import { mkdirp } from 'fs-extra';
|
||||
|
||||
const version = '1';
|
||||
const CACHE_FILE = 'cache.json';
|
||||
|
||||
interface CacheEntry {
|
||||
hash: string;
|
||||
packageName: string;
|
||||
restoreTo: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export class PackageDocsCache {
|
||||
private keyCache: Map<string, string>;
|
||||
constructor(
|
||||
private readonly lockfile: Lockfile,
|
||||
private readonly cache: Map<string, CacheEntry>,
|
||||
private readonly cacheDir: string,
|
||||
) {
|
||||
this.keyCache = new Map();
|
||||
}
|
||||
static async loadAsync(cacheDir: string, lockfile: Lockfile) {
|
||||
const cacheFiles = await globby(`**/${CACHE_FILE}`, {
|
||||
cwd: cacheDir,
|
||||
});
|
||||
const map = new Map<string, CacheEntry>();
|
||||
for (const file of cacheFiles) {
|
||||
console.log(file);
|
||||
const pkg = dirname(file);
|
||||
const cache = await readFile(workerPath.join(cacheDir, file), 'utf-8');
|
||||
const cacheJson = JSON.parse(cache);
|
||||
map.set(pkg, cacheJson);
|
||||
}
|
||||
return new PackageDocsCache(lockfile, map, cacheDir);
|
||||
}
|
||||
|
||||
async directoryToName(directory: string) {
|
||||
const packageJson = await readFile(
|
||||
workerPath.join(directory, 'package.json'),
|
||||
'utf-8',
|
||||
);
|
||||
return JSON.parse(packageJson).name;
|
||||
}
|
||||
|
||||
async toKey(pkg: string) {
|
||||
if (this.keyCache.has(pkg)) {
|
||||
return this.keyCache.get(pkg)!;
|
||||
}
|
||||
const name = await this.directoryToName(pkg);
|
||||
const result = await globby('src/**', {
|
||||
gitignore: true,
|
||||
onlyFiles: true,
|
||||
cwd: pkg,
|
||||
});
|
||||
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(version);
|
||||
hash.update('\0');
|
||||
|
||||
for (const path of result.sort()) {
|
||||
const absPath = cliPaths.resolveTargetRoot(pkg, path);
|
||||
const pathInPackage = workerPath.join(absPath, path);
|
||||
hash.update(pathInPackage);
|
||||
hash.update('\0');
|
||||
hash.update(await readFile(absPath));
|
||||
hash.update('\0');
|
||||
}
|
||||
hash.update(this.lockfile.getDependencyTreeHash(name));
|
||||
hash.update('\0');
|
||||
const hashString = hash.digest('hex');
|
||||
this.keyCache.set(pkg, hashString);
|
||||
return hashString;
|
||||
}
|
||||
|
||||
async has(pkg: string) {
|
||||
const cache = this.cache.get(pkg);
|
||||
if (!cache) {
|
||||
return false;
|
||||
}
|
||||
const hashString = await this.toKey(pkg);
|
||||
return cache.hash === hashString;
|
||||
}
|
||||
|
||||
async restore(pkg: string) {
|
||||
if (!this.has(pkg)) {
|
||||
throw new Error(`Cache entry for ${pkg} not found`);
|
||||
}
|
||||
const cacheEntry = this.cache.get(pkg);
|
||||
const restoreTo = cacheEntry!.restoreTo;
|
||||
const cacheDir = workerPath.join(this.cacheDir, pkg);
|
||||
const contentsDir = workerPath.join(cacheDir, 'contents');
|
||||
|
||||
const targetDir = cliPaths.resolveTargetRoot(restoreTo);
|
||||
await mkdirp(targetDir);
|
||||
console.log(contentsDir, targetDir);
|
||||
await cp(contentsDir, targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
async write(pkg: string, contentDirectory: string) {
|
||||
const cacheDir = workerPath.join(this.cacheDir, pkg);
|
||||
const contentsDir = workerPath.join(cacheDir, 'contents');
|
||||
await mkdirp(cacheDir);
|
||||
const hashString = await this.toKey(pkg);
|
||||
await cp(contentDirectory, contentsDir, { recursive: true });
|
||||
const cacheEntry: CacheEntry = {
|
||||
hash: hashString,
|
||||
packageName: await this.directoryToName(pkg),
|
||||
restoreTo: workerPath.relative(
|
||||
cliPaths.resolveTargetRoot(),
|
||||
contentDirectory,
|
||||
),
|
||||
version,
|
||||
};
|
||||
await writeFile(
|
||||
workerPath.join(cacheDir, CACHE_FILE),
|
||||
JSON.stringify(cacheEntry),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,11 @@ import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths';
|
||||
import { createTemporaryTsConfig } from './utils';
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import pLimit from 'p-limit';
|
||||
import { mkdirp } from 'fs-extra';
|
||||
import { PackageDocsCache } from './Cache';
|
||||
import { Lockfile } from '@backstage/cli-node';
|
||||
|
||||
const limit = pLimit(8);
|
||||
|
||||
@@ -38,6 +41,7 @@ const EXCLUDE = [
|
||||
'packages/techdocs-cli-embedded-app',
|
||||
'packages/yarn-plugin',
|
||||
'packages/backend',
|
||||
'packages/backend-legacy',
|
||||
];
|
||||
|
||||
const HIGHLIGHT_LANGUAGES = [
|
||||
@@ -54,6 +58,8 @@ const HIGHLIGHT_LANGUAGES = [
|
||||
'json',
|
||||
];
|
||||
|
||||
const CACHE_DIR = '.cache/package-docs';
|
||||
|
||||
function getExports(packageJson: any) {
|
||||
if (packageJson.exports) {
|
||||
return Object.values(packageJson.exports).filter(
|
||||
@@ -75,41 +81,35 @@ async function generateDocJson(pkg: string) {
|
||||
!exports.length ||
|
||||
!exports.some(e => e.startsWith('src') || e.startsWith('./src'))
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(cliPaths.resolveTargetRoot(`dist-types`, pkg), {
|
||||
recursive: true,
|
||||
});
|
||||
await mkdirp(cliPaths.resolveTargetRoot(`dist-types`, pkg));
|
||||
|
||||
const { stdout, stderr } = await execAsync(
|
||||
[
|
||||
cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'),
|
||||
'--json',
|
||||
cliPaths.resolveTargetRoot(`dist-types`, pkg, 'docs.json'),
|
||||
'--tsconfig',
|
||||
temporaryTsConfigPath,
|
||||
'--basePath',
|
||||
cliPaths.targetRoot,
|
||||
'--skipErrorChecking',
|
||||
...(getExports(packageJson).flatMap(e => [
|
||||
'--entryPoints',
|
||||
e,
|
||||
]) as string[]),
|
||||
].join(' '),
|
||||
{
|
||||
cwd: pkg,
|
||||
env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=12288' },
|
||||
},
|
||||
);
|
||||
console.log(`### Processed ${pkg}`);
|
||||
console.log(stdout);
|
||||
console.error(stderr);
|
||||
} catch (e) {
|
||||
console.error('Failed to generate docs for', pkg);
|
||||
console.error(e);
|
||||
}
|
||||
const { stdout, stderr } = await execAsync(
|
||||
[
|
||||
cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'),
|
||||
'--json',
|
||||
cliPaths.resolveTargetRoot(`dist-types`, pkg, 'docs.json'),
|
||||
'--tsconfig',
|
||||
temporaryTsConfigPath,
|
||||
'--basePath',
|
||||
cliPaths.targetRoot,
|
||||
'--skipErrorChecking',
|
||||
...(getExports(packageJson).flatMap(e => [
|
||||
'--entryPoints',
|
||||
e,
|
||||
]) as string[]),
|
||||
].join(' '),
|
||||
{
|
||||
cwd: pkg,
|
||||
env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=12288' },
|
||||
},
|
||||
);
|
||||
console.log(`### Processed ${pkg}`);
|
||||
console.log(stdout);
|
||||
console.error(stderr);
|
||||
return true;
|
||||
}
|
||||
|
||||
export default async function packageDocs(paths: string[] = [], opts: any) {
|
||||
@@ -120,6 +120,12 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
|
||||
exclude: opts.exclude,
|
||||
});
|
||||
|
||||
await mkdirp(CACHE_DIR);
|
||||
const cache = await PackageDocsCache.loadAsync(
|
||||
CACHE_DIR,
|
||||
await Lockfile.load(cliPaths.resolveTargetRoot('yarn.lock')),
|
||||
);
|
||||
|
||||
console.log(`### Generating docs.`);
|
||||
await Promise.all(
|
||||
selectedPackageDirs.map(pkg =>
|
||||
@@ -127,8 +133,29 @@ export default async function packageDocs(paths: string[] = [], opts: any) {
|
||||
if (EXCLUDE.includes(pkg)) {
|
||||
return;
|
||||
}
|
||||
console.log(`### Processing ${pkg}`);
|
||||
await generateDocJson(pkg);
|
||||
if (await cache.has(pkg)) {
|
||||
console.log(`### Skipping ${pkg} due to cache hit`);
|
||||
try {
|
||||
await cache.restore(pkg);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error('Failed to restore cache for', pkg);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log(`### Processing ${pkg}`);
|
||||
const success = await generateDocJson(pkg);
|
||||
if (success) {
|
||||
await cache.write(
|
||||
pkg,
|
||||
cliPaths.resolveTargetRoot(`dist-types`, pkg),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to generate docs for', pkg);
|
||||
console.error(e);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user