Skip to content

Commit 3b41c41

Browse files
committed
v0
0 parents  commit 3b41c41

24 files changed

Lines changed: 5543 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
branches:
9+
- master
10+
11+
jobs:
12+
build-and-test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-go@v5
18+
with:
19+
go-version-file: go.mod
20+
21+
- name: Build
22+
run: go build -o mbc ./cmd/mbc
23+
24+
- name: Test
25+
run: go test ./...

.github/workflows/release.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
create-release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Create GitHub Release
18+
env:
19+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
run: gh release create "${{ github.ref_name }}" --generate-notes
21+
22+
build:
23+
needs: create-release
24+
runs-on: ubuntu-latest
25+
strategy:
26+
matrix:
27+
include:
28+
- goos: linux
29+
goarch: amd64
30+
suffix: linux-amd64
31+
- goos: linux
32+
goarch: arm64
33+
suffix: linux-arm64
34+
- goos: darwin
35+
goarch: amd64
36+
suffix: darwin-amd64
37+
- goos: darwin
38+
goarch: arm64
39+
suffix: darwin-arm64
40+
41+
steps:
42+
- uses: actions/checkout@v4
43+
44+
- uses: actions/setup-go@v5
45+
with:
46+
go-version-file: go.mod
47+
48+
- name: Build ${{ matrix.suffix }}
49+
env:
50+
GOOS: ${{ matrix.goos }}
51+
GOARCH: ${{ matrix.goarch }}
52+
CGO_ENABLED: "0"
53+
run: go build -o mbc-${{ matrix.suffix }} ./cmd/mbc
54+
55+
- name: Upload to release
56+
env:
57+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
run: |
59+
gh release upload "${{ github.ref_name }}" \
60+
mbc-${{ matrix.suffix }} \
61+
--clobber

CHANGELOG.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Changelog
2+
3+
## 2026-03-06
4+
5+
### New commands
6+
7+
- `mbc check-bq` — BigQuery dry-run only; no Metabase auth required; reports estimated bytes/cost per card
8+
- `mbc check-metabase` — Metabase execution only; skips BQ dry-run
9+
- `--max-cost` flag on `check`/`check-bq`: skip cards whose estimated BQ cost exceeds threshold
10+
- `mbc add-table` — add a new table ref and sync its BQ schema in one step
11+
12+
### Enhancements
13+
14+
- `mbc import-existing --id <N>` now auto-detects resource type (card or dashboard) via Metabase API — replaces the old `--card-id`/`--dashboard-id` flags
15+
- Disorder and vehicle card date filters now default to `past30days~` so dashboards load with data out of the box
16+
- Exposure filenames now include the resource type prefix (`dashboard_station_disorder_overview.yml` instead of `station_disorder_overview.yml`) to avoid name collisions between cards and dashboards
17+
- `mbc exposures` cleans up stale `.yml` files in the output directory when resources are removed or renamed
18+
- `mbc sync-bq-schema` validates `partition_overrides` entries against BQ schema and warns on columns not found
19+
20+
## 2026-03-02
21+
22+
### New commands
23+
24+
- `mbc diff` — show what would change before pushing, without making changes; `--pull` flag for pull perspective
25+
- `mbc sync-schema` — fetch and cache Metabase database metadata to `resources/schema.yaml` for offline field-ID resolution
26+
- `mbc import-existing --card-id <id>` / `--dashboard-id <id>` — bootstrap a local YAML from an untracked Metabase resource; suggests `ref()` substitutions for hardcoded table names
27+
- `mbc delete` — delete resources from Metabase by selector; prompts to remove YAML files afterward
28+
29+
### Enhancements
30+
31+
- `mbc push --interactive`: TUI checkbox to select which resources to push before committing
32+
- `mbc push --target prod` now skips unchanged existing resources (shows diff first, only pushes what changed)
33+
- `mbc check` now runs card checks in parallel (capped by `--jobs`)
34+
- `mbc list` shows collection in a fixed-width column and supports `--collection` filter
35+
- `mbc validate` checks that `template_tags` dimension refs exist in `refs.yaml` and warns on unrecognized inline YAML keys
36+
- `mbc exposures` works without `METABASE_HOST` set — generates files without URLs
37+
- `--jobs` flag (default 8): cap parallel API calls across all commands
38+
- Schema cache (`resources/schema.yaml`) is auto-loaded on every command that uses a Metabase client
39+
40+
### Bug fixes
41+
42+
- Dashboard pull now syncs card layout (position, parameter mappings) — previously discarded on pull (BUG-5)
43+
- Dashboard diff now compares card positions and parameter mappings, not just card count (BUG-6)
44+
- `UpdateCard` error on new card creation was silently ignored (BUG-1)
45+
- SQL metadata header `refs` and `tables` fields were always empty (BUG-2)
46+
- `ResolveRefs` corrupted refs without a project prefix (BUG-3)
47+
- Duplicate resource slugs silently overwrote each other at load time (BUG-7)
48+
- Filename/slug mismatch created a ghost YAML file (BUG-8)
49+
50+
## 2026-02-27
51+
52+
- `prod_id` now stored directly in resource YAML files — the separate state branch is eliminated
53+
- New `--new-only` flag on `mbc push`: skip resources that already have an ID for the target (works for both dev and prod)
54+
- New `--yes` flag on `mbc push --target prod`: skip interactive confirmation (for CI use)
55+
- New `dev-only` tag: resources tagged `dev-only` are skipped when pushing to prod
56+
- `mbc push --target prod` shows a diff and prompts for confirmation before updating existing resources
57+
- `mbc validate --target prod` checks that all non-dev-only resources have a `prod_id`
58+
- `mbc exposures --check`: verifies committed exposure files are up to date; wired into CI validate job
59+
- CI `validate` job now checks formatting, prod IDs, and exposures in sequence
60+
- CI `push-prod` simplified: no state branch checkout or commit step, `contents: read` permission only
61+
- YAML resources standardised to 2-space indentation

