Skip to content

Commit d00dc82

Browse files
authored
🔐 Validate PAT tokens and improve RunOptions (#16169)
* validate PAT tokens are fine grained and remove --use-local-secrets * add RunOptions for simplicity * fix up secret collection * fix up secret collection * fix up secret collection * fix up secret collection
1 parent cf0ad8c commit d00dc82

25 files changed

+1015
-631
lines changed

actions/setup/sh/validate_multi_secret.sh

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,74 @@ if [ "$all_empty" = true ]; then
8787
exit 1
8888
fi
8989

90+
# Validate COPILOT_GITHUB_TOKEN is a fine-grained PAT if it's one of the secrets being validated
91+
for secret_name in "${SECRET_NAMES[@]}"; do
92+
if [ "$secret_name" = "COPILOT_GITHUB_TOKEN" ]; then
93+
secret_value="${!secret_name}"
94+
if [ -n "$secret_value" ]; then
95+
# Check token type by prefix
96+
# github_pat_ = Fine-grained PAT (valid)
97+
# ghp_ = Classic PAT (invalid)
98+
# gho_ = OAuth token (invalid)
99+
if [[ "$secret_value" == ghp_* ]]; then
100+
{
101+
echo "❌ Error: COPILOT_GITHUB_TOKEN is a classic Personal Access Token (ghp_...)"
102+
echo "Classic PATs are not supported for GitHub Copilot."
103+
echo "Please create a fine-grained PAT (github_pat_...) at:"
104+
echo "https://github.com/settings/personal-access-tokens/new"
105+
echo ""
106+
echo "Configure the token with:"
107+
echo "• Resource owner: Your personal account"
108+
echo "• Repository access: \"Public repositories\""
109+
echo "• Account permissions → Copilot Requests: Read-only"
110+
} >> "$GITHUB_STEP_SUMMARY"
111+
112+
echo "Error: COPILOT_GITHUB_TOKEN is a classic Personal Access Token (ghp_...)" >&2
113+
echo "Classic PATs are not supported for GitHub Copilot." >&2
114+
echo "Please create a fine-grained PAT (github_pat_...) at: https://github.com/settings/personal-access-tokens/new" >&2
115+
116+
if [ -n "$GITHUB_OUTPUT" ]; then
117+
echo "verification_result=failed" >> "$GITHUB_OUTPUT"
118+
fi
119+
exit 1
120+
elif [[ "$secret_value" == gho_* ]]; then
121+
{
122+
echo "❌ Error: COPILOT_GITHUB_TOKEN is an OAuth token (gho_...)"
123+
echo "OAuth tokens are not supported for GitHub Copilot."
124+
echo "Please create a fine-grained PAT (github_pat_...) at:"
125+
echo "https://github.com/settings/personal-access-tokens/new"
126+
} >> "$GITHUB_STEP_SUMMARY"
127+
128+
echo "Error: COPILOT_GITHUB_TOKEN is an OAuth token (gho_...)" >&2
129+
echo "OAuth tokens are not supported for GitHub Copilot." >&2
130+
echo "Please create a fine-grained PAT (github_pat_...) at: https://github.com/settings/personal-access-tokens/new" >&2
131+
132+
if [ -n "$GITHUB_OUTPUT" ]; then
133+
echo "verification_result=failed" >> "$GITHUB_OUTPUT"
134+
fi
135+
exit 1
136+
elif [[ "$secret_value" != github_pat_* ]]; then
137+
{
138+
echo "❌ Error: COPILOT_GITHUB_TOKEN has an unrecognized format"
139+
echo "GitHub Copilot requires a fine-grained PAT (starting with 'github_pat_')."
140+
echo "Please create a fine-grained PAT at:"
141+
echo "https://github.com/settings/personal-access-tokens/new"
142+
} >> "$GITHUB_STEP_SUMMARY"
143+
144+
echo "Error: COPILOT_GITHUB_TOKEN has an unrecognized format" >&2
145+
echo "GitHub Copilot requires a fine-grained PAT (starting with 'github_pat_')." >&2
146+
echo "Please create a fine-grained PAT at: https://github.com/settings/personal-access-tokens/new" >&2
147+
148+
if [ -n "$GITHUB_OUTPUT" ]; then
149+
echo "verification_result=failed" >> "$GITHUB_OUTPUT"
150+
fi
151+
exit 1
152+
fi
153+
fi
154+
break
155+
fi
156+
done
157+
90158
# Log success in collapsible section
91159
echo "<details>"
92160
echo "<summary>Agent Environment Validation</summary>"
@@ -97,7 +165,12 @@ echo ""
97165
first_secret="${SECRET_NAMES[0]}"
98166
first_value="${!first_secret}"
99167
if [ -n "$first_value" ]; then
100-
echo "$first_secret: Configured"
168+
# Show extra info for COPILOT_GITHUB_TOKEN indicating fine-grained PAT
169+
if [ "$first_secret" = "COPILOT_GITHUB_TOKEN" ]; then
170+
echo "$first_secret: Configured (fine-grained PAT)"
171+
else
172+
echo "$first_secret: Configured"
173+
fi
101174
# Middle secrets use elif (if there are more than 2 secrets)
102175
elif [ "${#SECRET_NAMES[@]}" -gt 2 ]; then
103176
found=false

cmd/gh-aw/main.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,6 @@ Examples:
368368
repoOverride, _ := cmd.Flags().GetString("repo")
369369
refOverride, _ := cmd.Flags().GetString("ref")
370370
autoMergePRs, _ := cmd.Flags().GetBool("auto-merge-prs")
371-
pushSecrets, _ := cmd.Flags().GetBool("use-local-secrets")
372371
inputs, _ := cmd.Flags().GetStringArray("raw-field")
373372
push, _ := cmd.Flags().GetBool("push")
374373
dryRun, _ := cmd.Flags().GetBool("dry-run")
@@ -395,10 +394,21 @@ Examples:
395394
return fmt.Errorf("workflow inputs cannot be specified in interactive mode (they will be collected interactively)")
396395
}
397396

