Skip to content

Commit

Permalink
build: roll windows arc images automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
MarshallOfSound committed Nov 13, 2024
1 parent 7e33eff commit d4e29a8
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
lib
node_modules
coverage
.envrc
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export const REPOS = {
owner: 'electron',
repo: 'electron',
},
electronInfra: {
owner: 'electron',
repo: 'infra',
},
node: {
owner: 'nodejs',
repo: 'node',
Expand Down Expand Up @@ -41,6 +45,11 @@ export const ORB_TARGETS = [
export const BACKPORT_CHECK_SKIP = 'backport-check-skip';
export const NO_BACKPORT = 'no-backport';

export const ARC_RUNNER_ENVIRONMENTS = {
prod: 'argo/arc-cluster/runner-sets/runners.yaml',
};
export const WINDOWS_DOCKER_IMAGE_NAME = 'windows-actions-runner';

export interface Commit {
sha: string;
message: string;
Expand Down
34 changes: 34 additions & 0 deletions src/utils/arc-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Octokit } from '@octokit/rest';
import { MAIN_BRANCH, REPOS } from '../constants';
import { getOctokit } from './octokit';

const WINDOWS_IMAGE_REGEX =
/electronarc\.azurecr\.io\/win-actions-runner:main-[a-f0-9]{7}@sha256:[a-f0-9]{64}/;
const LINUX_IMAGE_REGEX =
/ghcr\.io\/actions\/actions-runner:[0-9]+\.[0-9]+\.[0-9]+@sha256:[a-f0-9]{64}/;

export async function getFileContent(octokit: Octokit, filePath: string, ref = MAIN_BRANCH) {
const { data } = await octokit.repos.getContent({
...REPOS.electronInfra,
path: filePath,
ref,
});
if ('content' in data) {
return { raw: Buffer.from(data.content, 'base64').toString('utf8'), sha: data.sha };
}
throw 'wat';
}

export const currentWindowsImage = (content: string) => {
return content.match(WINDOWS_IMAGE_REGEX)?.[0];
};

export const didFileChangeBetweenShas = async (file: string, sha1: string, sha2: string) => {
const octokit = await getOctokit();
const [start, end] = await Promise.all([
await getFileContent(octokit, file, sha1),
await getFileContent(octokit, file, sha2),
]);

return start.raw.trim() !== end.raw.trim();
};
7 changes: 7 additions & 0 deletions src/utils/pr-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,10 @@ Original-Version: ${previousVersion}
Notes: Updated Node.js to ${newVersion}.`,
};
}

export function getInfraPRText(bumpSubject: string, newShortVersion: string) {
return {
title: `build: bump ${bumpSubject} to ${newShortVersion}`,
body: `Updating ${bumpSubject} to \`${newShortVersion}\``,
};
}
144 changes: 144 additions & 0 deletions src/utils/roll-infra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as debug from 'debug';

import { MAIN_BRANCH, REPOS } from '../constants';
import { getOctokit } from './octokit';
import { PullsListResponseItem } from '../types';
import { Octokit } from '@octokit/rest';
import { getInfraPRText } from './pr-text';
import { getFileContent } from './arc-image';