cmd/mbc/interactive.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/birotaio/mbc/internal/models"
9+
"golang.org/x/term"
10+
)
11+
12+
// SelectResources shows an interactive TUI for selecting which resources to push.
13+
// All items start checked. Arrow keys move the cursor, Space toggles, Enter confirms, q/Esc cancels.
14+
// Returns the selected subset; falls back to returning all if the terminal isn't a TTY.
15+
func SelectResources(cards []*models.Card, dashes []*models.Dashboard) ([]*models.Card, []*models.Dashboard) {
16+
type item struct {
17+
label string
18+
checked bool
19+
isCard bool
20+
idx int
21+
}
22+
23+
items := make([]item, 0, len(cards)+len(dashes))
24+
for i, c := range cards {
25+
items = append(items, item{
26+
label: fmt.Sprintf("%-45s (card)", c.Slug),
27+
checked: true,
28+
isCard: true,
29+
idx: i,
30+
})
31+
}
32+
for i, d := range dashes {
33+
items = append(items, item{
34+
label: fmt.Sprintf("%-45s (dashboard)", d.Slug),
35+
checked: true,
36+
isCard: false,
37+
idx: i,
38+
})
39+
}
40+
41+
if len(items) == 0 {
42+
return cards, dashes
43+
}
44+
45+
fd := int(os.Stdin.Fd())
46+
if !term.IsTerminal(fd) {
47+
return cards, dashes
48+
}
49+
50+
oldState, err := term.MakeRaw(fd)
51+
if err != nil {
52+
return cards, dashes
53+
}
54+
defer term.Restore(fd, oldState)
55+
56+
cursor := 0
57+
nLines := len(items) + 1 // header + items
58+
59+
renderAll := func(first bool) {
60+
if !first {
61+
// Move cursor up to the header line and overwrite.
62+
fmt.Print(strings.Repeat("\033[A", nLines))
63+
}
64+
fmt.Printf("\033[2K\r Space=toggle \u2191\u2193=move Enter=confirm q=quit\r\n")
65+
for i, it := range items {
66+
check := "[ ]"
67+
if it.checked {
68+
check = "[x]"
69+
}
70+
arrow := " "
71+
if i == cursor {
72+
arrow = "> "
73+
}
74+
fmt.Printf("\033[2K\r %s %s %s\r\n", arrow, check, it.label)
75+
}
76+
}
77+
78+
renderAll(true)
79+
80+
buf := make([]byte, 4)
81+
for {
82+
n, err := os.Stdin.Read(buf)
83+
if err != nil || n == 0 {
84+
break
85+
}
86+
b := buf[:n]
87+
88+
switch {
89+
case n >= 3 && b[0] == 0x1b && b[1] == '[' && b[2] == 'A': // up arrow
90+
if cursor > 0 {
91+
cursor--
92+
}
93+
case n >= 3 && b[0] == 0x1b && b[1] == '[' && b[2] == 'B': // down arrow
94+
if cursor < len(items)-1 {
95+
cursor++
96+
}
97+
case n == 1 && b[0] == ' ': // space: toggle
98+
items[cursor].checked = !items[cursor].checked
99+
case n == 1 && (b[0] == '\r' || b[0] == '\n'): // enter: confirm
100+
renderAll(false)
101+
term.Restore(fd, oldState)
102+
103+
var selCards []*models.Card
104+
var selDashes []*models.Dashboard
105+
for _, it := range items {
106+
if it.checked {
107+
if it.isCard {
108+
selCards = append(selCards, cards[it.idx])
109+
} else {
110+
selDashes = append(selDashes, dashes[it.idx])
111+
}
112+
}
113+
}
114+
return selCards, selDashes
115+
case n == 1 && (b[0] == 'q' || b[0] == 0x1b): // q or Esc: cancel
116+
renderAll(false)
117+
term.Restore(fd, oldState)
118+
fmt.Println("Aborted.")
119+
os.Exit(0)
120+
}
121+
122+
renderAll(false)
123+
}
124+
125+
return cards, dashes
126+
}

0 commit comments

Comments
 (0)