From f4b6bd982f8ec0e0281166ef1ee9185341bca5d8 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 6 Mar 2026 19:55:54 +0000 Subject: [PATCH 1/5] Parse/render YAML documents for `sync_back.ts` --- pr-checks/sync_back.test.ts | 56 ++++++++----- pr-checks/sync_back.ts | 154 ++++++++++++++++++++++++------------ 2 files changed, 143 insertions(+), 67 deletions(-) diff --git a/pr-checks/sync_back.test.ts b/pr-checks/sync_back.test.ts index 316d2b7303..3688c2a6af 100755 --- a/pr-checks/sync_back.test.ts +++ b/pr-checks/sync_back.test.ts @@ -56,9 +56,18 @@ jobs: const result = scanGeneratedWorkflows(workflowDir); - assert.equal(result["actions/checkout"], "v4"); - assert.equal(result["actions/setup-node"], "v5"); - assert.equal(result["actions/setup-go"], "v6"); + assert.deepEqual(result["actions/checkout"], { + version: "v4", + comment: undefined, + }); + assert.deepEqual(result["actions/setup-node"], { + version: "v5", + comment: undefined, + }); + assert.deepEqual(result["actions/setup-go"], { + version: "v6", + comment: undefined, + }); }); it("scanning workflows with version comments", () => { @@ -78,12 +87,18 @@ jobs: const result = scanGeneratedWorkflows(workflowDir); - assert.equal(result["actions/checkout"], "v4"); - assert.equal( - result["ruby/setup-ruby"], - "44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0", - ); - assert.equal(result["actions/setup-python"], "v6 # Latest Python"); + assert.deepEqual(result["actions/checkout"], { + version: "v4", + comment: undefined, + }); + assert.deepEqual(result["ruby/setup-ruby"], { + version: "44511735964dcb71245e7e55f72539531f7bc0eb", + comment: " v1.257.0", + }); + assert.deepEqual(result["actions/setup-python"], { + version: "v6", + comment: " Latest Python", + }); }); it("ignores local actions", () => { @@ -103,7 +118,10 @@ jobs: const result = scanGeneratedWorkflows(workflowDir); - assert.equal(result["actions/checkout"], "v4"); + assert.deepEqual(result["actions/checkout"], { + version: "v4", + comment: undefined, + }); assert.equal("./.github/actions/local-action" in result, false); assert.equal("./another-local-action" in result, false); }); @@ -128,8 +146,8 @@ const steps = [ fs.writeFileSync(syncTsPath, syncTsContent); const actionVersions = { - "actions/setup-node": "v5", - "actions/setup-go": "v6", + "actions/setup-node": { version: "v5" }, + "actions/setup-go": { version: "v6" }, }; const result = updateSyncTs(syncTsPath, actionVersions); @@ -155,7 +173,7 @@ const steps = [ fs.writeFileSync(syncTsPath, syncTsContent); const actionVersions = { - "actions/setup-node": "v5 # Latest version", + "actions/setup-node": { version: "v5", comment: " Latest version" }, }; const result = updateSyncTs(syncTsPath, actionVersions); @@ -182,7 +200,7 @@ const steps = [ fs.writeFileSync(syncTsPath, syncTsContent); const actionVersions = { - "actions/setup-node": "v5", + "actions/setup-node": { version: "v5" }, }; const result = updateSyncTs(syncTsPath, actionVersions); @@ -206,8 +224,8 @@ steps: fs.writeFileSync(templatePath, templateContent); const actionVersions = { - "actions/checkout": "v4", - "actions/setup-node": "v5 # Latest", + "actions/checkout": { version: "v4" }, + "actions/setup-node": { version: "v5", comment: " Latest" }, }; const result = updateTemplateFiles(checksDir, actionVersions); @@ -232,8 +250,10 @@ steps: fs.writeFileSync(templatePath, templateContent); const actionVersions = { - "ruby/setup-ruby": - "55511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0", + "ruby/setup-ruby": { + version: "55511735964dcb71245e7e55f72539531f7bc0eb", + comment: " v1.257.0", + }, }; const result = updateTemplateFiles(checksDir, actionVersions); diff --git a/pr-checks/sync_back.ts b/pr-checks/sync_back.ts index 7e1375580b..67ab04afb7 100755 --- a/pr-checks/sync_back.ts +++ b/pr-checks/sync_back.ts @@ -1,5 +1,7 @@ #!/usr/bin/env npx tsx +import * as yaml from "yaml"; + /* Sync-back script to automatically update action versions in source templates from the generated workflow files after Dependabot updates. @@ -27,14 +29,74 @@ const CHECKS_DIR = path.join(THIS_DIR, "checks"); const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows"); const SYNC_TS_PATH = path.join(THIS_DIR, "sync.ts"); +/** Records information about the version of an Action with an optional comment. */ +type ActionVersion = { version: string; comment?: string }; + +/** Converts `info` to a string that includes the version and comment. */ +function versionWithCommentStr(info: ActionVersion): string { + const comment = info.comment ? ` #${info.comment}` : ""; + return `${info.version}${comment}`; +} + +/** + * Constructs a `yaml.visitor` which calls `fn` for `yaml.Pair` nodes where the key is "uses" and + * the value is a `yaml.Scalar`. + */ +function usesVisitor( + fn: ( + pair: yaml.Pair, + actionName: string, + actionVersion: ActionVersion, + ) => void, +): yaml.visitor { + return { + Pair(_, pair) { + if ( + yaml.isScalar(pair.key) && + yaml.isScalar(pair.value) && + pair.key.value === "uses" && + typeof pair.value.value === "string" + ) { + const usesValue = pair.value.value; + + // Only track non-local actions (those with / but not starting with ./) + if (!usesValue.startsWith("./")) { + const parts = (pair.value.value as string).split("@"); + + if (parts.length !== 2) { + throw new Error(`Unexpected 'uses' value: ${usesValue}`); + } + + const actionName = parts[0]; + const actionVersion = parts[1].trimEnd(); + const comment = pair.value.comment?.trimEnd(); + + fn(pair as yaml.Pair, actionName, { + version: actionVersion, + comment, + }); + } + + // Do not visit the children of this node. + return yaml.visit.SKIP; + } + + // Do nothing and continue. + return undefined; + }, + }; +} + /** * Scan generated workflow files to extract the latest action versions. * * @param workflowDir - Path to .github/workflows directory * @returns Map from action names to their latest versions (including comments) */ -export function scanGeneratedWorkflows(workflowDir: string): Record { - const actionVersions: Record = {}; +export function scanGeneratedWorkflows( + workflowDir: string, +): Record { + const actionVersions: Record = {}; const generatedFiles = fs .readdirSync(workflowDir) @@ -43,22 +105,15 @@ export function scanGeneratedWorkflows(workflowDir: string): Record { // Assume that version numbers are consistent (this should be the case on a Dependabot update PR) - actionVersions[actionName] = versionWithComment; - } - } + actionVersions[actionName] = actionVersion; + }), + ); } return actionVersions; @@ -73,7 +128,7 @@ export function scanGeneratedWorkflows(workflowDir: string): Record, + actionVersions: Record, ): boolean { if (!fs.existsSync(syncTsPath)) { throw new Error(`Could not find ${syncTsPath}`); @@ -83,24 +138,16 @@ export function updateSyncTs( const originalContent = content; // Update hardcoded action versions - for (const [actionName, versionWithComment] of Object.entries( - actionVersions, - )) { - // Extract just the version part (before any comment) for sync.ts - const version = versionWithComment.includes("#") - ? versionWithComment.split("#")[0].trim() - : versionWithComment.trim(); - - // Look for patterns like uses: "actions/setup-node@v4" + for (const [actionName, versionInfo] of Object.entries(actionVersions)) { // Note that this will break if we store an Action uses reference in a // variable - that's a risk we're happy to take since in that case the // PR checks will just fail. const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp( - `(uses:\\s*")${escaped}@(?:[^"]+)(")`, - "g", + const pattern = new RegExp(`(uses:\\s*")${escaped}@(?:[^"]+)(")`, "g"); + content = content.replace( + pattern, + `$1${actionName}@${versionInfo.version}$2`, ); - content = content.replace(pattern, `$1${actionName}@${version}$2`); } if (content !== originalContent) { @@ -122,7 +169,7 @@ export function updateSyncTs( */ export function updateTemplateFiles( checksDir: string, - actionVersions: Record, + actionVersions: Record, ): string[] { const modifiedFiles: string[] = []; @@ -132,24 +179,33 @@ export function updateTemplateFiles( .map((f) => path.join(checksDir, f)); for (const filePath of templateFiles) { - let content = fs.readFileSync(filePath, "utf8"); - const originalContent = content; - - // Update action versions - for (const [actionName, versionWithComment] of Object.entries( - actionVersions, - )) { - // Look for patterns like 'uses: actions/setup-node@v4' or 'uses: actions/setup-node@sha # comment' - const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp( - `(uses:\\s+${escaped})@(?:[^@\n]+)`, - "g", - ); - content = content.replace(pattern, `$1@${versionWithComment}`); - } + const content = fs.readFileSync(filePath, "utf8"); + const doc = yaml.parseDocument(content, { keepSourceTokens: true }); + let modified: boolean = false; + + yaml.visit( + doc, + usesVisitor((pair, actionName, actionVersion) => { + // Try to look up version information for this action. + const versionInfo = actionVersions[actionName]; + + // If we found version information, and the version is different from that in the template, + // then update the pair node accordingly. + if (versionInfo && versionInfo.version !== actionVersion.version) { + pair.value.value = `${actionName}@${versionInfo.version}`; + pair.value.comment = versionInfo.comment; + modified = true; + } + }), + ); - if (content !== originalContent) { - fs.writeFileSync(filePath, content, "utf8"); + // Write the YAML document back to the file if we made changes. + if (modified) { + fs.writeFileSync( + filePath, + yaml.stringify(doc, { lineWidth: 0, flowCollectionPadding: false }), + "utf8", + ); modifiedFiles.push(filePath); console.info(`Updated ${filePath}`); } @@ -178,7 +234,7 @@ function main(): number { if (verbose) { console.info("Found action versions:"); for (const [action, version] of Object.entries(actionVersions)) { - console.info(` ${action}@${version}`); + console.info(` ${action}@${versionWithCommentStr(version)}`); } } From 5f152be4c2a5e2ab6d517f4bbb439cccbfe5756c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 6 Mar 2026 23:40:49 +0000 Subject: [PATCH 2/5] Minor adjustments to PR check templates These are needed to render them correctly in `sync_back.ts` --- ...utobuild-direct-tracing-with-working-dir.yml | 2 +- pr-checks/checks/init-with-registries.yml | 17 ++++++++--------- pr-checks/checks/overlay-init-fallback.yml | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pr-checks/checks/autobuild-direct-tracing-with-working-dir.yml b/pr-checks/checks/autobuild-direct-tracing-with-working-dir.yml index f492ac85a4..44b7b94332 100644 --- a/pr-checks/checks/autobuild-direct-tracing-with-working-dir.yml +++ b/pr-checks/checks/autobuild-direct-tracing-with-working-dir.yml @@ -1,5 +1,5 @@ name: "Autobuild direct tracing (custom working directory)" -description: > +description: | An end-to-end integration test of a Java repository built using 'build-mode: autobuild', with direct tracing enabled and a custom working directory specified as the input to the autobuild Action. diff --git a/pr-checks/checks/init-with-registries.yml b/pr-checks/checks/init-with-registries.yml index cedc62aab0..59559769ba 100644 --- a/pr-checks/checks/init-with-registries.yml +++ b/pr-checks/checks/init-with-registries.yml @@ -4,12 +4,11 @@ # basic mechanics of multi-registry auth is working. name: "Packaging: Download using registries" description: "Checks that specifying a registries block and associated auth works as expected" -versions: [ - # This feature is not compatible with older CLIs - "default", - "linked", - "nightly-latest", -] +versions: + # This feature is not compatible with older CLIs + - "default" + - "linked" + - "nightly-latest" permissions: contents: read @@ -24,9 +23,9 @@ steps: config-file: ./.github/codeql/codeql-config-registries.yml languages: javascript registries: | - - url: "https://ghcr.io/v2/" - packages: "*/*" - token: "${{ secrets.GITHUB_TOKEN }}" + - url: "https://ghcr.io/v2/" + packages: "*/*" + token: "${{ secrets.GITHUB_TOKEN }}" - name: Verify packages installed run: | diff --git a/pr-checks/checks/overlay-init-fallback.yml b/pr-checks/checks/overlay-init-fallback.yml index bfcfd27e79..88f103566c 100644 --- a/pr-checks/checks/overlay-init-fallback.yml +++ b/pr-checks/checks/overlay-init-fallback.yml @@ -4,7 +4,7 @@ versions: ["linked", "nightly-latest"] steps: - uses: ./../action/init with: - languages: actions # Any language without overlay support will do + languages: actions # Any language without overlay support will do tools: ${{ steps.prepare-test.outputs.tools-url }} env: CODEQL_OVERLAY_DATABASE_MODE: overlay-base From 13e47f85a54a1a02f0ff5dac83a1acfdbf2e260f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 6 Mar 2026 23:42:08 +0000 Subject: [PATCH 3/5] Type options and add `--force` option --- pr-checks/sync_back.test.ts | 21 ++++++++++++++++----- pr-checks/sync_back.ts | 27 +++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/pr-checks/sync_back.test.ts b/pr-checks/sync_back.test.ts index 3688c2a6af..307820e068 100755 --- a/pr-checks/sync_back.test.ts +++ b/pr-checks/sync_back.test.ts @@ -11,6 +11,7 @@ import * as path from "node:path"; import { afterEach, beforeEach, describe, it } from "node:test"; import { + Options, scanGeneratedWorkflows, updateSyncTs, updateTemplateFiles, @@ -21,6 +22,8 @@ let workflowDir: string; let checksDir: string; let syncTsPath: string; +const defaultOptions: Options = { verbose: false, force: false }; + beforeEach(() => { /** Set up temporary directories and files for testing */ testDir = fs.mkdtempSync(path.join(os.tmpdir(), "sync-back-test-")); @@ -150,7 +153,7 @@ const steps = [ "actions/setup-go": { version: "v6" }, }; - const result = updateSyncTs(syncTsPath, actionVersions); + const result = updateSyncTs(defaultOptions, syncTsPath, actionVersions); assert.equal(result, true); const updatedContent = fs.readFileSync(syncTsPath, "utf8"); @@ -176,7 +179,7 @@ const steps = [ "actions/setup-node": { version: "v5", comment: " Latest version" }, }; - const result = updateSyncTs(syncTsPath, actionVersions); + const result = updateSyncTs(defaultOptions, syncTsPath, actionVersions); assert.equal(result, true); const updatedContent = fs.readFileSync(syncTsPath, "utf8"); @@ -203,7 +206,7 @@ const steps = [ "actions/setup-node": { version: "v5" }, }; - const result = updateSyncTs(syncTsPath, actionVersions); + const result = updateSyncTs(defaultOptions, syncTsPath, actionVersions); assert.equal(result, false); }); }); @@ -228,7 +231,11 @@ steps: "actions/setup-node": { version: "v5", comment: " Latest" }, }; - const result = updateTemplateFiles(checksDir, actionVersions); + const result = updateTemplateFiles( + defaultOptions, + checksDir, + actionVersions, + ); assert.equal(result.length, 1); assert.ok(result.includes(templatePath)); @@ -256,7 +263,11 @@ steps: }, }; - const result = updateTemplateFiles(checksDir, actionVersions); + const result = updateTemplateFiles( + defaultOptions, + checksDir, + actionVersions, + ); assert.equal(result.length, 1); const updatedContent = fs.readFileSync(templatePath, "utf8"); diff --git a/pr-checks/sync_back.ts b/pr-checks/sync_back.ts index 67ab04afb7..8ce41fdf64 100755 --- a/pr-checks/sync_back.ts +++ b/pr-checks/sync_back.ts @@ -29,6 +29,12 @@ const CHECKS_DIR = path.join(THIS_DIR, "checks"); const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows"); const SYNC_TS_PATH = path.join(THIS_DIR, "sync.ts"); +/** Command-line options for this program. */ +export type Options = { + verbose: boolean; + force: boolean; +}; + /** Records information about the version of an Action with an optional comment. */ type ActionVersion = { version: string; comment?: string }; @@ -122,11 +128,13 @@ export function scanGeneratedWorkflows( /** * Update hardcoded action versions in pr-checks/sync.ts * + * @param options - The command-line options. * @param syncTsPath - Path to sync.ts file * @param actionVersions - Map of action names to versions (may include comments) * @returns True if the file was modified, false otherwise */ export function updateSyncTs( + options: Options, syncTsPath: string, actionVersions: Record, ): boolean { @@ -150,7 +158,7 @@ export function updateSyncTs( ); } - if (content !== originalContent) { + if (content !== originalContent || options.force) { fs.writeFileSync(syncTsPath, content, "utf8"); console.info(`Updated ${syncTsPath}`); return true; @@ -163,11 +171,13 @@ export function updateSyncTs( /** * Update action versions in template files in pr-checks/checks/ * + * @param options - The command-line options. * @param checksDir - Path to pr-checks/checks directory * @param actionVersions - Map of action names to versions (may include comments) * @returns List of files that were modified */ export function updateTemplateFiles( + options: Options, checksDir: string, actionVersions: Record, ): string[] { @@ -200,7 +210,7 @@ export function updateTemplateFiles( ); // Write the YAML document back to the file if we made changes. - if (modified) { + if (modified || options.force) { fs.writeFileSync( filePath, yaml.stringify(doc, { lineWidth: 0, flowCollectionPadding: false }), @@ -222,6 +232,11 @@ function main(): number { short: "v", default: false, }, + force: { + type: "boolean", + short: "f", + default: false, + }, }, strict: true, }); @@ -248,12 +263,16 @@ function main(): number { const modifiedFiles: string[] = []; // Update sync.ts - if (updateSyncTs(SYNC_TS_PATH, actionVersions)) { + if (updateSyncTs(values, SYNC_TS_PATH, actionVersions)) { modifiedFiles.push(SYNC_TS_PATH); } // Update template files - const templateModified = updateTemplateFiles(CHECKS_DIR, actionVersions); + const templateModified = updateTemplateFiles( + values, + CHECKS_DIR, + actionVersions, + ); modifiedFiles.push(...templateModified); if (modifiedFiles.length > 0) { From 6679b049d5c522c94269d43e699ce5058b28698c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 7 Mar 2026 00:13:25 +0000 Subject: [PATCH 4/5] Generate separate TS file with version mappings --- pr-checks/sync_back.test.ts | 106 +++++++++++++++--------------------- pr-checks/sync_back.ts | 50 +++++++---------- 2 files changed, 64 insertions(+), 92 deletions(-) diff --git a/pr-checks/sync_back.test.ts b/pr-checks/sync_back.test.ts index 307820e068..364f421a65 100755 --- a/pr-checks/sync_back.test.ts +++ b/pr-checks/sync_back.test.ts @@ -13,14 +13,14 @@ import { afterEach, beforeEach, describe, it } from "node:test"; import { Options, scanGeneratedWorkflows, - updateSyncTs, + updateActionVersions, updateTemplateFiles, } from "./sync_back"; let testDir: string; let workflowDir: string; let checksDir: string; -let syncTsPath: string; +let actionVersionsTsPath: string; const defaultOptions: Options = { verbose: false, force: false }; @@ -32,8 +32,8 @@ beforeEach(() => { fs.mkdirSync(workflowDir, { recursive: true }); fs.mkdirSync(checksDir, { recursive: true }); - // Create sync.ts file path - syncTsPath = path.join(testDir, "pr-checks", "sync.ts"); + // Create action-versions.ts file path + actionVersionsTsPath = path.join(testDir, "pr-checks", "action-versions.ts"); }); afterEach(() => { @@ -130,83 +130,67 @@ jobs: }); }); -describe("updateSyncTs", () => { - it("updates sync.ts file", () => { - /** Test updating sync.ts file */ - const syncTsContent = ` -const steps = [ - { - uses: "actions/setup-node@v4", - with: { "node-version": "16" }, +describe("updateActionVersions", () => { + it("updates action-versions.ts file", () => { + /** Test updating action-versions.ts file */ + const actionVersionsTsContent = ` +export const ACTION_VERSIONS = { + "actions/setup-node": { + "version": "v4" }, - { - uses: "actions/setup-go@v5", - with: { "go-version": "1.19" }, - }, -]; -`; + "actions/setup-go": { + "version": "v5" + } +}; +`.trim(); - fs.writeFileSync(syncTsPath, syncTsContent); + fs.writeFileSync(actionVersionsTsPath, actionVersionsTsContent); const actionVersions = { "actions/setup-node": { version: "v5" }, "actions/setup-go": { version: "v6" }, }; - const result = updateSyncTs(defaultOptions, syncTsPath, actionVersions); - assert.equal(result, true); - - const updatedContent = fs.readFileSync(syncTsPath, "utf8"); - - assert.ok(updatedContent.includes('uses: "actions/setup-node@v5"')); - assert.ok(updatedContent.includes('uses: "actions/setup-go@v6"')); - }); - - it("strips comments from versions", () => { - /** Test updating sync.ts file when versions have comments */ - const syncTsContent = ` -const steps = [ - { - uses: "actions/setup-node@v4", - with: { "node-version": "16" }, - }, -]; -`; - - fs.writeFileSync(syncTsPath, syncTsContent); - - const actionVersions = { - "actions/setup-node": { version: "v5", comment: " Latest version" }, - }; - - const result = updateSyncTs(defaultOptions, syncTsPath, actionVersions); + const result = updateActionVersions( + defaultOptions, + actionVersionsTsPath, + actionVersions, + ); assert.equal(result, true); - const updatedContent = fs.readFileSync(syncTsPath, "utf8"); + const updatedContent = fs.readFileSync(actionVersionsTsPath, "utf8"); - // sync.ts should get the version without comment - assert.ok(updatedContent.includes('uses: "actions/setup-node@v5"')); - assert.ok(!updatedContent.includes("# Latest version")); + assert.ok( + updatedContent.includes('"actions/setup-node": {\n "version": "v5"'), + ); + assert.ok( + updatedContent.includes('"actions/setup-go": {\n "version": "v6"'), + ); }); it("returns false when no changes are needed", () => { - /** Test that updateSyncTs returns false when no changes are needed */ - const syncTsContent = ` -const steps = [ - { - uses: "actions/setup-node@v5", - with: { "node-version": "16" }, - }, -]; -`; + /** Test that updateActionVersions returns false when no changes are needed */ + const actionVersionsTsContent = ` +export const ACTION_VERSIONS = { + "actions/setup-node": { + "version": "v5" + } +}; +`.trim(); - fs.writeFileSync(syncTsPath, syncTsContent); + fs.writeFileSync(actionVersionsTsPath, actionVersionsTsContent); const actionVersions = { "actions/setup-node": { version: "v5" }, }; - const result = updateSyncTs(defaultOptions, syncTsPath, actionVersions); + const result = updateActionVersions( + defaultOptions, + actionVersionsTsPath, + actionVersions, + ); + const updatedContent = fs.readFileSync(actionVersionsTsPath, "utf8"); + assert.equal(updatedContent, actionVersionsTsContent); assert.equal(result, false); }); }); diff --git a/pr-checks/sync_back.ts b/pr-checks/sync_back.ts index 8ce41fdf64..407df1ed0f 100755 --- a/pr-checks/sync_back.ts +++ b/pr-checks/sync_back.ts @@ -27,7 +27,7 @@ import * as path from "path"; const THIS_DIR = __dirname; const CHECKS_DIR = path.join(THIS_DIR, "checks"); const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows"); -const SYNC_TS_PATH = path.join(THIS_DIR, "sync.ts"); +const ACTION_VERSIONS_PATH = path.join(THIS_DIR, "action-versions.ts"); /** Command-line options for this program. */ export type Options = { @@ -126,46 +126,34 @@ export function scanGeneratedWorkflows( } /** - * Update hardcoded action versions in pr-checks/sync.ts + * Update hardcoded action versions in pr-checks/action-versions.ts * * @param options - The command-line options. - * @param syncTsPath - Path to sync.ts file + * @param actionVersionsTsPath - Path to action-versions.ts file * @param actionVersions - Map of action names to versions (may include comments) * @returns True if the file was modified, false otherwise */ -export function updateSyncTs( +export function updateActionVersions( options: Options, - syncTsPath: string, + actionVersionsTsPath: string, actionVersions: Record, ): boolean { - if (!fs.existsSync(syncTsPath)) { - throw new Error(`Could not find ${syncTsPath}`); - } + // Build content for the file. + let newContent: string = `export const ACTION_VERSIONS = ${JSON.stringify(actionVersions, null, 2)};`; - let content = fs.readFileSync(syncTsPath, "utf8"); - const originalContent = content; + if (fs.existsSync(actionVersionsTsPath)) { + const content = fs.readFileSync(actionVersionsTsPath, "utf8"); - // Update hardcoded action versions - for (const [actionName, versionInfo] of Object.entries(actionVersions)) { - // Note that this will break if we store an Action uses reference in a - // variable - that's a risk we're happy to take since in that case the - // PR checks will just fail. - const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`(uses:\\s*")${escaped}@(?:[^"]+)(")`, "g"); - content = content.replace( - pattern, - `$1${actionName}@${versionInfo.version}$2`, - ); + if (content === newContent && !options.force) { + console.info(`No changes needed in ${actionVersionsTsPath}`); + return false; + } } - if (content !== originalContent || options.force) { - fs.writeFileSync(syncTsPath, content, "utf8"); - console.info(`Updated ${syncTsPath}`); - return true; - } else { - console.info(`No changes needed in ${syncTsPath}`); - return false; - } + // Update hardcoded action versions + fs.writeFileSync(actionVersionsTsPath, newContent, "utf8"); + console.info(`Updated ${actionVersionsTsPath}`); + return true; } /** @@ -263,8 +251,8 @@ function main(): number { const modifiedFiles: string[] = []; // Update sync.ts - if (updateSyncTs(values, SYNC_TS_PATH, actionVersions)) { - modifiedFiles.push(SYNC_TS_PATH); + if (updateActionVersions(values, ACTION_VERSIONS_PATH, actionVersions)) { + modifiedFiles.push(ACTION_VERSIONS_PATH); } // Update template files From 0b27c7173152717dc81533bdf1f094a9e68440dc Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 7 Mar 2026 00:29:11 +0000 Subject: [PATCH 5/5] Generate and use `action-versions.ts` --- pr-checks/action-versions.ts | 30 ++++++++++++++++++++++++++++++ pr-checks/sync.ts | 23 +++++++++++++++++------ 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 pr-checks/action-versions.ts diff --git a/pr-checks/action-versions.ts b/pr-checks/action-versions.ts new file mode 100644 index 0000000000..aa8914a492 --- /dev/null +++ b/pr-checks/action-versions.ts @@ -0,0 +1,30 @@ +export const ACTION_VERSIONS = { + "actions/checkout": { + "version": "v6" + }, + "actions/setup-go": { + "version": "v6" + }, + "actions/setup-dotnet": { + "version": "v5" + }, + "actions/upload-artifact": { + "version": "v7" + }, + "actions/github-script": { + "version": "v8" + }, + "actions/setup-python": { + "version": "v6" + }, + "actions/setup-java": { + "version": "v5" + }, + "actions/setup-node": { + "version": "v6" + }, + "ruby/setup-ruby": { + "version": "09a7688d3b55cf0e976497ff046b70949eeaccfd", + "comment": " v1.288.0" + } +}; diff --git a/pr-checks/sync.ts b/pr-checks/sync.ts index ca2b069cb6..6ed36ac237 100755 --- a/pr-checks/sync.ts +++ b/pr-checks/sync.ts @@ -5,6 +5,8 @@ import * as path from "path"; import * as yaml from "yaml"; +import { ACTION_VERSIONS } from "./action-versions"; + /** Known workflow input names. */ enum KnownInputName { GoVersion = "go-version", @@ -13,6 +15,9 @@ enum KnownInputName { DotnetVersion = "dotnet-version", } +/** Known Action names that we have version information for. */ +type KnownAction = keyof typeof ACTION_VERSIONS; + /** * Represents workflow input definitions. */ @@ -94,6 +99,12 @@ const THIS_DIR = __dirname; const CHECKS_DIR = path.join(THIS_DIR, "checks"); const OUTPUT_DIR = path.join(THIS_DIR, "..", ".github", "workflows"); +/** Gets an `actionName@ref` string for `actionName`. */ +function getActionRef(actionName: KnownAction): string { + const versionInfo = ACTION_VERSIONS[actionName]; + return `${actionName}@${versionInfo.version}`; +} + /** * Loads and parses a YAML file. */ @@ -216,7 +227,7 @@ function main(): void { const steps: any[] = [ { name: "Check out repository", - uses: "actions/checkout@v6", + uses: getActionRef("actions/checkout"), }, ]; @@ -226,7 +237,7 @@ function main(): void { steps.push( { name: "Install Node.js", - uses: "actions/setup-node@v6", + uses: getActionRef("actions/setup-node"), with: { "node-version": "20.x", cache: "npm", @@ -265,7 +276,7 @@ function main(): void { steps.push({ name: "Install Go", - uses: "actions/setup-go@v6", + uses: getActionRef("actions/setup-go"), with: { "go-version": "${{ inputs.go-version || '" + baseGoVersionExpr + "' }}", @@ -289,7 +300,7 @@ function main(): void { steps.push({ name: "Install Java", - uses: "actions/setup-java@v5", + uses: getActionRef("actions/setup-java"), with: { "java-version": "${{ inputs.java-version || '" + baseJavaVersionExpr + "' }}", @@ -312,7 +323,7 @@ function main(): void { steps.push({ name: "Install Python", if: "matrix.version != 'nightly-latest'", - uses: "actions/setup-python@v6", + uses: getActionRef("actions/setup-python"), with: { "python-version": "${{ inputs.python-version || '" + basePythonVersionExpr + "' }}", @@ -333,7 +344,7 @@ function main(): void { steps.push({ name: "Install .NET", - uses: "actions/setup-dotnet@v5", + uses: getActionRef("actions/setup-dotnet"), with: { "dotnet-version": "${{ inputs.dotnet-version || '" + baseDotNetVersionExpr + "' }}",