Skip to content

Commit

Permalink
schem/apply: apply schema changes to the target database (#218)
Browse files Browse the repository at this point in the history
* schem/apply: apply schema changes to the target database

* chore: test script for schema/apply action
  • Loading branch information
giautm authored Sep 16, 2024
1 parent 28f4872 commit 16c70fd
Show file tree
Hide file tree
Showing 31 changed files with 614 additions and 5 deletions.
48 changes: 46 additions & 2 deletions .github/workflows/ci-go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ jobs:
- id: sanity
uses: ./schema/push
with:
working-directory: atlasaction/testdata/plan
working-directory: atlasaction/testdata/schema-apply/legacy
env: test
schema-plan:
runs-on: ubuntu-latest
Expand All @@ -318,9 +318,53 @@ jobs:
- id: sanity
uses: ./schema/plan
with:
working-directory: atlasaction/testdata/plan
working-directory: atlasaction/testdata/schema-apply/legacy
env: test
from: |-
env://url
env:
GITHUB_TOKEN: ${{ github.token }}
schema-apply:
runs-on: ubuntu-latest
env:
ATLAS_ACTION_LOCAL: 1
strategy:
fail-fast: false
matrix:
test:
- directory: lint-review
- directory: on-the-fly
auto-approve: "true"
- directory: remote-repo
plan: "atlas://atlas-action/plans/20240910183610"
to: "atlas://atlas-action?tag=e2e"
- directory: local-plan
plan: "file://20240910173744.plan.hcl"
- directory: multiple-envs
plan: "file://20240910173744.plan.hcl"
- directory: legacy
atlas: 'v0.27.0'
auto-approve: "true"
steps:
- uses: ariga/setup-atlas@v0
with:
cloud-token: ${{ secrets.ATLAS_TOKEN }}
version: ${{ matrix.test.atlas }}
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: go.mod
- run: go install ./cmd/atlas-action
env:
CGO_ENABLED: 0
- name: Apply changes
continue-on-error: true
uses: ./schema/apply
env:
GITHUB_TOKEN: ${{ github.token }}
with:
working-directory: atlasaction/testdata/schema-apply/${{ matrix.test.directory }}
env: test
plan: ${{ matrix.test.plan }}
to: ${{ matrix.test.to }}
auto-approve: ${{ matrix.test.auto-approve }}
52 changes: 49 additions & 3 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ type AtlasExec interface {
SchemaPlanLint(context.Context, *atlasexec.SchemaPlanLintParams) (*atlasexec.SchemaPlan, error)
// SchemaPlanApprove runs the `schema plan approve` command.
SchemaPlanApprove(context.Context, *atlasexec.SchemaPlanApproveParams) (*atlasexec.SchemaPlanApprove, error)
// SchemaApplySlice runs the `schema apply` command.
SchemaApplySlice(context.Context, *atlasexec.SchemaApplyParams) ([]*atlasexec.SchemaApply, error)
}

// Context holds the context of the environment the action is running in.
Expand Down Expand Up @@ -150,9 +152,10 @@ const (
CmdMigrateDown = "migrate/down"
CmdMigrateTest = "migrate/test"
// Declarative workflow Commands
CmdSchemaPush = "schema/push"
CmdSchemaTest = "schema/test"
CmdSchemaPlan = "schema/plan"
CmdSchemaPush = "schema/push"
CmdSchemaTest = "schema/test"
CmdSchemaPlan = "schema/plan"
CmdSchemaApply = "schema/apply"
)

// Run runs the action based on the command name.
Expand Down Expand Up @@ -180,6 +183,8 @@ func (a *Actions) Run(ctx context.Context, act string) error {
return a.SchemaTest(ctx)
case CmdSchemaPlan:
return a.SchemaPlan(ctx)
case CmdSchemaApply:
return a.SchemaApply(ctx)
default:
return fmt.Errorf("unknown action: %s", act)
}
Expand Down Expand Up @@ -615,6 +620,47 @@ func (a *Actions) SchemaPlan(ctx context.Context) error {
return nil
}

// SchemaApply runs the GitHub Action for "ariga/atlas-action/schema/apply"
func (a *Actions) SchemaApply(ctx context.Context) error {
params := &atlasexec.SchemaApplyParams{
ConfigURL: a.GetInput("config"),
Env: a.GetInput("env"),
Vars: a.GetVarsInput("vars"),
URL: a.GetInput("url"),
To: a.GetInput("to"),
DryRun: a.GetBoolInput("dry-run"),
AutoApprove: a.GetBoolInput("auto-approve"),
PlanURL: a.GetInput("plan"),
TxMode: a.GetInput("tx-mode"), // Hidden param.
}
results, err := a.Atlas.SchemaApplySlice(ctx, params)
// Any errors will print at the end of execution.
if mErr := (&atlasexec.SchemaApplyError{}); errors.As(err, &mErr) {
// If the error is a SchemaApplyError, we can still get the successful runs.
results = mErr.Result
}
for _, result := range results {
switch summary, err := RenderTemplate("schema-apply.tmpl", result); {
case err != nil:
a.Errorf("failed to create summary: %v", err)
default:
a.AddStepSummary(summary)
}
if result.Error != "" {
a.SetOutput("error", result.Error)
return errors.New(result.Error)
}
a.Infof(`"atlas schema apply" completed successfully on the target %q`, result.URL)
}
// We generate summary for the successful runs.
// Then fail the action if there is an error.
if err != nil {
a.SetOutput("error", err.Error())
return err
}
return nil
}

// WorkingDir returns the working directory for the action.
func (a *Actions) WorkingDir() string {
return a.GetInput("working-directory")
Expand Down
6 changes: 6 additions & 0 deletions atlasaction/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ func (m *mockAtlas) SchemaPlanLint(ctx context.Context, p *atlasexec.SchemaPlanL
return m.schemaPlanLint(ctx, p)
}

// SchemaPlanStatus implements AtlasExec.
func (m *mockAtlas) SchemaApplySlice(ctx context.Context, params *atlasexec.SchemaApplyParams) ([]*atlasexec.SchemaApply, error) {
panic("unimplemented")
}

// MigrateDown implements AtlasExec.
func (m *mockAtlas) MigrateDown(ctx context.Context, params *atlasexec.MigrateDownParams) (*atlasexec.MigrateDown, error) {
return m.migrateDown(ctx, params)
}
Expand Down
71 changes: 71 additions & 0 deletions atlasaction/comments/schema-apply.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{{- template "migrate-header" . -}}
<h4><code>atlas schema apply</code> Summary:</h4>
{{- with .Plan -}}
<table>
<tr>
<td>Database URL</td>
<td><code>{{ $.URL }}</code></td>
</tr>
{{- with .File -}}
<tr>
<td>Plan Name</td>
<td><code>{{ .Name }}</code></td>
</tr>
<tr>
<td>From Hash</td>
<td><code>{{ .FromHash }}</code></td>
</tr>
<tr>
<td>To Hash</td>
<td><code>{{ .ToHash }}</code></td>
</tr>
{{- if .URL -}}
<tr>
<td>Plan URL</td>
<td>
{{- $planURL := printf "<code>%s</code>" .URL -}}
{{- with .Link -}}
{{- . | link $planURL -}}
{{- else -}}
{{- $planURL -}}
{{- end -}}
</td>
</tr>
{{- end -}}
{{- with .Status -}}
<tr><td>Plan Status</td><td>{{ . }}</td></tr>
{{- end -}}
{{- end -}}
{{- with $.Error -}}
<tr><td>Error</td><td>{{ . }}</td></tr>
{{- else -}}
<tr>
<td>Total Time</td>
<td>{{ execTime $.Start $.End }}</td>
</tr>
{{- end -}}
</table>
{{- if $.Applied -}}
{{- template "applied-file" $.Applied -}}
{{- end -}}
{{- $kind := or (and .File.URL "Pre-planned SQL") "SQL" -}}
{{- .File.Migration | codeblock "sql" | details (printf "📄 View %s Statements" $kind) -}}
{{- with .Lint -}}
<h4>Atlas lint results</h4>
{{- template "lint-report" . -}}
{{- end -}}
{{/* Fallback to the old output */}}
{{- else with .Changes -}}
{{- with .Error -}}
The following SQL statement failed to execute:
{{- .Stmt | codeblock "sql" -}}
<br>Database returned the following error:
{{- .Text | codeblock "" -}}
{{- end -}}
{{- with .Applied -}}
{{- join . "\n" | codeblock "sql" | details "📄 Succeeded SQL Applied" -}}
{{- end -}}
{{- with .Pending -}}
{{- join . "\n" | codeblock "sql" | details "📄 Pending SQL statements" -}}
{{- end }}
{{- end -}}
136 changes: 136 additions & 0 deletions atlasaction/testdata/github/schema-apply-envs.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Mock the atlas command outputs
mock-atlas $WORK/schema-apply
# Setup the action input variables
env INPUT_ENV=test
env INPUT_PLAN=file://20240910173744.plan.hcl
# Run the action
! atlas-action schema/apply
stdout '"atlas schema apply" completed successfully on the target "sqlite://local-bu.db"'
stdout '"atlas schema apply" completed successfully on the target "sqlite://local-pi.db"'
stdout '"atlas schema apply" completed successfully on the target "sqlite://local-su.db"'

summary summary.html
output output.txt

-- schema-apply/1/args --
schema apply --format {{ json . }} --env test --plan file://20240910173744.plan.hcl
-- schema-apply/1/stderr --
Abort: The plan "From" hash does not match the current state hash (passed with --from):

- iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE= (plan value)
+ R1cGcSfo1oWYK4dz+7WvgCtE/QppFo9lKFEqEDzoS4o= (current hash)

-- schema-apply/1/stdout --
{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Opaque":"","User":null,"Host":"local-bu.db","Path":"","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":"","Schema":"main"},"Start":"2024-09-12T21:31:20.339663+07:00","End":"2024-09-12T21:31:20.351603+07:00","Applied":{"Name":"20240910173744.sql","Version":"20240910173744","Start":"2024-09-12T21:31:20.350607+07:00","End":"2024-09-12T21:31:20.351228+07:00","Applied":["ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;"]},"Plan":{"Env":{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Opaque":"","User":null,"Host":"local-bu.db","Path":"","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":"","Schema":"main"}},"File":{"Name":"20240910173744","FromHash":"iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE=","ToHash":"Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4=","Migration":"-- Add column \"c2\" to table: \"t4\"\nALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;\n","URL":"file://20240910173744.plan.hcl"}},"Changes":{"Applied":["ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;"]}}
{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Opaque":"","User":null,"Host":"local-pi.db","Path":"","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":"","Schema":"main"},"Start":"2024-09-12T21:31:20.354074+07:00","End":"2024-09-12T21:31:20.35764+07:00","Applied":{"Name":"20240910173744.sql","Version":"20240910173744","Start":"2024-09-12T21:31:20.356221+07:00","End":"2024-09-12T21:31:20.356755+07:00","Applied":["ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;"]},"Plan":{"Env":{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Opaque":"","User":null,"Host":"local-pi.db","Path":"","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":"","Schema":"main"}},"File":{"Name":"20240910173744","FromHash":"iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE=","ToHash":"Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4=","Migration":"-- Add column \"c2\" to table: \"t4\"\nALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;\n","URL":"file://20240910173744.plan.hcl"}},"Changes":{"Applied":["ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;"]}}
{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Opaque":"","User":null,"Host":"local-su.db","Path":"","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":"","Schema":"main"},"Start":"2024-09-12T21:31:20.360863+07:00","End":"2024-09-12T21:31:20.368395+07:00","Applied":{"Name":"20240910173744.sql","Version":"20240910173744","Start":"2024-09-12T21:31:20.364331+07:00","End":"2024-09-12T21:31:20.365086+07:00","Applied":["ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;"]},"Plan":{"Env":{"Driver":"sqlite3","URL":{"Scheme":"sqlite","Opaque":"","User":null,"Host":"local-su.db","Path":"","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":"","Schema":"main"}},"File":{"Name":"20240910173744","FromHash":"iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE=","ToHash":"Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4=","Migration":"-- Add column \"c2\" to table: \"t4\"\nALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;\n","URL":"file://20240910173744.plan.hcl"}},"Changes":{"Applied":["ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;"]}}
-- output.txt --
error<<_GitHubActionsFileCommandDelimeter_
Abort: The plan "From" hash does not match the current state hash (passed with --from): - iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE= (plan value) + R1cGcSfo1oWYK4dz+7WvgCtE/QppFo9lKFEqEDzoS4o= (current hash)
_GitHubActionsFileCommandDelimeter_
-- summary.html --
<h2><picture><source media="(prefers-color-scheme: light)" srcset="https://release.ariga.io/images/assets/success.svg?v=1"><img width="22px" height="22px" src="https://release.ariga.io/images/assets/success.svg?v=1"/></picture> Migration Passed</h2><h4><code>atlas schema apply</code> Summary:</h4><table>
<tr>
<td>Database URL</td>
<td><code>sqlite://local-bu.db</code></td>
</tr><tr>
<td>Plan Name</td>
<td><code>20240910173744</code></td>
</tr>
<tr>
<td>From Hash</td>
<td><code>iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE=</code></td>
</tr>
<tr>
<td>To Hash</td>
<td><code>Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4=</code></td>
</tr><tr>
<td>Plan URL</td>
<td><code>file://20240910173744.plan.hcl</code></td>
</tr><tr>
<td>Total Time</td>
<td>11.94ms</td>
</tr></table><h4>Version 20240910173744.sql:</h4>
<table>
<tr>
<th>Status</th>
<th>Executed Statements</th>
<th>Execution Time</th>
<th>Error</th>
<th>Error Statement</th>
</tr>
<tr><td><div align="center"><picture><source media="(prefers-color-scheme: light)" srcset="https://release.ariga.io/images/assets/success.svg?v=1"><img width="20px" height="20px" src="https://release.ariga.io/images/assets/success.svg?v=1"/></picture></div></td><td>1</td>
<td>621µs</td><td>-</td><td>-</td></tr>
</table><details><summary>📄 View Pre-planned SQL Statements</summary><pre lang="sql"><code>-- Add column "c2" to table: "t4"
ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;
</code></pre></details>
<h2><picture><source media="(prefers-color-scheme: light)" srcset="https://release.ariga.io/images/assets/success.svg?v=1"><img width="22px" height="22px" src="https://release.ariga.io/images/assets/success.svg?v=1"/></picture> Migration Passed</h2><h4><code>atlas schema apply</code> Summary:</h4><table>
<tr>
<td>Database URL</td>
<td><code>sqlite://local-pi.db</code></td>
</tr><tr>
<td>Plan Name</td>
<td><code>20240910173744</code></td>
</tr>
<tr>
<td>From Hash</td>
<td><code>iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE=</code></td>
</tr>
<tr>
<td>To Hash</td>
<td><code>Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4=</code></td>
</tr><tr>
<td>Plan URL</td>
<td><code>file://20240910173744.plan.hcl</code></td>
</tr><tr>
<td>Total Time</td>
<td>3.566ms</td>
</tr></table><h4>Version 20240910173744.sql:</h4>
<table>
<tr>
<th>Status</th>
<th>Executed Statements</th>
<th>Execution Time</th>
<th>Error</th>
<th>Error Statement</th>
</tr>
<tr><td><div align="center"><picture><source media="(prefers-color-scheme: light)" srcset="https://release.ariga.io/images/assets/success.svg?v=1"><img width="20px" height="20px" src="https://release.ariga.io/images/assets/success.svg?v=1"/></picture></div></td><td>1</td>
<td>534µs</td><td>-</td><td>-</td></tr>
</table><details><summary>📄 View Pre-planned SQL Statements</summary><pre lang="sql"><code>-- Add column "c2" to table: "t4"
ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;
</code></pre></details>
<h2><picture><source media="(prefers-color-scheme: light)" srcset="https://release.ariga.io/images/assets/success.svg?v=1"><img width="22px" height="22px" src="https://release.ariga.io/images/assets/success.svg?v=1"/></picture> Migration Passed</h2><h4><code>atlas schema apply</code> Summary:</h4><table>
<tr>
<td>Database URL</td>
<td><code>sqlite://local-su.db</code></td>
</tr><tr>
<td>Plan Name</td>
<td><code>20240910173744</code></td>
</tr>
<tr>
<td>From Hash</td>
<td><code>iHZMQ1EoarAXt/KU0KQbBljbbGs8gVqX2ZBXefePSGE=</code></td>
</tr>
<tr>
<td>To Hash</td>
<td><code>Cp8xCVYilZuwULkggsfJLqIQHaxYcg/IpU+kgjVUBA4=</code></td>
</tr><tr>
<td>Plan URL</td>
<td><code>file://20240910173744.plan.hcl</code></td>
</tr><tr>
<td>Total Time</td>
<td>7.532ms</td>
</tr></table><h4>Version 20240910173744.sql:</h4>
<table>
<tr>
<th>Status</th>
<th>Executed Statements</th>
<th>Execution Time</th>
<th>Error</th>
<th>Error Statement</th>
</tr>
<tr><td><div align="center"><picture><source media="(prefers-color-scheme: light)" srcset="https://release.ariga.io/images/assets/success.svg?v=1"><img width="20px" height="20px" src="https://release.ariga.io/images/assets/success.svg?v=1"/></picture></div></td><td>1</td>
<td>755µs</td><td>-</td><td>-</td></tr>
</table><details><summary>📄 View Pre-planned SQL Statements</summary><pre lang="sql"><code>-- Add column "c2" to table: "t4"
ALTER TABLE `t4` ADD COLUMN `c2` integer NOT NULL;
</code></pre></details>
Loading

0 comments on commit 16c70fd

Please sign in to comment.