@stndrds/cli 1.0.0-alpha.258 → 1.0.0-alpha.260

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ name: Standards schema/view drift
2
+
3
+ # Runs "standards diff" against the deployed (prod) Standards instance on every
4
+ # PR, so schema/view drift that would be silently overwritten or destroyed on
5
+ # the next deploy is caught at review time instead of after rollout.
6
+ on:
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ standards-drift:
15
+ name: standards diff
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+
26
+ - name: Enable corepack (pnpm)
27
+ run: corepack enable
28
+
29
+ - name: Install dependencies
30
+ run: pnpm install --frozen-lockfile
31
+
32
+ # `standards diff` reads its credentials the same way every other CLI
33
+ # command does (see packages/cli/src/config.ts:resolveCliConfig /
34
+ # getEnvProfile): STANDARDS_API_URL + STANDARDS_API_KEY. These are the
35
+ # exact env var names the client reads — do not rename without updating
36
+ # both the CLI and this workflow.
37
+ #
38
+ # `--format json` is captured for the PR comment step below; the human-
39
+ # readable table is still what a developer sees if they run the same
40
+ # command locally without --format. Stderr is captured separately
41
+ # (`standards diff` prints its exit-2 failure message via
42
+ # `console.error`, i.e. to stderr, never into the JSON stdout) so the
43
+ # comment step has something to show for an infra failure instead of an
44
+ # empty code block.
45
+ - name: Run standards diff
46
+ id: standards-diff
47
+ env:
48
+ STANDARDS_API_URL: ${{ secrets.STANDARDS_API_URL }}
49
+ STANDARDS_API_KEY: ${{ secrets.STANDARDS_API_KEY }}
50
+ run: |
51
+ set +e
52
+ pnpm exec standards diff --format json > standards-diff.json 2> standards-diff.err
53
+ echo "exit_code=$?" >> "$GITHUB_OUTPUT"
54
+ cat standards-diff.json
55
+ cat standards-diff.err >&2
56
+ exit 0
57
+
58
+ # Fork PRs run with a read-only GITHUB_TOKEN (no `pull-requests: write`),
59
+ # so `github.rest.issues.createComment`/`updateComment` below would 403.
60
+ # Skip the comment (not the check itself — "Fail the job" below still
61
+ # runs and still gates the merge) for PRs from forks. See the maintainer
62
+ # note at the bottom of this file.
63
+ - name: Comment drift report on PR
64
+ if: github.event.pull_request.head.repo.full_name == github.repository
65
+ uses: actions/github-script@v7
66
+ with:
67
+ script: |
68
+ const fs = require("fs");
69
+ const exitCode = Number("${{ steps.standards-diff.outputs.exit_code }}");
70
+ const marker = "<!-- standards-drift-report -->";
71
+
72
+ const stdout = fs.existsSync("standards-diff.json") ? fs.readFileSync("standards-diff.json", "utf-8") : "";
73
+ const stderr = fs.existsSync("standards-diff.err") ? fs.readFileSync("standards-diff.err", "utf-8") : "";
74
+
75
+ let report = null;
76
+ if (stdout.trim().length > 0) {
77
+ try {
78
+ report = JSON.parse(stdout);
79
+ } catch {
80
+ // exit code 2 (infra failure) can leave non-JSON stdout (e.g.
81
+ // the process never got far enough to print a report) — the
82
+ // stderr capture above is what carries the actual message then.
83
+ }
84
+ }
85
+
86
+ // `state !== "in-sync"` on its own would lump `code-changed`/
87
+ // `conflict-resolved` (informational — will simply apply, nothing
88
+ // blocking) in with `conflict`/`runtime-diverged` (blocking,
89
+ // needs a developer decision). Keep them in separate tables so
90
+ // the "needs a decision" table is true of every row it lists.
91
+ function renderTable(rows) {
92
+ if (rows.length === 0) return "_None._";
93
+ const header = "| View | State | Changed fields |\n| --- | --- | --- |";
94
+ const body = rows
95
+ .map((v) => `| \`${v.key}\` | ${v.state} | ${(v.changedFields ?? []).join(", ") || "—"} |`)
96
+ .join("\n");
97
+ return `${header}\n${body}`;
98
+ }
99
+
100
+ function renderDecisionViewsTable(views) {
101
+ return renderTable(views.filter((v) => v.state === "conflict" || v.state === "runtime-diverged"));
102
+ }
103
+
104
+ function renderInformationalViewsTable(views) {
105
+ return renderTable(views.filter((v) => v.state === "code-changed" || v.state === "conflict-resolved"));
106
+ }
107
+
108
+ function renderSchemaTable(schema) {
109
+ const lines = [];
110
+ for (const obj of schema?.customObjects ?? []) {
111
+ lines.push(`| \`${obj.name}\` | custom object, not in code |`);
112
+ }
113
+ for (const entry of schema?.systemObjectDrift ?? []) {
114
+ const unexpected = entry.sealed
115
+ ? entry.customAttributes.filter((a) => !a.tolerated)
116
+ : [];
117
+ if (entry.sealed && unexpected.length > 0) {
118
+ lines.push(
119
+ `| \`${entry.objectName}\` | ${unexpected.length} unexpected attribute(s) (sealed) |`
120
+ );
121
+ }
122
+ }
123
+ if (lines.length === 0) return "_No object/attribute drift._";
124
+ return `| Object | Note |\n| --- | --- |\n${lines.join("\n")}`;
125
+ }
126
+
127
+ let body;
128
+ if (exitCode === 2) {
129
+ const details = stderr.trim().length > 0 ? stderr.trim() : stdout.trim().length > 0 ? stdout.trim() : "(no output captured)";
130
+ body = [
131
+ marker,
132
+ "## ⚠️ standards diff — could not run (exit code 2)",
133
+ "",
134
+ "This is an **infra/configuration failure**, not schema or view drift —",
135
+ "the check itself did not complete. Common causes: a missing or invalid",
136
+ "`STANDARDS_API_URL` / `STANDARDS_API_KEY` secret, an unreachable API, or a",
137
+ "schema entry file that fails to load in isolation.",
138
+ "",
139
+ "```",
140
+ details,
141
+ "```",
142
+ ].join("\n");
143
+ } else if (exitCode === 0) {
144
+ body = [marker, "## ✅ standards diff — no blocking drift", "", "Next deploy is a no-op for schema/views."].join(
145
+ "\n"
146
+ );
147
+ } else {
148
+ body = [
149
+ marker,
150
+ "## 🚫 standards diff — unresolved drift (exit code 1)",
151
+ "",
152
+ "### Needs a decision",
153
+ "",
154
+ "These views WOULD have their runtime change overwritten or destroyed by",
155
+ "the next deploy. Run `standards pull` locally to resolve each one (promote",
156
+ "the runtime change into code, or explicitly overwrite it), then push again.",
157
+ "",
158
+ renderDecisionViewsTable(report?.views ?? []),
159
+ "",
160
+ "### Will apply automatically (informational, no action needed)",
161
+ "",
162
+ renderInformationalViewsTable(report?.views ?? []),
163
+ "",
164
+ "### Schema (objects/attributes)",
165
+ "",
166
+ renderSchemaTable(report?.schema ?? {}),
167
+ ].join("\n");
168
+ }
169
+
170
+ const { data: comments } = await github.rest.issues.listComments({
171
+ owner: context.repo.owner,
172
+ repo: context.repo.repo,
173
+ issue_number: context.issue.number,
174
+ });
175
+ const existing = comments.find((c) => c.body?.includes(marker));
176
+
177
+ if (existing) {
178
+ await github.rest.issues.updateComment({
179
+ owner: context.repo.owner,
180
+ repo: context.repo.repo,
181
+ comment_id: existing.id,
182
+ body,
183
+ });
184
+ } else {
185
+ await github.rest.issues.createComment({
186
+ owner: context.repo.owner,
187
+ repo: context.repo.repo,
188
+ issue_number: context.issue.number,
189
+ body,
190
+ });
191
+ }
192
+
193
+ - name: Fail the job on unresolved drift or infra failure
194
+ if: steps.standards-diff.outputs.exit_code != '0'
195
+ run: |
196
+ echo "standards diff exited with code ${{ steps.standards-diff.outputs.exit_code }}"
197
+ exit 1
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Notes for whoever maintains this workflow
201
+ # ---------------------------------------------------------------------------
202
+ #
203
+ # 1. Deploy-time re-check (close the merge -> deploy TOCTOU window):
204
+ # A PR can be green here and still go stale — a runtime user can edit a
205
+ # view in the time between "this PR merged" and "this commit actually
206
+ # deployed". Copy the "Run standards diff" step (same env vars, same
207
+ # command) into your deploy pipeline immediately BEFORE the rollout step,
208
+ # and treat a non-zero exit there as a deploy abort:
209
+ # - exit 1 -> abort the deploy, drift must be resolved first (the exact
210
+ # same resolution flow as above: `standards pull`, or hand this off to
211
+ # the `standards-pull` agent skill).
212
+ # - exit 2 -> abort the deploy, this is an infra failure (bad
213
+ # credentials, unreachable API), not a drift decision.
214
+ # Without this second check, this PR-time gate only protects the instant
215
+ # of merge, not the instant of deploy.
216
+ #
217
+ # 2. v1 assumes single-tenant consumer deployments: one STANDARDS_API_KEY
218
+ # maps to exactly one tenant. If your deployment serves multiple tenants
219
+ # per Standards instance, this workflow (and `standards diff`/`standards
220
+ # pull` generally) only checks the one tenant reachable with the configured
221
+ # key — repeat the job (or the deploy-time re-check) once per tenant.
222
+ #
223
+ # 3. Exit codes, spelled out (see packages/cli/src/commands/diff.ts):
224
+ # 0 = no blocking drift, next deploy is a no-op for schema/views.
225
+ # 1 = blocking drift found (an unresolved conflict, or unexpected sealed-
226
+ # object drift) — the next deploy WOULD destroy runtime changes.
227
+ # 2 = infra failure: network/auth error, missing/invalid
228
+ # STANDARDS_API_URL / STANDARDS_API_KEY, or the schema entry failed
229
+ # to load in isolation. This is NOT drift — do not treat it as "no
230
+ # drift" (it is not 0) and do not treat it as a conflict to resolve
231
+ # (editing schema code will not fix a missing secret).
232
+ #
233
+ # 4. Fork PRs: GitHub gives a `pull_request` run triggered from a fork a
234
+ # read-only GITHUB_TOKEN (no `pull-requests: write`), so the "Comment
235
+ # drift report on PR" step is guarded with
236
+ # `if: github.event.pull_request.head.repo.full_name == github.repository`
237
+ # and is simply skipped for fork PRs — it would otherwise fail with a 403
238
+ # trying to post the comment. The "Fail the job on unresolved drift or
239
+ # infra failure" step is NOT guarded — it doesn't call the GitHub API, so
240
+ # it still gates the merge for fork PRs; the developer just won't get the
241
+ # rendered table, only the job's own log output (which still contains the
242
+ # exit code and, on exit 2, the captured stderr).
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: standards-pull
3
+ description: Resolve Standards schema/view drift between the code-defined schema and the deployed runtime — walks each conflicting or runtime-diverged view with the developer, promotes runtime changes into builder code, and only stops once "standards diff" reports no drift. Use when "standards diff" exits 1, when a PR's standards-drift CI check fails, or when the developer asks to resolve/promote/reconcile schema or view drift.
4
+ ---
5
+
6
+ # standards-pull — resolve schema/view drift
7
+
8
+ ## What drift means here
9
+
10
+ Standards is schema-driven: the code-defined schema (`object()`/`view()` builders) is
11
+ the source of truth, and it gets synced into the runtime database on deploy. Between
12
+ deploys, runtime users can edit views (labels, tabs, field layout) through the product
13
+ UI. `standards diff` compares the two and classifies every view into one of:
14
+
15
+ - `code-changed` — only code changed since the last sync; the next deploy will simply
16
+ apply it. Nothing to do.
17
+ - `runtime-diverged` — a runtime user changed the view and code has NOT changed it;
18
+ the next deploy would silently overwrite their change. Needs a decision.
19
+ - `conflict` — BOTH code and the runtime changed the same view since the last sync.
20
+ The next deploy WOULD DESTROY the runtime change. Blocking (exit code 1).
21
+ - `conflict-resolved` — a conflict that a developer already acknowledged via
22
+ `standards pull`'s "overwrite" decision; code will apply cleanly.
23
+ - `in-sync` — no drift, no line printed.
24
+
25
+ Exit codes (`standards diff` / `standards pull`): `0` = nothing blocking, `1` = at
26
+ least one `conflict` or sealed-object drift is unresolved, `2` = infra failure
27
+ (network error, the schema entry failed to load, misconfigured/missing credentials).
28
+ **Exit code 2 is not drift** — it means the check itself could not run (e.g. a CI job
29
+ with a missing `STANDARDS_API_KEY` secret). Never treat a `2` as "no drift" or as a
30
+ conflict to resolve; fix the underlying infra/config problem and re-run.
31
+
32
+ ## Your job, precisely
33
+
34
+ You are invoked either because a developer asked you to resolve drift, or because a
35
+ CI check (`standards-drift.yml`) is failing on a PR. In both cases:
36
+
37
+ 1. Run `standards diff --format json` and parse the JSON report. This is the ONLY
38
+ source of truth for what has drifted — never guess from reading builder files
39
+ alone, and never rely on a stale report from earlier in the conversation.
40
+ 2. If the exit code is `2`, STOP and report the infra error message to the developer
41
+ verbatim — this is not something you can resolve by editing schema code (missing
42
+ `STANDARDS_API_KEY`/`STANDARDS_API_URL`, unreachable API, a schema entry that fails
43
+ to load in isolation). Fix what's actually broken (e.g. a bad import in the entry
44
+ file) and re-run `standards diff` before doing anything else.
45
+ 3. If the exit code is `0`, there is nothing to do — say so and stop.
46
+ 4. Otherwise, walk every item in the report **with the developer** — do not
47
+ silently decide on their behalf for anything destructive:
48
+ - `conflict` and `runtime-diverged` views are what need a decision. For each one,
49
+ tell the developer what changed on each side (the report's `changedFields`) and
50
+ ask: **promote** the runtime change into code, or (for `conflict` only)
51
+ **overwrite** it by running `standards pull` and choosing "Overwrite" for that
52
+ view (accepts the code version, discards the runtime change — only do this with
53
+ explicit developer confirmation, it is destructive to the runtime edit).
54
+ - `customObjects` / unexpected `systemObjectDrift` attributes: tell the developer
55
+ these exist only in the runtime, not in code, and ask whether to promote them
56
+ into the builder (add the attribute/object in code) or leave them as runtime-only
57
+ extensions.
58
+ 5. To **promote**: run `standards pull` and choose "Promote" for the relevant view.
59
+ This writes `.standards/promotions/<object>-<name>-<type>.md`, containing:
60
+ - the view's current **runtime config** (the JSON to fold into the builder call —
61
+ tabs, fields, label, icon, etc.)
62
+ - a **builder file hint**: the file path and line of the existing
63
+ `listView(...)`/`detailView(...)` call for that view, when one is found
64
+ Read that file, then edit the builder call in the hinted file so the fluent
65
+ builder chain (`.tab(...)`, `.form(...)`, `.label(...)`, etc.) produces the same
66
+ shape as the runtime config in the promotion file. This is a translation task —
67
+ the promotion file gives you the target *data*, you write the target *code*.
68
+ 6. **Loop `standards diff` until it exits 0.** After every edit (a promotion applied,
69
+ an overwrite decided, a builder file changed), re-run `standards diff --format
70
+ json` and re-parse it. Do not stop after one pass over the report — new drift can
71
+ surface (e.g. an edit that doesn't fully match the promoted config still shows as
72
+ `runtime-diverged`), and the loop is the only thing that proves you actually closed
73
+ every gap. **Never declare success without a green (`exitCode: 0`) `standards
74
+ diff` run.** A clean-looking diff of your own code edits is not sufficient — the
75
+ deterministic tool is what closes the loop, not your judgment about whether the
76
+ edits "look right".
77
+
78
+ ## Guardrails
79
+
80
+ - Never run `standards pull`'s "Overwrite" decision without explicit developer
81
+ confirmation for that specific view — it discards real runtime user edits.
82
+ - Never hand-edit `.standards/config.json` or `.standards/promotions/*.md` — those
83
+ are written by the CLI (`standards init` / `standards pull`); editing them by hand
84
+ does not change what the runtime actually has and will desync your mental model
85
+ from `standards diff`'s next run.
86
+ - If a promotion file's builder hint says "No `listView`/`detailView` call found",
87
+ search the schema entry's directory yourself for where that object's other views
88
+ are declared — the view may be newly created at runtime and have no code
89
+ counterpart yet; promoting it means adding a NEW builder call, not editing one.
90
+ - If `standards diff` keeps reporting the same drift after an edit you believe is
91
+ correct, re-read the promotion file's runtime config carefully — a mismatched field
92
+ name, tab id, or attribute list is the most common cause, not a tooling bug.
93
+
94
+ ## Related: deploy-time re-check
95
+
96
+ The merge of a PR and the actual deploy of that code are two different moments in
97
+ time — a runtime user can edit a view in the window between them. Projects that wired
98
+ `standards init --ci` should also run `standards diff` as a gate immediately before
99
+ the deploy step in their deploy pipeline (exit code `1` aborts the rollout), so this
100
+ skill's job — reconciling runtime drift — never gets skipped by that gap. If asked to
101
+ set that up, see the comment block at the end of `.github/workflows/standards-drift.yml`.