Skip to content

Commit 71fb4f5

Browse files
authored
Merge pull request cli#13723 from cli/bagtoad/disable-auth-check-release-clone
Allow downloading release assets without authentication
2 parents 954ffc3 + 7b681a4 commit 71fb4f5

5 files changed

Lines changed: 142 additions & 12 deletions

File tree

pkg/cmd/release/download/download.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobr
111111
cmd.Flags().BoolVar(&opts.OverwriteExisting, "clobber", false, "Overwrite existing files of the same name")
112112
cmd.Flags().BoolVar(&opts.SkipExisting, "skip-existing", false, "Skip downloading when files of the same name exist")
113113

114+
cmdutil.DisableAuthCheck(cmd)
115+
114116
return cmd
115117
}
116118

pkg/cmd/release/download/download_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,37 @@ func Test_downloadRun(t *testing.T) {
218218
"windows-64bit.zip",
219219
},
220220
},
221+
{
222+
name: "downloads published release when the draft lookup is unauthorized",
223+
isTTY: true,
224+
opts: DownloadOptions{
225+
TagName: "v1.2.3",
226+
Destination: ".",
227+
Concurrency: 2,
228+
},
229+
httpStubs: func(reg *httpmock.Registry) {
230+
reg.Register(
231+
httpmock.REST("GET", "repos/OWNER/REPO/releases/tags/v1.2.3"),
232+
httpmock.StringResponse(`{
233+
"assets": [
234+
{ "name": "linux.tgz", "size": 56, "url": "https://api.github.com/assets/5678" }
235+
]
236+
}`),
237+
)
238+
// An unauthenticated draft lookup fails over GraphQL and must not
239+
// mask the published release found over REST.
240+
reg.Register(
241+
httpmock.GraphQL(`query RepositoryReleaseByTag\b`),
242+
httpmock.StatusStringResponse(403, `{"message":"This endpoint requires you to be authenticated."}`),
243+
)
244+
reg.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`))
245+
},
246+
wantStdout: ``,
247+
wantStderr: ``,
248+
wantFiles: []string{
249+
"linux.tgz",
250+
},
251+
},
221252
{
222253
name: "download assets matching pattern into destination directory",
223254
isTTY: true,

pkg/cmd/release/shared/fetch.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,27 @@ func FetchRelease(ctx context.Context, httpClient *http.Client, repo ghrepo.Inte
201201
results <- fetchResult{release: release, error: err}
202202
}()
203203

204-
res := <-results
205-
if errors.Is(res.error, ErrReleaseNotFound) {
206-
res = <-results
207-
cancel() // satisfy the linter even though no goroutines are running anymore
208-
} else {
204+
// Prefer a release found by either lookup. A single failed lookup, such as
205+
// the draft lookup when unauthenticated, must not mask a release found by
206+
// the other; only report an error when both lookups fail.
207+
first := <-results
208+
if first.error == nil {
209209
cancel()
210210
<-results // drain the channel
211+
return first.release, nil
211212
}
212-
return res.release, res.error
213+
214+
second := <-results
215+
cancel() // satisfy the linter even though no goroutines are running anymore
216+
if second.error == nil {
217+
return second.release, nil
218+
}
219+
220+
// Both lookups failed; prefer reporting the release as not found.
221+
if errors.Is(second.error, ErrReleaseNotFound) {
222+
return nil, second.error
223+
}
224+
return nil, first.error
213225
}
214226

215227
// FetchLatestRelease finds the latest published release for a repository.

pkg/cmdutil/repo_override.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,25 @@ import (
99
"github.com/spf13/cobra"
1010
)
1111

12-
func executeParentHooks(cmd *cobra.Command, args []string) error {
13-
for cmd.HasParent() {
14-
cmd = cmd.Parent()
15-
if cmd.PersistentPreRunE != nil {
16-
return cmd.PersistentPreRunE(cmd, args)
12+
// executeParentHook re-runs the nearest ancestor's persistent pre-run hook,
13+
// which the hook installed by EnableRepoOverride would otherwise shadow. By
14+
// default cobra runs only the nearest PersistentPreRunE found walking up from
15+
// the invoked command, so without this the nearest ancestor hook, such as the
16+
// root auth gate, would never run for a repo-override command.
17+
//
18+
// That ancestor hook receives the invoked leaf cmd, not the ancestor, matching
19+
// how cobra passes the leaf to every persistent hook:
20+
// https://github.com/spf13/cobra/blob/v1.10.2/command.go#L984-L986
21+
//
22+
// cobra's EnableTraverseRunHooks global is the native equivalent and runs the
23+
// whole root-to-leaf chain for us, but it is global. Enabling it would change
24+
// pre-run behavior for every command: double-running the parents that issue
25+
// develop and EnableRepoOverride re-run by hand, and un-suppressing the root
26+
// gate that agent-task and skills intentionally shadow.
27+
func executeParentHook(overrideCmd, cmd *cobra.Command, args []string) error {
28+
for p := overrideCmd.Parent(); p != nil; p = p.Parent() {
29+
if p.PersistentPreRunE != nil {
30+
return p.PersistentPreRunE(cmd, args)
1731
}
1832
}
1933
return nil
@@ -47,8 +61,9 @@ func EnableRepoOverride(cmd *cobra.Command, f *Factory) {
4761
return results, cobra.ShellCompDirectiveNoFileComp
4862
})
4963

64+
overrideCmd := cmd
5065
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
51-
if err := executeParentHooks(cmd, args); err != nil {
66+
if err := executeParentHook(overrideCmd, cmd, args); err != nil {
5267
return err
5368
}
5469
repoOverride, _ := cmd.Flags().GetString("repo")

pkg/cmdutil/repo_override_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package cmdutil
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// Test_EnableRepoOverride_authCheckIntegration is an integration test for the
11+
// coupling between repo override and the root auth gate, wired through cobra's
12+
// persistent pre-run hooks. EnableRepoOverride replaces a command's
13+
// PersistentPreRunE, so cobra no longer reaches the root auth gate on its own;
14+
// the override hook re-runs the ancestor hooks itself. That re-run must evaluate
15+
// the command the user actually invoked, the same way cobra hands the target
16+
// command to parent hooks:
17+
// https://github.com/spf13/cobra/blob/v1.10.2/command.go#L984-L986
18+
// This pins that a leaf's DisableAuthCheck is honored under a repo-override
19+
// parent.
20+
func Test_EnableRepoOverride_authCheckIntegration(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
disableAuthLeaf bool
24+
wantAuthChecked bool
25+
}{
26+
{
27+
name: "leaf opts out, honored under repo-override parent",
28+
disableAuthLeaf: true,
29+
wantAuthChecked: false,
30+
},
31+
{
32+
name: "leaf does not opt out, auth still checked",
33+
disableAuthLeaf: false,
34+
wantAuthChecked: true,
35+
},
36+
}
37+
38+
for _, tt := range tests {
39+
t.Run(tt.name, func(t *testing.T) {
40+
var gotAuthChecked, ranLeaf bool
41+
42+
// Stand in for the real root auth gate: it judges whatever command
43+
// it is handed, so it must receive the invoked leaf command.
44+
root := &cobra.Command{Use: "root"}
45+
root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
46+
gotAuthChecked = IsAuthCheckEnabled(cmd)
47+
return nil
48+
}
49+
50+
parent := &cobra.Command{Use: "parent"}
51+
EnableRepoOverride(parent, &Factory{})
52+
root.AddCommand(parent)
53+
54+
leaf := &cobra.Command{
55+
Use: "leaf",
56+
RunE: func(cmd *cobra.Command, args []string) error { ranLeaf = true; return nil },
57+
}
58+
if tt.disableAuthLeaf {
59+
DisableAuthCheck(leaf)
60+
}
61+
parent.AddCommand(leaf)
62+
63+
root.SetArgs([]string{"parent", "leaf"})
64+
require.NoError(t, root.Execute())
65+
66+
require.True(t, ranLeaf, "leaf command should have executed")
67+
require.Equal(t, tt.wantAuthChecked, gotAuthChecked)
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)