398-
return cli.RunWorkflowInteractively(cmd.Context(), verboseFlag, repoOverride, refOverride, autoMergePRs, pushSecrets, push, engineOverride, dryRun)
397+
return cli.RunWorkflowInteractively(cmd.Context(), verboseFlag, repoOverride, refOverride, autoMergePRs, push, engineOverride, dryRun)
399398
}
400399

401-
return cli.RunWorkflowsOnGitHub(cmd.Context(), args, repeatCount, enable, engineOverride, repoOverride, refOverride, autoMergePRs, pushSecrets, push, inputs, verboseFlag, dryRun)
400+
return cli.RunWorkflowsOnGitHub(cmd.Context(), args, cli.RunOptions{
401+
RepeatCount: repeatCount,
402+
Enable: enable,
403+
EngineOverride: engineOverride,
404+
RepoOverride: repoOverride,
405+
RefOverride: refOverride,
406+
AutoMergePRs: autoMergePRs,
407+
Push: push,
408+
Inputs: inputs,
409+
Verbose: verboseFlag,
410+
DryRun: dryRun,
411+
})
402412
},
403413
}
404414

@@ -577,7 +587,6 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
577587
runCmd.Flags().StringP("repo", "r", "", "Target repository (owner/repo format). Defaults to current repository")
578588
runCmd.Flags().String("ref", "", "Branch or tag name to run the workflow on (default: current branch)")
579589
runCmd.Flags().Bool("auto-merge-prs", false, "Auto-merge any pull requests created during the workflow execution")
580-
runCmd.Flags().Bool("use-local-secrets", false, "Use local environment API key secrets for workflow execution (pushes and cleans up secrets in repository)")
581590
runCmd.Flags().StringArrayP("raw-field", "F", []string{}, "Add a string parameter in key=value format (can be used multiple times)")
582591
runCmd.Flags().Bool("push", false, "Commit and push workflow files (including transitive imports) before running")
583592
runCmd.Flags().Bool("dry-run", false, "Validate workflow without actually triggering execution on GitHub Actions")

docs/interactive-run-mode.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ All standard `run` command flags work in interactive mode:
5252
- `--ref branch` - Run on a specific branch
5353
- `--engine copilot` - Override AI engine
5454
- `--auto-merge-prs` - Auto-merge created PRs
55-
- `--use-local-secrets` - Use local secrets
5655
- `--push` - Push changes before running
5756

5857
## Limitations

