Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0aa8e41
ui: buffer spinner + worker rows into single synchronized writes
nodeselector Jun 12, 2026
f0d8aa8
ui: erase spinner line before redraw, hide cursor during animation
nodeselector Jun 12, 2026
e2e9fe5
ui: fix data race on spinner Suffix between label updates and render …
nodeselector Jun 12, 2026
a8ddcc9
ui: erase worker rows immediately on ClearWorkerStatuses instead of l…
nodeselector Jun 12, 2026
58daf3b
ui: static glyph-only top line, all detail in worker rows
nodeselector Jun 12, 2026
1bab682
ui: align spinner with cli/cli — label left, glyph right, race-free
nodeselector Jun 12, 2026
5f904fe
ui: static spinner label — set once in StartProgress, never changed
nodeselector Jun 12, 2026
fda2ea3
ui: skip worker row render on spinner erase writes
nodeselector Jun 12, 2026
c2182f1
ui: move worker row glyph to right, matching main spinner style
nodeselector Jun 12, 2026
c6a7637
address Copilot review: tighten erase detection, lock cursor escapes,…
nodeselector Jun 12, 2026
df0255d
support GH_ACTIONS_PIN_WORKFLOWS_DIR for non-standard workflow locations
nodeselector Jun 12, 2026
21e7a05
remove duplicate 'All valid' message from terminal report
nodeselector Jun 12, 2026
4d00102
remove duplicate SSO URL from --no-fix path
nodeselector Jun 12, 2026
833e593
pin summary: handle zero-workflow case gracefully
nodeselector Jun 12, 2026
be3ca41
TermCaution: use yellow instead of red
nodeselector Jun 12, 2026
58050c6
resolution record path: use TermDetail for better visibility
nodeselector Jun 12, 2026
090b047
add scenario and unit test coverage for CLI UX fixes
nodeselector Jun 12, 2026
04d160a
fix isErase detection (wrong byte lengths), clear slot 0 on empty detail
nodeselector Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions cmd/gh-actions-pin/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,6 @@ func runCheck(cmd *cobra.Command, opts *checkOptions, newResolver resolverFunc)
// Strict gate — any blocking finding is a non-zero exit.
if opts.noFix {
console.StopProgress()
if gc := r.GHClient(); gc != nil {
if ssoURL := gc.SSOURL(); ssoURL != "" {
console.TermBlank()
console.TermDetail("Authorize in your web browser: %s", ssoURL)
}
}
if opts.jsonFields != "" {
if err := format.WriteJSON(out, report, valid, opts.jsonFields, cliVersion(), store.File().Version); err != nil {
return err
Expand Down Expand Up @@ -332,7 +326,7 @@ func runCheck(cmd *cobra.Command, opts *checkOptions, newResolver resolverFunc)
if path, werr := record.WriteJSON(); werr == nil {
defer func() {
console.TermBlank()
console.TermNeutral("Resolution record: %s", path)
console.TermDetail("Resolution record: %s", path)
}()
}

Expand Down
4 changes: 1 addition & 3 deletions cmd/gh-actions-pin/format/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ func PresentResults(out *ui.UI, report *checks.Report, valid bool, willRemediate
}
checked := validCount + failedCount

if valid && checked > 0 {
out.Success("All %d %s valid", checked, ui.Pluralize(checked, "workflow", "workflows"))
} else if checked > 0 {
if !valid && checked > 0 {
renderErrorFindings(out, report, failedCount, checked)
}

Expand Down
18 changes: 18 additions & 0 deletions cmd/gh-actions-pin/format/terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ func TestPresentResults_WarningsReachTerminal(t *testing.T) {
}
}

// TestPresentResults_NoSuccessLine verifies PresentResults no longer emits
// the "All N workflows valid" success line — renderPinSummary owns that.
func TestPresentResults_NoSuccessLine(t *testing.T) {
u, buf := newTestUI()
report := &checks.Report{
Workflows: []checks.WorkflowReport{
{Path: ".github/workflows/a.yml"},
{Path: ".github/workflows/b.yml"},
},
}
PresentResults(u, report, true, false)

got := buf.String()
if strings.Contains(got, "All") && strings.Contains(got, "valid") {
t.Errorf("PresentResults should not print success line, got:\n%s", got)
}
}

// TestPresentResults_RemediateHints locks the willRemediate-aware "↳"
// follow-up lines that PresentResults emits under each warning headline.
// Categories the remediator auto-fixes (NotPinned, SHAAsRef) flip between
Expand Down
4 changes: 4 additions & 0 deletions cmd/gh-actions-pin/pin_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func renderPinSummary(console *ui.UI, record *pin.Record, report *checks.Report,
}

total := len(report.Workflows)
if total == 0 {
console.TermNeutral("No workflows to check")
return nil
}
if len(pinned) == 0 && len(investigated) == 0 && len(unresolvedEntries) == 0 && !hasInconclusive {
console.TermSuccess("All %d %s valid", total, ui.Pluralize(total, "workflow", "workflows"))
if skippedRescan > 0 {
Expand Down
23 changes: 20 additions & 3 deletions cmd/gh-actions-pin/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ $ gh actions-pin --no-fix --json=valid,findings
// from the existing lockfile so repeat scans short-circuit the per-branch
// Compare walk. newResolver is the DI seam; pass nil for production wiring.
func newRun(workflowPaths []string, hostname string, pool *pinpool.Pool, newResolver resolverFunc) ([]string, *resolve.Resolver, *lockfile.State, error) {
paths, err := discoverWorkflowPaths(workflowPaths)
workflowsDir := os.Getenv("GH_ACTIONS_PIN_WORKFLOWS_DIR")
paths, err := discoverWorkflowPaths(workflowPaths, workflowsDir)
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -139,7 +140,12 @@ func newRun(workflowPaths []string, hostname string, pool *pinpool.Pool, newReso
return nil, nil, nil, err
}

store, err := lockfile.LoadState(".", r)
var store *lockfile.State
if workflowsDir != "" {
store, err = lockfile.LoadStateAt(filepath.Join(workflowsDir, "actions.lock"), r)
} else {
store, err = lockfile.LoadState(".", r)
}
if err != nil {
return nil, nil, nil, fmt.Errorf("opening lockfile: %w", err)
}
Expand All @@ -148,11 +154,22 @@ func newRun(workflowPaths []string, hostname string, pool *pinpool.Pool, newReso
return paths, r, store, nil
}

func discoverWorkflowPaths(existing []string) ([]string, error) {
func discoverWorkflowPaths(existing []string, workflowsDir string) ([]string, error) {
if len(existing) > 0 {
return expandWorkflowPaths(existing)
}

if workflowsDir != "" {
paths, err := workflowfile.DiscoverWorkflowsIn(workflowsDir)
if err != nil {
return nil, err
}
if len(paths) == 0 {
return nil, fmt.Errorf("no workflow files found in %s", workflowsDir)
}
return paths, nil
}

paths, err := workflowfile.DiscoverWorkflows()
if err != nil {
return nil, err
Expand Down
16 changes: 11 additions & 5 deletions internal/lockfile/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type MetadataResolver interface {
// a single store instance without external synchronization.
type State struct {
mu sync.Mutex
repoRoot string
lockPath string // full path to actions.lock on disk
file parserlock.File
meta MetadataResolver
idCache map[string][2]int64
Expand All @@ -48,8 +48,14 @@ type State struct {
// LoadState reads the lockfile at repoRoot, returning an empty in-memory file
// when none exists on disk.
func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
full := filepath.Join(repoRoot, parserlock.Path)
contents, err := os.ReadFile(full)
return LoadStateAt(filepath.Join(repoRoot, parserlock.Path), meta)
}

// LoadStateAt reads the lockfile at the given path, returning an empty
// in-memory file when none exists on disk. Use this when the lockfile
// lives outside the standard .github/workflows/ location.
func LoadStateAt(lockfilePath string, meta MetadataResolver) (*State, error) {
contents, err := os.ReadFile(lockfilePath)

var file parserlock.File
switch {
Expand Down Expand Up @@ -88,7 +94,7 @@ func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
}

s := &State{
repoRoot: repoRoot,
lockPath: lockfilePath,
file: file,
meta: meta,
idCache: map[string][2]int64{},
Expand Down Expand Up @@ -360,7 +366,7 @@ func (s *State) Save() error {
}
}

full := filepath.Join(s.repoRoot, parserlock.Path)
full := s.lockPath

if len(s.file.Dependencies) == 0 && len(s.file.Workflows) == 0 {
if err := os.Remove(full); err != nil && !errors.Is(err, os.ErrNotExist) {
Expand Down
Loading