Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2021,7 +2021,7 @@ schemabot apply -e staging
```


Apply Blocked: Database Locked
Apply blocked: database locked

┌───────────────────────────────────┐
│ Database: testapp (mysql) │
Expand All @@ -2047,7 +2047,7 @@ Options:
```


Apply Blocked: Database Locked
Apply blocked: database locked

┌────────────────────────────────────────────────┐
│ Database: testapp (mysql) │
Expand Down Expand Up @@ -7708,7 +7708,7 @@ schemabot apply -e production
│ Deployments: 1 completed · 1 halted · 1 failed │
└───────────────────────────────────────────────────────┘

First failure: eu-west — duplicate key name 'idx_orders_source'
First failure: eu-west — duplicate key name 'idx_orders_source'

Next: review failure in eu-west

Expand Down Expand Up @@ -8243,7 +8243,7 @@ Lint violations: Non-blocking warnings during plan/apply

Unsafe blocked: Destructive changes require --allow-unsafe

Unsafe Changes Detected:
Apply blocked: 3 unsafe change(s) detected
• users: DROP COLUMN email
• orders: DROP TABLE
• products:
Expand Down
8 changes: 4 additions & 4 deletions docs/lint-and-safety-levels.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ rejected up front while they are present.

| Icon | Where it appears | Meaning |
|---|---|---|
| ⛔ | Plan comment (**Cannot apply**), apply-rejection comments (**Unsafe Changes Detected**, **Apply rejected**, **Apply Blocked: PR Is Merged/Closed**), CLI unsafe-changes list | Refusal: this will not or did not proceed |
| ⚠️ | Plan comment (**Issues**) | Caution: unsafe changes to review before applying |
| ⛔ | Plan comment (**Cannot apply**), apply-rejection comments (**Unsafe Changes Detected**, **Apply rejected**, **Apply Blocked: PR Is Merged/Closed**), CLI apply-blocked headings (**Apply blocked**) | Refusal: this will not or did not proceed |
| ⚠️ | Plan comment (**Issues**), CLI plan output (**Unsafe Changes Detected**) | Caution: unsafe changes to review before applying |
| 🚨 | Apply-rejection comment; CLI apply output | The `--allow-unsafe` instruction, or (CLI) the banner confirming it was supplied |
| ⚙️ | Plan and locked apply comments (**Direct execution**) | Consent disclosure for native-DDL statements |
| 💡 | Plan comment and CLI (**Lint Warnings**) | Advisory best-practice findings |
Expand All @@ -172,5 +172,5 @@ Presentation notes:
the collapsed header. Short lists render inline.
- Quoted identifiers and types inside finding messages (index names, column
types) render as inline code.
- The CLI uses ⛔ for unsafe changes at both plan and apply time, since the
terminal is both the review surface and the gate.
- The CLI and the plan comment share the same severity reading: ⚠️ marks
unsafe changes awaiting review at plan time, and ⛔ marks the refused apply.
8 changes: 5 additions & 3 deletions e2e/local/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ CREATE TABLE %s (
out := e2eutil.RunCLIInDir(t, binPath, schemaDir, "plan", "-e", "staging", "--endpoint", endpoint)
e2eutil.AssertContains(t, out, "DROP COLUMN")
e2eutil.AssertContains(t, out, "legacy_field")
// Unsafe changes should be shown with ⛔ (not ⚠️ lint warning)
// Unsafe changes at plan time await consent, shown with ⚠️
e2eutil.AssertContains(t, out, "Unsafe Changes Detected")
}

