Skip to content

Commit b624e25

Browse files
committed
Add project readiness status
1 parent fa59d09 commit b624e25

8 files changed

Lines changed: 565 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- Added identity realm/client candidate validation for the Okta/Auth0 assessment path.
3232
- Added edge VCL, HAProxy, and Coraza candidate validation for the Cloudflare/Akamai assessment path.
3333
- Added LiteLLM/vLLM config candidate validation for the OpenAI/Anthropic assessment path.
34+
- Added project readiness status with pipeline summaries, export readiness, next actions, and `--json` output.
3435
- Updated CI and release workflows to Node.js 24-native GitHub Actions.
3536

3637
## 0.1.0 - 2026-05-24

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ make example VERSION=0.1.0-dev
5757

5858
- `openexit version`
5959
- `openexit init <project-dir> [--source <type> --target <type>]`
60-
- `openexit status --project <project-dir>`
60+
- `openexit status --project <project-dir> [--json]`
6161
- `openexit collect fixture --project <project-dir> --input <file>`
6262
- `openexit collect github --project <project-dir> --owner <org> [--base-url https://github.example.com/api/v3] [--token-env GITHUB_TOKEN] [--repo owner/name]`
6363
- `openexit collect github-fixture --project <project-dir> --input <file>`
@@ -98,6 +98,7 @@ Fixture workflows run the full local OpenExit workflow with sample or customer-p
9898
Included in the current implementation:
9999

100100
- CLI skeleton and project init/status.
101+
- Project readiness status with pipeline counts, validation state, export readiness, and JSON output for automation.
101102
- Typed project, inventory, assessment, mapping, and validation manifests.
102103
- Fixture-based Datadog inventory import.
103104
- Read-only Datadog collection for dashboards, monitors, SLOs, installed integration metadata, and referenced metric/tag metadata.

docs/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ The CI test suite also runs this definition-of-done pipeline against the fixture
2727

2828
The checked-in Datadog example can be refreshed with `make example VERSION=0.1.0-dev`. CI also runs `make example-smoke` as part of `make verify` to ensure the example fixture still completes the full pipeline.
2929

30+
`openexit status --project <project-dir>` summarizes the current pipeline state: project layout, source/target pair, inventory counts, assessment finding severity, mapping counts, generated candidate artifacts, validation check totals, export readiness, and the next recommended command. Use `--json` to feed the same readiness data into automation or release gates.
31+
3032
`openexit validate` performs typed consistency checks, embedded JSON Schema validation, Grafana dashboard candidate validation, Prometheus alert-rule candidate validation, OpenTelemetry collector candidate validation, ArgoCD candidate validation, Forgejo migration candidate validation, identity realm/client candidate validation, edge VCL/HAProxy/Coraza candidate validation, LiteLLM/vLLM candidate validation, YAML/JSON parse checks, evidence reference checks, secret scanning, and optional external tool checks when `promtool` or `kubeconform` are installed.
3133

3234
`openexit export` refuses to package symlinks from exported project sections, even with `--force`, so evidence bundles cannot accidentally include files from outside the project tree.

docs/release.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The implementation plan names Datadog to Grafana LGTM, Prometheus-compatible ale
88

99
Release-blocking v0.1 requirements:
1010

11-
- CLI skeleton, project init/status, version command.
11+
- CLI skeleton, project init, readiness status, version command.
1212
- Datadog fixture collector and read-only live Datadog collector.
1313
- Inventory and assessment manifests with typed validation.
1414
- Source-to-target mapping manifest with candidate paths and manual-review entries.
@@ -41,6 +41,7 @@ The AI provider path is complete for local fixture assessment workflows and incl
4141
`init`, `collect fixture`, `assess`, `map`, `generate --all`, `validate`, `export`.
4242
- [ ] GitHub, Okta, Auth0, Cloudflare, Akamai, OpenAI, and Anthropic fixture/live-collector test coverage pass, and supported fixture provider pipelines validate.
4343
- [ ] `openexit version` prints name, version, commit, and date from release build flags.
44+
- [ ] `openexit status --project <demo>` reports inventory, assessment, mapping, generated artifacts, validation status, export readiness, and matching `--json` output.
4445
- [ ] `README.md`, `docs/cli.md`, `docs/security.md`, and this checklist reflect current behavior.
4546
- [ ] `examples/datadog-to-grafana/README.md` reproduces the primary local demo.
4647
- [ ] `assessment/openexit.migration-plan.yaml`, `.json`, and `migration-plan.md` are generated by the demo pipeline and included in exported bundles.

