refactor(remote): rename HostHeaders to HeadersByHost

The type is a map of host to headers, not a flat header set. Name it after
that shape, and rename the fields carrying it to authHeadersByHost so the
lookup step is visible at every call site.

Claude-Session: https://claude.ai/code/session_01KNPMznEzkRpxFZMLisjdqL
This commit is contained in:
Valentin Maerten
2026-08-29 21:47:53 +02:00
parent bff4f97d7e
commit 89b8d04ad9
6 changed files with 94 additions and 94 deletions

View File

@@ -37,7 +37,7 @@ type (
Download bool
Offline bool
TrustedHosts []string
RemoteAuth taskfile.HostHeaders
RemoteAuth taskfile.HeadersByHost
Timeout time.Duration
CacheExpiryDuration time.Duration
RemoteCacheDir string
@@ -281,12 +281,12 @@ func (o *trustedHostsOption) ApplyToExecutor(e *Executor) {
// WithRemoteAuth configures the [Executor] with the HTTP headers to send when
// fetching a remote Taskfile, keyed by host.
func WithRemoteAuth(remoteAuth taskfile.HostHeaders) ExecutorOption {
func WithRemoteAuth(remoteAuth taskfile.HeadersByHost) ExecutorOption {
return &remoteAuthOption{remoteAuth}
}
type remoteAuthOption struct {
remoteAuth taskfile.HostHeaders
remoteAuth taskfile.HeadersByHost
}
func (o *remoteAuthOption) ApplyToExecutor(e *Executor) {

View File

@@ -80,7 +80,7 @@ var (
Download bool
Offline bool
TrustedHosts []string
RemoteAuth taskfile.HostHeaders
RemoteAuth taskfile.HeadersByHost
ClearCache bool
Timeout time.Duration
CacheExpiryDuration time.Duration
@@ -318,11 +318,11 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
// remoteAuth flattens the configured entries into a lookup by host, the last
// entry winning as it does when configuration files are merged.
func remoteAuth(config *taskrcast.TaskRC) taskfile.HostHeaders {
func remoteAuth(config *taskrcast.TaskRC) taskfile.HeadersByHost {
if config == nil || len(config.Remote.Auth) == 0 {
return nil
}
byHost := make(taskfile.HostHeaders, len(config.Remote.Auth))
byHost := make(taskfile.HeadersByHost, len(config.Remote.Auth))
for _, auth := range config.Remote.Auth {
byHost[auth.Host] = auth.Headers
}

View File

@@ -12,9 +12,9 @@ import (
"github.com/go-task/task/v3/internal/templater"
)
// HostHeaders maps a host to the HTTP headers to send when fetching a remote
// HeadersByHost maps a host to the HTTP headers to send when fetching a remote
// Taskfile from it. Values are templated, but no variables are available.
type HostHeaders map[string]map[string]string
type HeadersByHost map[string]map[string]string
type authTransport struct {
base http.RoundTripper
@@ -38,7 +38,7 @@ func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// authenticatedClient resolves on each read, not at build time, so a cached
// run needs no credentials.
func (node *HTTPNode) authenticatedClient() (*http.Client, error) {
headers, err := resolveAuthHeaders(node.authHeaders, node.url.Host)
headers, err := resolveAuthHeaders(node.authHeadersByHost, node.url.Host)
if err != nil {
return nil, err
}
@@ -61,9 +61,9 @@ func withAuthHeaders(client *http.Client, host string, headers map[string]string
}
// resolveAuthHeaders returns the expanded headers for host, or nil if none.
func resolveAuthHeaders(hostHeaders HostHeaders, host string) (map[string]string, error) {
func resolveAuthHeaders(headersByHost HeadersByHost, host string) (map[string]string, error) {
var headers map[string]string
for pattern, patternHeaders := range hostHeaders {
for pattern, patternHeaders := range headersByHost {
if hostMatches(pattern, host) {
headers = patternHeaders
break

View File

@@ -12,98 +12,98 @@ import (
func TestResolveAuthHeaders(t *testing.T) { //nolint:paralleltest // t.Setenv cannot be used in parallel tests
tests := []struct {
name string
hostHeaders HostHeaders
host string
env map[string]string
want map[string]string
wantErr string
name string
headersByHost HeadersByHost
host string
env map[string]string
want map[string]string
wantErr string
}{
{
name: "no configuration",
hostHeaders: nil,
host: "gitlab.com",
name: "no configuration",
headersByHost: nil,
host: "gitlab.com",
},
{
name: "host does not match",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "token"}},
host: "example.com",
name: "host does not match",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "token"}},
host: "example.com",
},
{
name: "port is part of the host",
hostHeaders: HostHeaders{"example.com": {"PRIVATE-TOKEN": "token"}},
host: "example.com:8080",
name: "port is part of the host",
headersByHost: HeadersByHost{"example.com": {"PRIVATE-TOKEN": "token"}},
host: "example.com:8080",
},
{
name: "literal value",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "token"}},
host: "gitlab.com",
want: map[string]string{"PRIVATE-TOKEN": "token"},
name: "literal value",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "token"}},
host: "gitlab.com",
want: map[string]string{"PRIVATE-TOKEN": "token"},
},
{
name: "environment variable",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"}}`}}, //nolint:gosec // an env var reference, not a credential
host: "gitlab.com",
env: map[string]string{"TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"PRIVATE-TOKEN": "s3cret"},
name: "environment variable",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"}}`}}, //nolint:gosec // an env var reference, not a credential
host: "gitlab.com",
env: map[string]string{"TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"PRIVATE-TOKEN": "s3cret"},
},
{
name: "environment variable inside a longer value",
hostHeaders: HostHeaders{"gitlab.com": {"Authorization": `Bearer {{env "TASK_TEST_TOKEN"}}`}},
host: "gitlab.com",
env: map[string]string{"TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"Authorization": "Bearer s3cret"},
name: "environment variable inside a longer value",
headersByHost: HeadersByHost{"gitlab.com": {"Authorization": `Bearer {{env "TASK_TEST_TOKEN"}}`}},
host: "gitlab.com",
env: map[string]string{"TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"Authorization": "Bearer s3cret"},
},
{
name: "undefined environment variable expands to nothing",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_UNSET"}}`}}, //nolint:gosec // an env var reference, not a credential
host: "gitlab.com",
want: map[string]string{"PRIVATE-TOKEN": ""},
name: "undefined environment variable expands to nothing",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_UNSET"}}`}}, //nolint:gosec // an env var reference, not a credential
host: "gitlab.com",
want: map[string]string{"PRIVATE-TOKEN": ""},
},
{
name: "functions compose, so Basic auth needs no manual base64",
hostHeaders: HostHeaders{"gitlab.com": {"Authorization": `Basic {{ printf "%s:%s" (env "TASK_TEST_USER") (env "TASK_TEST_TOKEN") | b64enc }}`}},
host: "gitlab.com",
env: map[string]string{"TASK_TEST_USER": "alice", "TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"Authorization": "Basic YWxpY2U6czNjcmV0"},
name: "functions compose, so Basic auth needs no manual base64",
headersByHost: HeadersByHost{"gitlab.com": {"Authorization": `Basic {{ printf "%s:%s" (env "TASK_TEST_USER") (env "TASK_TEST_TOKEN") | b64enc }}`}},
host: "gitlab.com",
env: map[string]string{"TASK_TEST_USER": "alice", "TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"Authorization": "Basic YWxpY2U6czNjcmV0"},
},
{
// The .taskrc is read before any Taskfile, so no variable exists.
name: "a variable reference resolves to nothing",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "{{.TASK_TEST_TOKEN}}"}}, //nolint:gosec // a template, not a credential
host: "gitlab.com",
env: map[string]string{"TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"PRIVATE-TOKEN": ""},
name: "a variable reference resolves to nothing",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "{{.TASK_TEST_TOKEN}}"}}, //nolint:gosec // a template, not a credential
host: "gitlab.com",
env: map[string]string{"TASK_TEST_TOKEN": "s3cret"},
want: map[string]string{"PRIVATE-TOKEN": ""},
},
{
name: "a literal value is left untouched",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "p$ssw0rd"}}, //nolint:gosec // a test fixture
host: "gitlab.com",
want: map[string]string{"PRIVATE-TOKEN": "p$ssw0rd"},
name: "a literal value is left untouched",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "p$ssw0rd"}}, //nolint:gosec // a test fixture
host: "gitlab.com",
want: map[string]string{"PRIVATE-TOKEN": "p$ssw0rd"},
},
{
name: "malformed template",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"`}}, //nolint:gosec // a template, not a credential
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": template: :1: unclosed action`,
name: "malformed template",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"`}}, //nolint:gosec // a template, not a credential
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": template: :1: unclosed action`,
},
{
name: "header name with a space",
hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE TOKEN": "token"}},
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": invalid header name "PRIVATE TOKEN"`,
name: "header name with a space",
headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE TOKEN": "token"}},
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": invalid header name "PRIVATE TOKEN"`,
},
{
name: "header name outside the HTTP token grammar",
hostHeaders: HostHeaders{"gitlab.com": {"X-Foo(bar)": "token"}},
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": invalid header name "X-Foo(bar)"`,
name: "header name outside the HTTP token grammar",
headersByHost: HeadersByHost{"gitlab.com": {"X-Foo(bar)": "token"}},
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": invalid header name "X-Foo(bar)"`,
},
{
name: "empty header name",
hostHeaders: HostHeaders{"gitlab.com": {"": "token"}},
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": invalid header name ""`,
name: "empty header name",
headersByHost: HeadersByHost{"gitlab.com": {"": "token"}},
host: "gitlab.com",
wantErr: `remote auth for host "gitlab.com": invalid header name ""`,
},
}
@@ -112,7 +112,7 @@ func TestResolveAuthHeaders(t *testing.T) { //nolint:paralleltest // t.Setenv ca
for name, value := range test.env {
t.Setenv(name, value)
}
headers, err := resolveAuthHeaders(test.hostHeaders, test.host)
headers, err := resolveAuthHeaders(test.headersByHost, test.host)
if test.wantErr != "" {
require.EqualError(t, err, test.wantErr)
return
@@ -178,7 +178,7 @@ func TestHTTPNodeAuthHeaders(t *testing.T) { //nolint:paralleltest // t.Setenv c
t.Setenv("TASK_TEST_TOKEN", "s3cret")
node, err := NewHTTPNode(srv.URL+"/Taskfile.yml", "", true,
WithAuthHeaders(HostHeaders{
WithAuthHeaders(HeadersByHost{
mustHost(t, srv.URL): {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"}}`}, //nolint:gosec // an env var reference, not a credential
}),
)
@@ -208,7 +208,7 @@ func TestHTTPNodeAuthHeadersNotSentOnRedirect(t *testing.T) {
defer srv.Close()
node, err := NewHTTPNode(srv.URL+"/Taskfile.yml", "", true,
WithAuthHeaders(HostHeaders{
WithAuthHeaders(HeadersByHost{
mustHost(t, srv.URL): {"PRIVATE-TOKEN": "s3cret"},
}),
)
@@ -226,7 +226,7 @@ func TestHTTPNodeAuthHeadersNotSentOnRedirect(t *testing.T) {
// cached and offline runs do not require them.
func TestHTTPNodeAuthHeadersResolvedLazily(t *testing.T) { //nolint:paralleltest // t.Setenv cannot be used in parallel tests
node, err := NewHTTPNode("https://gitlab.com/Taskfile.yml", "", false,
WithAuthHeaders(HostHeaders{
WithAuthHeaders(HeadersByHost{
"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_LAZY"}}`}, //nolint:gosec // an env var reference, not a credential
}),
)
@@ -235,7 +235,7 @@ func TestHTTPNodeAuthHeadersResolvedLazily(t *testing.T) { //nolint:paralleltest
// Defined only after the node was built: the value must still be picked up.
t.Setenv("TASK_TEST_LAZY", "s3cret")
headers, err := resolveAuthHeaders(node.authHeaders, node.url.Host)
headers, err := resolveAuthHeaders(node.authHeadersByHost, node.url.Host)
require.NoError(t, err)
assert.Equal(t, map[string]string{"PRIVATE-TOKEN": "s3cret"}, headers)
}

View File

@@ -7,13 +7,13 @@ type (
// designed to be embedded in other node types so that this boilerplate code
// does not need to be repeated.
baseNode struct {
parent Node
dir string
checksum string
caCert string
cert string
certKey string
authHeaders HostHeaders
parent Node
dir string
checksum string
caCert string
cert string
certKey string
authHeadersByHost HeadersByHost
}
)
@@ -78,8 +78,8 @@ func WithCertKey(certKey string) NodeOption {
}
// WithAuthHeaders sets the HTTP headers to send, keyed by host.
func WithAuthHeaders(authHeaders HostHeaders) NodeOption {
func WithAuthHeaders(authHeadersByHost HeadersByHost) NodeOption {
return func(node *baseNode) {
node.authHeaders = authHeaders
node.authHeadersByHost = authHeadersByHost
}
}

View File

@@ -51,7 +51,7 @@ type (
caCert string
cert string
certKey string
authHeaders HostHeaders
authHeadersByHost HeadersByHost
debugFunc DebugFunc
promptFunc PromptFunc
promptMutex sync.Mutex
@@ -244,16 +244,16 @@ func (o *readerCertKeyOption) ApplyToReader(r *Reader) {
}
// WithReaderAuthHeaders sets the HTTP headers to send to each configured host.
func WithReaderAuthHeaders(authHeaders HostHeaders) ReaderOption {
return &readerAuthHeadersOption{authHeaders: authHeaders}
func WithReaderAuthHeaders(authHeadersByHost HeadersByHost) ReaderOption {
return &readerAuthHeadersOption{authHeadersByHost: authHeadersByHost}
}
type readerAuthHeadersOption struct {
authHeaders HostHeaders
authHeadersByHost HeadersByHost
}
func (o *readerAuthHeadersOption) ApplyToReader(r *Reader) {
r.authHeaders = o.authHeaders
r.authHeadersByHost = o.authHeadersByHost
}
// Read will read the Taskfile defined by the [Reader]'s [Node] and recurse
@@ -371,7 +371,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
WithCACert(r.caCert),
WithCert(r.cert),
WithCertKey(r.certKey),
WithAuthHeaders(r.authHeaders),
WithAuthHeaders(r.authHeadersByHost),
)
if err != nil {
if include.Optional {