Expand Down Expand Up @@ -575,7 +575,8 @@ CREATE TABLE %s (
require.Error(t, err, "expected apply to fail without --allow-unsafe for DROP INDEX")

// Verify the output contains expected messages
e2eutil.AssertContains(t, out, "Unsafe Changes Detected")
e2eutil.AssertContains(t, out, "Apply blocked")
e2eutil.AssertContains(t, out, "unsafe change(s) detected")
e2eutil.AssertContains(t, out, "DROP INDEX")
e2eutil.AssertContains(t, out, "--allow-unsafe")
// Should also show the plan
Expand Down Expand Up @@ -680,7 +681,8 @@ CREATE TABLE %s (
require.Error(t, err, "expected apply to fail without --allow-unsafe for DROP TABLE")

// Verify the output contains expected messages
e2eutil.AssertContains(t, out, "Unsafe Changes Detected")
e2eutil.AssertContains(t, out, "Apply blocked")
e2eutil.AssertContains(t, out, "unsafe change(s) detected")
e2eutil.AssertContains(t, out, "DROP TABLE")
e2eutil.AssertContains(t, out, "--allow-unsafe")
}
Expand Down
9 changes: 6 additions & 3 deletions e2e/local/vitess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,8 @@ func TestVitess_Apply_DropIndex_BlockedWithoutFlag(t *testing.T) {
"-s", ".", "-e", "staging", "--endpoint", endpoint, "-y", "-o", "log")
t.Logf("DROP INDEX apply output:\n%s", out)
require.Error(t, err, "expected apply to fail without --allow-unsafe")
assert.Contains(t, out, "Unsafe Changes Detected")
assert.Contains(t, out, "Apply blocked")
assert.Contains(t, out, "unsafe change(s) detected")
}

// TestVitess_Apply_VSchemaVindexRemoval_BlockedWithoutFlag exercises the
Expand Down Expand Up @@ -1199,7 +1200,8 @@ func TestVitess_Apply_VSchemaVindexRemoval_BlockedWithoutFlag(t *testing.T) {
"-s", ".", "-e", "staging", "--endpoint", endpoint, "-y", "-o", "log")
t.Logf("VSchema vindex removal apply output:\n%s", out)
require.Error(t, err, "expected apply to fail without --allow-unsafe")
assert.Contains(t, out, "Unsafe Changes Detected")
assert.Contains(t, out, "Apply blocked")
assert.Contains(t, out, "unsafe change(s) detected")

// Acknowledging the removal with --allow-unsafe lets the apply proceed.
clearSchemaBotState(t)
Expand Down Expand Up @@ -1250,7 +1252,8 @@ func TestVitess_Apply_VSchemaVindexTypeChange_BlockedWithoutFlag(t *testing.T) {
"-s", ".", "-e", "staging", "--endpoint", endpoint, "-y", "-o", "log")
t.Logf("VSchema vindex type-change apply output:\n%s", out)
require.Error(t, err, "expected apply to fail without --allow-unsafe")
assert.Contains(t, out, "Unsafe Changes Detected")
assert.Contains(t, out, "Apply blocked")
assert.Contains(t, out, "unsafe change(s) detected")

// Acknowledging the mutation with --allow-unsafe lets the apply proceed.
clearSchemaBotState(t)
Expand Down
3 changes: 2 additions & 1 deletion integration/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,8 @@ CREATE TABLE accounts (
// Should exit with error
require.Error(t, err, "expected apply to fail without --allow-unsafe")
// Should show unsafe changes message
assertContains(t, out, "Unsafe Changes Detected")
assertContains(t, out, "Apply blocked")
assertContains(t, out, "unsafe change(s) detected")
assertContains(t, out, "--allow-unsafe")
})

Expand Down
28 changes: 14 additions & 14 deletions integration/locks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ CREATE TABLE users (
"--watch=false",
)
assert.Error(t, err, "expected CLI apply to fail when PR holds lock")
assertContains(t, out, "Database Locked")
assertContains(t, out, "Apply blocked: database locked")
assertContains(t, out, "block/myrepo#123")
assertContains(t, out, "--force")
})
Expand Down Expand Up @@ -574,7 +574,7 @@ CREATE TABLE users (
"--watch=false",
)
assert.Error(t, err, "expected CLI apply to fail when other CLI user holds lock")
assertContains(t, out, "Database Locked")
assertContains(t, out, "Apply blocked: database locked")
assertContains(t, out, "cli:otheruser@othermachine")
})

Expand Down Expand Up @@ -667,8 +667,8 @@ CREATE TABLE users (
"--endpoint", endpoint,
"--watch=false",
)
// Should NOT show "Database Locked" error - cutover doesn't check locks
assert.NotContains(t, stripANSI(out), "Database Locked", "cutover should not be blocked by locking")
// Should NOT show "Apply blocked: database locked" error - cutover doesn't check locks
assert.NotContains(t, stripANSI(out), "Apply blocked: database locked", "cutover should not be blocked by locking")
})

