diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 35b211236..9a0acb58e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,7 +6,7 @@ We accept pull requests for bug fixes and features where we've discussed the app Please do: -* Check existing issues to verify that an existing [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature. +* Check issues to verify that a [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature. * Open an issue if things aren't working as expected. * Open an issue to propose a significant change. * Open an issue to propose a design for an issue labelled [`needs-design` and `help wanted`][needs design and help wanted], following the [proposing a design guidelines](#proposing-a-design) instructions below. diff --git a/acceptance/README.md b/acceptance/README.md index 732369bc0..750cb75d1 100644 --- a/acceptance/README.md +++ b/acceptance/README.md @@ -57,7 +57,7 @@ The following custom environment variables are made available to the scripts: * `ORG`: Set to the value of the `GH_ACCEPTANCE_ORG` env var provided to `go test` * `GH_TOKEN`: Set to the value of the `GH_ACCEPTANCE_TOKEN` env var provided to `go test` * `RANDOM_STRING`: Set to a length 10 random string of letters to help isolate globally visible resources - * `SCRIPT_NAME`: Set to the name of the `testscript` currently running, without extension e.g. `pr-view` + * `SCRIPT_NAME`: Set to the name of the `testscript` currently running, without extension and replacing hyphens with underscores e.g. `pr_view` * `HOME`: Set to the initial working directory. Required for `git` operations * `GH_CONFIG_DIR`: Set to the initial working directory. Required for `gh` operations @@ -72,13 +72,6 @@ The following custom commands are defined within [`acceptance_test.go`](./accept defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING ``` -- `env2lower`: set environment variable to the lowercase version of another environment variable - - ```txtar - # Prepare repository name, which is only lowercase - env2lower REPO_NAME=$RANDOM_STRING - ``` - - `env2upper`: set environment variable to the uppercase version of another environment variable ```txtar @@ -86,6 +79,22 @@ The following custom commands are defined within [`acceptance_test.go`](./accept env2upper ORG_SECRET_NAME=$RANDOM_STRING ``` +- `replace`: replace placeholders in file with interpolated content provided + + ```txtar + env2upper SECRET_NAME=$SCRIPT_NAME_$RANDOM_STRING + + # Modify workflow file to use generated organization secret name + mv ../workflow.yml .github/workflows/workflow.yml + replace .github/workflows/workflow.yml SECRET_NAME=$SECRET_NAME + + -- workflow.yml -- + on: + workflow_dispatch: + env: + ORG_SECRET: ${{ secrets.$SECRET_NAME }} + ``` + - `stdout2env`: set environment variable containing standard output from previous command ```txtar diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index ef8e85ddc..bdd631703 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -6,8 +6,10 @@ import ( "fmt" "os" "path" + "strconv" "strings" "testing" + "time" "math/rand" @@ -43,6 +45,51 @@ func TestIssues(t *testing.T) { testscript.Run(t, testScriptParamsFor(tsEnv, "pr")) } +func TestWorkflows(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "workflow")) +} + +func TestAPI(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "api")) +} + +func TestReleases(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "release")) +} + +func TestSearches(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "search")) +} + +func TestRepo(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "repo")) +} + func TestSecrets(t *testing.T) { var tsEnv testScriptEnv if err := tsEnv.fromEnv(); err != nil { @@ -91,7 +138,11 @@ func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error { if !ok { ts.T().Fatal("script name not found") } - ts.Setenv("SCRIPT_NAME", scriptName) + + // When using script name to uniquely identify where test data comes from, + // some places like GitHub Actions secret names don't accept hyphens. + // Replace them with underscores until such a time this becomes a problem. + ts.Setenv("SCRIPT_NAME", strings.ReplaceAll(scriptName, "-", "_")) ts.Setenv("HOME", ts.Cd) ts.Setenv("GH_CONFIG_DIR", ts.Cd) @@ -135,23 +186,6 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, } }) }, - "env2lower": func(ts *testscript.TestScript, neg bool, args []string) { - if neg { - ts.Fatalf("unsupported: ! env2lower") - } - if len(args) == 0 { - ts.Fatalf("usage: env2lower name=value ...") - } - for _, env := range args { - i := strings.Index(env, "=") - - if i < 0 { - ts.Fatalf("env2lower: argument does not match name=value") - } - - ts.Setenv(env[:i], strings.ToLower(env[i+1:])) - } - }, "env2upper": func(ts *testscript.TestScript, neg bool, args []string) { if neg { ts.Fatalf("unsupported: ! env2upper") @@ -169,6 +203,43 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, ts.Setenv(env[:i], strings.ToUpper(env[i+1:])) } }, + "replace": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! replace") + } + if len(args) < 2 { + ts.Fatalf("usage: replace file env...") + } + + src := ts.MkAbs(args[0]) + ts.Logf("replace src: %s", src) + + // Preserve the existing file mode while replacing the contents similar to native cp behavior + info, err := os.Stat(src) + ts.Check(err) + mode := info.Mode() & 0o777 + data, err := os.ReadFile(src) + ts.Check(err) + + for _, arg := range args[1:] { + i := strings.Index(arg, "=") + if i < 0 { + ts.Fatalf("replace: %s argument does not match name=value", arg) + } + + name := fmt.Sprintf("$%s", arg[:i]) + value := arg[i+1:] + ts.Logf("replace %s: %s", name, value) + + // `replace` was originally built similar to `cp` and `cmpenv`, expanding environment variables within a file. + // However files with content that looks like environments variable such as GitHub Actions workflows + // were being modified unexpectedly. Thus `replace` has been designed to using string replacement + // looking for `$KEY` specifically. + data = []byte(strings.ReplaceAll(string(data), name, value)) + } + + ts.Check(os.WriteFile(src, data, mode)) + }, "stdout2env": func(ts *testscript.TestScript, neg bool, args []string) { if neg { ts.Fatalf("unsupported: ! stdout2env") @@ -179,6 +250,23 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, ts.Setenv(args[0], strings.TrimRight(ts.ReadFile("stdout"), "\n")) }, + "sleep": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! sleep") + } + if len(args) != 1 { + ts.Fatalf("usage: sleep seconds") + } + + // sleep for the given number of seconds + seconds, err := strconv.Atoi(args[0]) + if err != nil { + ts.Fatalf("invalid number of seconds: %v", err) + } + + d := time.Duration(seconds) * time.Second + time.Sleep(d) + }, } } diff --git a/acceptance/testdata/api/basic-graphql.txtar b/acceptance/testdata/api/basic-graphql.txtar new file mode 100644 index 000000000..15c16c49c --- /dev/null +++ b/acceptance/testdata/api/basic-graphql.txtar @@ -0,0 +1,3 @@ +# Basic graphql request +exec gh api graphql -f query='query { viewer { login } }' +stdout '"login":' \ No newline at end of file diff --git a/acceptance/testdata/api/basic-rest.txtar b/acceptance/testdata/api/basic-rest.txtar new file mode 100644 index 000000000..58d3b7570 --- /dev/null +++ b/acceptance/testdata/api/basic-rest.txtar @@ -0,0 +1,3 @@ +# Basic REST request +exec gh api /user +stdout '"login":' \ No newline at end of file diff --git a/acceptance/testdata/release/release-create.txtar b/acceptance/testdata/release/release-create.txtar new file mode 100644 index 000000000..3bdafe769 --- /dev/null +++ b/acceptance/testdata/release/release-create.txtar @@ -0,0 +1,12 @@ +# 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 release in the repo +cd $SCRIPT_NAME-$RANDOM_STRING +exec gh release create v1.2.3 --notes 'awesome release' --latest diff --git a/acceptance/testdata/release/release-list.txtar b/acceptance/testdata/release/release-list.txtar new file mode 100644 index 000000000..844b25daa --- /dev/null +++ b/acceptance/testdata/release/release-list.txtar @@ -0,0 +1,16 @@ +# 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 release in the repo +cd $SCRIPT_NAME-$RANDOM_STRING +exec gh release create v1.2.3 --notes 'awesome release' --latest + +# List the releases +exec gh release list +stdout 'v1.2.3' \ No newline at end of file diff --git a/acceptance/testdata/release/release-upload-download.txtar b/acceptance/testdata/release/release-upload-download.txtar new file mode 100644 index 000000000..e19bc06d7 --- /dev/null +++ b/acceptance/testdata/release/release-upload-download.txtar @@ -0,0 +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 release in the repo +cd $SCRIPT_NAME-$RANDOM_STRING +exec gh release create v1.2.3 --notes 'awesome release' --latest + +# Upload an asset to the release +exec gh release upload v1.2.3 ../asset.txt + +# Download the asset from the release +exec gh release download v1.2.3 +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 + +-- asset.txt -- +Hello, world! diff --git a/acceptance/testdata/release/release-view.txtar b/acceptance/testdata/release/release-view.txtar new file mode 100644 index 000000000..a7138812a --- /dev/null +++ b/acceptance/testdata/release/release-view.txtar @@ -0,0 +1,16 @@ +# 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 release in the repo +cd $SCRIPT_NAME-$RANDOM_STRING +exec gh release create v1.2.3 --notes 'awesome release' --latest + +# View the release +exec gh release view v1.2.3 +stdout 'v1.2.3' \ No newline at end of file diff --git a/acceptance/testdata/repo/repo-archive-unarchive.txtar b/acceptance/testdata/repo/repo-archive-unarchive.txtar new file mode 100644 index 000000000..33ff519f3 --- /dev/null +++ b/acceptance/testdata/repo/repo-archive-unarchive.txtar @@ -0,0 +1,23 @@ +# 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 + +# Check that the repo exists and isn't archived +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived' +stdout 'false' + +# Archive the repo +exec gh repo archive $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes + +# Check that the repo is archived +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived' +stdout 'true' + +# Unarchive the repo +exec gh repo unarchive $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes + +# Check that the repo is unarchived +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=isArchived --jq='.isArchived' +stdout 'false' diff --git a/acceptance/testdata/repo/repo-clone.txtar b/acceptance/testdata/repo/repo-clone.txtar new file mode 100644 index 000000000..b90a0894b --- /dev/null +++ b/acceptance/testdata/repo/repo-clone.txtar @@ -0,0 +1,11 @@ +# 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 + +# Ensure the repo was cloned +exists $SCRIPT_NAME-$RANDOM_STRING/README.md diff --git a/acceptance/testdata/repo/repo-create-view.txtar b/acceptance/testdata/repo/repo-create-view.txtar new file mode 100644 index 000000000..9774def35 --- /dev/null +++ b/acceptance/testdata/repo/repo-create-view.txtar @@ -0,0 +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 + +# Check that the repo exists +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json=name --jq='.name' +stdout $SCRIPT_NAME-$RANDOM_STRING diff --git a/acceptance/testdata/repo/repo-delete.txtar b/acceptance/testdata/repo/repo-delete.txtar new file mode 100644 index 000000000..b82388068 --- /dev/null +++ b/acceptance/testdata/repo/repo-delete.txtar @@ -0,0 +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 + +# Check that the repo exists +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json name --jq '.name' +stdout $SCRIPT_NAME-$RANDOM_STRING + +# Delete the repo +exec gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING + +# Ensure that the repo was deleted +! exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING +stderr 'Could not resolve to a Repository with the name' diff --git a/acceptance/testdata/repo/repo-deploy-key.txtar b/acceptance/testdata/repo/repo-deploy-key.txtar new file mode 100644 index 000000000..d93d07ee5 --- /dev/null +++ b/acceptance/testdata/repo/repo-deploy-key.txtar @@ -0,0 +1,31 @@ +# 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 + +# 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 + +# 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 + +# Get the deploy key id +exec gh repo deploy-key list --json=title,id --jq='.[].title="myTitle" | .[].id' +stdout2env DEPLOY_KEY_ID + +# Delete the deploy key +exec gh repo deploy-key delete $DEPLOY_KEY_ID + +# Ensure the deploy key was deleted +exec gh repo deploy-key list --json=id --jq='.[].id' +! stdout $DEPLOY_KEY_ID + +-- deployKey.pub -- +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAZmdeRNskfpvYL5YHB/YJaW8hTEXpnvPMkx5Ri+YwUr myTitle diff --git a/acceptance/testdata/repo/repo-edit.txtar b/acceptance/testdata/repo/repo-edit.txtar new file mode 100644 index 000000000..00d3cdd2c --- /dev/null +++ b/acceptance/testdata/repo/repo-edit.txtar @@ -0,0 +1,16 @@ +# 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 + +# Check that the repo description is empty +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json description --jq '.description' +! stdout '.' + +# Edit the repo description +exec gh repo edit $ORG/$SCRIPT_NAME-$RANDOM_STRING --description 'newDescription' + +# Check that the repo description is updated +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING --json description --jq '.description' +stdout 'newDescription' diff --git a/acceptance/testdata/repo/repo-fork-sync.txtar b/acceptance/testdata/repo/repo-fork-sync.txtar new file mode 100644 index 000000000..6ed7b94e1 --- /dev/null +++ b/acceptance/testdata/repo/repo-fork-sync.txtar @@ -0,0 +1,42 @@ +# 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 + +# Fork and clone the repo +exec gh repo fork $ORG/$SCRIPT_NAME-$RANDOM_STRING --org $ORG --fork-name $SCRIPT_NAME-$RANDOM_STRING-fork --clone + +# Defer fork cleanup +defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING-fork --yes + +# Sleep so that the BE has time to sync +sleep 5 + +# Check that the repo was forked +exec gh repo view $ORG/$SCRIPT_NAME-$RANDOM_STRING-fork --json='isFork' --jq='.isFork' +stdout 'true' + +# Modify original repo +cd $SCRIPT_NAME-$RANDOM_STRING +mv ../asset.txt asset.txt +exec git add . +exec git commit -m 'Add asset.txt' +exec git push + +# Checkout the forked repo and ensure asset.txt is not present +cd ../$SCRIPT_NAME-$RANDOM_STRING-fork +exec git checkout main +! exists asset.txt + +# Sync the forked repo with the original repo +exec gh repo sync + +# Check that asset.txt now exists in the fork +exists asset.txt + +-- asset.txt -- +Hello, world! diff --git a/acceptance/testdata/repo/repo-list-rename.txtar b/acceptance/testdata/repo/repo-list-rename.txtar new file mode 100644 index 000000000..7f3ff1281 --- /dev/null +++ b/acceptance/testdata/repo/repo-list-rename.txtar @@ -0,0 +1,16 @@ +# Create a repository with a file so it has a default branch +exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private + +# List the repos and check for the new repo +exec gh repo list $ORG --json=name --jq='.[].name' +stdout $SCRIPT_NAME-$RANDOM_STRING + +# 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-set-default.txtar b/acceptance/testdata/repo/repo-set-default.txtar new file mode 100644 index 000000000..4f7fa3273 --- /dev/null +++ b/acceptance/testdata/repo/repo-set-default.txtar @@ -0,0 +1,17 @@ +# 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 + +# Ensure that no default is set +cd $SCRIPT_NAME-$RANDOM_STRING +exec gh repo set-default --view +stderr 'no default repository has been set; use `gh repo set-default` to select one' + +# Set the default +exec gh repo set-default $ORG/$SCRIPT_NAME-$RANDOM_STRING + +# Check that the default is set +exec gh repo set-default --view +stdout $ORG/$SCRIPT_NAME-$RANDOM_STRING diff --git a/acceptance/testdata/search/search-issues.txtar b/acceptance/testdata/search/search-issues.txtar new file mode 100644 index 000000000..82184f3f1 --- /dev/null +++ b/acceptance/testdata/search/search-issues.txtar @@ -0,0 +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 an issue in the repo +cd $SCRIPT_NAME-$RANDOM_STRING + +exec gh issue create --title 'Feature Request' --body $RANDOM_STRING + +# It takes some time for the issue to be created and indexed +sleep 5 + +# 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 diff --git a/acceptance/testdata/secret/secret-org.txtar b/acceptance/testdata/secret/secret-org.txtar index d6d2fd213..7d383009c 100644 --- a/acceptance/testdata/secret/secret-org.txtar +++ b/acceptance/testdata/secret/secret-org.txtar @@ -1,16 +1,85 @@ -# Prepare organization secret, GitHub Actions uppercases secret names -env2upper ORG_SECRET_NAME=$RANDOM_STRING +# 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 + +# Clone the repo +exec gh repo clone $ORG/$REPO +cd $REPO # Confirm organization secret does not exist, will fail admin:org scope missing -! exec gh api /orgs/$ORG/actions/secrets/$ORG_SECRET_NAME -stdout '"status":"404"' +exec gh secret list --org $ORG +! stdout $SECRET_NAME # Create an organization secret -exec gh secret set $ORG_SECRET_NAME --org $ORG --body 'does not matter as cannot read it once set' +exec gh secret set $SECRET_NAME --org $ORG --body 'just an organization secret' --repos $REPO # Defer organization secret cleanup -defer gh secret delete $ORG_SECRET_NAME --org $ORG +defer gh secret delete $SECRET_NAME --org $ORG -# Confirm organization secret exists +# Verify new organization secret exists exec gh secret list --org $ORG -stdout $ORG_SECRET_NAME +stdout $SECRET_NAME + +# Commit workflow file creating dispatchable workflow able to verify secret matches +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 + +# 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: + ORG_SECRET: ${{ secrets.$SECRET_NAME }} + run: | + if [[ "$ORG_SECRET" == "just an organization 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-repo-env.txtar b/acceptance/testdata/secret/secret-repo-env.txtar index b5236ea92..a9a2c7353 100644 --- a/acceptance/testdata/secret/secret-repo-env.txtar +++ b/acceptance/testdata/secret/secret-repo-env.txtar @@ -1,8 +1,10 @@ -# Force GitHub CLI to treat testscript as TTY -env GH_FORCE_TTY=80 +# Setup environment variables used for testscript env REPO=$SCRIPT_NAME-$RANDOM_STRING -# Create a repository where the secret will be registered +# 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 @@ -13,17 +15,66 @@ exec gh repo clone $ORG/$REPO cd $REPO # Create a repository environment, will fail if organization does not have environment support -exec gh api /repos/$ORG/$REPO/environments/testscripts -X PUT --jq '.name' -stdout 'testscripts' - -# Verify no repository environment secrets exist -exec gh secret list --env testscripts -stderr 'no secrets found' +exec gh api /repos/$ORG/$REPO/environments/testscripts -X PUT # Create a repository environment secret -exec gh secret set TESTSCRIPTS_ENV --env testscripts --body 'does not matter as cannot read it once set' -stdout 'Set Actions secret TESTSCRIPTS_ENV for' +exec gh secret set TESTSCRIPTS_ENV --env testscripts --body 'just a repository environment secret' # Verify new repository secret exists exec gh secret list --env testscripts -stdout 'TESTSCRIPTS_ENV\s+less than a minute ago' +stdout 'TESTSCRIPTS_ENV' + +# 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 + environment: testscripts + steps: + - name: Assert secret value matches + env: + TESTSCRIPTS_ENV: ${{ secrets.TESTSCRIPTS_ENV }} + run: | + if [[ "$TESTSCRIPTS_ENV" == "just a repository environment 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-repo.txtar b/acceptance/testdata/secret/secret-repo.txtar index f69a06e57..ed336626f 100644 --- a/acceptance/testdata/secret/secret-repo.txtar +++ b/acceptance/testdata/secret/secret-repo.txtar @@ -1,8 +1,10 @@ -# Force GitHub CLI to treat testscript as TTY -env GH_FORCE_TTY=80 +# Setup environment variables used for testscript env REPO=$SCRIPT_NAME-$RANDOM_STRING -# Create a repository where the secret will be registered +# 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 @@ -12,14 +14,63 @@ defer gh repo delete --yes $ORG/$REPO exec gh repo clone $ORG/$REPO cd $REPO -# Verify no repository secrets exist -exec gh secret list -stderr 'no secrets found' - # Create a repository secret -exec gh secret set TESTSCRIPTS --body 'does not matter as cannot read it once set' -stdout 'Set Actions secret TESTSCRIPTS for' +exec gh secret set TESTSCRIPTS --body 'just a repository secret' # Verify new repository secret exists exec gh secret list -stdout 'TESTSCRIPTS\s+less than a minute ago' +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/workflow/cache-list-delete.txtar b/acceptance/testdata/workflow/cache-list-delete.txtar new file mode 100644 index 000000000..6a99f4bc2 --- /dev/null +++ b/acceptance/testdata/workflow/cache-list-delete.txtar @@ -0,0 +1,69 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 +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 + +# List the cache +exec gh cache list +stdout 'Linux-values' + +# Delete the cache +exec gh cache delete 'Linux-values' + +-- workflow.yml -- +name: Test Workflow Name + +on: workflow_dispatch + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Cache values + id: cache-values + uses: actions/cache@v4 + with: + path: values.txt + key: ${{ runner.os }}-values + + - name: Generate values file + if: steps.cache-values.outputs.cache-hit != 'true' + run: echo "values" > values.txt diff --git a/acceptance/testdata/workflow/run-cancel.txtar b/acceptance/testdata/workflow/run-cancel.txtar new file mode 100644 index 000000000..08d8d519a --- /dev/null +++ b/acceptance/testdata/workflow/run-cancel.txtar @@ -0,0 +1,73 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 cancel +exec gh run list --json databaseId --jq '.[0].databaseId' +stdout2env RUN_ID + +# 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' + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # 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 diff --git a/acceptance/testdata/workflow/run-delete.txtar b/acceptance/testdata/workflow/run-delete.txtar new file mode 100644 index 000000000..72d098740 --- /dev/null +++ b/acceptance/testdata/workflow/run-delete.txtar @@ -0,0 +1,74 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 + +# Delete the workflow run +exec gh run delete $RUN_ID +stdout '✓ Request to delete workflow submitted.' + +# It takes some time for a workflow run to be deleted +sleep 5 + +# Check the workflow run is cancelled, which is implied by an empty list +exec gh run list +stdout '' + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! diff --git a/acceptance/testdata/workflow/run-download.txtar b/acceptance/testdata/workflow/run-download.txtar new file mode 100644 index 000000000..653fdbef5 --- /dev/null +++ b/acceptance/testdata/workflow/run-download.txtar @@ -0,0 +1,70 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 +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 + +# Download the artifact +exec gh run download $RUN_ID + +# Check if we downloaded the artifact +exists ./my-artifact/world.txt + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - run: echo hello > world.txt + - uses: actions/upload-artifact@v4 + with: + name: my-artifact + path: world.txt diff --git a/acceptance/testdata/workflow/run-rerun.txtar b/acceptance/testdata/workflow/run-rerun.txtar new file mode 100644 index 000000000..446aabbc4 --- /dev/null +++ b/acceptance/testdata/workflow/run-rerun.txtar @@ -0,0 +1,72 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 rerun +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 + +# Rerun the workflow run +exec gh run rerun $RUN_ID + +# It takes some time for a workflow run to register +sleep 10 + +# Wait for workflow to complete +exec gh run watch $RUN_ID --exit-status + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! diff --git a/acceptance/testdata/workflow/run-view.txtar b/acceptance/testdata/workflow/run-view.txtar new file mode 100644 index 000000000..25f12c3a5 --- /dev/null +++ b/acceptance/testdata/workflow/run-view.txtar @@ -0,0 +1,66 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 view +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 + +# View the workflow run +exec gh run view $RUN_ID + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! diff --git a/acceptance/testdata/workflow/workflow-enable-disable.txtar b/acceptance/testdata/workflow/workflow-enable-disable.txtar new file mode 100644 index 000000000..f0b58116f --- /dev/null +++ b/acceptance/testdata/workflow/workflow-enable-disable.txtar @@ -0,0 +1,66 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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' + +# disable the workflow +exec gh workflow disable 'Test Workflow Name' + +# Check that the listing shows it is disabled +exec gh workflow list --all +stdout 'Test\s+Workflow\s+Name\s+disabled_manually\s+\d+' + +# enable the workflow +exec gh workflow enable 'Test Workflow Name' + +# Check the workflow is indeed enabled +exec gh workflow list +stdout 'Test\s+Workflow\s+Name\s+active\s+\d+' + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # 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: echo Hello, world! diff --git a/acceptance/testdata/workflow/workflow-list.txtar b/acceptance/testdata/workflow/workflow-list.txtar new file mode 100644 index 000000000..ad0d87c88 --- /dev/null +++ b/acceptance/testdata/workflow/workflow-list.txtar @@ -0,0 +1,52 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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' + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # 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: echo Hello, world! diff --git a/acceptance/testdata/workflow/workflow-run.txtar b/acceptance/testdata/workflow/workflow-run.txtar new file mode 100644 index 000000000..010189c01 --- /dev/null +++ b/acceptance/testdata/workflow/workflow-run.txtar @@ -0,0 +1,62 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 + +# Check the workflow run exists +exec gh run list +stdout 'Test Workflow Name' + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # 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: echo Hello, world! diff --git a/acceptance/testdata/workflow/workflow-view.txtar b/acceptance/testdata/workflow/workflow-view.txtar new file mode 100644 index 000000000..d3bc3d252 --- /dev/null +++ b/acceptance/testdata/workflow/workflow-view.txtar @@ -0,0 +1,52 @@ +# 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 + +# commit the workflow file +cd $SCRIPT_NAME-$RANDOM_STRING +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 view 'Test Workflow Name' +stdout 'Test Workflow Name - workflow.yml' + +-- workflow.yml -- +# This is a basic workflow to help you get started with Actions + +name: Test Workflow Name + +# Controls when the workflow will run +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # 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: echo Hello, world! diff --git a/go.mod b/go.mod index 874885da6..76a7818f8 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 github.com/creack/pty v1.1.23 github.com/distribution/reference v0.5.0 - github.com/gabriel-vasile/mimetype v1.4.5 + github.com/gabriel-vasile/mimetype v1.4.6 github.com/gdamore/tcell/v2 v2.5.4 github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.20.2 @@ -43,10 +43,10 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/zalando/go-keyring v0.2.5 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.28.0 golang.org/x/sync v0.8.0 - golang.org/x/term v0.24.0 - golang.org/x/text v0.18.0 + golang.org/x/term v0.25.0 + golang.org/x/text v0.19.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/h2non/gock.v1 v1.1.2 @@ -159,7 +159,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect diff --git a/go.sum b/go.sum index 28e339f9e..848747498 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= -github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k= @@ -486,8 +486,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -496,8 +496,8 @@ golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -519,15 +519,15 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 2ffb11a9d..94ba88208 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -16,12 +16,19 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, tenant, owner, return errors.New("no attestations proccessing results") } + var atLeastOneVerified bool for _, attestation := range results { if err := verifyCertExtensions(attestation, tenant, owner, repo, issuer); err != nil { return err } + atLeastOneVerified = true + } + + if atLeastOneVerified { + return nil + } else { + return ErrNoAttestationsVerified } - return nil } func verifyCertExtensions(attestation *AttestationProcessingResult, tenant, owner, repo, issuer string) error { diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index ca4015db2..e237a3eb9 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "crypto/x509" + "errors" "fmt" "os" @@ -48,6 +49,8 @@ type LiveSigstoreVerifier struct { config SigstoreConfig } +var ErrNoAttestationsVerified = errors.New("no attestations were verified") + // NewLiveSigstoreVerifier creates a new LiveSigstoreVerifier struct // that is used to verify artifacts and attestations against the // Public Good, GitHub, or a custom trusted root. @@ -170,18 +173,20 @@ func getLowestCertInChain(ca *root.CertificateAuthority) (*x509.Certificate, err } func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy verify.PolicyBuilder) *SigstoreResults { - // initialize the processing results before attempting to verify + // initialize the processing apResults before attempting to verify // with multiple verifiers - results := make([]*AttestationProcessingResult, len(attestations)) + apResults := make([]*AttestationProcessingResult, len(attestations)) for i, att := range attestations { apr := &AttestationProcessingResult{ Attestation: att, } - results[i] = apr + apResults[i] = apr } + var atLeastOneVerified bool + totalAttestations := len(attestations) - for i, apr := range results { + for i, apr := range apResults { v.config.Logger.VerbosePrintf("Verifying attestation %d/%d against the configured Sigstore trust roots\n", i+1, totalAttestations) // determine which verifier should attempt verification against the bundle @@ -212,10 +217,15 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve "SUCCESS - attestation signature verified with \"%s\"\n", issuer, )) apr.VerificationResult = result + atLeastOneVerified = true } - return &SigstoreResults{ - VerifyResults: results, + if atLeastOneVerified { + return &SigstoreResults{ + VerifyResults: apResults, + } + } else { + return &SigstoreResults{Error: ErrNoAttestationsVerified} } } diff --git a/pkg/cmd/attestation/verification/sigstore_integration_test.go b/pkg/cmd/attestation/verification/sigstore_integration_test.go index 97b44581e..1d3ec2d75 100644 --- a/pkg/cmd/attestation/verification/sigstore_integration_test.go +++ b/pkg/cmd/attestation/verification/sigstore_integration_test.go @@ -85,6 +85,21 @@ func TestLiveSigstoreVerifier(t *testing.T) { require.Len(t, res.VerifyResults, 0) require.ErrorContains(t, res.Error, "unsupported bundle version") }) + + t.Run("with no attestations", func(t *testing.T) { + attestations := []*api.Attestation{} + require.Len(t, attestations, 0) + + verifier := NewLiveSigstoreVerifier(SigstoreConfig{ + Logger: io.NewTestHandler(), + TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"), + }) + + res := verifier.Verify(attestations, publicGoodPolicy(t)) + require.Len(t, res.VerifyResults, 0) + require.NotNil(t, res.Error) + }) + } func publicGoodPolicy(t *testing.T) verify.PolicyBuilder { diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index 82d4ab133..f51cde4c9 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -222,7 +222,7 @@ func createRun(opts *CreateOptions) (err error) { defer prShared.PreserveInput(opts.IO, &tb, &err)() if opts.Title == "" { - err = prShared.TitleSurvey(opts.Prompter, &tb) + err = prShared.TitleSurvey(opts.Prompter, opts.IO, &tb) if err != nil { return } diff --git a/pkg/cmd/issue/create/create_test.go b/pkg/cmd/issue/create/create_test.go index b95b5ef66..8e49700a0 100644 --- a/pkg/cmd/issue/create/create_test.go +++ b/pkg/cmd/issue/create/create_test.go @@ -611,7 +611,7 @@ func TestIssueCreate_recover(t *testing.T) { pm := &prompter.PrompterMock{} pm.InputFunc = func(p, d string) (string, error) { - if p == "Title" { + if p == "Title (required)" { return d, nil } else { return "", prompter.NoSuchPromptErr(p) @@ -736,7 +736,7 @@ func TestIssueCreate_continueInBrowser(t *testing.T) { pm := &prompter.PrompterMock{} pm.InputFunc = func(p, d string) (string, error) { - if p == "Title" { + if p == "Title (required)" { return "hello", nil } else { return "", prompter.NoSuchPromptErr(p) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 7e893395c..1242af2c5 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -379,7 +379,7 @@ func createRun(opts *CreateOptions) (err error) { } else { if !opts.TitleProvided { - err = shared.TitleSurvey(opts.Prompter, state) + err = shared.TitleSurvey(opts.Prompter, opts.IO, state) if err != nil { return } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index cba552101..81684ff00 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1210,7 +1210,7 @@ func Test_createRun(t *testing.T) { }, promptStubs: func(pm *prompter.PrompterMock) { pm.InputFunc = func(p, d string) (string, error) { - if p == "Title" { + if p == "Title (required)" { return d, nil } else { return "", prompter.NoSuchPromptErr(p) @@ -1316,7 +1316,7 @@ func Test_createRun(t *testing.T) { } pm.InputFunc = func(p, d string) (string, error) { - if p == "Title" { + if p == "Title (required)" { return d, nil } else if p == "Body" { return d, nil diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index 5b8bde0eb..ce38535d9 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -110,10 +110,17 @@ func BodySurvey(p Prompt, state *IssueMetadataState, templateContent string) err return nil } -func TitleSurvey(p Prompt, state *IssueMetadataState) error { - result, err := p.Input("Title", state.Title) - if err != nil { - return err +func TitleSurvey(p Prompt, io *iostreams.IOStreams, state *IssueMetadataState) error { + var err error + result := "" + for result == "" { + result, err = p.Input("Title (required)", state.Title) + if err != nil { + return err + } + if result == "" { + fmt.Fprintf(io.ErrOut, "%s Title cannot be blank\n", io.ColorScheme().FailureIcon()) + } } if result != state.Title { diff --git a/pkg/cmd/pr/shared/survey_test.go b/pkg/cmd/pr/shared/survey_test.go index 8df492785..d74696460 100644 --- a/pkg/cmd/pr/shared/survey_test.go +++ b/pkg/cmd/pr/shared/survey_test.go @@ -158,3 +158,44 @@ type testEditor struct { func (e testEditor) Edit(filename, text string) (string, error) { return e.edit(text) } + +func TestTitleSurvey(t *testing.T) { + tests := []struct { + name string + prompterMockInputs []string + expectedTitle string + expectStderr bool + }{ + { + name: "title provided", + prompterMockInputs: []string{"title"}, + expectedTitle: "title", + }, + { + name: "first input empty", + prompterMockInputs: []string{"", "title"}, + expectedTitle: "title", + expectStderr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, stderr := iostreams.Test() + pm := prompter.NewMockPrompter(t) + for _, input := range tt.prompterMockInputs { + pm.RegisterInput("Title (required)", func(string, string) (string, error) { + return input, nil + }) + } + + state := &IssueMetadataState{} + err := TitleSurvey(pm, io, state) + + assert.NoError(t, err) + assert.Equal(t, tt.expectedTitle, state.Title) + if tt.expectStderr { + assert.Equal(t, "X Title cannot be blank\n", stderr.String()) + } + }) + } +} diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index e5f09dec9..90ab5a7fe 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -180,16 +180,22 @@ func (r *Run) ExportData(fields []string) map[string]interface{} { for _, j := range r.Jobs { steps := make([]interface{}, 0, len(j.Steps)) for _, s := range j.Steps { + var stepCompletedAt time.Time + if !s.CompletedAt.IsZero() { + stepCompletedAt = s.CompletedAt + } steps = append(steps, map[string]interface{}{ - "name": s.Name, - "status": s.Status, - "conclusion": s.Conclusion, - "number": s.Number, + "name": s.Name, + "status": s.Status, + "conclusion": s.Conclusion, + "number": s.Number, + "startedAt": s.StartedAt, + "completedAt": stepCompletedAt, }) } - var completedAt time.Time + var jobCompletedAt time.Time if !j.CompletedAt.IsZero() { - completedAt = j.CompletedAt + jobCompletedAt = j.CompletedAt } jobs = append(jobs, map[string]interface{}{ "databaseId": j.ID, @@ -198,7 +204,7 @@ func (r *Run) ExportData(fields []string) map[string]interface{} { "name": j.Name, "steps": steps, "startedAt": j.StartedAt, - "completedAt": completedAt, + "completedAt": jobCompletedAt, "url": j.URL, }) } @@ -225,11 +231,13 @@ type Job struct { } type Step struct { - Name string - Status Status - Conclusion Conclusion - Number int - Log *zip.File + Name string + Status Status + Conclusion Conclusion + Number int + StartedAt time.Time `json:"started_at"` + CompletedAt time.Time `json:"completed_at"` + Log *zip.File } type Steps []Step diff --git a/pkg/cmd/run/shared/shared_test.go b/pkg/cmd/run/shared/shared_test.go index 15663cd0a..752ba2fc4 100644 --- a/pkg/cmd/run/shared/shared_test.go +++ b/pkg/cmd/run/shared/shared_test.go @@ -110,8 +110,12 @@ func TestRun_Duration(t *testing.T) { func TestRunExportData(t *testing.T) { oldestStartedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:20:13Z") + oldestStepStartedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:20:15Z") + oldestStepCompletedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:21:10Z") oldestCompletedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:21:16Z") newestStartedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:20:55Z") + newestStepStartedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:21:01Z") + newestStepCompletedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:23:10Z") newestCompletedAt, _ := time.Parse(time.RFC3339, "2022-07-20T11:23:16Z") tests := []struct { @@ -132,10 +136,12 @@ func TestRunExportData(t *testing.T) { Name: "macos", Steps: []Step{ { - Name: "Checkout", - Status: "completed", - Conclusion: "success", - Number: 1, + Name: "Checkout", + Status: "completed", + Conclusion: "success", + Number: 1, + StartedAt: oldestStepStartedAt, + CompletedAt: oldestStepCompletedAt, }, }, StartedAt: oldestStartedAt, @@ -144,7 +150,7 @@ func TestRunExportData(t *testing.T) { }, }, }, - output: `{"jobs":[{"completedAt":"2022-07-20T11:21:16Z","conclusion":"success","databaseId":123456,"name":"macos","startedAt":"2022-07-20T11:20:13Z","status":"completed","steps":[{"conclusion":"success","name":"Checkout","number":1,"status":"completed"}],"url":"https://example.com/OWNER/REPO/actions/runs/123456"}]}`, + output: `{"jobs":[{"completedAt":"2022-07-20T11:21:16Z","conclusion":"success","databaseId":123456,"name":"macos","startedAt":"2022-07-20T11:20:13Z","status":"completed","steps":[{"completedAt":"2022-07-20T11:21:10Z","conclusion":"success","name":"Checkout","number":1,"startedAt":"2022-07-20T11:20:15Z","status":"completed"}],"url":"https://example.com/OWNER/REPO/actions/runs/123456"}]}`, }, { name: "exports workflow run's multiple jobs", @@ -158,10 +164,12 @@ func TestRunExportData(t *testing.T) { Name: "macos", Steps: []Step{ { - Name: "Checkout", - Status: "completed", - Conclusion: "success", - Number: 1, + Name: "Checkout", + Status: "completed", + Conclusion: "success", + Number: 1, + StartedAt: oldestStepStartedAt, + CompletedAt: oldestStepCompletedAt, }, }, StartedAt: oldestStartedAt, @@ -175,10 +183,12 @@ func TestRunExportData(t *testing.T) { Name: "windows", Steps: []Step{ { - Name: "Checkout", - Status: "completed", - Conclusion: "error", - Number: 2, + Name: "Checkout", + Status: "completed", + Conclusion: "error", + Number: 2, + StartedAt: newestStepStartedAt, + CompletedAt: newestStepCompletedAt, }, }, StartedAt: newestStartedAt, @@ -187,7 +197,7 @@ func TestRunExportData(t *testing.T) { }, }, }, - output: `{"jobs":[{"completedAt":"2022-07-20T11:21:16Z","conclusion":"success","databaseId":123456,"name":"macos","startedAt":"2022-07-20T11:20:13Z","status":"completed","steps":[{"conclusion":"success","name":"Checkout","number":1,"status":"completed"}],"url":"https://example.com/OWNER/REPO/actions/runs/123456"},{"completedAt":"2022-07-20T11:23:16Z","conclusion":"error","databaseId":234567,"name":"windows","startedAt":"2022-07-20T11:20:55Z","status":"completed","steps":[{"conclusion":"error","name":"Checkout","number":2,"status":"completed"}],"url":"https://example.com/OWNER/REPO/actions/runs/234567"}]}`, + output: `{"jobs":[{"completedAt":"2022-07-20T11:21:16Z","conclusion":"success","databaseId":123456,"name":"macos","startedAt":"2022-07-20T11:20:13Z","status":"completed","steps":[{"completedAt":"2022-07-20T11:21:10Z","conclusion":"success","name":"Checkout","number":1,"startedAt":"2022-07-20T11:20:15Z","status":"completed"}],"url":"https://example.com/OWNER/REPO/actions/runs/123456"},{"completedAt":"2022-07-20T11:23:16Z","conclusion":"error","databaseId":234567,"name":"windows","startedAt":"2022-07-20T11:20:55Z","status":"completed","steps":[{"completedAt":"2022-07-20T11:23:10Z","conclusion":"error","name":"Checkout","number":2,"startedAt":"2022-07-20T11:21:01Z","status":"completed"}],"url":"https://example.com/OWNER/REPO/actions/runs/234567"}]}`, }, { name: "exports workflow run with attempt count",