internal/app/command.go

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"os"
89
"path/filepath"
10+
"sort"
911
"strconv"
1012
"strings"
1113
"time"
@@ -79,25 +81,152 @@ func newInitCommand() *cobra.Command {
7981

8082
func newStatusCommand() *cobra.Command {
8183
var project string
84+
var jsonOutput bool
8285
cmd := &cobra.Command{
8386
Use: "status",
84-
Short: "Validate an OpenExit project layout",
87+
Short: "Show OpenExit project readiness",
8588
RunE: func(cmd *cobra.Command, args []string) error {
8689
status, err := CheckProject(project)
90+
if jsonOutput {
91+
enc := json.NewEncoder(cmd.OutOrStdout())
92+
enc.SetIndent("", " ")
93+
if encErr := enc.Encode(status); encErr != nil {
94+
return encErr
95+
}
96+
return err
97+
}
8798
if err != nil {
8899
return err
89100
}
101+
writeProjectStatus(cmd.OutOrStdout(), status)
90102
if len(status.Missing) > 0 {
91103
return fmt.Errorf("project is missing required directories: %s", strings.Join(status.Missing, ", "))
92104
}
93-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "project ok: %s\n", status.ProjectDir)
94105
return nil
95106
},
96107
}
97108
cmd.Flags().StringVar(&project, "project", ".", "OpenExit project directory")
109+
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Write machine-readable project status")
98110
return cmd
99111
}
100112

