Skip to content

Commit

Permalink
schema/plan/approve: distinct action for approve the plan
Browse files Browse the repository at this point in the history
  • Loading branch information
giautm committed Sep 18, 2024
1 parent c58bc5e commit e1da448
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 48 deletions.
105 changes: 69 additions & 36 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,11 @@ const (
CmdMigrateDown = "migrate/down"
CmdMigrateTest = "migrate/test"
// Declarative workflow Commands
CmdSchemaPush = "schema/push"
CmdSchemaTest = "schema/test"
CmdSchemaPlan = "schema/plan"
CmdSchemaApply = "schema/apply"
CmdSchemaPush = "schema/push"
CmdSchemaTest = "schema/test"
CmdSchemaPlan = "schema/plan"
CmdSchemaPlanApprove = "schema/plan/approve"
CmdSchemaApply = "schema/apply"
)

// Run runs the action based on the command name.
Expand Down Expand Up @@ -183,6 +184,8 @@ func (a *Actions) Run(ctx context.Context, act string) error {
return a.SchemaTest(ctx)
case CmdSchemaPlan:
return a.SchemaPlan(ctx)
case CmdSchemaPlanApprove:
return a.SchemaPlanApprove(ctx)
case CmdSchemaApply:
return a.SchemaApply(ctx)
default:
Expand Down Expand Up @@ -492,8 +495,11 @@ func (a *Actions) SchemaTest(ctx context.Context) error {
// SchemaPlan runs the GitHub Action for "ariga/atlas-action/schema/plan"
func (a *Actions) SchemaPlan(ctx context.Context) error {
tc, err := a.GetTriggerContext()
if err != nil {
switch {
case err != nil:
return fmt.Errorf("unable to get the trigger context: %w", err)
case tc.PullRequest == nil:
return fmt.Errorf("the action should be run in a pull request context")
}
var plan *atlasexec.SchemaPlan
params := &atlasexec.SchemaPlanListParams{
Expand All @@ -510,36 +516,12 @@ func (a *Actions) SchemaPlan(ctx context.Context) error {
switch planFiles, err := a.Atlas.SchemaPlanList(ctx, params); {
case err != nil:
return fmt.Errorf("failed to list schema plans: %w", err)
// Multiple existing plans.
case len(planFiles) > 1:
for _, f := range planFiles {
a.Infof("Found schema plan: %s", f.URL)
}
// There are existing plans.
return fmt.Errorf("found multiple schema plans, please approve or delete the existing plans")
// Branch context with no plan
case tc.PullRequest == nil && len(planFiles) == 0:
a.Infof("No schema plan found")
return nil
// Branch context with pending plan
case tc.PullRequest == nil && len(planFiles) == 1:
result, err := a.Atlas.SchemaPlanApprove(ctx, &atlasexec.SchemaPlanApproveParams{
ConfigURL: params.ConfigURL,
Env: params.Env,
Vars: params.Vars,
URL: planFiles[0].URL,
})
if err != nil {
return fmt.Errorf("failed to approve the schema plan: %w", err)
}
// Successfully approved the plan.
a.Infof("Schema plan approved successfully: %s", result.Link)
a.SetOutput("link", result.Link)
a.SetOutput("plan", result.URL)
a.SetOutput("status", result.Status)
return nil
// PR context and existing plan
case tc.PullRequest != nil && len(planFiles) == 1:
case len(planFiles) == 1:
a.Infof("Schema plan already exists, linting the plan %q", planFiles[0].Name)
plan, err = a.Atlas.SchemaPlanLint(ctx, &atlasexec.SchemaPlanLintParams{
ConfigURL: params.ConfigURL,
Expand All @@ -555,12 +537,7 @@ func (a *Actions) SchemaPlan(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to get the schema plan: %w", err)
}
// FIXME(giautm): Remove this workaround after fixing the schema URL issue.
plan.File.URL = planFiles[0].URL
plan.File.Link = planFiles[0].Link
plan.File.Status = "PENDING"
// PR context and no existing plan
case tc.PullRequest != nil && len(planFiles) == 0:
case len(planFiles) == 0:
name := a.GetInput("name")
runPlan:
// Dry run if the name is not provided.
Expand Down Expand Up @@ -621,6 +598,62 @@ func (a *Actions) SchemaPlan(ctx context.Context) error {
return nil
}

// SchemaPlanApprove runs the GitHub Action for "ariga/atlas-action/schema/plan/approve"
func (a *Actions) SchemaPlanApprove(ctx context.Context) error {
tc, err := a.GetTriggerContext()
switch {
case err != nil:
return fmt.Errorf("unable to get the trigger context: %w", err)
case tc.PullRequest != nil:
return fmt.Errorf("the action should be run in a branch context")
}
params := &atlasexec.SchemaPlanApproveParams{
ConfigURL: a.GetInput("config"),
Env: a.GetInput("env"),
Vars: a.GetVarsInput("vars"),
URL: a.GetInput("plan"),
}
if params.URL == "" {
a.Infof("No plan URL provided, searching for the pending plan")
switch planFiles, err := a.Atlas.SchemaPlanList(ctx, &atlasexec.SchemaPlanListParams{
ConfigURL: params.ConfigURL,
Env: params.Env,
Vars: params.Vars,
Context: a.GetRunContext(ctx, tc),
Repo: a.GetInput("schema-name"),
DevURL: a.GetInput("dev-url"),
From: a.GetArrayInput("from"),
To: a.GetArrayInput("to"),
Pending: true,
}); {
case err != nil:
return fmt.Errorf("failed to list schema plans: %w", err)
case len(planFiles) > 1:
for _, f := range planFiles {
a.Infof("Found schema plan: %s", f.URL)
}
return fmt.Errorf("found multiple schema plans, please approve or delete the existing plans")
case len(planFiles) == 0:
a.Infof("No schema plan found")
return nil
case len(planFiles) == 1:
params.URL = planFiles[0].URL
default:
return fmt.Errorf("unexpected context, please contact the atlas team")
}
}
result, err := a.Atlas.SchemaPlanApprove(ctx, params)
if err != nil {
return fmt.Errorf("failed to approve the schema plan: %w", err)
}
// Successfully approved the plan.
a.Infof("Schema plan approved successfully: %s", result.Link)
a.SetOutput("link", result.Link)
a.SetOutput("plan", result.URL)
a.SetOutput("status", result.Status)
return nil
}

// SchemaApply runs the GitHub Action for "ariga/atlas-action/schema/apply"
func (a *Actions) SchemaApply(ctx context.Context) error {
params := &atlasexec.SchemaApplyParams{
Expand Down
96 changes: 84 additions & 12 deletions atlasaction/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2061,14 +2061,88 @@ func TestSchemaPlan(t *testing.T) {
"link": "https://gh.atlasgo.cloud/plan/pr-1-Rl4lBdMk",
}, act.output, "expected output with plan URL")

// Check all logs output
require.Equal(t, `time=NOW level=INFO msg="Found schema plan: atlas://atlas-action/plans/pr-1-Rl4lBdMk"
time=NOW level=INFO msg="Found schema plan: atlas://atlas-action/plans/pr-1-Rl4lBdMk"
time=NOW level=INFO msg="The current state is synced with the desired state, no changes to be made"
time=NOW level=INFO msg="Schema plan does not exist, creating a new one with name \"pr-1-ufnTS7Nr\""
time=NOW level=INFO msg="Schema plan already exists, linting the plan \"pr-1-Rl4lBdMk\""
`, out.String())
}

func TestSchemaPlanApprove(t *testing.T) {
h := http.NewServeMux()
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL)
})
srv := httptest.NewServer(h)
t.Cleanup(srv.Close)
planFile := &atlasexec.SchemaPlanFile{
Name: "pr-1-Rl4lBdMk",
FromHash: "ufnTS7NrAgkvQlxbpnSxj119MAPGNqVj0i3Eelv+iLc=", // Used as comment marker
ToHash: "Rl4lBdMkvFoGQ4xu+3sYCeogTVnamJ7bmDoq9pMXcjw=",
URL: "atlas://atlas-action/plans/pr-1-Rl4lBdMk",
Link: "https://gh.atlasgo.cloud/plan/pr-1-Rl4lBdMk",
Status: "PENDING",
}
var planFiles []atlasexec.SchemaPlanFile
var approveErr error
m := &mockAtlas{
schemaPlanList: func(_ context.Context, p *atlasexec.SchemaPlanListParams) ([]atlasexec.SchemaPlanFile, error) {
return planFiles, nil
},
schemaPlanApprove: func(_ context.Context, p *atlasexec.SchemaPlanApproveParams) (*atlasexec.SchemaPlanApprove, error) {
require.Equal(t, "file://atlas.hcl", p.ConfigURL)
require.Equal(t, "atlas://atlas-action/plans/pr-1-Rl4lBdMk", p.URL)
if approveErr != nil {
return nil, approveErr
}
return &atlasexec.SchemaPlanApprove{
URL: "atlas://atlas-action/plans/pr-1-Rl4lBdMk",
Link: "https://gh.atlasgo.cloud/plan/pr-1-Rl4lBdMk",
Status: "APPROVED",
}, nil
},
}
t.Setenv("GITHUB_TOKEN", "token")
out := &bytes.Buffer{}
act := &mockAction{
inputs: map[string]string{
// "schema-name": "atlas://atlas-action",
"from": "sqlite://file?_fk=1&mode=memory",
"config": "file://atlas.hcl",
"env": "test",
},
logger: slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.String(slog.TimeKey, "NOW") // Fake time
}
return a
},
})),
trigger: &atlasaction.TriggerContext{
SCM: atlasaction.SCM{Type: atlasexec.SCMTypeGithub, APIURL: srv.URL},
Repo: "ariga/atlas-action",
RepoURL: "https://github.com/ariga/atlas-action",
Branch: "g/feature-1",
Commit: "commit-id",
},
}
ctx := context.Background()
// Multiple plans will fail with an error
planFiles = []atlasexec.SchemaPlanFile{*planFile, *planFile}
act.resetOutputs()
require.ErrorContains(t, (&atlasaction.Actions{Action: act, Atlas: m}).SchemaPlanApprove(ctx), "found multiple schema plans, please approve or delete the existing plans")
require.Len(t, act.summary, 0, "Expected 1 summary")

// Trigger with no pull request, master branch
planFiles = []atlasexec.SchemaPlanFile{*planFile}
act.trigger.PullRequest = nil
act.trigger.Branch = "master"
act.resetOutputs()
require.NoError(t, (&atlasaction.Actions{Action: act, Atlas: m}).SchemaPlan(ctx))
require.Len(t, act.summary, 2, "No more summaries generated")
require.Equal(t, 1, commentCounter, "No more comments generated")
require.Equal(t, 1, commentEdited, "No comment should be edited")
require.NoError(t, (&atlasaction.Actions{Action: act, Atlas: m}).SchemaPlanApprove(ctx))
require.Len(t, act.summary, 0, "No more summaries generated")
require.EqualValues(t, map[string]string{
"plan": "atlas://atlas-action/plans/pr-1-Rl4lBdMk",
"status": "APPROVED",
Expand All @@ -2078,19 +2152,17 @@ func TestSchemaPlan(t *testing.T) {
// No pending plan
planFiles = nil
act.resetOutputs()
require.NoError(t, (&atlasaction.Actions{Action: act, Atlas: m}).SchemaPlan(ctx))
require.Len(t, act.summary, 2, "No more summaries generated")
require.Equal(t, 1, commentCounter, "No more comments generated")
require.Equal(t, 1, commentEdited, "No comment should be edited")
require.NoError(t, (&atlasaction.Actions{Action: act, Atlas: m}).SchemaPlanApprove(ctx))
require.Len(t, act.summary, 0, "No more summaries generated")
require.EqualValues(t, map[string]string{}, act.output, "expected output with plan URL")

// Check all logs output
require.Equal(t, `time=NOW level=INFO msg="Found schema plan: atlas://atlas-action/plans/pr-1-Rl4lBdMk"
require.Equal(t, `time=NOW level=INFO msg="No plan URL provided, searching for the pending plan"
time=NOW level=INFO msg="Found schema plan: atlas://atlas-action/plans/pr-1-Rl4lBdMk"
time=NOW level=INFO msg="The current state is synced with the desired state, no changes to be made"
time=NOW level=INFO msg="Schema plan does not exist, creating a new one with name \"pr-1-ufnTS7Nr\""
time=NOW level=INFO msg="Schema plan already exists, linting the plan \"pr-1-Rl4lBdMk\""
time=NOW level=INFO msg="Found schema plan: atlas://atlas-action/plans/pr-1-Rl4lBdMk"
time=NOW level=INFO msg="No plan URL provided, searching for the pending plan"
time=NOW level=INFO msg="Schema plan approved successfully: https://gh.atlasgo.cloud/plan/pr-1-Rl4lBdMk"
time=NOW level=INFO msg="No plan URL provided, searching for the pending plan"
time=NOW level=INFO msg="No schema plan found"
`, out.String())
}
Expand Down
53 changes: 53 additions & 0 deletions schema/plan/approve/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: 'Schema Plan Approve'
description: 'Approve a migration plan by its URL'
branding:
icon: database
author: 'Ariga'
inputs:
working-directory:
description: Atlas working directory, default is project root
required: false
config:
description: |
The URL of the Atlas configuration file. By default, Atlas will look for a file
named `atlas.hcl` in the current directory. For example, `file://config/atlas.hcl`.
Learn more about [Atlas configuration files](https://atlasgo.io/atlas-schema/projects).
required: false
env:
description: The environment to use from the Atlas configuration file. For example, `dev`.
required: false
vars:
description: |
A JSON object containing variables to be used in the Atlas configuration file.
For example, `{"var1": "value1", "var2": "value2"}`.
required: false
dev-url:
description: |
The URL of the dev-database to use for analysis. For example: `mysql://root:pass@localhost:3306/dev`.
Read more about [dev-databases](https://atlasgo.io/concepts/dev-database).
required: false
plan:
description: |
The URL of the plan to be approved. For example, `atlas://<schema>/plans/<id>`.
required: false
schema-name:
description: The name (slug) of the project in Atlas Cloud.
required: false
from:
description: |
URL(s) of the current schema state.
required: false
to:
description: |
URL(s) of the desired schema state.
required: false
outputs:
plan:
description: The plan to be applied or generated. (ig. `atlas://<schema>/plans/<id>`)
link: # id of the output
description: Link to the schema plan on Atlas.
status:
description: The status of the plan. (ig. `PENDING`, `APPROVED`)
runs:
using: node20
main: index.js
1 change: 1 addition & 0 deletions schema/plan/approve/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('../../../shim/dist')('schema/plan/approve')

0 comments on commit e1da448

Please sign in to comment.