@oxy-hq/cli 0.1.0
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.
- package/README.md +163 -0
- package/dist/main.mjs +6688 -0
- package/dist/main.mjs.map +1 -0
- package/dist/mcp-VdontkUr.mjs +316 -0
- package/dist/mcp-VdontkUr.mjs.map +1 -0
- package/json-schemas/agent-test.json +104 -0
- package/json-schemas/agentic.json +406 -0
- package/json-schemas/app.json +1884 -0
- package/json-schemas/config.json +2215 -0
- package/json-schemas/flow-test.json +124 -0
- package/json-schemas/global-semantics.json +782 -0
- package/json-schemas/workflow.json +1907 -0
- package/package.json +65 -0
- package/skills/oxy-airway-pipelines/SKILL.md +338 -0
- package/skills/oxy-automations/SKILL.md +67 -0
- package/skills/oxy-cli/SKILL.md +134 -0
- package/skills/oxy-custom-apps/SKILL.md +330 -0
- package/skills/oxy-data-architecture/SKILL.md +225 -0
- package/skills/oxy-semantic-modeling/SKILL.md +216 -0
- package/template/.gitattributes +2 -0
- package/template/.github/scripts/ci-publish.sh +77 -0
- package/template/.github/scripts/discover-apps.sh +175 -0
- package/template/.github/scripts/publish-env.sh +55 -0
- package/template/.github/workflows/publish.yaml +653 -0
- package/template/.github/workflows/validate.yaml +264 -0
- package/template/.nvmrc +1 -0
- package/template/.oxyc-managed +297 -0
- package/template/README.md +414 -0
- package/template/_gitignore +32 -0
- package/template/apps/.gitkeep +0 -0
- package/template/config.yml +53 -0
- package/template/memory/.gitkeep +0 -0
- package/template/package.json +17 -0
- package/template/pipelines/.gitkeep +0 -0
- package/template/pnpm-workspace.yaml +32 -0
- package/template/scripts/dev.sh +443 -0
- package/template/semantics/topics/.gitkeep +0 -0
- package/template/semantics/views/.gitkeep +0 -0
- package/template/workflows/.gitkeep +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
The oxy workspace for **__NAME__** — everything we build for this customer in
|
|
4
|
+
one repo: their semantic model, pipelines, automations, custom apps, and the
|
|
5
|
+
agent memory the team shares.
|
|
6
|
+
|
|
7
|
+
Start a session scoped to this customer with:
|
|
8
|
+
|
|
9
|
+
oxyc __SLUG__
|
|
10
|
+
|
|
11
|
+
## Layout
|
|
12
|
+
|
|
13
|
+
| Path | What goes here |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `memory/` | Agent memory, written by Claude Code during `oxyc` sessions. `MEMORY.md` is **generated** from the frontmatter of the fact files beside it — do not edit it by hand; regenerate on conflict. |
|
|
16
|
+
| `semantics/views/` | `*.view.yml` — one per table: entities, dimensions, measures. The `description` is a contract with the agent, not a label. |
|
|
17
|
+
| `semantics/topics/` | `*.topic.yml` — question areas: a base view plus the views it may join to. Not one per table. |
|
|
18
|
+
| `pipelines/` | `*.airway.yml` — Airway ELT pipelines. Credentials live in the secret manager, never in the YAML. |
|
|
19
|
+
| `workflows/` | `*.automation.yml` — automations. `.automation.yml` is the canonical extension; `.procedure.yml` is legacy and still accepted, so do not start new files with it. |
|
|
20
|
+
| `apps/` | Custom app bundles (React + Vite, shipped with `oxy publish`). |
|
|
21
|
+
| `config.yml` | Workspace root config — models, databases, defaults. |
|
|
22
|
+
| `.github/workflows/` | GitHub Actions CI for this repo. Not the same thing as `workflows/` above, which holds oxy automations — the two are one letter of context apart and easy to confuse. |
|
|
23
|
+
| `.github/scripts/` | Shell the workflows call. Kept out of the YAML because a `run:` block can only ever be executed on a runner, while a script can be run — and tested — anywhere. |
|
|
24
|
+
| `scripts/` | Shell **you** run. `dev.sh` is the local dev entry point — see "Developing an app locally" below; `pnpm dev` is how you invoke it. |
|
|
25
|
+
| `.oxyc-managed` | Which files here are the tooling's and which are yours, so `oxyc update` can refresh one without overwriting the other. It is the only thing that decides what a sync may rewrite. Add a line when you add a file to the template — not when you add a view or an app. |
|
|
26
|
+
| `.gitignore` | What never gets committed: `node_modules/`, build output (`out/`, `dist/`, `build/`), `.oxy_state/`, `.env*`. The same directory names CI prunes when it walks the tree. |
|
|
27
|
+
|
|
28
|
+
The scaffold ships empty directories on purpose. There are no example views or
|
|
29
|
+
pipelines to copy: a stale example carried forward is how a whole workspace
|
|
30
|
+
ends up on a deprecated file extension.
|
|
31
|
+
|
|
32
|
+
## What commits itself, and what does not
|
|
33
|
+
|
|
34
|
+
A session started with `oxyc` regenerates the memory index and then commits and
|
|
35
|
+
pushes **`memory/` only**, at the end of the session, with no review step.
|
|
36
|
+
|
|
37
|
+
Everything else here — the semantic model, pipelines, automations, apps — you
|
|
38
|
+
commit yourself, deliberately, after review. That split is deliberate: a
|
|
39
|
+
customer's semantic model should never land in `main` because a session ended.
|
|
40
|
+
|
|
41
|
+
The practical consequence: do not put anything in `memory/` that needs review
|
|
42
|
+
before the team sees it.
|
|
43
|
+
|
|
44
|
+
## What CI checks
|
|
45
|
+
|
|
46
|
+
Every pull request runs `.github/workflows/validate.yaml`: a Postgres 16
|
|
47
|
+
service container, then `oxy compile` over this whole tree. It needs no
|
|
48
|
+
warehouse credentials — compiling is a pure YAML-to-rows transform — so the
|
|
49
|
+
gate works before a single database is connected.
|
|
50
|
+
|
|
51
|
+
It catches:
|
|
52
|
+
|
|
53
|
+
- **YAML that does not parse**, anywhere in the tree, `config.yml` included.
|
|
54
|
+
The failing path is emitted as a GitHub error annotation, so the failure
|
|
55
|
+
lands on the diff rather than only in the log. It anchors to the **top of
|
|
56
|
+
the file**, not to the offending line: `oxy compile --json` reports a path, a
|
|
57
|
+
kind and a message per failure, and no line or column for the workflow to
|
|
58
|
+
pass on.
|
|
59
|
+
- **Two files of one kind declaring the same `name:`** — two views both called
|
|
60
|
+
`orders`, say, which is what a copy-pasted file usually produces.
|
|
61
|
+
|
|
62
|
+
It does **not** catch, and each of these compiles green today:
|
|
63
|
+
|
|
64
|
+
- **A view whose `datasource:` names a database that is not in `config.yml`.**
|
|
65
|
+
There is no referential check at compile time; the typo surfaces at query
|
|
66
|
+
time instead.
|
|
67
|
+
- **A view with no `name:` field at all.** The compiler derives one from the
|
|
68
|
+
file path rather than failing, so the view exists under a name nobody chose.
|
|
69
|
+
- **Anything about whether the model is right** — a measure with the wrong SQL,
|
|
70
|
+
a join on the wrong key, a description that misdescribes the column. Nothing
|
|
71
|
+
mechanical checks those.
|
|
72
|
+
|
|
73
|
+
So a green check means "this parses, and the identifiers are unique". It does
|
|
74
|
+
not mean the model is correct, and it is not a substitute for reading the diff.
|
|
75
|
+
|
|
76
|
+
The `oxy` CLI version is **pinned** in that workflow, and the job fails if the
|
|
77
|
+
installed binary is older. Both halves matter: the installer does not fail on a
|
|
78
|
+
version tag that does not exist, and this CLI answers a subcommand it does not
|
|
79
|
+
have with its own help text and exit 0 — so an unpinned, unguarded job can go
|
|
80
|
+
green while doing less than you think.
|
|
81
|
+
|
|
82
|
+
## Developing an app locally
|
|
83
|
+
|
|
84
|
+
An app in `apps/` is a React + Vite bundle. Running one on your machine means
|
|
85
|
+
running it against **a real oxy in the cloud** — there is no local warehouse,
|
|
86
|
+
no fixture data, and no local `oxy serve` to point at. From the repo root:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
oxy login --env dev # once per env; opens a browser, caches a token
|
|
90
|
+
pnpm dev --env dev # starts everything
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`pnpm dev` runs `scripts/dev.sh`, and that script is the whole answer: it finds
|
|
94
|
+
the apps in this repo, starts both processes, prints what it is doing, and
|
|
95
|
+
stops both together when you press Ctrl-C.
|
|
96
|
+
|
|
97
|
+
**`--env` is required, and that is deliberate.** It says which oxy the app
|
|
98
|
+
reads from — `dev`, `staging`, `production`, a full URL
|
|
99
|
+
(`--env https://acme.oxygen-hq.com`), or any name the app's own `oxy-app.json`
|
|
100
|
+
declares under `environments`. The underlying `oxy proxy` defaults to
|
|
101
|
+
**production** if nothing says otherwise, and a `pnpm dev` that quietly serves
|
|
102
|
+
a customer's live data is not something this repo will do. `--env production`
|
|
103
|
+
additionally needs `--yes`.
|
|
104
|
+
|
|
105
|
+
If the repo holds more than one app, name the one you want — by its directory
|
|
106
|
+
or just its name:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pnpm dev --env dev sales-dashboard
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### The two processes
|
|
113
|
+
|
|
114
|
+
| Process | Port | What it is |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `oxy proxy --env <env>` | `3000` | Background. Every `/api` call the app makes goes through here, signed with the token `oxy login` cached, and out to the cloud. It stands in for a local `oxy serve`, which is why the port is not configurable — the Vite plugin already proxies `/api` to `3000`. |
|
|
117
|
+
| `pnpm dev` in the app | `5173` | Foreground. Vite, with hot reload. **This is the URL you open**: <http://localhost:5173>. |
|
|
118
|
+
|
|
119
|
+
Ctrl-C stops both, and it always returns: a proxy that ignores the polite
|
|
120
|
+
signal is given five seconds and then killed outright, because a dev script
|
|
121
|
+
whose Ctrl-C sometimes hangs is worse than one that leaks. If a run ever fails
|
|
122
|
+
with *port 3000 is already taken*, a proxy from an earlier session outlived its
|
|
123
|
+
terminal — the message names the process holding it.
|
|
124
|
+
|
|
125
|
+
### The guardrails, and how to lift them
|
|
126
|
+
|
|
127
|
+
The proxy is not a transparent tunnel. Two kinds of traffic are held back so
|
|
128
|
+
that an afternoon of local clicking cannot change what the customer sees:
|
|
129
|
+
|
|
130
|
+
- **Side-effecting calls are HELD** — Oxy Functions (`/fn`), agent runs,
|
|
131
|
+
automation runs. They do not reach the cloud at all. This is the single most
|
|
132
|
+
common "my function isn't running and there is no error" — pass
|
|
133
|
+
**`--allow-writes`** to forward them for real.
|
|
134
|
+
- **Tracking events are DROPPED**, so local clicking never lands in the
|
|
135
|
+
customer's analytics. Pass **`--allow-events`** to forward them.
|
|
136
|
+
|
|
137
|
+
`pnpm dev` prints both, every run, before it starts anything.
|
|
138
|
+
|
|
139
|
+
### If something is wrong
|
|
140
|
+
|
|
141
|
+
- **Requests come back 401** — the cached token for that env has expired.
|
|
142
|
+
`oxy login --env <env>` again.
|
|
143
|
+
- **`the 'oxy' CLI is not on your PATH`** — install it with
|
|
144
|
+
`curl -sSfL https://get.oxy.tech | bash`; it lands in `~/.local/bin`.
|
|
145
|
+
- **`this repo has no custom apps yet`** — expected on a fresh repo, and it is
|
|
146
|
+
not an error. The message says how to add one.
|
|
147
|
+
- **A change to `apps/` does not show up** — Vite serves the app, but the data
|
|
148
|
+
comes from the cloud through the proxy; a stale *answer* is a semantic-model
|
|
149
|
+
or warehouse question, not a dev-server one.
|
|
150
|
+
|
|
151
|
+
## Publishing custom apps
|
|
152
|
+
|
|
153
|
+
**Publishing is self-serve here, and CI publishes nothing unless this repo
|
|
154
|
+
turns it on.** That is the default deliberately: it is how Oxy ships its own
|
|
155
|
+
custom apps, and it means this repo is useful on the day it is created without
|
|
156
|
+
a long-lived credential sitting in it.
|
|
157
|
+
|
|
158
|
+
### The default: you publish, from your machine
|
|
159
|
+
|
|
160
|
+
From the app's own directory under `apps/`:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
oxy login --env dev # once per env; opens a browser, caches a token
|
|
164
|
+
oxy publish --env dev # a draft, to the dev environment
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`--env` is not optional in practice. The CLI defaults it to **production**, so
|
|
168
|
+
an invocation that leaves the flag off does not publish "nowhere in
|
|
169
|
+
particular" — it publishes at the customer's live environment. Name it every
|
|
170
|
+
time. The draft channel is the default; `--promote` publishes live instead.
|
|
171
|
+
|
|
172
|
+
**A push to `main` still runs `.github/workflows/publish.yaml`, and it is
|
|
173
|
+
still worth having.** Its `build` job installs the workspace and builds every
|
|
174
|
+
bundle under `apps/` — which needs no credential — so a bundle that stopped
|
|
175
|
+
compiling fails on the merge, rather than in front of you an hour later when
|
|
176
|
+
you sit down to publish it. The `publish` job is then **skipped**, the run is
|
|
177
|
+
**green**, and the log carries a notice saying publishing here is self-serve
|
|
178
|
+
and naming the two commands above.
|
|
179
|
+
|
|
180
|
+
### Turning CI publishing on
|
|
181
|
+
|
|
182
|
+
One repository **variable**:
|
|
183
|
+
|
|
184
|
+
> Settings → Secrets and variables → Actions → **Variables** →
|
|
185
|
+
> `OXY_CI_PUBLISH` = `true`
|
|
186
|
+
|
|
187
|
+
A variable rather than a secret, for two reasons. It is read in the `build`
|
|
188
|
+
job — the only job that runs when publishing is off, and so the only one that
|
|
189
|
+
can say anything about it — and that job must reference no secret at all,
|
|
190
|
+
because it executes the postinstall and build scripts of every dependency your
|
|
191
|
+
apps pull in. The other reason is plainer: a switch whose whole job is to tell
|
|
192
|
+
"off on purpose" apart from "misconfigured" has to be legible, in Settings and
|
|
193
|
+
in a run log. A secret is neither.
|
|
194
|
+
|
|
195
|
+
`true`, `yes`, `on` and `1` all mean on; `false`, `no`, `off`, `0` and a
|
|
196
|
+
variable that is not set at all mean off. **Anything else fails the run**
|
|
197
|
+
instead of being read as one of them: `OXY_CI_PUBLISH=ture` guessed as off
|
|
198
|
+
stops publishing with nobody told, and guessed as on publishes when nobody
|
|
199
|
+
asked, so it is not guessed.
|
|
200
|
+
|
|
201
|
+
Note what the switch does **not** do. It decides whether the publish job runs;
|
|
202
|
+
it never softens what happens once it has. With CI publishing on, a missing
|
|
203
|
+
`OXY_TOKEN` is a **hard failure** exactly as it always was. That is the reason
|
|
204
|
+
there is a switch at all rather than the obvious shortcut of skipping whenever
|
|
205
|
+
no token turns up: inferred from the token, "this repo does not publish from
|
|
206
|
+
CI" and "somebody forgot production's token" are the same observation — and
|
|
207
|
+
the second one ships nothing, quietly, for as long as nobody looks.
|
|
208
|
+
|
|
209
|
+
Everything from here to the end of this section describes a repo that has
|
|
210
|
+
opted in.
|
|
211
|
+
|
|
212
|
+
### Where an opted-in CI publishes
|
|
213
|
+
|
|
214
|
+
`.github/workflows/publish.yaml` builds every bundle under `apps/` and ships
|
|
215
|
+
it with `oxy publish`. It runs on a push to `main` that touches `apps/`, the
|
|
216
|
+
lockfile, or the workflow itself, and it can be run by hand from the Actions
|
|
217
|
+
tab.
|
|
218
|
+
|
|
219
|
+
**Where it publishes is decided by how it was triggered, and it is always
|
|
220
|
+
said out loud:**
|
|
221
|
+
|
|
222
|
+
| Trigger | Event | `oxy publish --env` | Channel | Reviewers |
|
|
223
|
+
| --- | --- | --- | --- | --- |
|
|
224
|
+
| push to `main` | `push` | `dev` | draft | none |
|
|
225
|
+
| Actions tab (**Run workflow**) | `workflow_dispatch` | `production` | draft, or live with **Promote** | whatever `production` requires |
|
|
226
|
+
|
|
227
|
+
The **Event** column is the raw GitHub event name, and it is here rather than
|
|
228
|
+
implied because it is the key the mapping is written against — this table and
|
|
229
|
+
`.github/scripts/publish-env.sh` are checked against each other, row by row,
|
|
230
|
+
by `customer-tooling`'s own suite.
|
|
231
|
+
|
|
232
|
+
That mapping lives in `.github/scripts/publish-env.sh`, and a trigger it has
|
|
233
|
+
no entry for **fails the run** rather than picking one. The reason is worth
|
|
234
|
+
knowing, because it was a live bug in this workflow: `oxy publish` defaults
|
|
235
|
+
`--env` to **production**, so an invocation that leaves the flag off does not
|
|
236
|
+
publish "nowhere in particular" — it publishes at the customer's live
|
|
237
|
+
environment. Every publish here names its environment explicitly, and there
|
|
238
|
+
is no code path that can fall back to a default.
|
|
239
|
+
|
|
240
|
+
There is deliberately **no environment picker** on the manual run. An input
|
|
241
|
+
would need a default, which is the thing being removed; and the same resolved
|
|
242
|
+
name also selects the GitHub environment whose `OXY_TOKEN` the job publishes
|
|
243
|
+
with, so leaving it to a form field would mean the credential and the
|
|
244
|
+
destination agree only by an operator's care. To publish a dev draft by hand,
|
|
245
|
+
re-run the push-triggered run from the Actions tab — a re-run keeps the
|
|
246
|
+
original event, so it still resolves to `dev`.
|
|
247
|
+
|
|
248
|
+
It is **two jobs**, and the split is the security boundary rather than a
|
|
249
|
+
tidiness one. `build` runs `pnpm install` and `pnpm -r build` — which is to
|
|
250
|
+
say it executes the postinstall and build scripts of every dependency your
|
|
251
|
+
apps pull in — and holds no publish credential at all. It hands the built
|
|
252
|
+
output to `publish` as a workflow artifact. `publish` holds `OXY_TOKEN`, and
|
|
253
|
+
runs no package script: `oxy publish --dir` uploads a pre-built directory
|
|
254
|
+
as-is instead of building anything. So a compromised dependency in an app's
|
|
255
|
+
tree never runs on a runner where the token exists.
|
|
256
|
+
|
|
257
|
+
**On a fresh repo it does nothing at all, and that is the intended
|
|
258
|
+
behaviour.** The scaffold ships no apps and no `pnpm-lock.yaml` — there is
|
|
259
|
+
nothing to lock yet — so the first step looks for `apps/**/oxy-app.json`,
|
|
260
|
+
finds none, skips the install and the build, skips the whole publish job, and
|
|
261
|
+
the run goes **green**. The install is skipped along with the publish
|
|
262
|
+
deliberately: `pnpm install --frozen-lockfile` fails without a lockfile, and
|
|
263
|
+
a repo whose CI is red the day it is created is a repo where nobody reads CI.
|
|
264
|
+
|
|
265
|
+
Before it can publish anything, each of the two **GitHub environments** —
|
|
266
|
+
`dev` and `production` — needs its own **`OXY_TOKEN`** (Settings →
|
|
267
|
+
Environments → *environment* → Environment secrets), a publish-scoped API key
|
|
268
|
+
minted in oxy against that workspace. This is the credential the self-serve
|
|
269
|
+
model does without, and it is worth knowing what it is before you create one:
|
|
270
|
+
a personal API key records the project it was minted against, but that project
|
|
271
|
+
never reaches validation — a leaked key carries the minter's **user-level**
|
|
272
|
+
access, not "this app's project only".
|
|
273
|
+
|
|
274
|
+
**A missing one fails the run, loudly, and that is the intended behaviour.**
|
|
275
|
+
It is also why CI publishing is a switch rather than something inferred from
|
|
276
|
+
the token's presence: with the switch on, no token means a red run naming the
|
|
277
|
+
environment it was looked for in, never a green run that published nothing.
|
|
278
|
+
|
|
279
|
+
**And any repository-level `OXY_TOKEN` must be DELETED, not left in place.**
|
|
280
|
+
GitHub's order is environment → repository → organisation, and that is
|
|
281
|
+
*precedence, not exclusion*: an environment secret overrides a repository one
|
|
282
|
+
of the same name, it does not suppress it. A repo still carrying an old
|
|
283
|
+
repository-level token therefore serves **both** environments from it wherever
|
|
284
|
+
the environment has none — every run looks healthy, the missing-token check
|
|
285
|
+
never fires, and production ships under whatever that token was scoped to.
|
|
286
|
+
|
|
287
|
+
Nothing in CI can catch it. GitHub does not tell a job which level a secret
|
|
288
|
+
came from, so the two cases are identical from inside the run. It is a
|
|
289
|
+
one-time human check, and it is the single thing to get right when moving an
|
|
290
|
+
existing repo onto environments:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
gh secret list --repo <owner>/<repo> # repository level
|
|
294
|
+
gh secret delete OXY_TOKEN --repo <owner>/<repo> # if one is listed
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Nothing else needs configuring: each app's `oxy-app.json` carries its own
|
|
298
|
+
`slug` and `orgSlug`, and the project is resolved from the target at publish
|
|
299
|
+
time. If an environment's oxy is not where `--env` would resolve it, set
|
|
300
|
+
`OXY_TARGET` as an **environment** variable on that environment — never as a
|
|
301
|
+
repository one, since it *overrides* `--env` and a repository-level value
|
|
302
|
+
points both environments at the same oxy.
|
|
303
|
+
|
|
304
|
+
So adding the first app is two things — three, if this repo publishes from CI:
|
|
305
|
+
|
|
306
|
+
1. the bundle at `apps/<app>/` (or `apps/<org>/<app>/`), with an
|
|
307
|
+
`oxy-app.json` carrying `slug` and `orgSlug`;
|
|
308
|
+
2. a committed `pnpm-lock.yaml` — run `pnpm install` once and commit the
|
|
309
|
+
lockfile it writes. Only the lockfile: `node_modules/` and every app's
|
|
310
|
+
build output are covered by `.gitignore`, so `git add -A` after an install
|
|
311
|
+
stages the one file CI needs and none of the ones it does not;
|
|
312
|
+
3. **only with `OXY_CI_PUBLISH` set** — an `OXY_TOKEN` in each environment.
|
|
313
|
+
|
|
314
|
+
Miss one and the workflow fails loudly and names it: the discovery step
|
|
315
|
+
points at the manifest, or the missing lockfile, or the missing secret. It
|
|
316
|
+
does not guess and it does not half-publish. (The first two are needed even
|
|
317
|
+
with CI publishing off: the `build` job installs and builds either way.)
|
|
318
|
+
|
|
319
|
+
A push publishes to the **draft** channel of the **dev** environment — draft
|
|
320
|
+
is the CLI's own default, not a policy this workflow invented; dev is this
|
|
321
|
+
repo's. Going live in production is a separate, deliberate act, and it is
|
|
322
|
+
worth knowing exactly what the two ways of doing it are:
|
|
323
|
+
|
|
324
|
+
- **`/admin/apps`** promotes *the build you are looking at*. If you have
|
|
325
|
+
reviewed a draft and want that exact bundle live, this is the one you want.
|
|
326
|
+
- **The Actions tab**, running this workflow with a **Ref** and **Promote**
|
|
327
|
+
ticked, **rebuilds that ref and publishes the result live**. It does not
|
|
328
|
+
promote an existing draft — no command in the `oxy` CLI can, so the
|
|
329
|
+
workflow cannot either. If `main` has moved since the draft you reviewed,
|
|
330
|
+
this ships the newer commit, under a new build id.
|
|
331
|
+
|
|
332
|
+
That is why the ref is an explicit, required input rather than something the
|
|
333
|
+
run infers: pin it to the SHA you reviewed and the two routes agree. Leaving
|
|
334
|
+
it at its default, `main`, is fine and publishes main's current tip — the run
|
|
335
|
+
resolves the commit it actually checked out and prints it, so what shipped is
|
|
336
|
+
never left to inference. Do not read the commit off the run's title or the
|
|
337
|
+
branch you launched it from: on a manual run those track GitHub's "Use
|
|
338
|
+
workflow from" dropdown, which is a separate control from the Ref input here.
|
|
339
|
+
|
|
340
|
+
### Putting a human in front of a publish
|
|
341
|
+
|
|
342
|
+
The publish job runs in the GitHub Actions **environment** it is publishing
|
|
343
|
+
to — `dev` or `production`, the same string it passes to `--env`. On a fresh
|
|
344
|
+
repo those are labels and nothing else: GitHub creates a referenced
|
|
345
|
+
environment that does not exist, with no protection rules. (Environments in a
|
|
346
|
+
private repo need GitHub Team or Pro; the `oxy-hq` org is on Team.)
|
|
347
|
+
|
|
348
|
+
Either becomes a gate when someone wants one — Settings → Environments →
|
|
349
|
+
`production` → **Required reviewers** — and no workflow edit is needed to add
|
|
350
|
+
it. Because the trigger already decides the environment, reviewers on
|
|
351
|
+
`production` hold up exactly the manual runs and none of the dev drafts a
|
|
352
|
+
merge produces. Putting them on `dev` instead would hold up every merge, which
|
|
353
|
+
is almost never what anyone means.
|
|
354
|
+
|
|
355
|
+
It does **not** run tests, typecheck, or check that an app works. If the
|
|
356
|
+
build produces output, the bundle ships. One case is worth knowing about
|
|
357
|
+
because it is silent everywhere else: an app whose `package.json` has no
|
|
358
|
+
`build` script is skipped by pnpm **without failing**, so the workflow
|
|
359
|
+
refuses to publish an empty output directory rather than uploading nothing
|
|
360
|
+
successfully. Nothing catches a bundle that builds and is simply wrong.
|
|
361
|
+
|
|
362
|
+
## This repo is the customer's oxy workspace
|
|
363
|
+
|
|
364
|
+
The semantic model in here does nothing until the platform compiles it, so this
|
|
365
|
+
repo is registered as __NAME__'s oxy workspace — **twice**, once as dev and
|
|
366
|
+
once as production. Both track this repo and this same `main`.
|
|
367
|
+
|
|
368
|
+
Registration happens through the **org onboarding UI** and needs a GitHub App
|
|
369
|
+
installation for the org. There is no CLI command for it — do not go looking for
|
|
370
|
+
one. Registration supports pointing at a subdirectory, so the workspace root can
|
|
371
|
+
sit inside a larger repo if that is ever needed.
|
|
372
|
+
|
|
373
|
+
Until it is registered, the model here is still worth writing and still worth
|
|
374
|
+
validating; it just is not queryable by agents or Ask Oxygen yet.
|
|
375
|
+
|
|
376
|
+
### Two workspaces, one branch — and who promotes what
|
|
377
|
+
|
|
378
|
+
There is no `dev` branch, and that is the design rather than an omission. A
|
|
379
|
+
semantic model is not application code: a long-lived divergent branch means a
|
|
380
|
+
measure exists in one environment and not the other, and the symptom is a
|
|
381
|
+
query that worked yesterday quietly returning nothing. One definition, two
|
|
382
|
+
pointers at it.
|
|
383
|
+
|
|
384
|
+
It works because **merging to `main` promotes nothing on its own** — the
|
|
385
|
+
platform compiles on an explicit action (the IDE button, admin's *Run compile
|
|
386
|
+
now*, the CLI) and never on a push. So the two workspaces differ only in when
|
|
387
|
+
someone points each at the branch:
|
|
388
|
+
|
|
389
|
+
| What | Who moves it |
|
|
390
|
+
| --- | --- |
|
|
391
|
+
| Custom apps → dev | CI, on every merge to `main` (draft) |
|
|
392
|
+
| Custom apps → production | CI, on a manual run from the Actions tab |
|
|
393
|
+
| Semantic model → dev | a human, or a scheduled compile if one is set up |
|
|
394
|
+
| **Semantic model → production** | **a human, always** |
|
|
395
|
+
|
|
396
|
+
Promoting the production semantic model is a deliberate, human step: open the
|
|
397
|
+
production workspace in Oxygen and pull the latest commit. **CI does not do
|
|
398
|
+
it**, and holds no credential that could — the `OXY_TOKEN` in each environment
|
|
399
|
+
is publish-scoped and cannot compile or promote a workspace at all. So a merge
|
|
400
|
+
to `main` never changes what production answers until someone decides it
|
|
401
|
+
should.
|
|
402
|
+
|
|
403
|
+
## Node and pnpm
|
|
404
|
+
|
|
405
|
+
Node **20** (`.nvmrc`), matching the working app monorepo this build config is
|
|
406
|
+
derived from. Use **pnpm**, never npm or yarn — there is no `package-lock.json`
|
|
407
|
+
here and there should not be one.
|
|
408
|
+
|
|
409
|
+
pnpm is floored at **>= 10**, one major above that monorepo's `>= 9`, and the
|
|
410
|
+
difference is deliberate: pnpm 9 reads no settings at all from
|
|
411
|
+
`pnpm-workspace.yaml`, so on pnpm 9 the `onlyBuiltDependencies` block there is
|
|
412
|
+
silently inert and an app's `esbuild` / `sharp` postinstall is blocked with no
|
|
413
|
+
diagnostic. A floor that admits a version where the config does nothing is
|
|
414
|
+
worse than no floor.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# What never belongs in this repo.
|
|
2
|
+
#
|
|
3
|
+
# Not housekeeping — load-bearing. Onboarding step 1 runs `git add -A`, and
|
|
4
|
+
# the README tells you to run `pnpm install` and commit the lockfile it
|
|
5
|
+
# writes; both of those stage everything in the tree that git is not told to
|
|
6
|
+
# ignore. Without this file the first commit of a customer workspace carries
|
|
7
|
+
# node_modules/ and every app's build output.
|
|
8
|
+
#
|
|
9
|
+
# Both CI workflows already prune these same directory names when they walk
|
|
10
|
+
# the tree (.github/scripts/discover-apps.sh, and the compile floor check in
|
|
11
|
+
# validate.yaml), so this list and those prunes are two views of one rule:
|
|
12
|
+
# these directories are generated, and nothing here reads them as source.
|
|
13
|
+
|
|
14
|
+
# Dependencies. Restored by `pnpm install` from pnpm-lock.yaml, which IS
|
|
15
|
+
# committed.
|
|
16
|
+
node_modules/
|
|
17
|
+
|
|
18
|
+
# Build output. `out/` is oxy publish's default outDir; `dist/` and `build/`
|
|
19
|
+
# are what a Vite or Next app writes when its oxy-app.json names one instead.
|
|
20
|
+
out/
|
|
21
|
+
dist/
|
|
22
|
+
build/
|
|
23
|
+
|
|
24
|
+
# The oxy CLI's local state directory.
|
|
25
|
+
.oxy_state/
|
|
26
|
+
|
|
27
|
+
# Local secrets. Credentials for pipelines and apps belong in the platform's
|
|
28
|
+
# secret manager, never in a file in this repo — an ignored .env is where
|
|
29
|
+
# people put them anyway, so make sure an accident stays local.
|
|
30
|
+
.env*
|
|
31
|
+
|
|
32
|
+
.DS_Store
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/oxy-hq/oxy/refs/heads/main/json-schemas/config.json
|
|
2
|
+
#
|
|
3
|
+
# Workspace config for __NAME__.
|
|
4
|
+
#
|
|
5
|
+
# Minimal and valid on purpose: this compiles clean on day one, before a single
|
|
6
|
+
# view, topic, pipeline or automation exists. `models:` must be present even
|
|
7
|
+
# when empty — `oxy compile` fails with "missing field `models`" without the
|
|
8
|
+
# key. `databases:` may be omitted entirely (the compiler synthesizes an empty
|
|
9
|
+
# list), and is spelled out here only so there is an obvious place to add one.
|
|
10
|
+
#
|
|
11
|
+
# EVERYTHING COMMENTED OUT BELOW IS A SHAPE, NOT A WORKING DEFAULT. Any value
|
|
12
|
+
# written as <replace-me-...> is a placeholder and will not work as written.
|
|
13
|
+
#
|
|
14
|
+
# Secrets are never written in this file. A `*_var` field names an ENVIRONMENT
|
|
15
|
+
# VARIABLE, resolved from the workspace secret store at runtime — never the
|
|
16
|
+
# secret itself.
|
|
17
|
+
|
|
18
|
+
models: []
|
|
19
|
+
# models:
|
|
20
|
+
# - vendor: anthropic
|
|
21
|
+
# name: <replace-me-model-name> # how this workspace refers to it
|
|
22
|
+
# model_ref: claude-sonnet-4-6 # the vendor's own model id
|
|
23
|
+
# key_var: ANTHROPIC_API_KEY
|
|
24
|
+
# api_url: https://api.anthropic.com/v1
|
|
25
|
+
|
|
26
|
+
databases: []
|
|
27
|
+
# databases:
|
|
28
|
+
# - name: <replace-me-database-name>
|
|
29
|
+
# type: clickhouse
|
|
30
|
+
# host_var: CLICKHOUSE_HOST
|
|
31
|
+
# user_var: CLICKHOUSE_USERNAME
|
|
32
|
+
# password_var: CLICKHOUSE_PASSWORD
|
|
33
|
+
# database_var: CLICKHOUSE_DATABASE
|
|
34
|
+
#
|
|
35
|
+
# # A managed Airhouse warehouse takes no *_var fields at all — its
|
|
36
|
+
# # credentials are brokered at runtime.
|
|
37
|
+
# - name: <replace-me-database-name>
|
|
38
|
+
# type: airhouse_managed
|
|
39
|
+
|
|
40
|
+
# defaults:
|
|
41
|
+
# database: <replace-me-database-name> # must name an entry above; note
|
|
42
|
+
# # that `oxy compile` does NOT
|
|
43
|
+
# # check this reference, so a typo
|
|
44
|
+
# # here surfaces at query time
|
|
45
|
+
|
|
46
|
+
# No `health_check:` block, deliberately. An absent block means this workspace
|
|
47
|
+
# is never evaluated by Workspace Health, which is the honest state of one
|
|
48
|
+
# nobody has tuned. It does not mean "healthy" — an unevaluated workspace is
|
|
49
|
+
# absent from the admin health table rather than shown green. Add a block when
|
|
50
|
+
# someone owns the thresholds; a guessed interval reads as a real opinion.
|
|
51
|
+
|
|
52
|
+
# No `integrations:` either. Every entry is one specific vendor's credentials,
|
|
53
|
+
# and this workspace has no vendor yet.
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__SLUG__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=20",
|
|
6
|
+
"pnpm": ">=10"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "./scripts/dev.sh",
|
|
10
|
+
"build": "pnpm -r build",
|
|
11
|
+
"lint": "pnpm -r lint",
|
|
12
|
+
"typecheck": "pnpm -r typecheck"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.9.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Custom app bundles live under `apps/`. Both globs are carried from the
|
|
2
|
+
# customer-apps monorepo's working workspace file, and both earn their place:
|
|
3
|
+
#
|
|
4
|
+
# apps/* one app per directory — what a single-customer repo uses
|
|
5
|
+
# apps/*/* an org-nested layout — kept so a bundle lifted out of the
|
|
6
|
+
# shared monorepo, where apps sit at apps/<org>/<app>/, works
|
|
7
|
+
# here unchanged
|
|
8
|
+
#
|
|
9
|
+
# The URL an app is served at comes from its own oxy-app.json slug, not from
|
|
10
|
+
# its folder depth, so either layout is valid.
|
|
11
|
+
packages:
|
|
12
|
+
- "apps/*"
|
|
13
|
+
- "apps/*/*"
|
|
14
|
+
|
|
15
|
+
# pnpm 10 does not run a dependency's install/postinstall script unless the
|
|
16
|
+
# package is named here. These three are what the real app bundles need
|
|
17
|
+
# (esbuild and unrs-resolver fetch a platform binary, sharp builds one), so
|
|
18
|
+
# without this block the first app added here fails to build. Add to the list
|
|
19
|
+
# rather than replacing it.
|
|
20
|
+
#
|
|
21
|
+
# The key is `onlyBuiltDependencies`, NOT the `allowBuilds` that customer-apps
|
|
22
|
+
# carries. Verified against pnpm 10.8.0: a wrong-typed `onlyBuiltDependencies`
|
|
23
|
+
# crashes inside `createAllowBuildFunction`, proving pnpm reads it, while a
|
|
24
|
+
# wrong-typed `allowBuilds` installs cleanly — pnpm never looks at that key at
|
|
25
|
+
# all, with no warning. customer-apps' block is therefore inert; ours is not.
|
|
26
|
+
#
|
|
27
|
+
# pnpm 9 ignores this file's settings entirely (same probe, pnpm 9.15.9:
|
|
28
|
+
# no crash), which is why package.json floors pnpm at >=10 rather than >=9.
|
|
29
|
+
onlyBuiltDependencies:
|
|
30
|
+
- esbuild
|
|
31
|
+
- sharp
|
|
32
|
+
- unrs-resolver
|