docs/src/content/docs/patterns/trialops.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ jobs:
267267
|-------|----------|
268268
| `workflow not found` | Use correct format: `owner/repo/workflow-name`, `owner/repo/.github/workflows/workflow.md`, or `./local-workflow.md` |
269269
| `workflow_dispatch not supported` | Add `workflow_dispatch:` to workflow frontmatter `on:` section |
270-
| `authentication failed` | Set API keys: `COPILOT_GITHUB_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`. Use `--use-local-secrets` to push to trial repo |
270+
| `authentication failed` | Set API keys: `COPILOT_GITHUB_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`. Trial automatically prompts for missing secrets and uploads them to the trial repo |
271271
| `failed to create trial repository` | Check `gh auth status`, verify quota with `gh api user \| jq .plan`, try explicit `--host-repo name` |
272272
| `execution timed out` | Increase with `--timeout 60` (minutes, default: 30) |
273273
| No issues/PRs created | Configure `safe-outputs` in workflow frontmatter, check Actions logs for errors |

docs/src/content/docs/setup/cli.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,14 @@ Test workflows in temporary private repositories (default) or run directly in sp
233233

234234
```bash wrap
235235
gh aw trial githubnext/agentics/ci-doctor # Test remote workflow
236-
gh aw trial ./workflow.md --use-local-secrets # Test with local API keys
237236
gh aw trial ./workflow.md --logical-repo owner/repo # Act as different repo
238237
gh aw trial ./workflow.md --repo owner/repo # Run directly in repository
239238
gh aw trial ./workflow.md --dry-run # Preview without executing
240239
```
241240

242-
**Options:** `-e`, `--engine`, `--auto-merge-prs`, `--repeat`, `--delete-host-repo-after`, `--use-local-secrets`, `--logical-repo`, `--clone-repo`, `--trigger-context`, `--repo`, `--dry-run`
241+
**Options:** `-e`, `--engine`, `--auto-merge-prs`, `--repeat`, `--delete-host-repo-after`, `--logical-repo`, `--clone-repo`, `--trigger-context`, `--repo`, `--dry-run`
242+
243+
**Secret Handling:** API keys required for the selected engine are automatically checked. If missing from the target repository, they are prompted for interactively and uploaded.
243244

244245
#### `run`
245246

