22package workflow
33
44import (
5+ "encoding/json"
56 "fmt"
67 "path/filepath"
78 "strings"
@@ -10,6 +11,11 @@ import (
1011const (
1112 GCSLoginAction = "google-github-actions/auth"
1213 GCSUploadAction = "google-github-actions/upload-cloud-storage"
14+
15+ VaultSecretsAction = "grafana/shared-workflows/actions/get-vault-secrets"
16+ ArgoWorkflowAction = "grafana/shared-workflows/actions/trigger-argo-workflow"
17+
18+ GitHubAppTokenAction = "actions/create-github-app-token"
1319)
1420
1521// CopyMockFilesStep returns a Step that copies mock files from a source folder to a destination folder.
@@ -19,7 +25,6 @@ const (
1925// You can use GitHub Actions expressions in destFolder, e.g., "${{ github.workspace }}/plugins/my-plugin/dist".
2026func CopyMockFilesStep (sourceFolder string , destFolder string ) Step {
2127 return Step {
22- Name : "Copy mock files" ,
2328 Run : Commands {
2429 "set -x" ,
2530 "mkdir -p " + destFolder ,
@@ -33,10 +38,9 @@ func CopyMockFilesStep(sourceFolder string, destFolder string) Step {
3338
3439// NoOpStep returns a Step that does nothing (no-op) for testing purposes.
3540// The step simply echoes a message indicating it is a no-op step.
36- func NoOpStep (id string ) Step {
41+ func NoOpStep (originalStep Step ) Step {
3742 return Step {
38- Name : id + " (no-opp'ed for testing)" ,
39- ID : id ,
43+ Name : originalStep .nameOrID () + " (no-op'd for testing)" ,
4044 Run : "echo 'noop-ed step for testing'" ,
4145 Shell : "bash" ,
4246 }
@@ -90,3 +94,159 @@ func MockGCSUploadStep(originalStep Step) (Step, error) {
9094func LocalMockdataPath (parts ... string ) string {
9195 return filepath .Join ("tests" , "act" , "mockdata" , filepath .Join (parts ... ))
9296}
97+
98+ // VaultSecrets allows defining the secret values that the mocked get-vault-secrets step should return.
99+ // The keys in the maps must match the secret reference used in the workflow step inputs
100+ // (the value on the right side of the equals sign).
101+ //
102+ // Example:
103+ //
104+ // If the workflow has:
105+ // common_secrets: |
106+ // MY_SECRET=secret/path:key
107+ //
108+ // Then the VaultSecrets should have:
109+ // CommonSecrets: map[string]string{
110+ // "secret/path:key": "mock-value",
111+ // }
112+ type VaultSecrets struct {
113+ // CommonSecrets contains secrets that are referenced in the 'common_secrets' input.
114+ CommonSecrets map [string ]string
115+ // RepoSecrets contains secrets that are referenced in the 'repo_secrets' input.
116+ RepoSecrets map [string ]string
117+
118+ // DefaultValue is the default value to use for secrets that are not defined in CommonSecrets or RepoSecrets.
119+ // If nil, the step will fail to be constructed if a secret is not defined.
120+ // If not nil, this value will be used for secrets that are not defined.
121+ DefaultValue * string
122+ }
123+
124+ // MockVaultSecretsStep returns a Step that mocks the grafana/shared-workflows/actions/get-vault-secrets action.
125+ // Instead of actually fetching secrets from Vault, it outputs the provided secrets in the expected format.
126+ // The originalStep parameter is the original Vault secrets step to be mocked.
127+ // The original step must use the `grafana/shared-workflows/actions/get-vault-secrets` action.
128+ // If those conditions are not met, an error is returned.
129+ //
130+ // The mocked step mimics the behavior of the original step, but instead of fetching secrets from Vault,
131+ // it outputs the provided secrets in the expected format.
132+ // If export_env is true, the secrets are exported as environment variables.
133+ // If export_env is false, the secrets are exported as a JSON object.
134+ //
135+ // If a secret is not found in the provided VaultSecrets:
136+ // - If DefaultValue is nil, an error is returned.
137+ // - If DefaultValue is not nil, the DefaultValue is used.
138+ func MockVaultSecretsStep (originalStep Step , secrets VaultSecrets ) (Step , error ) {
139+ // Make sure the original step is indeed a Vault secrets step
140+ if ! strings .HasPrefix (originalStep .Uses , VaultSecretsAction ) {
141+ return Step {}, fmt .Errorf ("cannot mock vault secrets for a step that uses %q action, must be %q" , originalStep .Uses , VaultSecretsAction )
142+ }
143+
144+ // Extract original inputs with safe type assertions and defaults
145+ var commonSecretsInput , repoSecretsInput string
146+ if v , ok := originalStep .With ["common_secrets" ].(string ); ok {
147+ commonSecretsInput = v
148+ }
149+ if v , ok := originalStep .With ["repo_secrets" ].(string ); ok {
150+ repoSecretsInput = v
151+ }
152+ exportEnvInput := true
153+ if v , ok := originalStep .With ["export_env" ].(bool ); ok {
154+ exportEnvInput = v
155+ }
156+ output := map [string ]string {}
157+ for _ , s := range []struct {
158+ input string
159+ secrets map [string ]string
160+ }{
161+ {commonSecretsInput , secrets .CommonSecrets },
162+ {repoSecretsInput , secrets .RepoSecrets },
163+ } {
164+ for i , line := range strings .Split (s .input , "\n " ) {
165+ line = strings .TrimSpace (line )
166+ if line == "" {
167+ continue
168+ }
169+ parts := strings .Split (line , "=" )
170+ if len (parts ) != 2 {
171+ return Step {}, fmt .Errorf ("invalid input, not enough parts on line %d: %s" , i , line )
172+ }
173+ outputName := strings .TrimSpace (parts [0 ])
174+ secretReference := strings .TrimSpace (parts [1 ])
175+ secretValue , ok := s .secrets [secretReference ]
176+ if ! ok && secrets .DefaultValue == nil {
177+ return Step {}, fmt .Errorf ("secret reference %q not found in provided fake secrets %+v" , secretReference , secrets )
178+ }
179+ if ! ok {
180+ secretValue = * secrets .DefaultValue
181+ }
182+ output [outputName ] = secretValue
183+ }
184+ }
185+
186+ step := Step {
187+ Env : map [string ]string {},
188+ Shell : "bash" ,
189+ }
190+ var stepCommands Commands
191+ if exportEnvInput {
192+ // Workflow-level env vars output
193+ for k , v := range output {
194+ stepCommands = append (stepCommands , fmt .Sprintf (`echo "%s=%s" >> "$GITHUB_ENV"` , k , v ))
195+ }
196+ } else {
197+ // JSON output
198+ secretsJSON , err := json .Marshal (output )
199+ if err != nil {
200+ return Step {}, fmt .Errorf ("marshal vault secrets to json: %w" , err )
201+ }
202+ stepCommands = append (stepCommands , `echo "secrets=${SECRETS_JSON}" >> "$GITHUB_OUTPUT"` )
203+ step .Env = map [string ]string {"SECRETS_JSON" : string (secretsJSON )}
204+ }
205+ step .Run = stepCommands .String ()
206+ return step , nil
207+ }
208+
209+ // MockArgoWorkflowStep returns a Step that mocks the grafana/shared-workflows/actions/trigger-argo-workflow action.
210+ // Instead of actually triggering an Argo Workflow, it outputs a mock URI for the workflow.
211+ // The originalStep parameter is the original Argo Workflow trigger step to be mocked.
212+ // The original step must use the `grafana/shared-workflows/actions/trigger-argo-workflow` action.
213+ // If those conditions are not met, an error is returned.
214+ //
215+ // The mocked step outputs the `uri` output expected by subsequent steps.
216+ func MockArgoWorkflowStep (originalStep Step ) (Step , error ) {
217+ // Make sure the original step is indeed an Argo Workflow trigger step
218+ if ! strings .HasPrefix (originalStep .Uses , ArgoWorkflowAction ) {
219+ return Step {}, fmt .Errorf ("cannot mock argo workflow for a step that uses %q action, must be %q" , originalStep .Uses , ArgoWorkflowAction )
220+ }
221+
222+ return Step {
223+ Run : Commands {
224+ `echo "Mocking Argo Workflow trigger step"` ,
225+ `echo "uri=https://mock-argo-workflows.example.com/workflows/grafana-plugins-cd/mock-workflow-id" >> "$GITHUB_OUTPUT"` ,
226+ }.String (),
227+ Shell : "bash" ,
228+ }, nil
229+ }
230+
231+ // MockGitHubAppTokenStep returns a Step that mocks the actions/create-github-app-token action.
232+ // Instead of actually creating a GitHub app token, it outputs a mock token.
233+ // The originalStep parameter is the original GitHub app token step to be mocked.
234+ // The original step must use the `actions/create-github-app-token` action.
235+ // If those conditions are not met, an error is returned.
236+ //
237+ // The mocked step outputs the `token` output expected by subsequent steps.
238+ func MockGitHubAppTokenStep (originalStep Step , token string ) (Step , error ) {
239+ if ! strings .HasPrefix (originalStep .Uses , GitHubAppTokenAction ) {
240+ return Step {}, fmt .Errorf ("cannot mock github app token for a step that uses %q action, must be %q" , originalStep .Uses , GitHubAppTokenAction )
241+ }
242+ return Step {
243+ Run : Commands {
244+ `echo "Mocking GitHub app token step"` ,
245+ `echo "token=${MOCK_TOKEN}" >> "$GITHUB_OUTPUT"` ,
246+ }.String (),
247+ Shell : "bash" ,
248+ Env : map [string ]string {
249+ "MOCK_TOKEN" : token ,
250+ },
251+ }, nil
252+ }
0 commit comments