-
Notifications
You must be signed in to change notification settings - Fork 10
build: roll github actions runners automatically #147
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
Open
jkleinsc
wants to merge
4
commits into
main
Choose a base branch
from
roll-actions-runner
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { rollActionsRunner } from './actions-runner-handler'; | ||
|
||
if (require.main === module) { | ||
rollActionsRunner().catch((err: Error) => { | ||
console.log('Actions Runner Cron Failed'); | ||
console.error(err); | ||
process.exit(1); | ||
}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import debug from 'debug'; | ||
|
||
import { WINDOWS_DOCKER_FILE, ARC_RUNNER_ENVIRONMENTS } from './constants'; | ||
import { getOctokit } from './utils/octokit'; | ||
|
||
import { getLatestRunnerImages } from './utils/get-latest-runner-images'; | ||
import { | ||
getCurrentWindowsRunnerVersion, | ||
getFileContent, | ||
currentLinuxImages, | ||
} from './utils/arc-image'; | ||
import { rollInfra } from './utils/roll-infra'; | ||
|
||
export async function rollActionsRunner() { | ||
const d = debug(`roller/infra:rollActionsRunner()`); | ||
|
||
const octokit = await getOctokit(); | ||
const { archDigests, latestVersion } = (await getLatestRunnerImages(octokit)) ?? { | ||
archDigests: {}, | ||
latestVersion: '', | ||
}; | ||
if (latestVersion === '') { | ||
d('No latest version found for github actions runner, exiting...'); | ||
return; | ||
} | ||
|
||
const windowsDockerFile = await getFileContent(octokit, WINDOWS_DOCKER_FILE); | ||
const currentWindowsRunnerVersion = await getCurrentWindowsRunnerVersion(windowsDockerFile.raw); | ||
if (currentWindowsRunnerVersion !== latestVersion) { | ||
d(`Runner version ${currentWindowsRunnerVersion} is outdated, updating to ${latestVersion}.`); | ||
const newDockerFile = windowsDockerFile.raw.replace(currentWindowsRunnerVersion, latestVersion); | ||
await rollInfra( | ||
`prod/actions-runner`, | ||
'github actions runner images', | ||
latestVersion, | ||
WINDOWS_DOCKER_FILE, | ||
newDockerFile, | ||
); | ||
} | ||
|
||
for (const arcEnv of Object.keys(ARC_RUNNER_ENVIRONMENTS)) { | ||
d(`Fetching current version of "${arcEnv}" arc image in: ${ARC_RUNNER_ENVIRONMENTS[arcEnv]}`); | ||
|
||
const runnerFile = await getFileContent(octokit, ARC_RUNNER_ENVIRONMENTS['prod']); | ||
|
||
const currentImages = currentLinuxImages(runnerFile.raw); | ||
if (currentImages.amd64 !== archDigests.amd64 || currentImages.arm64 !== archDigests.arm64) { | ||
d(`Current linux images in "${arcEnv}" are outdated, updating to ${latestVersion}.`); | ||
let newContent = runnerFile.raw.replace(currentImages.amd64, archDigests.amd64); | ||
newContent = newContent.replace(currentImages.arm64, archDigests.arm64); | ||
await rollInfra( | ||
`${arcEnv}/actions-runner`, | ||
'github actions runner images', | ||
latestVersion, | ||
ARC_RUNNER_ENVIRONMENTS[arcEnv], | ||
newContent, | ||
); | ||
} else { | ||
d(`Current linux images in "${arcEnv}" are up-to-date, skipping...`); | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// Fetches the latest linux/amd64 and linux/arm64 images for actions/runner from GitHub Container Registry | ||
|
||
import { Octokit } from '@octokit/rest'; | ||
|
||
const OWNER = 'actions'; | ||
const PACKAGE = 'actions-runner'; | ||
|
||
type PackageVersion = { | ||
metadata?: { | ||
container?: { | ||
tags?: string[]; | ||
}; | ||
}; | ||
}; | ||
|
||
type ManifestPlatform = { | ||
os?: string; | ||
architecture?: string; | ||
}; | ||
|
||
type Manifest = { | ||
platform?: ManifestPlatform; | ||
digest: string; | ||
}; | ||
|
||
type ManifestList = { | ||
manifests?: Manifest[]; | ||
}; | ||
|
||
export async function getLatestRunnerImages( | ||
octokit: Octokit, | ||
): Promise<{ archDigests: Record<string, string>; latestVersion: string } | null> { | ||
let versions: PackageVersion[]; | ||
try { | ||
const response = await octokit.rest.packages.getAllPackageVersionsForPackageOwnedByOrg({ | ||
package_type: 'container', | ||
package_name: PACKAGE, | ||
org: OWNER, | ||
per_page: 10, | ||
}); | ||
versions = response.data as PackageVersion[]; | ||
} catch (e) { | ||
console.error('Failed to fetch package versions:', e); | ||
return null; | ||
} | ||
|
||
// Find the version with the 'latest' tag | ||
const latestVersion = versions.find((v) => v.metadata?.container?.tags?.includes('latest')); | ||
const tags = latestVersion?.metadata?.container?.tags || []; | ||
// Find the first tag that matches a semver version (e.g., 2.315.0) | ||
const tagVersion = tags.find((t) => /^\d+\.\d+\.\d+$/.test(t)); | ||
|
||
if (!latestVersion || !tagVersion) { | ||
console.error("No version with the 'latest' tag found; tags were:", tags); | ||
return null; | ||
} | ||
|
||
// Fetch the manifest list for the latest tag | ||
const manifestUrl = `https://ghcr.io/v2/${OWNER}/${PACKAGE}/manifests/${tagVersion}`; | ||
const manifestHeaders = { | ||
'User-Agent': 'node.js', | ||
Accept: 'application/vnd.oci.image.index.v1+json', | ||
Authorization: 'Bearer QQ==', | ||
}; | ||
let manifestList: ManifestList; | ||
try { | ||
const manifestResponse = await fetch(manifestUrl, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should check |
||
headers: manifestHeaders, | ||
}); | ||
manifestList = await manifestResponse.json(); | ||
} catch (e) { | ||
console.error('Failed to fetch manifest list:', e); | ||
return null; | ||
} | ||
|
||
// Find digests for linux/amd64 and linux/arm64 | ||
const archDigests: Record<string, string> = {}; | ||
for (const manifest of manifestList.manifests || []) { | ||
const platform = manifest.platform; | ||
if ( | ||
platform?.os === 'linux' && | ||
(platform.architecture === 'amd64' || platform.architecture === 'arm64') | ||
) { | ||
archDigests[platform.architecture] = manifest.digest; | ||
} | ||
} | ||
|
||
if (!archDigests.amd64 && !archDigests.arm64) { | ||
console.error('No linux/amd64 or linux/arm64 digests found in manifest list.'); | ||
return null; | ||
} | ||
return { | ||
archDigests, | ||
latestVersion: tagVersion, | ||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
import * as arcImage from '../src/utils/arc-image'; | ||
import * as getLatestRunnerImagesModule from '../src/utils/get-latest-runner-images'; | ||
import * as rollInfraModule from '../src/utils/roll-infra'; | ||
import * as constants from '../src/constants'; | ||
import { getOctokit } from '../src/utils/octokit'; | ||
|
||
import { rollActionsRunner } from '../src/actions-runner-handler'; | ||
|
||
vi.mock('debug', () => ({ default: vi.fn(() => vi.fn()) })); | ||
vi.mock('../src/utils/get-latest-runner-images'); | ||
vi.mock('../src/utils/arc-image'); | ||
vi.mock('../src/utils/roll-infra'); | ||
vi.mock('../src/utils/octokit'); | ||
|
||
const mockOctokit = {}; | ||
|
||
const mockFileContent = (raw: string) => ({ raw, sha: 'sha' }); | ||
|
||
const latestVersion = '2.325.0'; | ||
const archDigests = { | ||
amd64: 'sha256:amd64digest', | ||
arm64: 'sha256:arm64digest', | ||
}; | ||
|
||
const newWinDockerFile = 'ARG RUNNER_VERSION=2.325.0'; | ||
const oldWinDockerFile = 'ARG RUNNER_VERSION=2.324.0'; | ||
|
||
const oldLinuxImages = { | ||
amd64: 'ghcr.io/actions/actions-runner:2.324.0@sha256:oldamd64', | ||
arm64: 'ghcr.io/actions/actions-runner:2.324.0@sha256:oldarm64', | ||
}; | ||
const newLinuxImages = { | ||
amd64: archDigests.amd64, | ||
arm64: archDigests.arm64, | ||
}; | ||
|
||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
(getOctokit as any).mockResolvedValue(mockOctokit); | ||
vi.mocked(getLatestRunnerImagesModule.getLatestRunnerImages).mockResolvedValue({ | ||
archDigests, | ||
latestVersion, | ||
}); | ||
vi.mocked(arcImage.getFileContent).mockImplementation(async (_octokit: any, file: string) => { | ||
if (file === constants.WINDOWS_DOCKER_FILE) return mockFileContent(oldWinDockerFile); | ||
if (file === constants.ARC_RUNNER_ENVIRONMENTS.prod) | ||
return mockFileContent(`amd64: ${oldLinuxImages.amd64}\narm64: ${oldLinuxImages.arm64}`); | ||
return mockFileContent(''); | ||
}); | ||
vi.mocked(arcImage.getCurrentWindowsRunnerVersion).mockImplementation( | ||
async (raw: string) => raw.match(/([\d.]+)/)?.[1] || '', | ||
); | ||
vi.mocked(arcImage.currentLinuxImages).mockImplementation((raw: string) => { | ||
if (raw.includes('oldamd64')) return oldLinuxImages; | ||
return newLinuxImages; | ||
}); | ||
vi.mocked(rollInfraModule.rollInfra).mockResolvedValue(undefined); | ||
}); | ||
|
||
describe('rollActionsRunner', () => { | ||
it('should update windows runner if version is outdated', async () => { | ||
await rollActionsRunner(); | ||
expect(rollInfraModule.rollInfra).toHaveBeenCalledWith( | ||
'prod/actions-runner', | ||
'github actions runner images', | ||
latestVersion, | ||
constants.WINDOWS_DOCKER_FILE, | ||
expect.stringContaining(latestVersion), | ||
); | ||
}); | ||
|
||
it('should update linux images if digests are outdated', async () => { | ||
await rollActionsRunner(); | ||
expect(rollInfraModule.rollInfra).toHaveBeenCalledWith( | ||
'prod/actions-runner', | ||
'github actions runner images', | ||
latestVersion, | ||
constants.ARC_RUNNER_ENVIRONMENTS.prod, | ||
expect.any(String), | ||
); | ||
}); | ||
|
||
it('should skip update if everything is up-to-date', async () => { | ||
vi.mocked(arcImage.getFileContent).mockImplementation(async (_octokit: any, file: string) => { | ||
if (file === constants.WINDOWS_DOCKER_FILE) return mockFileContent(newWinDockerFile); | ||
if (file === constants.ARC_RUNNER_ENVIRONMENTS.prod) | ||
return mockFileContent(`amd64: ${archDigests.amd64}\narm64: ${archDigests.arm64}`); | ||
return mockFileContent(''); | ||
}); | ||
vi.mocked(arcImage.getCurrentWindowsRunnerVersion).mockImplementation( | ||
async (raw: string) => latestVersion, | ||
); | ||
vi.mocked(arcImage.currentLinuxImages).mockImplementation((raw: string) => newLinuxImages); | ||
await rollActionsRunner(); | ||
expect(rollInfraModule.rollInfra).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { describe, it, expect } from 'vitest'; | ||
import { | ||
currentWindowsImage, | ||
currentLinuxImages, | ||
getCurrentWindowsRunnerVersion, | ||
} from '../../src/utils/arc-image'; | ||
|
||
const windowsImageContent = `electronarc.azurecr.io/win-actions-runner:main-abcdef0@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef`; | ||
|
||
const linuxImageContent = `containers: | ||
- name: runner | ||
resources: | ||
requests: | ||
cpu: "{{ .cpuRequest }}" | ||
memory: "{{ .memoryRequest }}" | ||
{{- if eq .cpuPlatform "linux" }} | ||
{{- if eq .cpuArch "amd64" }} | ||
image: ghcr.io/actions/actions-runner:2.325.0@sha256:b865e3f046f0a92a4b936ae75c5bc5615b99b64eb4801b0e5220f13f8867d6b8 | ||
{{- else }} | ||
image: ghcr.io/actions/actions-runner:2.325.0@sha256:ab3fb968f7bcc8b34677b93a98f576142a2affde57ea2e7b461f515fd8a12453 | ||
{{- end }}`; | ||
|
||
const runnerVersionContent = `FROM something-else:latest | ||
LABEL name=arc-runner-windows | ||
|
||
ARG RUNNER_VERSION=2.325.0 | ||
ENV RUNNER_VERSION=$RUNNER_VERSION | ||
`; | ||
|
||
const invalidLinuxContent = `{{- if eq .cpuArch "amd64" }}\nimage: something-else\n{{- else }}\nimage: something-else\n`; | ||
|
||
describe('arc-image utils', () => { | ||
it('should extract the current Windows image', () => { | ||
expect(currentWindowsImage(windowsImageContent)).toBe( | ||
'electronarc.azurecr.io/win-actions-runner:main-abcdef0@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', | ||
); | ||
}); | ||
|
||
it('should extract both linux images', () => { | ||
const images = currentLinuxImages(linuxImageContent); | ||
expect(images.amd64).toBe( | ||
'ghcr.io/actions/actions-runner:2.325.0@sha256:b865e3f046f0a92a4b936ae75c5bc5615b99b64eb4801b0e5220f13f8867d6b8', | ||
); | ||
expect(images.arm64).toBe( | ||
'ghcr.io/actions/actions-runner:2.325.0@sha256:ab3fb968f7bcc8b34677b93a98f576142a2affde57ea2e7b461f515fd8a12453', | ||
); | ||
}); | ||
|
||
it('should return empty strings for missing linux images', () => { | ||
const images = currentLinuxImages(invalidLinuxContent); | ||
expect(images.amd64).toBe(''); | ||
expect(images.arm64).toBe(''); | ||
}); | ||
|
||
it('should extract the current Windows runner version', async () => { | ||
expect(await getCurrentWindowsRunnerVersion(runnerVersionContent)).toBe('2.325.0'); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.