diff --git a/.github/actions/deploy-client-stack/action.yml b/.github/actions/deploy-client-stack/action.yml index b435f2dd..c44bb30c 100644 --- a/.github/actions/deploy-client-stack/action.yml +++ b/.github/actions/deploy-client-stack/action.yml @@ -54,6 +54,10 @@ inputs: description: 'Skip Pulumi refresh operation before deployment' required: false default: 'false' + preview-only: + description: 'Run pulumi preview (dry-run) instead of pulumi up. Outputs the diff and exits without applying any changes.' + required: false + default: 'false' outputs: version: @@ -66,6 +70,8 @@ outputs: description: 'Deployment duration' status: description: 'Deployment status (success/failure)' + preview-mode: + description: 'Whether the run executed in preview/dry-run mode (true/false)' runs: using: 'docker' @@ -86,3 +92,4 @@ runs: COMMIT_AUTHOR: ${{ inputs.commit-author }} COMMIT_MESSAGE: ${{ inputs.commit-message }} SKIP_REFRESH: ${{ inputs.skip-refresh }} + PREVIEW_ONLY: ${{ inputs.preview-only }} diff --git a/pkg/githubactions/actions/executor.go b/pkg/githubactions/actions/executor.go index 731c1d3a..265fd280 100644 --- a/pkg/githubactions/actions/executor.go +++ b/pkg/githubactions/actions/executor.go @@ -36,10 +36,18 @@ func NewExecutor(prov provisioner.Provisioner, log logger.Logger, gitRepo git.Re return executor } -// isPreviewMode checks if the executor should run in preview/dry-run mode +// isPreviewMode checks if the executor should run in preview/dry-run mode. +// Returns true when any of the following env vars are set to "true": +// - PREVIEW_ONLY — explicit opt-in via the deploy-client-stack action's `preview-only` input +// - SC_PREVIEW — legacy SC alias +// - SC_DRY_RUN — legacy SC alias +// - DRY_RUN — generic alias +// - SC_DEPLOY_PREVIEW — legacy SC alias +// +// Also returns true when GITHUB_EVENT_NAME is "pull_request" (auto-preview on PR builds). func (e *Executor) isPreviewMode() bool { - // Check various environment variables that indicate preview mode - return os.Getenv("SC_PREVIEW") == "true" || + return os.Getenv("PREVIEW_ONLY") == "true" || + os.Getenv("SC_PREVIEW") == "true" || os.Getenv("SC_DRY_RUN") == "true" || os.Getenv("DRY_RUN") == "true" || os.Getenv("SC_DEPLOY_PREVIEW") == "true" || diff --git a/pkg/githubactions/actions/executor_test.go b/pkg/githubactions/actions/executor_test.go new file mode 100644 index 00000000..984366d5 --- /dev/null +++ b/pkg/githubactions/actions/executor_test.go @@ -0,0 +1,66 @@ +package actions + +import "testing" + +func TestExecutor_IsPreviewMode(t *testing.T) { + // Env vars that should each individually flip preview mode on. + previewEnvVars := []string{ + "PREVIEW_ONLY", + "SC_PREVIEW", + "SC_DRY_RUN", + "DRY_RUN", + "SC_DEPLOY_PREVIEW", + } + + clearEnv := func() { + for _, v := range previewEnvVars { + t.Setenv(v, "") + } + t.Setenv("GITHUB_EVENT_NAME", "") + } + + executor := &Executor{} + + t.Run("all unset returns false", func(t *testing.T) { + clearEnv() + if executor.isPreviewMode() { + t.Fatal("isPreviewMode() with no env set should return false") + } + }) + + for _, name := range previewEnvVars { + t.Run(name+"=true triggers preview", func(t *testing.T) { + clearEnv() + t.Setenv(name, "true") + if !executor.isPreviewMode() { + t.Fatalf("isPreviewMode() with %s=true should return true", name) + } + }) + + t.Run(name+" non-true value does not trigger preview", func(t *testing.T) { + clearEnv() + // Anything other than the literal string "true" must not trigger preview — + // guards against e.g. "false" or "1" producing a surprising mode flip. + t.Setenv(name, "1") + if executor.isPreviewMode() { + t.Fatalf("isPreviewMode() with %s=1 should return false", name) + } + }) + } + + t.Run("GITHUB_EVENT_NAME=pull_request triggers preview", func(t *testing.T) { + clearEnv() + t.Setenv("GITHUB_EVENT_NAME", "pull_request") + if !executor.isPreviewMode() { + t.Fatal("isPreviewMode() with GITHUB_EVENT_NAME=pull_request should return true") + } + }) + + t.Run("GITHUB_EVENT_NAME=push does not trigger preview", func(t *testing.T) { + clearEnv() + t.Setenv("GITHUB_EVENT_NAME", "push") + if executor.isPreviewMode() { + t.Fatal("isPreviewMode() with GITHUB_EVENT_NAME=push should return false") + } + }) +}