SSO: replace browser URL with actionable fix guidance by nodeselector · Pull Request #68 · github/gh-actions-lock · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions cmd/gh-actions-lock/codespace.go
24 changes: 24 additions & 0 deletions cmd/gh-actions-lock/codespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSSOFixHints(t *testing.T) {
t.Run("codespace points to devcontainer docs", func(t *testing.T) {
t.Setenv("CODESPACES", "true")
hints := ssoFixHints()
assert.Len(t, hints, 2)
assert.Contains(t, hints[0], "cannot be SSO-authorized")
assert.Contains(t, hints[1], "devcontainer permissions")
})

t.Run("non-codespace points to gh auth refresh", func(t *testing.T) {
t.Setenv("CODESPACES", "")
hints := ssoFixHints()
assert.Len(t, hints, 1)
assert.Contains(t, hints[0], "gh auth refresh")
})
}
19 changes: 1 addition & 18 deletions cmd/gh-actions-lock/pin_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,30 +504,13 @@ func cleanUnresolvedReason(reason, nwo, ref string) (string, string) {
// extractFixHint returns an actionable hint for common resolution errors.
// Returns "" when no actionable guidance can be inferred.
func extractFixHint(reason string) string {
// SSO/SAML enforcement: extract the authorization URL.
// Matches cli/cli's format: "Authorize in your web browser: <url>"
if strings.Contains(reason, "SSO authorization required") ||
strings.Contains(reason, "SAML enforcement") {
if url := extractURLWithPrefix(reason, "https://github.com/orgs/"); url != "" {
return fmt.Sprintf("Authorize in your web browser: %s", url)
}
return strings.Join(ssoFixHints(), "\n")
}
return ""
}

// extractURLWithPrefix finds the first URL in text starting with prefix.
func extractURLWithPrefix(text, prefix string) string {
idx := strings.Index(text, prefix)
if idx < 0 {
return ""
}
end := idx
for end < len(text) && text[end] != ' ' && text[end] != '\n' && text[end] != ')' {
end++
}
return text[idx:end]
}

// stripNWORefPrefix removes a leading "owner/repo@ref: " pattern from s.
// The ref can be a tag (v4.3.1), branch, or full SHA. This handles both
// the current action's own prefix and cross-action references that appear
Expand Down
2 changes: 1 addition & 1 deletion cmd/gh-actions-lock/pin_summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func TestCleanUnresolvedReason(t *testing.T) {
nwo: "actions/checkout",
ref: "v4.3.1",
wantText: `SSO authorization required: your token is not authorized for the "actions" organization (SAML enforcement)`,
wantHint: "Authorize in your web browser: https://github.com/orgs/actions/sso",
wantHint: "Run: gh auth refresh -h github.com",
},
{
name: "bare reason without prefixes passes through",
Expand Down
25 changes: 14 additions & 11 deletions cmd/gh-actions-lock/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,16 @@ func runCheck(cmd *cobra.Command, opts *checkOptions, newResolver resolverFunc)
return err
}
}
// Surface SSO URL even in read-only mode — it's the actionable fix
// for SAML-gated repos and shouldn't require a --fix run to see.
// Surface SSO guidance even in read-only mode — it's the actionable
// fix for SAML-gated repos and shouldn't require a --fix run to see.
// Suppressed in JSON mode to avoid corrupting stdout.
if opts.jsonFields == "" {
if gc := r.GHClient(); gc != nil {
if ssoURL := gc.SSOURL(); ssoURL != "" {
if gc.SSOURL() != "" {
console.TermBlank()
console.TermDetail("Authorize in your web browser: %s", ssoURL)
for _, hint := range ssoFixHints() {
console.TermDetail("%s", hint)
}
Comment thread
nodeselector marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -355,16 +357,17 @@ func runCheck(cmd *cobra.Command, opts *checkOptions, newResolver resolverFunc)
hasInconclusive := opts.rescan && report.HasInconclusive()
summaryErr := renderPinSummary(ctx, console, record, report, r, skippedRescan, hasInconclusive, refusedLabels, opts.noNarrow, opts.acceptMoved, store.OriginalVersion())

// Surface the SAML SSO authorization URL if one was captured during
// the run, matching cli/cli's "Authorize in your web browser:" line.
// This runs even when renderPinSummary returns errSilent (unresolved
// entries exist) because the SSO hint is the fix for those entries.
// Suppressed in JSON mode to avoid corrupting stdout.
// Surface SAML SSO fix guidance if an SSO header was captured during
// the run. This runs even when renderPinSummary returns errSilent
// (unresolved entries exist) because the SSO hint is the fix for
// those entries. Suppressed in JSON mode to avoid corrupting stdout.
if opts.jsonFields == "" {
if gc := r.GHClient(); gc != nil {
if ssoURL := gc.SSOURL(); ssoURL != "" {
if gc.SSOURL() != "" {
console.TermBlank()
console.TermDetail("Authorize in your web browser: %s", ssoURL)
for _, hint := range ssoFixHints() {
console.TermDetail("%s", hint)
}
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions test/scenarios/catalog.yml
Loading