Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/build-images-chromium-deps-cron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { handleBuildImagesChromiumDepsCheck } from './build-images-chromium-deps-handler';

if (require.main === module) {
handleBuildImagesChromiumDepsCheck().catch((err) => {
console.log('Build Images Chromium Deps Cron Failed');
console.error(err);
process.exit(1);
});
}
87 changes: 87 additions & 0 deletions src/build-images-chromium-deps-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as debug from 'debug';

import { REPOS, BUILD_IMAGES_INSTALL_DEPS_FILE, CHROMIUM_DEPS_FILES } from './constants';
import { getOctokit } from './utils/octokit';
import { getChromiumHeadSha, didChromiumFilesChange } from './utils/chromium-gitiles';
import { rollBuildImages, getFileContentFromBuildImages } from './utils/roll-build-images';

// Regex to extract CHROMIUM_SRC_SHA from install-deps.sh
const CHROMIUM_SHA_REGEX = /CHROMIUM_SRC_SHA="([a-f0-9]{40})"/;

/**
* Get the currently pinned Chromium SHA from the build-images repo
*/
async function getCurrentPinnedSha(): Promise<string> {
const octokit = await getOctokit();
const { raw: content } = await getFileContentFromBuildImages(
octokit,
BUILD_IMAGES_INSTALL_DEPS_FILE,
);

const match = content.match(CHROMIUM_SHA_REGEX);
if (!match || !match[1]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!match || !match[1]) {
if (!match?.[1]) {

suggestion(non-blocking): modern syntax is nice

throw new Error(`Could not find CHROMIUM_SRC_SHA in ${BUILD_IMAGES_INSTALL_DEPS_FILE}`);
}

return match[1];
}

/**
* Update the install-deps.sh file with a new Chromium SHA
*/
function updateInstallDepsContent(content: string, newSha: string): string {
return content.replace(CHROMIUM_SHA_REGEX, `CHROMIUM_SRC_SHA="${newSha}"`);
}

export async function handleBuildImagesChromiumDepsCheck(): Promise<void> {
const d = debug('roller/build-images-chromium-deps:handleBuildImagesChromiumDepsCheck()');

const octokit = await getOctokit();

d('Getting current pinned SHA from build-images');
const currentPinnedSha = await getCurrentPinnedSha();
d(`Current pinned SHA: ${currentPinnedSha}`);

d('Getting current HEAD SHA from Chromium');
const currentHeadSha = await getChromiumHeadSha();
d(`Current HEAD SHA: ${currentHeadSha}`);

if (currentPinnedSha === currentHeadSha) {
d('Pinned SHA is already at HEAD, nothing to do');
return;
}

d(
`Checking if any of ${CHROMIUM_DEPS_FILES.join(', ')} changed between ${currentPinnedSha} and ${currentHeadSha}`,
);
const filesChanged = await didChromiumFilesChange(
CHROMIUM_DEPS_FILES,
currentPinnedSha,
currentHeadSha,
);

if (!filesChanged) {
d('No changes detected in monitored files, skipping roll');
return;
}

d('Changes detected, creating PR to update SHA');

const { raw: currentContent } = await getFileContentFromBuildImages(
octokit,
BUILD_IMAGES_INSTALL_DEPS_FILE,
);

const newContent = updateInstallDepsContent(currentContent, currentHeadSha);

await rollBuildImages(
'chromium-deps',
'chromium deps',
currentPinnedSha,
currentHeadSha,
BUILD_IMAGES_INSTALL_DEPS_FILE,
newContent,
);

d('Successfully created/updated PR');
}
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export const REPOS = {
owner: 'electron',
repo: 'infra',
},
buildImages: {
owner: 'electron',
repo: 'build-images',
},
node: {
owner: 'nodejs',
repo: 'node',
Expand Down Expand Up @@ -51,6 +55,10 @@ export const ARC_RUNNER_ENVIRONMENTS = {
export const WINDOWS_DOCKER_FILE = 'docker/windows-actions-runner/Dockerfile';
export const WINDOWS_DOCKER_IMAGE_NAME = 'windows-actions-runner';

// Build-images Chromium deps configuration
export const BUILD_IMAGES_INSTALL_DEPS_FILE = 'tools/install-deps.sh';
export const CHROMIUM_DEPS_FILES = ['build/install-build-deps.sh', 'build/install-build-deps.py'];

export interface Commit {
sha: string;
message: string;
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { handleNodeCheck } from './node-handler';
import { handleChromiumCheck } from './chromium-handler';
import { handleBuildImagesCheck } from './build-images-handler';
import { handleBuildImagesChromiumDepsCheck } from './build-images-chromium-deps-handler';
import { ROLL_TARGETS } from './constants';

const handler = (robot: Probot) => {
Expand All @@ -25,6 +26,9 @@ const handler = (robot: Probot) => {
if (isChromiumPR) {
d('Chromium PR merged - opening a new one');
await handleChromiumCheck();
// Also check if build-images chromium deps need updating
d('Checking if build-images chromium deps need updating');
await handleBuildImagesChromiumDepsCheck();
} else if (isNodePR) {
d('Node.js PR merged - opening a new one');
await handleNodeCheck();
Expand Down
44 changes: 44 additions & 0 deletions src/utils/chromium-gitiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const CHROMIUM_GITILES_BASE = 'https://chromium.googlesource.com/chromium/src';

/**
* Get the current HEAD SHA from Chromium main branch
*/
export async function getChromiumHeadSha(): Promise<string> {
const response = await fetch(`${CHROMIUM_GITILES_BASE}/+/refs/heads/main?format=JSON`);
const text = await response.text();
// Gitiles returns JSON with )]}' prefix for security
const data = JSON.parse(text.slice(text.indexOf('{')));
return data.commit;
}

/**
* Get file content at a specific SHA from Chromium repo
*/
export async function getChromiumFileContent(filePath: string, sha: string): Promise<string> {
const url = `${CHROMIUM_GITILES_BASE}/+/${sha}/${filePath}?format=TEXT`;
const response = await fetch(url);
const base64Content = await response.text();
return Buffer.from(base64Content, 'base64').toString('utf8');
}

/**
* Check if any of the specified files changed between two SHAs
*/
export async function didChromiumFilesChange(
files: string[],
fromSha: string,
toSha: string,
): Promise<boolean> {
for (const file of files) {
const [fromContent, toContent] = await Promise.all([
getChromiumFileContent(file, fromSha),
getChromiumFileContent(file, toSha),
]);

if (fromContent.trim() !== toContent.trim()) {
return true;
}
}

return false;
}
164 changes: 164 additions & 0 deletions src/utils/roll-build-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as debug from 'debug';

import { MAIN_BRANCH, REPOS } from '../constants';
import { getOctokit } from './octokit';
import { PullsListResponseItem } from '../types';
import { Octokit } from '@octokit/rest';

export async function getFileContentFromBuildImages(
octokit: Octokit,
filePath: string,
ref = MAIN_BRANCH,
) {
const { data } = await octokit.repos.getContent({
...REPOS.buildImages,
path: filePath,
ref,
});
if ('content' in data) {
return { raw: Buffer.from(data.content, 'base64').toString('utf8'), sha: data.sha };
}
throw new Error(`Failed to get content for ${filePath}`);
}

function getBuildImagesPRText(bumpSubject: string, newShortVersion: string, diffLink: string) {
return {
title: `build: bump ${bumpSubject} to ${newShortVersion.substring(0, 12)}`,
body: `Updating ${bumpSubject} to \`${newShortVersion}\`

See [changes in Chromium](${diffLink})`,
};
}

export async function rollBuildImages(
rollKey: string,
bumpSubject: string,
previousSha: string,
newSha: string,
filePath: string,
newContent: string,
): Promise<any> {
const d = debug(`roller/build-images/${rollKey}:rollBuildImages()`);
const octokit = await getOctokit();

const branchName = `roller/build-images/${rollKey}`;
const shortRef = `heads/${branchName}`;
const ref = `refs/${shortRef}`;

const { owner, repo } = REPOS.buildImages;

const diffLink =
`https://chromium.googlesource.com/chromium/src/+log/` +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`https://chromium.googlesource.com/chromium/src/+log/` +
'https://chromium.googlesource.com/chromium/src/+log/' +

suggestion(non-blocking): this could be single quotes rather than backticks since there's no substitution in this part which is just being concatenated.

`${previousSha}..${newSha}?n=10000&pretty=fuller`;

// Look for a pre-existing PR that targets this branch to see if we can update that.
let existingPrsForBranch: PullsListResponseItem[] = [];
try {
existingPrsForBranch = (await octokit.paginate('GET /repos/:owner/:repo/pulls', {
head: `${owner}:${branchName}`,
owner,
repo,
state: 'open',
})) as PullsListResponseItem[];
} catch {}

const prs = existingPrsForBranch.filter((pr) =>
pr.title.startsWith(`build: bump ${bumpSubject}`),
);

const defaultBranchHeadSha = (
await octokit.repos.getBranch({
owner,
repo,
branch: MAIN_BRANCH,
})
).data.commit.sha;

if (prs.length) {
// Update existing PR(s)
for (const pr of prs) {
d(`Found existing PR: #${pr.number} opened by ${pr.user.login}`);

// Check to see if automatic roll has been temporarily disabled
const hasPauseLabel = pr.labels.some((label) => label.name === 'roller/pause');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: at some point we should really pull 'roller/pause' out into src/constants.ts since multiple files now have it duplicated.

if (hasPauseLabel) {
d(`Automatic updates have been paused for #${pr.number}, skipping roll.`);
continue;
}

d(`Attempting update for #${pr.number}`);
const { raw: currentContent, sha: currentSha } = await getFileContentFromBuildImages(
octokit,
filePath,
pr.head.ref,
);
if (currentContent.trim() !== newContent.trim()) {
await updateFile(
octokit,
bumpSubject,
newSha,
filePath,
newContent,
branchName,
currentSha,
);
}

await octokit.pulls.update({
owner,
repo,
pull_number: pr.number,
...getBuildImagesPRText(bumpSubject, newSha, diffLink),
});
}
} else {
try {
d(`roll triggered for ${bumpSubject}=${newSha}`);

try {
await octokit.git.getRef({ owner, repo, ref: shortRef });
d(`Ref ${ref} already exists, deleting`);
await octokit.git.deleteRef({ owner, repo, ref: shortRef });
} catch {
// Ignore
} finally {
d(`Creating ref=${ref} at sha=${defaultBranchHeadSha}`);
await octokit.git.createRef({ owner, repo, ref, sha: defaultBranchHeadSha });
}

const { sha: currentSha } = await getFileContentFromBuildImages(octokit, filePath);

await updateFile(octokit, bumpSubject, newSha, filePath, newContent, branchName, currentSha);

d(`Raising a PR for ${branchName} to ${repo}`);
await octokit.pulls.create({
owner,
repo,
base: MAIN_BRANCH,
head: `${owner}:${branchName}`,
...getBuildImagesPRText(bumpSubject, newSha, diffLink),
});
} catch (e) {
d(`Error rolling ${owner}/${repo} to ${newSha}`, e);
}
}
}

async function updateFile(
octokit: Octokit,
bumpSubject: string,
newShortVersion: string,
filePath: string,
newContent: string,
branchName: string,
currentSha: string,
) {
await octokit.repos.createOrUpdateFileContents({
...REPOS.buildImages,
path: filePath,
message: `build: bump ${bumpSubject} in ${filePath} to ${newShortVersion.substring(0, 12)}`,
content: Buffer.from(newContent).toString('base64'),
branch: branchName,
sha: currentSha,
});
}
Loading