From 12fafbd56d76954d97594104b58befda139a454f Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sun, 10 May 2026 11:23:17 +0400 Subject: [PATCH] feat(actions): add preview-only input to deploy-client-stack action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an explicit `preview-only` input to the deploy-client-stack action so consumers can run `pulumi preview` (dry-run) from CI without applying any changes. Maps to a new PREVIEW_ONLY env var that the github-actions binary's existing isPreviewMode() check now recognises. Use case: validating in-flight SC API changes against a real consumer stack before merging the SC change. Until now, the only ways to dry-run a deploy were via the legacy SC env vars (SC_PREVIEW, SC_DRY_RUN, etc.) or by running on a pull_request trigger. Neither is convenient when the deploy is normally invoked from a workflow_dispatch. Wiring: - New action input `preview-only: 'false'` (default keeps current behavior) - Sets `PREVIEW_ONLY` env var on the docker container - isPreviewMode() in pkg/githubactions/actions/executor.go now also returns true when PREVIEW_ONLY=true - New `preview-mode` action output exposes the resolved mode (already emitted by the binary, just needed declaring in action.yml) Tests: - TestExecutor_IsPreviewMode covers all preview env triggers individually, asserts non-"true" values do not flip the mode, and verifies the pull_request auto-preview path When isPreviewMode() returns true, executeDeploy() routes to provisioner.Preview() instead of provisioner.Deploy() — that branch was already present in operation_executor.go, no changes needed there. Signed-off-by: Dmitrii Creed --- .../actions/deploy-client-stack/action.yml | 7 ++ pkg/githubactions/actions/executor.go | 14 +++- pkg/githubactions/actions/executor_test.go | 66 +++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 pkg/githubactions/actions/executor_test.go 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") + } + }) +}