113+
func writeProjectStatus(w io.Writer, status *ProjectStatus) {
114+
_, _ = fmt.Fprintf(w, "project: %s\n", status.ProjectDir)
115+
_, _ = fmt.Fprintf(w, "source: %s\n", status.Source)
116+
_, _ = fmt.Fprintf(w, "target: %s\n", status.Target)
117+
if len(status.Missing) > 0 {
118+
_, _ = fmt.Fprintf(w, "layout: missing %s\n", strings.Join(status.Missing, ", "))
119+
} else {
120+
_, _ = fmt.Fprintln(w, "layout: ok")
121+
}
122+
_, _ = fmt.Fprintf(w, "inventory: %s\n", formatInventoryStatus(status.Inventory))
123+
_, _ = fmt.Fprintf(w, "assessment: %s\n", formatAssessmentStatus(status.Assessment))
124+
_, _ = fmt.Fprintf(w, "mapping: %s\n", formatMappingStatus(status.Mapping))
125+
_, _ = fmt.Fprintf(w, "generated: %s\n", formatGeneratedStatus(status.Generated))
126+
_, _ = fmt.Fprintf(w, "validation: %s\n", formatValidationStatus(status.Validation))
127+
if status.ReadyForExport {
128+
_, _ = fmt.Fprintln(w, "export-ready: yes")
129+
} else {
130+
_, _ = fmt.Fprintln(w, "export-ready: no")
131+
}
132+
for _, action := range status.NextActions {
133+
_, _ = fmt.Fprintf(w, "next: %s\n", action)
134+
}
135+
}
136+
137+
func formatInventoryStatus(status InventoryStatus) string {
138+
if !status.Present {
139+
return "missing"
140+
}
141+
text := "present"
142+
if counts := formatCounts(status.Assets); counts != "" {
143+
text += " (" + counts + ")"
144+
}
145+
if status.Warnings > 0 {
146+
text += fmt.Sprintf(" warnings=%d", status.Warnings)
147+
}
148+
if status.Error != "" {
149+
text += " error=" + status.Error
150+
}
151+
return text
152+
}
153+
154+
func formatAssessmentStatus(status AssessmentStatus) string {
155+
if !status.Present {
156+
return "missing"
157+
}
158+
parts := []string{fmt.Sprintf("findings=%d", status.Findings)}
159+
if severity := formatCounts(status.Severity); severity != "" {
160+
parts = append(parts, severity)
161+
}
162+
if status.Score > 0 {
163+
parts = append(parts, fmt.Sprintf("score=%d", status.Score))
164+
}
165+
if status.Level != "" {
166+
parts = append(parts, "level="+status.Level)
167+
}
168+
if status.Warnings > 0 {
169+
parts = append(parts, fmt.Sprintf("warnings=%d", status.Warnings))
170+
}
171+
text := "present (" + strings.Join(parts, " ") + ")"
172+
if status.Error != "" {
173+
text += " error=" + status.Error
174+
}
175+
return text
176+
}
177+
178+
func formatMappingStatus(status MappingStatus) string {
179+
if !status.Present {
180+
return "missing"
181+
}
182+
text := fmt.Sprintf("present (dashboardDrafts=%d alertRuleDrafts=%d unsupportedItems=%d manualReview=%d)", status.DashboardDrafts, status.AlertRuleDrafts, status.UnsupportedItems, status.ManualReview)
183+
if status.Error != "" {
184+
text += " error=" + status.Error
185+
}
186+
return text
187+
}
188+
189+
func formatGeneratedStatus(status GeneratedStatus) string {
190+
if !status.Present {
191+
if status.Error != "" {
192+
return "missing error=" + status.Error
193+
}
194+
return "missing"
195+
}
196+
text := fmt.Sprintf("present (files=%d candidates=%d)", status.Files, status.Candidates)
197+
if status.Error != "" {
198+
text += " error=" + status.Error
199+
}
200+
return text
201+
}
202+
203+
func formatValidationStatus(status ValidationStatus) string {
204+
if !status.Present {
205+
return "missing"
206+
}
207+
text := fmt.Sprintf("%s (checks=%d passed=%d failed=%d warnings=%d)", status.Status, status.Checks, status.Passed, status.Failed, status.Warnings)
208+
if status.Error != "" {
209+
text += " error=" + status.Error
210+
}
211+
return text
212+
}
213+
214+
func formatCounts(counts map[string]int) string {
215+
if len(counts) == 0 {
216+
return ""
217+
}
218+
keys := make([]string, 0, len(counts))
219+
for key := range counts {
220+
keys = append(keys, key)
221+
}
222+
sort.Strings(keys)
223+
parts := make([]string, 0, len(keys))
224+
for _, key := range keys {
225+
parts = append(parts, fmt.Sprintf("%s=%d", key, counts[key]))
226+
}
227+
return strings.Join(parts, " ")
228+
}
229+
101230
func newCollectCommand() *cobra.Command {
102231
collectCmd := &cobra.Command{
103232
Use: "collect",

internal/app/e2e_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bytes"
66
"crypto/sha256"
77
"encoding/hex"
8+
"encoding/json"
89
"io"
910
"os"
1011
"path/filepath"
@@ -52,6 +53,61 @@ func TestDefinitionOfDonePipelineAndBundle(t *testing.T) {
5253
}
5354
}
5455

56+
func TestStatusReportsPipelineReadiness(t *testing.T) {
57+
projectDir := filepath.Join(t.TempDir(), "demo")
58+
fixturePath := filepath.Join("..", "..", "testdata", "datadog", "small.json")
59+
60+
commands := [][]string{
61+
{"init", projectDir},
62+
{"collect", "fixture", "--project", projectDir, "--input", fixturePath},
63+
{"assess", "--project", projectDir, "--target", "grafana-lgtm"},
64+
{"map", "--project", projectDir},
65+
{"generate", "--project", projectDir, "--all"},
66+
{"validate", "--project", projectDir},
67+
}
68+
for _, args := range commands {
69+
if err := executeForTest(args...); err != nil {
70+
t.Fatalf("openexit %s failed: %v", strings.Join(args, " "), err)
71+
}
72+
}
73+
74+
out, err := executeForTestWithOutput("status", "--project", projectDir)
75+
if err != nil {
76+
t.Fatalf("openexit status failed: %v", err)
77+
}
78+
for _, marker := range []string{
79+
"source: datadog",
80+
"target: grafana-lgtm",
81+
"layout: ok",
82+
"inventory: present",
83+
"assessment: present",
84+
"mapping: present",
85+
"generated: present",
86+
"validation: passed",
87+
"export-ready: yes",
88+
"next: openexit export",
89+
} {
90+
if !strings.Contains(out, marker) {
91+
t.Fatalf("expected status marker %q, got:\n%s", marker, out)
92+
}
93+
}
94+
95+
jsonOut, err := executeForTestWithOutput("status", "--project", projectDir, "--json")
96+
if err != nil {
97+
t.Fatalf("openexit status --json failed: %v", err)
98+
}
99+
var status ProjectStatus
100+
if err := json.Unmarshal([]byte(jsonOut), &status); err != nil {
101+
t.Fatalf("decode status JSON: %v\n%s", err, jsonOut)
102+
}
103+
if !status.ReadyForExport || status.Validation.Status != "passed" {
104+
t.Fatalf("expected export-ready passed status, got %+v", status)
105+
}
106+
if status.Inventory.Assets["dashboards"] != 1 || status.Generated.Candidates == 0 || len(status.NextActions) != 1 {
107+
t.Fatalf("unexpected status summary: %+v", status)
108+
}
109+
}
110+
55111
func TestCLICommandFailuresAreActionable(t *testing.T) {
56112
projectDir := filepath.Join(t.TempDir(), "demo")
57113
if err := executeForTest("init", projectDir); err != nil {
@@ -911,6 +967,16 @@ func executeForTest(args ...string) error {
911967
return cmd.Execute()
912968
}
913969

970+
func executeForTestWithOutput(args ...string) (string, error) {
971+
cmd := NewRootCommand()
972+
var out bytes.Buffer
973+
cmd.SetArgs(args)
974+
cmd.SetOut(&out)
975+
cmd.SetErr(io.Discard)
976+
err := cmd.Execute()
977+
return out.String(), err
978+
}
979+
914980
func expectedProjectFiles() []string {
915981
return []string{
916982
"openexit.yaml",

0 commit comments

Comments
 (0)