t.Run("stop_works_while_locked", func(t *testing.T) {
Expand All @@ -678,8 +678,8 @@ CREATE TABLE users (
"-e", "staging",
"--endpoint", endpoint,
)
// Should NOT show "Database Locked" error
assert.NotContains(t, stripANSI(out), "Database Locked", "stop should not be blocked by locking")
// Should NOT show "Apply blocked: database locked" error
assert.NotContains(t, stripANSI(out), "Apply blocked: database locked", "stop should not be blocked by locking")
})

t.Run("start_works_while_locked", func(t *testing.T) {
Expand All @@ -690,8 +690,8 @@ CREATE TABLE users (
"--endpoint", endpoint,
"--watch=false",
)
// Should NOT show "Database Locked" error
assert.NotContains(t, stripANSI(out), "Database Locked", "start should not be blocked by locking")
// Should NOT show "Apply blocked: database locked" error
assert.NotContains(t, stripANSI(out), "Apply blocked: database locked", "start should not be blocked by locking")
})

t.Run("volume_works_while_locked", func(t *testing.T) {
Expand All @@ -702,8 +702,8 @@ CREATE TABLE users (
"-v", "5",
"--endpoint", endpoint,
)
// Should NOT show "Database Locked" error
assert.NotContains(t, stripANSI(out), "Database Locked", "volume should not be blocked by locking")
// Should NOT show "Apply blocked: database locked" error
assert.NotContains(t, stripANSI(out), "Apply blocked: database locked", "volume should not be blocked by locking")
})

t.Run("apply_blocked_while_locked", func(t *testing.T) {
Expand All @@ -716,7 +716,7 @@ CREATE TABLE users (
"--watch=false",
)
assert.Error(t, err, "expected apply to fail when locked by another user")
assertContains(t, out, "Database Locked")
assertContains(t, out, "Apply blocked: database locked")
})

// Cleanup
Expand Down Expand Up @@ -762,7 +762,7 @@ CREATE TABLE users (
"--no-lock",
)
// Should not show lock conflict
assert.NotContains(t, stripANSI(out), "Database Locked")
assert.NotContains(t, stripANSI(out), "Apply blocked: database locked")
// Should proceed with apply
assertContains(t, out, "Apply started")
waitForApplyFromOutput(t, endpoint, out, "completed", 30*time.Second)
Expand Down Expand Up @@ -906,7 +906,7 @@ CREATE TABLE users (
"--watch=false",
)
assert.Error(t, err, "expected CLI to be blocked on same environment")
assertContains(t, out, "Database Locked")
assertContains(t, out, "Apply blocked: database locked")
assertContains(t, out, "block/repo#100")
})

Expand Down Expand Up @@ -952,7 +952,7 @@ func TestCLI_Locking_DifferentEnvBlocked(t *testing.T) {
// Document the intended behavior:
// If we had production configured, attempting to apply would fail:
// $ schemabot apply -e production # Would be blocked!
// Error: Database Locked
// Error: Apply blocked: database locked
// Locked by: block/repo#200
// ...
// This prevents concurrent schema changes across environments.
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/commands/fixlint.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/block/schemabot/pkg/cmd/cliname"
"github.com/block/schemabot/pkg/glyph"
"github.com/block/schemabot/pkg/lint"
)

