diff --git a/acceptance/README.md b/acceptance/README.md index 732369bc0..82d7e1ab6 100644 --- a/acceptance/README.md +++ b/acceptance/README.md @@ -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 e3f439d36..58e27f506 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -27,13 +27,13 @@ func TestMain(m *testing.M) { })) } -func TestPullRequests(t *testing.T) { +func TestAPI(t *testing.T) { var tsEnv testScriptEnv if err := tsEnv.fromEnv(); err != nil { t.Fatal(err) } - testscript.Run(t, testScriptParamsFor(tsEnv, "pr")) + testscript.Run(t, testScriptParamsFor(tsEnv, "api")) } func TestIssues(t *testing.T) { @@ -45,6 +45,24 @@ func TestIssues(t *testing.T) { testscript.Run(t, testScriptParamsFor(tsEnv, "pr")) } +func TestPullRequests(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "pr")) +} + +func TestReleases(t *testing.T) { + var tsEnv testScriptEnv + if err := tsEnv.fromEnv(); err != nil { + t.Fatal(err) + } + + testscript.Run(t, testScriptParamsFor(tsEnv, "release")) +} + func TestSecrets(t *testing.T) { var tsEnv testScriptEnv if err := tsEnv.fromEnv(); err != nil { @@ -63,25 +81,6 @@ func TestWorkflows(t *testing.T) { 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")) ->>>>>>> trunk -} - func testScriptParamsFor(tsEnv testScriptEnv, command string) testscript.Params { var files []string if tsEnv.script != "" { @@ -156,23 +155,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") @@ -190,6 +172,41 @@ 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) + 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 `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") diff --git a/acceptance/testdata/secret/secret-org.txtar b/acceptance/testdata/secret/secret-org.txtar index d6d2fd213..2bc6a845b 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