@@ -249,12 +250,11 @@ Execute workflows immediately in GitHub Actions. Displays workflow URL for track
249250
gh aw run workflow # Run workflow
250251
gh aw run workflow1 workflow2 # Run multiple workflows
251252
gh aw run workflow --repeat 3 # Repeat 3 times
252-
gh aw run workflow --use-local-secrets # Use local API keys
253253
gh aw run workflow --push # Auto-commit, push, and dispatch workflow
254254
gh aw run workflow --push --ref main # Push to specific branch
255255
```
256256

257-
**Options:** `--repeat`, `--use-local-secrets`, `--push` (see [--push flag](#the---push-flag)), `--ref`
257+
**Options:** `--repeat`, `--push` (see [--push flag](#the---push-flag)), `--ref`, `--auto-merge-prs`, `--enable-if-needed`
258258

259259
When `--push` is used, automatically recompiles outdated `.lock.yml` files, stages all transitive imports, and triggers workflow run after successful push. Without `--push`, warnings are displayed for missing or outdated lock files.
260260

pkg/cli/add_interactive_engine.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/charmbracelet/huh"
88
"github.com/github/gh-aw/pkg/console"
99
"github.com/github/gh-aw/pkg/constants"
10-
"github.com/github/gh-aw/pkg/parser"
10+
"github.com/github/gh-aw/pkg/stringutil"
1111
)
1212

1313
// selectAIEngineAndKey prompts the user to select an AI engine and provide API key
@@ -63,10 +63,6 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error {
6363
break
6464
}
6565
}
66-
// Priority 3: Check if user likely has Copilot (default)
67-
if token, err := parser.GetGitHubToken(); err == nil && token != "" {
68-
defaultEngine = string(constants.CopilotEngine)
69-
}
7066
}
7167
}
7268

@@ -150,12 +146,20 @@ func (c *AddInteractiveConfig) collectCopilotPAT() error {
150146
// Check if COPILOT_GITHUB_TOKEN is already in environment
151147
existingToken := os.Getenv("COPILOT_GITHUB_TOKEN")
152148
if existingToken != "" {
153-
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found COPILOT_GITHUB_TOKEN in environment"))
154-
return nil
149+
// Validate the existing token is a fine-grained PAT
150+
if err := stringutil.ValidateCopilotPAT(existingToken); err != nil {
151+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("COPILOT_GITHUB_TOKEN in environment is not a fine-grained PAT: %s", stringutil.GetPATTypeDescription(existingToken))))
152+
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
153+
// Continue to prompt for a new token
154+
} else {
155+
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found valid fine-grained COPILOT_GITHUB_TOKEN in environment"))
156+
return nil
157+
}
155158
}
156159

157160
fmt.Fprintln(os.Stderr, "")
158-
fmt.Fprintln(os.Stderr, "GitHub Copilot requires a Personal Access Token (PAT) with Copilot permissions.")
161+
fmt.Fprintln(os.Stderr, "GitHub Copilot requires a fine-grained Personal Access Token (PAT) with Copilot permissions.")
162+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Classic PATs (ghp_...) are not supported. You must use a fine-grained PAT (github_pat_...)."))
159163
fmt.Fprintln(os.Stderr, "")
160164
fmt.Fprintln(os.Stderr, "Please create a token at:")
161165
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" https://github.com/settings/personal-access-tokens/new"))
@@ -172,15 +176,16 @@ func (c *AddInteractiveConfig) collectCopilotPAT() error {
172176
form := huh.NewForm(
173177
huh.NewGroup(
174178
huh.NewInput().
175-
Title("After creating, please paste your Copilot PAT:").
176-
Description("The token will be stored securely as a repository secret").
179+
Title("After creating, please paste your fine-grained Copilot PAT:").
180+
Description("Must start with 'github_pat_'. Classic PATs (ghp_...) are not supported.").
177181
EchoMode(huh.EchoModePassword).
178182
Value(&token).
179183
Validate(func(s string) error {
180184
if len(s) < 10 {
181185
return fmt.Errorf("token appears to be too short")
182186
}
183-
return nil
187+
// Validate it's a fine-grained PAT
188+
return stringutil.ValidateCopilotPAT(s)
184189
}),
185190
),
186191
).WithAccessible(console.IsAccessibleMode())
@@ -191,7 +196,7 @@ func (c *AddInteractiveConfig) collectCopilotPAT() error {
191196

192197
// Store in environment for later use
193198
_ = os.Setenv("COPILOT_GITHUB_TOKEN", token)
194-
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Copilot token received"))
199+
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Valid fine-grained Copilot token received"))
195200

196201
return nil
197202
}

pkg/cli/add_interactive_workflow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error
126126
if parsed != nil {
127127
fmt.Fprintln(os.Stderr, "")
128128

129-
if err := RunSpecificWorkflowInteractively(ctx, parsed.WorkflowName, c.Verbose, c.EngineOverride, c.RepoOverride, "", false, false, false, false); err != nil {
129+
if err := RunSpecificWorkflowInteractively(ctx, parsed.WorkflowName, c.Verbose, c.EngineOverride, c.RepoOverride, "", false, false, false); err != nil {
130130
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to run workflow: %v", err)))
131131
c.showFinalInstructions()
132132
return nil

pkg/cli/commands_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -339,39 +339,39 @@ func TestDisableWorkflowsFailureScenarios(t *testing.T) {
339339

340340
func TestRunWorkflowOnGitHub(t *testing.T) {
341341
// Test with empty workflow name
342-
err := RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, false, []string{}, false, false)
342+
err := RunWorkflowOnGitHub(context.Background(), "", RunOptions{})
343343
if err == nil {
344344
t.Error("RunWorkflowOnGitHub should return error for empty workflow name")
345345
}
346346

347347
// Test with nonexistent workflow (this will fail but gracefully)
348-
err = RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", false, "", "", "", false, false, false, false, []string{}, false, false)
348+
err = RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", RunOptions{})
349349
if err == nil {
350350
t.Error("RunWorkflowOnGitHub should return error for non-existent workflow")
351351
}
352352
}
353353

354354
func TestRunWorkflowsOnGitHub(t *testing.T) {
355355
// Test with empty workflow list
356-
err := RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, false, []string{}, false, false)
356+
err := RunWorkflowsOnGitHub(context.Background(), []string{}, RunOptions{})
357357
if err == nil {
358358
t.Error("RunWorkflowsOnGitHub should return error for empty workflow list")
359359
}
360360

361361
// Test with workflow list containing empty name
362-
err = RunWorkflowsOnGitHub(context.Background(), []string{"valid-workflow", ""}, 0, false, "", "", "", false, false, false, []string{}, false, false)
362+
err = RunWorkflowsOnGitHub(context.Background(), []string{"valid-workflow", ""}, RunOptions{})
363363
if err == nil {
364364
t.Error("RunWorkflowsOnGitHub should return error for workflow list containing empty name")
365365
}
366366

367367
// Test with nonexistent workflows (this will fail but gracefully)
368-
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow1", "nonexistent-workflow2"}, 0, false, "", "", "", false, false, false, []string{}, false, false)
368+
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow1", "nonexistent-workflow2"}, RunOptions{})
369369
if err == nil {
370370
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflows")
371371
}
372372

373373
// Test with negative repeat seconds (should work as 0)
374-
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow"}, -1, false, "", "", "", false, false, false, []string{}, false, false)
374+
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow"}, RunOptions{RepeatCount: -1})
375375
if err == nil {
376376
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflow regardless of repeat value")
377377
}
@@ -482,10 +482,10 @@ Test workflow for command existence.`
482482
{func() error { return EnableWorkflows("nonexistent") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
483483
{func() error { return DisableWorkflows("nonexistent") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
484484
{func() error {
485-
return RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, false, []string{}, false, false)
485+
return RunWorkflowOnGitHub(context.Background(), "", RunOptions{})
486486
}, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
487487
{func() error {
488-
return RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, false, []string{}, false, false)
488+
return RunWorkflowsOnGitHub(context.Background(), []string{}, RunOptions{})
489489
}, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
490490
}
491491

@@ -1134,13 +1134,13 @@ func TestCalculateTimeRemaining(t *testing.T) {
11341134

11351135
func TestRunWorkflowOnGitHubWithEnable(t *testing.T) {
11361136
// Test with enable flag enabled (should not error for basic validation)
1137-
err := RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", true, "", "", "", false, false, false, false, []string{}, false, false)
1137+
err := RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", RunOptions{Enable: true})
11381138
if err == nil {
11391139
t.Error("RunWorkflowOnGitHub should return error for non-existent workflow even with enable flag")
11401140
}
11411141

11421142
// Test with empty workflow name and enable flag
1143-
err = RunWorkflowOnGitHub(context.Background(), "", true, "", "", "", false, false, false, false, []string{}, false, false)
1143+
err = RunWorkflowOnGitHub(context.Background(), "", RunOptions{Enable: true})
11441144
if err == nil {
11451145
t.Error("RunWorkflowOnGitHub should return error for empty workflow name regardless of enable flag")
11461146
}

pkg/cli/context_cancellation_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestRunWorkflowOnGitHubWithCancellation(t *testing.T) {
1717
cancel()
1818

1919
// Try to run a workflow with a cancelled context
20-
err := RunWorkflowOnGitHub(ctx, "test-workflow", false, "", "", "", false, false, false, false, []string{}, false, false)
20+
err := RunWorkflowOnGitHub(ctx, "test-workflow", RunOptions{})
2121

2222
// Should return context.Canceled error
2323
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
@@ -30,7 +30,7 @@ func TestRunWorkflowsOnGitHubWithCancellation(t *testing.T) {
3030
cancel()
3131

3232
// Try to run workflows with a cancelled context
33-
err := RunWorkflowsOnGitHub(ctx, []string{"test-workflow"}, 0, false, "", "", "", false, false, false, []string{}, false, false)
33+
err := RunWorkflowsOnGitHub(ctx, []string{"test-workflow"}, RunOptions{})
3434

3535
// Should return context.Canceled error
3636
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
@@ -98,7 +98,7 @@ func TestRunWorkflowsOnGitHubCancellationDuringExecution(t *testing.T) {
9898
// Try to run multiple workflows that would take a long time
9999
// This should fail validation before timeout, but if it gets past validation,
100100
// it should respect the context cancellation
101-
err := RunWorkflowsOnGitHub(ctx, []string{"nonexistent-workflow-1", "nonexistent-workflow-2"}, 0, false, "", "", "", false, false, false, []string{}, false, false)
101+
err := RunWorkflowsOnGitHub(ctx, []string{"nonexistent-workflow-1", "nonexistent-workflow-2"}, RunOptions{})
102102

103103
// Should return an error (either validation error or context error)
104104
assert.Error(t, err, "Should return an error")

0 commit comments

Comments
 (0)