Repository: ariga/atlas
Pull request #2066 (2023-09-11 04:03:43)

doc/website: update operator v0.3.3
Related Issue: #2051
Need to bump version of Operator charts before merge


Patch:
diff --git a/cmd/atlas/internal/cmdapi/cmdapi_oss.go b/cmd/atlas/internal/cmdapi/cmdapi_oss.go
index 8b515dc9..d2ad1d5d 100644
--- a/cmd/atlas/internal/cmdapi/cmdapi_oss.go
+++ b/cmd/atlas/internal/cmdapi/cmdapi_oss.go
@@ -7,12 +7,16 @@
 package cmdapi
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"text/template"
 
+	"ariga.io/atlas/cmd/atlas/internal/cloudapi"
 	cmdmigrate "ariga.io/atlas/cmd/atlas/internal/migrate"
 	"ariga.io/atlas/cmd/atlas/internal/migratelint"
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlcheck"
 	"ariga.io/atlas/sql/sqlclient"
 
@@ -108,3 +112,20 @@ func migrateLintRun(cmd *cobra.Command, _ []string, flags migrateLintFlags) erro
 	cmd.SilenceUsage = cmd.SilenceErrors
 	return err
 }
+
+func promptApply(cmd *cobra.Command,
+	changes []schema.Change,
+	flags schemaApplyFlags,
+	apply func(context.Context, []schema.Change, ...migrate.PlanOption) error,
+	_ *sqlclient.Client,
+) error {
+	if !flags.dryRun && (flags.autoApprove || promptUser(cmd)) {
+		return apply(cmd.Context(), changes)
+	}
+	return nil
+}
+
+// withTokenContext allows attaching token to the context.
+func withTokenContext(ctx context.Context, _ string, _ *cloudapi.Client) (context.Context, error) {
+	return ctx, nil
+}
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 22504405..a36e617e 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -98,13 +98,24 @@ If run with the "--dry-run" flag, atlas will not execute any SQL.`,
 					}
 					return migrateApplyRun(cmd, args, flags, &MigrateReport{}) // nop reporter
 				default:
-					project, envs, err := EnvByName(GlobalFlags.SelectedEnv, WithInput(GlobalFlags.Vars))
+					project, envs, err := EnvByName(cmd, GlobalFlags.SelectedEnv, WithInput(GlobalFlags.Vars))
 					if err != nil {
 						return err
 					}
-					set := NewReportProvider(cmd.Context(), project, envs)
-					defer func() { set.Flush(cmd, cmdErr) }()
+					var (
+						hasRemote bool
+						set       = NewReportProvider(cmd.Context(), project, envs)
+					)
+					defer func() {
+						if hasRemote {
+							set.Flush(cmd, cmdErr)
+						}
+					}()
 					return cmdEnvsRun(envs, setMigrateEnvFlags, cmd, func(env *Env) error {
+						// Report deployments only if one of the migration directories is a cloud directory.
+						if u, err := url.Parse(flags.dirURL); err == nil && u.Scheme != cmdmigrate.DirTypeFile {
+							hasRemote = true
+						}
 						return migrateApplyRun(cmd, args, flags, set.ReportFor(flags, env))
 					})
 				}
@@ -431,7 +442,14 @@ func (r *MigrateReport) Done(cmd *cobra.Command, flags migrateApplyFlags) error
 		ver = rev.Version
 	}
 	dirName := flags.dirURL
-	if u, err := url.Parse(flags.dirURL); err == nil {
+	switch u, err := url.Parse(flags.dirURL); {
+	case err != nil:
+	// Local directories are reported as (dangling)
+	// deployments without a directory.
+	case u.Scheme == cmdmigrate.DirTypeFile:
+		dirName = cloudapi.DefaultDirName
+	// Directory slug.
+	default:
 		dirName = filepath.Join(u.Host, u.Path)
 	}
 	r.done(&cloudapi.ReportMigrationInput{
diff --git a/cmd/atlas/internal/cmdapi/project.go b/cmd/atlas/internal/cmdapi/project.go
index d21e048f..adab66de 100644
--- a/cmd/atlas/internal/cmdapi/project.go
+++ b/cmd/atlas/internal/cmdapi/project.go
@@ -20,6 +20,7 @@ import (
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclparse"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/spf13/cobra"
 	"github.com/zclconf/go-cty/cty"
 	"github.com/zclconf/go-cty/cty/function"
 )
@@ -335,7 +336,7 @@ func (e *Env) asMap() (map[string]string, error) {
 }
 
 // EnvByName parses and returns the project configuration with selected environments.
-func EnvByName(name string, opts ...LoadOption) (*Project, []*Env, error) {
+func EnvByName(cmd *cobra.Command, name string, opts ...LoadOption) (*Project, []*Env, error) {
 	u, err := url.Parse(GlobalFlags.ConfigURL)
 	if err != nil {
 		return nil, nil, err
@@ -354,6 +355,15 @@ func EnvByName(name string, opts ...LoadOption) (*Project, []*Env, error) {
 	if err != nil {
 		return nil, nil, err
 	}
+	// The project token predates 'atlas login' command. If exists,
+	// attach it to the context to indicate the user is authenticated.
+	if project.cfg.Token != "" && project.cfg.Client != nil {
+		ctx, err := withTokenContext(cmd.Context(), project.cfg.Token, project.cfg.Client)
+		if err != nil {
+			return nil, nil, err
+		}
+		cmd.SetContext(ctx)
+	}
 	if err := project.Lint.remainedLog(); err != nil {
 		return nil, nil, err
 	}
diff --git a/cmd/atlas/internal/cmdapi/schema.go b/cmd/atlas/internal/cmdapi/schema.go
index be277cac..007ad5f5 100644
--- a/cmd/atlas/internal/cmdapi/schema.go
+++ b/cmd/atlas/internal/cmdapi/schema.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"io/fs"
 	"os"
 	"path/filepath"
@@ -87,7 +88,7 @@ migration.`,
 					}
 					return schemaApplyRun(cmd, flags, env)
 				default:
-					_, envs, err := EnvByName(GlobalFlags.SelectedEnv, WithInput(GlobalFlags.Vars))
+					_, envs, err := EnvByName(cmd, GlobalFlags.SelectedEnv, WithInput(GlobalFlags.Vars))
 					if err != nil {
 						return err
 					}
@@ -228,10 +229,7 @@ func schemaApplyRun(cmd *cobra.Command, flags schemaApplyFlags, env *Env) error
 		if err := summary(cmd, client, changes, format); err != nil {
 			return err
 		}
-		if !flags.dryRun && (flags.autoApprove || promptUser()) {
-			return apply(ctx, changes)
-		}
-		return nil
+		return promptApply(cmd, changes, flags, apply, client)
 	}
 }
 
@@ -302,7 +300,7 @@ func schemaCleanRun(cmd *cobra.Command, _ []string, flags schemeCleanFlags) erro
 	if err := summary(cmd, c, drop, cmdlog.SchemaPlanTemplate); err != nil {
 		return err
 	}
