mfeflow 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 +8 -3
- package/assets/features/deploy-cloudflare/feature.json +85 -0
- package/assets/features/deploy-cloudflare/files/cloudflare.json.eta +13 -0
- package/assets/features/deploy-cloudflare/files/cloudflare.md.eta +104 -0
- package/assets/features/deploy-cloudflare/files/deploy-cloudflare.ts +318 -0
- package/assets/features/deploy-cloudflare/files/deploy-cloudflare.yml +111 -0
- package/assets/features/deploy-cloudflare/files/headers.remote +14 -0
- package/assets/features/deploy-cloudflare/files/headers.shell.eta +18 -0
- package/assets/features/deploy-cloudflare/files/r2-cors.json +10 -0
- package/assets/features/deploy-cloudflare/files/registry-proxy.ts +80 -0
- package/assets/features/deploy-cloudflare/files/wrangler.remote.jsonc.eta +21 -0
- package/assets/features/deploy-cloudflare/files/wrangler.shell.jsonc.eta +40 -0
- package/assets/features/deploy-docker/feature.json +22 -0
- package/assets/features/deploy-docker/files/40-mfe-config.sh +76 -0
- package/assets/features/deploy-docker/files/Dockerfile +48 -0
- package/assets/features/deploy-docker/files/compose-registry.json.eta +19 -0
- package/assets/features/deploy-docker/files/compose.yaml.eta +50 -0
- package/assets/features/deploy-docker/files/docker.md +52 -0
- package/assets/features/deploy-docker/files/dockerignore +17 -0
- package/assets/features/deploy-docker/files/k8s.yaml +114 -0
- package/assets/features/deploy-docker/files/nginx.remote.conf +73 -0
- package/assets/features/deploy-docker/files/nginx.shell.conf +84 -0
- package/assets/features/registry-server/feature.json +128 -0
- package/assets/features/registry-server/files/deploy-server.yml +69 -0
- package/assets/features/registry-server/files/registry-server.md.eta +52 -0
- package/assets/features/registry-server/files/registry-sources.ts +12 -0
- package/assets/features/registry-static/feature.json +61 -0
- package/assets/features/registry-static/files/deploy.yml +61 -0
- package/assets/features/registry-static/files/registry.md.eta +49 -0
- package/assets/features/release/feature.json +43 -0
- package/assets/features/release/files/changeset-readme.md +7 -0
- package/assets/features/release/files/config.json +10 -0
- package/assets/features/release/files/releasing.md +21 -0
- package/assets/templates/monorepo/feature.json +7 -6
- package/assets/templates/monorepo/files/remote/{mfe.json.eta → mfeflow.app.json.eta} +1 -0
- package/assets/templates/monorepo/files/root/README.md.eta +1 -1
- package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +3 -2
- package/assets/templates/monorepo/files/shell/{mfe.json.eta → mfeflow.app.json.eta} +1 -0
- package/assets/templates/monorepo/files/shell/rsbuild.config.ts.eta +42 -6
- package/assets/templates/monorepo/migrations/0.4.0-app-file.mjs +58 -0
- package/assets/templates/polyrepo/feature.json +305 -0
- package/assets/templates/polyrepo/files/AGENTS.md.eta +15 -0
- package/assets/templates/polyrepo/files/changeset/README.md +5 -0
- package/assets/templates/polyrepo/files/changeset/config.json +9 -0
- package/assets/templates/polyrepo/files/packages/contracts/package.json.eta +23 -0
- package/assets/templates/polyrepo/files/packages/contracts/src/index.ts +11 -0
- package/assets/templates/polyrepo/files/packages/contracts/tsconfig.json.eta +5 -0
- package/assets/templates/polyrepo/files/packages/tokens/package.json.eta +23 -0
- package/assets/templates/polyrepo/files/packages/tokens/src/index.ts +13 -0
- package/assets/templates/polyrepo/files/packages/tokens/tsconfig.json.eta +5 -0
- package/assets/templates/polyrepo/files/platform/README.md.eta +43 -0
- package/assets/templates/polyrepo/files/platform/default.json.eta +27 -0
- package/assets/templates/polyrepo/files/remote/README.md.eta +23 -0
- package/assets/templates/polyrepo/files/renovate.json.eta +4 -0
- package/assets/templates/polyrepo/files/shell/README.md.eta +32 -0
- package/assets/templates/polyrepo/files/tsconfig.base.json.eta +13 -0
- package/assets/templates/polyrepo/files/workflows/platform-ci.yml +29 -0
- package/assets/templates/polyrepo/files/workflows/release.yml +40 -0
- package/assets/templates/polyrepo/files/workflows/reusable-ci.yml.eta +50 -0
- package/assets/templates/polyrepo/files/workflows/team-ci.yml.eta +12 -0
- package/dist/against.d.ts +16 -0
- package/dist/catalog.d.ts +4 -0
- package/dist/checks/compat.d.ts +12 -0
- package/dist/checks/index.d.ts +0 -1
- package/dist/commands/add.d.ts +0 -2
- package/dist/commands/adopt.d.ts +54 -0
- package/dist/commands/deploy.d.ts +35 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/init.d.ts +9 -1
- package/dist/commands/server-mode.d.ts +210 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/team-repo.d.ts +9 -0
- package/dist/commands/upgrade.d.ts +66 -0
- package/dist/devremotes.d.ts +15 -0
- package/dist/exit.d.ts +1 -0
- package/dist/glue.d.ts +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4600 -1875
- package/dist/inspect.d.ts +14 -2
- package/dist/output.d.ts +138 -3
- package/dist/pipeline.d.ts +5 -2
- package/dist/polyrepo.d.ts +69 -0
- package/dist/publish.d.ts +10 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/server-client.d.ts +69 -0
- package/dist/system.d.ts +2 -0
- package/dist/testing.d.ts +17 -1
- package/dist/upgrade.d.ts +25 -0
- package/dist/write.d.ts +8 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ pnpm dev # shell on :4300, remotes on :4311,
|
|
|
26
26
|
| `mfeflow registry build <dir>` | Check a folder of features and write `index.json` and the files to host |
|
|
27
27
|
| `mfeflow dev` | Run the shell and every remote, one prefixed log each; follows `add remote` |
|
|
28
28
|
| `mfeflow doctor` | Singletons, compatibility, config health, drift; `--fix` repairs the safe ones |
|
|
29
|
-
| `mfeflow validate` | `mfeflow.json`, every `
|
|
29
|
+
| `mfeflow validate` | `mfeflow.json`, every `mfeflow.app.json` and every registry entry against schemas |
|
|
30
30
|
| `mfeflow check-compat` | Each remote's shared ranges against the shells it runs in |
|
|
31
31
|
| `mfeflow budget` | Built first-load JS of the shell and each remote against the cost-model budget |
|
|
32
32
|
| `mfeflow deploy <remote> --env <env>` | Upload a remote's build to a static registry host and point the env's index at it |
|
|
@@ -48,13 +48,17 @@ runs part of the project; `--remote name=url` uses a server you run yourself (th
|
|
|
48
48
|
localhost unless `--allow-origin <origin>` names it); Ctrl+C stops every app, process trees included, on Windows too.
|
|
49
49
|
It exits 130 after Ctrl+C.
|
|
50
50
|
|
|
51
|
+
Each app keeps its owner, routes and budgets in `mfeflow.app.json`. An app that still has the old `mfe.json` works:
|
|
52
|
+
the CLI reads it and prints one notice per command. An app with both files is an error (E122).
|
|
53
|
+
|
|
51
54
|
`doctor` runs these checks, each with a code and a docs page (`https://mfeflow.github.io/mfeflow/errors/<code>`). It exits with 0
|
|
52
55
|
when there are no errors, else with the lowest exit code among the errors' ranges (project 3 before build 5).
|
|
53
56
|
|
|
54
57
|
| Check | Code | Fixed by `--fix` |
|
|
55
58
|
| ------------------------------------- | -------------- | ----------------------------- |
|
|
56
59
|
| every app in mfeflow.json is on disk | E112 | |
|
|
57
|
-
| valid `
|
|
60
|
+
| valid `mfeflow.app.json`, name match | E113, E116 | |
|
|
61
|
+
| no app has both app file names | E122 | |
|
|
58
62
|
| no shared port | E115 | |
|
|
59
63
|
| no route claimed twice | E116 | |
|
|
60
64
|
| registry entry per remote and env | E114 | writes the missing ones |
|
|
@@ -70,7 +74,7 @@ when there are no errors, else with the lowest exit code among the errors' range
|
|
|
70
74
|
| server keys listed in `turbo.json` | E121 (warning) | |
|
|
71
75
|
|
|
72
76
|
`validate` runs the schema and registry checks, `check-compat` the compatibility check, `budget` reads the build output
|
|
73
|
-
(`dist/`): each remote's entry against `budget.remoteKb` (10), a route against `
|
|
77
|
+
(`dist/`): each remote's entry against `budget.remoteKb` (10), a route against `mfeflow.app.json` `budgets.initialJsKb`, and the
|
|
74
78
|
shell against `budget.baselineKb + budget.shellKb` (32) when a baseline is set. All four take `--json` and exit non-zero on
|
|
75
79
|
errors. LCP is measured by the perf job, not by `budget`.
|
|
76
80
|
|
|
@@ -111,6 +115,7 @@ MFEFLOW_E108 The target folder is not empty: /work/app already has notes.txt
|
|
|
111
115
|
| 5 | build or runtime problem: `MFEFLOW_E3xx` |
|
|
112
116
|
| 6 | feature registry problem: `MFEFLOW_E4xx` |
|
|
113
117
|
| 7 | deploy problem: `MFEFLOW_E5xx` |
|
|
118
|
+
| 8 | upgrade problem, or files that need a decision: `MFEFLOW_E6xx` |
|
|
114
119
|
| 130 | cancelled at a prompt (nothing was written), or Ctrl+C in `mfeflow dev` |
|
|
115
120
|
|
|
116
121
|
## Try it from a checkout
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deploy-cloudflare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One way to host the shell and remotes on Cloudflare Workers static assets: a wrangler.jsonc per app with staging and prod, _headers with the cache and CORS rules, a script that picks each environment's runtime config and registry index without rebuilding, an optional R2 proxy for /registry/*, and, when ci-github is present, a workflow. Opt-in; every file is yours to edit or delete.",
|
|
5
|
+
"keywords": ["cloudflare", "workers", "wrangler", "r2", "deploy"],
|
|
6
|
+
"requires": ["core"],
|
|
7
|
+
"prompts": [
|
|
8
|
+
{
|
|
9
|
+
"name": "r2Proxy",
|
|
10
|
+
"type": "boolean",
|
|
11
|
+
"message": "Serve /registry/* from an R2 bucket through a small Worker (the remotes are then published with mfeflow deploy)",
|
|
12
|
+
"default": false
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"to": "wrangler.jsonc",
|
|
18
|
+
"target": "shell",
|
|
19
|
+
"from": "files/wrangler.shell.jsonc.eta",
|
|
20
|
+
"template": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"to": "wrangler.jsonc",
|
|
24
|
+
"target": "remotes",
|
|
25
|
+
"from": "files/wrangler.remote.jsonc.eta",
|
|
26
|
+
"template": true,
|
|
27
|
+
"when": { "r2Proxy": false }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"to": "public/_headers",
|
|
31
|
+
"target": "shell",
|
|
32
|
+
"from": "files/headers.shell.eta",
|
|
33
|
+
"template": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"to": "public/_headers",
|
|
37
|
+
"target": "remotes",
|
|
38
|
+
"from": "files/headers.remote",
|
|
39
|
+
"when": { "r2Proxy": false }
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"to": "worker/registry-proxy.ts",
|
|
43
|
+
"target": "shell",
|
|
44
|
+
"from": "files/registry-proxy.ts",
|
|
45
|
+
"when": { "r2Proxy": true }
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"to": "deploy/r2-cors.json",
|
|
49
|
+
"target": "root",
|
|
50
|
+
"from": "files/r2-cors.json",
|
|
51
|
+
"when": { "r2Proxy": true }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"to": "deploy/cloudflare.json",
|
|
55
|
+
"target": "root",
|
|
56
|
+
"from": "files/cloudflare.json.eta",
|
|
57
|
+
"template": true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"to": "scripts/deploy-cloudflare.ts",
|
|
61
|
+
"target": "root",
|
|
62
|
+
"from": "files/deploy-cloudflare.ts"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"to": "docs/cloudflare.md",
|
|
66
|
+
"target": "root",
|
|
67
|
+
"from": "files/cloudflare.md.eta",
|
|
68
|
+
"template": true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"to": ".github/workflows/deploy-cloudflare.yml",
|
|
72
|
+
"target": "root",
|
|
73
|
+
"from": "files/deploy-cloudflare.yml",
|
|
74
|
+
"when": { "feature:ci-github": true }
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"dependencies": [{ "name": "wrangler", "range": "4.156.0", "target": "root", "dev": true }],
|
|
78
|
+
"scripts": [
|
|
79
|
+
{
|
|
80
|
+
"name": "deploy:cloudflare",
|
|
81
|
+
"command": "node --experimental-strip-types scripts/deploy-cloudflare.ts",
|
|
82
|
+
"target": "root"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Cloudflare
|
|
2
|
+
|
|
3
|
+
This is one way to host the project on Cloudflare Workers. The files were added by `deploy-cloudflare`. They are
|
|
4
|
+
yours: edit them, replace them, or delete them. Nothing else in the project depends on them.
|
|
5
|
+
|
|
6
|
+
## What was added
|
|
7
|
+
|
|
8
|
+
<% if (it.options.r2Proxy) { %>- `apps/shell/wrangler.jsonc`: the shell as a Worker with static assets, with a `staging` and a `prod` environment.
|
|
9
|
+
- `apps/shell/worker/registry-proxy.ts`: a small Worker that serves `/registry/*` from an R2 bucket.
|
|
10
|
+
- `deploy/r2-cors.json`: the CORS rules for the bucket, for the case where you serve it from its own domain.
|
|
11
|
+
<% } else { %>- `apps/<app>/wrangler.jsonc` for the shell and for each remote: one Worker per app, each with a `staging` and a `prod` environment. A remote added later gets its own file.
|
|
12
|
+
<% } %>- `apps/<app>/public/_headers`: the cache rules<% if (!it.options.r2Proxy) { %>, and on remotes the CORS header<% } %>.
|
|
13
|
+
- `deploy/cloudflare.json`: the address each environment is served from.
|
|
14
|
+
- `scripts/deploy-cloudflare.ts` and the script `pnpm deploy:cloudflare`.
|
|
15
|
+
- `.github/workflows/deploy-cloudflare.yml`, only when `ci-github` is in the project.
|
|
16
|
+
- `wrangler` as a dev dependency.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
<% if (it.options.r2Proxy) { %>The shell is one Worker. Remotes are published to an R2 bucket with `mfeflow deploy`, and the shell Worker serves the bucket's index and files under `/registry`. <% } else { %>The shell and each remote are separate Workers on their own addresses, so the shell loads remotes from another origin. <% } %>The build is made once. `pnpm deploy:cloudflare prod` then copies each `dist` into `apps/<app>/.mfeflow/cloudflare/prod`, and changes only that copy:
|
|
21
|
+
|
|
22
|
+
- The shell gets `config/runtime-config.json` from `runtime-config.prod.json`. The other environments' files are left out.
|
|
23
|
+
<% if (!it.options.r2Proxy) { %>- The shell gets `registry/index.json`, written from the remotes' versions and the addresses in `deploy/cloudflare.json`, and their origins are added to `registryOrigins` so the runtime accepts them.
|
|
24
|
+
- Each remote's `_headers` gets the shell's address as its `Access-Control-Allow-Origin`.
|
|
25
|
+
<% } %>
|
|
26
|
+
Then it runs `wrangler deploy --env prod` for each app. Cloudflare makes one Worker per environment, named `<%= it.scope.slice(1) %>-<app>-prod`. Bindings and `vars` are not inherited by an environment, so they are repeated in each block.
|
|
27
|
+
|
|
28
|
+
## Set it up
|
|
29
|
+
|
|
30
|
+
1. Put the real addresses in `deploy/cloudflare.json`. The script stops on an empty or `example.com` address.
|
|
31
|
+
2. Choose how each Worker is reached. Without a `routes` entry, Cloudflare uses a `workers.dev` address. For your own domain, add `"routes": [{ "pattern": "shop.example.com", "custom_domain": true }]` to the environment in `wrangler.jsonc`.
|
|
32
|
+
3. Build and look at what would be deployed:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pnpm build
|
|
36
|
+
pnpm deploy:cloudflare staging --dry-run
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Deploy. Wrangler reads `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from the environment:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pnpm deploy:cloudflare staging
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pass app names to deploy only those: `pnpm deploy:cloudflare prod shell`.
|
|
46
|
+
|
|
47
|
+
<% if (it.options.r2Proxy) { %>## The R2 bucket
|
|
48
|
+
|
|
49
|
+
Create one bucket for each environment, named in `wrangler.jsonc` (`<%= it.scope.slice(1) %>-registry-staging` and `<%= it.scope.slice(1) %>-registry-prod`):
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pnpm wrangler r2 bucket create <%= it.scope.slice(1) %>-registry-prod
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Publish remotes with `mfeflow deploy`, using the bucket's S3 endpoint and the shell's address plus `/registry` as the base URL, so that the entries in the index point through the Worker:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
export MFEFLOW_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
|
|
59
|
+
export MFEFLOW_S3_REGION=auto
|
|
60
|
+
mfeflow deploy catalog --env prod --host s3://<%= it.scope.slice(1) %>-registry-prod --base-url https://shop.acme.dev/registry
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The access key and secret come from an R2 API token, in `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
|
|
64
|
+
|
|
65
|
+
The Worker answers `/registry/index.json` from `envs/<env>/index.json`, and `/registry/<name>/<version>/<file>` from the same key in the bucket. The index is checked on every load, and versioned files are cached for a year.
|
|
66
|
+
|
|
67
|
+
Set `registryUrls` in the shell's runtime config only if you use another address. The default is `/registry/index.json`.
|
|
68
|
+
|
|
69
|
+
If you would rather serve the bucket from its own domain, connect the domain in the dashboard and allow the shell's origin. Edit `deploy/r2-cors.json`, then:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pnpm wrangler r2 bucket cors set <%= it.scope.slice(1) %>-registry-prod --file deploy/r2-cors.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
<% } %>## In CI
|
|
76
|
+
|
|
77
|
+
With `ci-github` in the project, `.github/workflows/deploy-cloudflare.yml` builds once and deploys each app with `cloudflare/wrangler-action`. A push to `main` deploys staging. Run it by hand to choose `prod`. Add these as secrets on the GitHub environments `cloudflare-staging` and `cloudflare-prod`:
|
|
78
|
+
|
|
79
|
+
- `CLOUDFLARE_API_TOKEN`: an API token that may edit Workers
|
|
80
|
+
- `CLOUDFLARE_ACCOUNT_ID`
|
|
81
|
+
|
|
82
|
+
Add a required reviewer to `cloudflare-prod` to hold production until someone approves. The workflow starts with no permissions, gives each job `contents: read`, and pins every action to a commit SHA. The workflow is only written when `ci-github` is already in the project, so add `ci-github` first.
|
|
83
|
+
|
|
84
|
+
## Cache rules
|
|
85
|
+
|
|
86
|
+
`_headers` is read by Cloudflare for static assets. It is not applied to responses your own Worker code makes<% if (it.options.r2Proxy) { %>, which is why `registry-proxy.ts` sets its own `Cache-Control`<% } %>. Each path matches one rule, because a header set by two rules is joined with a comma. The file holds up to 100 rules.
|
|
87
|
+
|
|
88
|
+
- `no-cache`: `/`, `/index.html`, `/mf-manifest.json`, `config/runtime-config*`<% if (!it.options.r2Proxy) { %>, `registry/index.json`<% } %>
|
|
89
|
+
- `public, max-age=31536000, immutable`: `/static/*`, whose names carry a hash
|
|
90
|
+
|
|
91
|
+
## Change or remove it
|
|
92
|
+
|
|
93
|
+
- Rename a Worker or add a route: edit `wrangler.jsonc`.
|
|
94
|
+
- Add a third environment: add a block to each `wrangler.jsonc`, to `deploy/cloudflare.json`, and the name to `ENVS` in the script.
|
|
95
|
+
- Stop using it: delete the files above and `pnpm remove wrangler`. Run `pnpm mfeflow validate` afterwards.
|
|
96
|
+
- Roll back: `pnpm wrangler rollback --env prod --config apps/<app>/wrangler.jsonc` makes the previous version of a Worker live again.
|
|
97
|
+
|
|
98
|
+
## Gotchas
|
|
99
|
+
|
|
100
|
+
- With the shell's single-page fallback, a mistyped path returns `index.html` with status 200. Remotes have no fallback, so a missing file is a 404.
|
|
101
|
+
- pnpm 11 stops an install when a package has an install script nobody has approved. `workerd`, which wrangler pulls in, is set to `false` under `allowBuilds` in `pnpm-workspace.yaml` of new projects, and wrangler deploys without running that script, and `wrangler dev` starts and serves the build with it (tried with wrangler 4.156.0). If your workspace file lacks the entry, add `workerd: false` there, or the install fails with `ERR_PNPM_IGNORED_BUILDS`.
|
|
102
|
+
- `compatibility_date` in `wrangler.jsonc` is `2026-10-28`. `wrangler dev` refuses a date newer than its bundled runtime knows, so raise the date together with wrangler.
|
|
103
|
+
- Each environment has one shell address, because a remote sends a single `Access-Control-Allow-Origin`. Two shell addresses for the same remote need two environments, or a rule of your own in the remote's `_headers`.
|
|
104
|
+
- The shell's `_headers` also sends `X-Content-Type-Options`, `Referrer-Policy` and `X-Frame-Options: SAMEORIGIN`. Remove the last one if another site must embed your shell in a frame.
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Stages each app's build for one environment and deploys it to Cloudflare Workers. It is your code:
|
|
2
|
+
// change it freely. The build is made once; only the staged copy differs per environment.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/deploy-cloudflare.ts <staging|prod> [app...] stage, then wrangler deploy
|
|
5
|
+
// node scripts/deploy-cloudflare.ts <env> [app...] --prepare-only stage only (CI runs wrangler itself)
|
|
6
|
+
// node scripts/deploy-cloudflare.ts <env> [app...] --dry-run stage and print the wrangler commands
|
|
7
|
+
// node scripts/deploy-cloudflare.ts plan <env>[,env] print the CI matrix as JSON
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const ENVS = ['staging', 'prod'];
|
|
13
|
+
const STAGE = '.mfeflow/cloudflare';
|
|
14
|
+
const SHELL_ORIGIN_TOKEN = '__SHELL_ORIGIN__';
|
|
15
|
+
|
|
16
|
+
interface Project {
|
|
17
|
+
shell: string;
|
|
18
|
+
remotes: string[];
|
|
19
|
+
packageDirs: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EnvConfig {
|
|
23
|
+
shell?: string;
|
|
24
|
+
remotes?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Config {
|
|
28
|
+
registry: string;
|
|
29
|
+
envs: Record<string, EnvConfig>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(file: string): unknown {
|
|
33
|
+
let text: string;
|
|
34
|
+
try {
|
|
35
|
+
text = fs.readFileSync(file, 'utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`${file} is missing`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${file} is not valid JSON`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readProject(root: string): Project {
|
|
51
|
+
const file = path.join(root, 'mfeflow.json');
|
|
52
|
+
const json = readJson(file);
|
|
53
|
+
const apps = isRecord(json) && isRecord(json.apps) ? json.apps : {};
|
|
54
|
+
let shell = '';
|
|
55
|
+
const remotes: string[] = [];
|
|
56
|
+
const packageDirs: Record<string, string> = {};
|
|
57
|
+
for (const [name, app] of Object.entries(apps)) {
|
|
58
|
+
if (!isRecord(app) || typeof app.path !== 'string') continue;
|
|
59
|
+
// These end up in file paths and in the CI matrix, so keep to what mfeflow itself allows.
|
|
60
|
+
if (
|
|
61
|
+
!/^[a-z][a-z0-9_]*$/.test(name) ||
|
|
62
|
+
!/^[\w.-]+(\/[\w.-]+)*$/.test(app.path) ||
|
|
63
|
+
app.path.split('/').includes('..')
|
|
64
|
+
) {
|
|
65
|
+
throw new Error(`${file}: app "${name}" has a name or path this script will not use`);
|
|
66
|
+
}
|
|
67
|
+
packageDirs[name] = app.path;
|
|
68
|
+
if (app.kind === 'shell') shell = name;
|
|
69
|
+
if (app.kind === 'remote') remotes.push(name);
|
|
70
|
+
}
|
|
71
|
+
if (shell === '') throw new Error(`${file} has no shell app`);
|
|
72
|
+
return { shell, remotes, packageDirs };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readConfig(root: string): Config {
|
|
76
|
+
const json = readJson(path.join(root, 'deploy/cloudflare.json'));
|
|
77
|
+
if (!isRecord(json) || !isRecord(json.envs))
|
|
78
|
+
throw new Error('deploy/cloudflare.json has no "envs"');
|
|
79
|
+
const envs: Record<string, EnvConfig> = {};
|
|
80
|
+
for (const [name, value] of Object.entries(json.envs)) {
|
|
81
|
+
if (!isRecord(value)) continue;
|
|
82
|
+
const remotes: Record<string, string> = {};
|
|
83
|
+
if (isRecord(value.remotes)) {
|
|
84
|
+
for (const [remote, origin] of Object.entries(value.remotes)) {
|
|
85
|
+
if (typeof origin === 'string') remotes[remote] = origin;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
envs[name] = { remotes, ...(typeof value.shell === 'string' ? { shell: value.shell } : {}) };
|
|
89
|
+
}
|
|
90
|
+
return { registry: json.registry === 'r2' ? 'r2' : 'assets', envs };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The origin (scheme and host) of an address, or an error saying which setting is wrong. https only;
|
|
95
|
+
* http is accepted for localhost outside prod, for trying a deploy on your own machine.
|
|
96
|
+
*/
|
|
97
|
+
export function checkOrigin(value: string | undefined, label: string, env = 'staging'): string {
|
|
98
|
+
if (value === undefined || value === '') {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${label} is empty. Set it in deploy/cloudflare.json to the address people will use.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
// The URL parser drops tabs and newlines, so a value with one would pass as something else.
|
|
104
|
+
if (
|
|
105
|
+
/\s/.test(value) ||
|
|
106
|
+
Array.from(value, (c) => c.charCodeAt(0)).some((n) => n < 32 || n === 127)
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(`${label} must not contain spaces or control characters`);
|
|
109
|
+
}
|
|
110
|
+
const url = URL.canParse(value) ? new URL(value) : undefined;
|
|
111
|
+
const local = url?.protocol === 'http:' && url.hostname === 'localhost' && env !== 'prod';
|
|
112
|
+
if (url === undefined || (url.protocol !== 'https:' && !local)) {
|
|
113
|
+
throw new Error(`${label} must be an https address, got "${value}"`);
|
|
114
|
+
}
|
|
115
|
+
if (url.username !== '' || url.password !== '') {
|
|
116
|
+
throw new Error(`${label} must not contain a user name or password`);
|
|
117
|
+
}
|
|
118
|
+
if (/(^|\.)example\.(com|org|net)$/.test(url.hostname)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`${label} still holds the placeholder "${value}". Put your real address in deploy/cloudflare.json.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
|
|
124
|
+
throw new Error(`${label} must be an origin without a path, got "${value}"`);
|
|
125
|
+
}
|
|
126
|
+
return url.origin;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface IndexEntry {
|
|
130
|
+
name: string;
|
|
131
|
+
version: string;
|
|
132
|
+
entry: string;
|
|
133
|
+
routes: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildIndex(env: string, remotes: IndexEntry[]): string {
|
|
137
|
+
return `${JSON.stringify({ env, remotes }, null, 2)}\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function selectApps(project: Project, config: Config, requested: string[]): string[] {
|
|
141
|
+
const deployable =
|
|
142
|
+
config.registry === 'r2' ? [project.shell] : [project.shell, ...project.remotes];
|
|
143
|
+
if (requested.length === 0) return deployable;
|
|
144
|
+
for (const app of requested) {
|
|
145
|
+
if (!deployable.includes(app))
|
|
146
|
+
throw new Error(`"${app}" is not an app this project deploys (${deployable.join(', ')})`);
|
|
147
|
+
}
|
|
148
|
+
return requested;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function packageVersion(root: string, project: Project, app: string): string {
|
|
152
|
+
const dir = project.packageDirs[app] ?? `apps/${app}`;
|
|
153
|
+
const json = readJson(path.join(root, dir, 'package.json'));
|
|
154
|
+
return isRecord(json) && typeof json.version === 'string' ? json.version : '0.0.0';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function stageDir(root: string, project: Project, app: string, env: string): string {
|
|
158
|
+
return path.join(root, project.packageDirs[app] ?? `apps/${app}`, STAGE, env);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Copies the app's dist to its staging folder and applies this environment's settings. Returns the folder. */
|
|
162
|
+
export function prepareApp(root: string, app: string, env: string): string {
|
|
163
|
+
const project = readProject(root);
|
|
164
|
+
const config = readConfig(root);
|
|
165
|
+
const envConfig = config.envs[env];
|
|
166
|
+
if (envConfig === undefined)
|
|
167
|
+
throw new Error(`deploy/cloudflare.json has no "${env}" environment`);
|
|
168
|
+
const dist = path.join(root, project.packageDirs[app] ?? `apps/${app}`, 'dist');
|
|
169
|
+
if (!fs.existsSync(dist)) throw new Error(`${dist} does not exist. Run the build first.`);
|
|
170
|
+
|
|
171
|
+
const out = stageDir(root, project, app, env);
|
|
172
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
173
|
+
fs.cpSync(dist, out, { recursive: true });
|
|
174
|
+
|
|
175
|
+
if (app === project.shell) prepareShell(root, project, config, envConfig, env, out);
|
|
176
|
+
else prepareRemote(app, envConfig, env, out);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function prepareShell(
|
|
181
|
+
root: string,
|
|
182
|
+
project: Project,
|
|
183
|
+
config: Config,
|
|
184
|
+
envConfig: EnvConfig,
|
|
185
|
+
env: string,
|
|
186
|
+
out: string,
|
|
187
|
+
): void {
|
|
188
|
+
checkOrigin(envConfig.shell, `envs.${env}.shell`, env);
|
|
189
|
+
const configDir = path.join(out, 'config');
|
|
190
|
+
const chosen = path.join(configDir, `runtime-config.${env}.json`);
|
|
191
|
+
if (!fs.existsSync(chosen)) throw new Error(`The build has no config/runtime-config.${env}.json`);
|
|
192
|
+
const runtime = readJson(chosen);
|
|
193
|
+
const values: Record<string, unknown> = isRecord(runtime) ? runtime : {};
|
|
194
|
+
|
|
195
|
+
const origins = new Map<string, string>();
|
|
196
|
+
if (config.registry === 'assets') {
|
|
197
|
+
for (const remote of project.remotes) {
|
|
198
|
+
origins.set(
|
|
199
|
+
remote,
|
|
200
|
+
checkOrigin(envConfig.remotes?.[remote], `envs.${env}.remotes.${remote}`, env),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const known = Array.isArray(values.registryOrigins)
|
|
204
|
+
? values.registryOrigins.filter((o) => typeof o === 'string')
|
|
205
|
+
: [];
|
|
206
|
+
values.registryOrigins = [...new Set([...known, ...origins.values()])];
|
|
207
|
+
const entries = project.remotes.map((remote) => ({
|
|
208
|
+
name: remote,
|
|
209
|
+
version: packageVersion(root, project, remote),
|
|
210
|
+
entry: `${origins.get(remote) ?? ''}/mf-manifest.json`,
|
|
211
|
+
routes: [`/${remote}`],
|
|
212
|
+
}));
|
|
213
|
+
fs.mkdirSync(path.join(out, 'registry'), { recursive: true });
|
|
214
|
+
fs.writeFileSync(path.join(out, 'registry/index.json'), buildIndex(env, entries));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const file of fs.readdirSync(configDir)) {
|
|
218
|
+
if (/^runtime-config\..+\.json$/.test(file)) fs.rmSync(path.join(configDir, file));
|
|
219
|
+
}
|
|
220
|
+
fs.writeFileSync(
|
|
221
|
+
path.join(configDir, 'runtime-config.json'),
|
|
222
|
+
`${JSON.stringify(values, null, 2)}\n`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function prepareRemote(app: string, envConfig: EnvConfig, env: string, out: string): void {
|
|
227
|
+
const shellOrigin = checkOrigin(envConfig.shell, `envs.${env}.shell`, env);
|
|
228
|
+
if (!fs.existsSync(path.join(out, 'mf-manifest.json')))
|
|
229
|
+
throw new Error(`${out} has no mf-manifest.json. Is this a remote build?`);
|
|
230
|
+
const headers = path.join(out, '_headers');
|
|
231
|
+
if (!fs.existsSync(headers)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`${out} has no _headers, so the shell could not load ${app} across origins. Keep apps/${app}/public/_headers and rebuild.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
fs.writeFileSync(
|
|
237
|
+
headers,
|
|
238
|
+
fs.readFileSync(headers, 'utf8').replaceAll(SHELL_ORIGIN_TOKEN, () => shellOrigin),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface Target {
|
|
243
|
+
app: string;
|
|
244
|
+
env: string;
|
|
245
|
+
/** The app's wrangler config, relative to the project root. */
|
|
246
|
+
config: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The CI matrix: one entry per app and environment. */
|
|
250
|
+
export function plan(root: string, envs: string[]): Target[] {
|
|
251
|
+
for (const env of envs) {
|
|
252
|
+
if (!ENVS.includes(env)) throw new Error(`unknown environment "${env}" (${ENVS.join(', ')})`);
|
|
253
|
+
}
|
|
254
|
+
const project = readProject(root);
|
|
255
|
+
const apps = selectApps(project, readConfig(root), []);
|
|
256
|
+
return envs.flatMap((env) =>
|
|
257
|
+
apps.map((app) => ({
|
|
258
|
+
app,
|
|
259
|
+
env,
|
|
260
|
+
config: `${project.packageDirs[app] ?? `apps/${app}`}/wrangler.jsonc`,
|
|
261
|
+
})),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function wranglerArgs(app: string, env: string, project: Project): string[] {
|
|
266
|
+
const dir = project.packageDirs[app] ?? `apps/${app}`;
|
|
267
|
+
return ['deploy', '--config', `${dir}/wrangler.jsonc`, '--env', env];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Wrangler's own script, so it can run under node without a shell, which works on every OS. */
|
|
271
|
+
function wranglerScript(root: string): string {
|
|
272
|
+
const manifest = path.join(root, 'node_modules/wrangler/package.json');
|
|
273
|
+
const json = readJson(manifest);
|
|
274
|
+
const bin = isRecord(json) && isRecord(json.bin) ? json.bin.wrangler : undefined;
|
|
275
|
+
if (typeof bin !== 'string')
|
|
276
|
+
throw new Error(`${manifest} names no wrangler script. Run pnpm install.`);
|
|
277
|
+
return path.join(path.dirname(manifest), bin);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function main(argv: string[], root = process.cwd()): number {
|
|
281
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
|
282
|
+
const [first, ...rest] = argv.filter((a) => !a.startsWith('--'));
|
|
283
|
+
try {
|
|
284
|
+
if (first === 'plan') {
|
|
285
|
+
console.log(JSON.stringify(plan(root, (rest[0] ?? '').split(',').filter(Boolean))));
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (first === undefined || !ENVS.includes(first)) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`usage: deploy-cloudflare <${ENVS.join('|')}> [app...] [--prepare-only] [--dry-run]`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const project = readProject(root);
|
|
294
|
+
for (const app of selectApps(project, readConfig(root), rest)) {
|
|
295
|
+
const out = prepareApp(root, app, first);
|
|
296
|
+
console.log(`staged ${app} for ${first}: ${path.relative(root, out)}`);
|
|
297
|
+
if (flags.has('--prepare-only')) continue;
|
|
298
|
+
const args = wranglerArgs(app, first, project);
|
|
299
|
+
if (flags.has('--dry-run')) {
|
|
300
|
+
console.log(`wrangler ${args.join(' ')}`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const run = spawnSync(process.execPath, [wranglerScript(root), ...args], {
|
|
304
|
+
cwd: root,
|
|
305
|
+
stdio: 'inherit',
|
|
306
|
+
});
|
|
307
|
+
if (run.status !== 0) throw new Error(`wrangler deploy failed for ${app}`);
|
|
308
|
+
}
|
|
309
|
+
return 0;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(error instanceof Error ? error.message : error);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (process.argv[1] !== undefined && path.resolve(process.argv[1]) === import.meta.filename) {
|
|
317
|
+
process.exitCode = main(process.argv.slice(2));
|
|
318
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
name: deploy-cloudflare
|
|
2
|
+
|
|
3
|
+
# Builds once, then deploys every app to the Cloudflare Workers named in deploy/cloudflare.json. A push to main
|
|
4
|
+
# deploys staging. Run it by hand to choose staging or prod. Needs the secrets CLOUDFLARE_API_TOKEN and
|
|
5
|
+
# CLOUDFLARE_ACCOUNT_ID in the GitHub environments cloudflare-staging and cloudflare-prod (see docs/cloudflare.md).
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
env:
|
|
12
|
+
description: Environment to deploy to
|
|
13
|
+
type: choice
|
|
14
|
+
options: [staging, prod]
|
|
15
|
+
default: staging
|
|
16
|
+
|
|
17
|
+
permissions: {}
|
|
18
|
+
|
|
19
|
+
# Two deploys at once would race on the same Workers; let the first finish.
|
|
20
|
+
concurrency:
|
|
21
|
+
group: deploy-cloudflare
|
|
22
|
+
cancel-in-progress: false
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
plan:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 5
|
|
28
|
+
# A manual run from another branch would otherwise deploy that branch.
|
|
29
|
+
if: github.ref == 'refs/heads/main'
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
outputs:
|
|
33
|
+
targets: ${{ steps.plan.outputs.targets }}
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
|
+
with:
|
|
37
|
+
persist-credentials: false
|
|
38
|
+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
39
|
+
with:
|
|
40
|
+
node-version: lts/*
|
|
41
|
+
- name: List the apps and environments
|
|
42
|
+
id: plan
|
|
43
|
+
env:
|
|
44
|
+
DEPLOY_ENV: ${{ inputs.env || 'staging' }}
|
|
45
|
+
run: |
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
targets=$(node --experimental-strip-types scripts/deploy-cloudflare.ts plan "$DEPLOY_ENV")
|
|
48
|
+
echo "targets=$targets" >> "$GITHUB_OUTPUT"
|
|
49
|
+
|
|
50
|
+
build:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
timeout-minutes: 20
|
|
53
|
+
if: github.ref == 'refs/heads/main'
|
|
54
|
+
permissions:
|
|
55
|
+
contents: read
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
58
|
+
with:
|
|
59
|
+
persist-credentials: false
|
|
60
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
61
|
+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
62
|
+
with:
|
|
63
|
+
node-version: lts/*
|
|
64
|
+
cache: pnpm
|
|
65
|
+
- run: pnpm install --frozen-lockfile
|
|
66
|
+
- run: pnpm build
|
|
67
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
68
|
+
with:
|
|
69
|
+
name: dist
|
|
70
|
+
path: apps/*/dist
|
|
71
|
+
if-no-files-found: error
|
|
72
|
+
|
|
73
|
+
deploy:
|
|
74
|
+
needs: [plan, build]
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
timeout-minutes: 15
|
|
77
|
+
environment: cloudflare-${{ matrix.target.env }}
|
|
78
|
+
permissions:
|
|
79
|
+
contents: read
|
|
80
|
+
strategy:
|
|
81
|
+
fail-fast: false
|
|
82
|
+
matrix:
|
|
83
|
+
target: ${{ fromJSON(needs.plan.outputs.targets) }}
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
86
|
+
with:
|
|
87
|
+
persist-credentials: false
|
|
88
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
89
|
+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
90
|
+
with:
|
|
91
|
+
node-version: lts/*
|
|
92
|
+
cache: pnpm
|
|
93
|
+
- run: pnpm install --frozen-lockfile
|
|
94
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
95
|
+
with:
|
|
96
|
+
name: dist
|
|
97
|
+
path: apps
|
|
98
|
+
- name: Stage the build for this environment
|
|
99
|
+
env:
|
|
100
|
+
DEPLOY_ENV: ${{ matrix.target.env }}
|
|
101
|
+
DEPLOY_APP: ${{ matrix.target.app }}
|
|
102
|
+
run: |
|
|
103
|
+
set -euo pipefail
|
|
104
|
+
node --experimental-strip-types scripts/deploy-cloudflare.ts "$DEPLOY_ENV" "$DEPLOY_APP" --prepare-only
|
|
105
|
+
- name: Deploy
|
|
106
|
+
uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3
|
|
107
|
+
with:
|
|
108
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
109
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
110
|
+
packageManager: pnpm
|
|
111
|
+
command: deploy --config ${{ matrix.target.config }} --env ${{ matrix.target.env }}
|