@@ -80,37 +80,67 @@ func PinAction(action, inputYaml string, exemptedActions []string, pinToImmutabl
8080 return inputYaml , updated
8181 }
8282
83- pinnedAction := fmt .Sprintf ("%s@%s # %s" , leftOfAt [0 ], commitSHA , tagOrBranch )
83+ // pinnedAction := fmt.Sprintf("%s@%s # %s", leftOfAt[0], commitSHA, tagOrBranch)
84+ // build separately so we can quote only the ref, not the comment
85+ pinnedRef := fmt .Sprintf ("%s@%s" , leftOfAt [0 ], commitSHA )
86+ comment := fmt .Sprintf (" # %s" , tagOrBranch )
87+ fullPinned := pinnedRef + comment
8488
8589 // if the action with version is immutable, then pin the action with version instead of sha
8690 pinnedActionWithVersion := fmt .Sprintf ("%s@%s" , leftOfAt [0 ], tagOrBranch )
8791 if pinToImmutable && semanticTagRegex .MatchString (tagOrBranch ) && IsImmutableAction (pinnedActionWithVersion ) {
88- pinnedAction = pinnedActionWithVersion
89- }
92+ // strings.ReplaceAll is not suitable here because it would incorrectly replace substrings
93+ // For example, if we want to replace "actions/checkout@v1" to "actions/[email protected] ", it would also incorrectly match and replace in "actions/[email protected] " 94+ // making new string to "actions/[email protected] " 95+ //
96+ // Instead, we use a regex pattern that ensures we only replace complete action references:
97+ // Pattern: (<action>@<version>)($|\s|"|')
98+ // - Group 1 (<action>@<version>): Captures the exact action reference
99+ // - Group 2 ($|\s|"|'): Captures the delimiter that follows (end of line, whitespace, or quotes)
100+ //
101+ // Examples:
102+ // - "actions/[email protected] " - No match (no delimiter after v1) 103+ // - "actions/checkout@v1 " - Matches (space delimiter)
104+ // - "actions/checkout@v1"" - Matches (quote delimiter)
105+ // - "actions/checkout@v1" - Matches (quote delimiter)
106+ // - "actions/checkout@v1\n" - Matches (newline is considered whitespace \s)
107+
108+ actionRegex := regexp .MustCompile (`(` + regexp .QuoteMeta (action ) + `)($|\s|"|')` )
109+ inputYaml = actionRegex .ReplaceAllString (inputYaml , pinnedActionWithVersion + "$2" )
110+
111+ inputYaml , _ = removePreviousActionComments (pinnedActionWithVersion , inputYaml )
112+ return inputYaml , ! strings .EqualFold (action , pinnedActionWithVersion )
113+ }
114+
115+ updated = ! strings .EqualFold (action , fullPinned )
116+
117+ // 1) Double-quoted form: "owner/repo@oldRef"
118+ doubleQuotedPattern := `"` + regexp .QuoteMeta (action ) + `"` + `($|\s|"|')`
119+ doubleQuotedRe := regexp .MustCompile (doubleQuotedPattern )
120+ inputYaml = doubleQuotedRe .ReplaceAllString (
121+ inputYaml ,
122+ fmt .Sprintf (`"%s"%s$1` , pinnedRef , comment ),
123+ )
124+ inputYaml , _ = removePreviousActionComments (fmt .Sprintf (`"%s"%s` , pinnedRef , comment ), inputYaml )
125+
126+ // 2) Single-quoted form: 'owner/repo@oldRef'
127+ singleQuotedPattern := `'` + regexp .QuoteMeta (action ) + `'` + `($|\s|"|')`
128+ singleQuotedRe := regexp .MustCompile (singleQuotedPattern )
129+ inputYaml = singleQuotedRe .ReplaceAllString (
130+ inputYaml ,
131+ fmt .Sprintf (`'%s'%s$1` , pinnedRef , comment ),
132+ )
133+ inputYaml , _ = removePreviousActionComments (fmt .Sprintf (`'%s'%s` , pinnedRef , comment ), inputYaml )
134+
135+ // 3) Unquoted form: owner/repo@oldRef
136+ unqPattern := `\b` + regexp .QuoteMeta (action ) + `\b` + `($|\s|"|')`
137+ unqRe := regexp .MustCompile (unqPattern )
138+ inputYaml = unqRe .ReplaceAllString (
139+ inputYaml ,
140+ fullPinned + `$1` ,
141+ )
142+ inputYaml , _ = removePreviousActionComments (fullPinned , inputYaml )
90143
91- updated = ! strings .EqualFold (action , pinnedAction )
92-
93- // strings.ReplaceAll is not suitable here because it would incorrectly replace substrings
94- // For example, if we want to replace "actions/checkout@v1" to "actions/[email protected] ", it would also incorrectly match and replace in "actions/[email protected] " 95- // making new string to "actions/[email protected] " 96- //
97- // Instead, we use a regex pattern that ensures we only replace complete action references:
98- // Pattern: (<action>@<version>)($|\s|"|')
99- // - Group 1 (<action>@<version>): Captures the exact action reference
100- // - Group 2 ($|\s|"|'): Captures the delimiter that follows (end of line, whitespace, or quotes)
101- //
102- // Examples:
103- // - "actions/[email protected] " - No match (no delimiter after v1) 104- // - "actions/checkout@v1 " - Matches (space delimiter)
105- // - "actions/checkout@v1"" - Matches (quote delimiter)
106- // - "actions/checkout@v1" - Matches (quote delimiter)
107- // - "actions/checkout@v1\n" - Matches (newline is considered whitespace \s)
108- actionRegex := regexp .MustCompile (`(` + regexp .QuoteMeta (action ) + `)($|\s|"|')` )
109- inputYaml = actionRegex .ReplaceAllString (inputYaml , pinnedAction + "$2" )
110- yamlWithPreviousActionCommentsRemoved , wasModified := removePreviousActionComments (pinnedAction , inputYaml )
111- if wasModified {
112- return yamlWithPreviousActionCommentsRemoved , updated
113- }
114144 return inputYaml , updated
115145}
116146
@@ -126,11 +156,12 @@ func removePreviousActionComments(pinnedAction, inputYaml string) (string, bool)
126156 inputYaml = stringParts [0 ]
127157 for idx := 1 ; idx < len (stringParts ); idx ++ {
128158 trimmedString := strings .SplitN (stringParts [idx ], "\n " , 2 )
159+ inputYaml = inputYaml + pinnedAction
129160 if len (trimmedString ) > 1 {
130161 if strings .Contains (trimmedString [0 ], "#" ) {
131162 updated = true
132163 }
133- inputYaml = inputYaml + pinnedAction + "\n " + trimmedString [1 ]
164+ inputYaml = inputYaml + "\n " + trimmedString [1 ]
134165 }
135166 }
136167 }
0 commit comments