Skip to content

Commit aaf2d8d

Browse files
committed
Document regular expressions
1 parent b1b5550 commit aaf2d8d

File tree

1 file changed

+70
-16
lines changed

1 file changed

+70
-16
lines changed

pr-checks/sync_back.ts

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,79 @@ const CHECKS_DIR = path.join(THIS_DIR, "checks");
2727
const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows");
2828
const 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 `version_str`, allowing `version_str` to be replaced with a new version string.
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 groups `uses: actionName`
74+
* and `rest_of_line`, 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,9 @@ 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+
while ((match = EXTRACT_ACTION_REF_PATTERN.exec(content)) !== null) {
53115
const actionName = match[1];
54116
const versionWithComment = match[2].trimEnd();
55117

@@ -95,11 +157,7 @@ export function updateSyncTs(
95157
// Note that this will break if we store an Action uses reference in a
96158
// variable - that's a risk we're happy to take since in that case the
97159
// PR checks will just fail.
98-
const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99-
const pattern = new RegExp(
100-
`(uses:\\s*")${escaped}@(?:[^"]+)(")`,
101-
"g",
102-
);
160+
const pattern = makeReplacementPattern(TS_PATTERN, actionName);
103161
content = content.replace(pattern, `$1${actionName}@${version}$2`);
104162
}
105163

@@ -140,11 +198,7 @@ export function updateTemplateFiles(
140198
actionVersions,
141199
)) {
142200
// 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+
const pattern = makeReplacementPattern(YAML_PATTERN, actionName);
148202
content = content.replace(pattern, `$1@${versionWithComment}`);
149203
}
150204

0 commit comments

Comments
 (0)