Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: roll windows arc images automatically #124

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
Copy link
Member

Choose a reason for hiding this comment

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

Not a big deal, but do we need this line?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really, I just ignore it in any project I touch that has local dev secrets. It's just because I use https://direnv.net/

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 =
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved
/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...`,
);
}
}
}
}
}