@@ -2,9 +2,12 @@ package workflow
22
33import (
44 "fmt"
5+ "os"
56 "strings"
67
78 actionpins "github.com/github/gh-aw/pkg/actionpins"
9+ "github.com/github/gh-aw/pkg/console"
10+ "github.com/github/gh-aw/pkg/gitutil"
811 "github.com/github/gh-aw/pkg/logger"
912 "github.com/github/gh-aw/pkg/semverutil"
1013)
@@ -198,6 +201,68 @@ func getCachedActionPin(repo string, data *WorkflowData) string {
198201// Step-level helpers that depend on WorkflowStep (stay in pkg/workflow)
199202// --------------------------------------------------------------------------
200203
204+ // warnIfOutdatedActionVersion emits a warning to stderr when rawVersion is a semver
205+ // action-version tag (e.g. "v3", "v4.0.0") and a strictly newer version exists in the
206+ // embedded action pins. The check is skipped when rawVersion is already a full SHA or a
207+ // non-semver ref (branch name, etc.), and when the latest available version is not newer.
208+ //
209+ // Warnings are deduplicated per repo@version pair via data.ActionPinWarnings so that the
210+ // same step appearing in multiple places (pre-steps, steps, post-steps) only produces one
211+ // diagnostic.
212+ //
213+ // Partial version tags (e.g. "v4" without minor/patch) are only flagged when the latest
214+ // available major version is higher than the requested one; within the same major the tag
215+ // is treated as a floating reference and no warning is emitted.
216+ func warnIfOutdatedActionVersion (actionRepo , rawVersion , latestVersion string , data * WorkflowData ) {
217+ if data == nil {
218+ return
219+ }
220+
221+ // SHAs are already pinned to a specific commit — no version to compare.
222+ if gitutil .IsValidFullSHA (rawVersion ) {
223+ return
224+ }
225+ // Only check recognised action version tags (vN, vN.M, vN.M.P).
226+ if ! semverutil .IsActionVersionTag (rawVersion ) {
227+ return
228+ }
229+
230+ latestSemver := semverutil .ParseVersion (latestVersion )
231+ requestedSemver := semverutil .ParseVersion (rawVersion )
232+ if latestSemver == nil || requestedSemver == nil {
233+ return
234+ }
235+
236+ // For tags without a patch component (e.g. "@v4", "@v4.1"), treat them as
237+ // floating references that resolve to the latest compatible patch within that
238+ // major version line (for major-only tags) or minor version line (for
239+ // major.minor tags). Only warn when the latest available major version is
240+ // higher — same-major newer minors/patches are not "outdated" for a floating tag.
241+ isPartialTag := strings .Count (strings .TrimPrefix (rawVersion , "v" ), "." ) < 2
242+ if isPartialTag {
243+ if latestSemver .Major <= requestedSemver .Major {
244+ return
245+ }
246+ } else if ! latestSemver .IsNewer (requestedSemver ) {
247+ return
248+ }
249+
250+ // Deduplicate: only emit the warning once per repo@version within this compilation.
251+ cacheKey := "outdated:" + actionpins .FormatCacheKey (actionRepo , rawVersion )
252+ if data .ActionPinWarnings == nil {
253+ data .ActionPinWarnings = make (map [string ]bool )
254+ }
255+ if data .ActionPinWarnings [cacheKey ] {
256+ return
257+ }
258+ data .ActionPinWarnings [cacheKey ] = true
259+
260+ warningMsg := fmt .Sprintf ("Action %s@%s is outdated; latest available version is %s.\n Consider upgrading (update the version tag in your workflow file)." ,
261+ actionRepo , rawVersion , latestVersion )
262+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (warningMsg ))
263+ actionPinsLog .Printf ("Outdated action version detected: %s@%s (latest: %s)" , actionRepo , rawVersion , latestVersion )
264+ }
265+
201266// applyActionPinToTypedStep applies SHA pinning to a WorkflowStep if it uses an action.
202267// Returns a modified copy of the step with pinned references.
203268// If the step doesn't use an action or the action is not pinned, returns the original step.
@@ -237,6 +302,11 @@ func applyActionPinToTypedStep(step *WorkflowStep, data *WorkflowData) (*Workflo
237302 // Uses strings like "repo@sha # version" are treated as already-pinned.
238303 rawVersion , _ , _ := strings .Cut (version , " " )
239304
305+ // Warn if the requested version is older than the latest available in embedded pins.
306+ if latestPin , hasLatest := getLatestActionPinByRepo (actionRepo ); hasLatest {
307+ warnIfOutdatedActionVersion (actionRepo , rawVersion , latestPin .Version , data )
308+ }
309+
240310 pinnedRef , err := getActionPinWithData (actionRepo , rawVersion , data )
241311 if err != nil || pinnedRef == "" {
242312 actionPinsLog .Printf ("Skipping pin for %s@%s: no pin available" , actionRepo , rawVersion )
0 commit comments