diff --git a/.github/skills/writing-acceptance-tests/SKILL.md b/.github/skills/writing-acceptance-tests/SKILL.md new file mode 100644 index 00000000000..c61c5eea597 --- /dev/null +++ b/.github/skills/writing-acceptance-tests/SKILL.md @@ -0,0 +1,131 @@ +--- +name: writing-acceptance-tests +description: Use when adding or changing GitHub CLI acceptance tests, txtar scripts, testdata groups, repository fixtures, or acceptance harness behavior. +--- + +# Writing Acceptance Tests + +Acceptance tests exercise `gh` against live GitHub resources. Minimize repository +creation and cloning without allowing concurrent scripts to interfere. + +Read `acceptance/README.md` and nearby scripts before editing. Test groups are +discovered from `acceptance/testdata//`; do not register them manually. + +## Choose one repository mode + +Every script must contain exactly one declaration: + +| Mode | Use when | +| --- | --- | +| `fixture-repo shared REPO` | One initialized private repository is enough and every operation can coexist with concurrent and accumulated state. | +| `fixture-repo isolated REPO` | One initialized private repository is enough, but the test needs clean state, changes repository-global state, or performs multiple coordinated Git ref updates. | +| `fixture-repo none` | No repository is needed, or the test needs multiple repositories, public visibility, special creation options, or repository lifecycle coverage. | + +Managed fixtures are initialized, have discussions enabled, and are deleted by +the harness. Scripts using `shared` or `isolated` must not run `gh repo create`. + +With `none`, create every required repository explicitly and immediately +register deferred cleanup. Use `defer gh repo delete --yes $ORG/$REPO` when the +repository should still exist under that name. Use `defer cleanup-repo $REPO` +when testing deletion or renaming; it is scoped to `$ORG` and treats an already +absent repository as success. + +Repositories created with an initial commit can remain busy after `gh repo +create` returns. Before renaming one, use `wait-for-repository-ready +$ORG/$REPO` to wait for its default-branch commit. GitHub can retain the +repository creation lock after that commit becomes readable, so keep the +`gh repo rename` command inline and allow a 10-second stabilization delay +between this readiness check and that command. + +## Shared fixture contract + +Scripts within a group run concurrently. A shared script must remain correct +when the repository already contains unrelated resources. + +- Never rename, transfer, archive, delete, or globally reconfigure the + repository. Do not toggle features, change its description, or mutate its + default branch. +- Give resources unique, reasonably short names using `$RANDOM_STRING`, adding + `$SCRIPT_NAME` only when useful. Width-constrained commands such as + `gh pr status` truncate long titles. +- Make commits unique across concurrent scripts. Unique branch names do not + affect commit IDs, so include the script's `$RANDOM_STRING` in the commit + contents or message when scripts could otherwise create the same tree from the + same parent. +- Consolidate related repository-scoped operations, such as merging pull + requests, into one `isolated` script. Unique refs do not prevent concurrent + Git pushes and merges in a shared repository from contending. +- GitHub normalizes some identifiers. Use `env2upper` for Actions variables and + secrets whose generated names are asserted later. +- Capture the created resource's URL or ID. Filter and paginate list operations; + never select the first, latest, or only result. +- Target the fixture with `--repo $ORG/$REPO` or `GH_REPO=$ORG/$REPO`. Clone only + when local Git behavior is part of the test. + +If any operation violates this contract, use `isolated`; do not weaken +assertions to make sharing appear safe. + +## API request budgets + +Scripts share token-wide API rate limits. Count requests across the whole test +process and combine compatible live assertions. Keep coverage for a narrowly +limited endpoint in one script so concurrent scripts cannot burst the limit. +For Code Search's 10 requests/minute bucket, use at most five HTTP requests in +the entire acceptance process, even when they run sequentially. This reserves +half the bucket for pagination, retries, and other token activity. Count the +requests made by each command, keep representative live coverage, and move +remaining variants to unit tests. + +Use bounded condition-based waits for asynchronously registered resources. +Fixed sleeps are both slower when registration is fast and unreliable when it +is slow. Use `wait-for-workflow` after pushing a new workflow definition, then +use `wait-for-run` before watching or inspecting a triggered workflow run. +After `gh workflow run`, `wait-for-run` captures the returned run URL when the +server provides one and falls back to polling for older servers. +On timeout, `wait-for-run` captures the pushed commit and remote ref, workflow +files, recent unfiltered runs, commit check suites, and an Actions API request +ID. Use that evidence to distinguish event ingestion, run indexing, filtering, +and push/setup failures before changing fixture isolation or timeout budgets. +Cancellation tests must use a self-contained workflow with a deliberately long +step so the run cannot finish before the cancellation request, plus a short job +timeout so a failed cancellation cannot run for the full step. After +registration, use `wait-for-run-status RUN_ID in_progress` before cancellation +because the cancellation endpoint can reject a run that is still starting. Do +not wait for GitHub to finish cancellation after the command confirms that the +request was submitted; that tests Actions' eventual behavior rather than the +CLI contract. Do not lengthen registration sleeps to compensate for a short or +externally dependent job. + +## Organization safety + +Every live mutation must resolve through `$ORG/$REPO`, a resource ID or URL +captured from something created under `$ORG`, or fixture cleanup scoped to +`GH_ACCEPTANCE_ORG`. Do not rely on ambient Git remotes or repository context. + +## Example + +```txtar +env2upper VAR_NAME=TESTSCRIPTS_${RANDOM_STRING} +fixture-repo shared REPO +env GH_REPO=$ORG/$REPO + +exec gh variable set $VAR_NAME --body value +exec gh variable get $VAR_NAME +stdout '^value$' +``` + +## Validate + +Run metadata checks without live credentials: + +```sh +go test -tags=acceptance \ + -run '^(TestSelectAcceptanceTestGroups|TestAcceptanceScriptsDeclareFixtureRepository|TestValidateFixtureRepositoryDeclaration|TestFixtureRepositoryManager)$' \ + ./acceptance +``` + +Run a changed live script with `GH_ACCEPTANCE_GROUP` and +`GH_ACCEPTANCE_SCRIPT`; see `acceptance/README.md` for the credential variables. +Use `-count=1` to bypass the Go test cache. Start with one script. If concurrency +is required to reproduce the behavior, pass only the contending scripts as a +comma-separated filter and repeat that focused set before widening the run. diff --git a/AGENTS.md b/AGENTS.md index ec098f3add2..28a87020b8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ contributors. They carry repository conventions, not optional suggestions. | Command wiring, flags, prompts, help, or output | [Command development](docs/command-development.md) | | Tests, fixtures, or generated test doubles | [Testing](docs/testing.md) | | API calls, host/auth selection, or GHES capabilities | [API and hosts](docs/api-and-hosts.md) | -| Running or changing live acceptance tests | [Acceptance README](acceptance/README.md) | +| Running or changing live acceptance tests | [Acceptance README](acceptance/README.md) and [writing-acceptance-tests skill](.github/skills/writing-acceptance-tests/SKILL.md) | | Finding source files | [Project layout](docs/project-layout.md) | | Toolchain or environment setup | [CONTRIBUTING](.github/CONTRIBUTING.md#building-the-project), [go.mod](go.mod), and the [Copilot setup workflow](.github/workflows/copilot-setup-steps.yml) where applicable | diff --git a/acceptance/README.md b/acceptance/README.md index a4743a308fb..2f41d5f73cf 100644 --- a/acceptance/README.md +++ b/acceptance/README.md @@ -24,6 +24,12 @@ The token to use for authenticating with the `GH_ACCEPTANCE_HOST`. This must alr It's recommended to create and use a Legacy PAT for this; Fine-Grained PATs do not offer all the necessary privileges required. You can use an OAuth token provided via `gh auth login --web` and can provide it to the acceptance tests via `GH_ACCEPTANCE_TOKEN=$(gh auth token --hostname )` but this can be a bit confusing and annoying if you `gh auth login` again without `-s` and lose the required scopes. +Managed fixture repositories reduce repository creation by sharing state where +tests can safely coexist. + +Acceptance test groups are discovered from the directories under `testdata`, so +adding a group does not require updating the test harness. + --- A full example invocation can be found below: @@ -32,12 +38,19 @@ A full example invocation can be found below: GH_ACCEPTANCE_HOST= GH_ACCEPTANCE_ORG= GH_ACCEPTANCE_TOKEN= go test -tags=acceptance ./acceptance ``` -While writing a new test, it can be useful to target that specific script by providing the `GH_ACCEPTANCE_SCRIPT` env var in combination with the `-run` flag, for example: +While writing a new test, target the smallest live surface that can reproduce +the behavior. Provide one or more comma-separated script names with +`GH_ACCEPTANCE_SCRIPT`, use `-run` to select their group, and use `-count=1` to +bypass Go's test cache: ``` -GH_ACCEPTANCE_SCRIPT=pr-view.txtar GH_ACCEPTANCE_HOST= GH_ACCEPTANCE_ORG= GH_ACCEPTANCE_TOKEN= go test -tags=acceptance -run ^TestPullRequests$ ./acceptance +GH_ACCEPTANCE_SCRIPT=pr-view.txtar GH_ACCEPTANCE_HOST= GH_ACCEPTANCE_ORG= GH_ACCEPTANCE_TOKEN= go test -tags=acceptance -count=1 -run '^TestAcceptance$/^pr$' ./acceptance ``` +Start with one script for a deterministic failure. If concurrency is part of +the failure, select only the scripts that exercise the contended resource and +repeat that focused set before widening to the complete group or suite. + #### Code Coverage To get code coverage, `go test` can be invoked with `coverpkg` and `coverprofile` like so: @@ -61,10 +74,105 @@ The following custom environment variables are made available to the scripts: * `HOME`: Set to the initial working directory. Required for `git` operations * `GH_CONFIG_DIR`: Set to the initial working directory. Required for `gh` operations +#### Script Metadata + +Every script must declare exactly one repository fixture mode: + +```txtar +fixture-repo shared REPO +fixture-repo isolated REPO +fixture-repo none +``` + +`shared` reuses one initialized private repository across all opting-in scripts +in the test process. Shared scripts must tolerate concurrent and accumulated +state: use unique resource names, paginate and filter list operations, capture +resource IDs instead of selecting the first or latest result, and avoid +repository-global or default-branch mutations. When scripts create the same +tree from the same parent, include the script's `$RANDOM_STRING` in the commit +contents or message; branch names do not affect commit IDs. + +`isolated` creates an initialized private repository exclusively for the script. +Use it when clean state, repository-global mutation, or multiple coordinated Git +ref updates are required. Consolidate related operations into one isolated +script when they can share that repository sequentially. + +`none` creates no managed repository. Use it when no repository is needed or +when a test needs multiple repositories, public visibility, special creation +options, or direct coverage of repository lifecycle commands. In that mode, the +script owns creation and cleanup. + +Scripts share token-wide API rate limits. Count requests across the whole test +process and combine compatible live assertions. Keep coverage for a narrowly +limited endpoint in one script so concurrent scripts cannot burst the limit. +For Code Search's 10 requests/minute bucket, use at most five HTTP requests in +the entire acceptance process even when they run sequentially. This leaves room +for pagination, retries, and other token activity. Keep representative live +coverage and use unit tests for remaining variants. + +Tests that cancel workflow runs should use a self-contained, deliberately +long-running job so it cannot finish before the cancellation request, plus a +short job timeout to bound a failed cancellation. Wait for the run to become +`in_progress` before canceling, but do not wait for GitHub to finish processing +an accepted cancellation request. + +After pushing a new workflow file, use `wait-for-workflow` instead of a fixed +sleep before invoking or inspecting it. Use `wait-for-run` to allow up to one +minute for a triggered run to appear. After `gh workflow run`, the helper uses +the run URL returned by GitHub.com or a compatible GitHub Enterprise Server and +only polls when no URL is available. If that deadline expires, the helper logs +the run filters, local and remote refs, workflow files, recent runs, commit check +suites, and an Actions API request ID before the repository is cleaned up. + #### Custom Commands The following custom commands are defined within [`acceptance_test.go`](./acceptance_test.go) to help with writing tests: +- `fixture-repo`: select the script's repository fixture mode. For `shared` and + `isolated`, the final argument names the environment variable that receives + the repository's bare name. + + ```txtar + fixture-repo shared REPO + exec gh issue create --repo $ORG/$REPO --title $SCRIPT_NAME-$RANDOM_STRING --body Body + ``` + +- `cleanup-repo`: idempotently delete an unmanaged repository during deferred + cleanup. Use this when a lifecycle test may have already deleted or renamed + the repository. + + ```txtar + defer cleanup-repo $SCRIPT_NAME-$RANDOM_STRING + ``` + +- `wait-for-workflow`: poll until GitHub registers a pushed workflow definition. + + ```txtar + wait-for-workflow 'Test Workflow Name' + exec gh workflow run 'Test Workflow Name' + ``` + +- `wait-for-repository-ready`: poll until an initialized repository's default + branch commit is available. Use it before repository-global operations that + can conflict with asynchronous repository initialization. Repository rename + is a narrow exception: GitHub can retain its creation-operation lock after + the commit becomes readable, so keep the `gh repo rename` command inline and + allow a 10-second stabilization delay after this check. + + ```txtar + wait-for-repository-ready $ORG/$REPO + ``` + +- `wait-for-run-status`: poll a registered workflow run until it reaches the + requested status. Use this before operations such as cancellation that can + race with run startup. + + ```txtar + wait-for-run RUN_ID + wait-for-run-status $RUN_ID in_progress + exec gh run cancel $RUN_ID + ``` + - `defer`: register a command to run after the testscript completes ```txtar @@ -103,6 +211,14 @@ The following custom commands are defined within [`acceptance_test.go`](./accept stdout2env PR_URL ``` +- `wait-for-run`: poll for a workflow run until it registers, then set an + environment variable to its database ID. Pass `gh run list` filter flags after + the variable name. + + ```txtar + wait-for-run RUN_ID --branch $WORKFLOW_BRANCH --event push + ``` + - `jq-assert`: evaluate a jq expression on a JSON environment variable and assert the result matches a regexp ```txtar @@ -136,8 +252,9 @@ When tests fail they fail like this: ``` ➜ go test -tags=acceptance ./acceptance ---- FAIL: TestPullRequests (0.00s) - --- FAIL: TestPullRequests/pr-merge (11.07s) +--- FAIL: TestAcceptance (0.00s) + --- FAIL: TestAcceptance/pr (0.00s) + --- FAIL: TestAcceptance/pr/pr-merge (11.07s) testscript.go:584: WORK=/private/var/folders/45/sdnm1hp10nj1s9q57dp3bc5h0000gn/T/go-test-script2778137936/script-pr-merge # Use gh as a credential helper (0.693s) # Create a repository with a file so it has a default branch (1.155s) diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 3f198711984..65597121b7b 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -8,6 +8,7 @@ import ( cryptorand "crypto/rand" "errors" "fmt" + "net/url" "os" "path" "path/filepath" @@ -29,9 +30,199 @@ import ( ) func ghMain() int { + if repositoryCreationIsManaged(os.Args[1:], os.Getenv("GH_ACCEPTANCE_FIXTURE_MODE")) { + fmt.Fprintln(os.Stderr, "gh repo create requires 'fixture-repo none'") + return 1 + } return int(ghcmd.Main()) } +func repositoryCreationIsManaged(args []string, fixtureMode string) bool { + return len(args) > 1 && args[0] == "repo" && args[1] == "create" && fixtureMode != "none" +} + +const ( + workflowPollAttempts = 13 + workflowPollInterval = 5 * time.Second + workflowRunStatusPollAttempts = 61 + workflowRunStatusPollInterval = time.Second + repositoryReadyPollAttempts = 31 + repositoryReadyPollInterval = time.Second +) + +var errWorkflowRunRegistrationTimeout = errors.New("workflow run did not register within 60 seconds") + +func waitForWorkflowRun(list func() (string, error), sleep func(time.Duration)) (string, error) { + for attempt := 0; attempt < workflowPollAttempts; attempt++ { + runID, err := list() + if err != nil { + return "", err + } + if runID != "" { + return runID, nil + } + if attempt < workflowPollAttempts-1 { + sleep(workflowPollInterval) + } + } + return "", errWorkflowRunRegistrationTimeout +} + +func workflowRunIDFromOutput(output string) string { + runURL, err := url.ParseRequestURI(strings.TrimSpace(output)) + if err != nil || runURL.Scheme == "" || runURL.Host == "" || + path.Base(path.Dir(runURL.Path)) != "runs" || + path.Base(path.Dir(path.Dir(runURL.Path))) != "actions" { + return "" + } + runID := path.Base(runURL.Path) + if _, err := strconv.ParseInt(runID, 10, 64); err != nil { + return "" + } + return runID +} + +func resolveWorkflowRunID(output string, list func() (string, error), sleep func(time.Duration)) (string, error) { + if runID := workflowRunIDFromOutput(output); runID != "" { + return runID, nil + } + return waitForWorkflowRun(list, sleep) +} + +type workflowRunDiagnosticExecutor func(name string, args ...string) (stdout string, stderr string, err error) + +func collectWorkflowRunDiagnostics(exec workflowRunDiagnosticExecutor, runListArgs []string) string { + var diagnostics strings.Builder + fmt.Fprintf(&diagnostics, "run filters: %s\n", strings.Join(runListArgs, " ")) + run := func(label string, name string, args ...string) string { + stdout, stderr, err := exec(name, args...) + fmt.Fprintf(&diagnostics, "%s:\n", label) + if output := strings.TrimSpace(stdout); output != "" { + fmt.Fprintln(&diagnostics, output) + } + if output := strings.TrimSpace(stderr); output != "" { + fmt.Fprintf(&diagnostics, "stderr:\n%s\n", output) + } + if err != nil { + fmt.Fprintf(&diagnostics, "error: %v\n", err) + } + return strings.TrimSpace(stdout) + } + + run("repository", "git", "remote", "get-url", "origin") + commit := run("local commit", "git", "rev-parse", "HEAD") + branch := commandFlagValue(runListArgs, "--branch", "-b") + event := commandFlagValue(runListArgs, "--event", "-e") + + if branch != "" { + run("remote branch", "git", "ls-remote", "origin", "refs/heads/"+branch) + } + run("workflow files", "git", "ls-tree", "-r", "--name-only", "HEAD", ".github/workflows") + run("recent workflow runs", "gh", "run", "list", "--limit", "20", "--json", "databaseId,workflowName,event,headBranch,headSha,status,createdAt") + + if commit != "" { + run( + "commit check suites", + "gh", "api", "repos/{owner}/{repo}/commits/"+commit+"/check-suites", + "--jq", `.check_suites[] | {id, status, conclusion, app: .app.slug, head_sha}`, + ) + } + + apiArgs := []string{ + "api", "repos/{owner}/{repo}/actions/runs", + "--method", "GET", + "-f", "per_page=1", + "--include", + "--silent", + } + if branch != "" { + apiArgs = append(apiArgs, "-f", "branch="+branch) + } + if event != "" { + apiArgs = append(apiArgs, "-f", "event="+event) + } + if commit != "" { + apiArgs = append(apiArgs, "-f", "head_sha="+commit) + } + run("Actions API response headers", "gh", apiArgs...) + + return strings.TrimSpace(diagnostics.String()) +} + +func commandFlagValue(args []string, names ...string) string { + for i, arg := range args { + for _, name := range names { + if arg == name && i+1 < len(args) { + return args[i+1] + } + if value, ok := strings.CutPrefix(arg, name+"="); ok { + return value + } + } + } + return "" +} + +func waitForWorkflow(list func() ([]string, error), expected string, sleep func(time.Duration)) error { + for attempt := 0; attempt < workflowPollAttempts; attempt++ { + workflows, err := list() + if err != nil { + return err + } + for _, workflow := range workflows { + if workflow == expected { + return nil + } + } + if attempt < workflowPollAttempts-1 { + sleep(workflowPollInterval) + } + } + return fmt.Errorf("workflow %q did not register within 60 seconds", expected) +} + +func waitForRepositoryReady(check func() (bool, error), sleep func(time.Duration)) error { + for attempt := 0; attempt < repositoryReadyPollAttempts; attempt++ { + ready, err := check() + if err != nil { + return err + } + if ready { + return nil + } + if attempt < repositoryReadyPollAttempts-1 { + sleep(repositoryReadyPollInterval) + } + } + return errors.New("repository did not finish initializing within 30 seconds") +} + +func waitForWorkflowRunStatus(view func() (string, error), expected string, sleep func(time.Duration)) error { + for attempt := 0; attempt < workflowRunStatusPollAttempts; attempt++ { + status, err := view() + if err != nil { + return err + } + if status == expected { + return nil + } + if status == "completed" { + return fmt.Errorf("workflow run completed before reaching status %q", expected) + } + if attempt < workflowRunStatusPollAttempts-1 { + sleep(workflowRunStatusPollInterval) + } + } + return fmt.Errorf("workflow run did not reach status %q within 60 seconds", expected) +} + +func outputForEnvironment(output string) (string, error) { + if strings.TrimSpace(output) == "" { + return "", errors.New("command output is empty") + } + return strings.TrimRight(output, "\n"), nil +} + func TestMain(m *testing.M) { os.Exit(testscript.RunMain(m, map[string]func() int{ "gh": ghMain, @@ -67,220 +258,491 @@ func TestSandboxFilePath(t *testing.T) { assert.EqualError(t, err, "path must stay within the testscript sandbox") } -func TestAPI(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } +func TestRepositoryCreationIsManaged(t *testing.T) { + assert.True(t, repositoryCreationIsManaged([]string{"repo", "create", "example"}, "shared")) + assert.True(t, repositoryCreationIsManaged([]string{"repo", "create", "example"}, "isolated")) + assert.True(t, repositoryCreationIsManaged([]string{"repo", "create", "example"}, "undeclared")) + assert.False(t, repositoryCreationIsManaged([]string{"repo", "create", "example"}, "none")) + assert.False(t, repositoryCreationIsManaged([]string{"repo", "view", "example"}, "shared")) +} - testscript.Run(t, testScriptParamsFor(t, tsEnv, "api")) +func TestWaitForWorkflowRun(t *testing.T) { + t.Run("returns registered run", func(t *testing.T) { + var attempts int + var sleeps []time.Duration + runID, err := waitForWorkflowRun(func() (string, error) { + attempts++ + if attempts == 3 { + return "1234", nil + } + return "", nil + }, func(duration time.Duration) { + sleeps = append(sleeps, duration) + }) + + require.NoError(t, err) + assert.Equal(t, "1234", runID) + assert.Equal(t, []time.Duration{workflowPollInterval, workflowPollInterval}, sleeps) + }) + + t.Run("returns list error", func(t *testing.T) { + _, err := waitForWorkflowRun(func() (string, error) { + return "", errors.New("listing runs") + }, func(time.Duration) { + t.Fatal("unexpected sleep") + }) + + require.EqualError(t, err, "listing runs") + }) + + t.Run("times out", func(t *testing.T) { + var attempts int + var sleeps int + _, err := waitForWorkflowRun(func() (string, error) { + attempts++ + return "", nil + }, func(time.Duration) { + sleeps++ + }) + + require.EqualError(t, err, "workflow run did not register within 60 seconds") + assert.Equal(t, 13, attempts) + assert.Equal(t, 12, sleeps) + }) } -func TestAuth(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func TestWorkflowRunIDFromOutput(t *testing.T) { + tests := []struct { + name string + output string + want string + }{ + { + name: "GitHub.com workflow run URL", + output: "https://github.com/OWNER/REPO/actions/runs/1234\n", + want: "1234", + }, + { + name: "GHEC workflow run URL", + output: "https://example.ghe.com/OWNER/REPO/actions/runs/5678\n", + want: "5678", + }, + { + name: "empty legacy dispatch output", + output: "", + }, + { + name: "unrelated URL", + output: "https://github.com/OWNER/REPO/actions/workflows/main.yml", + }, + { + name: "non-numeric run identifier", + output: "https://github.com/OWNER/REPO/actions/runs/latest", + }, } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "auth")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, workflowRunIDFromOutput(tt.output)) + }) + } } -func TestGists(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } +func TestResolveWorkflowRunIDUsesDispatchOutput(t *testing.T) { + runID, err := resolveWorkflowRunID( + "https://github.com/OWNER/REPO/actions/runs/1234\n", + func() (string, error) { + t.Fatal("unexpected workflow run list") + return "", nil + }, + func(time.Duration) { + t.Fatal("unexpected sleep") + }, + ) - testscript.Run(t, testScriptParamsFor(t, tsEnv, "gist")) + require.NoError(t, err) + assert.Equal(t, "1234", runID) } -func TestGPGKeys(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } +func TestResolveWorkflowRunIDFallsBackToPolling(t *testing.T) { + var attempts int + runID, err := resolveWorkflowRunID("", func() (string, error) { + attempts++ + return "5678", nil + }, func(time.Duration) { + t.Fatal("unexpected sleep") + }) - testscript.Run(t, testScriptParamsFor(t, tsEnv, "gpg-key")) + require.NoError(t, err) + assert.Equal(t, "5678", runID) + assert.Equal(t, 1, attempts) } -func TestExtensions(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func TestCollectWorkflowRunDiagnostics(t *testing.T) { + // Given diagnostic commands that expose each Actions registration boundary + var actionsAPIArgs []string + exec := func(name string, args ...string) (string, string, error) { + switch { + case name == "git" && args[0] == "rev-parse": + return "abc123\n", "", nil + case name == "git" && args[0] == "remote": + return "https://github.com/example/repo.git\n", "", nil + case name == "git" && args[0] == "ls-remote": + return "abc123\trefs/heads/feature\n", "", nil + case name == "git" && args[0] == "ls-tree": + return ".github/workflows/workflow.yml\n", "", nil + case name == "gh" && args[0] == "run": + return `[{"databaseId":42,"headBranch":"other"}]`, "", nil + case name == "gh" && args[1] == "repos/{owner}/{repo}/commits/abc123/check-suites": + return `{"id":7,"app":"actions"}`, "", nil + case name == "gh" && args[1] == "repos/{owner}/{repo}/actions/runs": + actionsAPIArgs = append([]string(nil), args...) + return "x-github-request-id: REQUEST-ID\n", "", nil + default: + return "", "", fmt.Errorf("unexpected command: %s %s", name, strings.Join(args, " ")) + } } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "extension")) + // When collecting diagnostics for a run that did not register + diagnostics := collectWorkflowRunDiagnostics(exec, []string{"--branch", "feature", "--event", "push"}) + + // Then the snapshot includes evidence from every boundary and the request ID + assert.Contains(t, diagnostics, "run filters: --branch feature --event push") + assert.Contains(t, diagnostics, "repository:\nhttps://github.com/example/repo.git") + assert.Contains(t, diagnostics, "local commit:\nabc123") + assert.Contains(t, diagnostics, "remote branch:\nabc123\trefs/heads/feature") + assert.Contains(t, diagnostics, "workflow files:\n.github/workflows/workflow.yml") + assert.Contains(t, diagnostics, "recent workflow runs:\n"+`[{"databaseId":42,"headBranch":"other"}]`) + assert.Contains(t, diagnostics, "commit check suites:\n"+`{"id":7,"app":"actions"}`) + assert.Contains(t, diagnostics, "Actions API response headers:\nx-github-request-id: REQUEST-ID") + assert.Equal(t, []string{ + "api", "repos/{owner}/{repo}/actions/runs", + "--method", "GET", + "-f", "per_page=1", + "--include", + "--silent", + "-f", "branch=feature", + "-f", "event=push", + "-f", "head_sha=abc123", + }, actionsAPIArgs) } -func TestIssues(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func TestCollectWorkflowRunDiagnosticsIncludesCommandFailures(t *testing.T) { + // Given diagnostic commands that fail while gathering supplementary evidence + exec := func(string, ...string) (string, string, error) { + return "", "service unavailable", errors.New("exit status 1") } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "issue")) + // When collecting diagnostics for a run that did not register + diagnostics := collectWorkflowRunDiagnostics(exec, nil) + + // Then each failure is reported without replacing the original timeout + assert.Contains(t, diagnostics, "stderr:\nservice unavailable") + assert.Contains(t, diagnostics, "error: exit status 1") } -func TestDiscussions(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } +func TestWaitForWorkflow(t *testing.T) { + // Given a workflow definition that appears after GitHub processes the pushed file + var attempts int + var sleeps []time.Duration - testscript.Run(t, testScriptParamsFor(t, tsEnv, "discussion")) -} + // When waiting for the workflow by name + err := waitForWorkflow(func() ([]string, error) { + attempts++ + if attempts == 3 { + return []string{"Other Workflow", "Test Workflow Name"}, nil + } + return []string{"Other Workflow"}, nil + }, "Test Workflow Name", func(duration time.Duration) { + sleeps = append(sleeps, duration) + }) -func TestIssues2_0(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } + // Then polling continues until that exact workflow is registered + require.NoError(t, err) + assert.Equal(t, []time.Duration{workflowPollInterval, workflowPollInterval}, sleeps) +} - testscript.Run(t, testScriptParamsFor(t, tsEnv, "issues-2.0")) +func TestWaitForWorkflowTimesOut(t *testing.T) { + // Given a pushed workflow definition that never appears + var attempts int + var sleeps int + + // When waiting for the workflow by name + err := waitForWorkflow(func() ([]string, error) { + attempts++ + return []string{"Other Workflow"}, nil + }, "Test Workflow Name", func(time.Duration) { + sleeps++ + }) + + // Then the wait stops after one minute + require.EqualError(t, err, `workflow "Test Workflow Name" did not register within 60 seconds`) + assert.Equal(t, 13, attempts) + assert.Equal(t, 12, sleeps) } -func TestLabels(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } +func TestWaitForRepositoryReady(t *testing.T) { + // Given a repository whose initialized default branch appears after creation + checks := []bool{false, false, true} + var sleeps []time.Duration + + // When waiting for repository initialization to finish + err := waitForRepositoryReady(func() (bool, error) { + ready := checks[0] + checks = checks[1:] + return ready, nil + }, func(duration time.Duration) { + sleeps = append(sleeps, duration) + }) + + // Then polling continues until the default branch commit is available + require.NoError(t, err) + assert.Equal(t, []time.Duration{repositoryReadyPollInterval, repositoryReadyPollInterval}, sleeps) +} - testscript.Run(t, testScriptParamsFor(t, tsEnv, "label")) +func TestWaitForRepositoryReadyTimesOut(t *testing.T) { + // Given a repository whose default branch remains unavailable + var attempts int + var sleeps int + + // When waiting for repository initialization to finish + err := waitForRepositoryReady(func() (bool, error) { + attempts++ + return false, nil + }, func(time.Duration) { + sleeps++ + }) + + // Then the wait stops after thirty seconds + require.EqualError(t, err, "repository did not finish initializing within 30 seconds") + assert.Equal(t, 31, attempts) + assert.Equal(t, 30, sleeps) } -func TestOrg(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func TestWaitForRepositoryReadyReturnsUnexpectedError(t *testing.T) { + // Given a repository readiness check that fails unexpectedly + check := func() (bool, error) { + return false, errors.New("checking repository") } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "org")) -} + // When waiting for repository initialization to finish + err := waitForRepositoryReady(check, func(time.Duration) { + t.Fatal("unexpected sleep") + }) -func TestProject(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } + // Then the unexpected failure is returned instead of retried + require.EqualError(t, err, "checking repository") +} - testscript.Run(t, testScriptParamsFor(t, tsEnv, "project")) +func TestWaitForWorkflowRunStatus(t *testing.T) { + // Given a workflow run that is registered but still queued + statuses := []string{"queued", "in_progress"} + var sleeps []time.Duration + + // When waiting for the run to start + err := waitForWorkflowRunStatus(func() (string, error) { + status := statuses[0] + statuses = statuses[1:] + return status, nil + }, "in_progress", func(duration time.Duration) { + sleeps = append(sleeps, duration) + }) + + // Then polling continues until cancellation can target the active run + require.NoError(t, err) + assert.Equal(t, []time.Duration{workflowRunStatusPollInterval}, sleeps) } -func TestPullRequests(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func TestWaitForWorkflowRunStatusStopsWhenRunCompletes(t *testing.T) { + // Given a workflow run that completed before reaching the expected status + view := func() (string, error) { + return "completed", nil } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "pr")) -} + // When waiting for a status the run can no longer reach + err := waitForWorkflowRunStatus(view, "in_progress", func(time.Duration) { + t.Fatal("unexpected sleep") + }) -func TestReleases(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } + // Then the failed precondition is reported immediately + require.EqualError(t, err, `workflow run completed before reaching status "in_progress"`) +} - testscript.Run(t, testScriptParamsFor(t, tsEnv, "release")) +func TestWaitForWorkflowRunStatusTimesOut(t *testing.T) { + // Given a workflow run that remains queued + var attempts int + var sleeps int + + // When waiting for it to start + err := waitForWorkflowRunStatus(func() (string, error) { + attempts++ + return "queued", nil + }, "in_progress", func(time.Duration) { + sleeps++ + }) + + // Then the wait stops after one minute + require.EqualError(t, err, `workflow run did not reach status "in_progress" within 60 seconds`) + assert.Equal(t, 61, attempts) + assert.Equal(t, 60, sleeps) } -func TestRepo(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } +func TestOutputForEnvironment(t *testing.T) { + value, err := outputForEnvironment("1234\n") + require.NoError(t, err) + assert.Equal(t, "1234", value) - testscript.Run(t, testScriptParamsFor(t, tsEnv, "repo")) + _, err = outputForEnvironment("\n") + require.EqualError(t, err, "command output is empty") } -func TestRulesets(t *testing.T) { +func TestAcceptance(t *testing.T) { var tsEnv testScriptEnv if err := tsEnv.fromEnv(); err != nil { t.Fatal(err) } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "ruleset")) -} - -func TestSearches(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { + fixtureRepositories, err := newFixtureRepositoryManager(tsEnv) + if err != nil { t.Fatal(err) } + registerFixtureRepositoryCleanup(t, tsEnv.skipDefer, fixtureRepositories) - testscript.Run(t, testScriptParamsFor(t, tsEnv, "search")) -} - -func TestSecrets(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { + testGroups, err := selectAcceptanceTestGroups( + acceptanceTestGroups(t), + os.Getenv("GH_ACCEPTANCE_GROUP"), + ) + if err != nil { t.Fatal(err) } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "secret")) -} + validateAcceptanceScripts(t, tsEnv, testGroups) -func TestSSHKeys(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) + for _, group := range testGroups { + t.Run(group, func(t *testing.T) { + testscript.Run(t, testScriptParamsFor(t, tsEnv, fixtureRepositories, group)) + }) } +} - testscript.Run(t, testScriptParamsFor(t, tsEnv, "ssh-key")) +func registerFixtureRepositoryCleanup(t *testing.T, skip bool, fixtureRepositories *fixtureRepositoryManager) { + t.Helper() + if skip { + return + } + t.Cleanup(func() { + if err := fixtureRepositories.cleanup(); err != nil { + t.Errorf("cleaning up fixture repositories: %v", err) + } + }) } -func TestVariables(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func validateAcceptanceScripts(t *testing.T, tsEnv testScriptEnv, groups []string) { + t.Helper() + + for _, group := range groups { + candidates, _, err := acceptanceScriptCandidates(tsEnv, group) + require.NoError(t, err) + for _, file := range candidates { + require.NoError(t, validateFixtureRepositoryDeclaration(file)) + } } +} + +func acceptanceTestGroups(t *testing.T) []string { + t.Helper() - testscript.Run(t, testScriptParamsFor(t, tsEnv, "variable")) + entries, err := os.ReadDir("testdata") + require.NoError(t, err) + + var groups []string + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + files, err := filepath.Glob(filepath.Join("testdata", entry.Name(), "*.txtar")) + require.NoError(t, err) + if len(files) > 0 { + groups = append(groups, entry.Name()) + } + } + require.NotEmpty(t, groups) + return groups } -func TestWorkflows(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func selectAcceptanceTestGroups(available []string, requested string) ([]string, error) { + if requested == "" || requested == "all" { + return available, nil } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "workflow")) + for _, group := range available { + if group == requested { + return []string{requested}, nil + } + } + + return nil, fmt.Errorf("unknown acceptance test group %q; available groups: %s", requested, strings.Join(available, ", ")) } -func TestTelemetry(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) +func TestSelectAcceptanceTestGroups(t *testing.T) { + available := []string{"api", "pr", "repo"} + + tests := []struct { + name string + requested string + want []string + wantErr string + }{ + {name: "empty selects all", want: available}, + {name: "all selects all", requested: "all", want: available}, + {name: "group selects one", requested: "pr", want: []string{"pr"}}, + {name: "unknown group errors", requested: "pull-request", wantErr: `unknown acceptance test group "pull-request"; available groups: api, pr, repo`}, } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "telemetry")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := selectAcceptanceTestGroups(available, tt.requested) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } } -func testScriptParamsFor(t *testing.T, tsEnv testScriptEnv, command string) testscript.Params { +func testScriptParamsFor(t *testing.T, tsEnv testScriptEnv, fixtureRepositories *fixtureRepositoryManager, command string) testscript.Params { t.Helper() - files, filtered := selectScripts(command, tsEnv.scripts) - var dir string - if !filtered { - // No filter was set - run everything in the directory. - dir = path.Join("testdata", command) - } else if len(files) == 0 { - // A filter was set but none of the selected scripts belong to this - // command directory, so skip rather than running the whole directory. + candidates, filtered, err := acceptanceScriptCandidates(tsEnv, command) + if err != nil { + t.Fatal(err) + } + if filtered && len(candidates) == 0 { t.Skipf("testdata/%s: no selected script belongs to this command directory", command) } return testscript.Params{ - Dir: dir, - Files: files, + Files: candidates, Setup: sharedSetup(tsEnv), - Cmds: sharedCmds(tsEnv), + Cmds: sharedCmds(tsEnv, fixtureRepositories), RequireExplicitExec: true, RequireUniqueNames: true, TestWork: tsEnv.preserveWorkDir, } } +func acceptanceScriptCandidates(tsEnv testScriptEnv, command string) ([]string, bool, error) { + files, filtered := selectScripts(command, tsEnv.scripts) + if filtered { + return files, true, nil + } + files, err := filepath.Glob(filepath.Join("testdata", command, "*.txtar")) + return files, false, err +} + var keyT struct{} func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error { @@ -321,6 +783,7 @@ func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error { return fmt.Errorf("writing sandbox hosts.yml: %w", err) } } + ts.Setenv("GH_ACCEPTANCE_FIXTURE_MODE", "undeclared") ts.Setenv("RANDOM_STRING", randomString(10)) @@ -354,7 +817,7 @@ func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error { } // sharedCmds defines a collection of custom testscript commands for our use. -func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, neg bool, args []string) { +func sharedCmds(tsEnv testScriptEnv, fixtureRepositories *fixtureRepositoryManager) map[string]func(ts *testscript.TestScript, neg bool, args []string) { return map[string]func(ts *testscript.TestScript, neg bool, args []string){ "defer": func(ts *testscript.TestScript, neg bool, args []string) { if neg { @@ -371,6 +834,17 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, } ts.Defer(func() { + if args[0] == "cleanup-repo" { + if len(args) != 2 { + tt.Fatal("usage: defer cleanup-repo REPO") + return + } + if err := fixtureRepositories.delete(args[1]); err != nil { + tt.Fatal(err) + } + return + } + // If you're wondering why we're not using ts.Check here, it's because it raises a panic, and testscript // only catches the panics directly from commands, not from the deferred functions. So what we do // instead is grab the `t` in the setup function and store it as a value. It's important that we use @@ -399,6 +873,23 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, ts.Setenv(env[:i], strings.ToUpper(env[i+1:])) } }, + "fixture-repo": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! fixture-repo") + } + if len(args) == 1 && args[0] == "none" { + ts.Setenv("GH_ACCEPTANCE_FIXTURE_MODE", "none") + return + } + if len(args) != 2 || (args[0] != "shared" && args[0] != "isolated") { + ts.Fatalf("usage: fixture-repo (shared|isolated) ENV_VAR, or fixture-repo none") + } + + repository, err := fixtureRepositories.repository(args[0]) + ts.Check(err) + ts.Setenv("GH_ACCEPTANCE_FIXTURE_MODE", args[0]) + ts.Setenv(args[1], repository) + }, "generate-ssh-key": func(ts *testscript.TestScript, neg bool, args []string) { if neg { ts.Fatalf("unsupported: ! generate-ssh-key") @@ -462,7 +953,91 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, ts.Fatalf("usage: stdout2env name") } - ts.Setenv(args[0], strings.TrimRight(ts.ReadFile("stdout"), "\n")) + value, err := outputForEnvironment(ts.ReadFile("stdout")) + ts.Check(err) + ts.Setenv(args[0], value) + }, + "wait-for-run": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! wait-for-run") + } + if len(args) < 1 { + ts.Fatalf("usage: wait-for-run ENV_VAR [run-list-flags...]") + } + + runID, err := resolveWorkflowRunID(ts.ReadFile("stdout"), func() (string, error) { + listArgs := append([]string{"run", "list"}, args[1:]...) + listArgs = append(listArgs, "--limit", "1", "--json", "databaseId", "--jq", ".[].databaseId") + if err := ts.Exec("gh", listArgs...); err != nil { + return "", err + } + return strings.TrimSpace(ts.ReadFile("stdout")), nil + }, time.Sleep) + if errors.Is(err, errWorkflowRunRegistrationTimeout) { + diagnostics := collectWorkflowRunDiagnostics(func(name string, args ...string) (string, string, error) { + err := ts.Exec(name, args...) + return ts.ReadFile("stdout"), ts.ReadFile("stderr"), err + }, args[1:]) + ts.Logf("workflow run registration diagnostics:\n%s", diagnostics) + } + ts.Check(err) + ts.Setenv(args[0], runID) + }, + "wait-for-workflow": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! wait-for-workflow") + } + if len(args) != 1 { + ts.Fatalf("usage: wait-for-workflow NAME") + } + + err := waitForWorkflow(func() ([]string, error) { + if err := ts.Exec("gh", "workflow", "list", "--all", "--limit", "1000", "--json", "name", "--jq", ".[].name"); err != nil { + return nil, err + } + output := strings.TrimSpace(ts.ReadFile("stdout")) + if output == "" { + return nil, nil + } + return strings.Split(output, "\n"), nil + }, args[0], time.Sleep) + ts.Check(err) + }, + "wait-for-repository-ready": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! wait-for-repository-ready") + } + if len(args) != 1 { + ts.Fatalf("usage: wait-for-repository-ready OWNER/REPO") + } + + err := waitForRepositoryReady(func() (bool, error) { + if err := ts.Exec("gh", "api", "repos/"+args[0]+"/commits/HEAD", "--silent"); err != nil { + stderr := ts.ReadFile("stderr") + if strings.Contains(stderr, "HTTP 404") || strings.Contains(stderr, "HTTP 409") { + return false, nil + } + return false, err + } + return true, nil + }, time.Sleep) + ts.Check(err) + }, + "wait-for-run-status": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! wait-for-run-status") + } + if len(args) != 2 { + ts.Fatalf("usage: wait-for-run-status RUN_ID STATUS") + } + + err := waitForWorkflowRunStatus(func() (string, error) { + if err := ts.Exec("gh", "run", "view", args[0], "--json", "status", "--jq", ".status"); err != nil { + return "", err + } + return strings.TrimSpace(ts.ReadFile("stdout")), nil + }, args[1], time.Sleep) + ts.Check(err) }, "sleep": func(ts *testscript.TestScript, neg bool, args []string) { if neg { @@ -660,11 +1235,3 @@ func (e *testScriptEnv) fromEnv() error { return nil } - -func TestSkills(t *testing.T) { - var tsEnv testScriptEnv - if err := tsEnv.fromEnv(); err != nil { - t.Fatal(err) - } - testscript.Run(t, testScriptParamsFor(t, tsEnv, "skills")) -} diff --git a/acceptance/fixture_repository_test.go b/acceptance/fixture_repository_test.go new file mode 100644 index 00000000000..4939bdc01cb --- /dev/null +++ b/acceptance/fixture_repository_test.go @@ -0,0 +1,347 @@ +//go:build acceptance + +package acceptance_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + ghAPI "github.com/cli/go-gh/v2/pkg/api" + "github.com/cli/go-internal/testscript" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type fixtureRepositoryClient interface { + create(name string) error + delete(name string) error +} + +type liveFixtureRepositoryClient struct { + apiClient *ghAPI.RESTClient + org string +} + +func newLiveFixtureRepositoryClient(tsEnv testScriptEnv) (*liveFixtureRepositoryClient, error) { + return newLiveFixtureRepositoryClientWithTransport(tsEnv, nil) +} + +func newLiveFixtureRepositoryClientWithTransport(tsEnv testScriptEnv, transport http.RoundTripper) (*liveFixtureRepositoryClient, error) { + apiClient, err := ghAPI.NewRESTClient(ghAPI.ClientOptions{ + Host: tsEnv.host, + APIHost: tsEnv.apiHost, + AuthToken: tsEnv.token, + LogIgnoreEnv: true, + Timeout: 30 * time.Second, + Transport: transport, + }) + if err != nil { + return nil, err + } + return &liveFixtureRepositoryClient{ + apiClient: apiClient, + org: tsEnv.org, + }, nil +} + +func (c *liveFixtureRepositoryClient) create(name string) error { + body, err := json.Marshal(struct { + Name string `json:"name"` + Private bool `json:"private"` + AutoInit bool `json:"auto_init"` + HasDiscussions bool `json:"has_discussions"` + }{ + Name: name, + Private: true, + AutoInit: true, + HasDiscussions: true, + }) + if err != nil { + return err + } + path := fmt.Sprintf("orgs/%s/repos", url.PathEscape(c.org)) + return c.apiClient.Do(http.MethodPost, path, bytes.NewReader(body), nil) +} + +func (c *liveFixtureRepositoryClient) delete(name string) error { + path := fmt.Sprintf("repos/%s/%s", url.PathEscape(c.org), url.PathEscape(name)) + err := c.apiClient.Do(http.MethodDelete, path, nil, nil) + var httpErr *ghAPI.HTTPError + if errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound { + return nil + } + return err +} + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +type fixtureRepositoryManager struct { + client fixtureRepositoryClient + sleep func(time.Duration) + + mu sync.Mutex + shared string + repositories []string +} + +const ( + repositoryDeletePollAttempts = 31 + repositoryDeletePollInterval = time.Second +) + +func newFixtureRepositoryManager(tsEnv testScriptEnv) (*fixtureRepositoryManager, error) { + client, err := newLiveFixtureRepositoryClient(tsEnv) + if err != nil { + return nil, err + } + return &fixtureRepositoryManager{ + client: client, + sleep: time.Sleep, + }, nil +} + +func (m *fixtureRepositoryManager) repository(mode string) (string, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if mode == "shared" && m.shared != "" { + return m.shared, nil + } + + name := fmt.Sprintf("gh-acceptance-%s-%s", mode, strings.ToLower(randomString(16))) + m.repositories = append(m.repositories, name) + if err := m.client.create(name); err != nil { + return "", fmt.Errorf("creating %s fixture repository: %w", mode, err) + } + if mode == "shared" { + m.shared = name + } + return name, nil +} + +func (m *fixtureRepositoryManager) cleanup() error { + m.mu.Lock() + defer m.mu.Unlock() + + var errs []error + for i := len(m.repositories) - 1; i >= 0; i-- { + if err := m.deleteRepository(m.repositories[i]); err != nil { + errs = append(errs, fmt.Errorf("deleting %s: %w", m.repositories[i], err)) + } + } + return errors.Join(errs...) +} + +func (m *fixtureRepositoryManager) delete(name string) error { + m.mu.Lock() + defer m.mu.Unlock() + return m.deleteRepository(name) +} + +func (m *fixtureRepositoryManager) deleteRepository(name string) error { + var conflictErr error + for attempt := 0; attempt < repositoryDeletePollAttempts; attempt++ { + err := m.client.delete(name) + if err == nil { + return nil + } + + var httpErr *ghAPI.HTTPError + if !errors.As(err, &httpErr) || httpErr.StatusCode != http.StatusConflict { + return err + } + conflictErr = err + + if attempt < repositoryDeletePollAttempts-1 { + m.sleep(repositoryDeletePollInterval) + } + } + return fmt.Errorf("repository operation remained in progress for 30 seconds: %w", conflictErr) +} + +type fakeFixtureRepositoryClient struct { + created []string + deleted []string + deleteErrors []error +} + +func (c *fakeFixtureRepositoryClient) create(name string) error { + c.created = append(c.created, name) + return nil +} + +func (c *fakeFixtureRepositoryClient) delete(name string) error { + c.deleted = append(c.deleted, name) + if len(c.deleteErrors) > 0 { + err := c.deleteErrors[0] + c.deleteErrors = c.deleteErrors[1:] + return err + } + return nil +} + +func TestFixtureRepositoryManager(t *testing.T) { + client := &fakeFixtureRepositoryClient{} + manager := &fixtureRepositoryManager{client: client} + + firstShared, err := manager.repository("shared") + require.NoError(t, err) + secondShared, err := manager.repository("shared") + require.NoError(t, err) + firstIsolated, err := manager.repository("isolated") + require.NoError(t, err) + secondIsolated, err := manager.repository("isolated") + require.NoError(t, err) + + assert.Equal(t, firstShared, secondShared) + assert.NotEqual(t, firstIsolated, secondIsolated) + assert.Len(t, client.created, 3) + + require.NoError(t, manager.cleanup()) + assert.Equal(t, []string{secondIsolated, firstIsolated, firstShared}, client.deleted) +} + +func TestFixtureRepositoryManagerRetriesConflictingDelete(t *testing.T) { + // Given a repository whose preceding operation is still settling + client := &fakeFixtureRepositoryClient{ + deleteErrors: []error{ + &ghAPI.HTTPError{StatusCode: http.StatusConflict}, + &ghAPI.HTTPError{StatusCode: http.StatusConflict}, + }, + } + var sleeps []time.Duration + manager := &fixtureRepositoryManager{ + client: client, + sleep: func(duration time.Duration) { + sleeps = append(sleeps, duration) + }, + } + + // When deferred cleanup deletes the repository + err := manager.delete("renamed") + + // Then only the conflicting operation is retried until deletion succeeds + require.NoError(t, err) + assert.Equal(t, []string{"renamed", "renamed", "renamed"}, client.deleted) + assert.Equal(t, []time.Duration{repositoryDeletePollInterval, repositoryDeletePollInterval}, sleeps) +} + +func TestRegisterFixtureRepositoryCleanup(t *testing.T) { + tests := []struct { + name string + skip bool + wantDeleted int + }{ + {name: "cleans up managed repositories", wantDeleted: 1}, + {name: "preserves managed repositories", skip: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := &fakeFixtureRepositoryClient{} + manager := &fixtureRepositoryManager{client: client} + _, err := manager.repository("shared") + require.NoError(t, err) + + t.Run("register cleanup", func(t *testing.T) { + registerFixtureRepositoryCleanup(t, tt.skip, manager) + }) + + assert.Len(t, client.deleted, tt.wantDeleted) + }) + } +} + +func TestLiveFixtureRepositoryClientUsesAPIHost(t *testing.T) { + var request *http.Request + var requestBody []byte + transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) { + var err error + request = req + requestBody, err = io.ReadAll(req.Body) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusCreated, + Status: "201 Created", + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(`{}`)), + Request: req, + }, nil + }) + client, err := newLiveFixtureRepositoryClientWithTransport(testScriptEnv{ + host: "github.com", + apiHost: "gateway.example.com", + org: "example", + token: "ghs_token", + }, transport) + require.NoError(t, err) + + require.NoError(t, client.create("fixture")) + require.NotNil(t, request) + assert.Equal(t, "gateway.example.com", request.URL.Host) + assert.Equal(t, "/orgs/example/repos", request.URL.Path) + assert.Equal(t, "token ghs_token", request.Header.Get("Authorization")) + assert.JSONEq(t, `{"name":"fixture","private":true,"auto_init":true,"has_discussions":true}`, string(requestBody)) +} + +func TestDeferredRepositoryCleanup(t *testing.T) { + file := filepath.Join(t.TempDir(), "cleanup.txt") + require.NoError(t, os.WriteFile(file, []byte("fixture-repo none\ndefer cleanup-repo stale-repo\n"), 0o600)) + + client := &fakeFixtureRepositoryClient{} + manager := &fixtureRepositoryManager{client: client} + tsEnv := testScriptEnv{ + host: "github.com", + org: "example", + token: "ghs_token", + } + t.Run("script", func(t *testing.T) { + testscript.Run(t, testscript.Params{ + Files: []string{file}, + Setup: sharedSetup(tsEnv), + Cmds: sharedCmds(tsEnv, manager), + RequireExplicitExec: true, + }) + }) + + assert.Equal(t, []string{"stale-repo"}, client.deleted) +} + +func TestManagedFixtureRejectsRepositoryCreation(t *testing.T) { + file := filepath.Join(t.TempDir(), "managed.txt") + script := "fixture-repo shared REPO\n" + + "! exec gh repo create example --private\n" + + "stderr 'requires.*fixture-repo none'\n" + require.NoError(t, os.WriteFile(file, []byte(script), 0o600)) + + client := &fakeFixtureRepositoryClient{} + manager := &fixtureRepositoryManager{client: client} + tsEnv := testScriptEnv{ + host: "github.com", + org: "example", + token: "ghs_token", + } + testscript.Run(t, testscript.Params{ + Files: []string{file}, + Setup: sharedSetup(tsEnv), + Cmds: sharedCmds(tsEnv, manager), + RequireExplicitExec: true, + }) +} diff --git a/acceptance/testdata/api/basic-graphql.txtar b/acceptance/testdata/api/basic-graphql.txtar index 15c16c49c7f..4a0e677c01f 100644 --- a/acceptance/testdata/api/basic-graphql.txtar +++ b/acceptance/testdata/api/basic-graphql.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Basic graphql request -exec gh api graphql -f query='query { viewer { login } }' -stdout '"login":' \ No newline at end of file +exec gh api graphql -f query='query { rateLimit { limit } }' --jq .data.rateLimit.limit +stdout '^[0-9]+$' \ No newline at end of file diff --git a/acceptance/testdata/api/basic-rest.txtar b/acceptance/testdata/api/basic-rest.txtar index 58d3b7570d2..f25aa9e4d53 100644 --- a/acceptance/testdata/api/basic-rest.txtar +++ b/acceptance/testdata/api/basic-rest.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Basic REST request -exec gh api /user -stdout '"login":' \ No newline at end of file +exec gh api /rate_limit --jq .rate.limit +stdout '^[0-9]+$' \ No newline at end of file diff --git a/acceptance/testdata/auth/auth-login-logout.txtar b/acceptance/testdata/auth/auth-login-logout.txtar index fb25f9eb54c..9fbb0274f76 100644 --- a/acceptance/testdata/auth/auth-login-logout.txtar +++ b/acceptance/testdata/auth/auth-login-logout.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # We aren't logged in at the moment, but GH_TOKEN will override the # need to login. We are going to clear GH_TOKEN first to ensure no # overrides are happening diff --git a/acceptance/testdata/auth/auth-setup-git.txtar b/acceptance/testdata/auth/auth-setup-git.txtar index e3be28cd51e..a8f631423f8 100644 --- a/acceptance/testdata/auth/auth-setup-git.txtar +++ b/acceptance/testdata/auth/auth-setup-git.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Check that the credential helper is unset for the host. This command is # expected to fail before gh auth setup-git is run. ! exec git config --get credential.https://${GH_HOST}.helper diff --git a/acceptance/testdata/auth/auth-status.txtar b/acceptance/testdata/auth/auth-status.txtar index 2afee1eb61b..bddc463d6bc 100644 --- a/acceptance/testdata/auth/auth-status.txtar +++ b/acceptance/testdata/auth/auth-status.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Check the authentication status exec gh auth status --hostname $GH_HOST stdout '✓ Logged in to ' \ No newline at end of file diff --git a/acceptance/testdata/auth/auth-token.txtar b/acceptance/testdata/auth/auth-token.txtar index 614d11817b4..2efcc6963a7 100644 --- a/acceptance/testdata/auth/auth-token.txtar +++ b/acceptance/testdata/auth/auth-token.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Check authentication token exec gh auth token --hostname $GH_HOST stdout $GH_TOKEN \ No newline at end of file diff --git a/acceptance/testdata/discussion/discussion-comment.txtar b/acceptance/testdata/discussion/discussion-comment.txtar index 3646b5e782e..351cad22792 100644 --- a/acceptance/testdata/discussion/discussion-comment.txtar +++ b/acceptance/testdata/discussion/discussion-comment.txtar @@ -1,15 +1,9 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO -# Enable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a discussion to comment on exec gh discussion create --title 'Comment Test' --body 'Discussion for comment tests' --category 'General' diff --git a/acceptance/testdata/discussion/discussion-create.txtar b/acceptance/testdata/discussion/discussion-create.txtar index 33ce90516f9..5df879d2405 100644 --- a/acceptance/testdata/discussion/discussion-create.txtar +++ b/acceptance/testdata/discussion/discussion-create.txtar @@ -1,22 +1,19 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo isolated REPO -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Explicitly disable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=false +exec gh api repos/$ORG/$REPO -X PATCH -F has_discussions=false # Creating a discussion should fail when discussions are disabled ! exec gh discussion create --title 'Fail' --body 'Body' --category 'General' stderr 'has discussions disabled' # Enable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true +exec gh api repos/$ORG/$REPO -X PATCH -F has_discussions=true # Create with title + body + category exec gh discussion create --title 'Basic Discussion' --body 'Basic body' --category 'General' diff --git a/acceptance/testdata/discussion/discussion-edit.txtar b/acceptance/testdata/discussion/discussion-edit.txtar index 4379eb8815c..fb1487d9d6f 100644 --- a/acceptance/testdata/discussion/discussion-edit.txtar +++ b/acceptance/testdata/discussion/discussion-edit.txtar @@ -1,15 +1,9 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO -# Enable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a discussion to edit exec gh discussion create --title 'Original Title' --body 'Original body' --category 'General' diff --git a/acceptance/testdata/discussion/discussion-list.txtar b/acceptance/testdata/discussion/discussion-list.txtar index fe3bc26dcc9..46a4fd096d4 100644 --- a/acceptance/testdata/discussion/discussion-list.txtar +++ b/acceptance/testdata/discussion/discussion-list.txtar @@ -1,22 +1,19 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo isolated REPO -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Explicitly disable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=false +exec gh api repos/$ORG/$REPO -X PATCH -F has_discussions=false # Listing discussions should fail when discussions are disabled ! exec gh discussion list stderr 'has discussions disabled' # Enable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true +exec gh api repos/$ORG/$REPO -X PATCH -F has_discussions=true # List before creating any discussions (empty results) exec gh discussion list diff --git a/acceptance/testdata/discussion/discussion-view.txtar b/acceptance/testdata/discussion/discussion-view.txtar index e258f710434..c046b607c26 100644 --- a/acceptance/testdata/discussion/discussion-view.txtar +++ b/acceptance/testdata/discussion/discussion-view.txtar @@ -1,22 +1,19 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo isolated REPO -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Explicitly disable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=false +exec gh api repos/$ORG/$REPO -X PATCH -F has_discussions=false # Viewing a discussion should fail when discussions are disabled ! exec gh discussion view 1 stderr 'has discussions disabled' # Enable discussions -exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true +exec gh api repos/$ORG/$REPO -X PATCH -F has_discussions=true # Create a discussion to view exec gh discussion create --title 'View Test' --body 'Discussion body content' --category 'General' --label bug,enhancement diff --git a/acceptance/testdata/extension/extension-env.txtar b/acceptance/testdata/extension/extension-env.txtar index b49b65829d7..23736428303 100644 --- a/acceptance/testdata/extension/extension-env.txtar +++ b/acceptance/testdata/extension/extension-env.txtar @@ -1,8 +1,11 @@ + # Verify that gh tells an extension when it is being run as an extension # Skip if Bash is not available given script extension [!exec:bash] skip +fixture-repo none + # Setup environment variables used for testscript env EXT_NAME=printenv-${RANDOM_STRING} env EXT_DIR=gh-${EXT_NAME} diff --git a/acceptance/testdata/extension/extension.txtar b/acceptance/testdata/extension/extension.txtar index a4d194757de..9bfdb1b6c8a 100644 --- a/acceptance/testdata/extension/extension.txtar +++ b/acceptance/testdata/extension/extension.txtar @@ -1,6 +1,9 @@ + # Skip if Bash is not available given script extension [!exec:bash] skip +fixture-repo none + # Setup environment variables used for testscript env EXT_NAME=${SCRIPT_NAME}-${RANDOM_STRING} env EXT_SCRIPT=gh-${EXT_NAME} diff --git a/acceptance/testdata/gist/gist-create-view-delete.txtar b/acceptance/testdata/gist/gist-create-view-delete.txtar index 86ed08f53fb..b1b4660eb33 100644 --- a/acceptance/testdata/gist/gist-create-view-delete.txtar +++ b/acceptance/testdata/gist/gist-create-view-delete.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Gists are owned by the authenticated user rather than an org, so unlike most # other acceptance scripts there is no repository to create or clean up. # diff --git a/acceptance/testdata/gist/gist-edit-rename-list.txtar b/acceptance/testdata/gist/gist-edit-rename-list.txtar index 537e63b9295..5a91cb5dee2 100644 --- a/acceptance/testdata/gist/gist-edit-rename-list.txtar +++ b/acceptance/testdata/gist/gist-edit-rename-list.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Gists are owned by the authenticated user rather than an org, so unlike most # other acceptance scripts there is no repository to create or clean up. # diff --git a/acceptance/testdata/gpg-key/gpg-key.txtar b/acceptance/testdata/gpg-key/gpg-key.txtar index 8f0d7154567..81e3bf9f8b8 100644 --- a/acceptance/testdata/gpg-key/gpg-key.txtar +++ b/acceptance/testdata/gpg-key/gpg-key.txtar @@ -1,5 +1,8 @@ + skip 'it modifies the user''s personal GitHub account GPG keys' +fixture-repo none + # This test requires the admin:gpg_key scope to add and delete GPG keys to and # from the user's personal GitHub account. # This test uses a GPG key that generated for this test only. The private key diff --git a/acceptance/testdata/issue/issue-comment-edit-last-with-comments.txtar b/acceptance/testdata/issue/issue-comment-edit-last-with-comments.txtar index a30286ce38c..2c43c1c4f95 100644 --- a/acceptance/testdata/issue/issue-comment-edit-last-with-comments.txtar +++ b/acceptance/testdata/issue/issue-comment-edit-last-with-comments.txtar @@ -1,18 +1,15 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone ${ORG}/${REPO} +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd ${REPO} -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' stdout2env ISSUE_URL # Comment on the issue diff --git a/acceptance/testdata/issue/issue-comment-edit-last-without-comments-creates.txtar b/acceptance/testdata/issue/issue-comment-edit-last-without-comments-creates.txtar index 44532680b7b..bf063a578ef 100644 --- a/acceptance/testdata/issue/issue-comment-edit-last-without-comments-creates.txtar +++ b/acceptance/testdata/issue/issue-comment-edit-last-without-comments-creates.txtar @@ -1,18 +1,15 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone ${ORG}/${REPO} +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd ${REPO} -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' stdout2env ISSUE_URL # Comment on the issue diff --git a/acceptance/testdata/issue/issue-comment-edit-last-without-comments-errors.txtar b/acceptance/testdata/issue/issue-comment-edit-last-without-comments-errors.txtar index 30013118043..64e27c599e7 100644 --- a/acceptance/testdata/issue/issue-comment-edit-last-without-comments-errors.txtar +++ b/acceptance/testdata/issue/issue-comment-edit-last-without-comments-errors.txtar @@ -1,18 +1,15 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone ${ORG}/${REPO} +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd ${REPO} -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' stdout2env ISSUE_URL # Comment on the issue diff --git a/acceptance/testdata/issue/issue-comment-new.txtar b/acceptance/testdata/issue/issue-comment-new.txtar index 12524b6d5fa..a8795883963 100644 --- a/acceptance/testdata/issue/issue-comment-new.txtar +++ b/acceptance/testdata/issue/issue-comment-new.txtar @@ -1,18 +1,15 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone ${ORG}/${REPO} +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd ${REPO} -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' stdout2env ISSUE_URL # Comment on the issue diff --git a/acceptance/testdata/issue/issue-create-basic.txtar b/acceptance/testdata/issue/issue-create-basic.txtar index ddba28eec23..272e329b57c 100644 --- a/acceptance/testdata/issue/issue-create-basic.txtar +++ b/acceptance/testdata/issue/issue-create-basic.txtar @@ -1,17 +1,15 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' stdout2env ISSUE_URL # Check the issue was created exec gh issue view $ISSUE_URL -stdout 'title:\tFeature Request$' +stdout 'title:\t'$ISSUE_TITLE'$' diff --git a/acceptance/testdata/issue/issue-create-edit-with-project.txtar b/acceptance/testdata/issue/issue-create-edit-with-project.txtar index 568530cb9d3..f20f02992a4 100644 --- a/acceptance/testdata/issue/issue-create-edit-with-project.txtar +++ b/acceptance/testdata/issue/issue-create-edit-with-project.txtar @@ -1,43 +1,39 @@ -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Create a project -env PROJECT_TITLE=${REPO}-project +env PROJECT_TITLE=${SCRIPT_NAME}-${RANDOM_STRING}-project exec gh project create --owner=${ORG} --title=${PROJECT_TITLE} --format='json' --jq='.number' stdout2env PROJECT_NUMBER defer gh project delete --owner=${ORG} ${PROJECT_NUMBER} -# Clone the repo -exec gh repo clone ${ORG}/${REPO} +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd ${REPO} -exec gh issue create --title 'Feature Request' --body 'Feature Body' --project ${PROJECT_TITLE} +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' --project ${PROJECT_TITLE} stdout2env ISSUE_URL # Check that default issue view is working exec gh issue view ${ISSUE_URL} # Check the issue was added to the project -exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[0].title' +exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[].title' stdout ${PROJECT_TITLE} # Remove the issue from the project exec gh issue edit ${ISSUE_URL} --remove-project ${PROJECT_TITLE} # Check the issue was removed from the project -exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[0].title' +exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[].title' ! stdout ${PROJECT_TITLE} # Re add the issue to the project exec gh issue edit ${ISSUE_URL} --add-project ${PROJECT_TITLE} # Check the issue was added to the project -exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[0].title' +exec gh issue view ${ISSUE_URL} --json projectItems --jq '.projectItems[].title' stdout ${PROJECT_TITLE} diff --git a/acceptance/testdata/issue/issue-create-with-metadata.txtar b/acceptance/testdata/issue/issue-create-with-metadata.txtar index 8187f96cf0f..b2b8bcb3e31 100644 --- a/acceptance/testdata/issue/issue-create-with-metadata.txtar +++ b/acceptance/testdata/issue/issue-create-with-metadata.txtar @@ -1,19 +1,17 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh issue create --title 'Feature Request' --body 'Feature Body' --assignee '@me' --label 'bug' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' --assignee '@me' --label 'bug' stdout2env ISSUE_URL # Check the issue was create exec gh issue view $ISSUE_URL -stdout 'title:\tFeature Request$' +stdout 'title:\t'$ISSUE_TITLE'$' stdout 'assignees:\t.+$' stdout 'labels:\tbug$' diff --git a/acceptance/testdata/issue/issue-develop-worktree-cross-repo.txtar b/acceptance/testdata/issue/issue-develop-worktree-cross-repo.txtar index 7a084f80af7..601ad2f0c41 100644 --- a/acceptance/testdata/issue/issue-develop-worktree-cross-repo.txtar +++ b/acceptance/testdata/issue/issue-develop-worktree-cross-repo.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Develop an issue in a branch repository worktree, then reuse that worktree. # Set up env vars @@ -7,12 +10,12 @@ env BRANCH_REPO=${SCRIPT_NAME}-branch-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git -# Create the issue and branch repositories +# Create the issue repository exec gh repo create ${ORG}/${ISSUE_REPO} --add-readme --private -exec gh repo create ${ORG}/${BRANCH_REPO} --add-readme --private - -# Defer repo cleanup defer gh repo delete --yes ${ORG}/${ISSUE_REPO} + +# Create the branch repository +exec gh repo create ${ORG}/${BRANCH_REPO} --add-readme --private defer gh repo delete --yes ${ORG}/${BRANCH_REPO} # Clone the issue repository and configure the branch repository as a remote diff --git a/acceptance/testdata/issue/issue-develop-worktree.txtar b/acceptance/testdata/issue/issue-develop-worktree.txtar index ef692064414..5e8ebd5fa0a 100644 --- a/acceptance/testdata/issue/issue-develop-worktree.txtar +++ b/acceptance/testdata/issue/issue-develop-worktree.txtar @@ -1,49 +1,48 @@ + # Develop an issue in a fresh worktree, reuse that worktree, and add another # worktree after the local branch exists. # Set up env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} cd ${REPO} # Create an issue to develop -exec gh issue create --title 'Feature Request' --body 'Request Body' +exec gh issue create --title $ISSUE_TITLE --body 'Request Body' stdout2env ISSUE_URL # Targeting an existing, non-empty directory that is not a linked worktree is # rejected before any branch is created, so the issue is left without an orphan # linked branch on the remote. exists ../occupied/keep.txt -! exec gh issue develop ${ISSUE_URL} --name feature-branch --checkout --worktree ../occupied +! exec gh issue develop ${ISSUE_URL} --name $BRANCH_NAME --checkout --worktree ../occupied stderr '--worktree path must be empty' exec gh issue develop --list ${ISSUE_URL} ! stdout . # Develop the issue in a fresh worktree -exec gh issue develop ${ISSUE_URL} --name feature-branch --checkout --worktree ../wt +exec gh issue develop ${ISSUE_URL} --name $BRANCH_NAME --checkout --worktree ../wt exists ../wt exec git -C ../wt rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The main working copy stays on the default branch exec git rev-parse --abbrev-ref HEAD stdout '(?m)^main$' # Developing the same issue into the same path reuses the linked worktree -exec gh issue develop ${ISSUE_URL} --name feature-branch --checkout --worktree ../wt +exec gh issue develop ${ISSUE_URL} --name $BRANCH_NAME --checkout --worktree ../wt exec git -C ../wt rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The main working copy remains untouched exec git rev-parse --abbrev-ref HEAD @@ -51,10 +50,10 @@ stdout '(?m)^main$' # Removing the worktree and using a fresh path adds the existing local branch exec git worktree remove ../wt -exec gh issue develop ${ISSUE_URL} --name feature-branch --checkout --worktree ../wt2 +exec gh issue develop ${ISSUE_URL} --name $BRANCH_NAME --checkout --worktree ../wt2 exists ../wt2 exec git -C ../wt2 rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The main working copy remains untouched exec git rev-parse --abbrev-ref HEAD diff --git a/acceptance/testdata/issue/issue-list.txtar b/acceptance/testdata/issue/issue-list.txtar index 5a810f5e1a7..76a0647ef1c 100644 --- a/acceptance/testdata/issue/issue-list.txtar +++ b/acceptance/testdata/issue/issue-list.txtar @@ -1,16 +1,14 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' -# Check the issue is included in the list output -exec gh issue list -stdout 'OPEN\tFeature Request' +# Check the issue is included in the paginated default output +exec gh issue list --limit 1000 +stdout 'OPEN\t'$ISSUE_TITLE diff --git a/acceptance/testdata/issue/issue-view.txtar b/acceptance/testdata/issue/issue-view.txtar index ddba28eec23..272e329b57c 100644 --- a/acceptance/testdata/issue/issue-view.txtar +++ b/acceptance/testdata/issue/issue-view.txtar @@ -1,17 +1,15 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh issue create --title 'Feature Request' --body 'Feature Body' +exec gh issue create --title $ISSUE_TITLE --body 'Feature Body' stdout2env ISSUE_URL # Check the issue was created exec gh issue view $ISSUE_URL -stdout 'title:\tFeature Request$' +stdout 'title:\t'$ISSUE_TITLE'$' diff --git a/acceptance/testdata/issues-2.0/issue-create-and-edit-issue-type.txtar b/acceptance/testdata/issues-2.0/issue-create-and-edit-issue-type.txtar index a1fde33a96c..ae0cb79f5fa 100644 --- a/acceptance/testdata/issues-2.0/issue-create-and-edit-issue-type.txtar +++ b/acceptance/testdata/issues-2.0/issue-create-and-edit-issue-type.txtar @@ -1,16 +1,13 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_PREFIX=${SCRIPT_NAME}-${RANDOM_STRING} -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue with --type -exec gh issue create --title 'with type' --body '' --type 'Bug' +exec gh issue create --title $ISSUE_PREFIX-with-type --body '' --type 'Bug' stdout2env ISSUE_URL # Confirm the type stuck diff --git a/acceptance/testdata/issues-2.0/issue-create-and-edit-parent.txtar b/acceptance/testdata/issues-2.0/issue-create-and-edit-parent.txtar index 707fc1df793..40a3efbbf55 100644 --- a/acceptance/testdata/issues-2.0/issue-create-and-edit-parent.txtar +++ b/acceptance/testdata/issues-2.0/issue-create-and-edit-parent.txtar @@ -1,20 +1,17 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_PREFIX=${SCRIPT_NAME}-${RANDOM_STRING} -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create the parent issue -exec gh issue create --title 'parent' --body '' +exec gh issue create --title $ISSUE_PREFIX-parent --body '' stdout2env PARENT_URL # Create a child via --parent on create -exec gh issue create --title 'child via create' --body '' --parent $PARENT_URL +exec gh issue create --title $ISSUE_PREFIX-child-via-create --body '' --parent $PARENT_URL stdout2env CHILD_URL # Confirm parent is set diff --git a/acceptance/testdata/issues-2.0/issue-create-and-edit-relationships.txtar b/acceptance/testdata/issues-2.0/issue-create-and-edit-relationships.txtar index 4ea8762a79c..6b024ef095e 100644 --- a/acceptance/testdata/issues-2.0/issue-create-and-edit-relationships.txtar +++ b/acceptance/testdata/issues-2.0/issue-create-and-edit-relationships.txtar @@ -1,23 +1,20 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_PREFIX=${SCRIPT_NAME}-${RANDOM_STRING} -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create the two helper issues that the main issue will block / be blocked by -exec gh issue create --title 'blocker' --body '' +exec gh issue create --title $ISSUE_PREFIX-blocker --body '' stdout2env BLOCKER_URL -exec gh issue create --title 'blocked' --body '' +exec gh issue create --title $ISSUE_PREFIX-blocked --body '' stdout2env BLOCKED_URL # Create the main issue with both relationships set on create -exec gh issue create --title 'main' --body '' --blocked-by $BLOCKER_URL --blocking $BLOCKED_URL +exec gh issue create --title $ISSUE_PREFIX-main --body '' --blocked-by $BLOCKER_URL --blocking $BLOCKED_URL stdout2env MAIN_URL # Confirm both relationships landed @@ -28,10 +25,10 @@ exec gh issue view $MAIN_URL --json blocking --jq '.blocking.nodes[].url' stdout $BLOCKED_URL # Add a second blocker / blocked via edit -exec gh issue create --title 'blocker 2' --body '' +exec gh issue create --title $ISSUE_PREFIX-blocker-2 --body '' stdout2env BLOCKER_2_URL -exec gh issue create --title 'blocked 2' --body '' +exec gh issue create --title $ISSUE_PREFIX-blocked-2 --body '' stdout2env BLOCKED_2_URL exec gh issue edit $MAIN_URL --add-blocked-by $BLOCKER_2_URL --add-blocking $BLOCKED_2_URL @@ -46,9 +43,9 @@ stdout '^2$' exec gh issue edit $MAIN_URL --remove-blocked-by $BLOCKER_URL --remove-blocking $BLOCKED_URL exec gh issue view $MAIN_URL --json blockedBy --jq '.blockedBy.nodes[].title' -stdout '^blocker 2$' -! stdout '^blocker$' +stdout '^'$ISSUE_PREFIX'-blocker-2$' +! stdout '^'$ISSUE_PREFIX'-blocker$' exec gh issue view $MAIN_URL --json blocking --jq '.blocking.nodes[].title' -stdout '^blocked 2$' -! stdout '^blocked$' +stdout '^'$ISSUE_PREFIX'-blocked-2$' +! stdout '^'$ISSUE_PREFIX'-blocked$' diff --git a/acceptance/testdata/issues-2.0/issue-edit-sub-issues.txtar b/acceptance/testdata/issues-2.0/issue-edit-sub-issues.txtar index 94d0c9621ae..f5e74f1d75c 100644 --- a/acceptance/testdata/issues-2.0/issue-edit-sub-issues.txtar +++ b/acceptance/testdata/issues-2.0/issue-edit-sub-issues.txtar @@ -1,22 +1,19 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_PREFIX=${SCRIPT_NAME}-${RANDOM_STRING} -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create three issues: parent A, parent B, candidate C -exec gh issue create --title 'parent A' --body '' +exec gh issue create --title $ISSUE_PREFIX-parent-a --body '' stdout2env PARENT_A_URL -exec gh issue create --title 'parent B' --body '' +exec gh issue create --title $ISSUE_PREFIX-parent-b --body '' stdout2env PARENT_B_URL -exec gh issue create --title 'candidate C' --body '' +exec gh issue create --title $ISSUE_PREFIX-candidate-c --body '' stdout2env CANDIDATE_URL # Add C as a sub-issue of A diff --git a/acceptance/testdata/issues-2.0/issue-list-filter-by-type.txtar b/acceptance/testdata/issues-2.0/issue-list-filter-by-type.txtar index 5150857c6f5..35574acb69c 100644 --- a/acceptance/testdata/issues-2.0/issue-list-filter-by-type.txtar +++ b/acceptance/testdata/issues-2.0/issue-list-filter-by-type.txtar @@ -1,21 +1,18 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_PREFIX=${SCRIPT_NAME}-${RANDOM_STRING} -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create one Bug-typed issue and one untyped issue -exec gh issue create --title 'typed-bug' --body '' --type 'Bug' -exec gh issue create --title 'untyped' --body '' +exec gh issue create --title $ISSUE_PREFIX-typed-bug --body '' --type 'Bug' +exec gh issue create --title $ISSUE_PREFIX-untyped --body '' sleep 3 # Filtering by type returns only the typed issue -exec gh issue list --type 'Bug' -stdout 'typed-bug' -! stdout 'untyped' +exec gh issue list --type 'Bug' --limit 1000 --json title --jq='.[].title' +stdout $ISSUE_PREFIX'-typed-bug' +! stdout $ISSUE_PREFIX'-untyped' diff --git a/acceptance/testdata/issues-2.0/issue-view-issues-2.0-fields.txtar b/acceptance/testdata/issues-2.0/issue-view-issues-2.0-fields.txtar index f73043de2dd..5db98697ef5 100644 --- a/acceptance/testdata/issues-2.0/issue-view-issues-2.0-fields.txtar +++ b/acceptance/testdata/issues-2.0/issue-view-issues-2.0-fields.txtar @@ -1,29 +1,26 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_PREFIX=${SCRIPT_NAME}-${RANDOM_STRING} -cd $SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a parent, a sub-issue, a blocker, and a blocked target -exec gh issue create --title 'parent' --body '' +exec gh issue create --title $ISSUE_PREFIX-parent --body '' stdout2env PARENT_URL -exec gh issue create --title 'sub' --body '' +exec gh issue create --title $ISSUE_PREFIX-sub --body '' stdout2env SUB_URL -exec gh issue create --title 'blocker' --body '' +exec gh issue create --title $ISSUE_PREFIX-blocker --body '' stdout2env BLOCKER_URL -exec gh issue create --title 'blocked' --body '' +exec gh issue create --title $ISSUE_PREFIX-blocked --body '' stdout2env BLOCKED_URL # Create the main issue wired up to all four -exec gh issue create --title 'main' --body '' --type 'Bug' --parent $PARENT_URL --blocked-by $BLOCKER_URL --blocking $BLOCKED_URL +exec gh issue create --title $ISSUE_PREFIX-main --body '' --type 'Bug' --parent $PARENT_URL --blocked-by $BLOCKER_URL --blocking $BLOCKED_URL stdout2env MAIN_URL # Attach the sub-issue diff --git a/acceptance/testdata/label/label.txtar b/acceptance/testdata/label/label.txtar index 3acfd4a64ce..27bfe2f751a 100644 --- a/acceptance/testdata/label/label.txtar +++ b/acceptance/testdata/label/label.txtar @@ -1,25 +1,23 @@ + # Setup useful env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository -exec gh repo create ${ORG}/${REPO} --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env LABEL_NAME=acceptance-test-${RANDOM_STRING} # Set the GH_REPO env var to reduce redundant flags env GH_REPO=${ORG}/${REPO} # Create a custom label -exec gh label create 'acceptance-test' --description 'First Description' +exec gh label create $LABEL_NAME --description 'First Description' # List the labels and check our custom label is there -exec gh label list -stdout 'acceptance-test\tFirst Description' +exec gh label list --limit 1000 +stdout $LABEL_NAME'\tFirst Description' # Edit the label -exec gh label edit 'acceptance-test' --description 'Edited Description' +exec gh label edit $LABEL_NAME --description 'Edited Description' # List the labels and check our custom label has been updated -exec gh label list -stdout 'acceptance-test\tEdited Description' +exec gh label list --limit 1000 +stdout $LABEL_NAME'\tEdited Description' diff --git a/acceptance/testdata/org/org-list.txtar b/acceptance/testdata/org/org-list.txtar index ca114babeb1..fe0b7fae732 100644 --- a/acceptance/testdata/org/org-list.txtar +++ b/acceptance/testdata/org/org-list.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # This test could fail if the user is a member of more than 30 organizations because # the `gh org list` command only returns the first 30 organizations the user is a member of diff --git a/acceptance/testdata/pr/pr-checkout-by-number.txtar b/acceptance/testdata/pr/pr-checkout-by-number.txtar index 374926f1d27..cba74cd88c5 100644 --- a/acceptance/testdata/pr/pr-checkout-by-number.txtar +++ b/acceptance/testdata/pr/pr-checkout-by-number.txtar @@ -1,33 +1,37 @@ + # Set up env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL +# Capture the created PR number +exec gh pr view $PR_URL --json number --jq .number +stdout2env PR_NUMBER + # Remove the local branch exec git checkout main -exec git branch -D feature-branch -stdout 'Deleted branch feature-branch' +exec git branch -D $BRANCH_NAME +stdout $BRANCH_NAME # Checkout the PR -exec gh pr checkout 1 -stderr 'Switched to a new branch ''feature-branch''' +exec gh pr checkout $PR_NUMBER +exec git branch --show-current +stdout '^'$BRANCH_NAME'$' diff --git a/acceptance/testdata/pr/pr-checkout-with-url-from-fork.txtar b/acceptance/testdata/pr/pr-checkout-with-url-from-fork.txtar index 637422a5aef..c74a041632b 100644 --- a/acceptance/testdata/pr/pr-checkout-with-url-from-fork.txtar +++ b/acceptance/testdata/pr/pr-checkout-with-url-from-fork.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Set up env vars env REPO=${SCRIPT_NAME}-${RANDOM_STRING} diff --git a/acceptance/testdata/pr/pr-checkout-worktree-detach.txtar b/acceptance/testdata/pr/pr-checkout-worktree-detach.txtar index c0feec4e2e4..cc10394d08c 100644 --- a/acceptance/testdata/pr/pr-checkout-worktree-detach.txtar +++ b/acceptance/testdata/pr/pr-checkout-worktree-detach.txtar @@ -1,28 +1,27 @@ + # Checkout a PR into a worktree with a detached HEAD, then reuse that worktree. # Set up env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Return to the default branch @@ -38,7 +37,7 @@ exists ../wt # The detached HEAD points at the PR head commit exec git -C ../wt rev-parse HEAD stdout2env WT_HEAD -exec git rev-parse origin/feature-branch +exec git rev-parse origin/$BRANCH_NAME stdout ${WT_HEAD} # The main working copy is left untouched diff --git a/acceptance/testdata/pr/pr-checkout-worktree-from-fork.txtar b/acceptance/testdata/pr/pr-checkout-worktree-from-fork.txtar index 8d9fc89cc2e..d8c675b60d7 100644 --- a/acceptance/testdata/pr/pr-checkout-worktree-from-fork.txtar +++ b/acceptance/testdata/pr/pr-checkout-worktree-from-fork.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Checkout a fork PR whose head repository is not configured as a remote into a # worktree, then reuse that worktree. diff --git a/acceptance/testdata/pr/pr-checkout-worktree.txtar b/acceptance/testdata/pr/pr-checkout-worktree.txtar index afde97b3508..a7f8c817ffb 100644 --- a/acceptance/testdata/pr/pr-checkout-worktree.txtar +++ b/acceptance/testdata/pr/pr-checkout-worktree.txtar @@ -1,35 +1,35 @@ + # Checkout a PR into a git worktree, then reuse that worktree, rename its branch, # force sync it, and add a second worktree once the local branch already exists. # Set up env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +env RENAMED_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-renamed # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Remove the local branch so checkout has to create it from the remote exec git checkout main -exec git branch -D feature-branch -stdout 'Deleted branch feature-branch' +exec git branch -D $BRANCH_NAME +stdout $BRANCH_NAME # Targeting an existing, non-empty directory that is not a linked worktree is # rejected up front with a clear error instead of deferring to git worktree add. @@ -43,7 +43,7 @@ exists ../wt # The worktree is on the PR branch exec git -C ../wt rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The main working copy stays on the default branch exec git rev-parse --abbrev-ref HEAD @@ -52,16 +52,16 @@ stdout '(?m)^main$' # Checking out the same PR into the same worktree again reuses it exec gh pr checkout ${PR_URL} --worktree ../wt exec git -C ../wt rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The main working copy is left untouched exec git rev-parse --abbrev-ref HEAD stdout '(?m)^main$' # Checking out into the reused worktree with a new branch name creates that branch -exec gh pr checkout ${PR_URL} --worktree ../wt --branch renamed-branch +exec gh pr checkout ${PR_URL} --worktree ../wt --branch $RENAMED_BRANCH exec git -C ../wt rev-parse --abbrev-ref HEAD -stdout '(?m)^renamed-branch$' +stdout '(?m)^'$RENAMED_BRANCH'$' # The main working copy is left untouched exec git rev-parse --abbrev-ref HEAD @@ -70,13 +70,13 @@ stdout '(?m)^main$' # Give the worktree's PR branch a local commit so it diverges from the PR head. # A plain reuse would fast-forward-only merge and keep this commit, so --force is # required to discard it with a hard reset. -exec git -C ../wt checkout feature-branch +exec git -C ../wt checkout $BRANCH_NAME exec git -C ../wt commit --allow-empty -m 'Diverging local commit' # Force checking out the PR into the reused worktree hard resets it to the PR head exec gh pr checkout ${PR_URL} --worktree ../wt --force exec git -C ../wt rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The diverging local commit was discarded by the hard reset exec git -C ../wt log -1 --format=%s @@ -85,7 +85,7 @@ exec git -C ../wt log -1 --format=%s # The worktree branch now matches the PR head exec git -C ../wt rev-parse HEAD stdout2env WT_HEAD -exec git rev-parse origin/feature-branch +exec git rev-parse origin/$BRANCH_NAME stdout ${WT_HEAD} # The main working copy is left untouched @@ -98,7 +98,7 @@ exec git worktree remove ../wt exec gh pr checkout ${PR_URL} --worktree ../wt2 exists ../wt2 exec git -C ../wt2 rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' # The main working copy is left untouched exec git rev-parse --abbrev-ref HEAD @@ -107,7 +107,7 @@ stdout '(?m)^main$' # Now create a two-sided divergence: advance the remote PR branch with a pushed # commit the local branch will not have... exec git -C ../wt2 commit --allow-empty -m 'Remote commit' -exec git -C ../wt2 push origin feature-branch +exec git -C ../wt2 push origin $BRANCH_NAME # ...then rewind the local branch and give it a different, local-only commit, so # neither branch is an ancestor of the other @@ -125,7 +125,7 @@ stdout 'Local commit' # Forcing the checkout hard resets the branch to the advanced PR head exec gh pr checkout ${PR_URL} --worktree ../wt2 --force exec git -C ../wt2 rev-parse --abbrev-ref HEAD -stdout '(?m)^feature-branch$' +stdout '(?m)^'$BRANCH_NAME'$' exec git -C ../wt2 log -1 --format=%s stdout 'Remote commit' ! stdout 'Local commit' @@ -133,7 +133,7 @@ stdout 'Remote commit' # The worktree branch now matches the advanced PR head exec git -C ../wt2 rev-parse HEAD stdout2env WT2_HEAD -exec git rev-parse origin/feature-branch +exec git rev-parse origin/$BRANCH_NAME stdout ${WT2_HEAD} # The main working copy is left untouched diff --git a/acceptance/testdata/pr/pr-checkout.txtar b/acceptance/testdata/pr/pr-checkout.txtar index 4cfe96c1a08..b14a41a086d 100644 --- a/acceptance/testdata/pr/pr-checkout.txtar +++ b/acceptance/testdata/pr/pr-checkout.txtar @@ -1,30 +1,31 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Prepare a branch to PR -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +cd $REPO +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Remove the local branch exec git checkout main -exec git branch -D feature-branch -stdout 'Deleted branch feature-branch' +exec git branch -D $BRANCH_NAME +stdout $BRANCH_NAME # Checkout the PR exec gh pr checkout $PR_URL -stderr 'Switched to a new branch ''feature-branch''' +exec git branch --show-current +stdout '^'$BRANCH_NAME'$' diff --git a/acceptance/testdata/pr/pr-comment-edit-last-with-comments.txtar b/acceptance/testdata/pr/pr-comment-edit-last-with-comments.txtar index be4650ffa28..591346e4169 100644 --- a/acceptance/testdata/pr/pr-comment-edit-last-with-comments.txtar +++ b/acceptance/testdata/pr/pr-comment-edit-last-with-comments.txtar @@ -1,27 +1,25 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch - +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Comment on the PR diff --git a/acceptance/testdata/pr/pr-comment-edit-last-without-comments-creates.txtar b/acceptance/testdata/pr/pr-comment-edit-last-without-comments-creates.txtar index e6205737f78..faa6d87bdf1 100644 --- a/acceptance/testdata/pr/pr-comment-edit-last-without-comments-creates.txtar +++ b/acceptance/testdata/pr/pr-comment-edit-last-without-comments-creates.txtar @@ -1,27 +1,25 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch - +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Comment on the PR diff --git a/acceptance/testdata/pr/pr-comment-edit-last-without-comments-errors.txtar b/acceptance/testdata/pr/pr-comment-edit-last-without-comments-errors.txtar index 3a70adb7236..de22fb63b97 100644 --- a/acceptance/testdata/pr/pr-comment-edit-last-without-comments-errors.txtar +++ b/acceptance/testdata/pr/pr-comment-edit-last-without-comments-errors.txtar @@ -1,27 +1,25 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch - +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Comment on the PR diff --git a/acceptance/testdata/pr/pr-comment-new.txtar b/acceptance/testdata/pr/pr-comment-new.txtar index c5b80314c45..9cb9ef29177 100644 --- a/acceptance/testdata/pr/pr-comment-new.txtar +++ b/acceptance/testdata/pr/pr-comment-new.txtar @@ -1,26 +1,25 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Comment on the PR diff --git a/acceptance/testdata/pr/pr-create-basic.txtar b/acceptance/testdata/pr/pr-create-basic.txtar index 98bb2faa9dc..72618e7d71b 100644 --- a/acceptance/testdata/pr/pr-create-basic.txtar +++ b/acceptance/testdata/pr/pr-create-basic.txtar @@ -1,24 +1,24 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Prepare a branch to PR -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +cd $REPO +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' # Check the PR is indeed created exec gh pr view -stdout 'Feature Title' +stdout $PR_TITLE diff --git a/acceptance/testdata/pr/pr-create-edit-with-project.txtar b/acceptance/testdata/pr/pr-create-edit-with-project.txtar index 9850313f0a0..028b1efc1e5 100644 --- a/acceptance/testdata/pr/pr-create-edit-with-project.txtar +++ b/acceptance/testdata/pr/pr-create-edit-with-project.txtar @@ -1,16 +1,14 @@ + # Use gh as a credential helper exec gh auth setup-git -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} - # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Create a project -env PROJECT_TITLE=${REPO}-project +env PROJECT_TITLE=${SCRIPT_NAME}-${RANDOM_STRING}-project exec gh project create --owner=${ORG} --title=${PROJECT_TITLE} --format='json' --jq='.number' stdout2env PROJECT_NUMBER @@ -21,31 +19,31 @@ exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' --project ${PROJECT_TITLE} +exec gh pr create --title $PR_TITLE --body 'Feature Body' --project ${PROJECT_TITLE} stdout2env PR_URL # Check that default pr view is working exec gh pr view ${PR_URL} # Check the pr was added to the project -exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[0].title' +exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[].title' stdout ${PROJECT_TITLE} # Remove the pr from the project exec gh pr edit ${PR_URL} --remove-project ${PROJECT_TITLE} # Check the pr was removed from the project -exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[0].title' +exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[].title' ! stdout ${PROJECT_TITLE} # Re add the pr to the project exec gh pr edit ${PR_URL} --add-project ${PROJECT_TITLE} # Check the pr was added to the project -exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[0].title' +exec gh pr view ${PR_URL} --json projectItems --jq '.projectItems[].title' stdout ${PROJECT_TITLE} diff --git a/acceptance/testdata/pr/pr-create-from-issue-develop-base.txtar b/acceptance/testdata/pr/pr-create-from-issue-develop-base.txtar index f0619940e06..3a2826ecb2e 100644 --- a/acceptance/testdata/pr/pr-create-from-issue-develop-base.txtar +++ b/acceptance/testdata/pr/pr-create-from-issue-develop-base.txtar @@ -1,37 +1,38 @@ + # Set up env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +env BASE_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-base +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING}-issue # Clone the repo exec gh repo clone ${ORG}/${REPO} # Create a branch to act as the merge base branch cd ${REPO} -exec git checkout -b long-lived-feature-branch -exec git push -u origin long-lived-feature-branch +exec git checkout -b $BASE_BRANCH +exec git push -u origin $BASE_BRANCH # Create an issue to develop against -exec gh issue create --title 'Feature Request' --body 'Request Body' +exec gh issue create --title $ISSUE_TITLE --body 'Request Body' stdout2env ISSUE_URL # Create a new branch using issue develop with the long lived branch as the base -exec gh issue develop --name 'feature-branch' --base 'long-lived-feature-branch' --checkout ${ISSUE_URL} +exec gh issue develop --name $BRANCH_NAME --base $BASE_BRANCH --checkout ${ISSUE_URL} # Prepare a PR on the develop branch exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' # Check the PR is created against the base branch we specified exec gh pr view --json 'baseRefName' --jq '.baseRefName' -stdout 'long-lived-feature-branch' +stdout $BASE_BRANCH diff --git a/acceptance/testdata/pr/pr-create-from-manual-merge-base.txtar b/acceptance/testdata/pr/pr-create-from-manual-merge-base.txtar index 97ae168f539..9e99147c5a4 100644 --- a/acceptance/testdata/pr/pr-create-from-manual-merge-base.txtar +++ b/acceptance/testdata/pr/pr-create-from-manual-merge-base.txtar @@ -1,34 +1,34 @@ + # Set up env vars -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +env BASE_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-base # Clone the repo exec gh repo clone ${ORG}/${REPO} # Create a branch to act as the merge base branch cd ${REPO} -exec git checkout -b long-lived-feature-branch -exec git push -u origin long-lived-feature-branch +exec git checkout -b $BASE_BRANCH +exec git push -u origin $BASE_BRANCH # Prepare a branch from the merge base to PR -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Set the merge-base branch config -exec git config 'branch.feature-branch.gh-merge-base' 'long-lived-feature-branch' +exec git config branch.$BRANCH_NAME.gh-merge-base $BASE_BRANCH # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' # Check the PR is created against the merge base branch exec gh pr view --json 'baseRefName' --jq '.baseRefName' -stdout 'long-lived-feature-branch' +stdout $BASE_BRANCH diff --git a/acceptance/testdata/pr/pr-create-guesses-remote-from-sha-with-branch-name-slash.txtar b/acceptance/testdata/pr/pr-create-guesses-remote-from-sha-with-branch-name-slash.txtar index c3717ab2314..363fce2f8d2 100644 --- a/acceptance/testdata/pr/pr-create-guesses-remote-from-sha-with-branch-name-slash.txtar +++ b/acceptance/testdata/pr/pr-create-guesses-remote-from-sha-with-branch-name-slash.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-guesses-remote-from-sha.txtar b/acceptance/testdata/pr/pr-create-guesses-remote-from-sha.txtar index 6359672e158..d290a5ab98e 100644 --- a/acceptance/testdata/pr/pr-create-guesses-remote-from-sha.txtar +++ b/acceptance/testdata/pr/pr-create-guesses-remote-from-sha.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-no-local-repo.txtar b/acceptance/testdata/pr/pr-create-no-local-repo.txtar index cb42d99f829..5caadacc3e5 100644 --- a/acceptance/testdata/pr/pr-create-no-local-repo.txtar +++ b/acceptance/testdata/pr/pr-create-no-local-repo.txtar @@ -1,27 +1,28 @@ + # Use gh as a credential helper exec gh auth setup-git # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Leave the repo so there's no local repo cd ${WORK} # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' --repo ${ORG}/${REPO} --head feature-branch -stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1 \ No newline at end of file +exec gh pr create --title $PR_TITLE --body 'Feature Body' --repo ${ORG}/${REPO} --head $BRANCH_NAME +stdout2env PR_URL +exec gh pr view $PR_URL --json headRefName --jq .headRefName +stdout $BRANCH_NAME \ No newline at end of file diff --git a/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref-fork.txtar b/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref-fork.txtar index b51e13d13e1..1814bbff655 100644 --- a/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref-fork.txtar +++ b/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref-fork.txtar @@ -1,6 +1,9 @@ + skip 'it creates a fork owned by the user running the test' skip 'this never worked, but could be fixed if we fixed show-refs' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref.txtar b/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref.txtar index 90c5cde50f8..d2c5f10a464 100644 --- a/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref.txtar +++ b/acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref.txtar @@ -1,7 +1,7 @@ + skip 'it creates a fork owned by the user running the test' # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git @@ -11,10 +11,7 @@ exec gh api user --jq .login stdout2env USER # Create a repository to act as upstream with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup of upstream -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo isolated REPO # Clone the repo exec gh repo clone ${ORG}/${REPO} diff --git a/acceptance/testdata/pr/pr-create-remote-ref-with-branch-name-slash.txtar b/acceptance/testdata/pr/pr-create-remote-ref-with-branch-name-slash.txtar index b8b1515f530..d8835b7dd45 100644 --- a/acceptance/testdata/pr/pr-create-remote-ref-with-branch-name-slash.txtar +++ b/acceptance/testdata/pr/pr-create-remote-ref-with-branch-name-slash.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar index cbfc7dcb120..5ad31a56844 100644 --- a/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar +++ b/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-respects-push-destination.txtar b/acceptance/testdata/pr/pr-create-respects-push-destination.txtar index 24fb2781736..ab8d487fee8 100644 --- a/acceptance/testdata/pr/pr-create-respects-push-destination.txtar +++ b/acceptance/testdata/pr/pr-create-respects-push-destination.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar index 48e8fa6cccd..4877e824f57 100644 --- a/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar +++ b/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar index 3781ec925b8..6fef67600d1 100644 --- a/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar +++ b/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar @@ -1,14 +1,14 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} -# Defer repo cleanup of repo -defer gh repo delete --yes ${ORG}/${REPO} exec gh repo view ${ORG}/${REPO} --json id --jq '.id' stdout2env REPO_ID @@ -20,15 +20,15 @@ cd ${REPO} exec git config push.default simple # Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git branch --set-upstream-to origin/main exec git commit --allow-empty -m 'Empty Commit' -exec git push origin feature-branch +exec git push origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' -stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1 +exec gh pr create --title $PR_TITLE --body 'Feature Body' +stdout2env PR_URL # Assert that the PR was created with the correct head repository and refs -exec gh pr view --json headRefName,headRepository,baseRefName,isCrossRepository -stdout {"baseRefName":"main","headRefName":"feature-branch","headRepository":{"id":"${REPO_ID}","name":"${REPO}","nameWithOwner":"${ORG}/${REPO}"},"isCrossRepository":false} +exec gh pr view $PR_URL --json headRefName,headRepository,baseRefName,isCrossRepository +stdout {"baseRefName":"main","headRefName":"$BRANCH_NAME","headRepository":{"id":"${REPO_ID}","name":"${REPO}","nameWithOwner":"${ORG}/${REPO}"},"isCrossRepository":false} diff --git a/acceptance/testdata/pr/pr-create-respects-user-colon-branch-syntax.txtar b/acceptance/testdata/pr/pr-create-respects-user-colon-branch-syntax.txtar index 7c45b1d3756..03fbcbfb11c 100644 --- a/acceptance/testdata/pr/pr-create-respects-user-colon-branch-syntax.txtar +++ b/acceptance/testdata/pr/pr-create-respects-user-colon-branch-syntax.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-create-with-metadata.txtar b/acceptance/testdata/pr/pr-create-with-metadata.txtar index 3e06b533be8..2ae9daf7c1f 100644 --- a/acceptance/testdata/pr/pr-create-with-metadata.txtar +++ b/acceptance/testdata/pr/pr-create-with-metadata.txtar @@ -1,23 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Prepare a branch to PR -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +cd $REPO +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' --assignee '@me' --label 'bug' +exec gh pr create --title $PR_TITLE --body 'Feature Body' --assignee '@me' --label 'bug' stdout2env PR_URL # Check the PR is indeed created diff --git a/acceptance/testdata/pr/pr-create-without-upstream-config.txtar b/acceptance/testdata/pr/pr-create-without-upstream-config.txtar index e5a40af72a1..0396c6e96ed 100644 --- a/acceptance/testdata/pr/pr-create-without-upstream-config.txtar +++ b/acceptance/testdata/pr/pr-create-without-upstream-config.txtar @@ -1,29 +1,27 @@ + # This test is the same as pr-create-basic, except that the git push doesn't include the -u argument # This causes a git config read to fail during gh pr create, but it should not be fatal -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} - # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} # Prepare a branch to PR cd ${REPO} -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push origin feature-branch +exec git push origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' # Check the PR is indeed created exec gh pr view -stdout 'Feature Title' +stdout $PR_TITLE diff --git a/acceptance/testdata/pr/pr-list.txtar b/acceptance/testdata/pr/pr-list.txtar index 6fcd8e6b717..10cc25e81a8 100644 --- a/acceptance/testdata/pr/pr-list.txtar +++ b/acceptance/testdata/pr/pr-list.txtar @@ -1,24 +1,24 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Prepare a branch to PR -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +cd $REPO +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' -# List PRs and see the new PR is in the list -exec gh pr list -stdout 'Feature Title\tfeature-branch\tOPEN' +# List PRs and see the new PR is in the paginated default output +exec gh pr list --limit 1000 +stdout $PR_TITLE'\t'$BRANCH_NAME'\tOPEN' diff --git a/acceptance/testdata/pr/pr-merge-merge-strategy.txtar b/acceptance/testdata/pr/pr-merge-merge-strategy.txtar index 1d8355506c3..6c5506de795 100644 --- a/acceptance/testdata/pr/pr-merge-merge-strategy.txtar +++ b/acceptance/testdata/pr/pr-merge-merge-strategy.txtar @@ -1,29 +1,35 @@ + # Use gh as a credential helper exec gh auth setup-git -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Multiple coordinated ref updates and merges need repository-level isolation +fixture-repo isolated REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +env BASE_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-base +env REBASE_BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING}-rebase +env REBASE_PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING}-rebase +env REBASE_BASE_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-rebase-base # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO -# Prepare a branch to PR with a single file -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +# Prepare a unique base branch and a branch to PR with a single file +cd $REPO +exec git checkout -b $BASE_BRANCH +exec git push -u origin $BASE_BRANCH +exec git checkout -b $BRANCH_NAME mv ../file.txt file.txt exec git add . -exec git commit -m 'Add file.txt' -exec git push -u origin feature-branch +exec git commit -m 'Add file.txt' -m $BRANCH_NAME +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --base $BASE_BRANCH --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL -# Check that the file doesn't exist on the main branch -exec git checkout main +# Check that the file doesn't exist on the base branch +exec git checkout $BASE_BRANCH ! exists file.txt # Merge the PR @@ -33,13 +39,39 @@ exec gh pr merge $PR_URL --merge exec gh pr view $PR_URL stdout 'state:\tMERGED$' -# Pull and check the file exists on the main branch -exec git pull -r +# Pull and check the file exists on the unique base branch +exec git pull -r origin $BASE_BRANCH exists file.txt # And check we had a merge commit exec git show HEAD -stdout 'Merge pull request #1' +stdout 'Merge pull request #[0-9]+' + +# Prepare unique branches for a rebase merge +exec git checkout -b $REBASE_BASE_BRANCH origin/main +exec git push -u origin $REBASE_BASE_BRANCH +exec git checkout -b $REBASE_BRANCH_NAME +mv ../rebase-file.txt rebase-file.txt +exec git add . +exec git commit -m 'Add rebase-file.txt' -m $REBASE_BRANCH_NAME +exec git push -u origin $REBASE_BRANCH_NAME + +# Create and merge the PR with the rebase strategy +exec gh pr create --base $REBASE_BASE_BRANCH --title $REBASE_PR_TITLE --body 'Feature Body' +stdout2env REBASE_PR_URL +exec git checkout $REBASE_BASE_BRANCH +! exists rebase-file.txt +exec gh pr merge $REBASE_PR_URL --rebase + +# Check the rebased commit landed on the unique base branch +exec gh pr view $REBASE_PR_URL +stdout 'state:\tMERGED$' +exec git pull -r origin $REBASE_BASE_BRANCH +exists rebase-file.txt +exec git show HEAD +stdout 'Add rebase-file.txt' -- file.txt -- Unimportant contents +-- rebase-file.txt -- +Unimportant rebase contents diff --git a/acceptance/testdata/pr/pr-merge-rebase-strategy.txtar b/acceptance/testdata/pr/pr-merge-rebase-strategy.txtar deleted file mode 100644 index f26338c4ade..00000000000 --- a/acceptance/testdata/pr/pr-merge-rebase-strategy.txtar +++ /dev/null @@ -1,45 +0,0 @@ -# Use gh as a credential helper -exec gh auth setup-git - -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING - -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING - -# Prepare a branch to PR with a single file -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch -mv ../file.txt file.txt -exec git add . -exec git commit -m 'Add file.txt' -exec git push -u origin feature-branch - -# Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' -stdout2env PR_URL - -# Check that the file doesn't exist on the main branch -exec git checkout main -! exists file.txt - -# Merge the PR -exec gh pr merge $PR_URL --rebase - -# Check that the state of the PR is now merged -exec gh pr view $PR_URL -stdout 'state:\tMERGED$' - -# Pull and check the file exists on the main branch -exec git pull -r -exists file.txt - -# And check our commit was rebased -exec git show HEAD -stdout 'Add file.txt' - --- file.txt -- -Unimportant contents diff --git a/acceptance/testdata/pr/pr-status-respects-cross-org.txtar b/acceptance/testdata/pr/pr-status-respects-cross-org.txtar index 4505be92352..95a07b043aa 100644 --- a/acceptance/testdata/pr/pr-status-respects-cross-org.txtar +++ b/acceptance/testdata/pr/pr-status-respects-cross-org.txtar @@ -1,5 +1,8 @@ + skip 'it creates a fork owned by the user running the test' +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-view-outside-repo.txtar b/acceptance/testdata/pr/pr-view-outside-repo.txtar index edfb37ed4c1..d68f443fd92 100644 --- a/acceptance/testdata/pr/pr-view-outside-repo.txtar +++ b/acceptance/testdata/pr/pr-view-outside-repo.txtar @@ -1,23 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Prepare a branch to PR -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +cd $REPO +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # Change directories @@ -25,4 +25,4 @@ cd .. # View the PR exec gh pr view $PR_URL -stdout 'Feature Title' +stdout $PR_TITLE diff --git a/acceptance/testdata/pr/pr-view-same-org-fork.txtar b/acceptance/testdata/pr/pr-view-same-org-fork.txtar index eed524dec05..15cd4949d7c 100644 --- a/acceptance/testdata/pr/pr-view-same-org-fork.txtar +++ b/acceptance/testdata/pr/pr-view-same-org-fork.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar index 4e1e5e64ac7..1a1721d931b 100644 --- a/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar +++ b/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar b/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar index ff9db4037c0..7880e637e4b 100644 --- a/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar +++ b/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar @@ -1,14 +1,13 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=branch-${RANDOM_STRING} +env PR_TITLE=PR-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} @@ -18,21 +17,21 @@ cd ${REPO} exec git config push.default current # Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git branch --set-upstream-to origin/main -exec git rev-parse --abbrev-ref feature-branch@{upstream} +exec git rev-parse --abbrev-ref $BRANCH_NAME@{upstream} stdout origin/main # Create the PR exec git commit --allow-empty -m 'Empty Commit' exec git push -exec gh pr create -B main -H feature-branch --title 'Feature Title' --body 'Feature Body' +exec gh pr create -B main -H $BRANCH_NAME --title $PR_TITLE --body 'Feature Body' # View the PR exec gh pr view -stdout 'Feature Title' +stdout $PR_TITLE # Check the PR status -env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] exec gh pr status -stdout $PR_STATUS_BRANCH +stdout $PR_TITLE +stdout $BRANCH_NAME diff --git a/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar index 6c0743a6f14..a537c264761 100644 --- a/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar +++ b/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Setup environment variables used for testscript env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env FORK=${REPO}-fork diff --git a/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar index b9621ea72cc..b449d379353 100644 --- a/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar +++ b/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar @@ -1,14 +1,13 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO +env BRANCH_NAME=branch-${RANDOM_STRING} +env PR_TITLE=PR-${RANDOM_STRING} # Clone the repo exec gh repo clone ${ORG}/${REPO} @@ -18,19 +17,19 @@ cd ${REPO} exec git config push.default simple # Prepare a branch where changes are pulled from the default branch instead of remote branch of same name -exec git checkout -b feature-branch +exec git checkout -b $BRANCH_NAME exec git branch --set-upstream-to origin/main # Create the PR exec git commit --allow-empty -m 'Empty Commit' -exec git push origin feature-branch -exec gh pr create -H feature-branch --title 'Feature Title' --body 'Feature Body' +exec git push origin $BRANCH_NAME +exec gh pr create -H $BRANCH_NAME --title $PR_TITLE --body 'Feature Body' # View the PR exec gh pr view -stdout 'Feature Title' +stdout $PR_TITLE # Check the PR status -env PR_STATUS_BRANCH=#1 Feature Title [feature-branch] exec gh pr status -stdout $PR_STATUS_BRANCH +stdout $PR_TITLE +stdout $BRANCH_NAME diff --git a/acceptance/testdata/pr/pr-view.txtar b/acceptance/testdata/pr/pr-view.txtar index 6166d15ad80..1cafe4b19cf 100644 --- a/acceptance/testdata/pr/pr-view.txtar +++ b/acceptance/testdata/pr/pr-view.txtar @@ -1,25 +1,25 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env BRANCH_NAME=${SCRIPT_NAME}-${RANDOM_STRING} +env PR_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Prepare a branch to PR -cd $SCRIPT_NAME-$RANDOM_STRING -exec git checkout -b feature-branch +cd $REPO +exec git checkout -b $BRANCH_NAME exec git commit --allow-empty -m 'Empty Commit' -exec git push -u origin feature-branch +exec git push -u origin $BRANCH_NAME # Create the PR -exec gh pr create --title 'Feature Title' --body 'Feature Body' +exec gh pr create --title $PR_TITLE --body 'Feature Body' stdout2env PR_URL # View the PR exec gh pr view $PR_URL -stdout 'Feature Title' +stdout $PR_TITLE diff --git a/acceptance/testdata/project/project-create-delete.txtar b/acceptance/testdata/project/project-create-delete.txtar index 1152536695c..a7a8ab114f4 100644 --- a/acceptance/testdata/project/project-create-delete.txtar +++ b/acceptance/testdata/project/project-create-delete.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Create a project and get the project number env PROJECT_TITLE=$SCRIPT_NAME-$RANDOM_STRING exec gh project create --owner=$ORG --title=$PROJECT_TITLE --format='json' --jq='.number' diff --git a/acceptance/testdata/release/release-create.txtar b/acceptance/testdata/release/release-create.txtar index 3bdafe76926..e5c3b610b54 100644 --- a/acceptance/testdata/release/release-create.txtar +++ b/acceptance/testdata/release/release-create.txtar @@ -1,12 +1,10 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env TAG=v${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a release in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh release create v1.2.3 --notes 'awesome release' --latest +exec gh release create $TAG --notes 'awesome release' diff --git a/acceptance/testdata/release/release-delete.txtar b/acceptance/testdata/release/release-delete.txtar index 3eaf5c12bc6..02147863f81 100644 --- a/acceptance/testdata/release/release-delete.txtar +++ b/acceptance/testdata/release/release-delete.txtar @@ -1,38 +1,37 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env TAG=v${SCRIPT_NAME}-${RANDOM_STRING} # Create a release in the repo -exec gh release create v1.2.3 --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --notes 'awesome release' --latest +exec gh release create $TAG --repo $ORG/$REPO --notes 'awesome release' # Upload an asset to the release -exec gh release upload v1.2.3 --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING asset.txt +exec gh release upload $TAG --repo $ORG/$REPO asset.txt # Delete the asset from the release -exec gh release delete-asset v1.2.3 asset.txt --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes +exec gh release delete-asset $TAG asset.txt --repo $ORG/$REPO --yes # Verify the release has no assets -exec gh release view v1.2.3 --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --json assets --jq '.assets | length' +exec gh release view $TAG --repo $ORG/$REPO --json assets --jq '.assets | length' stdout '0' # Downloading the deleted asset should fail -! exec gh release download v1.2.3 --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING +! exec gh release download $TAG --repo $ORG/$REPO stderr 'no assets to download' # Delete the release and its tag -exec gh release delete v1.2.3 --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes --cleanup-tag +exec gh release delete $TAG --repo $ORG/$REPO --yes --cleanup-tag # Wait for tag deletion to become visible through the ref lookup sleep 5 # Verify the release is gone -! exec gh release view v1.2.3 --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING +! exec gh release view $TAG --repo $ORG/$REPO stderr 'release not found' # Verify the tag is gone -! exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING/git/ref/tags/v1.2.3 +! exec gh api repos/$ORG/$REPO/git/ref/tags/$TAG stderr 'Not Found' -- asset.txt -- diff --git a/acceptance/testdata/release/release-list.txtar b/acceptance/testdata/release/release-list.txtar index 844b25daa21..e12b76f97ba 100644 --- a/acceptance/testdata/release/release-list.txtar +++ b/acceptance/testdata/release/release-list.txtar @@ -1,16 +1,14 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env TAG=v${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a release in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh release create v1.2.3 --notes 'awesome release' --latest +exec gh release create $TAG --notes 'awesome release' -# List the releases -exec gh release list -stdout 'v1.2.3' \ No newline at end of file +# Find the release in the paginated default output +exec gh release list --limit 1000 +stdout $TAG diff --git a/acceptance/testdata/release/release-upload-download.txtar b/acceptance/testdata/release/release-upload-download.txtar index e19bc06d727..6543fb9d8f0 100644 --- a/acceptance/testdata/release/release-upload-download.txtar +++ b/acceptance/testdata/release/release-upload-download.txtar @@ -1,26 +1,27 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env TAG=v${SCRIPT_NAME}-${RANDOM_STRING} +env ARCHIVE_FILENAME=${REPO}-${TAG}.zip -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a release in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh release create v1.2.3 --notes 'awesome release' --latest +mkdir downloads +cd downloads +exec gh release create $TAG --notes 'awesome release' # Upload an asset to the release -exec gh release upload v1.2.3 ../asset.txt +exec gh release upload $TAG ../asset.txt # Download the asset from the release -exec gh release download v1.2.3 +exec gh release download $TAG exists asset.txt # Download the asset in archive form -exec gh release download v1.2.3 --archive=zip -exists $SCRIPT_NAME-$RANDOM_STRING-1.2.3.zip +exec gh release download $TAG --archive=zip +exists $ARCHIVE_FILENAME -- asset.txt -- Hello, world! diff --git a/acceptance/testdata/release/release-view.txtar b/acceptance/testdata/release/release-view.txtar index a7138812abb..7bc6049b120 100644 --- a/acceptance/testdata/release/release-view.txtar +++ b/acceptance/testdata/release/release-view.txtar @@ -1,16 +1,14 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env TAG=v${SCRIPT_NAME}-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a release in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh release create v1.2.3 --notes 'awesome release' --latest +exec gh release create $TAG --notes 'awesome release' # View the release -exec gh release view v1.2.3 -stdout 'v1.2.3' \ No newline at end of file +exec gh release view $TAG +stdout $TAG \ No newline at end of file diff --git a/acceptance/testdata/repo/repo-archive-unarchive.txtar b/acceptance/testdata/repo/repo-archive-unarchive.txtar index 33ff519f3a1..7c2f34e1e67 100644 --- a/acceptance/testdata/repo/repo-archive-unarchive.txtar +++ b/acceptance/testdata/repo/repo-archive-unarchive.txtar @@ -1,23 +1,21 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo isolated REPO # Check that the repo exists and isn't archived -exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived' +exec gh repo view $ORG/$REPO --json=isArchived --jq='.isArchived' stdout 'false' # Archive the repo -exec gh repo archive $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes +exec gh repo archive $ORG/$REPO --yes # Check that the repo is archived -exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived' +exec gh repo view $ORG/$REPO --json=isArchived --jq='.isArchived' stdout 'true' # Unarchive the repo -exec gh repo unarchive $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes +exec gh repo unarchive $ORG/$REPO --yes # Check that the repo is unarchived -exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived' +exec gh repo view $ORG/$REPO --json=isArchived --jq='.isArchived' stdout 'false' diff --git a/acceptance/testdata/repo/repo-autolink.txtar b/acceptance/testdata/repo/repo-autolink.txtar index 6310eed1ac3..59d01b8df46 100644 --- a/acceptance/testdata/repo/repo-autolink.txtar +++ b/acceptance/testdata/repo/repo-autolink.txtar @@ -1,31 +1,26 @@ -# Create a repository to hold the autolink references -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING -# List the autolinks. There should be none -exec gh repo autolink list --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=keyPrefix -! stdout keyPrefix +# Create a repository to hold the autolink references +fixture-repo shared REPO +env AUTOLINK_PREFIX=T${RANDOM_STRING}- # Create an alphanumeric autolink -exec gh repo autolink create --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING TICKET- 'https://example.com/TICKET?query=' +exec gh repo autolink create --repo $ORG/$REPO $AUTOLINK_PREFIX 'https://example.com/TICKET?query=' # Ensure the autolink was created -exec gh repo autolink list --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=keyPrefix --jq='.[].keyPrefix' -stdout 'TICKET-' +exec gh repo autolink list --repo $ORG/$REPO --json=keyPrefix --jq='.[].keyPrefix' +stdout $AUTOLINK_PREFIX # Get the autolink id -exec gh repo autolink list --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=keyPrefix,id --jq='.[] | select(.keyPrefix == "TICKET-") | .id' +exec gh repo autolink list --repo $ORG/$REPO --json=keyPrefix,id --jq='.[] | select(.keyPrefix == "'$AUTOLINK_PREFIX'") | .id' stdout2env AUTOLINK_ID # View the autolink and ensure the url template round tripped -exec gh repo autolink view --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING $AUTOLINK_ID --json=urlTemplate --jq='.urlTemplate' +exec gh repo autolink view --repo $ORG/$REPO $AUTOLINK_ID --json=urlTemplate --jq='.urlTemplate' stdout 'https://example\.com/TICKET\?query=' # Delete the autolink -exec gh repo autolink delete --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING $AUTOLINK_ID --yes +exec gh repo autolink delete --repo $ORG/$REPO $AUTOLINK_ID --yes # Ensure the autolink was deleted -exec gh repo autolink list --repo $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=id --jq='.[].id' +exec gh repo autolink list --repo $ORG/$REPO --json=id --jq='.[].id' ! stdout $AUTOLINK_ID diff --git a/acceptance/testdata/repo/repo-clone.txtar b/acceptance/testdata/repo/repo-clone.txtar index b90a0894b40..ddd08fad757 100644 --- a/acceptance/testdata/repo/repo-clone.txtar +++ b/acceptance/testdata/repo/repo-clone.txtar @@ -1,11 +1,9 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Ensure the repo was cloned -exists $SCRIPT_NAME-$RANDOM_STRING/README.md +exists $REPO/README.md diff --git a/acceptance/testdata/repo/repo-create-bare.txtar b/acceptance/testdata/repo/repo-create-bare.txtar index b835c420b4d..d71356f4756 100644 --- a/acceptance/testdata/repo/repo-create-bare.txtar +++ b/acceptance/testdata/repo/repo-create-bare.txtar @@ -1,6 +1,9 @@ + # It's unclear what we want to do with these acceptance tests beyond our GHEC discovery, so skip new ones by default skip +fixture-repo none + # Set up env var env REPO=${SCRIPT_NAME}-${RANDOM_STRING} diff --git a/acceptance/testdata/repo/repo-create-view.txtar b/acceptance/testdata/repo/repo-create-view.txtar index 9774def3502..a543f8d33bf 100644 --- a/acceptance/testdata/repo/repo-create-view.txtar +++ b/acceptance/testdata/repo/repo-create-view.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Create a repository with a file so it has a default branch exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private diff --git a/acceptance/testdata/repo/repo-delete.txtar b/acceptance/testdata/repo/repo-delete.txtar index b82388068e7..a629995b536 100644 --- a/acceptance/testdata/repo/repo-delete.txtar +++ b/acceptance/testdata/repo/repo-delete.txtar @@ -1,5 +1,9 @@ + +fixture-repo none + # Create a repository with a file so it has a default branch exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private +defer cleanup-repo $SCRIPT_NAME-$RANDOM_STRING # Check that the repo exists exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json name --jq '.name' diff --git a/acceptance/testdata/repo/repo-deploy-key.txtar b/acceptance/testdata/repo/repo-deploy-key.txtar index 5a1151d7d00..5cc228acd2b 100644 --- a/acceptance/testdata/repo/repo-deploy-key.txtar +++ b/acceptance/testdata/repo/repo-deploy-key.txtar @@ -1,26 +1,24 @@ -# Create and clone a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create and clone a repository with a file so it has a default branch +fixture-repo shared REPO +env DEPLOY_KEY_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +exec gh repo clone $ORG/$REPO # Generate a globally unique deploy key -generate-ssh-key deployKey.pub myTitle +generate-ssh-key deployKey.pub $DEPLOY_KEY_TITLE -# cd to the repo and list the deploy keys. There should be no keys -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh repo deploy-key list --json=title -! stdout title +# cd to the repository +cd $REPO # Add a deploy key exec gh repo deploy-key add ../deployKey.pub # Ensure the deploy key was added exec gh repo deploy-key list --json=title --jq='.[].title' -stdout myTitle +stdout $DEPLOY_KEY_TITLE # Get the deploy key id -exec gh repo deploy-key list --json=title,id --jq='.[].title="myTitle" | .[].id' +exec gh repo deploy-key list --json=title,id --jq='.[] | select(.title == "'$DEPLOY_KEY_TITLE'") | .id' stdout2env DEPLOY_KEY_ID # Delete the deploy key diff --git a/acceptance/testdata/repo/repo-edit.txtar b/acceptance/testdata/repo/repo-edit.txtar index 00d3cdd2c5b..ca11a02be1e 100644 --- a/acceptance/testdata/repo/repo-edit.txtar +++ b/acceptance/testdata/repo/repo-edit.txtar @@ -1,16 +1,14 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo isolated REPO # Check that the repo description is empty -exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json description --jq '.description' +exec gh repo view $ORG/$REPO --json description --jq '.description' ! stdout '.' # Edit the repo description -exec gh repo edit $ORG/$SCRIPT_NAME-$RANDOM_STRING --description 'newDescription' +exec gh repo edit $ORG/$REPO --description 'newDescription' # Check that the repo description is updated -exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json description --jq '.description' +exec gh repo view $ORG/$REPO --json description --jq '.description' stdout 'newDescription' diff --git a/acceptance/testdata/repo/repo-fork-sync.txtar b/acceptance/testdata/repo/repo-fork-sync.txtar index 04c4c584555..9a801eb5724 100644 --- a/acceptance/testdata/repo/repo-fork-sync.txtar +++ b/acceptance/testdata/repo/repo-fork-sync.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Use gh as a credential helper exec gh auth setup-git diff --git a/acceptance/testdata/repo/repo-list-rename.txtar b/acceptance/testdata/repo/repo-list-rename.txtar index 7f3ff1281df..c483adaab72 100644 --- a/acceptance/testdata/repo/repo-list-rename.txtar +++ b/acceptance/testdata/repo/repo-list-rename.txtar @@ -1,16 +1,25 @@ + +fixture-repo none + # Create a repository with a file so it has a default branch exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private +# Register both possible names so failures before or after the rename cannot leak the repository. +defer cleanup-repo $SCRIPT_NAME-$RANDOM_STRING +defer cleanup-repo $SCRIPT_NAME-$RANDOM_STRING-renamed + +# Wait for default-branch initialization to finish before renaming the repository +wait-for-repository-ready $ORG/$SCRIPT_NAME-$RANDOM_STRING # List the repos and check for the new repo exec gh repo list $ORG --json=name --jq='.[].name' stdout $SCRIPT_NAME-$RANDOM_STRING +# GitHub can retain the repository creation lock after the default branch becomes readable. +sleep 10 + # Rename the repo exec gh repo rename $SCRIPT_NAME-$RANDOM_STRING-renamed --repo=$ORG/$SCRIPT_NAME-$RANDOM_STRING --yes -# Defer repo deletion -defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING-renamed --yes - # List the repos and check for the renamed repo exec gh repo list $ORG --json=name --jq='.[].name' stdout $SCRIPT_NAME-$RANDOM_STRING-renamed diff --git a/acceptance/testdata/repo/repo-read-dir.txtar b/acceptance/testdata/repo/repo-read-dir.txtar index 6e0aa7e3949..a997dcf71bc 100644 --- a/acceptance/testdata/repo/repo-read-dir.txtar +++ b/acceptance/testdata/repo/repo-read-dir.txtar @@ -1,15 +1,18 @@ + # List directory contents of a repository without cloning. # Use gh as a credential helper exec gh auth setup-git # Create a private repo with a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --private --add-readme -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env CONTENT_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING} +env V2_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-v2 # Clone the repo and add a docs dir plus an executable script -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO +cd $REPO +exec git checkout -b $CONTENT_BRANCH mkdir docs mkdir script cp $WORK/guide.md docs/guide.md @@ -17,49 +20,50 @@ cp $WORK/run.sh script/run.sh chmod 755 script/run.sh exec git add -A exec git commit -m 'Add docs and script' -exec git push origin main +exec git push -u origin $CONTENT_BRANCH # Add a v2 branch with an extra file in docs, to exercise --ref -exec git checkout -b v2 +exec git checkout -b $V2_BRANCH cp $WORK/extra.md docs/extra.md exec git add -A exec git commit -m 'Add docs/extra.md on v2' -exec git push -u origin v2 -exec git checkout main +exec git push -u origin $V2_BRANCH +exec git checkout $CONTENT_BRANCH -# List the repository root. Non-TTY output is tab-separated as type, name, -# octal mode, and byte size. Directories report mode 040000 and size 0; the -# auto-generated README has a non-deterministic size, so match it as digits. -exec gh repo read-dir -stdout '^file\tREADME\.md\t100644\t\d+$' -stdout '^dir\tdocs\t040000\t0$' -stdout '^dir\tscript\t040000\t0$' +# List the shared fixture's root from its default branch +exec gh repo read-dir --json name,type --jq '.entries[] | .name + " " + .type' +stdout '^README\.md file$' +! stdout '^docs ' +! stdout '^script ' # List a subdirectory -exec gh repo read-dir docs +exec gh repo read-dir docs --ref $CONTENT_BRANCH stdout '^file\tguide\.md\t100644\t19$' ! stdout 'extra\.md' # Executable files are reported as a file with octal mode 100755 -exec gh repo read-dir script --json name,type,modeOctal --jq '.entries[] | .name + " " + .type + " " + .modeOctal' +exec gh repo read-dir script --ref $CONTENT_BRANCH --json name,type,modeOctal --jq '.entries[] | .name + " " + .type + " " + .modeOctal' stdout '^run\.sh file 100755$' -# JSON output lists directory entries -exec gh repo read-dir --json name,type --jq '.entries[] | select(.type=="dir") | .name' -stdout '^docs$' -stdout '^script$' +# Non-TTY output is tab-separated as type, name, octal mode, and byte size. +# Directories report mode 040000 and size 0; the auto-generated README has a +# non-deterministic size, so match it as digits. +exec gh repo read-dir --ref $CONTENT_BRANCH +stdout '^file\tREADME\.md\t100644\t\d+$' +stdout '^dir\tdocs\t040000\t0$' +stdout '^dir\tscript\t040000\t0$' # A ref can select a different tree -exec gh repo read-dir docs --ref v2 +exec gh repo read-dir docs --ref $V2_BRANCH stdout 'guide\.md' stdout 'extra\.md' # Error: the path points to a file, not a directory -! exec gh repo read-dir README.md +! exec gh repo read-dir README.md --ref $CONTENT_BRANCH stderr 'is a file, not a directory' # Error: the path does not exist -! exec gh repo read-dir does-not-exist +! exec gh repo read-dir does-not-exist --ref $CONTENT_BRANCH stderr 'could not find' -- guide.md -- diff --git a/acceptance/testdata/repo/repo-read-file.txtar b/acceptance/testdata/repo/repo-read-file.txtar index 205a63287d5..2f9786c618b 100644 --- a/acceptance/testdata/repo/repo-read-file.txtar +++ b/acceptance/testdata/repo/repo-read-file.txtar @@ -1,85 +1,88 @@ + # Read files from a repository without cloning, in several modes. # Use gh as a credential helper exec gh auth setup-git # Create a private repo with a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --private --add-readme -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo shared REPO +env CONTENT_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING} +env V2_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING}-v2 # Clone the repo and add a text file under a subdirectory -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO +cd $REPO +exec git checkout -b $CONTENT_BRANCH mkdir docs cp $WORK/guide.md docs/guide.md exec git add -A exec git commit -m 'Add docs/guide.md' -exec git push origin main +exec git push -u origin $CONTENT_BRANCH # Add a v2 branch where the same file has different content, to exercise --ref -exec git checkout -b v2 +exec git checkout -b $V2_BRANCH cp $WORK/guide-v2.md docs/guide.md exec git add -A exec git commit -m 'Update docs/guide.md on v2' -exec git push -u origin v2 -exec git checkout main +exec git push -u origin $V2_BRANCH +exec git checkout $CONTENT_BRANCH # Create a small binary file (PNG signature) via the Contents API -exec gh api -X PUT repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING/contents/assets/logo.png -f message='Add binary' -f content=iVBORw0KGgo= +exec gh api -X PUT repos/$ORG/$REPO/contents/assets/logo.png -f message='Add binary' -f content=iVBORw0KGgo= -f branch=$CONTENT_BRANCH # Create a text file containing ANSI terminal escape sequences via the Contents API -exec gh api -X PUT repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING/contents/ansi.txt -f message='Add ansi' -f content=G1szMW1oZWxsbxtbMG0K +exec gh api -X PUT repos/$ORG/$REPO/contents/ansi.txt -f message='Add ansi' -f content=G1szMW1oZWxsbxtbMG0K -f branch=$CONTENT_BRANCH -# Read a file from the default branch: raw content goes to stdout -exec gh repo read-file docs/guide.md -cmp stdout $WORK/guide.md +# Read the shared fixture's README from its default branch +exec gh repo read-file README.md +stdout $REPO -# Read the same file at a specific ref -exec gh repo read-file docs/guide.md --ref v2 +# Read a file at a specific ref +exec gh repo read-file docs/guide.md --ref $V2_BRANCH cmp stdout $WORK/guide-v2.md # Save a file to disk, and confirm --clobber is required to overwrite -exec gh repo read-file docs/guide.md --output out.txt +exec gh repo read-file docs/guide.md --ref $CONTENT_BRANCH --output out.txt cmp out.txt $WORK/guide.md -! exec gh repo read-file docs/guide.md --output out.txt +! exec gh repo read-file docs/guide.md --ref $CONTENT_BRANCH --output out.txt stderr 'already exists' -exec gh repo read-file docs/guide.md --output out.txt --clobber +exec gh repo read-file docs/guide.md --ref $CONTENT_BRANCH --output out.txt --clobber cmp out.txt $WORK/guide.md # Save into a directory: a trailing separator writes under the remote basename -exec gh repo read-file docs/guide.md --output download-dir/ +exec gh repo read-file docs/guide.md --ref $CONTENT_BRANCH --output download-dir/ cmp download-dir/guide.md $WORK/guide.md # Save to an explicit path inside a directory, creating it as needed -exec gh repo read-file docs/guide.md --output download-dir/out.txt +exec gh repo read-file docs/guide.md --ref $CONTENT_BRANCH --output download-dir/out.txt cmp download-dir/out.txt $WORK/guide.md # JSON output exposes file metadata -exec gh repo read-file docs/guide.md --json name,path,size,type --jq '.name' +exec gh repo read-file docs/guide.md --ref $CONTENT_BRANCH --json name,path,size,type --jq '.name' stdout '^guide\.md$' # Read a binary file: the raw bytes written to stdout match the saved copy, # and the base64 content is exposed via --json -exec gh repo read-file assets/logo.png --output downloaded.png +exec gh repo read-file assets/logo.png --ref $CONTENT_BRANCH --output downloaded.png exists downloaded.png -exec gh repo read-file assets/logo.png +exec gh repo read-file assets/logo.png --ref $CONTENT_BRANCH cmp stdout downloaded.png -exec gh repo read-file assets/logo.png --json size,type,content --jq '.content' +exec gh repo read-file assets/logo.png --ref $CONTENT_BRANCH --json size,type,content --jq '.content' stdout '^iVBORw0KGgo=$' # A file with terminal escape sequences is refused by default, but readable # with --allow-escape-sequences -! exec gh repo read-file ansi.txt +! exec gh repo read-file ansi.txt --ref $CONTENT_BRANCH stderr 'terminal escape sequences' -exec gh repo read-file ansi.txt --allow-escape-sequences +exec gh repo read-file ansi.txt --ref $CONTENT_BRANCH --allow-escape-sequences stdout 'hello' # Error: the path is a directory -! exec gh repo read-file docs +! exec gh repo read-file docs --ref $CONTENT_BRANCH stderr 'is a directory' # Error: the path does not exist -! exec gh repo read-file does/not/exist.md +! exec gh repo read-file does/not/exist.md --ref $CONTENT_BRANCH stderr 'Not Found' -- guide.md -- diff --git a/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar b/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar index 5075a20a223..e93bfbebd44 100644 --- a/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar +++ b/acceptance/testdata/repo/repo-rename-transfer-ownership.txtar @@ -1,9 +1,7 @@ + # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private +fixture-repo shared REPO # Attempt to rename the repo with a slash in the name -! exec gh repo rename $ORG/new-name --repo=$ORG/$SCRIPT_NAME-$RANDOM_STRING --yes +! exec gh repo rename $ORG/new-name --repo=$ORG/$REPO --yes stderr 'New repository name cannot contain \''/\'' character - to transfer a repository to a new owner, see .' - -# Defer repo deletion -defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes diff --git a/acceptance/testdata/repo/repo-set-default.txtar b/acceptance/testdata/repo/repo-set-default.txtar index de4eda11f6a..886aa6f7896 100644 --- a/acceptance/testdata/repo/repo-set-default.txtar +++ b/acceptance/testdata/repo/repo-set-default.txtar @@ -1,17 +1,16 @@ -# Create and clone a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create and clone a repository with a file so it has a default branch +fixture-repo shared REPO +exec gh repo clone $ORG/$REPO # Ensure that no default is set -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO exec gh repo set-default --view stderr 'No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help' # Set the default -exec gh repo set-default $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo set-default $ORG/$REPO # Check that the default is set exec gh repo set-default --view -stdout $ORG/$SCRIPT_NAME-$RANDOM_STRING +stdout $ORG/$REPO diff --git a/acceptance/testdata/repo/repo-sync-worktree.txtar b/acceptance/testdata/repo/repo-sync-worktree.txtar index 05ebc4e7ee5..12227f008a1 100644 --- a/acceptance/testdata/repo/repo-sync-worktree.txtar +++ b/acceptance/testdata/repo/repo-sync-worktree.txtar @@ -1,14 +1,13 @@ + # Use gh as a credential helper exec gh auth setup-git # Create and clone a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO +exec gh repo clone $ORG/$REPO # Advance the default branch on the remote, then rewind the local branch so a sync is required -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mv ../asset.txt asset.txt exec git add . exec git commit -m 'Add asset.txt' @@ -26,11 +25,11 @@ stdout '^main$' # Syncing from the linked worktree must refuse rather than silently move the default branch ref cd ../topic-worktree ! exec gh repo sync -stderr 'can''t sync "main" because it''s checked out in another worktree at .*'$SCRIPT_NAME'-'$RANDOM_STRING'$' +stderr 'can''t sync "main" because it''s checked out in another worktree at .*'$REPO'$' stderr 'tip: run `gh repo sync` from that worktree instead' # The primary worktree's branch and working tree are left untouched -cd ../$SCRIPT_NAME-$RANDOM_STRING +cd ../$REPO exec git rev-parse HEAD stdout $PRIMARY_HEAD exec git status --porcelain diff --git a/acceptance/testdata/repo/repo-sync.txtar b/acceptance/testdata/repo/repo-sync.txtar index b491353d6ec..e096c8993a6 100644 --- a/acceptance/testdata/repo/repo-sync.txtar +++ b/acceptance/testdata/repo/repo-sync.txtar @@ -1,14 +1,13 @@ + # Use gh as a credential helper exec gh auth setup-git # Create and clone a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private --clone - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO +exec gh repo clone $ORG/$REPO # Advance the default branch on the remote, then rewind the local branch so a sync is required -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mv ../asset.txt asset.txt exec git add . exec git commit -m 'Add asset.txt' diff --git a/acceptance/testdata/ruleset/ruleset.txtar b/acceptance/testdata/ruleset/ruleset.txtar index 99be3683c80..b810bc8fc4f 100644 --- a/acceptance/testdata/ruleset/ruleset.txtar +++ b/acceptance/testdata/ruleset/ruleset.txtar @@ -1,14 +1,14 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$REPO --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$REPO +fixture-repo none +env REPO=$SCRIPT_NAME-$RANDOM_STRING +exec gh repo create $ORG/$REPO --add-readme --public +defer cleanup-repo $REPO # Clone the repo exec gh repo clone $ORG/$REPO diff --git a/acceptance/testdata/search/search-issues.txtar b/acceptance/testdata/search/search-issues.txtar index b44bb86eb1f..9707ef9b793 100644 --- a/acceptance/testdata/search/search-issues.txtar +++ b/acceptance/testdata/search/search-issues.txtar @@ -1,16 +1,15 @@ -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Create a repository with a file so it has a default branch +fixture-repo shared REPO +env ISSUE_TITLE=${SCRIPT_NAME}-${RANDOM_STRING} +env SEARCH_TOKEN=body-${RANDOM_STRING} -# Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create an issue in the repo -cd $SCRIPT_NAME-$RANDOM_STRING -exec gh issue create --title 'Feature Request' --body $RANDOM_STRING +exec gh issue create --title $ISSUE_TITLE --body $SEARCH_TOKEN # It takes some time for the issue to be created and indexed. Search reads a # separate index rather than the issue itself, so this wait is longer than the @@ -18,5 +17,5 @@ exec gh issue create --title 'Feature Request' --body $RANDOM_STRING sleep 20 # Search for the issue -exec gh search issues $RANDOM_STRING -R $ORG/$SCRIPT_NAME-$RANDOM_STRING -stdout $RANDOM_STRING \ No newline at end of file +exec gh search issues $SEARCH_TOKEN -R $ORG/$REPO +stdout $ISSUE_TITLE diff --git a/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar b/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar index 9d6bfed845e..dd0e383b681 100644 --- a/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar +++ b/acceptance/testdata/secret/secret-org-with-selected-visibility.txtar @@ -1,15 +1,12 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env2upper SECRET_NAME=${SCRIPT_NAME}_${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo shared REPO # Confirm organization secret does not exist, will fail admin:org scope missing exec gh secret list --org ${ORG} @@ -33,7 +30,7 @@ stdout 0 exec gh secret set ${SECRET_NAME} --org ${ORG} --body 'just an organization secret' --repos ${REPO} # Verify the secret is now shared with the repository -exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq '.repositories[0].name' +exec gh api -X GET /orgs/${ORG}/actions/secrets/${SECRET_NAME}/repositories --jq='.repositories[] | select(.name == "'$REPO'") | .name' stdout ${REPO} # Set the same organization secret with shared visibility back to no repositories selected diff --git a/acceptance/testdata/secret/secret-org.txtar b/acceptance/testdata/secret/secret-org.txtar index 3465628b77f..4314dc81e6f 100644 --- a/acceptance/testdata/secret/secret-org.txtar +++ b/acceptance/testdata/secret/secret-org.txtar @@ -1,21 +1,21 @@ + # Setup environment variables used for testscript -# This script will most likely fail because you are most likely targeting a repo that is not public and an org -# that is not on the right plan: https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-an-organization env REPO=${SCRIPT_NAME}-${RANDOM_STRING} env2upper SECRET_NAME=${SCRIPT_NAME}_${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$REPO --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$REPO +# Organization secrets are available to public repositories on GitHub Free +fixture-repo none +exec gh repo create $ORG/$REPO --add-readme --public +defer cleanup-repo $REPO +env WORKFLOW_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING} # Clone the repo exec gh repo clone $ORG/$REPO cd $REPO +exec git checkout -b $WORKFLOW_BRANCH # Confirm organization secret does not exist, will fail admin:org scope missing exec gh secret list --org $ORG @@ -31,30 +31,16 @@ defer gh secret delete $SECRET_NAME --org $ORG exec gh secret list --org $ORG stdout $SECRET_NAME -# Commit workflow file creating dispatchable workflow able to verify secret matches +# Commit a push-triggered workflow that verifies the secret mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml replace .github/workflows/workflow.yml SECRET_NAME=$SECRET_NAME exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' -exec git push -u origin main - -# Sleep because it takes a second for the workflow to register -sleep 1 - -# Check the workflow is indeed created -exec gh workflow list -stdout 'Test Workflow Name' - -# Run the workflow -exec gh workflow run 'Test Workflow Name' - -# It takes some time for a workflow run to register -sleep 10 +exec git push -u origin $WORKFLOW_BRANCH -# Get the run ID we want to watch & delete -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run on the uniquely named branch to register +wait-for-run RUN_ID --branch $WORKFLOW_BRANCH --event push # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status @@ -67,8 +53,7 @@ stdout 'GitHub Actions secret value matches$' # This workflow is intended to assert the value of the GitHub Actions secret was set appropriately name: Test Workflow Name on: - # Allow workflow to be dispatched by gh workflow run - workflow_dispatch: + push: jobs: # This workflow contains a single job called "assert" that should only pass if the GitHub Actions secret value matches diff --git a/acceptance/testdata/secret/secret-repo-env.txtar b/acceptance/testdata/secret/secret-repo-env.txtar index a9a2c735354..3f1b3e45971 100644 --- a/acceptance/testdata/secret/secret-repo-env.txtar +++ b/acceptance/testdata/secret/secret-repo-env.txtar @@ -1,80 +1,88 @@ + # Setup environment variables used for testscript -env REPO=$SCRIPT_NAME-$RANDOM_STRING +env ENV_NAME=testscripts-${RANDOM_STRING} +env2upper ENV_SECRET_NAME=${SCRIPT_NAME}_ENV_${RANDOM_STRING} +env2upper REPO_SECRET_NAME=${SCRIPT_NAME}_REPO_${RANDOM_STRING} +env WORKFLOW_BRANCH=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$REPO --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$REPO +fixture-repo shared REPO # Clone the repo exec gh repo clone $ORG/$REPO cd $REPO +exec git checkout -b $WORKFLOW_BRANCH + +# Create a repository secret +exec gh secret set $REPO_SECRET_NAME --body 'just a repository secret' +defer gh secret delete $REPO_SECRET_NAME + +# Verify new repository secret exists +exec gh secret list +stdout $REPO_SECRET_NAME # Create a repository environment, will fail if organization does not have environment support -exec gh api /repos/$ORG/$REPO/environments/testscripts -X PUT +exec gh api /repos/$ORG/$REPO/environments/$ENV_NAME -X PUT +defer gh api /repos/$ORG/$REPO/environments/$ENV_NAME -X DELETE # Create a repository environment secret -exec gh secret set TESTSCRIPTS_ENV --env testscripts --body 'just a repository environment secret' +exec gh secret set $ENV_SECRET_NAME --env $ENV_NAME --body 'just a repository environment secret' # Verify new repository secret exists -exec gh secret list --env testscripts -stdout 'TESTSCRIPTS_ENV' +exec gh secret list --env $ENV_NAME +stdout $ENV_SECRET_NAME -# Commit workflow file creating dispatchable workflow able to verify secret matches +# Verify both secret scopes in one workflow so the shared repository receives +# only one workflow-triggering push from this test group at a time. mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml +replace .github/workflows/workflow.yml ENV_SECRET_NAME=$ENV_SECRET_NAME +replace .github/workflows/workflow.yml REPO_SECRET_NAME=$REPO_SECRET_NAME +replace .github/workflows/workflow.yml ENV_NAME=$ENV_NAME exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' -exec git push -u origin main - -# Sleep because it takes a second for the workflow to register -sleep 1 +exec git push -u origin $WORKFLOW_BRANCH -# Check the workflow is indeed created -exec gh workflow list -stdout 'Test Workflow Name' - -# Run the workflow -exec gh workflow run 'Test Workflow Name' - -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to watch & delete -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run on the uniquely named branch to register +wait-for-run RUN_ID --branch $WORKFLOW_BRANCH --event push # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status -# Verify secret matched what was set earlier +# Verify both secrets matched what was set earlier exec gh run view $RUN_ID --log -stdout 'GitHub Actions secret value matches$' +stdout 'GitHub Actions repository secret value matches$' +stdout 'GitHub Actions environment secret value matches$' -- workflow.yml -- -# This workflow is intended to assert the value of the GitHub Actions secret was set appropriately +# This workflow is intended to assert the values of the GitHub Actions secrets were set appropriately name: Test Workflow Name on: - # Allow workflow to be dispatched by gh workflow run - workflow_dispatch: + push: jobs: - # This workflow contains a single job called "assert" that should only pass if the GitHub Actions secret value matches + # This workflow contains a single job called "assert" that should only pass if both GitHub Actions secret values match assert: runs-on: ubuntu-latest - environment: testscripts + environment: $ENV_NAME steps: - - name: Assert secret value matches + - name: Assert secret values match env: - TESTSCRIPTS_ENV: ${{ secrets.TESTSCRIPTS_ENV }} + TESTSCRIPTS_REPO: ${{ secrets.$REPO_SECRET_NAME }} + TESTSCRIPTS_ENV: ${{ secrets.$ENV_SECRET_NAME }} run: | + if [[ "$TESTSCRIPTS_REPO" == "just a repository secret" ]]; then + echo "GitHub Actions repository secret value matches" + else + echo "GitHub Actions repository secret value does not match" + exit 1 + fi if [[ "$TESTSCRIPTS_ENV" == "just a repository environment secret" ]]; then - echo "GitHub Actions secret value matches" + echo "GitHub Actions environment secret value matches" else - echo "GitHub Actions secret value does not match" + echo "GitHub Actions environment secret value does not match" exit 1 fi diff --git a/acceptance/testdata/secret/secret-repo.txtar b/acceptance/testdata/secret/secret-repo.txtar deleted file mode 100644 index ed336626f86..00000000000 --- a/acceptance/testdata/secret/secret-repo.txtar +++ /dev/null @@ -1,76 +0,0 @@ -# Setup environment variables used for testscript -env REPO=$SCRIPT_NAME-$RANDOM_STRING - -# Use gh as a credential helper -exec gh auth setup-git - -# Create a repository with a file so it has a default branch -exec gh repo create $ORG/$REPO --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$REPO - -# Clone the repo -exec gh repo clone $ORG/$REPO -cd $REPO - -# Create a repository secret -exec gh secret set TESTSCRIPTS --body 'just a repository secret' - -# Verify new repository secret exists -exec gh secret list -stdout 'TESTSCRIPTS' - -# Commit workflow file creating dispatchable workflow able to verify secret matches -mkdir .github/workflows -mv ../workflow.yml .github/workflows/workflow.yml -exec git add .github/workflows/workflow.yml -exec git commit -m 'Create workflow file' -exec git push -u origin main - -# Sleep because it takes a second for the workflow to register -sleep 1 - -# Check the workflow is indeed created -exec gh workflow list -stdout 'Test Workflow Name' - -# Run the workflow -exec gh workflow run 'Test Workflow Name' - -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to watch & delete -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID - -# Wait for workflow to complete -exec gh run watch $RUN_ID --exit-status - -# Verify secret matched what was set earlier -exec gh run view $RUN_ID --log -stdout 'GitHub Actions secret value matches$' - --- workflow.yml -- -# This workflow is intended to assert the value of the GitHub Actions secret was set appropriately -name: Test Workflow Name -on: - # Allow workflow to be dispatched by gh workflow run - workflow_dispatch: - -jobs: - # This workflow contains a single job called "assert" that should only pass if the GitHub Actions secret value matches - assert: - runs-on: ubuntu-latest - steps: - - name: Assert secret value matches - env: - TESTSCRIPTS: ${{ secrets.TESTSCRIPTS }} - run: | - if [[ "$TESTSCRIPTS" == "just a repository secret" ]]; then - echo "GitHub Actions secret value matches" - else - echo "GitHub Actions secret value does not match" - exit 1 - fi diff --git a/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar b/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar index f3fa4a47a0a..d3e92e26b59 100644 --- a/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar +++ b/acceptance/testdata/secret/secret-require-remote-disambiguation.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Set up env vars env REPO=${SCRIPT_NAME}-${RANDOM_STRING} diff --git a/acceptance/testdata/skills/skills-install-force.txtar b/acceptance/testdata/skills/skills-install-force.txtar index e6bd520b9cf..d822fd8867b 100644 --- a/acceptance/testdata/skills/skills-install-force.txtar +++ b/acceptance/testdata/skills/skills-install-force.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Install with --force should overwrite an existing skill without error exec gh skill install github/awesome-copilot git-commit --force --dir $WORK/force-test stdout 'Installed git-commit' diff --git a/acceptance/testdata/skills/skills-install-from-local.txtar b/acceptance/testdata/skills/skills-install-from-local.txtar index 0b003fd3ef2..66fb11681cd 100644 --- a/acceptance/testdata/skills/skills-install-from-local.txtar +++ b/acceptance/testdata/skills/skills-install-from-local.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Install from a local directory using --from-local exec gh skill install --from-local $WORK/local-repo git-commit --dir $WORK/output --force stdout 'Installed git-commit' diff --git a/acceptance/testdata/skills/skills-install-invalid-agent.txtar b/acceptance/testdata/skills/skills-install-invalid-agent.txtar index 7e85a9faea1..c680c76de76 100644 --- a/acceptance/testdata/skills/skills-install-invalid-agent.txtar +++ b/acceptance/testdata/skills/skills-install-invalid-agent.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Invalid agent ID should error with valid options ! exec gh skill install github/awesome-copilot git-commit --agent bogus-agent --force stderr 'invalid argument' diff --git a/acceptance/testdata/skills/skills-install-invalid-repo.txtar b/acceptance/testdata/skills/skills-install-invalid-repo.txtar index 2b59582e19d..367b7965add 100644 --- a/acceptance/testdata/skills/skills-install-invalid-repo.txtar +++ b/acceptance/testdata/skills/skills-install-invalid-repo.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Nonexistent repo should error ! exec gh skill install nonexistent-owner-xyz/nonexistent-repo-abc --force --dir $WORK/tmp stderr 'Not Found' diff --git a/acceptance/testdata/skills/skills-install-namespaced.txtar b/acceptance/testdata/skills/skills-install-namespaced.txtar index 9aa83ef5650..5ff22ad2bb6 100644 --- a/acceptance/testdata/skills/skills-install-namespaced.txtar +++ b/acceptance/testdata/skills/skills-install-namespaced.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Two namespaced skills with different base names in the same repo should # be independently installable using path-based disambiguation. # Skills are installed flat (by base name) so each must have a unique name. diff --git a/acceptance/testdata/skills/skills-install-nested-files.txtar b/acceptance/testdata/skills/skills-install-nested-files.txtar index c4fe085e446..2877ac55d26 100644 --- a/acceptance/testdata/skills/skills-install-nested-files.txtar +++ b/acceptance/testdata/skills/skills-install-nested-files.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Install a skill that has nested subdirectories and verify file tree exec gh skill install github/awesome-copilot git-commit --force --dir $WORK/nested-test exists $WORK/nested-test/git-commit/SKILL.md diff --git a/acceptance/testdata/skills/skills-install-nonexistent-skill.txtar b/acceptance/testdata/skills/skills-install-nonexistent-skill.txtar index 44187c4ff8d..93671a8a0ac 100644 --- a/acceptance/testdata/skills/skills-install-nonexistent-skill.txtar +++ b/acceptance/testdata/skills/skills-install-nonexistent-skill.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Installing a skill that doesn't exist in a valid repo should error ! exec gh skill install github/awesome-copilot nonexistent-skill-xyz --force --dir $WORK/tmp stderr 'not found' diff --git a/acceptance/testdata/skills/skills-install-pin.txtar b/acceptance/testdata/skills/skills-install-pin.txtar index 7c87e4b33ff..84978ca61e4 100644 --- a/acceptance/testdata/skills/skills-install-pin.txtar +++ b/acceptance/testdata/skills/skills-install-pin.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Install with --pin to a specific ref exec gh skill install github/awesome-copilot git-commit --scope user --force --pin main stdout 'Installed git-commit' diff --git a/acceptance/testdata/skills/skills-install-scope.txtar b/acceptance/testdata/skills/skills-install-scope.txtar index 52270178a08..593c052ef68 100644 --- a/acceptance/testdata/skills/skills-install-scope.txtar +++ b/acceptance/testdata/skills/skills-install-scope.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Install with --scope project writes to the git repo's .agents/skills/ exec git init --initial-branch=main $WORK/myrepo cd $WORK/myrepo diff --git a/acceptance/testdata/skills/skills-install.txtar b/acceptance/testdata/skills/skills-install.txtar index 442edb797f6..bda81a85b57 100644 --- a/acceptance/testdata/skills/skills-install.txtar +++ b/acceptance/testdata/skills/skills-install.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Install a single skill from a public repo exec gh skill install github/awesome-copilot git-commit --scope user --force --agent github-copilot stdout 'Installed git-commit' diff --git a/acceptance/testdata/skills/skills-preview-noninteractive.txtar b/acceptance/testdata/skills/skills-preview-noninteractive.txtar index 7c276b8d32a..f01327a4856 100644 --- a/acceptance/testdata/skills/skills-preview-noninteractive.txtar +++ b/acceptance/testdata/skills/skills-preview-noninteractive.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Preview with repo only and non-interactive should error ! exec gh skill preview github/awesome-copilot stderr 'must specify a skill name' diff --git a/acceptance/testdata/skills/skills-preview.txtar b/acceptance/testdata/skills/skills-preview.txtar index 76aa9a6ecb1..b2050af435a 100644 --- a/acceptance/testdata/skills/skills-preview.txtar +++ b/acceptance/testdata/skills/skills-preview.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Preview renders skill content and file tree exec gh skill preview github/awesome-copilot git-commit stdout 'SKILL.md' diff --git a/acceptance/testdata/skills/skills-publish-dir-remote.txtar b/acceptance/testdata/skills/skills-publish-dir-remote.txtar index 8f833a76ca4..5725841fbfd 100644 --- a/acceptance/testdata/skills/skills-publish-dir-remote.txtar +++ b/acceptance/testdata/skills/skills-publish-dir-remote.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # When a directory argument is provided to `gh skill publish --dry-run`, # the remote detection must use the target directory's git remotes, # not the current working directory's remotes. diff --git a/acceptance/testdata/skills/skills-publish-dry-run.txtar b/acceptance/testdata/skills/skills-publish-dry-run.txtar index fe4d160c314..0ab7be60ba1 100644 --- a/acceptance/testdata/skills/skills-publish-dry-run.txtar +++ b/acceptance/testdata/skills/skills-publish-dry-run.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Publish dry-run from a directory with no skills/ should fail gracefully mkdir $WORK/empty-dir ! exec gh skill publish --dry-run $WORK/empty-dir diff --git a/acceptance/testdata/skills/skills-publish-lifecycle.txtar b/acceptance/testdata/skills/skills-publish-lifecycle.txtar index d3d6f0a3a72..ed39c5bd211 100644 --- a/acceptance/testdata/skills/skills-publish-lifecycle.txtar +++ b/acceptance/testdata/skills/skills-publish-lifecycle.txtar @@ -1,15 +1,15 @@ + # Full publish lifecycle: create repo, publish, install from it, clean up # Use gh as a credential helper exec gh auth setup-git # Create a private repo for testing -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --private --add-readme -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING -cd $SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO +cd $REPO # Add a test skill mkdir skills/hello-world/scripts @@ -27,7 +27,7 @@ exec gh release view v0.1.0 stdout 'v0.1.0' # Install from our test repo -exec gh skill install $ORG/$SCRIPT_NAME-$RANDOM_STRING hello-world --scope user --force +exec gh skill install $ORG/$REPO hello-world --scope user --force stdout 'Installed hello-world' # Verify installed files exist with correct metadata @@ -36,11 +36,11 @@ exists $HOME/.copilot/skills/hello-world/scripts/setup.sh grep 'github-repo' $HOME/.copilot/skills/hello-world/SKILL.md # Install with --pin -exec gh skill install $ORG/$SCRIPT_NAME-$RANDOM_STRING hello-world --scope user --force --pin v0.1.0 +exec gh skill install $ORG/$REPO hello-world --scope user --force --pin v0.1.0 stdout 'Installed hello-world' # Preview from our test repo -exec gh skill preview $ORG/$SCRIPT_NAME-$RANDOM_STRING hello-world +exec gh skill preview $ORG/$REPO hello-world stdout 'Hello World' # Update dry-run should find installed skill diff --git a/acceptance/testdata/skills/skills-search-noresults.txtar b/acceptance/testdata/skills/skills-search-noresults.txtar deleted file mode 100644 index c51d7b56811..00000000000 --- a/acceptance/testdata/skills/skills-search-noresults.txtar +++ /dev/null @@ -1,4 +0,0 @@ -# Search for something unlikely to exist returns empty stdout -# NoResultsError is silent in non-TTY (exits 0 with no output) -exec gh skill search zzzznonexistenttotallyfakeskillxyz123 -! stdout . diff --git a/acceptance/testdata/skills/skills-search-page.txtar b/acceptance/testdata/skills/skills-search-page.txtar deleted file mode 100644 index 48409c2354d..00000000000 --- a/acceptance/testdata/skills/skills-search-page.txtar +++ /dev/null @@ -1,3 +0,0 @@ -# Pagination returns results on page 2 -exec gh skill search --owner github copilot --page 2 -stdout 'copilot' diff --git a/acceptance/testdata/skills/skills-search.txtar b/acceptance/testdata/skills/skills-search.txtar index e16936b0d1b..7dc24db2af2 100644 --- a/acceptance/testdata/skills/skills-search.txtar +++ b/acceptance/testdata/skills/skills-search.txtar @@ -1,12 +1,17 @@ -# Search for skills matching a query -exec gh skill search --owner github copilot -stdout 'copilot' -# Search with JSON output -exec gh skill search copilot --json skillName,repo --limit 1 +fixture-repo none + +# Keep these two live searches in one script and within the five-request budget. +# Search for skills from an owner with JSON output and a result limit +exec gh skill search --owner github copilot --json skillName,repo --limit 1 +stdout 'copilot' stdout '"skillName"' stdout '"repo"' +# Pagination returns table results on page 2 +exec gh skill search --owner github copilot --page 2 +stdout 'copilot' + # Search with a short query should error ! exec gh skill search a stderr 'at least' \ No newline at end of file diff --git a/acceptance/testdata/skills/skills-update-inplace.txtar b/acceptance/testdata/skills/skills-update-inplace.txtar index b7dde99a96d..4fecb809cff 100644 --- a/acceptance/testdata/skills/skills-update-inplace.txtar +++ b/acceptance/testdata/skills/skills-update-inplace.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Updating a namespaced skill via --dir must write back to its original # location and must NOT delete the original directory (issue #13370). diff --git a/acceptance/testdata/skills/skills-update-noinstalled.txtar b/acceptance/testdata/skills/skills-update-noinstalled.txtar index 7fd19541bc0..0fe60302b31 100644 --- a/acceptance/testdata/skills/skills-update-noinstalled.txtar +++ b/acceptance/testdata/skills/skills-update-noinstalled.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Update with no installed skills should report appropriately exec gh skill update --dry-run --all --dir $WORK/empty-dir stderr 'No installed skills found' diff --git a/acceptance/testdata/skills/skills-update.txtar b/acceptance/testdata/skills/skills-update.txtar index 52933a5f86d..5f5194286be 100644 --- a/acceptance/testdata/skills/skills-update.txtar +++ b/acceptance/testdata/skills/skills-update.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Dry-run update should find the installed skill and report status exec gh skill update --dry-run --all --dir $WORK/skills-dir stdout 'git-commit' diff --git a/acceptance/testdata/ssh-key/ssh-key.txtar b/acceptance/testdata/ssh-key/ssh-key.txtar index 62d04b02711..f7fd2c63978 100644 --- a/acceptance/testdata/ssh-key/ssh-key.txtar +++ b/acceptance/testdata/ssh-key/ssh-key.txtar @@ -1,5 +1,8 @@ + skip 'it modifies the user''s personal GitHub account SSH keys' +fixture-repo none + # scopes admin:ssh_signing_key,admin:public_key # Generate a globally unique account SSH key diff --git a/acceptance/testdata/telemetry/accessibility-dimensions-disabled.txtar b/acceptance/testdata/telemetry/accessibility-dimensions-disabled.txtar index 2a10d23da71..e3617f90c75 100644 --- a/acceptance/testdata/telemetry/accessibility-dimensions-disabled.txtar +++ b/acceptance/testdata/telemetry/accessibility-dimensions-disabled.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Telemetry log mode records accessibility features as disabled by default env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/accessibility-dimensions.txtar b/acceptance/testdata/telemetry/accessibility-dimensions.txtar index 9df0b524019..3988f402285 100644 --- a/acceptance/testdata/telemetry/accessibility-dimensions.txtar +++ b/acceptance/testdata/telemetry/accessibility-dimensions.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Telemetry log mode records accessibility feature state as dimensions env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/agent-dimensions.txtar b/acceptance/testdata/telemetry/agent-dimensions.txtar index 14dbe47b82d..bb6a208597d 100644 --- a/acceptance/testdata/telemetry/agent-dimensions.txtar +++ b/acceptance/testdata/telemetry/agent-dimensions.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Telemetry log mode records the invoking agent and adapted spinner state env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/command-invocation.txtar b/acceptance/testdata/telemetry/command-invocation.txtar index d174c5c08f1..18abc1ac824 100644 --- a/acceptance/testdata/telemetry/command-invocation.txtar +++ b/acceptance/testdata/telemetry/command-invocation.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Telemetry log mode outputs command invocation event to stderr env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/no-telemetry-for-alias.txtar b/acceptance/testdata/telemetry/no-telemetry-for-alias.txtar index 2bfe0657dc2..9087881a55e 100644 --- a/acceptance/testdata/telemetry/no-telemetry-for-alias.txtar +++ b/acceptance/testdata/telemetry/no-telemetry-for-alias.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Aliases should not leak their user-defined names via telemetry, but the # resolved inner command should still record normally — its path is a core # gh command and conveys no user-authored identifier. diff --git a/acceptance/testdata/telemetry/no-telemetry-for-completion.txtar b/acceptance/testdata/telemetry/no-telemetry-for-completion.txtar index 1204a7913bb..24966fa3a5b 100644 --- a/acceptance/testdata/telemetry/no-telemetry-for-completion.txtar +++ b/acceptance/testdata/telemetry/no-telemetry-for-completion.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # The completion command should not generate a telemetry event env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/no-telemetry-for-extension.txtar b/acceptance/testdata/telemetry/no-telemetry-for-extension.txtar index 5e9d2ea5d2a..f0f668421d1 100644 --- a/acceptance/testdata/telemetry/no-telemetry-for-extension.txtar +++ b/acceptance/testdata/telemetry/no-telemetry-for-extension.txtar @@ -1,8 +1,11 @@ + # Third-party extensions must not generate telemetry events, since the # extension command name can be a user-authored identifier (e.g. an # organization or project name). [!exec:bash] skip +fixture-repo none + env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/no-telemetry-for-ghes-user.txtar b/acceptance/testdata/telemetry/no-telemetry-for-ghes-user.txtar index e8e1d8ffe97..1c145ba4480 100644 --- a/acceptance/testdata/telemetry/no-telemetry-for-ghes-user.txtar +++ b/acceptance/testdata/telemetry/no-telemetry-for-ghes-user.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # GHES users should not get telemetry even when telemetry is enabled env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/no-telemetry-for-send-telemetry.txtar b/acceptance/testdata/telemetry/no-telemetry-for-send-telemetry.txtar index 15e59fcf5e1..f11191e98af 100644 --- a/acceptance/testdata/telemetry/no-telemetry-for-send-telemetry.txtar +++ b/acceptance/testdata/telemetry/no-telemetry-for-send-telemetry.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # The send-telemetry command should not itself generate a telemetry event env GH_TELEMETRY=log env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/telemetry-failure-does-not-break-command.txtar b/acceptance/testdata/telemetry/telemetry-failure-does-not-break-command.txtar index 14c4b67a6a8..16f96524077 100644 --- a/acceptance/testdata/telemetry/telemetry-failure-does-not-break-command.txtar +++ b/acceptance/testdata/telemetry/telemetry-failure-does-not-break-command.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Command completes successfully even when telemetry endpoint is unreachable env GH_TELEMETRY=enabled env GH_TELEMETRY_SAMPLE_RATE=100 diff --git a/acceptance/testdata/telemetry/telemetry-for-official-extension-stub.txtar b/acceptance/testdata/telemetry/telemetry-for-official-extension-stub.txtar index 603dd2ae183..d6ddc6109af 100644 --- a/acceptance/testdata/telemetry/telemetry-for-official-extension-stub.txtar +++ b/acceptance/testdata/telemetry/telemetry-for-official-extension-stub.txtar @@ -1,3 +1,6 @@ + +fixture-repo none + # Official extension stubs (the hidden commands suggesting installation of # GitHub-owned extensions) are safe to report via telemetry: their command # names come from a fixed, hard-coded registry and do not contain any diff --git a/acceptance/testdata/variable/variable-org.txtar b/acceptance/testdata/variable/variable-org.txtar index c09381b295b..7f7f6197180 100644 --- a/acceptance/testdata/variable/variable-org.txtar +++ b/acceptance/testdata/variable/variable-org.txtar @@ -1,16 +1,18 @@ + +fixture-repo none + # Setup environment variables used for testscript env2upper VAR_NAME=${SCRIPT_NAME}_${RANDOM_STRING} -# Confirm organization variable does not exist, will fail admin:org scope missing -exec gh variable list --org $ORG -! stdout $VAR_NAME - # Create an organization variable exec gh variable set $VAR_NAME --org $ORG --body 'just an org variable' # Defer organization variable cleanup defer gh variable delete $VAR_NAME --org $ORG +# Allow the organization variable list to reflect the write +sleep 1 + # Verify new organization variable exists exec gh variable list --org $ORG stdout $VAR_NAME diff --git a/acceptance/testdata/variable/variable-repo-env.txtar b/acceptance/testdata/variable/variable-repo-env.txtar index 99dbb6b9f60..da2445ea100 100644 --- a/acceptance/testdata/variable/variable-repo-env.txtar +++ b/acceptance/testdata/variable/variable-repo-env.txtar @@ -1,28 +1,23 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} -env ENV_NAME=testscripts -env VAR_NAME=TESTSCRIPTS_ENV +env ENV_NAME=testscripts-${RANDOM_STRING} +env2upper VAR_NAME=TESTSCRIPTS_ENV_${RANDOM_STRING} # Create a repository where the variable will be registered -exec gh repo create $ORG/$REPO --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$REPO +fixture-repo shared REPO -# Clone the repo -exec gh repo clone $ORG/$REPO -cd $REPO +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a repository environment, will fail if organization does not have environment support exec gh api /repos/$ORG/$REPO/environments/$ENV_NAME -X PUT --jq '.name' -# Verify repository environment variable does not exist -exec gh variable list --env $ENV_NAME -! stdout $VAR_NAME - # Create a repository environment variable exec gh variable set $VAR_NAME --env $ENV_NAME --body 'just a repo env variable' +# Allow the environment variable list to reflect the write +sleep 1 + # Verify new repository environment variable exists exec gh variable list --env $ENV_NAME stdout $VAR_NAME diff --git a/acceptance/testdata/variable/variable-repo.txtar b/acceptance/testdata/variable/variable-repo.txtar index 9ff64db0ffe..cf20c9a33f4 100644 --- a/acceptance/testdata/variable/variable-repo.txtar +++ b/acceptance/testdata/variable/variable-repo.txtar @@ -1,24 +1,19 @@ + # Setup environment variables used for testscript -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} -env VAR_NAME=TESTSCRIPTS +env2upper VAR_NAME=TESTSCRIPTS_${RANDOM_STRING} # Create a repository where the variable will be registered -exec gh repo create $ORG/$REPO --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$REPO +fixture-repo shared REPO -# Clone the repo -exec gh repo clone $ORG/$REPO -cd $REPO - -# Verify repository variable does not exist -exec gh variable list -! stdout $VAR_NAME +# Target the fixture repository +env GH_REPO=$ORG/$REPO # Create a repository variable exec gh variable set $VAR_NAME --body 'just a repo variable' +# Allow the repository variable list to reflect the write +sleep 1 + # Verify new repository variable exists exec gh variable list stdout $VAR_NAME diff --git a/acceptance/testdata/workflow/cache-list-delete.txtar b/acceptance/testdata/workflow/cache-list-delete.txtar index 6a99f4bc268..ba10629338a 100644 --- a/acceptance/testdata/workflow/cache-list-delete.txtar +++ b/acceptance/testdata/workflow/cache-list-delete.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -28,12 +26,8 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to watch -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run to register +wait-for-run RUN_ID # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status diff --git a/acceptance/testdata/workflow/cache-list-empty.txtar b/acceptance/testdata/workflow/cache-list-empty.txtar index 0e6d32cb70a..694151da8b3 100644 --- a/acceptance/testdata/workflow/cache-list-empty.txtar +++ b/acceptance/testdata/workflow/cache-list-empty.txtar @@ -1,17 +1,14 @@ + # It's unclear what we want to do with these acceptance tests beyond our GHEC discovery, so skip new ones by default skip # Set up env vars -env REPO=${ORG}/${SCRIPT_NAME}-${RANDOM_STRING} # Create a repository with a file so it has a default branch -exec gh repo create ${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${REPO} +fixture-repo isolated REPO # Set the repo to be targeted by all following commands -env GH_REPO=${REPO} +env GH_REPO=${ORG}/${REPO} # Listing the cache non-interactively shows nothing exec gh cache list diff --git a/acceptance/testdata/workflow/run-cancel.txtar b/acceptance/testdata/workflow/run-cancel.txtar index 08d8d519a48..9830bf06a24 100644 --- a/acceptance/testdata/workflow/run-cancel.txtar +++ b/acceptance/testdata/workflow/run-cancel.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -28,23 +26,15 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 +# Wait for the run to register +wait-for-run RUN_ID -# Get the run ID we want to cancel -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait until the cancellation endpoint can target the run +wait-for-run-status $RUN_ID in_progress # cancel the workflow run exec gh run cancel $RUN_ID -stdout '✓ Request to cancel workflow [0-9]+ submitted.' - -# Wait for workflow to complete -exec gh run watch $RUN_ID - -# Check the workflow run is cancelled -exec gh run list --json conclusion --jq '.[0].conclusion' -stdout 'cancelled' +stdout '✓ Request to cancel workflow '$RUN_ID' submitted\.' -- workflow.yml -- # This is a basic workflow to help you get started with Actions @@ -62,12 +52,10 @@ jobs: build: # The type of runner that the job will run on runs-on: ubuntu-latest + timeout-minutes: 2 # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4 - - # Runs a single command using the runners shell - - name: Run a one-line script - run: sleep 30 + # Keep the run active long enough for the cancellation request. + - name: Wait to be cancelled + run: sleep 300 diff --git a/acceptance/testdata/workflow/run-delete.txtar b/acceptance/testdata/workflow/run-delete.txtar index b78135330e7..6b87f203216 100644 --- a/acceptance/testdata/workflow/run-delete.txtar +++ b/acceptance/testdata/workflow/run-delete.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -28,12 +26,8 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to watch & delete -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run to register +wait-for-run RUN_ID # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status diff --git a/acceptance/testdata/workflow/run-download.txtar b/acceptance/testdata/workflow/run-download.txtar index 8089cf2cd2e..35926cba020 100644 --- a/acceptance/testdata/workflow/run-download.txtar +++ b/acceptance/testdata/workflow/run-download.txtar @@ -1,14 +1,11 @@ + # Set up env -env REPO=${SCRIPT_NAME}-${RANDOM_STRING} # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create ${ORG}/${REPO} --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes ${ORG}/${REPO} +fixture-repo isolated REPO # Clone the repo exec gh repo clone ${ORG}/${REPO} @@ -21,8 +18,8 @@ exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -31,12 +28,8 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to watch -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run to register +wait-for-run RUN_ID # Wait for workflow to complete exec gh run watch ${RUN_ID} --exit-status diff --git a/acceptance/testdata/workflow/run-rerun.txtar b/acceptance/testdata/workflow/run-rerun.txtar index 446aabbc4ca..8413aff868d 100644 --- a/acceptance/testdata/workflow/run-rerun.txtar +++ b/acceptance/testdata/workflow/run-rerun.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -28,12 +26,8 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to rerun -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run to register +wait-for-run RUN_ID # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status diff --git a/acceptance/testdata/workflow/run-view-log-escape-sequences.txtar b/acceptance/testdata/workflow/run-view-log-escape-sequences.txtar index 47978cf4dce..f2d407a9974 100644 --- a/acceptance/testdata/workflow/run-view-log-escape-sequences.txtar +++ b/acceptance/testdata/workflow/run-view-log-escape-sequences.txtar @@ -1,3 +1,4 @@ + # This test ensures that a malicious workflow which emit terminal control sequences (ESC, OSC, CSI) in # its log output does not result in terminal injection when logs are displayed using `gh run view --log` @@ -5,34 +6,27 @@ exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # Commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow with escape sequences' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Escape Sequence PoC' # Run the workflow exec gh workflow run 'Escape Sequence PoC' -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to view -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run to register +wait-for-run RUN_ID # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status diff --git a/acceptance/testdata/workflow/run-view.txtar b/acceptance/testdata/workflow/run-view.txtar index 25f12c3a520..0002afa2b60 100644 --- a/acceptance/testdata/workflow/run-view.txtar +++ b/acceptance/testdata/workflow/run-view.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -28,12 +26,8 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 - -# Get the run ID we want to view -exec gh run list --json databaseId --jq '.[0].databaseId' -stdout2env RUN_ID +# Wait for the run to register +wait-for-run RUN_ID # Wait for workflow to complete exec gh run watch $RUN_ID --exit-status diff --git a/acceptance/testdata/workflow/workflow-enable-disable.txtar b/acceptance/testdata/workflow/workflow-enable-disable.txtar index f0b58116fc4..8965561a64f 100644 --- a/acceptance/testdata/workflow/workflow-enable-disable.txtar +++ b/acceptance/testdata/workflow/workflow-enable-disable.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list diff --git a/acceptance/testdata/workflow/workflow-list.txtar b/acceptance/testdata/workflow/workflow-list.txtar index ad0d87c88bd..70722e2e0db 100644 --- a/acceptance/testdata/workflow/workflow-list.txtar +++ b/acceptance/testdata/workflow/workflow-list.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list diff --git a/acceptance/testdata/workflow/workflow-run.txtar b/acceptance/testdata/workflow/workflow-run.txtar index 010189c0141..1cbdad36728 100644 --- a/acceptance/testdata/workflow/workflow-run.txtar +++ b/acceptance/testdata/workflow/workflow-run.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow list @@ -28,12 +26,12 @@ stdout 'Test Workflow Name' # Run the workflow exec gh workflow run 'Test Workflow Name' -# It takes some time for a workflow run to register -sleep 10 +# Wait for the run to register +wait-for-run RUN_ID # Check the workflow run exists exec gh run list -stdout 'Test Workflow Name' +stdout 'Test Workflow Name' -- workflow.yml -- # This is a basic workflow to help you get started with Actions diff --git a/acceptance/testdata/workflow/workflow-view.txtar b/acceptance/testdata/workflow/workflow-view.txtar index d3bc3d25224..b43c04c03d1 100644 --- a/acceptance/testdata/workflow/workflow-view.txtar +++ b/acceptance/testdata/workflow/workflow-view.txtar @@ -1,25 +1,23 @@ + # Use gh as a credential helper exec gh auth setup-git # Create a repository with a file so it has a default branch -exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private - -# Defer repo cleanup -defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING +fixture-repo isolated REPO # Clone the repo -exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING +exec gh repo clone $ORG/$REPO # commit the workflow file -cd $SCRIPT_NAME-$RANDOM_STRING +cd $REPO mkdir .github/workflows mv ../workflow.yml .github/workflows/workflow.yml exec git add .github/workflows/workflow.yml exec git commit -m 'Create workflow file' exec git push -u origin main -# Sleep because it takes a second for the workflow to register -sleep 1 +# Wait for the workflow definition to register +wait-for-workflow 'Test Workflow Name' # Check the workflow is indeed created exec gh workflow view 'Test Workflow Name' diff --git a/acceptance/user_capability_test.go b/acceptance/user_capability_test.go new file mode 100644 index 00000000000..dc78b9abc77 --- /dev/null +++ b/acceptance/user_capability_test.go @@ -0,0 +1,156 @@ +package acceptance_test + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var fixtureRepositoryEnvironmentVariable = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`) +var directRepositoryCreation = regexp.MustCompile(`^(?:\[[^]]+\] )*(?:! )?exec gh repo create(?: |$)`) + +const fixtureRepositoryDeclarationHelp = `choose one: + fixture-repo shared REPO + fixture-repo isolated REPO + fixture-repo none + +Use shared when concurrent scripts can safely reuse the repository, isolated +when the script needs clean or repository-global state, and none when the +script creates its own repositories or does not need one. +See acceptance/README.md#script-metadata for details.` + +func validateFixtureRepositoryDeclaration(file string) error { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + + var declarations [][]string + hasDirectRepositoryCreation := false + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSuffix(scanner.Text(), "\r") + if strings.HasPrefix(line, "-- ") && strings.HasSuffix(line, " --") { + break + } + if strings.HasPrefix(line, "fixture-repo ") { + declarations = append(declarations, strings.Fields(line)) + } + if directRepositoryCreation.MatchString(line) { + hasDirectRepositoryCreation = true + } + } + if err := scanner.Err(); err != nil { + return err + } + + if len(declarations) != 1 { + return fmt.Errorf("%s: script must contain exactly one fixture-repo declaration; %s", file, fixtureRepositoryDeclarationHelp) + } + + declaration := declarations[0] + switch { + case len(declaration) == 2 && declaration[1] == "none": + return nil + case len(declaration) == 3 && (declaration[1] == "shared" || declaration[1] == "isolated"): + if !fixtureRepositoryEnvironmentVariable.MatchString(declaration[2]) { + return fmt.Errorf("%s: fixture repository environment variable must match %s; for example:\n fixture-repo shared REPO", file, fixtureRepositoryEnvironmentVariable) + } + if hasDirectRepositoryCreation { + return fmt.Errorf("%s: scripts using a managed fixture repository must not run gh repo create; either remove gh repo create or use:\n fixture-repo none", file) + } + return nil + default: + return fmt.Errorf("%s: invalid fixture-repo declaration; %s", file, fixtureRepositoryDeclarationHelp) + } +} + +func TestAcceptanceScriptsDeclareFixtureRepository(t *testing.T) { + files, err := filepath.Glob(filepath.Join("testdata", "*", "*.txtar")) + require.NoError(t, err) + require.NotEmpty(t, files) + + for _, file := range files { + t.Run(file, func(t *testing.T) { + require.NoError(t, validateFixtureRepositoryDeclaration(file)) + }) + } +} + +func TestValidateFixtureRepositoryDeclaration(t *testing.T) { + tests := []struct { + name string + content string + wantErr string + }{ + { + name: "shared", + content: "fixture-repo shared REPO\n", + }, + { + name: "isolated", + content: "fixture-repo isolated REPO\n", + }, + { + name: "none", + content: "fixture-repo none\n", + }, + { + name: "missing", + content: "", + wantErr: "choose one:\n fixture-repo shared REPO\n fixture-repo isolated REPO\n fixture-repo none", + }, + { + name: "multiple", + content: "fixture-repo shared REPO\nfixture-repo isolated OTHER_REPO\n", + wantErr: "choose one:\n fixture-repo shared REPO\n fixture-repo isolated REPO\n fixture-repo none", + }, + { + name: "invalid mode", + content: "fixture-repo pristine REPO\n", + wantErr: "choose one:\n fixture-repo shared REPO\n fixture-repo isolated REPO\n fixture-repo none", + }, + { + name: "invalid environment variable", + content: "fixture-repo shared repo\n", + wantErr: "for example:\n fixture-repo shared REPO", + }, + { + name: "managed fixture creates repository", + content: "fixture-repo isolated REPO\nexec gh repo create $ORG/example --private\n", + wantErr: "either remove gh repo create or use:\n fixture-repo none", + }, + { + name: "managed fixture conditionally creates repository", + content: "fixture-repo shared REPO\n[windows] ! exec gh repo create $ORG/example --private\n", + wantErr: "either remove gh repo create or use:\n fixture-repo none", + }, + { + name: "ignores archive contents", + content: "fixture-repo none\n\n" + + "-- script.sh --\n" + + "fixture-repo shared REPO\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file := filepath.Join(t.TempDir(), "script.txtar") + require.NoError(t, os.WriteFile(file, []byte(tt.content), 0o600)) + + err := validateFixtureRepositoryDeclaration(file) + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} diff --git a/script/api-host-gateway/test.sh b/script/api-host-gateway/test.sh index ed273691075..03c7539f14d 100755 --- a/script/api-host-gateway/test.sh +++ b/script/api-host-gateway/test.sh @@ -184,7 +184,7 @@ expect_gh() { out=$(run_gh "$@" 2>"$WORK/stderr.txt") rc=$? - if [ $rc -ne 0 ]; then + if [ "$rc" -ne 0 ]; then fail "$desc: gh exited $rc: $(tr '\n' ' ' <"$WORK/stderr.txt")" return 1 fi @@ -205,7 +205,7 @@ expect_gh_failure() { out=$(run_gh "$@" 2>&1) rc=$? - if [ $rc -ne 0 ]; then + if [ "$rc" -ne 0 ]; then pass "$desc" else fail "$desc: gh unexpectedly succeeded with [$out]" @@ -329,14 +329,14 @@ blackhole_off if [ "${GH_APIHOST_ACCEPTANCE:-no}" = "yes" ]; then [ -n "$ORG_TOKEN" ] || die "GH_APIHOST_ORG_TOKEN is required for the acceptance run" - # run_subset