Expand Down Expand Up @@ -70,7 +71,7 @@ func (cmd *FixLintCmd) Run(g *Globals) error {

// Show unfixable issues
if len(result.UnfixableIssues) > 0 {
fmt.Printf("❌ %d issue(s) require manual fix:\n", len(result.UnfixableIssues))
fmt.Printf(glyph.Failed+" %d issue(s) require manual fix:\n", len(result.UnfixableIssues))
for _, issue := range result.UnfixableIssues {
loc := issue.Table
if issue.Column != "" {
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/commands/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func writeEnvPlan(result *apitypes.PlanResponse) {

// writePlanBody writes the plan body (errors, changes, unsafe warnings, lint, summary).
// Used by both writeEnvPlan (plan command) and OutputPlanResult (apply command).
// When isApply is true, the unsafe warning is skipped (apply shows its own 🚨 warning).
// When isApply is true, the ⚠️ unsafe warning is skipped (apply shows its own 🚨 warning).
func writePlanBody(result *apitypes.PlanResponse, isApply bool) {
// Check for errors
if len(result.Errors) > 0 {
Expand Down Expand Up @@ -295,7 +295,7 @@ func writePlanBody(result *apitypes.PlanResponse, isApply bool) {
templates.WriteNamespaceChanges(nsChanges, !isVitess, result.Database)
}

// Check for unsafe changes and show with ⛔ (error level)
// Check for unsafe changes and show with ⚠️ (attention — the changes await consent)
// Skip in apply context — apply shows its own 🚨 warning via WriteUnsafeWarningAllowed
unsafeChanges := result.UnsafeChanges()
if len(unsafeChanges) > 0 && !isApply {
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/commands/watch_tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func TestWatchModel_MultiDeploymentView(t *testing.T) {
view := m.View()

assert.Contains(t, view, "1 completed · 1 halted · 1 failed")
assert.Contains(t, view, " First failure: eu-west — duplicate key name 'idx_orders_source'")
assert.Contains(t, view, " First failure: eu-west — duplicate key name 'idx_orders_source'")
assert.Contains(t, view, "Apply ID: apply-multi-test")
Comment thread
aparajon marked this conversation as resolved.
assert.Contains(t, view, "Environment: production")
assertContainsInOrder(t, view,
Expand Down Expand Up @@ -318,7 +318,7 @@ func TestWatchModel_MultiDeploymentViewRunningDegraded(t *testing.T) {

assert.Contains(t, view, "running (degraded)")
assert.Contains(t, view, "1 running · 1 failed")
assert.Contains(t, view, " First failure: eu — duplicate column")
assert.Contains(t, view, " First failure: eu — duplicate column")
assert.Contains(t, view, "🔄 us — running table copy")
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/cmd/commands/watch_tui_view_multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/charmbracelet/lipgloss"

"github.com/block/schemabot/pkg/cmd/internal/templates"
"github.com/block/schemabot/pkg/glyph"
"github.com/block/schemabot/pkg/presentation"
"github.com/block/schemabot/pkg/state"
"github.com/block/schemabot/pkg/storage"
Expand Down Expand Up @@ -59,9 +60,9 @@ func (m WatchModel) writeMultiDeploymentHeader(b *strings.Builder, model present
if model.FirstFailure != nil {
errStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
if model.FirstFailure.Error != "" {
fmt.Fprintf(b, "%s\n", errStyle.Render(fmt.Sprintf("⚠ First failure: %s — %s", model.FirstFailure.Deployment, model.FirstFailure.Error)))
fmt.Fprintf(b, "%s\n", errStyle.Render(fmt.Sprintf(glyph.Failed+" First failure: %s — %s", model.FirstFailure.Deployment, model.FirstFailure.Error)))
} else {
fmt.Fprintf(b, "%s\n", errStyle.Render(fmt.Sprintf("⚠ First failure: %s", model.FirstFailure.Deployment)))
fmt.Fprintf(b, "%s\n", errStyle.Render(fmt.Sprintf(glyph.Failed+" First failure: %s", model.FirstFailure.Deployment)))
}
}
if m.applyID != "" {
Expand Down
7 changes: 4 additions & 3 deletions pkg/cmd/internal/templates/locks.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/block/schemabot/pkg/cmd/cliname"
"github.com/block/schemabot/pkg/glyph"
"github.com/block/schemabot/pkg/ui"
)

Expand All @@ -31,7 +32,7 @@ func WriteLockReleased(database, dbType string) {

// WriteLockForceReleased writes the force release message.
func WriteLockForceReleased(database, dbType, previousOwner string) {
fmt.Printf("⚠️ Force released lock for %s (%s)\n", database, dbType)
fmt.Printf(glyph.Attention+" Force released lock for %s (%s)\n", database, dbType)
fmt.Printf(" Previous owner: %s\n", previousOwner)
}

Expand All @@ -48,7 +49,7 @@ type LockConflictData struct {
// WriteLockConflict writes the lock conflict error message.
func WriteLockConflict(data LockConflictData) {
fmt.Println()
fmt.Println("❌ Apply Blocked: Database Locked")
fmt.Println(glyph.Refused + " Apply blocked: database locked")
fmt.Println()

// Show a table of lock info
Expand Down Expand Up @@ -124,7 +125,7 @@ func WriteLockTypeScanFailed(err error) {
// WriteUnlockNotOwned writes the message when trying to unlock without ownership.
func WriteUnlockNotOwned(database, dbType, currentOwner string) {
fmt.Println()
fmt.Println("⚠️ Cannot release lock")
fmt.Println(glyph.Attention + " Cannot release lock")
fmt.Println()
fmt.Printf(" Database: %s (%s)\n", database, dbType)
fmt.Printf(" Current owner: %s\n", currentOwner)
Expand Down
17 changes: 11 additions & 6 deletions pkg/cmd/internal/templates/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/block/schemabot/pkg/apitypes"
"github.com/block/schemabot/pkg/cmd/cliname"
"github.com/block/schemabot/pkg/glyph"
"github.com/block/schemabot/pkg/ui"
)

Expand Down Expand Up @@ -465,10 +466,10 @@ func WriteErrors(errors []string) {
// entry) — the namespaces they name are fully reconciled.
func WriteIgnoredNamespaces(ignored, unmatched []string) {
if len(ignored) > 0 {
fmt.Printf("ℹ️ Namespaces excluded by ignore_namespaces: %s\n", strings.Join(ignored, ", "))
fmt.Printf(glyph.Info+" Namespaces excluded by ignore_namespaces: %s\n", strings.Join(ignored, ", "))
}
for _, entry := range unmatched {
fmt.Printf("⚠️ ignore_namespaces entry %q matched no namespace and excluded nothing\n", entry)
fmt.Printf(glyph.Attention+" ignore_namespaces entry %q matched no namespace and excluded nothing\n", entry)
}
if len(ignored) > 0 || len(unmatched) > 0 {
fmt.Println()
Expand All @@ -479,23 +480,27 @@ func WriteIgnoredNamespaces(ignored, unmatched []string) {
type UnsafeChange = apitypes.UnsafeChange

// WriteUnsafeChangesWarning writes a warning about unsafe changes (for plan output).
// At plan time nothing has been refused yet — the changes await consent, so the
// heading carries Attention, matching the PR plan comment and list-plans.
func WriteUnsafeChangesWarning(changes []UnsafeChange) {
if len(changes) == 0 {
return
}
fmt.Println("⛔ Unsafe Changes Detected:")
fmt.Println(glyph.Attention + " Unsafe Changes Detected:")
writeUnsafeChangesList(changes)
fmt.Println()
}

// WriteUnsafeChangesBlocked writes the unsafe changes list and instruction to re-run with --allow-unsafe.
// The apply was refused, so Refused attaches to the refusal itself — the heading
// names the blocked apply, not the unsafeness of the changes.
func WriteUnsafeChangesBlocked(changes []UnsafeChange, database, environment, schemaDir string) {
if len(changes) > 0 {
fmt.Println("⛔ Unsafe Changes Detected:")
fmt.Printf(glyph.Refused+" Apply blocked: %d unsafe change(s) detected\n", len(changes))
writeUnsafeChangesList(changes)
fmt.Println()
}
fmt.Println("🚨 To proceed with these destructive changes, re-run with --allow-unsafe:")
fmt.Println(glyph.Escalation + " To proceed with these destructive changes, re-run with --allow-unsafe:")
fmt.Println()
fmt.Printf(" %s apply -s %s -e %s --allow-unsafe\n", cliname.Name(), schemaDir, environment)
fmt.Println()
Expand All @@ -507,7 +512,7 @@ func WriteUnsafeWarningAllowed(changes []UnsafeChange) {
return
}
fmt.Println()
fmt.Println("🚨 Unsafe Changes (--allow-unsafe enabled)")
fmt.Println(glyph.Escalation + " Unsafe Changes (--allow-unsafe enabled)")
fmt.Println()
fmt.Println("The following unsafe changes will be applied:")
writeUnsafeChangesList(changes)
Expand Down
Loading
Loading