-	if flags.AutoApprove || promptUser() {
+	if flags.AutoApprove || promptUser(cmd) {
 		if err := c.ApplyChanges(cmd.Context(), drop); err != nil {
 			return err
 		}
@@ -572,7 +570,7 @@ func selectEnv(cmd *cobra.Command) (*Env, error) {
 	switch name := GlobalFlags.SelectedEnv; {
 	// A config file was passed without an env.
 	case name == "" && cmd.Flags().Changed(flagConfig):
-		p, envs, err := EnvByName(name, WithInput(GlobalFlags.Vars))
+		p, envs, err := EnvByName(cmd, name, WithInput(GlobalFlags.Vars))
 		if err != nil {
 			return nil, err
 		}
@@ -585,7 +583,7 @@ func selectEnv(cmd *cobra.Command) (*Env, error) {
 		return &Env{Lint: &Lint{}, Migration: &Migration{}}, nil
 	// Env was passed.
 	default:
-		_, envs, err := EnvByName(name, WithInput(GlobalFlags.Vars))
+		_, envs, err := EnvByName(cmd, name, WithInput(GlobalFlags.Vars))
 		if err != nil {
 			return nil, err
 		}
@@ -700,11 +698,18 @@ const (
 	answerAbort = "Abort"
 )
 
-func promptUser() bool {
-	prompt := promptui.Select{
-		Label: "Are you sure?",
-		Items: []string{answerApply, answerAbort},
+// cmdPrompt returns a promptui.Select that uses the given command's input and output.
+func cmdPrompt(cmd *cobra.Command) *promptui.Select {
+	return &promptui.Select{
+		Stdin:  io.NopCloser(cmd.InOrStdin()),
+		Stdout: nopCloser{cmd.OutOrStdout()},
 	}
+}
+
+func promptUser(cmd *cobra.Command) bool {
+	prompt := cmdPrompt(cmd)
+	prompt.Items = []string{answerApply, answerAbort}
+
 	_, result, err := prompt.Run()
 	if err != nil && err != promptui.ErrInterrupt {
 		// Fail in case of unexpected errors.
@@ -713,6 +718,10 @@ func promptUser() bool {
 	return result == answerApply
 }
 
+type nopCloser struct{ io.Writer }
+
+func (nopCloser) Close() error { return nil }
+
 func tasks(path string) ([]fmttask, error) {
 	var tasks []fmttask
 	stat, err := os.Stat(path)
diff --git a/cmd/atlas/internal/cmdext/cmdext.go b/cmd/atlas/internal/cmdext/cmdext.go
index 22a371c6..558ddb64 100644
--- a/cmd/atlas/internal/cmdext/cmdext.go
+++ b/cmd/atlas/internal/cmdext/cmdext.go
@@ -44,9 +44,11 @@ import (
 	"github.com/hashicorp/hcl/v2/gohcl"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
 	"github.com/zclconf/go-cty/cty"
+	"gocloud.dev/gcerrors"
 	"gocloud.dev/runtimevar"
 	_ "gocloud.dev/runtimevar/awssecretsmanager"
 	_ "gocloud.dev/runtimevar/constantvar"
+	"gocloud.dev/runtimevar/driver"
 	_ "gocloud.dev/runtimevar/filevar"
 	_ "gocloud.dev/runtimevar/gcpsecretmanager"
 	_ "gocloud.dev/runtimevar/httpvar"
@@ -415,6 +417,7 @@ func TemplateDir(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
 // defined and executed on schemahcl Eval functions.
 type AtlasConfig struct {
 	Client  *cloudapi.Client // Client attached to Atlas Cloud.
+	Token   string           // User token.
 	Project string           // Optional project.
 }
 
@@ -424,7 +427,7 @@ type AtlasConfig struct {
 //	  cloud {
 //	    token   = data.runtimevar.token  // User token.
 //	    url     = var.cloud_url          // Optional URL.
-//	    project = var.project            // Optional project. If set, cloud reporting is enabled.
+//	    project = var.project            // Optional project. Defaults to DefaultProjectName.
 //	  }
 //	}
 func (c *AtlasConfig) InitBlock() schemahcl.Option {
@@ -442,6 +445,7 @@ func (c *AtlasConfig) InitBlock() schemahcl.Option {
 		if args.Cloud.Project == "" {
 			args.Cloud.Project = cloudapi.DefaultProjectName
 		}
+		c.Token = args.Cloud.Token
 		c.Project = args.Cloud.Project
 		c.Client = cloudapi.New(args.Cloud.URL, args.Cloud.Token)
 		cloud := cty.ObjectVal(map[string]cty.Value{
@@ -843,3 +847,72 @@ func dirFormatter(dir migrate.Dir) migrate.Formatter {
 		return migrate.DefaultFormatter
 	}
 }
+
+func init() {
+	runtimevar.DefaultURLMux().RegisterVariable(Scheme, &URLOpener{})
+}
+
+// Scheme is the URL scheme envvar registers its URLOpener under on runtimevar.DefaultURLMux.
+const Scheme = "env"
+
+// URLOpener opens envvar URLs like "env://VARIABLE".
+type URLOpener struct {
+}
+
+// OpenVariableURL opens the variable at the URL's path. See the package doc
+// for more details.
+func (o *URLOpener) OpenVariableURL(_ context.Context, u *url.URL) (*runtimevar.Variable, error) {
+	q := u.Query()
+	for param := range q {
+		return nil, fmt.Errorf("open variable %v: invalid query parameter %q", u, param)
+	}
+	return New(os.Getenv(u.Host)), nil
+}
+
+// New constructs a *runtimevar.Variable holding value.
+func New(value string) *runtimevar.Variable {
+	return runtimevar.New(&watcher{value: value, t: time.Now()})
+}
+
+// watcher implements driver.Watcher and driver.State.
+type watcher struct {
+	value string
+	err   error
+	t     time.Time
+}
+
+// Value implements driver.State.Value.
+func (w *watcher) Value() (interface{}, error) {
+	return w.value, w.err
+}
+
+// UpdateTime implements driver.State.UpdateTime.
+func (w *watcher) UpdateTime() time.Time {
+	return w.t
+}
+
+// As implements driver.State.As.
+func (w *watcher) As(any) bool { return false }
+
+// WatchVariable implements driver.WatchVariable.
+func (w *watcher) WatchVariable(ctx context.Context, prev driver.State) (driver.State, time.Duration) {
+	// The first time this is called, return the constant value.
+	if prev == nil {
+		return w, 0
+	}
+	// On subsequent calls, block forever as the value will never change.
+	<-ctx.Done()
+	w.err = ctx.Err()
+	return w, 0
+}
+
+// Close implements driver.Close.
+func (*watcher) Close() error { return nil }
+
+// ErrorAs implements driver.ErrorAs.
+func (*watcher) ErrorAs(error, any) bool { return false }
+
+// ErrorCode implements driver.ErrorCode
+func (*watcher) ErrorCode(error) gcerrors.ErrorCode {
+	return gcerrors.Unknown
+}
diff --git a/cmd/atlas/internal/migrate/migrate.go b/cmd/atlas/internal/migrate/migrate.go
index 5ad3667e..b0f95a69 100644
--- a/cmd/atlas/internal/migrate/migrate.go
+++ b/cmd/atlas/internal/migrate/migrate.go
@@ -296,7 +296,7 @@ var Formats = []string{FormatAtlas, FormatGolangMigrate, FormatGoose, FormatFlyw
 // Formatter returns the dir formatter for its URL.
 func Formatter(u *url.URL) (migrate.Formatter, error) {
 	switch f := u.Query().Get("format"); f {
-	case FormatAtlas:
+	case "", FormatAtlas:
 		return migrate.DefaultFormatter, nil
 	case FormatGolangMigrate:
 		return sqltool.GolangMigrateFormatter, nil
diff --git a/cmd/atlas/internal/migratelint/lint.go b/cmd/atlas/internal/migratelint/lint.go
index 24ecebb0..48774c6e 100644
--- a/cmd/atlas/internal/migratelint/lint.go
+++ b/cmd/atlas/internal/migratelint/lint.go
@@ -208,7 +208,7 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 	// Clean up after ourselves.
 	snap, ok := d.Dev.Driver.(migrate.Snapshoter)
 	if !ok {
-		return nil, errors.New("driver does not implement migrate.Snapshoter")
+		return nil, migrate.ErrSnapshotUnsupported
 	}
 	restore, err := snap.Snapshot(ctx)
 	if err != nil {
@@ -219,26 +219,7 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 			err = errors.Join(err, fmt.Errorf("restore dev-database snapshot: %w", err2))
 		}
 	}()
-	// Bring the dev environment to the base point. Skip to the first checkpoint, if there is one,
-	// assuming the history is replay-able before that point as this was tested in previous runs.
-	if i := migrate.FilesLastIndex(base, func(f migrate.File) bool {
-		ck, ok := f.(migrate.CheckpointFile)
-		return ok && ck.IsCheckpoint()
-	}); i != -1 {
-		base = base[i:]
-	}
-	for _, f := range base {
-		stmt, err := f.StmtDecls()
-		if err != nil {
-			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
-		}
-		for _, s := range stmt {
-			if _, err := d.Dev.ExecContext(ctx, s.Text); err != nil {
-				return nil, &FileError{File: f.Name(), Err: fmt.Errorf("executing statement: %w", err), Pos: s.Pos}
-			}
-		}
-	}
-	current, err := d.inspect(ctx)
+	current, err := d.base(ctx, base)
 	if err != nil {
 		return nil, err
 	}
@@ -279,6 +260,29 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 	return diff, nil
 }
 
+// base brings the dev environment to the base point and returns its state. It skips to the first checkpoint,
+// if there is one, assuming the history is replay-able before that point as this was tested in previous runs.
+func (d *DevLoader) base(ctx context.Context, base []migrate.File) (*schema.Realm, error) {
+	if i := migrate.FilesLastIndex(base, func(f migrate.File) bool {
+		ck, ok := f.(migrate.CheckpointFile)
+		return ok && ck.IsCheckpoint()
+	}); i != -1 {
+		base = base[i:]
+	}
+	for _, f := range base {
+		stmt, err := f.StmtDecls()
+		if err != nil {
+			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
+		}
+		for _, s := range stmt {
+			if _, err := d.Dev.ExecContext(ctx, s.Text); err != nil {
+				return nil, &FileError{File: f.Name(), Err: fmt.Errorf("executing statement: %w", err), Pos: s.Pos}
+			}
+		}
+	}
+	return d.inspect(ctx)
+}
+
 // next returns the next state of the database after executing the statements in
 // the file. The changes detected by the statements are attached to the file.
 func (d *DevLoader) next(ctx context.Context, f *sqlcheck.File, start *schema.Realm) (current *schema.Realm, err error) {
diff --git a/cmd/atlas/internal/migratelint/run.go b/cmd/atlas/internal/migratelint/run.go
index d6e5ed88..cadfbd57 100644
--- a/cmd/atlas/internal/migratelint/run.go
+++ b/cmd/atlas/internal/migratelint/run.go
@@ -120,7 +120,12 @@ func (r *Runner) summary(ctx context.Context) error {
 	r.sum.WriteSchema(r.Dev, diff)
 
 	// Analyze files.
-	for _, f := range diff.Files {
+	return r.analyze(ctx, diff.Files)
+}
+
+// analyze runs the analysis on the given files.
+func (r *Runner) analyze(ctx context.Context, files []*sqlcheck.File) error {
+	for _, f := range files {
 		var (
 			es []string
 			nl = nolintRules(f)
@@ -196,6 +201,10 @@ var (
 	{{- end }}
 {{- end -}}
 `))
+	// JSONTemplate is the JSON template used by CI wrappers.
+	JSONTemplate = template.Must(template.New("json").
+			Funcs(TemplateFuncs).
+			Parse("{{ json . }}"))
 )
 
 type (
diff --git a/doc/md/cloud/setup-ci.mdx b/doc/md/cloud/setup-ci.mdx
index d9e8f905..5fb9375e 100644
--- a/doc/md/cloud/setup-ci.mdx
+++ b/doc/md/cloud/setup-ci.mdx
@@ -13,23 +13,22 @@ schema is automatically reviewed and tested before it is deployed to production.
 
 In this guide, we'll show you how to set up CI for your database using Atlas Cloud.
 
-## Push or Pull Workflows
+## GitHub Actions or App Workflows
 
 There are two ways to set up CI for your database with Atlas Cloud:
 
-* **Pull (GitHub App)** - You install the Atlas Cloud GitHub App on your GitHub
-  repository. Atlas Cloud will automatically pull changes from your GitHub
-  repository, then run tests on them.
+* **GitHub Action** (recommended) - Install the [`ariga/atlas-action`](https://github.com/ariga/atlas-action)
+  GitHub Action on your GitHub repository and configure it to run on any pull request that modifies your database schema.
 
-  This is the fastest way to set Atlas Cloud up, but it does require that you
-  have the necessary permissions to install the GitHub App on your repository.
+  Note, this workflow does _not_ require you to grant Atlas Cloud any permissions on your repository. It also gives you
+  more control on how to set it up, and what [Dev Database](/concepts/dev-database) to use (e.g, PostGIS) for linting, change simulation, etc.
 
-* **Push (GitHub Action)** - You install the [`ariga/atlas-action`](https://github.com/ariga/atlas-action)
-  GitHub Action on your GitHub repository and configure it to run on any pull request
-  that modifies your database schema.
+* **GitHub App** - Install the Atlas Cloud GitHub App on your GitHub repository. Atlas Cloud will automatically pull
+  changes from your GitHub repository, then run tests on them.
+
+  This workflow is faster to set up, but it does require the necessary permissions to install the GitHub
+  App on your repository.
 
-  This is a bit more work to set up, but it does _not_ require you to grant Atlas
-  Cloud any permissions on your repository.
 
 ## Prerequisites
 
@@ -37,126 +36,16 @@ There are two ways to set up CI for your database with Atlas Cloud:
    and would like to experiment with this workflow, you can use the
    [`ariga/atlas-template`](https://github.com/ariga/atlas-template) template repo
    to create one for you.
-2. An [Atlas Cloud](https://atlasgo.cloud) account. If you don't have one, you can
-   [sign up for free](https://auth.atlasgo.cloud/signup).
-
-## Pull (GitHub App) Workflow
-
-### Step 1: Install the Atlas Cloud GitHub App
-
-After logging in to Atlas Cloud, create a new migration directory by clicking on
-"Migrations Directories" in the sidebar and then clicking on the "Add Directory"
-button.
-
-<details><summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-1.png)
-
-</details>
-
-Click "Connect with GitHub" to initiate the GitHub App installation flow.
-
-<details>
-<summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-2.png)
-</details>
-
-### Step 2: Connect Your Repository
-
-Once you've installed the GitHub App, you need to grant it access to
-the relevant repositories. Click on the "Organization" field and choose
-"Add Organization" from the dropdown:
-
-<details>
-<summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-3.png)
-</details>
-
-A popup requesting you to grant permission to specific repositories will appear.
-Select the repositories you want to grant access to, then click "Install and Authorize".
-
-<details>
-<summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-4.png)
-</details>
-
-### Step 3: Configure the Migration Directory
-
-Fill the rest of the form with the relevant configuration details:
-* **Directory Path** - The path to the directory containing your migrations relative to your repository root.
-* **Directory Name** - An identifier for the directory. This will be used to identify the directory in the Atlas Cloud and APIs.
-* **Migration Engine** - The format of the migrations in the directory. Atlas works best with migration directories in its
-  format, but also supports `golang-migrate`, Flyway, and Liquibase.
-* **Database** - the database engine for the target database.
-
-<details>
-<summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-5.png)
-</details>
-
-Click "Connect Directory" to finish the process.
-
-### Step 4: Test the Integration
-
-After connecting your directory, let's create a pull request with a destructive
-change to make sure that Atlas Cloud catches it.
-
-Create a new branch:
-
-```
-git checkout -b test-branch
-```
-
-Create a new migration file:
-
-```
-atlas migrate new --edit destructive
-```
-
-Add the following SQL to the migration file and save:
-
-```sql
-DROP TABLE posts;
-```
-
-Commit the changes and push them to GitHub:
-
-```
-git add .
-git commit -m "add destructive migration"
-git push origin test-branch
-```
-
-Create a pull request from the `test-branch` branch to your main branch.
-
-After a few seconds, Atlas Cloud will pick up the changes and run CI against
-them. Shortly after, you will see a comment on your pull request with the
-results of the CI run:
-
-<details>
-<summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/app-ci.png)
-</details>
+2. An [Atlas Cloud](https://atlasgo.cloud) account. If you don't have one, you can sign up for free in
+  [auth.atlasgo.cloud](https://auth.atlasgo.cloud/signup), or by running `atlas login` in your terminal:
 
+  ```shell
+  $ atlas login
+  //highlight-next-line-info
+  You are now connected to "a8m" on Atlas Cloud.
+  ```
 
-Amazing! Atlas Cloud caught the destructive change and prevented it from being
-accidentally merged to your main branch.
-
-To see a more detailed report of the CI run, click on the "Full Report on Atlas Cloud"
-link in the comment:
-
-<details>
-<summary>Screenshot Example</summary>
-
-![](https://atlasgo.io/uploads/cloud/ci-guide/app-cloud-report.png)
-</details>
-
-## Push (GitHub Action) Workflow
+## GitHub Action Workflow
 
 ### Step 1: Create a Bot Token for Atlas Cloud
 
@@ -376,6 +265,123 @@ and sync the migrations to Atlas Cloud on every push to `master`.
   ![](https://atlasgo.io/uploads/cloud/ci-guide/fixed-cloud-report-2.png)
   </details>
 
+
+## GitHub App Workflow
+
+### Step 1: Install the Atlas Cloud GitHub App
+
+After logging in to Atlas Cloud, create a new migration directory by clicking on
+"Projects" in the sidebar and then click on the "Add Directory"
+button.
+
+<details><summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-1.png)
+
+</details>
+
+Click "Connect with GitHub" to initiate the GitHub App installation flow.
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-2.png)
+</details>
+
+### Step 2: Connect Your Repository
+
+Once you've installed the GitHub App, you need to grant it access to
+the relevant repositories. Click on the "Organization" field and choose
+"Add Organization" from the dropdown:
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-3.png)
+</details>
+
+A popup requesting you to grant permission to specific repositories will appear.
+Select the repositories you want to grant access to, then click "Install and Authorize".
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-4.png)
+</details>
+
+### Step 3: Configure the Migration Directory
+
+Fill the rest of the form with the relevant configuration details:
+* **Directory Path** - The path to the directory containing your migrations relative to your repository root.
+* **Directory Name** - An identifier for the directory. This will be used to identify the directory in the Atlas Cloud and APIs.
+* **Migration Engine** - The format of the migrations in the directory. Atlas works best with migration directories in its
+  format, but also supports `golang-migrate`, Flyway, and Liquibase.
+* **Database** - the database engine for the target database.
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/create-dir-5.png)
+</details>
+
+Click "Connect Directory" to finish the process.
+
+### Step 4: Test the Integration
+
+After connecting your directory, let's create a pull request with a destructive
+change to make sure that Atlas Cloud catches it.
+
+Create a new branch:
+
+```
+git checkout -b test-branch
+```
+
+Create a new migration file:
+
+```
+atlas migrate new --edit destructive
+```
+
+Add the following SQL to the migration file and save:
+
+```sql
+DROP TABLE posts;
+```
+
+Commit the changes and push them to GitHub:
+
+```
+git add .
+git commit -m "add destructive migration"
+git push origin test-branch
+```
+
+Create a pull request from the `test-branch` branch to your main branch.
+
+After a few seconds, Atlas Cloud will pick up the changes and run CI against
+them. Shortly after, you will see a comment on your pull request with the
+results of the CI run:
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/app-ci.png)
+</details>
+
+
+Amazing! Atlas Cloud caught the destructive change and prevented it from being
+accidentally merged to your main branch.
+
+To see a more detailed report of the CI run, click on the "Full Report on Atlas Cloud"
+link in the comment:
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci-guide/app-cloud-report.png)
+</details>
+
 ## Summary
 
 In this guide, we've shown how to configure Atlas Cloud to apply continuous integration
diff --git a/doc/md/deployment/intro.md b/doc/md/deployment/intro.md
deleted file mode 100644
index ed5a4d8c..00000000
--- a/doc/md/deployment/intro.md
+++ /dev/null
@@ -1,87 +0,0 @@
----
-id: deployment
-slug: /deployment/intro
-title: Deployment
----
-
-## Introduction
-
-:::info
-This document pertains to [Atlas v0.3.7](https://github.com/ariga/atlas/releases/tag/v0.3.7) or earlier. Check out the
-new and improved UI in [Atlas Cloud](https://atlasgo.cloud).
-:::
-
-Teams using Atlas to manage their databases can benefit from deploying the Atlas Management UI to their
-cloud environment. The Management UI can be used to gain visibility of your team's database schemas as well
-as provide an intuitive interface for planning and running database migrations using Atlas's core engine.
-In addition, the Management UI automatically tracks and audits all changes using the "Activity & History" feature.
-
-The Atlas Management UI is designed to be a long-running process.
-It is therefore recommended running it against a persistent database that will make sure your team's schemas,
-database credentials, migration history and more are not lost in case you need to restart the service.
-Atlas uses [tink](https://developers.google.com/tink), a battle-tested encryption library created at Google, to encrypt all sensitive information.
-Following recommendations from the developers of tink, Atlas uses [AEAD](https://developers.google.com/tink/aead?hl=en)
-encryption with an AES256_GCM type key.
-
-In this section we will review two deployment and usage options:
-1. Deploying on a VM/EC2 instance with persistent storage.
-2. Deploying using an official Atlas Docker container.
-
-### Persistent Storage
-
-When serving Atlas on a VM/EC2, you will need to decide on which persistent storage you would like Atlas to store the
-configuration data.
-For now, you can choose any of the supported databases:
-1. SQLite (file) (in-memory is also supported but does not require encryption)
-2. MySQL
-3. TiDB
-4. MariaDB
-5. PostgresDB
-
-An example for serving Atlas with MySQL persistent storage:
-```
-atlas serve --storage "mysql://root:pass@localhost:3306/atlas"
-```
-On your first run, Atlas will generate an encryption key and store it under ```$HOME/.atlas/keyset.json```.
-This (private) file should not be deleted, otherwise Atlas will not be able to restore your configuration data.
-
-### Docker
-
-Atlas has an official docker that is updated with the latest and tagged versions,
-which can be found on [Docker Hub](https://hub.docker.com/r/arigaio/atlas).
-You can use this image to run the CLI and to serve the Atlas UI.
-
-To run Atlas in served and persisted mode inside a container:
-```
-docker run -v $HOME/.atlas/keyset.json:/root/.atlas/keyset.json -p 5800:5800 arigaio/atlas:latest serve --addr :5800 --storage "mysql://root:pass@tcp(host.docker.internal:3306)/atlas"
-```
-
-Let's review this command step by step:
-1. ```docker run``` - running a command.
-2. ```-v $HOME/.atlas/keyset.json:/root/.atlas/keyset.json``` binds a secret key from a persisted host into the docker container. If you don't have a key, use an empty file and Atlas will generate one for you.
-3. ``` -p 5800:5800``` binds an exposed serving port for connecting with a web client.
-4. ```5800:5800 arigaio/atlas:latest``` use the latest atlas image, for a versioned release use a specific tag such as ```arigaio/atlas:0.3.6```.
-5. ```serve --addr :5800 --storage "mysql://root:pass@tcp(host.docker.internal:3306)/atlas"``` serve Atlas on port 5800 with a persistent MySQL Database.
-
-### Encryption
-
-On its first run, Atlas generates a keyset.json file under `$HOME/.atlas/` containing
-an encryption keyset for you. Do not lose this file! Without this file you cannot later use
-any database credentials that save to Atlas.
-
-If you want to generate this key yourself, you can
-[install Tinkey](https://developers.google.com/tink/install-tinkey), Tink's official CLI,
-and use it to generate a keyset:
-
-```shell
-brew tap google/tink https://github.com/google/tink
-brew install tinkey
-tinkey create-keyset --key-template AES256_GCM --out ~/.atlas/keyset.json
-```
-
-Alternatively, you can use [rotemtam/tinkey](https://hub.docker.com/r/rotemtam/tinkey), an unofficial Docker image that
-wraps the official binary distribution:
-
-```shell
-docker run --rm rotemtam/tinkey create-keyset --key-template AES256_GCM > ~/.atlas/keyset.json
-```
diff --git a/doc/md/guides/ci-platforms/gitlab.mdx b/doc/md/guides/ci-platforms/gitlab.mdx
new file mode 100644
index 00000000..9150a433
--- /dev/null
+++ b/doc/md/guides/ci-platforms/gitlab.mdx
@@ -0,0 +1,205 @@
+---
+id: gitlab
+title: CI for Databases on GitLab
+slug: /guides/ci-platforms/gitlab
+---
+
+import InstallationInstructions from '../../components/_installation_instructions.mdx'
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+![](https://atlasgo.io/uploads/gitlab/gitlab-ci-edited.png)
+
+GitLab is a popular, open-source alternative to GitHub.  In addition to a self-hosted version,
+GitLab also offers a hosted version at [gitlab.com](https://gitlab.com). Similar to GitHub, GitLab offers
+users storage for Git repositories, issue tracking, and CI/CD pipelines.
+
+In this guide we will demonstrate how to use [GitLab CI](https://docs.gitlab.com/ee/ci/) and Atlas to setup CI
+pipelines for your database schema changes.
+
+## Prerequisites
+
+- A GitLab project with an [Atlas migration directory](/versioned/intro).
+
+## Setup
+
+### Install Atlas
+
+<InstallationInstructions />
+
+### Login to Atlas
+
+After installing Atlas locally, you will need to login to your Atlas account.  If you don't have an account yet,
+you will be prompted to create one for free during the login process.
+
+You can do this by running the following command:
+
+```bash
+atlas login
+```
+
+### Push your migrations to Atlas
+
+Once you have logged in, you can push your migrations to Atlas by running the following command:
+
+<Tabs>
+<TabItem value="postgres" label="PostgreSQL">
+
+```bash
+atlas migrate push app \
+  --dev-url "docker://postgres/15/dev?search_path=public"
+```
+
+</TabItem>
+<TabItem value="mysql" label="MySQL">
+
+```bash
+atlas migrate push app \
+  --dev-url "docker://mysql/8/dev"
+```
+
+</TabItem>
+</Tabs>
+
+Atlas will print a URL leading to your migrations on Atlas Cloud.  You can visit this URL to view your migrations.
+
+### Create a Bot Token
+
+To connect GitLab CI to Atlas, you will need to create a bot token.  To learn how to create a bot token, please
+visit the [Bot Tokens](/cloud/bots) guide.
+
+## Setup GitLab CI
+
+With your migrations pushed to Atlas, you can now setup GitLab CI to run your migrations on every commit.
+
+### Create a variable for your bot token
+
+In order to connect GitLab CI to Atlas, you will need to [create a variable](https://docs.gitlab.com/ee/ci/variables/#for-a-project)
+for your bot token.  To do this:
+1. Go to your projects *Settings* page.
+2. Click on the *CI/CD* tab.
+3. Expand the *Variables* tab.
+4. Click "Add Variable".
+5. Use `ATLAS_CLOUD_TOKEN` as the variable key and paste the bot token you created earlier as the variable value.
+6. Set the "Mask Variable" checkbox to true to prevent the token from being printed in the CI logs.
+7. Unset the "Protect Variable" checkbox so that this job can be run on all branches (necessary for running on merge requests).
+
+### Create a `.gitlab-ci.yml` file
+
+Next, you will need to create a `.gitlab-ci.yml` file in the root of your repository.  This file will tell GitLab CI
+how to run your Atlas CI pipeline.
+
+```yaml
+image: ubuntu:latest
+
+stages:
+  - lint
+  - push
+
+before_script:
+  - apt-get update -q && apt-get install -y curl
+  - 'which atlas || (curl -sSf https://atlasgo.sh | sh)'
+  - atlas login --token $ATLAS_CLOUD_TOKEN
+
+lint migrations:
+  stage: lint
+  rules:
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+      when: always
+    - if: '$CI_MERGE_REQUEST_ID'
+      changes:
+        - migrations/*
+      when: always
+      allow_failure: false
+  script:
+    - |
+      if [ -n "$CI_MERGE_REQUEST_IID" ]; then
+        URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
+      else
+        URL="${CI_PROJECT_URL}/-/commit/${CI_COMMIT_SHA}"
+      fi
+
+      ATLAS_CONTEXT=$(cat <<EOF
+      {
+        "repo": "$CI_PROJECT_PATH",
+        "branch": "$CI_COMMIT_REF_NAME",
+        "commit": "$CI_COMMIT_SHA",
+        "url": "$URL"
+      }
+      EOF
+      )
+    - atlas migrate lint --base atlas://gitlab -w --dev-url "sqlite://demo?mode=memory" --context "$ATLAS_CONTEXT"
+
+push migrations:
+  stage: push
+  rules:
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+  script:
+    - |
+      ATLAS_CONTEXT=$(cat <<EOF
+      {
+        "repo": "$CI_PROJECT_PATH",
+        "branch": "$CI_COMMIT_REF_NAME",
+        "commit": "$CI_COMMIT_SHA",
+        "url": "${CI_PROJECT_URL}"
+      }
+      EOF
+      )
+    - atlas migrate push gitlab --dev-url "sqlite://demo?mode=memory" --context "$ATLAS_CONTEXT"
+```
+
+Let's break down what this file is doing:
+1. The `image` directive tells GitLab CI to use the `ubuntu:latest` Docker image to run your CI jobs.
+2. The `stages` directive defines the stages that your CI pipeline will run.  In this case, we have two stages:
+   `lint` and `push`. In the `lint` stage, we will run the `atlas migrate lint` command to verify that our migrations
+   are valid and safe. In the `push` stage, we will run the `atlas migrate push` command to push our migrations to Atlas.
+3. The `before_script` directive defines a set of commands that will be run before each job.  In this case, we are
+   installing the `atlas` CLI and logging into Atlas using the bot token we created earlier.
+4. The `lint migrations` job will run the `atlas migrate lint` command.  This job will run on all commits to your default
+   branch and on all merge requests.  This job will fail if the migrations are invalid.
+5. The `push migrations` job will run the `atlas migrate push` command.  This job will only run on commits to your default
+    branch.
+
+### Commit and push your changes
+
+Now that you have created your `.gitlab-ci.yml` file, you can commit and push your changes to GitLab.
+
+From this point on, whenever you make a change to your migrations and push them to GitLab, GitLab CI will run your
+Atlas CI pipeline to verify their safety. To view a detailed analysis of your schema changes, look at the logs for
+a direct link to your Atlas Cloud account:
+```text
+$ which atlas || (curl -sSf https://atlasgo.sh | sh)
+Downloading https://release.ariga.io/atlas/atlas-linux-amd64-latest
+Installation successful!
+
+$ atlas login --token $ATLAS_CLOUD_TOKEN
+You are now connected to "rotemtam85" on Atlas Cloud.
+
+$ atlas migrate lint --base atlas://gitlab -w --dev-url "sqlite://demo?mode=memory" --context "$ATLAS_CONTEXT"
+// highlight-next-line
+https://rotemtam85.atlasgo.cloud/ci-runs/8589934616
+Cleaning up project directory and file based variables
+```
+
+Once you merge your changes to your default branch, the pipeline will
+push your latest changes to Atlas, so they can be deployed. Similarly, to get a visual
+representation of your database schema, follow the link from the logs to Atlas Cloud:
+
+```text
+$ which atlas || (curl -sSf https://atlasgo.sh | sh)
+Downloading https://release.ariga.io/atlas/atlas-linux-amd64-latest
+Installation successful!
+
+$ atlas login --token $ATLAS_CLOUD_TOKEN
+You are now connected to "rotemtam85" on Atlas Cloud.
+
+$ atlas migrate push gitlab --dev-url "sqlite://demo?mode=memory" --context "$ATLAS_CONTEXT"
+// highlight-next-line
+https://rotemtam85.atlasgo.cloud/dirs/4294967302
+```
+
+## Next Steps: Deployments
+
+Congratulations! You have successfully setup GitLab CI to lint and push your migration
+directory to Atlas Cloud. The next step is to deploy your migrations to your production database
+directly from Atlas Cloud using the [CLI](/guides/deploying/from-cloud).
diff --git a/doc/md/guides/deploying/k8s-argo.md b/doc/md/guides/deploying/k8s-argo.md
index 0c3042e6..adbcb230 100644
--- a/doc/md/guides/deploying/k8s-argo.md
+++ b/doc/md/guides/deploying/k8s-argo.md
@@ -400,10 +400,10 @@ kubectl get -n argocd applications.argoproj.io atlas-argocd-demo -o=jsonpath='{r
 `kubectl` will print something like this:
 
 ```
-Service: 	     mysql 	 (Synced) 	 ({"status":"Healthy"})
-Deployment: 	 mysql 	 (Synced) 	 ({"status":"Healthy"})
-Deployment: 	 nginx 	 (Synced) 	 ({"status":"Healthy"})
-AtlasSchema: 	 myapp 	 (Synced) 	 ({"message":"The schema has been applied successfully. Apply response: {\"Changes\":{}}","status":"Healthy"})%
+Service:       mysql   (Synced)    ({"status":"Healthy"})
+Deployment:    mysql   (Synced)    ({"status":"Healthy"})
+Deployment:    nginx   (Synced)    ({"status":"Healthy"})
+AtlasSchema:   myapp   (Synced)    ({"message":"The schema has been applied successfully. Apply response: {\"Changes\":{}}","status":"Healthy"})%
 ```
 
 Finally, on the ArgoCD UI we can see the health and dependency and status of all the resources:
diff --git a/doc/md/guides/deploying/k8s-flux.md b/doc/md/guides/deploying/k8s-flux.md
new file mode 100644
index 00000000..98588c71
--- /dev/null
+++ b/doc/md/guides/deploying/k8s-flux.md
@@ -0,0 +1,573 @@
+---
+id: k8s-flux
+title: Deploying to Kubernetes with the Atlas Operator and Flux CD
+slug: /guides/deploying/k8s-flux
+---
+
+[GitOps](https://www.gitops.tech/) is a software development and deployment methodology that uses Git as the central repository
+for both code and infrastructure configurations, enabling automated and auditable deployments.
+
+[FluxCD](https://fluxcd.io/) is a Continuous Delivery tool that implements GitOps principles. It uses a declarative approach
+to keep Kubernetes clusters in sync with sources of configuration (like Git repositories), and automates 
+configuration updates when there is new code to deploy.
+
+[Kubernetes Operators](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) are software extensions to Kubernetes that enable the automation and management 
+of complex, application-specific operational tasks and domain-specific knowledge within a Kubernetes cluster.
+
+In this guide, we will demonstrate how to use the [Atlas Kubernetes Operator](/integrations/kubernetes/operator) and Flux CD to achieve a 
+GitOps-based deployment workflow for your database schema.
+
+## Prerequisites
+
+* A running Kubernetes cluster  - For learning purposes, you can use 
+ [Minikube](https://minikube.sigs.k8s.io/docs/start/), which is a tool that runs a single-node
+ Kubernetes cluster inside a VM on your laptop.
+* [kubectl](https://kubernetes.io/docs/tasks/tools/) - a command-line tool for interacting with Kubernetes clusters.
+* [Helm](https://helm.sh/docs/intro/install/) - a package manager for Kubernetes.
+
+## High-level architecture
+
+Before we dive into the details of the deployment flow, let’s take a look at the high-level 
+architecture of our application.
+
+![Application Architecture](https://atlasgo.io/uploads/k8s/argocd/app-diagram.png)
+
+On a high level, our application consists of the following components:
+
+1. A backend application - in our example we will use a plain NGINX server
+   as a placeholder for a real backend application.
+2.  A database - in our example we will use a MySQL pod for the database. In a more realistic scenario,
+    you might want to use a managed database service like AWS RDS or GCP Cloud SQL.
+3. An `AtlasSchema`  Custom Resource that defines the database schema and is managed by the Atlas Operator.
+
+In our application architecture, we have a database that is connected to our application and managed 
+using Atlas CR (Custom Resource). The database plays a crucial role in storing and retrieving
+data for the application, while the Atlas CR provides seamless integration and management of the 
+database schema within our Kubernetes environment.
+
+## Incorporating schema changes into a GitOps flow 
+
+### Databases should be migrated before the application is deployed
+
+Integrating GitOps practices with a database in our application stack poses a unique challenge. 
+
+Flux CD provides a declarative approach to GitOps, allowing us to define a Flux CD application and
+effortlessly handle the synchronization process. When pushing changes to the database schema or application
+code to the Git repository, Flux CD automatically syncs those changes to the Kubernetes cluster.
+
+However, as we discussed in the introduction, ensuring the proper order of deployments is critical. 
+In our scenario, the database deployment must succeed before rolling out the application to guarantee its
+functionality. If the database deployment encounters an issue, it is essential to address it before
+proceeding with the application deployment. 
+
+### Flux CD Dependencies 101
+
+Flux CD supports [Dependencies](https://fluxcd.io/flux/components/kustomize/kustomizations/#dependencies),
+via `.spec.dependsOn`, a mechanism used to orchestrate multiple deployments in a specific ordered sequence to ensure certain resources 
+are healthy before subsequent resources are synced/reconciled.
+
+By using `.spec.dependsOn`, you can define the apply order and thus determine the sequence of manifest
+applications. `.spec.dependsOn` is used to refer to other Kustomization objects that the Kustomization 
+depends on. If specified, then the Kustomization is only applied after the referred Kustomizations 
+are ready, i.e. have the `Ready` condition marked as `True`. The readiness state of a Kustomization
+is determined by its last applied status condition.
+
+For example, let's assume we have a scenario where our application is comprised of two services, a `backend` 
+service and a `database` service. The `backend` service depends on the `database` service, and we want
+to ensure that the `database` service is ready before the `backend` service is applied. We can codify this
+dependency in the following way:
+
+```yaml
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: database
+  namespace: flux-system
+spec:
+  interval: 5m
+  path: "./kustomize"
+  prune: true
+  sourceRef:
+  kind: GitRepository
+  name: flux-system
+  // highlight-start
+  healthChecks:
+  - apiVersion: apps/v1
+    kind: Deployment
+    name: mysql
+    namespace: default
+  // highlight-end
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: backend
+  namespace: flux-system
+spec:
+  // highlight-start
+  dependsOn:
+  - name: mysql
+  // highlight-end
+  interval: 5m
+  path: "./kustomize"
+  prune: true
+  sourceRef:
+  kind: GitRepository
+  name: flux-system
+```
+
+In this manifest, `.spec.healthChecks` is used to refer to resources for which the Flux
+controller will perform health checks. This is used to determine the rollout status of
+deployed workloads and the `Ready` status of custom resources.
+
+This is helpful when there is a need to make sure other resources exist before the workloads
+defined in a Kustomization are deployed. To ensure that database resources are created and applied 
+before our application, we will utilize Flux CD `dependsOn` and `health checks` feature.
+
+With the theoretical background out of the way, let’s take a look at a practical example of how 
+to deploy an application with Flux CD and the Atlas Operator.
+
+## Installation
+
+### 1. Install the Atlas Operator
+
+To install the `Atlas Operator` run the following command:
+
+```bash
+helm install atlas-operator oci://ghcr.io/ariga/charts/atlas-operator
+```
+
+`Helm` will print something like this:
+
+```bash
+Pulled: ghcr.io/ariga/charts/atlas-operator:0.3.0
+ Digest: sha256:4dfed310f0197827b330d2961794e7fc221aa1da1d1b95736dde65c090e6c714
+ NAME: atlas-operator
+ LAST DEPLOYED: Tue Jun 27 16:58:30 2023
+ NAMESPACE: default
+ STATUS: deployed
+ REVISION: 1
+ TEST SUITE: None
+```
+
+Wait until the `atlas-operator` pod is running:
+
+```bash
+kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=atlas-operator -n default
+```
+
+`kubectl` will print something like this:
+
+```bash
+pod/atlas-operator-866dfbc56d-qkkkn condition met
+```
+
+### 2. Install the Flux CLI
+
+The flux command-line interface (CLI) is used to bootstrap and interact with Flux.
+
+To install it on  macOS or Linux, run:
+
+```bash
+curl -s https://fluxcd.io/install.sh | sh
+```
+
+To install the CLI with [Chocolatey](https://chocolatey.org/) for Windows, run:
+
+```bash
+choco install flux
+```
+
+Check you have everything needed to run Flux by running the following command:
+
+```bash
+flux check --pre
+```
+
+The output is similar to:
+
+```bash
+► checking prerequisites
+✔ kubernetes 1.26.3 >=1.23.0
+✔ prerequisites checks passed
+```
+
+### 3. Create a GitHub Personal Access Token
+
+The GitHub personal access token will be used in place of a password when authenticating to GitHub in the command line or with the API.
+
+1. In the upper-right corner of any page, click your profile photo, then click `Settings`.
+2. In the left sidebar, click  `< > Developer settings`.
+3. In the left sidebar, under  `Personal access tokens`, click `Tokens (classic)``.
+4. Select `Generate new token`, then click `Generate new token (classic)``.
+5. In the `"Note"`` field, give your token a descriptive name.
+6. To give your token an expiration, select `Expiration`, then choose a default option or click `Custom` to enter a date.
+7. Select the scopes you'd like to grant this token. To use your token to access repositories from the command line, select `repo`. A token with no assigned scopes can only access public information.
+8. Click `Generate token`.
+9. Copy the new token to your clipboard.
+
+### 4. Export your GitHub Credentials
+
+Export your GitHub personal access token and username:
+
+```bash
+export GITHUB_TOKEN=<your-token>
+export GITHUB_USER=<your-username>
+```
+
+### 5. Install Flux onto your Kubernetes Cluster
+
+Run the bootstrap command:
+
+```bash
+flux bootstrap github \
+  --owner=$GITHUB_USER \
+  --repository=flux-infrastructure \
+  --branch=main \
+  --path=./clusters/dev \
+  --personal
+```
+
+You will be prompted to enter your GitHub personal access token. The output is similar to:
+
+```bash
+► connecting to github.com
+✔ repository created
+✔ repository cloned
+✚ generating manifests
+✔ components manifests pushed
+► installing components in flux-system namespace
+deployment "source-controller" successfully rolled out
+deployment "kustomize-controller" successfully rolled out
+deployment "helm-controller" successfully rolled out
+deployment "notification-controller" successfully rolled out
+✔ install completed
+► configuring deploy key
+✔ deploy key configured
+► generating sync manifests
+✔ sync manifests pushed
+► applying sync manifests
+◎ waiting for cluster sync
+✔ bootstrap finished
+```
+
+Using the flux bootstrap command, you can install Flux on a Kubernetes cluster and configure it to manage itself 
+from a Git repository. The bootstrap command above does the following:
+
+- Creates a git repository flux-infrastructure on your GitHub account.
+- Adds Flux component manifests to the repository.
+- Deploys Flux Components to your Kubernetes Cluster.
+- Configures Flux components to track the path /clusters/dev/ in the repository.
+
+## Deploy the Sample Application on your Cluster
+
+In this example, we're using the `jmushiri/atlas-flux-demo` repository, which contains all of the
+Kubernetes manifests necessary to deploy our application.
+
+### 1. Fork the Sample Application
+
+To get started, you need to fork and then clone the sample application repository to your local machine.
+1. Open your web browser and go to the [rotemtam/atlas-operator-flux-demo](https://github.com/jmushiri/atlas-flux-demo)
+   repo on GitHub.
+2. Click on the `"Fork"` button in the top-right corner of the GitHub page. This will create a copy
+   of the repository under your GitHub account.
+ 
+### 2. Clone the Sample Application
+
+Once the forking process is complete, you will be redirected to your own forked repository. Open your terminal 
+or command prompt and run the following command to clone the forked repository to your local machine:
+
+```shell
+git clone https://github.com/$GITHUB_USER/atlas-operator-flux-demo.git
+```
+
+Observe this repository's structure:
+
+```text
+.
+├── LICENSE
+├── README.md
+└── kustomize
+    ├── app.yaml
+    ├── db.yaml
+    ├── kustomization.yaml
+    └── schema.yaml
+```
+
+* `kustomize/app.yaml` - contains the `Kustomization` manifest for the application, in our case, a dummy
+   nginx server.
+* `kustomize/db.yaml` - contains the `Kustomization` manifest for the database, in our case, a MySQL pod, a service
+   that exposes it, and a secret that contains the database credentials.
+* `kustomize/schema.yaml` - contains the `AtlasSchema` manifest that defines the database schema.
+* `kustomize/kustomization.yaml` - contains the `Kustomization` manifest that defines the different resources
+   that are managed by this `Kustomization`.
+
+### 3. Clone the Infra Repository
+
+Clone the `flux-infrastructure` repository to your local machine:
+ 
+```bash
+git clone https://github.com/$GITHUB_USER/flux-infrastructure
+ 
+cd flux-infrastructure
+```
+
+### 4. Add the Repository (atlas-flux-demo repository) to Flux
+ 
+Create a GitRepository manifest pointing to `atlas-flux-demo` repository’s main branch:
+
+```bash
+flux create source git atlas-operator-flux-demo --url=https://github.com/$GITHUB_USER/atlas-operator-flux-demo --branch=main --interval=30s --export > ./clusters/dev/atlas-operator-flux-demo-source.yaml
+```
+
+The output is similar to:
+
+```yaml
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: GitRepository
+metadata:
+  name: atlas-operator-flux-demo
+  namespace: flux-system
+spec:
+  interval: 30s
+  ref:
+    branch: main
+  url: https://github.com/$GITHUB_USER/atlas-operator-flux-demo
+```
+
+Commit and push the `atlas-flux-demo-source.yaml` file to the `flux-infrastructure` repository:
+
+```bash
+git add -A && git commit -m "Add atlas-flux-demo GitRepository"
+ 
+git push
+```
+
+## Implement a Continuous Deployment (CD) Flow
+
+It's time to configure Flux to build and apply the kustomize directory located in the `atlas-operator-flux-demo` repository.
+ 
+Use the `flux create` command to create a `Kustomization` that applies the `atlas-operator-flux-demo` deployment.
+
+```bash
+flux create kustomization atlas-flux-demo \
+  --target-namespace=default \
+  --source=atlas-operator-flux-demo \
+  --path="./kustomize" \
+  --prune=true \
+  --interval=5m \
+  --export > ./clusters/dev/atlas-operator-flux-demo-kustomization.yaml
+```
+
+The output is similar to:
+
+```yaml
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: atlas-flux-demo
+  namespace: flux-system
+spec:
+  interval: 5m0s
+  path: ./kustomize
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: atlas-operator-flux-demo
+  targetNamespace: default
+```
+
+The structure of the `flux-infrastructure` repo should be similar to:
+
+```bash
+flux-infrastructure
+└── clusters/
+    └── dev/
+        ├── flux-system/                        
+        │   ├── gotk-components.yaml
+        │   ├── gotk-sync.yaml
+        │   └── kustomization.yaml
+        ├── atlas-flux-demo-kustomization.yaml
+        └── atlas-flux-demo-source.yaml
+```
+
+## Implement the Deployment Flow
+
+To implement the deployment flow in a specific ordered sequence, we will use Flux CD’s `.spec.dependsOn`
+and `.spec.healthChecks` features.
+
+Edit the `atlas-operator-flux-demo-kustomization.yaml` file as follows:
+
+```yaml
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: mysql
+  namespace: flux-system
+spec:
+  interval: 30s
+  path: ./kustomize
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: atlas-operator-flux-demo
+  targetNamespace: default
+  // highlight-start
+  healthChecks:
+    - apiVersion: apps/v1
+      kind: Deployment
+      name: mysql
+      namespace: default
+  // highlight-end
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: app-schema
+  namespace: flux-system
+spec:
+  // highlight-start
+  dependsOn:
+    - name: mysql
+  // highlight-end
+  interval: 30s
+  path: ./kustomize
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: atlas-operator-flux-demo
+  targetNamespace: default
+  // highlight-start
+  healthChecks:
+    - apiVersion: db.atlasgo.io/v1alpha1
+      kind: AtlasSchema
+      name: app-schema
+      namespace: default
+  // highlight-end
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: nginx
+  namespace: flux-system
+spec:
+  // highlight-start
+  dependsOn:
+    - name: app-schema
+  // highlight-end
+  interval: 30s
+  path: ./kustomize
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: atlas-operator-flux-demo
+  targetNamespace: default
+```
+
+Notice the highlighted sections in the manifest above: 
+* We define health checks for the `mysql` deployment and the `app-schema` Atlas CR. This instructs Flux CD
+  explicitly when to consider each of these resources as ready.
+* We declare the dependency of the `app-schema` Atlas CR on the `mysql` deployment. This ensures that the
+  `mysql` deployment is ready before the `app-schema` Atlas CR is applied. In addition, we declare the dependency
+  of the `nginx` deployment on the `app-schema` Atlas CR. This ensures that the `app-schema` Atlas CR is ready
+  before the `nginx` deployment is applied.
+
+Together, these declarations achieve our requirement of ensuring that the database schema is applied before
+the application is deployed.  
+
+Commit and push the `Kustomization` manifest to the repository:
+
+```bash
+git add -A && git commit -m "Add atlas-flux-demo Kustomization"
+ 
+git push
+```
+
+## Watch Flux sync the Application 
+
+Use the `flux get` command to watch the deployment flow.
+
+```bash
+flux get kustomizations --watch  
+```
+
+This command allows you to fetch and observe the status of `Kustomize` resources managed by Flux in your
+Kubernetes cluster, with real-time updates as changes are made.
+
+To check whether the schema migrations have been successfully applied, run:
+
+```bash
+kubectl exec -it $(kubectl get pods -l app=mysql -o jsonpath='{.items[0].metadata.name}') -- mysql -uroot -ppass -e "describe example.users"
+```
+
+Our schema migrations have been successfully applied:
+
+```text
++-----------+--------------+------+-----+---------+----------------+
+| Field     | Type         | Null | Key | Default | Extra          |
++-----------+--------------+------+-----+---------+----------------+
+| id        | int          | NO   | PRI | NULL    | auto_increment |
+| name      | varchar(255) | NO   |     | NULL    |                |
+| email     | varchar(255) | NO   | UNI | NULL    |                |
+| short_bio | varchar(255) | NO   |     | NULL    |                |
++-----------+--------------+------+-----+---------+----------------+
+```
+
+### Make a change to the Application Database Schema
+
+To show how the continuous deployment flow works, let's make a change to the database schema.
+
+Open the `kustomize/schema.yaml` file and add a column to the `users` table in the `AtlasSchema` manifest:
+
+```yaml
+apiVersion: db.atlasgo.io/v1alpha1
+kind: AtlasSchema
+metadata:
+  name: app-schema
+spec:
+  url: mysql://root:pass@mysql:3306/example
+  schema:
+    sql: |
+      create table users (
+        id int not null auto_increment,
+        name varchar(255) not null,
+        email varchar(255) unique not null,
+        short_bio varchar(255) not null,
+        // highlight-next-line
+        new_col varchar(255) not null,
+        primary key (id)
+      );
+```
+
+Commit and push the change to the repository:
+
+```bash
+git add kustomize/schema.yaml
+git commit -m "Add new column to users table"
+git push
+```
+
+Next, let's wait for Flux to sync the changes, and check that our schema migrations have been successfully applied:
+
+```text
++-----------+--------------+------+-----+---------+----------------+
+| Field     | Type         | Null | Key | Default | Extra          |
++-----------+--------------+------+-----+---------+----------------+
+| id        | int          | NO   | PRI | NULL    | auto_increment |
+| name      | varchar(255) | NO   |     | NULL    |                |
+| email     | varchar(255) | NO   | UNI | NULL    |                |
+// highlight-next-line
+| short_bio | varchar(255) | NO   |     | NULL    |                |
+| new_col   | varchar(255) | NO   |     | NULL    |                |
++-----------+--------------+------+-----+---------+----------------+
+```
+Amazing, our schema migrations have been successfully applied!
+
+## Conclusion
+
+In this guide, we demonstrated how to use Flux CD to deploy an application that uses the
+Atlas Operator to manage the lifecycle of a database schema. We also showed how to use Flux
+dependency management to ensure that the schema changes were successfully applied before 
+deploying the application.
diff --git a/doc/md/guides/guides.mdx b/doc/md/guides/guides.mdx
index ea95d6e9..5a319bc6 100644
--- a/doc/md/guides/guides.mdx
+++ b/doc/md/guides/guides.mdx
@@ -47,6 +47,12 @@ import { Card, CardImage } from "../components/card";
     url="/guides/deploying/k8s-argo"
     image={CardImage.Deployment}
   />
+  <Card
+    name="Kubernetes with Flux CD"
+    description="Use the Atlas Kubernetes Operator and Flux CD"
+    url="/guides/deploying/k8s-flux"
+    image={CardImage.Deployment}
+  />
   <Card
     name="CloudSQL with GitHub Actions"
     description="Deploying to CloudSQL with GitHub Actions"
diff --git a/doc/md/integrations/github-actions.mdx b/doc/md/integrations/github-actions.mdx
index 88fac5c6..bacafb42 100644
--- a/doc/md/integrations/github-actions.mdx
+++ b/doc/md/integrations/github-actions.mdx
@@ -25,6 +25,7 @@ database schema management tasks.
 | [ariga/atlas-action](https://github.com/ariga/atlas-action)        | CI for schema changes                               |
 | [ariga/atlas-sync-action](https://github.com/ariga/atlas-sync-action)   | Sync your migration directory to Atlas Cloud (atlasgo.cloud) |
 | [ariga/atlas-deploy-action](https://github.com/ariga/atlas-deploy-action) | Deploy versioned migrations from GitHub Actions    |
+| [ariga/setup-atlas](https://github.com/ariga/setup-atlas) | Install Atlas from a GitHub Actions workflow    |
 
 
 ## `ariga/atlas-action` (CI)
@@ -379,4 +380,33 @@ Configure this action with the following inputs:
 
 #### `cloud-token`
 
-**Required** The Atlas Cloud token to use for authentication.
\ No newline at end of file
+**Required** The Atlas Cloud token to use for authentication.
+
+## `ariga/setup-atlas`
+
+The [ariga/setup-atlas](https://github.com/ariga/setup-atlas) action can be used to install Atlas from a GitHub Actions workflow.
+
+This is useful if your team wants to use Atlas via scripts or when you want to use features that are not exposed by other actions.
+
+### Example
+
+```yaml
+name: Deploy schema changes
+run-name: I'm using Atlas 🚀
+on: [push]
+jobs:
+  use-atlas:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: ariga/setup-atlas@master
+      - run: atlas version
+      # - run: atlas schema apply ... 
+```
+
+### Reference
+
+The action supports the `version` input:
+
+#### `version`
+
+By default, the latest version of Atlas is being used. If you want to lock a specific version of Atlas, you can do so by specifing the version parameter.
diff --git a/doc/md/integrations/kubernetes/operator.mdx b/doc/md/integrations/kubernetes/operator.mdx
index bf0ad320..e05e6001 100644
--- a/doc/md/integrations/kubernetes/operator.mdx
+++ b/doc/md/integrations/kubernetes/operator.mdx
@@ -217,6 +217,7 @@ spec:
       sslmode: disable
   # ... rest of the resource
 ```
+Note: `hostFrom` and `userFrom` are also supported to load the host and user from another resource.
 
 ## Declarative migrations
 
diff --git a/doc/md/lint/analyzers.mdx b/doc/md/lint/analyzers.mdx
index 2191c956..0f209f0f 100644
--- a/doc/md/lint/analyzers.mdx
+++ b/doc/md/lint/analyzers.mdx
@@ -24,6 +24,27 @@ safety of migration scripts.
 
 Below are the `Analyzer` implementations currently supported by Atlas.
 
+### Non-Linear Changes
+
+Non-additive changes, often referred to as non-linear changes, are changes to the migration directory that are not added
+in a sequential order. This is a bit like the linear history in version control systems, where migration files are
+commits and the migration directory is the repository. Let's explain with three examples why ensuring the linearity of the
+migration directory is important:
+
+- When a developer introduces a new migration file without having the **latest state** of the directory, there is a risk
+  of generating an incorrect file that might conflict with the actual schema and cause a failure during deployment.
+- When a developer merges a feature branch to the main branch with a new migration file that is not positioned at the
+  end of the directory, there is a risk that Atlas will skip this migration file, as there might be some database that already
+  contains a higher version of the migration directory. This scenario can cause unexpected and surprising behavior during
+  deployments.
+- Having non-linear history can make it challenging to roll back (or revert changes) to a specific version of the migration
+  directory, as the state of the database might be different than the state of the migration directory. Applying the migration
+  directory in consistent order promises deterministic behavior.
+
+Luckily, Atlas detects non-linear and non-additive changes made to a migration directory.
+To enable this behavior in your project, integrate Atlas into your [GitHub Actions](/cloud/setup-ci) or [GitLab](/guides/ci-platforms/gitlab)
+CI pipelines, and Atlas will automatically detect and report non-linear changes during the CI run.
+
 ### Destructive Changes
 
 Destructive changes are changes to a database schema that result in loss of data. For instance,
diff --git a/doc/md/ui/intro.md b/doc/md/ui/intro.md
deleted file mode 100644
index 57720557..00000000
--- a/doc/md/ui/intro.md
+++ /dev/null
@@ -1,135 +0,0 @@
----
-id: atlas-ui-intro
-slug: /ui/intro
-title: Management UI
----
-
-:::info
-The Management UI was deprecated after [Atlas v0.3.7](https://github.com/ariga/atlas/releases/tag/v0.3.7).
-To check out the new and improved UI, visit [Atlas Cloud](https://atlasgo.cloud).
-:::
-
-## Introduction
-
-As many other CLI tools, you can use Atlas to manage your schemas via your terminal.
-While this is the common interface for many infrastructure management workflows
-we believe that a visual, integrated environment can be beneficial in many use-cases.
-
-### Opening the UI
-
-Get started with the UI by running the command `atlas serve`.
-To ensure that your team's schemas, database credentials, migration history and more are not lost, save the data
-in the UI by running the command with [persistent storage](https://atlasgo.io/deployment/intro#persistent-storage):
-```
-atlas serve --storage mysql://root:pass@localhost:3306/storage_db
-```
-
-Alternatively, you can run the commands `schema inspect` or `schema apply` with the `-w` command-line
-flag:
-
-#### Inspect
-```
-atlas schema inspect -u "mysql://root:pass@localhost:3306/example" -w
-```
-
-#### Apply
-```
-atlas schema apply -u "mysql://root:pass@localhost:3306/example" -f atlas.hcl -w
-```
-
-### Usage
-:::note
-The following example is based on the schema created in the Getting Started tutorial.
-Click [here](http://localhost:3000/cli/getting-started/setting-up) to start the tutorial from the beginning.
-:::
-When the UI opens, we are taken to the schema page. This screen displays a high-level view of our Atlas schema.
-
-![Atlas Schema Synced](https://atlasgo.io/uploads/images/docs/synced-atlas-schema.png)
-
-
-On the top-left (1) we can see a box with information about our schema. The "Activity & History" panel (2) holds an audit history for all changes made to the schema. Each log is clickable, allowing us to view the diff between the schema and its previous state. On the bottom-right (3), the schema is described using the Atlas DDL.  On the bottom-left part of the screen (4), the UI displays an ERD (Entity-relation diagram) showing the different tables and the connections between them (via foreign-keys). By clicking the "expand" icon on the top-right corner of the ERD panel, we can open a more detailed view of our schema.
-
-![Schema ERD open](https://atlasgo.io/uploads/images/docs/schema-erd-open.png)
-
-#### Running a migration
-
-Now that we have seen how Atlas can visualize our database schema in the UI, let’s see how to initiate a migration.
-
-First, we will make changes to our schema by clicking on the "Edit Schema" button on the top right-hand corner.
-
-![Edit Schema Button](https://atlasgo.io/uploads/images/docs/edit-schema-button.png)
-
-Next, add the table `likes` to the schema:
-```hcl
-table "likes" {
-    schema = schema.example
-    column "id" {
-        null = false
-        type = int
-    }
-    column "user_id" {
-        null = false
-        type = int
-    }
-    column "blog_post_id" {
-        null = false
-        type = int
-    }
-    primary_key {
-        columns = [table.likes.column.id]
-    }
-    foreign_key "like_blog_post" {
-        columns     = [table.likes.column.blog_post_id]
-        ref_columns = [table.blog_posts.column.id]
-        on_update   = RESTRICT
-        on_delete   = RESTRICT
-    }
-    foreign_key "like_user" {
-        columns     = [table.likes.column.user_id]
-        ref_columns = [table.users.column.id]
-        on_update   = RESTRICT
-        on_delete   = RESTRICT
-    }
-    index "like_blog_post" {
-        columns = [table.likes.column.blog_post_id]
-    }
-    index "like_user" {
-        columns = [table.likes.column.user_id]
-    }
-}
-```
-
-Click "save" on the top right-hand corner.
-
-![Save Schema Button](https://atlasgo.io/uploads/images/docs/save-schema.png)
-
-Going back to our schema page, we can notice a few changes. First, our schema is now out of sync. This means we have created a change in the desired state that isn't yet reflected in the database. When a schema is out of sync, two separate schemas appear: desired and inspected.
-The _desired_ schema is the one being edited in the UI. The _inspected_ schema is the
-true, current state of the database. By clicking the toggle and changing the view to "unified", the schema is shown as one with a highlighted diff.
-
-Looking at the ERD, we can see that our table `likes` is green and has a plus-sign (+) next to it, showing that it needs to be added. This means the change has been recorded, but not yet applied.
-
-
-![Atlas Schema Not Synced Split](https://atlasgo.io/uploads/images/docs/schema-out-of-sync-unified.png)
-
-Now, let’s run a migration!
-Click "Migrate Schema" in the Schema Status box to apply the changes we want to make to our schema.
-
-The migration screen will open up, running the setup automatically. After the setup succeeds, we will see the migration plan.
-The diff in the schema in HCL on the left pane, and the planned SQL statements on the right.
-
-![Atlas Schema Not Synced Split](https://atlasgo.io/uploads/images/docs/migrate-plan-step.png)
-
-To start the migration, click "Apply". If you ever wish to not go through with a migration, you can click "Abort".
-
-![Atlas Schema Not Synced Split](https://atlasgo.io/uploads/images/docs/migrate-apply-step.png)
-
-Congrats, we have successfully completed a migration!
-
-As expected, after executing our migration plan, our database and desired schema are now synced.
-
-![Atlas Schema Not Synced Split](https://atlasgo.io/uploads/images/docs/synced-schema-after-migration.png)
-
-### Conclusion
-
-In this section we learned how to use the Management UI to inspect our database and run migrations.
diff --git a/doc/md/versioned/apply.mdx b/doc/md/versioned/apply.mdx
index 72b7b866..df287132 100644
--- a/doc/md/versioned/apply.mdx
+++ b/doc/md/versioned/apply.mdx
@@ -7,19 +7,114 @@ title: Applying Migrations
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 
-Atlas has its own migration execution engine, that works with the Atlas migration file format, e.g. migration files
-generated by [`atlas migrate diff`](/versioned/diff).
+With the `atlas migrate apply` command, users can apply pending migrations to database(s). The typical flow for introducing
+schema changes to databases is as follows: **Develop** ⇨ **Check (CI)** ⇨ **Push (CD)** ⇨ **Deploy**.
 
-### Arguments
-`atlas migrate apply` accepts one positional integer argument to specify how many pending migration files to run.
-* `atlas migrate apply` run all pending migrations
-* `atlas migrate apply 2` run at most 2 pending migrations
+1. **Develop** - Generate a migration file with the desired database changes using the [`atlas migrate diff`](/versioned/diff) command.
+2. **Check (CI)** - Use [`atlas migrate lint`](versioned/lint) to validate migrations, ensuring they don't conflict with other team members' changes
+   and align with best practices. Add Atlas to your CI pipeline in [GitHub Actions](/cloud/setup-ci) or [GitLab](/guides/ci-platforms/gitlab)
+   to review migrations files before they get merged into the main branch.
+3. **Push (Delivery)** - Use `atlas migrate push`, or set up a CI pipeline to push the latest migrations state to [Atlas Cloud](https://auth.atlasgo.cloud/signup).
+   Alternatively, you can package the migrations directory into a custom Docker image and push it to an artifactory.
+4. **Deploy** - Use `atlas migrate apply` to apply the pending migrations to your database(s).
+
+### Flags and Arguments
+
+By default, `atlas migrate apply` executes all pending migrations. However, you can pass an optional argument to limit the
+number of migrations applied. For instance, `atlas migrate apply 2` will apply up to 2 pending migrations.
+
+The following flags are required:
 
-### Flags
-When using `migrate apply` to apply migrations, users must supply multiple parameters:
 * `--url` the [URL](/concepts/url) to the database to apply migrations on.
-* `--dir` the URL of the migration directory, by default it is `file://migrations`, e.g a
- directory named `migrations` in the current working directory.
+* `--dir` the URL to the migration directory. It defaults to `file://migrations`.
+
+:::info Reading remote directories
+Users who have connected or pushed their migration directories to Atlas Cloud can read the migrations state directly from
+there without needing to have them locally. For example, `atlas migrate apply --dir "atlas://app"` will apply the pending
+migrations of the `app` project based on the most recent pushed state. To see it in action, run the following:
+
+**Login** or signup:
+```shell
+atlas login
+```
+
+**Push** a local migration directory and name it `app`:
+<Tabs>
+<TabItem value="postgres" label="PostgreSQL">
+
+```bash
+atlas migrate push app \
+  --dev-url "docker://postgres/15/dev?search_path=public"
+```
+
+</TabItem>
+<TabItem value="mysql" label="MySQL">
+
+```bash
+atlas migrate push app \
+  --dev-url "docker://mysql/8/dev"
+```
+
+</TabItem>
+<TabItem value="maria" label="Maria">
+
+```bash
+atlas migrate push app \
+  --dev-url "docker://mariadb/latest/dev"
+```
+
+</TabItem>
+<TabItem value="sqlite" label="SQLite">
+
+```bash
+atlas migrate push app \
+  --dev-url "sqlite://dev?mode=memory"
+```
+
+</TabItem>
+</Tabs>
+
+
+**Deploy** to a local database the remote migration directory named `app`:
+<Tabs>
+<TabItem value="postgres" label="PostgreSQL">
+
+```bash
+atlas migrate apply \
+  --dir "atlas://app" \
+  --url "postgres://postgres:pass@:5432/example?search_path=public&sslmode=disable"
+```
+
+</TabItem>
+<TabItem value="mysql" label="MySQL">
+
+```bash
+atlas migrate apply \
+  --dir "atlas://app" \
+  --url "mysql://root:pass@:3306/example"
+```
+
+</TabItem>
+<TabItem value="maria" label="Maria">
+
+```bash
+atlas migrate apply \
+  --dir "atlas://app" \
+  --url "maria://root:pass@:3306/example"
+```
+
+</TabItem>
+<TabItem value="sqlite" label="SQLite">
+
+```bash
+atlas migrate apply \
+  --dir "atlas://app" \
+  --url "sqlite://example.db"
+```
+
+</TabItem>
+</Tabs>
+:::
 
 ### Schema Revision Information
 
diff --git a/doc/md/versioned/diff.mdx b/doc/md/versioned/diff.mdx
index 0eca6272..3473f26f 100644
--- a/doc/md/versioned/diff.mdx
+++ b/doc/md/versioned/diff.mdx
@@ -14,7 +14,125 @@ where each change is explicitly defined as a migration script with a specific ve
 
 Practically speaking, this means developers define the desired state and Atlas maintains the `migrations` directory,
 which contains the explicit SQL scripts to move from one version to the next. The desired state can be defined using an
-[HCL](/atlas-schema/hcl) or SQL schema definition, or a database [URL](../concepts/url).
+[HCL](/atlas-schema/hcl) or SQL schema definition, a database [URL](../concepts/url), external schemas like
+[ORM](/atlas-schema/external).
+
+### Summary
+
+To get started with versioned migrations, run `atlas migrate diff`. This command generates a new migration file that aligns
+the migration directory with the state defined by the desired schema. Below are a few examples of how to use this command
+to generate migrations from various sources:
+
+<Tabs>
+<TabItem label="SQL Schema" value="sql">
+
+**MySQL/MariaDB:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "file://schema.sql" \
+  --dev-url "docker://mysql/8/dev" # Or: "docker://mariadb/latest/dev"
+```
+
+**PostgreSQL:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "file://schema.sql" \
+  --dev-url "docker://postgres/15/dev?search_path=public"
+```
+</TabItem>
+<TabItem label="HCL Schema" value="hcl">
+
+**MySQL/MariaDB:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "file://schema.hcl" \
+  --dev-url "docker://mysql/8/dev" # Or: "docker://mariadb/latest/dev"
+```
+
+**PostgreSQL:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "file://schema.hcl" \
+  --dev-url "docker://postgres/15/dev?search_path=public"
+```
+</TabItem>
+<TabItem label="ORM" value="orm">
+
+**MySQL/MariaDB:**
+
+```shell {2}
+atlas migrate diff create_users \
+  --env "gorm"
+  --dev-url "docker://mysql/8/dev" # Or: "docker://mariadb/latest/dev"
+```
+
+**PostgreSQL:**
+
+```shell {2}
+atlas migrate diff create_users \
+  --env "gorm"
+  --dev-url "docker://postgres/15/dev?search_path=public"
+```
+
+To learn more on how to configure Atlas to read the desired state of a schema from an ORM definition, see the
+[external schema documentation](/atlas-schema/external).
+
+</TabItem>
+<TabItem label="Database Schema" value="database-schema">
+
+**MySQL/MariaDB:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "mysql://root:pass@:3306/public" \
+  --dev-url "docker://mysql/8/dev" # Or: "docker://mariadb/latest/dev"
+```
+
+**PostgreSQL:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable" \
+  --dev-url "docker://postgres/15/dev?search_path=public"
+```
+</TabItem>
+<TabItem label="Database (multiple schemas)" value="database">
+
+**MySQL/MariaDB:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "mysql://root:pass@:3306/" \
+  --dev-url "docker://mysql/8" # Or: "docker://mariadb"
+```
+
+**PostgreSQL:**
+
+```shell {3}
+atlas migrate diff create_users \
+  --dir "file://migrations" \
+  --to "postgres://postgres:pass@localhost:5432/database?sslmode=disable" \
+  --dev-url "docker://postgres/15/dev"
+```
+</TabItem>
+</Tabs>
+
+:::info [Ensure linear migration history](/lint/analyzers#non-linear-changes)
+By utilizing the [`atlas migrate lint`](/versioned/lint) command, teams can ensure that changes are added to the
+migration directory in a sequential order, preventing unexpected behavior. Learn more on how to integrate Atlas into your
+[**GitHub Actions**](/cloud/setup-ci) or [**GitLab**](/guides/ci-platforms/gitlab) CI pipelines.
+:::
 
 ### Generate migrations from HCL schemas
 
@@ -40,42 +158,19 @@ creating our `users` table:
 * `--format` (optional) - [Go template](https://pkg.go.dev/text/template) to use to format the output.
 
 <Tabs
-defaultValue="docker"
 values={[
-{label: 'Docker', value: 'docker'},
 {label: 'MySQL', value: 'mysql'},
 {label: 'MariaDB', value: 'maria'},
 {label: 'PostgreSQL', value: 'postgres'},
 {label: 'SQLite', value: 'sqlite'},
 ]}>
-<TabItem value="docker">
-
-**MySQL/MariaDB:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.hcl" \
-  --dev-url "docker://mysql/8/test" # Or: "docker://mariadb/latest/test"
-```
-
-**PostgreSQL:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.hcl" \
-  --dev-url "docker://postgres/15/test?search_path=public"
-```
-
-</TabItem>
 <TabItem value="mysql">
 
 ```shell
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "mysql://root:pass@:3306/test"
+  --dev-url "docker://mysql/8/test"
 ```
 
 </TabItem>
@@ -85,7 +180,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "maria://root:pass@:3306/test"
+  --dev-url "docker://mariadb/latest/test"
 ```
 
 </TabItem>
@@ -95,7 +190,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
+  --dev-url "docker://postgres/15/test?search_path=public"
 ```
 
 </TabItem>
@@ -170,42 +265,19 @@ table "users" {
 Then, run `atlas migrate diff`:
 
 <Tabs
-defaultValue="docker"
 values={[
-{label: 'Docker', value: 'docker'},
 {label: 'MySQL', value: 'mysql'},
 {label: 'MariaDB', value: 'maria'},
 {label: 'PostgreSQL', value: 'postgres'},
 {label: 'SQLite', value: 'sqlite'},
 ]}>
-<TabItem value="docker">
-
-**MySQL/MariaDB:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.hcl" \
-  --dev-url "docker://mysql/8/test" # Or: "docker://mariadb/latest/test"
-```
-
-**PostgreSQL:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.hcl" \
-  --dev-url "docker://postgres/15/test?search_path=public"
-```
-
-</TabItem>
 <TabItem value="mysql">
 
 ```shell
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "mysql://root:pass@:3306/test"
+  --dev-url "docker://mysql/8/dev"
 ```
 
 </TabItem>
@@ -215,7 +287,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "maria://root:pass@:3306/test"
+  --dev-url "docker://mariadb/latest/dev"
 ```
 
 </TabItem>
@@ -225,7 +297,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
+  --dev-url "docker://postgres/15/dev?search_path=public"
 ```
 
 </TabItem>
@@ -235,7 +307,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "sqlite://file?mode=memory"
+  --dev-url "sqlite://dev?mode=memory"
 ```
 
 </TabItem>
@@ -311,42 +383,19 @@ creating our `users` table:
 * `--dev-url` a [URL](/concepts/url) to a [Dev Database](/concepts/dev-database) that will be used to compute the diff.
 
 <Tabs
-defaultValue="docker"
 values={[
-{label: 'Docker', value: 'docker'},
 {label: 'MySQL', value: 'mysql'},
 {label: 'MariaDB', value: 'maria'},
 {label: 'PostgreSQL', value: 'postgres'},
 {label: 'SQLite', value: 'sqlite'},
 ]}>
-<TabItem value="docker">
-
-**MySQL/MariaDB:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.sql" \
-  --dev-url "docker://mysql/8/test" # Or: "docker://mariadb/latest/test"
-```
-
-**PostgreSQL:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.sql" \
-  --dev-url "docker://postgres/15/test?search_path=public"
-```
-
-</TabItem>
 <TabItem value="mysql">
 
 ```shell
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.sql" \
-  --dev-url "mysql://root:pass@:3306/test"
+  --dev-url "docker://mysql/8/dev"
 ```
 
 </TabItem>
@@ -356,7 +405,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.sql" \
-  --dev-url "maria://root:pass@:3306/test"
+  --dev-url "docker://mariadb/latest/dev"
 ```
 
 </TabItem>
@@ -366,7 +415,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.sql" \
-  --dev-url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
+  --dev-url "docker://postgres/15/dev?search_path=public"
 ```
 
 </TabItem>
@@ -435,42 +484,19 @@ Then, run `atlas migrate diff`:
 
 
 <Tabs
-defaultValue="docker"
 values={[
-{label: 'Docker', value: 'docker'},
 {label: 'MySQL', value: 'mysql'},
 {label: 'MariaDB', value: 'maria'},
 {label: 'PostgreSQL', value: 'postgres'},
 {label: 'SQLite', value: 'sqlite'},
 ]}>
-<TabItem value="docker">
-
-**MySQL/MariaDB:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.sql" \
-  --dev-url "docker://mysql/8/test" # Or: "docker://mariadb/latest/test"
-```
-
-**PostgreSQL:**
-
-```shell
-atlas migrate diff create_users \
-  --dir "file://migrations" \
-  --to "file://schema.sql" \
-  --dev-url "docker://postgres/15/test?search_path=public"
-```
-
-</TabItem>
 <TabItem value="mysql">
 
 ```shell
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.sql" \
-  --dev-url "mysql://root:pass@:3306/test"
+  --dev-url "docker://mysql/8/dev"
 ```
 
 </TabItem>
@@ -480,7 +506,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.sql" \
-  --dev-url "maria://root:pass@:3306/test"
+  --dev-url "docker://mariadb/latest/dev"
 ```
 
 </TabItem>
@@ -490,7 +516,7 @@ atlas migrate diff create_users \
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.sql" \
-  --dev-url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
+  --dev-url "docker://postgres/15/dev?search_path=public"
 ```
 
 </TabItem>
@@ -574,7 +600,7 @@ creating our `users` table:
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "mysql://root:pass@:3306/public" \
-  --dev-url "mysql://root:pass@:3306/test"
+  --dev-url "docker://mysql/8/dev"
 ```
 
 Run `ls migrations`, and you will notice Atlas created 2 files:
@@ -623,7 +649,7 @@ the output:
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "mysql://root:pass@:3306/test" \
+  --dev-url "docker://mysql/8/dev" \
   --qualifier "market"
 ```
 
@@ -644,7 +670,7 @@ directory by passing in the `format` query parameter to the migration directory
 atlas migrate diff create_users \
   --dir "file://migrations?format=golang-migrate" \
   --to "file://schema.hcl" \
-  --dev-url "mysql://root:pass@:3306/test"
+  --dev-url "docker://mysql/8/dev"
 ```
 
 Run `ls migrations`, and you will notice Atlas created 3 files:
@@ -729,7 +755,7 @@ values={[
 atlas migrate diff create_all \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "mysql://root:pass@:3306/"
+  --dev-url "docker://mysql/8"
 ```
 
 Running `cat migrations/*.sql` will print the followings:
@@ -752,7 +778,7 @@ CREATE TABLE `market`.`users` (`name` int NOT NULL) CHARSET utf8mb4 COLLATE utf8
 atlas migrate diff create_all \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "postgres://root:pass@:5434/database?sslmode=disable"
+  --dev-url "docker://postgres/15"
 ```
 
 Running `cat migrations/*.sql` will print the followings:
@@ -836,7 +862,7 @@ generate the SQL statements with indentation, use the `--format` flag. For examp
 atlas migrate diff create_users \
   --dir "file://migrations" \
   --to "file://schema.hcl" \
-  --dev-url "docker://mysql/8/test" \
+  --dev-url "docker://mysql/8/dev" \
   --format '{{ sql . "  " }}'
 ```
 
diff --git a/doc/md/versioned/intro.mdx b/doc/md/versioned/intro.mdx
index 074b7cf7..497add18 100644
--- a/doc/md/versioned/intro.mdx
+++ b/doc/md/versioned/intro.mdx
@@ -141,7 +141,7 @@ h1:kGsXTVLtL0vs9Un8aaGKwMHZ2iMibgTQxwyAt7+Elcw=
 
 Now that we have our first migration, we can apply it to a database. There are multiple ways to accomplish this, with
 most methods covered in the [guides](/guides) section. In this example, we'll demonstrate how to push migrations to
-Atlas Cloud, much like how Docker images are pushed to Docker Hub.
+[Atlas Cloud](https://auth.atlasgo.cloud/signup), much like how Docker images are pushed to Docker Hub.
 
 <div style={{textAlign: 'center'}}>
     <img src="https://atlasgo.io/uploads/versioned/migrate-push1.jpg" alt="atlas migrate push" width="100%"/>
diff --git a/doc/md/versioned/lint.mdx b/doc/md/versioned/lint.mdx
index 5e962498..f6f7d78e 100644
--- a/doc/md/versioned/lint.mdx
+++ b/doc/md/versioned/lint.mdx
@@ -7,29 +7,179 @@ title: Verifying migration safety
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 
-With the `atlas migrate lint` command, users can analyze the migration directory
-to detect potentially dangerous changes to the database schema. This command may be
-incorporated in _continuous integration_ pipelines to enable teams to enforce desired
-policies with regard to schema changes. 
+The `atlas migrate lint` command enables team members to work together on database schemas, ensuring they are synchronized
+and avoid stepping on each other's toes. Atlas analyzes new schema changes in the migration directory that might
+be dangerous, violate best practices, or break the team's policies. Learn more about [Atlas's analyzers](/lint/analyzers).
 
-[Learn more about Atlas's analyzers](/lint/analyzers)
+While this command is commonly used locally, migration linting is frequently incorporated into CI pipelines, allowing
+teams to detect changes early before merging them into the main branch. Learn more on how to integrate Atlas into
+your [GitHub Actions](/cloud/setup-ci) or [GitLab](/guides/ci-platforms/gitlab) CI pipelines.
 
 ### Flags
 
 When using `migrate lint` to analyze migrations, users must supply multiple parameters:
-* `--dev-url` a [URL](/concepts/url) to a [Dev-database](/concepts/dev-database) that will be used
+* `--dev-url` - [URL](/concepts/url) to a [Dev-database](/concepts/dev-database) that will be used
  to simulate the changes and verify their correctness.
-* `--dir` the URL of the migration directory, by default it is `file://migrations`, e.g a
+* `--dir` - URL of the migration directory, by default it is `file://migrations`, e.g a
  directory named `migrations` in the current working directory.
+* `--web` / `-w` (optional) - open the lint report in the browser.
 
 ### Changeset detection
 
-When we run the `lint` command, we need to instruct Atlas on how to decide what 
-set of migration files to analyze. Currently, two modes are supported.
+When we run the `lint` command, we need to instruct Atlas on how to decide which set of migration files to analyze.
+Currently, three ways are supported. Let's go over them one by one:
 
-* `--git-base <branchName>`: which selects the diff between the provided branch and 
- the current one as the changeset.
-* `--latest <n>` which selects the latest n migration files as the changeset.
+#### Compare against the latest state of the migration directory
+
+Teams that have connected their project to Atlas Cloud (see [setup](/cloud/setup-ci) for more details), can simply run
+the following command to analyze the changes detected by comparing the **local state** of the migration directory
+to the **latest state**, as defined in Atlas Cloud. For example, either by running `atlas migrate push` or by connecting
+it to a CI pipeline.
+
+<Tabs>
+<TabItem value="mysql" label="MySQL">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://mysql/8/dev" \
+  -w
+```
+
+</TabItem>
+<TabItem value="maria" label="MariaDB">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://mariadb/latest/dev" \
+  -w
+```
+
+</TabItem>
+<TabItem value="postgres" label="PostgreSQL">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://postgres/15/test?search_path=public" \
+  -w
+```
+
+</TabItem>
+<TabItem value="sqlite" label="SQLite">
+
+```shell
+atlas migrate lint \
+  --dev-url "sqlite://file?mode=memory" \
+  -w
+```
+
+</TabItem>
+</Tabs>
+
+:::info Preventing non-additive changes
+By using this method, Atlas warns you if your local migration directory is not up-to-date with the latest state
+of the project and rebasing is required. For example, while working on a feature branch, another team member might have
+pushed a new migration file to the main branch.
+:::
+
+Here is an example of how a report of `atlas migrate lint` looks like:
+
+![CI for schema changes](https://atlasgo.io/uploads/lint-report-non-additive.png)
+
+
+#### Analyze the latest N migration files
+
+Users can instruct Atlas to analyze the latest N migration files by running `atlas migrate lint --latest N`:
+
+<Tabs>
+<TabItem value="mysql" label="MySQL">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://mysql/8/dev" \
+  --latest 1
+```
+
+</TabItem>
+<TabItem value="maria" label="MariaDB">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://mariadb/latest/dev" \
+  --latest 1
+```
+
+</TabItem>
+<TabItem value="postgres" label="PostgreSQL">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://postgres/15/test?search_path=public" \
+  --latest 1
+```
+
+</TabItem>
+<TabItem value="sqlite" label="SQLite">
+
+```shell
+atlas migrate lint \
+  --dev-url "sqlite://file?mode=memory" \
+  --latest 1
+```
+
+</TabItem>
+</Tabs>
+
+Similarly to the previous method, the `-w`/`--web` can be used to open the report in the browser, see the changes in ERD
+format, and more.
+
+#### Compare against a specific branch
+
+Users can instruct Atlas to detect which changes to analyze by comparing the current branch to a specific branch.
+For example, in order to analyze the changes between the current branch and the `main` branch, and open the report in the
+browser, run the following command:
+
+<Tabs>
+<TabItem value="mysql" label="MySQL">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://mysql/8/dev" \
+  --git-base "master" \
+  -w
+```
+
+</TabItem>
+<TabItem value="maria" label="MariaDB">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://mariadb/latest/dev" \
+  --git-base "master" \
+  -w
+```
+
+</TabItem>
+<TabItem value="postgres" label="PostgreSQL">
+
+```shell
+atlas migrate lint \
+  --dev-url "docker://postgres/15/test?search_path=public" \
+  --git-base "master" \
+  -w
+```
+
+</TabItem>
+<TabItem value="sqlite" label="SQLite">
+
+```shell
+atlas migrate lint \
+  --dev-url "sqlite://file?mode=memory" \
+  --git-base "master" \
+  -w
+```
+
+</TabItem>
+</Tabs>
 
 ### `nolint` directive
 
@@ -141,79 +291,81 @@ lines are used to separate file directives from its content.
 </TabItem>
 </Tabs>
 
-### Output
-
-Users may supply a [Go template](https://pkg.go.dev/text/template) string as the `--format` parameter to
-format the output of the `lint` command.
+### Open in the browser
 
-### Examples
-
-Analyze the latest migration file using the `docker` driver:
+In order to open the migration linting report in the browser, you first need to be logged in to Atlas, then use one of
+the example commands above with the `-w`/`--web` flag. For example:
 
 <Tabs>
-<TabItem value="MySQL">
+<TabItem value="mysql" label="MySQL">
 
 ```shell
-atlas migrate lint \
-  --latest 1 \
-  --dir "file://migrations" \
-  --dev-url "docker://mysql/8/test"
+atlas login
 ```
 
-</TabItem>
-<TabItem value="MariaDB">
-
 ```shell
 atlas migrate lint \
+  --dev-url "docker://mysql/8/dev" \
   --latest 1 \
-  --dir "file://migrations" \
-  --dev-url "docker://mariadb/latest/test"
+  -w
 ```
 
 </TabItem>
-<TabItem value="PostgreSQL">
+<TabItem value="maria" label="MariaDB">
+
+```shell
+atlas login
+```
 
 ```shell
 atlas migrate lint \
+  --dev-url "docker://mariadb/latest/dev" \
   --latest 1 \
-  --dir "file://migrations" \
-  --dev-url "docker://postgres/15/test?search_path=public"
+  -w
 ```
 
 </TabItem>
-<TabItem value="SQLite">
+<TabItem value="postgres" label="PostgreSQL">
+
+```shell
+atlas login
+```
 
 ```shell
 atlas migrate lint \
+  --dev-url "docker://postgres/15/test?search_path=public" \
   --latest 1 \
-  --dir "file://migrations" \
-  --dev-url "sqlite://file?mode=memory"
+  -w
 ```
 
 </TabItem>
-</Tabs>
+<TabItem value="sqlite" label="SQLite">
 
-Analyze all changes relative to the `master` Git branch:
 ```shell
-atlas migrate lint \
-  --dir "file://my/project/migrations" \
-  --dev-url "mysql://root:pass@localhost:3306/dev" \
-  --git-base "master"
+atlas login
 ```
 
-Format output as JSON:
 ```shell
 atlas migrate lint \
-  --dir "file://my/project/migrations" \
-  --dev-url "mysql://root:pass@localhost:3306/dev" \
-  --git-base "master" \
-  --format "{{ json .Files }}"
-```
-Use the configuration defined in [`atlas.hcl`](../atlas-schema/projects#configure-migration-linting):
-```shell
-atlas migrate lint --env "local"
+  --dev-url "sqlite://file?mode=memory" \
+  --latest 1 \
+  -w
 ```
 
+</TabItem>
+</Tabs>
+
+<details><summary>Screenshot example</summary>
+
+![CI for schema changes](https://atlasgo.io/uploads/lint-report-non-additive.png)
+
+</details>
+
+### Output
+
+Users may supply a [Go template](https://pkg.go.dev/text/template) string as the `--format` parameter to
+format the output of the `lint` command.
+
 ### Reference
 
 [CLI Command Reference](/cli-reference#atlas-migrate-lint)
\ No newline at end of file
diff --git a/doc/website/package.json b/doc/website/package.json
index b07fc004..bd19bcc7 100644
--- a/doc/website/package.json
+++ b/doc/website/package.json
@@ -16,7 +16,7 @@
     "invalidate-cdn": "aws cloudfront create-invalidation --distribution-id E3VOJYSV9YO33D --paths \"/*\""
   },
   "dependencies": {
-    "@ariga/atlas-website": "0.0.23",
+    "@ariga/atlas-website": "0.0.26",
     "@docusaurus/core": "^2.0.0-rc.1",
     "@docusaurus/plugin-client-redirects": "^2.4.1",
     "@docusaurus/plugin-ideal-image": "^2.4.1",
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 82edcf8a..ed966a2e 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -175,7 +175,12 @@ module.exports = {
                 {
                     type: 'doc',
                     id: 'guides/deploying/k8s-argo',
-                    label: 'Kubernetes (ArgoCD)'
+                    label: 'Kubernetes (Argo CD)'
+                },
+                {
+                    type: 'doc',
+                    id: 'guides/deploying/k8s-flux',
+                    label: 'Kubernetes (Flux CD)'
                 },
                 {
                     type: 'doc',
@@ -206,6 +211,18 @@ module.exports = {
                 }
             ]
         },
+        {
+            type: 'category',
+            label: 'CI Platforms',
+            collapsed: false,
+            items: [
+                {
+                    type: 'doc',
+                    label: 'GitLab',
+                    id: 'guides/ci-platforms/gitlab',
+                }
+            ]
+        },
         {
             type: 'category',
             label: 'Terraform',
diff --git a/internal/ci/ci_dialect.tmpl b/internal/ci/ci_dialect.tmpl
index 4b6b92f4..989479b1 100644
--- a/internal/ci/ci_dialect.tmpl
+++ b/internal/ci/ci_dialect.tmpl
@@ -15,7 +15,7 @@ on:
 jobs:
 {{- range $.Jobs }}
   integration-{{ .Version }}:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     {{ if .Image }}services:
       {{ .Version }}:
         image: {{ .Image }}
diff --git a/internal/ci/ci_go.tmpl b/internal/ci/ci_go.tmpl
index 4e71809d..ef65ac67 100644
--- a/internal/ci/ci_go.tmpl
+++ b/internal/ci/ci_go.tmpl
@@ -14,7 +14,7 @@ on:
 
 jobs:
   lint:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     steps:
       - uses: actions/setup-go@v4
         with:
@@ -26,7 +26,7 @@ jobs:
           args: --verbose
 
   generate-cmp:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     steps:
       - uses: actions/checkout@v2.4.0
       - uses: actions/setup-go@v4
@@ -48,7 +48,7 @@ jobs:
             exit 1
           fi
   unit:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     strategy:
       matrix:
         go: [ '1.20', '1.21' ]
@@ -65,7 +65,7 @@ jobs:
         working-directory: schemahcl
 
   cli:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     steps:
       - uses: actions/checkout@v2
       - uses: actions/setup-go@v4
@@ -76,7 +76,7 @@ jobs:
         working-directory: cmd/atlas
 
   integration:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     steps:
       - uses: actions/checkout@v2.3.4
       - uses: actions/setup-go@v4
diff --git a/internal/ci/ci_revisions.tmpl b/internal/ci/ci_revisions.tmpl
index 5d5cae38..0514611b 100644
--- a/internal/ci/ci_revisions.tmpl
+++ b/internal/ci/ci_revisions.tmpl
@@ -13,7 +13,7 @@ on:
       - 'cmd/atlas/internal/migrate/ent/**'
 jobs:
   revisions:
-    runs-on: ubuntu-latest
+    runs-on: {{ $.Runner }}
     steps:
       - uses: actions/checkout@v2.3.4
         with:
diff --git a/internal/ci/main.go b/internal/ci/main.go
index 0b9fb80c..d01a0536 100644
--- a/internal/ci/main.go
+++ b/internal/ci/main.go
@@ -165,17 +165,18 @@ var (
 )
 
 func main() {
-	var flavor, tags, suffix string
+	var flavor, tags, suffix, runner string
 	flag.StringVar(&flavor, "flavor", "", "")
 	flag.StringVar(&tags, "tags", "", "")
 	flag.StringVar(&suffix, "suffix", "", "")
+	flag.StringVar(&runner, "runner", "ubuntu-latest", "")
 	flag.Parse()
 	for _, n := range []string{"dialect", "go", "revisions"} {
 		var buf bytes.Buffer
 		if err := tpl.ExecuteTemplate(&buf, fmt.Sprintf("ci_%s.tmpl", n), struct {
-			Jobs         []Job
-			Flavor, Tags string
-		}{jobs, flavor, tags}); err != nil {
+			Jobs                 []Job
+			Flavor, Tags, Runner string
+		}{jobs, flavor, tags, runner}); err != nil {
 			log.Fatalln(err)
 		}
 		err := os.WriteFile(filepath.Clean(fmt.Sprintf("../../.github/workflows/ci-%s_%s.yaml", n, suffix)), buf.Bytes(), 0600)
diff --git a/schemahcl/stdlib.go b/schemahcl/stdlib.go
index 8c60a130..971205be 100644
--- a/schemahcl/stdlib.go
+++ b/schemahcl/stdlib.go
@@ -161,7 +161,8 @@ func stdFuncs() map[string]function.Function {
 		"trimsuffix":      stdlib.TrimSuffixFunc,
 		"try":             tryfunc.TryFunc,
 		"upper":           stdlib.UpperFunc,
-		"urlescape":       urlEscape,
+		"urlescape":       urlEscapeFunc,
+		"urluserinfo":     urlUserinfoFunc,
 		"urlqueryset":     urlQuerySetFunc,
 		"urlsetpath":      urlSetPathFunc,
 		"values":          stdlib.ValuesFunc,
@@ -276,6 +277,37 @@ var (
 			return cty.StringVal(u.String()), nil
 		},
 	})
+	urlUserinfoFunc = function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name: "url",
+				Type: cty.String,
+			},
+			{
+				Name: "user",
+				Type: cty.String,
+			},
+		},
+		VarParam: &function.Parameter{
+			Name:      "pass",
+			Type:      cty.String,
+			AllowNull: true,
+		},
+		Type: function.StaticReturnType(cty.String),
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			u, err := url.Parse(args[0].AsString())
+			if err != nil {
+				return cty.NilVal, err
+			}
+			user := args[1].AsString()
+			if len(args) > 2 && !args[2].IsNull() {
+				u.User = url.UserPassword(user, args[2].AsString())
+			} else {
+				u.User = url.User(user)
+			}
+			return cty.StringVal(u.String()), nil
+		},
+	})
 	urlSetPathFunc = function.New(&function.Spec{
 		Params: []function.Parameter{
 			{
@@ -298,7 +330,7 @@ var (
 		},
 	})
 
-	urlEscape = function.New(&function.Spec{
+	urlEscapeFunc = function.New(&function.Spec{
 		Params: []function.Parameter{
 			{
 				Name: "string",
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index 5e69bc5e..5a802ec0 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -448,11 +448,18 @@ func (d *Diff) fkChange(from, to *schema.ForeignKey) schema.ChangeKind {
 
 // similarUnnamedIndex searches for an unnamed index with the same index-parts in the table.
 func (d *Diff) similarUnnamedIndex(t *schema.Table, idx1 *schema.Index) (*schema.Index, bool) {
-	for _, idx2 := range t.Indexes {
-		if idx2.Name != "" || len(idx2.Parts) != len(idx1.Parts) || idx2.Unique != idx1.Unique {
-			continue
+	match := func(idx1, idx2 *schema.Index) bool {
+		return idx1.Unique == idx2.Unique && d.partsChange(idx1, idx2) == schema.NoChange
+	}
+	if f, ok := d.DiffDriver.(interface {
+		FindGeneratedIndex(*schema.Table, *schema.Index) (*schema.Index, bool)
+	}); ok {
+		if idx2, ok := f.FindGeneratedIndex(t, idx1); ok && match(idx1, idx2) {
+			return idx2, true
 		}
-		if d.partsChange(idx1, idx2) == schema.NoChange {
+	}
+	for _, idx2 := range t.Indexes {
+		if idx2.Name == "" && match(idx1, idx2) {
 			return idx2, true
 		}
 	}
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index bcc1220a..b8ea3efd 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -21,7 +21,7 @@ import (
 // DefaultDiff provides basic diffing capabilities for PostgreSQL dialects.
 // Note, it is recommended to call Open, create a new Driver and use its Differ
 // when a database connection is available.
-var DefaultDiff schema.Differ = &sqlx.Diff{DiffDriver: &diff{}}
+var DefaultDiff schema.Differ = &sqlx.Diff{DiffDriver: &diff{&conn{ExecQuerier: sqlx.NoRows}}}
 
 // A diff provides a PostgreSQL implementation for sqlx.DiffDriver.
 type diff struct{ *conn }
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index f19e5bbb..ef78e730 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -36,8 +36,6 @@ type (
 		// The schema in the `search_path` parameter (if given).
 		schema string
 		// System variables that are set on `Open`.
-		collate string
-		ctype   string
 		version int
 		crdb    bool
 	}
@@ -95,10 +93,9 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 	if err != nil {
 		return nil, fmt.Errorf("postgres: failed scanning rows: %w", err)
 	}
-	if len(params) != 3 && len(params) != 4 {
+	if len(params) != 1 && len(params) != 2 {
 		return nil, fmt.Errorf("postgres: unexpected number of rows: %d", len(params))
 	}
-	c.ctype, c.collate = params[1], params[2]
 	if c.version, err = strconv.Atoi(params[0]); err != nil {
 		return nil, fmt.Errorf("postgres: malformed version: %s: %w", params[0], err)
 	}
@@ -106,7 +103,7 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 		return nil, fmt.Errorf("postgres: unsupported postgres version: %d", c.version)
 	}
 	// Means we are connected to CockroachDB because we have a result for name='crdb_version'. see `paramsQuery`.
-	if c.crdb = len(params) == 4; c.crdb {
+	if c.crdb = len(params) == 2; c.crdb {
 		return noLockDriver{
 			&Driver{
 				conn:        c,
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index fbcbe411..2c789f09 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -34,8 +34,7 @@ func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOpt
 	if opts == nil {
 		opts = &schema.InspectRealmOption{}
 	}
-	r := schema.NewRealm(schemas...).SetCollation(i.collate)
-	r.Attrs = append(r.Attrs, &CType{V: i.ctype})
+	r := schema.NewRealm(schemas...)
 	if len(schemas) > 0 {
 		mode := sqlx.ModeInspectRealm(opts)
 		if mode.Is(schema.InspectTables) {
@@ -72,8 +71,7 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 	if opts == nil {
 		opts = &schema.InspectOptions{}
 	}
-	r := schema.NewRealm(schemas...).SetCollation(i.collate)
-	r.Attrs = append(r.Attrs, &CType{V: i.ctype})
+	r := schema.NewRealm(schemas...)
 	if sqlx.ModeInspectSchema(opts).Is(schema.InspectTables) {
 		if err := i.inspectTables(ctx, r, opts); err != nil {
 			return nil, err
@@ -702,12 +700,6 @@ func canConvert(t *schema.ColumnType, x string) (string, bool) {
 }
 
 type (
-	// CType describes the character classification setting (LC_CTYPE).
-	CType struct {
-		schema.Attr
-		V string
-	}
-
 	// UserDefinedType defines a user-defined type attribute.
 	UserDefinedType struct {
 		schema.Type
@@ -1161,7 +1153,7 @@ func newEnumType(t string, id int64) *enumType {
 
 const (
 	// Query to list runtime parameters.
-	paramsQuery = `SELECT setting FROM pg_settings WHERE name IN ('lc_collate', 'lc_ctype', 'server_version_num', 'crdb_version') ORDER BY name DESC`
+	paramsQuery = `SELECT setting FROM pg_settings WHERE name IN ('server_version_num', 'crdb_version') ORDER BY name DESC`
 
 	// Query to list database schemas.
 	schemasQuery = `
diff --git a/sql/sqlite/diff.go b/sql/sqlite/diff.go
index f49c0ef6..b4c02d5c 100644
--- a/sql/sqlite/diff.go
+++ b/sql/sqlite/diff.go
@@ -123,6 +123,18 @@ func (d *diff) IsGeneratedIndexName(t *schema.Table, idx *schema.Index) bool {
 	return err == nil && i > 0
 }
 
+// FindGeneratedIndex finds the table index that represents the generated index.
+// This is useful because unlike MySQL/PostgreSQL, SQLite does not allow creating
+// the generated indexes with their internal names. Therefore, they are renamed in
+// normalization phase. See migrate.go#normalizeIdxName for more details.
+func (d *diff) FindGeneratedIndex(t *schema.Table, idx *schema.Index) (*schema.Index, bool) {
+	nr := schema.NewIndex(idx.Name)
+	if normalizeIdxName(nr, t) != nil {
+		return nil, false
+	}
+	return t.Index(nr.Name)
+}
+
 // IndexAttrChanged reports if the index attributes were changed.
 func (*diff) IndexAttrChanged(from, to []schema.Attr) bool {
 	var p1, p2 IndexPredicate


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index 0ea805cc..31d9af86 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -802,6 +802,8 @@ func TestMigrate_ApplyCloudReport(t *testing.T) {
 			}
 			require.NoError(t, json.NewDecoder(r.Body).Decode(&m))
 			switch {
+			case strings.Contains(m.Query, "query") && strings.Contains(m.Query, "bot"):
+				fmt.Fprintln(w, `{"data": {"me":{ "name": "a8m", "org": "graph"}}}`)
 			case strings.Contains(m.Query, "query"):
 				// Checksum before archiving.
 				hf, err := dir.Checksum()
@@ -970,6 +972,22 @@ env {
 		require.EqualError(t, err, "openerror")
 		require.Equal(t, "Error: openerror", *reports.Error)
 	})
+
+	t.Run("LocalDirectory", func(t *testing.T) {
+		report.DirName = "NotReported"
+		cmd := migrateCmd()
+		cmd.AddCommand(migrateApplyCmd())
+		_, err := runCmd(
+			cmd, "apply",
+			"-c", "file://"+path,
+			"--env", "local",
+			"--dir", "file://"+t.TempDir(),
+			"--url", openSQLite(t, ""),
+			"--var", "cloud_url="+srv.URL,
+		)
+		require.NoError(t, err)
+		require.Equal(t, "NotReported", report.DirName)
+	})
 }
 
 func TestMigrate_ApplyCloudReportSet(t *testing.T) {
@@ -986,6 +1004,8 @@ func TestMigrate_ApplyCloudReportSet(t *testing.T) {
 			}
 			require.NoError(t, json.NewDecoder(r.Body).Decode(&m))
 			switch {
+			case strings.Contains(m.Query, "query") && strings.Contains(m.Query, "bot"):
+				fmt.Fprintln(w, `{"data": {"me":{ "name": "a8m", "org": "graph"}}}`)
 			case strings.Contains(m.Query, "query"):
 				// Checksum before archiving.
 				hf, err := dir.Checksum()
diff --git a/cmd/atlas/internal/cmdapi/project_test.go b/cmd/atlas/internal/cmdapi/project_test.go
index bc3a7ba5..115fffad 100644
--- a/cmd/atlas/internal/cmdapi/project_test.go
+++ b/cmd/atlas/internal/cmdapi/project_test.go
@@ -16,6 +16,7 @@ import (
 	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/schema"
 
+	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/require"
 	"github.com/zclconf/go-cty/cty"
 )
@@ -100,7 +101,7 @@ env "multi" {
 	require.NoError(t, os.Setenv("ATLAS_TOKEN", "token_atlas"))
 	t.Cleanup(func() { require.NoError(t, os.Unsetenv("ATLAS_TOKEN")) })
 	t.Run("ok", func(t *testing.T) {
-		_, envs, err := EnvByName("local")
+		_, envs, err := EnvByName(&cobra.Command{}, "local")
 		require.NoError(t, err)
 		require.Len(t, envs, 1)
 		env := envs[0]
@@ -160,7 +161,7 @@ env "multi" {
 		require.EqualValues(t, []string{"local/app.hcl"}, sources)
 	})
 	t.Run("multi", func(t *testing.T) {
-		_, envs, err := EnvByName("multi")
+		_, envs, err := EnvByName(&cobra.Command{}, "multi")
 		require.NoError(t, err)
 		require.Len(t, envs, 1)
 		srcs, err := envs[0].Sources()
@@ -168,7 +169,7 @@ env "multi" {
 		require.EqualValues(t, []string{"./a.hcl", "./b.hcl"}, srcs)
 	})
 	t.Run("with input", func(t *testing.T) {
-		_, envs, err := EnvByName("local", WithInput(map[string]cty.Value{
+		_, envs, err := EnvByName(&cobra.Command{}, "local", WithInput(map[string]cty.Value{
 			"name": cty.StringVal("goodbye"),
 		}))
 		require.NoError(t, err)
@@ -180,12 +181,12 @@ env "multi" {
 		require.EqualValues(t, "goodbye", val)
 	})
 	t.Run("wrong env", func(t *testing.T) {
-		_, _, err = EnvByName("home")
+		_, _, err = EnvByName(&cobra.Command{}, "home")
 		require.EqualError(t, err, `env "home" not defined in project file`)
 	})
 	t.Run("wrong dir", func(t *testing.T) {
 		GlobalFlags.ConfigURL = defaultConfigPath
-		_, _, err = EnvByName("home")
+		_, _, err = EnvByName(&cobra.Command{}, "home")
 		require.ErrorContains(t, err, `no such file or directory`)
 	})
 }
@@ -204,7 +205,7 @@ env {
 	err := os.WriteFile(path, []byte(h), 0600)
 	require.NoError(t, err)
 	GlobalFlags.ConfigURL = "file://" + path
-	_, envs, err := EnvByName("local")
+	_, envs, err := EnvByName(&cobra.Command{}, "local")
 	require.NoError(t, err)
 	require.Len(t, envs, 1)
 	require.Equal(t, "local", envs[0].Name)
@@ -244,7 +245,7 @@ diff {
 	err := os.WriteFile(path, []byte(h), 0600)
 	require.NoError(t, err)
 	GlobalFlags.ConfigURL = "file://" + path
-	project, envs, err := EnvByName("")
+	project, envs, err := EnvByName(&cobra.Command{}, "")
 	require.NoError(t, err)
 	require.Len(t, envs, 0)
 	require.Equal(t, 1, project.Lint.Latest)
@@ -280,13 +281,13 @@ env "dev" {
 	err := os.WriteFile(path, []byte(h), 0600)
 	require.NoError(t, err)
 	GlobalFlags.ConfigURL = "file://" + path
-	_, envs, err := EnvByName("unnamed")
+	_, envs, err := EnvByName(&cobra.Command{}, "unnamed")
 	require.NoError(t, err)
 	require.Len(t, envs, 1)
 	require.Equal(t, "unnamed", envs[0].Name)
 	require.Equal(t, "b", envs[0].Format.Schema.Diff)
 	require.Equal(t, "env: unnamed", envs[0].Format.Schema.Apply)
-	_, envs, err = EnvByName("dev")
+	_, envs, err = EnvByName(&cobra.Command{}, "dev")
 	require.NoError(t, err)
 	require.Len(t, envs, 2)
 	for _, e := range envs {
diff --git a/cmd/atlas/internal/cmdext/cmdext_test.go b/cmd/atlas/internal/cmdext/cmdext_test.go
index c4e918a3..a4bbdf06 100644
--- a/cmd/atlas/internal/cmdext/cmdext_test.go
+++ b/cmd/atlas/internal/cmdext/cmdext_test.go
@@ -25,6 +25,7 @@ import (
 	"ariga.io/atlas/sql/sqlclient"
 	_ "ariga.io/atlas/sql/sqlite"
 	"ariga.io/atlas/sql/sqltool"
+	"gocloud.dev/runtimevar"
 
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/require"
@@ -483,6 +484,7 @@ cloud_keys = keys(atlas.cloud)
 	require.Equal(t, []string{"client", "project"}, v.CloudKeys, "token and url should not be exported")
 	// Config options should be populated from the init block.
 	require.NotNil(t, cfg.Client)
+	require.Equal(t, "token", cfg.Token)
 	require.Equal(t, "atlasgo.io", cfg.Project)
 
 	err = state.EvalBytes([]byte(`
@@ -602,3 +604,16 @@ func backupEnv(keys ...string) (restoreFunc func()) {
 		}
 	}
 }
+
+func TestOpenVariable(t *testing.T) {
+	t.Setenv("FOO", "bar")
+	ctx := context.Background()
+	v, err := runtimevar.OpenVariable(ctx, "env://FOO")
+	if err != nil {
+		return
+	}
+	defer v.Close()
+	snapshot, err := v.Watch(ctx)
+	require.NoError(t, err)
+	require.Equal(t, "bar", snapshot.Value)
+}
diff --git a/cmd/atlas/internal/migrate/migrate_test.go b/cmd/atlas/internal/migrate/migrate_test.go
index 02fe2379..9c37b198 100644
--- a/cmd/atlas/internal/migrate/migrate_test.go
+++ b/cmd/atlas/internal/migrate/migrate_test.go
@@ -7,17 +7,37 @@ package migrate
 import (
 	"context"
 	"errors"
+	"net/url"
 	"testing"
 	"time"
 
 	"ariga.io/atlas/cmd/atlas/internal/migrate/ent/revision"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/sqlclient"
+	"ariga.io/atlas/sql/sqltool"
 
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/require"
 )
 
+func TestFormatter(t *testing.T) {
+	u, err := url.Parse("file://migrations")
+	require.NoError(t, err)
+	f, err := Formatter(u)
+	require.NoError(t, err)
+	require.Equal(t, migrate.DefaultFormatter, f)
+
+	u, err = url.Parse("file://migrations?format=atlas")
+	require.NoError(t, err)
+	f, err = Formatter(u)
+
+	u, err = url.Parse("file://migrations?format=flyway")
+	require.NoError(t, err)
+	f, err = Formatter(u)
+	require.NoError(t, err)
+	require.Equal(t, sqltool.FlywayFormatter, f)
+}
+
 func TestRevisionsForClient(t *testing.T) {
 	ctx := context.Background()
 	c, err := sqlclient.Open(ctx, "sqlite://?mode=memory")
diff --git a/internal/integration/cockroach_test.go b/internal/integration/cockroach_test.go
index 293fca56..2851b360 100644
--- a/internal/integration/cockroach_test.go
+++ b/internal/integration/cockroach_test.go
@@ -897,10 +897,6 @@ func (t *crdbTest) realm() *schema.Realm {
 				Name: "public",
 			},
 		},
-		Attrs: []schema.Attr{
-			&schema.Collation{V: "C.UTF-8"},
-			&postgres.CType{V: "C.UTF-8"},
-		},
 	}
 	r.Schemas[0].Realm = r
 	return r
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index cf1c67e8..c118e12d 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -1611,10 +1611,6 @@ func (t *pgTest) realm() *schema.Realm {
 				},
 			},
 		},
-		Attrs: []schema.Attr{
-			&schema.Collation{V: "en_US.utf8"},
-			&postgres.CType{V: "en_US.utf8"},
-		},
 	}
 	r.Schemas[0].Realm = r
 	return r
diff --git a/internal/integration/testdata/sqlite/cli-migrate-diff-sql.txt b/internal/integration/testdata/sqlite/cli-migrate-diff-sql.txt
index d93ac1cb..b694117c 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-diff-sql.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-diff-sql.txt
@@ -4,6 +4,9 @@ cmpmig 0 diff.sql
 atlas migrate diff --dev-url sqlite://dev --to file://schema.sql --dir file://migrations
 stdout 'The migration directory is synced with the desired state, no changes to be made'
 
+atlas schema diff --dev-url sqlite://dev?mode=memory --from file://migrations --to file://schema.sql --exclude atlas_schema_revisions
+stdout 'Schemas are synced, no changes to be made.'
+
 -- schema.sql --
 -- Create "records" table
 CREATE TABLE IF NOT EXISTS `records` (
@@ -13,6 +16,21 @@ CREATE TABLE IF NOT EXISTS `records` (
 );
 CREATE INDEX `records_price` ON `records` (`price`);
 
+-- Create "categories" table
+CREATE TABLE IF NOT EXISTS categories (
+  id INTEGER NOT NULL PRIMARY KEY,
+  category_name VARCHAR(255) NOT NULL
+);
+
+-- Create "products" table
+CREATE TABLE IF NOT EXISTS products (
+  id INTEGER NOT NULL PRIMARY KEY,
+  product_name VARCHAR(255) NOT NULL UNIQUE,
+  price DECIMAL(10,2) NOT NULL,
+  category_id INTEGER,
+  FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE SET NULL
+);
+
 -- diff.sql --
 -- Create "records" table
 CREATE TABLE `records` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` varchar NOT NULL DEFAULT '', `price` decimal NOT NULL DEFAULT 0);
@@ -20,3 +38,9 @@ CREATE TABLE `records` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name`
 CREATE UNIQUE INDEX `records_name` ON `records` (`name`);
 -- Create index "records_price" to table: "records"
 CREATE INDEX `records_price` ON `records` (`price`);
+-- Create "categories" table
+CREATE TABLE `categories` (`id` integer NOT NULL, `category_name` varchar NOT NULL, PRIMARY KEY (`id`));
+-- Create "products" table
+CREATE TABLE `products` (`id` integer NOT NULL, `product_name` varchar NOT NULL, `price` decimal NOT NULL, `category_id` integer NULL, PRIMARY KEY (`id`), CONSTRAINT `0` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON UPDATE NO ACTION ON DELETE SET NULL);
+-- Create index "products_product_name" to table: "products"
+CREATE UNIQUE INDEX `products_product_name` ON `products` (`product_name`);
\ No newline at end of file
diff --git a/schemahcl/stdlib_test.go b/schemahcl/stdlib_test.go
index c7933d4a..cdb9fc58 100644
--- a/schemahcl/stdlib_test.go
+++ b/schemahcl/stdlib_test.go
@@ -111,7 +111,7 @@ func TestURLQuerySetFunc(t *testing.T) {
 func TestURLEscapeFunc(t *testing.T) {
 	for _, tt := range []string{"foo", "foo?", "foo&"} {
 		t.Run(tt, func(t *testing.T) {
-			got, err := urlEscape.Call([]cty.Value{cty.StringVal(tt)})
+			got, err := urlEscapeFunc.Call([]cty.Value{cty.StringVal(tt)})
 			if err != nil {
 				t.Fatalf("unexpected error: %s", err)
 			}
@@ -122,6 +122,22 @@ func TestURLEscapeFunc(t *testing.T) {
 	}
 }
 
+func TestURLUserinfoFunc(t *testing.T) {
+	u := cty.StringVal("mysql://localhost:3306")
+	// Only user is provided
+	got, err := urlUserinfoFunc.Call([]cty.Value{u, cty.StringVal("user")})
+	require.NoError(t, err)
+	require.Equal(t, cty.StringVal("mysql://user@localhost:3306"), got)
+	// The password is null
+	got, err = urlUserinfoFunc.Call([]cty.Value{u, cty.StringVal("user"), cty.NullVal(cty.String)})
+	require.NoError(t, err)
+	require.Equal(t, cty.StringVal("mysql://user@localhost:3306"), got)
+	// Both user and password are provided
+	got, err = urlUserinfoFunc.Call([]cty.Value{u, cty.StringVal("user"), cty.StringVal("pass")})
+	require.NoError(t, err)
+	require.Equal(t, cty.StringVal("mysql://user:pass@localhost:3306"), got)
+}
+
 func TestMakeFileFunc(t *testing.T) {
 	fn := MakeFileFunc("testdata")
 	_, err := fn.Call([]cty.Value{cty.StringVal("foo")})
diff --git a/sql/postgres/inspect_test.go b/sql/postgres/inspect_test.go
index f95331d1..edf0c170 100644
--- a/sql/postgres/inspect_test.go
+++ b/sql/postgres/inspect_test.go
@@ -431,12 +431,10 @@ func TestDriver_InspectCRDBSchema(t *testing.T) {
 	mk := mock{m}
 	mk.ExpectQuery(sqltest.Escape(paramsQuery)).
 		WillReturnRows(sqltest.Rows(`
-					setting
-				------------
-				130000
-				en_US.utf8
-				en_US.utf8
-				cockroach
+	setting
+------------
+130000
+cockroach
 				`))
 	drv, err := Open(db)
 	require.NoError(t, err)
@@ -527,15 +525,6 @@ func TestDriver_InspectSchema(t *testing.T) {
 			Schemas: []*schema.Schema{
 				schema.New("test").SetComment("boring"),
 			},
-			// Server default configuration.
-			Attrs: []schema.Attr{
-				&schema.Collation{
-					V: "en_US.utf8",
-				},
-				&CType{
-					V: "en_US.utf8",
-				},
-			},
 		}
 		r.Schemas[0].Realm = r
 		return r.Schemas[0]
@@ -575,15 +564,6 @@ func TestDriver_Realm(t *testing.T) {
 					Name: "public",
 				},
 			},
-			// Server default configuration.
-			Attrs: []schema.Attr{
-				&schema.Collation{
-					V: "en_US.utf8",
-				},
-				&CType{
-					V: "en_US.utf8",
-				},
-			},
 		}
 		r.Schemas[0].Realm = r
 		r.Schemas[1].Realm = r
@@ -618,15 +598,6 @@ func TestDriver_Realm(t *testing.T) {
 					Name: "public",
 				},
 			},
-			// Server default configuration.
-			Attrs: []schema.Attr{
-				&schema.Collation{
-					V: "en_US.utf8",
-				},
-				&CType{
-					V: "en_US.utf8",
-				},
-			},
 		}
 		r.Schemas[0].Realm = r
 		r.Schemas[1].Realm = r
@@ -656,15 +627,6 @@ func TestDriver_Realm(t *testing.T) {
 					Name: "test",
 				},
 			},
-			// Server default configuration.
-			Attrs: []schema.Attr{
-				&schema.Collation{
-					V: "en_US.utf8",
-				},
-				&CType{
-					V: "en_US.utf8",
-				},
-			},
 		}
 		r.Schemas[0].Realm = r
 		return r
@@ -698,15 +660,6 @@ func TestInspectMode_InspectRealm(t *testing.T) {
 					Name: "public",
 				},
 			},
-			// Server default configuration.
-			Attrs: []schema.Attr{
-				&schema.Collation{
-					V: "en_US.utf8",
-				},
-				&CType{
-					V: "en_US.utf8",
-				},
-			},
 		}
 		r.Schemas[0].Realm = r
 		r.Schemas[1].Realm = r
@@ -737,8 +690,6 @@ func (m mock) version(version string) {
   setting
 ------------
  ` + version + `
- en_US.utf8
- en_US.utf8
 `))
 }