@@ -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+ / u s e s : \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 = / u s e s : \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