@@ -27,13 +27,79 @@ const CHECKS_DIR = path.join(THIS_DIR, "checks");
2727const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows");
2828const SYNC_TS_PATH = path.join(THIS_DIR, "sync.ts");
2929
30+ /**
31+ * Used to find action references (including versions and comments) in a workflow file.
32+ *
33+ * This pattern captures `action_name` and `version_with_possible_comment` from
34+ * `uses: action_name@version_with_possible_comment`. For example, if we have
35+ *
36+ * ```
37+ * uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
38+ * ```
39+ *
40+ * in a workflow file, this regular expression gets us:
41+ *
42+ * - `ruby/setup-ruby`; and
43+ * - `09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0`.
44+ */
45+ const EXTRACT_ACTION_REF_PATTERN: RegExp =
46+ /uses:\s+([^/\s]+\/[^@\s]+)@([^@\n]+)/g;
47+
48+ /**
49+ * Used to identify characters in `action_name` strings that need to
50+ * be escaped before inserting them into TypeScript or YAML strings.
51+ */
52+ const ESCAPE_PATTERN = /[.*+?^${}()|[\]\\]/g;
53+
54+ /**
55+ * A `SyncBackPattern` is a function which constructs a regular expression for a specific `actionName`,
56+ * which finds references to `actionName` and surrounding context in a particular file that we want
57+ * to sync updated versions back to.
58+ */
59+ type SyncBackPattern = (actionName: string) => RegExp;
60+
61+ /**
62+ * Used to find lines containing action references in `sync.ts`.
63+ *
64+ * Matches `uses: "actionName@version_str"` in PR check specifications and groups `uses: "`
65+ * and `"`, allowing `actionName@version_str` to be replaced with a new action reference.
66+ */
67+ const TS_PATTERN: SyncBackPattern = (actionName: string) =>
68+ new RegExp(`(uses:\\s*")${actionName}@(?:[^"]+)(")`, "g");
69+
70+ /**
71+ * Used to find lines containing action references in a PR check specification.
72+ *
73+ * Matches `uses: actionName@rest_of_line` in PR check specifications and extracts `uses: actionName`,
74+ * allowing `rest_of_line` to be replaced with a new version string.
75+ */
76+ const YAML_PATTERN: SyncBackPattern = (actionName: string) =>
77+ new RegExp(`(uses:\\s+${actionName})@(?:[^@\n]+)`, "g");
78+
79+ /**
80+ * Constructs a regular expression using `patternFunction` for `actionName`, which is sanitised
81+ * before `patternFunction` is called.
82+ *
83+ * @param patternFunction The pattern builder to use.
84+ * @param actionName The action name, which will be sanitised.
85+ * @returns The regular expression returned by `patternFunction`.
86+ */
87+ function makeReplacementPattern(
88+ patternFunction: SyncBackPattern,
89+ actionName: string,
90+ ): RegExp {
91+ return patternFunction(actionName.replace(ESCAPE_PATTERN, "\\$&"));
92+ }
93+
3094/**
3195 * Scan generated workflow files to extract the latest action versions.
3296 *
3397 * @param workflowDir - Path to .github/workflows directory
3498 * @returns Map from action names to their latest versions (including comments)
3599 */
36- export function scanGeneratedWorkflows(workflowDir: string): Record<string, string> {
100+ export function scanGeneratedWorkflows(
101+ workflowDir: string,
102+ ): Record<string, string> {
37103 const actionVersions: Record<string, string> = {};
38104
39105 const generatedFiles = fs
@@ -43,13 +109,10 @@ export function scanGeneratedWorkflows(workflowDir: string): Record<string, stri
43109
44110 for (const filePath of generatedFiles) {
45111 const content = fs.readFileSync(filePath, "utf8");
46-
47- // Find all action uses in the file, including potential comments
48- // This pattern captures: action_name@version_with_possible_comment
49- const pattern = /uses:\s+([^/\s]+\/[^@\s]+)@([^@\n]+)/g;
50112 let match: RegExpExecArray | null;
51113
52- while ((match = pattern.exec(content)) !== null) {
114+ EXTRACT_ACTION_REF_PATTERN.lastIndex = 0;
115+ while ((match = EXTRACT_ACTION_REF_PATTERN.exec(content)) !== null) {
53116 const actionName = match[1];
54117 const versionWithComment = match[2].trimEnd();
55118
@@ -91,15 +154,11 @@ export function updateSyncTs(
91154 ? versionWithComment.split("#")[0].trim()
92155 : versionWithComment.trim();
93156
94- // Look for patterns like uses: "actions/setup-node@v4"
157+ // Update uses of `actionName` for `version`.
95158 // Note that this will break if we store an Action uses reference in a
96159 // variable - that's a risk we're happy to take since in that case the
97160 // PR checks will just fail.
98- const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99- const pattern = new RegExp(
100- `(uses:\\s*")${escaped}@(?:[^"]+)(")`,
101- "g",
102- );
161+ const pattern = makeReplacementPattern(TS_PATTERN, actionName);
103162 content = content.replace(pattern, `$1${actionName}@${version}$2`);
104163 }
105164
@@ -139,12 +198,8 @@ export function updateTemplateFiles(
139198 for (const [actionName, versionWithComment] of Object.entries(
140199 actionVersions,
141200 )) {
142- // Look for patterns like 'uses: actions/setup-node@v4' or 'uses: actions/setup-node@sha # comment'
143- const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
144- const pattern = new RegExp(
145- `(uses:\\s+${escaped})@(?:[^@\n]+)`,
146- "g",
147- );
201+ // Update uses of `actionName` for `versionWithComment`.
202+ const pattern = makeReplacementPattern(YAML_PATTERN, actionName);
148203 content = content.replace(pattern, `$1@${versionWithComment}`);
149204 }
150205
0 commit comments