Skip to content

Commit 04671f1

Browse files
authored
Support token-cmd on Windows (#131)
execValue now runs the command via %COMSPEC% /c on Windows instead of returning an error, so token-cmd works on all platforms. Tests updated to use portable commands (exit 1 instead of false, %FORGE_DOMAIN% for the env-var expansion test on Windows). Fixes the windows-latest CI job which has been failing since #124.
1 parent 6a0dcf4 commit 04671f1

4 files changed

Lines changed: 26 additions & 14 deletions

File tree

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ token = abc123
8080

8181
### Token commands
8282

83-
Instead of a literal `token`, use `token-cmd` to specify a shell command (not supported on Windows).
84-
The command is executed via `sh -c` each time forge needs the token and its
85-
stdout is used as the value. This lets you fetch secrets from a password manager
86-
instead of storing them in plain text:
83+
Instead of a literal `token`, use `token-cmd` to specify a shell command.
84+
The command is executed via `sh -c` (Unix) or `%COMSPEC% /c` (Windows) each
85+
time forge needs the token and its stdout is used as the value. This lets you
86+
fetch secrets from a password manager instead of storing them in plain text:
8787

8888
```ini
8989
[github.com]
@@ -96,8 +96,9 @@ token-cmd = pass show forge/gitlab
9696
token-cmd = rbw get --raw myhostedgitlab | jq -r '.fields | map(select(.name == "token"))[0].value'
9797
```
9898

99-
The variable `FORGE_DOMAIN` is set to the domain name when the command runs,
100-
so a single command can serve multiple domains:
99+
The variable `FORGE_DOMAIN` is set to the domain name when the command runs
100+
(reference it as `$FORGE_DOMAIN` on Unix, `%FORGE_DOMAIN%` on Windows), so a
101+
single command can serve multiple domains:
101102

102103
```ini
103104
[github.com]

internal/cli/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func authLoginCmd() *cobra.Command {
8484

8585
cmd.Flags().StringVar(&domain, "domain", "", "Forge domain (e.g. github.com, gitea.example.com)")
8686
cmd.Flags().StringVar(&token, "token", "", "API token")
87-
cmd.Flags().StringVar(&tokenCmd, "token-cmd", "", "Shell command whose stdout is used as the token (Unix only)")
87+
cmd.Flags().StringVar(&tokenCmd, "token-cmd", "", "Shell command whose stdout is used as the token")
8888
cmd.Flags().StringVar(&forgeType, "type", "", "Forge type: github, gitlab, gitea, forgejo, bitbucket")
8989
cmd.MarkFlagsMutuallyExclusive("token", "token-cmd")
9090
return cmd

internal/config/config.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,28 @@ func parseGitProtocol(v string) (string, error) {
105105
}
106106
}
107107

108-
// execValue runs cmd via sh -c and returns its trimmed stdout.
108+
// execValue runs cmd via the platform shell (sh -c on Unix, %COMSPEC% /c on
109+
// Windows) and returns its trimmed stdout.
109110
// Shell features (pipes, quotes, substitutions) are supported.
110111
// FORGE_DOMAIN is set to domain in the command environment.
111112
// Stdin and stderr are wired to the terminal so interactive prompts
112113
// (e.g. pinentry, rbw unlock) work and error output is visible directly.
113114
func execValue(cmd, domain string) (string, error) {
114-
if runtime.GOOS == goosWindows {
115-
return "", fmt.Errorf("token-cmd is not supported on Windows")
116-
}
117115
cmd = strings.TrimSpace(cmd)
118116
if cmd == "" {
119117
return "", fmt.Errorf("empty command")
120118
}
121119
var stdout strings.Builder
122-
c := exec.Command("sh", "-c", cmd)
120+
var c *exec.Cmd
121+
if runtime.GOOS == goosWindows {
122+
shell := os.Getenv("COMSPEC")
123+
if shell == "" {
124+
shell = "cmd"
125+
}
126+
c = exec.Command(shell, "/c", cmd)
127+
} else {
128+
c = exec.Command("sh", "-c", cmd)
129+
}
123130
c.Env = append(os.Environ(), "FORGE_DOMAIN="+domain)
124131
c.Stdin = os.Stdin
125132
c.Stdout = &stdout

internal/config/config_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,10 +531,14 @@ token-cmd = rbw get github-token
531531
}
532532

533533
func TestLoadFileTokenCommandForgeDomain(t *testing.T) {
534+
tokenCmd := "echo $FORGE_DOMAIN"
535+
if runtime.GOOS == goosWindows {
536+
tokenCmd = "echo %FORGE_DOMAIN%"
537+
}
534538
dir := t.TempDir()
535539
path := filepath.Join(dir, "config")
536540
_ = os.WriteFile(path, []byte(`[gitlab.example.com]
537-
token-cmd = echo $FORGE_DOMAIN
541+
token-cmd = `+tokenCmd+`
538542
`), 0600)
539543

540544
cfg := &Config{Domains: make(map[string]DomainSection)}
@@ -555,7 +559,7 @@ func TestLoadFileTokenCommandFails(t *testing.T) {
555559
dir := t.TempDir()
556560
path := filepath.Join(dir, "config")
557561
_ = os.WriteFile(path, []byte(`[github.com]
558-
token-cmd = false
562+
token-cmd = exit 1
559563
`), 0600)
560564

561565
cfg := &Config{Domains: make(map[string]DomainSection)}

0 commit comments

Comments
 (0)