export async function rollInfra(
rollKey: string,
bumpSubject: string,
newShortVersion: string,
filePath: string,
newContent: string,
): Promise<any> {
const d = debug(`roller/infra/${rollKey}:rollInfra()`);
const octokit = await getOctokit();

const branchName = `roller/infra/${rollKey}`;
const shortRef = `heads/${branchName}`;
const ref = `refs/${shortRef}`;

const { owner, repo } = REPOS.electronInfra;

// 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: 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 infra roll has been temporarily disabled
const hasPauseLabel = pr.labels.some((label) => label.name === 'roller/pause');
if (hasPauseLabel) {
d(`Automatic updates have been paused for #${pr.number}, skipping infra roll.`);
continue;
}

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

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

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 getFileContent(octokit, filePath);

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

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

async function updateFile(
octokit: Octokit,
bumpSubject: string,
newShortVersion: string,
filePath: string,
newContent: string,
branchName: string,
currentSha: string,
) {
await octokit.repos.createOrUpdateFileContents({
...REPOS.electronInfra,
path: filePath,
message: `build: bump ${bumpSubject} in ${filePath} to ${newShortVersion}`,
content: Buffer.from(newContent).toString('base64'),
branch: branchName,
sha: currentSha,
});
}
9 changes: 9 additions & 0 deletions src/windows-image-cron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { rollWindowsArcImage } from './windows-image-handler';

if (require.main === module) {
rollWindowsArcImage().catch((err: Error) => {
console.log('Windows Image Cron Failed');
console.error(err);
process.exit(1);
});
}
92 changes: 92 additions & 0 deletions src/windows-image-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as debug from 'debug';

import {
REPOS,
WINDOWS_DOCKER_IMAGE_NAME,
ARC_RUNNER_ENVIRONMENTS,
MAIN_BRANCH,
} from './constants';
import { getOctokit } from './utils/octokit';
import { currentWindowsImage, didFileChangeBetweenShas } from './utils/arc-image';
import { rollInfra } from './utils/roll-infra';

async function getLatestVersionOfImage() {
const octokit = await getOctokit();
// return a list of tags for the repo, filter out any that aren't valid semver
const versions = await octokit.paginate(
'GET /orgs/{org}/packages/{package_type}/{package_name}/versions',
{
org: REPOS.electronInfra.owner,
package_type: 'container',
package_name: WINDOWS_DOCKER_IMAGE_NAME,
},
);

let best = null;
let bestMainTag = null;
for (const version of versions) {
// Only images built from main should be bumped to
const mainTag = version.metadata?.container?.tags?.find((t) => t.startsWith(`${MAIN_BRANCH}-`));
if (!mainTag) continue;
if (!best) {
best = version;
bestMainTag = mainTag;
continue;
}

if (new Date(best.created_at).getTime() < new Date(version.created_at).getTime()) {
best = version;
bestMainTag = mainTag;
}
}
return [`electronarc.azurecr.io/win-actions-runner:${bestMainTag}@${best.name}`, bestMainTag];
}

const WINDOWS_IMAGE_DOCKERFILE_PATH = 'docker/windows-actions-runner/Dockerfile';

export async function rollWindowsArcImage() {
const d = debug(`roller/infra:rollWindowsArcImage()`);
const octokit = await getOctokit();

const [latestWindowsImage, shortLatestTag] = await getLatestVersionOfImage();

for (const arcEnv of Object.keys(ARC_RUNNER_ENVIRONMENTS)) {
d(`Fetching current version of "${arcEnv}" arc image in: ${ARC_RUNNER_ENVIRONMENTS[arcEnv]}`);

const currentVersion = await octokit.repos.getContent({
owner: REPOS.electronInfra.owner,
repo: REPOS.electronInfra.repo,
path: ARC_RUNNER_ENVIRONMENTS[arcEnv],
});
const data = currentVersion.data;
if ('content' in data) {
const currentContent = Buffer.from(data.content, 'base64').toString('utf8');
const currentImage = currentWindowsImage(currentContent);

if (currentImage !== latestWindowsImage) {
const currentSha = currentImage.split(`${MAIN_BRANCH}-`)[1].split('@')[0];
if (
await didFileChangeBetweenShas(
WINDOWS_IMAGE_DOCKERFILE_PATH,
currentSha,
shortLatestTag.split('-')[1],
)
) {
d(`Current image in "${arcEnv}" is outdated, updating...`);
const newContent = currentContent.replace(currentImage, latestWindowsImage);
await rollInfra(
`${arcEnv}/windows-image`,
'windows arc image',
shortLatestTag,
ARC_RUNNER_ENVIRONMENTS[arcEnv],
newContent,
);
} else {
d(
`Current image in "${arcEnv}" is not latest sha but is considered up-to-date, skipping...`,
);
}
}
}
}
}

0 comments on commit d4e29a8

Please sign in to comment.