@oxy-hq/cli 0.5.0 → 0.7.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 +611 -51
- package/dist/main.mjs +8937 -3407
- package/dist/main.mjs.map +1 -1
- package/dist/mcp-xxWL3_dz.mjs +1137 -0
- package/dist/mcp-xxWL3_dz.mjs.map +1 -0
- package/json-schemas/agentic.json +1 -1
- package/json-schemas/flow-test.json +3 -3
- package/package.json +4 -4
- package/skills/oxy-cli/SKILL.md +27 -1
- package/skills/oxy-custom-apps/SKILL.md +128 -0
- package/template/.github/scripts/ci-publish.sh +11 -10
- package/template/.github/workflows/publish.yaml +102 -30
- package/template/README.md +95 -31
- package/dist/mcp-CIsyezzU.mjs +0 -316
- package/dist/mcp-CIsyezzU.mjs.map +0 -1
|
@@ -20,29 +20,40 @@
|
|
|
20
20
|
# scaffolded repo should behave like that on day one rather than arriving with a
|
|
21
21
|
# long-lived credential requirement nobody asked for.
|
|
22
22
|
#
|
|
23
|
-
# The switch is EXPLICIT rather than inferred from
|
|
24
|
-
# design rather than a preference. Skipping whenever
|
|
25
|
-
#
|
|
26
|
-
# indistinguishable from "somebody forgot production's
|
|
27
|
-
# which would then ship nothing, quietly, for as long as nobody
|
|
28
|
-
# switch ON a
|
|
29
|
-
# property the separate states exist to keep. The reasoning, and
|
|
30
|
-
# variable and not a secret, is in `.github/scripts/ci-publish.sh`.
|
|
23
|
+
# The switch is EXPLICIT rather than inferred from a credential, and that is
|
|
24
|
+
# the design rather than a preference. Skipping whenever the job could not
|
|
25
|
+
# authenticate would be one line, and it would make "this repo does not publish
|
|
26
|
+
# from CI" indistinguishable from "somebody forgot production's trust policy" —
|
|
27
|
+
# the second of which would then ship nothing, quietly, for as long as nobody
|
|
28
|
+
# looked. With the switch ON a run that cannot authenticate is a loud failure,
|
|
29
|
+
# which is the property the separate states exist to keep. The reasoning, and
|
|
30
|
+
# why it is a variable and not a secret, is in `.github/scripts/ci-publish.sh`.
|
|
31
31
|
#
|
|
32
32
|
# Deliberately NOT modelled on pokehouse-oxy's `franchise-report-app.yml`
|
|
33
33
|
# (Docker -> ECR -> ArgoCD). That is the only real, green CI on disk in this
|
|
34
34
|
# org, which is exactly what makes it tempting; it is a legacy path for the
|
|
35
35
|
# one app still on Next.js and has nothing to do with `oxyc publish`.
|
|
36
36
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
37
|
+
# AUTH IS GITHUB OIDC BY DEFAULT: NO STORED SECRET. The publish job holds
|
|
38
|
+
# `id-token: write`, and `oxyc publish` trades the job's OIDC token for a
|
|
39
|
+
# fifteen-minute Oxy token acting as a service account, which it revokes when
|
|
40
|
+
# the command ends. What makes the deployment honour that is a TRUST POLICY on
|
|
41
|
+
# the service account, naming this repository, this workflow file
|
|
42
|
+
# (`.github/workflows/publish.yaml`) and the job's `environment:` — one policy
|
|
43
|
+
# per environment, each granted publishing the apps here. An org admin registers
|
|
44
|
+
# it once, in the web app under Organization settings -> Service accounts ->
|
|
45
|
+
# Trusted access; "Publishing custom apps" in README.md has the steps.
|
|
46
|
+
#
|
|
47
|
+
# A STATIC `OXY_TOKEN` SECRET IS THE FALLBACK, and it wins when it is set: a
|
|
48
|
+
# repo that already publishes with one keeps doing exactly that, and a
|
|
49
|
+
# deployment without trusted access has no other way. It is one or the other
|
|
50
|
+
# per run, never a mix — with `OXY_TOKEN` set, no OIDC token is requested.
|
|
41
51
|
#
|
|
42
52
|
# TWO JOBS, and the split is the security boundary, not a style choice.
|
|
43
53
|
# `build` runs the bundles' own postinstall and build scripts — arbitrary
|
|
44
54
|
# third-party code out of node_modules — and holds no publish credential.
|
|
45
|
-
# `publish` holds
|
|
55
|
+
# `publish` holds the credential — the `id-token` permission, or the fallback
|
|
56
|
+
# `OXY_TOKEN` — downloads what `build` produced, and runs no
|
|
46
57
|
# package script at all. That includes Oxy Functions: `build` bundles them with
|
|
47
58
|
# `oxyc publish --build-only`, and `publish` runs `oxyc publish --prebuilt`,
|
|
48
59
|
# which checks they arrived rather than running esbuild — so the credential and
|
|
@@ -60,9 +71,10 @@
|
|
|
60
71
|
# fails rather than falling through to the more dangerous of the two.
|
|
61
72
|
#
|
|
62
73
|
# The same resolved name does two jobs: it is the `--env` passed to the CLI AND
|
|
63
|
-
# the GitHub `environment:` the publish job runs in
|
|
64
|
-
#
|
|
65
|
-
#
|
|
74
|
+
# the GitHub `environment:` the publish job runs in — the environment a trust
|
|
75
|
+
# policy is bound to, and where the fallback `OXY_TOKEN` for that env lives. So
|
|
76
|
+
# the credential and the destination cannot disagree — a production publish can
|
|
77
|
+
# only ever be made by a run production's policy (or token) covers. It is
|
|
66
78
|
# resolved ONCE, by `.github/scripts/publish-env.sh` in the `build` job, and
|
|
67
79
|
# read twice; a job's `environment:` can read `needs`, so that is the only
|
|
68
80
|
# place a single value can reach both consumers.
|
|
@@ -141,7 +153,14 @@ env:
|
|
|
141
153
|
# validate.yaml's `OXY_VERSION`: that one is the Rust `oxy` that validates the
|
|
142
154
|
# workspace, a different tool on a different release line. `--build-only` and
|
|
143
155
|
# `--prebuilt`, which the two jobs below depend on, arrived in 0.4.0.
|
|
144
|
-
|
|
156
|
+
#
|
|
157
|
+
# THE PIN DECIDES WHICH OIDC REGISTRATION THE PUBLISH JOB CAN USE. Through
|
|
158
|
+
# 0.6.0, `oxyc publish` knows one exchange: the app's own registered
|
|
159
|
+
# publisher (`/api/customer-apps/<app-id>/publishers`). Trust policies on a
|
|
160
|
+
# service account are honoured from 0.7.0, which tries them first and falls
|
|
161
|
+
# back to the publisher. A pin older than that still publishes, through the
|
|
162
|
+
# publisher alone.
|
|
163
|
+
OXYC_VERSION: "0.7.0"
|
|
145
164
|
|
|
146
165
|
jobs:
|
|
147
166
|
build:
|
|
@@ -404,15 +423,17 @@ jobs:
|
|
|
404
423
|
# The gate, and the credential boundary. `dev` on a push, `production` on
|
|
405
424
|
# a manual dispatch — the SAME string this job passes to `oxyc publish
|
|
406
425
|
# --env` below, resolved once in `build`, so the destination this job
|
|
407
|
-
# publishes to and the environment it
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
#
|
|
426
|
+
# publishes to and the environment it authenticates as are the same thing
|
|
427
|
+
# by construction. The environment's name is a claim in the job's OIDC
|
|
428
|
+
# token, so a trust policy registered for `production` cannot be satisfied
|
|
429
|
+
# by a run in `dev`; and where the fallback is in use, each environment
|
|
430
|
+
# holds its own `OXY_TOKEN`.
|
|
411
431
|
#
|
|
412
432
|
# The one hole, and it is a human's to close rather than this file's: a
|
|
413
433
|
# repository-level `OXY_TOKEN`, if one exists, is still reachable here —
|
|
414
434
|
# environment secrets take PRECEDENCE over repository secrets, they do not
|
|
415
|
-
# exclude them
|
|
435
|
+
# exclude them — and a token that is set wins over OIDC. See the note on
|
|
436
|
+
# `OXY_TOKEN` in the publish step.
|
|
416
437
|
#
|
|
417
438
|
# Referencing an environment that does not exist CREATES it on the first
|
|
418
439
|
# run ("Running a workflow that references an environment that does not
|
|
@@ -426,6 +447,17 @@ jobs:
|
|
|
426
447
|
# need GitHub Team or Pro; oxy-hq is on Team.
|
|
427
448
|
environment: ${{ needs.build.outputs.publish-env }}
|
|
428
449
|
|
|
450
|
+
# `id-token: write` lets this job ask GitHub for a signed statement of what
|
|
451
|
+
# it is — repository, workflow file, environment — which is the whole
|
|
452
|
+
# credential on the default path. It grants nothing in the repository, and
|
|
453
|
+
# it is job-level: every step below can use it, which is why this job runs
|
|
454
|
+
# no package script. `contents: read` is restated because a job-level
|
|
455
|
+
# `permissions` block REPLACES the workflow-level one rather than adding to
|
|
456
|
+
# it. The `build` job is deliberately not given the token.
|
|
457
|
+
permissions:
|
|
458
|
+
id-token: write
|
|
459
|
+
contents: read
|
|
460
|
+
|
|
429
461
|
steps:
|
|
430
462
|
- uses: actions/checkout@v4
|
|
431
463
|
with:
|
|
@@ -434,8 +466,8 @@ jobs:
|
|
|
434
466
|
# bundle built from one commit while claiming another is precisely
|
|
435
467
|
# the confusion the build id exists to prevent.
|
|
436
468
|
ref: ${{ needs.build.outputs.sha }}
|
|
437
|
-
# This job holds
|
|
438
|
-
# repository credential in .git/config.
|
|
469
|
+
# This job holds the publish credential. It has no reason to also be
|
|
470
|
+
# holding a repository credential in .git/config.
|
|
439
471
|
persist-credentials: false
|
|
440
472
|
|
|
441
473
|
- uses: actions/download-artifact@v4
|
|
@@ -492,6 +524,14 @@ jobs:
|
|
|
492
524
|
|
|
493
525
|
- name: Publish each bundle
|
|
494
526
|
env:
|
|
527
|
+
# THE FALLBACK CREDENTIAL, empty unless somebody set the secret. With
|
|
528
|
+
# it empty — the default — `oxyc publish` exchanges this job's OIDC
|
|
529
|
+
# token instead and nothing is stored anywhere. With it set, it WINS:
|
|
530
|
+
# no OIDC token is requested, and the run authenticates exactly as it
|
|
531
|
+
# did before OIDC was the default. Any kind of Oxy token works here —
|
|
532
|
+
# a service account token (`oxy_sat_…`), a personal access token
|
|
533
|
+
# (`oxy_pat_…`), or a workspace API key from before either existed.
|
|
534
|
+
#
|
|
495
535
|
# Step-scoped as well as job-isolated. The job split is what keeps
|
|
496
536
|
# this token away from the bundles' own build scripts — those ran
|
|
497
537
|
# in `build`, on a different runner, and nothing in THIS job runs a
|
|
@@ -509,15 +549,27 @@ jobs:
|
|
|
509
549
|
# precedence, not exclusion: an environment secret OVERRIDES a
|
|
510
550
|
# repository one of the same name, it does not suppress it. So a
|
|
511
551
|
# repo still carrying an old repository-level `OXY_TOKEN` serves
|
|
512
|
-
# BOTH environments from it wherever the environment has none —
|
|
513
|
-
#
|
|
514
|
-
#
|
|
552
|
+
# BOTH environments from it wherever the environment has none — OIDC
|
|
553
|
+
# is never tried, and each publish looks perfectly healthy while
|
|
554
|
+
# authenticating with a token nobody scoped to it.
|
|
515
555
|
#
|
|
516
556
|
# Nothing here can catch that: GitHub does not tell a job which
|
|
517
557
|
# level a secret came from, so from inside this step the two cases
|
|
518
558
|
# are identical. It has to be a human, once: DELETE any
|
|
519
|
-
# repository-level `OXY_TOKEN` after
|
|
559
|
+
# repository-level `OXY_TOKEN` — after registering the trust policies
|
|
560
|
+
# (or adding the environment-level tokens) that replace it. The step
|
|
561
|
+
# below prints which of the two credentials it used, every run.
|
|
520
562
|
OXY_TOKEN: ${{ secrets.OXY_TOKEN }}
|
|
563
|
+
# The ID of the service account the OIDC exchange acts as — from the
|
|
564
|
+
# account's page under Organization settings → API access. REQUIRED
|
|
565
|
+
# for a trust policy to be used: the deployment matches this run
|
|
566
|
+
# against that one account's policies and never picks an account on
|
|
567
|
+
# a run's behalf. Unset, the publish goes through each app's
|
|
568
|
+
# registered publisher instead. The ID, never `<org-slug>/<name>`: a
|
|
569
|
+
# name can be taken over by another organization, an ID cannot.
|
|
570
|
+
# Unused on the `OXY_TOKEN` path. An ENVIRONMENT variable where
|
|
571
|
+
# `dev` and `production` use different accounts.
|
|
572
|
+
OXY_SERVICE_ACCOUNT: ${{ vars.OXY_SERVICE_ACCOUNT }}
|
|
521
573
|
# `dev` or `production`, resolved once in the build job. Passed to
|
|
522
574
|
# `oxyc publish --env` below; the identical string selected the
|
|
523
575
|
# environment this job is running in.
|
|
@@ -552,8 +604,28 @@ jobs:
|
|
|
552
604
|
exit 1
|
|
553
605
|
fi
|
|
554
606
|
|
|
555
|
-
|
|
556
|
-
|
|
607
|
+
# Which credential this run uses, said out loud on every run — the
|
|
608
|
+
# two paths fail differently, and "which one was it" should never
|
|
609
|
+
# need working out from a stack of 403s.
|
|
610
|
+
if [ -n "${OXY_TOKEN:-}" ]; then
|
|
611
|
+
echo "authenticating with the OXY_TOKEN secret (the fallback path). To publish with no stored secret instead, register a trust policy for the '$PUBLISH_ENV' environment and delete the secret — see 'Publishing custom apps' in README.md."
|
|
612
|
+
# A service account is chosen by the OIDC exchange only; leaving
|
|
613
|
+
# the variable set would be a setting that silently does nothing.
|
|
614
|
+
unset OXY_SERVICE_ACCOUNT
|
|
615
|
+
elif [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
|
|
616
|
+
echo "authenticating with this job's GitHub OIDC token — no stored secret. oxyc exchanges it for a fifteen-minute token on each publish and revokes that token when the command ends."
|
|
617
|
+
# An unset secret arrives as an EMPTY variable, not a missing one.
|
|
618
|
+
# oxyc treats the two alike; unsetting it says so here too.
|
|
619
|
+
unset OXY_TOKEN
|
|
620
|
+
if [ -z "${OXY_SERVICE_ACCOUNT:-}" ]; then
|
|
621
|
+
unset OXY_SERVICE_ACCOUNT
|
|
622
|
+
echo "no OXY_SERVICE_ACCOUNT variable on the '$PUBLISH_ENV' environment: each app publishes through its registered publisher. To use a trust policy, set the variable to the service account's ID."
|
|
623
|
+
fi
|
|
624
|
+
else
|
|
625
|
+
# Reachable only if this job lost `id-token: write` — an edit to
|
|
626
|
+
# the `permissions` block above, or an org policy that withholds
|
|
627
|
+
# it. Fail here, by name, rather than as oxyc's "not authenticated".
|
|
628
|
+
echo "::error::this job has nothing to authenticate with: GitHub issued it no OIDC token and no OXY_TOKEN secret is set. The publish job needs 'permissions: id-token: write' (see the job's permissions block), and a trust policy registered for the '$PUBLISH_ENV' environment — or, as the fallback, an OXY_TOKEN secret under Settings -> Environments -> $PUBLISH_ENV -> Environment secrets, NOT at repository level." >&2
|
|
557
629
|
exit 1
|
|
558
630
|
fi
|
|
559
631
|
|
package/template/README.md
CHANGED
|
@@ -201,11 +201,11 @@ stops publishing with nobody told, and guessed as on publishes when nobody
|
|
|
201
201
|
asked, so it is not guessed.
|
|
202
202
|
|
|
203
203
|
Note what the switch does **not** do. It decides whether the publish job runs;
|
|
204
|
-
it never softens what happens once it has. With CI publishing on, a
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
it never softens what happens once it has. With CI publishing on, a run that
|
|
205
|
+
cannot authenticate is a **hard failure**. That is the reason there is a switch
|
|
206
|
+
at all rather than the obvious shortcut of skipping whenever no credential
|
|
207
|
+
turns up: inferred from the credential, "this repo does not publish from CI"
|
|
208
|
+
and "somebody forgot production's trust policy" are the same observation — and
|
|
209
209
|
the second one ships nothing, quietly, for as long as nobody looks.
|
|
210
210
|
|
|
211
211
|
Everything from here to the end of this section describes a repo that has
|
|
@@ -241,9 +241,10 @@ is no code path that can fall back to a default.
|
|
|
241
241
|
|
|
242
242
|
There is deliberately **no environment picker** on the manual run. An input
|
|
243
243
|
would need a default, which is the thing being removed; and the same resolved
|
|
244
|
-
name also selects the GitHub environment
|
|
245
|
-
|
|
246
|
-
destination agree only by an operator's care. To publish a
|
|
244
|
+
name also selects the GitHub environment the job runs in — which is what its
|
|
245
|
+
credential is bound to — so leaving it to a form field would mean the
|
|
246
|
+
credential and the destination agree only by an operator's care. To publish a
|
|
247
|
+
dev draft by hand,
|
|
247
248
|
re-run the push-triggered run from the Actions tab — a re-run keeps the
|
|
248
249
|
original event, so it still resolves to `dev`.
|
|
249
250
|
|
|
@@ -251,10 +252,10 @@ It is **two jobs**, and the split is the security boundary rather than a
|
|
|
251
252
|
tidiness one. `build` runs `pnpm install` and `pnpm -r build` — which is to
|
|
252
253
|
say it executes the postinstall and build scripts of every dependency your
|
|
253
254
|
apps pull in — and holds no publish credential at all. It hands the built
|
|
254
|
-
output to `publish` as a workflow artifact. `publish` holds
|
|
255
|
-
runs no package script: `oxyc publish --dir` uploads a pre-built directory
|
|
255
|
+
output to `publish` as a workflow artifact. `publish` holds the credential,
|
|
256
|
+
and runs no package script: `oxyc publish --dir` uploads a pre-built directory
|
|
256
257
|
as-is instead of building anything. So a compromised dependency in an app's
|
|
257
|
-
tree never runs on a runner where the
|
|
258
|
+
tree never runs on a runner where the credential exists.
|
|
258
259
|
|
|
259
260
|
**On a fresh repo it does nothing at all, and that is the intended
|
|
260
261
|
behaviour.** The scaffold ships no apps and no `pnpm-lock.yaml` — there is
|
|
@@ -264,32 +265,90 @@ the run goes **green**. The install is skipped along with the publish
|
|
|
264
265
|
deliberately: `pnpm install --frozen-lockfile` fails without a lockfile, and
|
|
265
266
|
a repo whose CI is red the day it is created is a repo where nobody reads CI.
|
|
266
267
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
268
|
+
#### How CI authenticates: no stored secret
|
|
269
|
+
|
|
270
|
+
**By default the publish job holds no secret at all.** It is granted
|
|
271
|
+
`id-token: write`, which lets it ask GitHub for a signed statement of what it
|
|
272
|
+
is — this repository, this workflow file, the environment the job runs in.
|
|
273
|
+
`oxyc publish` hands that statement to oxy and gets back a token that lasts
|
|
274
|
+
fifteen minutes, acts as a **service account** of your organization, and is
|
|
275
|
+
revoked when the command ends. There is nothing to rotate and nothing to leak
|
|
276
|
+
from Settings.
|
|
277
|
+
|
|
278
|
+
What makes oxy honour the statement is a **trust policy** on the service
|
|
279
|
+
account, and it has to exist before the first publish. An org admin registers
|
|
280
|
+
one **per environment**, once:
|
|
281
|
+
|
|
282
|
+
1. In the web app: **Organization settings → API access → Service accounts**.
|
|
283
|
+
Create a service account — `deployer` is the conventional name — with the
|
|
284
|
+
role *member*.
|
|
285
|
+
2. Open it and, under **Trusted access**, add a policy for each of `dev` and
|
|
286
|
+
`production`:
|
|
287
|
+
|
|
288
|
+
| Field | Value |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| Repository | this repo, as `<owner>/<repo>` |
|
|
291
|
+
| Workflow | `.github/workflows/publish.yaml` |
|
|
292
|
+
| Environment | `dev` (and a second policy for `production`) |
|
|
293
|
+
| Grant | publishing the apps under `apps/` — and nothing else |
|
|
294
|
+
|
|
295
|
+
The environment is part of the match, so the `production` policy cannot be
|
|
296
|
+
satisfied by a push-triggered run in `dev`: the credential and the destination
|
|
297
|
+
agree by construction, exactly as they did when each environment held its own
|
|
298
|
+
token. A run no policy matches fails with the repository, workflow and ref it
|
|
299
|
+
presented, which is usually enough to see which field is off.
|
|
300
|
+
|
|
301
|
+
**Name the account the workflow acts as, by its ID**, in an
|
|
302
|
+
`OXY_SERVICE_ACCOUNT` **variable** on the environment. The ID is on the
|
|
303
|
+
account's page (Organization settings → API access → Service accounts → the
|
|
304
|
+
account). It is required for a trust policy to be used at all: the deployment
|
|
305
|
+
matches a run against the policies of the one account its workflow names, and
|
|
306
|
+
never picks one on a run's behalf — anyone can register a policy that names a
|
|
307
|
+
repository. With the variable unset, the publish goes through each app's
|
|
308
|
+
*registered publisher* instead, as it did before trust policies existed.
|
|
309
|
+
|
|
310
|
+
The ID, never `<org-slug>/<name>`: a slug is free for anyone once its org
|
|
311
|
+
renames or is deleted, so a name can be taken over and an ID cannot. A name in
|
|
312
|
+
the variable is refused (`service_account_required`).
|
|
313
|
+
|
|
314
|
+
> Trust policies need a recent enough CLI in CI. The workflow pins the one it
|
|
315
|
+
> installs as `OXYC_VERSION`; the comment beside the pin says which release
|
|
316
|
+
> first honours them. An older pin still publishes with no stored secret, but
|
|
317
|
+
> only through an app's *registered publisher* — the earlier, per-app
|
|
318
|
+
> registration (`/api/customer-apps/<app-id>/publishers`), which Oxy staff set
|
|
319
|
+
> up.
|
|
320
|
+
|
|
321
|
+
#### The fallback: an `OXY_TOKEN` secret
|
|
322
|
+
|
|
323
|
+
A static token still works, and **when it is set it wins** — the job then
|
|
324
|
+
requests no OIDC token and authenticates exactly as this workflow always did.
|
|
325
|
+
Use it where the deployment has no trusted access yet, or where a repository
|
|
326
|
+
cannot be given a trust policy. Each of the two **GitHub environments** — `dev`
|
|
327
|
+
and `production` — holds its own **`OXY_TOKEN`** (Settings → Environments →
|
|
328
|
+
*environment* → Environment secrets). Prefer a **service account token**
|
|
329
|
+
(Organization settings → API access → Service accounts → *account* → Tokens),
|
|
330
|
+
granted only what publishing needs; a personal access token or an older
|
|
331
|
+
workspace API key also works, and carries its owner's access with it — a
|
|
332
|
+
leaked one is that person, not "this app only".
|
|
333
|
+
|
|
334
|
+
Every run prints which of the two it used. **A run with neither fails,
|
|
335
|
+
loudly, and that is the intended behaviour.** It is also why CI publishing is
|
|
336
|
+
a switch rather than something inferred from a credential's presence: with the
|
|
337
|
+
switch on, a job that cannot authenticate is a red run that says why, never a
|
|
338
|
+
green run that published nothing.
|
|
280
339
|
|
|
281
340
|
**And any repository-level `OXY_TOKEN` must be DELETED, not left in place.**
|
|
282
341
|
GitHub's order is environment → repository → organisation, and that is
|
|
283
342
|
*precedence, not exclusion*: an environment secret overrides a repository one
|
|
284
343
|
of the same name, it does not suppress it. A repo still carrying an old
|
|
285
344
|
repository-level token therefore serves **both** environments from it wherever
|
|
286
|
-
the environment has none — every run looks healthy,
|
|
287
|
-
|
|
345
|
+
the environment has none — every run looks healthy, OIDC is never tried, and
|
|
346
|
+
production ships under whatever that token was scoped to.
|
|
288
347
|
|
|
289
348
|
Nothing in CI can catch it. GitHub does not tell a job which level a secret
|
|
290
349
|
came from, so the two cases are identical from inside the run. It is a
|
|
291
350
|
one-time human check, and it is the single thing to get right when moving an
|
|
292
|
-
existing repo onto environments:
|
|
351
|
+
existing repo onto trust policies or onto environments:
|
|
293
352
|
|
|
294
353
|
```bash
|
|
295
354
|
gh secret list --repo <owner>/<repo> # repository level
|
|
@@ -311,10 +370,13 @@ So adding the first app is two things — three, if this repo publishes from CI:
|
|
|
311
370
|
lockfile it writes. Only the lockfile: `node_modules/` and every app's
|
|
312
371
|
build output are covered by `.gitignore`, so `git add -A` after an install
|
|
313
372
|
stages the one file CI needs and none of the ones it does not;
|
|
314
|
-
3. **only with `OXY_CI_PUBLISH` set** —
|
|
373
|
+
3. **only with `OXY_CI_PUBLISH` set** — a trust policy for each environment
|
|
374
|
+
that grants publishing the app (or, as the fallback, an `OXY_TOKEN` in each
|
|
375
|
+
environment).
|
|
315
376
|
|
|
316
377
|
Miss one and the workflow fails loudly and names it: the discovery step
|
|
317
|
-
points at the manifest, or the missing lockfile
|
|
378
|
+
points at the manifest, or the missing lockfile; the publish step says no
|
|
379
|
+
trust policy matched the run, or that it had nothing to authenticate with. It
|
|
318
380
|
does not guess and it does not half-publish. (The first two are needed even
|
|
319
381
|
with CI publishing off: the `build` job installs and builds either way.)
|
|
320
382
|
|
|
@@ -397,8 +459,10 @@ someone points each at the branch:
|
|
|
397
459
|
|
|
398
460
|
Promoting the production semantic model is a deliberate, human step: open the
|
|
399
461
|
production workspace in Oxygen and pull the latest commit. **CI does not do
|
|
400
|
-
it**, and holds no credential that could — the
|
|
401
|
-
|
|
462
|
+
it**, and holds no credential that could — the trust policy behind each
|
|
463
|
+
environment grants publishing apps and nothing else, so the token the job is
|
|
464
|
+
handed cannot compile or promote a workspace at all. (Keep it that way on the
|
|
465
|
+
fallback path: an `OXY_TOKEN` should be a token that can only publish.) So a merge
|
|
402
466
|
to `main` never changes what production answers until someone decides it
|
|
403
467
|
should.
|
|
404
468
|
|