@podosoft/podokit 0.16.3 → 0.17.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/dist/deploy-compose-exec.d.ts +93 -0
- package/dist/deploy-compose-exec.js +198 -0
- package/dist/deploy-compose-profile.d.ts +107 -0
- package/dist/deploy-compose-profile.js +361 -0
- package/dist/deploy-compose-render.d.ts +48 -0
- package/dist/deploy-compose-render.js +333 -0
- package/dist/deploy-compose-sync.d.ts +144 -0
- package/dist/deploy-compose-sync.js +412 -0
- package/dist/deploy-compose.d.ts +98 -0
- package/dist/deploy-compose.js +667 -0
- package/dist/deploy-dispatch.d.ts +33 -0
- package/dist/deploy-dispatch.js +58 -0
- package/dist/deploy-driver.d.ts +25 -0
- package/dist/deploy-driver.js +78 -0
- package/dist/deploy-profile.d.ts +4 -7
- package/dist/deploy-profile.js +86 -274
- package/dist/deploy-render.js +10 -1
- package/dist/deploy-schema.d.ts +56 -0
- package/dist/deploy-schema.js +261 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +178 -6
- package/dist/lib.d.ts +9 -0
- package/dist/lib.js +30 -1
- package/dist/templates/fullstack-nest-svelte/AGENTS.md +2 -1
- package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +34 -3
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +68 -7
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +60 -8
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +2 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/server.js +31 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/api.ts +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +24 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.js +267 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/[...path]/+server.ts +6 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/auth/[...all]/+server.ts +6 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +17 -7
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +50 -1
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/release.md +64 -0
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
- package/dist/templates/fullstack-nest-svelte/dot-dockerignore +7 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +7 -0
- package/dist/templates/fullstack-nest-svelte/dot-github/workflows/release.yml +142 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/README.md +18 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +6 -1
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +8 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/web-deployment.yaml +11 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.test.ts +62 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +3 -2
- package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +2 -1
- package/dist/templates/todo/AGENTS.md +2 -1
- package/dist/templates/todo/Dockerfile.dev +34 -3
- package/dist/templates/todo/apps/api/Dockerfile +68 -7
- package/dist/templates/todo/apps/web/Dockerfile +60 -8
- package/dist/templates/todo/apps/web/package.json +2 -1
- package/dist/templates/todo/apps/web/server.js +31 -0
- package/dist/templates/todo/apps/web/src/lib/server/api.ts +2 -2
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +24 -1
- package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.js +267 -0
- package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
- package/dist/templates/todo/apps/web/src/routes/api/[...path]/+server.ts +6 -2
- package/dist/templates/todo/apps/web/src/routes/api/auth/[...all]/+server.ts +6 -2
- package/dist/templates/todo/apps/web/vite.config.ts +17 -7
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +47 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/release.md +64 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
- package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
- package/dist/templates/todo/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
- package/dist/templates/todo/dot-dockerignore +7 -0
- package/dist/templates/todo/dot-env.example +7 -0
- package/dist/templates/todo/dot-github/workflows/release.yml +142 -0
- package/dist/templates/todo/infra/k3s/README.md +18 -0
- package/dist/templates/todo/infra/k3s/api-deployment.yaml +6 -1
- package/dist/templates/todo/infra/k3s/ingress.yaml +8 -0
- package/dist/templates/todo/infra/k3s/web-deployment.yaml +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: podokit-deploy-fast
|
|
3
|
+
description: Push locally built artifacts into a running Docker Compose deployment and restart it, without building or publishing an image. Use when iterating on code against a deployed environment and a full release round trip is too slow. Not for releases, migrations, dependency changes, or anything going to real users.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sync code into a running deployment
|
|
7
|
+
|
|
8
|
+
`podo deploy sync` copies this project's build output into the containers that are
|
|
9
|
+
already running and restarts them. `docker-compose` driver only — read the profile's
|
|
10
|
+
`driver` first and stop if it is `kubernetes-helm`, where the equivalent would have
|
|
11
|
+
to reach every node and would stop being a shortcut.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @podosoft/podokit deploy sync --profile <name> --build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Say what this does before doing it
|
|
18
|
+
|
|
19
|
+
It is not a release, and the user has to understand three consequences before you run
|
|
20
|
+
it:
|
|
21
|
+
|
|
22
|
+
1. **The deployment will run code its image tag does not describe.** A marker file
|
|
23
|
+
records the sync and `podo deploy status` reports it under `syncDrift`.
|
|
24
|
+
2. **Every connection the restarted containers hold is dropped** — WebSocket sessions,
|
|
25
|
+
in-flight requests, anything attached.
|
|
26
|
+
3. **It disappears on the next release.** Container writable layers do not survive
|
|
27
|
+
recreation, so `podo deploy apply` restores the image with no cleanup step. That is
|
|
28
|
+
the property that makes this safe; do not build anything that depends on the copy
|
|
29
|
+
persisting.
|
|
30
|
+
|
|
31
|
+
## When NOT to use it
|
|
32
|
+
|
|
33
|
+
Use a release instead — build the images and `podo deploy apply` — when any of these
|
|
34
|
+
is true. The first three are refusals the command makes itself; the last two it
|
|
35
|
+
cannot detect, which is why they are your job.
|
|
36
|
+
|
|
37
|
+
| Situation | Why |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| runtime dependencies changed | the container's `node_modules` was installed `--omit=dev` from the old manifest, so the new code imports something that is not there |
|
|
40
|
+
| a release holds the deployment lock | an apply is in progress |
|
|
41
|
+
| nothing is running for that project | there is no container to copy into |
|
|
42
|
+
| **the change needs a migration** | sync never migrates, and a migration belongs with the release whose code needs it |
|
|
43
|
+
| **the change is going to real users** | nothing is pushed, so nothing is reproducible from it |
|
|
44
|
+
|
|
45
|
+
## Procedure
|
|
46
|
+
|
|
47
|
+
1. Read the profile's `driver`. Stop unless it is `docker-compose`.
|
|
48
|
+
2. Tell the user what will restart and that connections drop. Get agreement if anyone
|
|
49
|
+
is using the deployment.
|
|
50
|
+
3. Run with `--build` unless the project is already built. `--build` compiles in the
|
|
51
|
+
order the images do — every `packages/*` before the apps — because a root build
|
|
52
|
+
script runs workspaces in manifest order and an app listed first would compile
|
|
53
|
+
against a workspace package's previous output.
|
|
54
|
+
4. Report what was copied, which containers restarted, and that the deployment is now
|
|
55
|
+
in sync drift.
|
|
56
|
+
5. Verify against the running deployment, not against the build: request a path the
|
|
57
|
+
change affects. If part of the build output is excluded (below), check one of
|
|
58
|
+
those paths too — that is the failure this exclusion exists to prevent, and it
|
|
59
|
+
does not make the page look broken.
|
|
60
|
+
|
|
61
|
+
## Restoring the image
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @podosoft/podokit deploy sync --profile <name> --revert
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This recreates the containers from the Compose project **already on the target**,
|
|
68
|
+
which is the description of what was actually applied — not a fresh render, which
|
|
69
|
+
would reproduce the profile as it is now.
|
|
70
|
+
|
|
71
|
+
A release does the same thing as a side effect, so `--revert` is for abandoning a
|
|
72
|
+
sync, not a required cleanup step.
|
|
73
|
+
|
|
74
|
+
## Excluding part of the build output
|
|
75
|
+
|
|
76
|
+
Some of a build output can come from a toolchain the developer's machine does not
|
|
77
|
+
have. Overwriting that part replaces real artifacts with an index of artifacts that
|
|
78
|
+
are no longer there, and the application keeps serving pages while the missing files
|
|
79
|
+
404 — so nothing looks broken. Those paths belong in the profile:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "sync": { "exclude": ["apps/web/build/static-assets/generated"] } }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If you find yourself about to sync over something this machine cannot rebuild, add it
|
|
86
|
+
here rather than remembering not to. `--clean` refuses while an exclude sits beneath
|
|
87
|
+
a synced path, for the same reason.
|
|
88
|
+
|
|
89
|
+
## Boundary
|
|
90
|
+
|
|
91
|
+
Never use this to ship. Never run migrations alongside it. Never create or rotate
|
|
92
|
+
credentials, and never print a secret value.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: podokit-deploy
|
|
3
|
-
description: Plan, inspect, apply, verify, and roll back PodoKit
|
|
3
|
+
description: Plan, inspect, apply, verify, and roll back PodoKit releases with an explicit profile and confirmation hash, on Kubernetes or Docker Compose. Use for release, rollout, and rollback work in a generated PodoKit project.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# PodoKit deployment
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: podokit-deploy-fast
|
|
3
|
+
description: Push locally built artifacts into a running Docker Compose deployment and restart it, without building or publishing an image. Use when iterating on code against a deployed environment and a full release round trip is too slow. Not for releases, migrations, dependency changes, or anything going to real users.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PodoKit fast deployment sync
|
|
7
|
+
|
|
8
|
+
Read and follow the canonical skill at `../../../.agents/skills/podokit-deploy-fast/SKILL.md`.
|
|
@@ -6,3 +6,10 @@ dist
|
|
|
6
6
|
**/.svelte-kit
|
|
7
7
|
.git
|
|
8
8
|
*.log
|
|
9
|
+
|
|
10
|
+
# Real environment files never enter a build context. The build stages copy the
|
|
11
|
+
# whole repository, so an .env left here is baked into an image layer even when the
|
|
12
|
+
# runtime stage does not copy it -- and getting-started tells you to create one.
|
|
13
|
+
.env
|
|
14
|
+
.env.*
|
|
15
|
+
!.env.example
|
|
@@ -12,6 +12,13 @@ POSTGRES_DB=podokit
|
|
|
12
12
|
|
|
13
13
|
# Web -> API boundary (server-side only; not exposed to the browser)
|
|
14
14
|
BACKEND_INTERNAL_URL=http://localhost:5002
|
|
15
|
+
|
|
16
|
+
# Exact paths the web server may proxy as WebSocket upgrades to the API, comma
|
|
17
|
+
# separated. Empty means none: a SvelteKit route cannot answer an upgrade, so a
|
|
18
|
+
# WebSocket gateway on the API is unreachable until it is named here. Each path is
|
|
19
|
+
# matched whole and must be authorised by the API at handshake time -- a relay that
|
|
20
|
+
# forwards anything is an open tunnel to every private route.
|
|
21
|
+
WS_PROXY_PATHS=
|
|
15
22
|
CORS_ORIGIN=http://localhost:5001
|
|
16
23
|
# adapter-node request limit; leave room for a 2 MiB multipart upload
|
|
17
24
|
BODY_SIZE_LIMIT=3M
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Build and publish the release images for a tag.
|
|
2
|
+
#
|
|
3
|
+
# `podo deploy` consumes images; it does not build them. This is the other half:
|
|
4
|
+
# tagging a commit produces the two images that `podo deploy apply` then rolls out.
|
|
5
|
+
# It stops at the push, because rolling out requires confirming a plan hash and that
|
|
6
|
+
# approval belongs to a person.
|
|
7
|
+
#
|
|
8
|
+
# ── Which runner, and what it costs ──────────────────────────────────────────────
|
|
9
|
+
#
|
|
10
|
+
# GitHub-hosted runners are free for PUBLIC repositories only. In a PRIVATE
|
|
11
|
+
# repository every minute is metered against the account's included Actions minutes
|
|
12
|
+
# and billed beyond them -- and image builds are the most expensive job most
|
|
13
|
+
# projects run. A self-hosted runner consumes no Actions minutes at all, whatever
|
|
14
|
+
# the repository's visibility.
|
|
15
|
+
#
|
|
16
|
+
# So the runner is a variable rather than a constant:
|
|
17
|
+
#
|
|
18
|
+
# PODOKIT_RUNNER repository or organization variable, a JSON array of labels.
|
|
19
|
+
# Unset -> ["ubuntu-latest"], which is right for a public
|
|
20
|
+
# repository and costs money in a private one.
|
|
21
|
+
# Self-hosted example: ["self-hosted","Linux","X64"]
|
|
22
|
+
#
|
|
23
|
+
# Set it before the first tag if this repository is private. Nothing here warns you
|
|
24
|
+
# when you have not: the workflow succeeds either way, and the difference shows up
|
|
25
|
+
# on a bill.
|
|
26
|
+
#
|
|
27
|
+
# ── The other two variables ──────────────────────────────────────────────────────
|
|
28
|
+
#
|
|
29
|
+
# PODOKIT_DEPLOY_PROFILE which .podokit/deploy/<name>.json names the image
|
|
30
|
+
# repositories. Unset -> production.
|
|
31
|
+
# PODOKIT_IMAGE_PLATFORM the architecture the deployment target runs.
|
|
32
|
+
# Unset -> linux/amd64. Building for the wrong one
|
|
33
|
+
# produces "exec format error" at rollout, after the
|
|
34
|
+
# migration has already run.
|
|
35
|
+
#
|
|
36
|
+
# ── Secrets ──────────────────────────────────────────────────────────────────────
|
|
37
|
+
#
|
|
38
|
+
# REGISTRY_USERNAME / REGISTRY_PASSWORD credentials for the registry named by
|
|
39
|
+
# the profile. A robot or deploy account is enough, but note that some registries
|
|
40
|
+
# refuse to let a robot CREATE a repository -- push the first tag of a new
|
|
41
|
+
# repository with an account that can, or create it in the registry first.
|
|
42
|
+
|
|
43
|
+
name: release
|
|
44
|
+
|
|
45
|
+
on:
|
|
46
|
+
push:
|
|
47
|
+
tags:
|
|
48
|
+
- "v*.*.*"
|
|
49
|
+
|
|
50
|
+
concurrency:
|
|
51
|
+
group: release-${{ github.ref }}
|
|
52
|
+
cancel-in-progress: false
|
|
53
|
+
|
|
54
|
+
jobs:
|
|
55
|
+
images:
|
|
56
|
+
# One job builds both images rather than a matrix of two. A self-hosted pool is
|
|
57
|
+
# usually small, and fanning out here takes runners away from every other
|
|
58
|
+
# repository that shares it for no wall-clock gain on a multi-core machine.
|
|
59
|
+
runs-on: ${{ fromJSON(vars.PODOKIT_RUNNER || '["ubuntu-latest"]') }}
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v5
|
|
62
|
+
|
|
63
|
+
- uses: actions/setup-node@v6
|
|
64
|
+
with:
|
|
65
|
+
node-version: 22
|
|
66
|
+
cache: npm
|
|
67
|
+
|
|
68
|
+
- run: npm ci --no-audit --no-fund
|
|
69
|
+
|
|
70
|
+
# Verify before publishing. An image built from a commit that never passed is
|
|
71
|
+
# indistinguishable from one that did, once it is in the registry.
|
|
72
|
+
- run: npm run lint
|
|
73
|
+
- run: npm test
|
|
74
|
+
|
|
75
|
+
- name: Read the release target from the deployment profile
|
|
76
|
+
id: profile
|
|
77
|
+
run: |
|
|
78
|
+
node --input-type=module -e '
|
|
79
|
+
import { readFileSync } from "node:fs";
|
|
80
|
+
const name = process.env.PROFILE || "production";
|
|
81
|
+
const path = `.podokit/deploy/${name}.json`;
|
|
82
|
+
let profile;
|
|
83
|
+
try {
|
|
84
|
+
profile = JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error(`No deployment profile at ${path}. Run podo deploy init first.`);
|
|
87
|
+
}
|
|
88
|
+
const { apiRepository, webRepository } = profile.release ?? {};
|
|
89
|
+
if (!apiRepository || !webRepository) {
|
|
90
|
+
throw new Error(`${path} does not declare release.apiRepository and release.webRepository.`);
|
|
91
|
+
}
|
|
92
|
+
// Everything before the first slash is a registry host only when it
|
|
93
|
+
// looks like one; otherwise the reference is a Docker Hub short name.
|
|
94
|
+
const first = apiRepository.split("/")[0];
|
|
95
|
+
const registry = /[.:]/.test(first) ? first : "";
|
|
96
|
+
const out = [
|
|
97
|
+
`api=${apiRepository}`,
|
|
98
|
+
`web=${webRepository}`,
|
|
99
|
+
`registry=${registry}`,
|
|
100
|
+
`tag=${process.env.GITHUB_REF_NAME}`,
|
|
101
|
+
].join("\n");
|
|
102
|
+
process.stdout.write(out + "\n");
|
|
103
|
+
' >> "$GITHUB_OUTPUT"
|
|
104
|
+
env:
|
|
105
|
+
PROFILE: ${{ vars.PODOKIT_DEPLOY_PROFILE }}
|
|
106
|
+
|
|
107
|
+
- uses: docker/setup-buildx-action@v3
|
|
108
|
+
|
|
109
|
+
- uses: docker/login-action@v3
|
|
110
|
+
with:
|
|
111
|
+
registry: ${{ steps.profile.outputs.registry }}
|
|
112
|
+
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
113
|
+
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
114
|
+
|
|
115
|
+
- name: Build and push the API image
|
|
116
|
+
uses: docker/build-push-action@v6
|
|
117
|
+
with:
|
|
118
|
+
context: .
|
|
119
|
+
file: apps/api/Dockerfile
|
|
120
|
+
platforms: ${{ vars.PODOKIT_IMAGE_PLATFORM || 'linux/amd64' }}
|
|
121
|
+
push: true
|
|
122
|
+
tags: ${{ steps.profile.outputs.api }}:${{ steps.profile.outputs.tag }}
|
|
123
|
+
cache-from: type=gha,scope=api
|
|
124
|
+
cache-to: type=gha,scope=api,mode=max
|
|
125
|
+
|
|
126
|
+
- name: Build and push the web image
|
|
127
|
+
uses: docker/build-push-action@v6
|
|
128
|
+
with:
|
|
129
|
+
context: .
|
|
130
|
+
file: apps/web/Dockerfile
|
|
131
|
+
platforms: ${{ vars.PODOKIT_IMAGE_PLATFORM || 'linux/amd64' }}
|
|
132
|
+
push: true
|
|
133
|
+
tags: ${{ steps.profile.outputs.web }}:${{ steps.profile.outputs.tag }}
|
|
134
|
+
cache-from: type=gha,scope=web
|
|
135
|
+
cache-to: type=gha,scope=web,mode=max
|
|
136
|
+
|
|
137
|
+
- name: Report what to deploy
|
|
138
|
+
run: |
|
|
139
|
+
echo "Pushed ${{ steps.profile.outputs.api }}:${{ steps.profile.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
|
|
140
|
+
echo "Pushed ${{ steps.profile.outputs.web }}:${{ steps.profile.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
|
|
141
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
142
|
+
echo 'Roll out with `podo deploy plan --profile <name> --release ${{ steps.profile.outputs.tag }}`, then apply the printed hash.' >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Example k3s manifests
|
|
2
|
+
|
|
3
|
+
These are **illustrative**. `podo deploy` does not read or apply them.
|
|
4
|
+
|
|
5
|
+
For a real deployment, initialize a profile and let PodoKit render the manifests it
|
|
6
|
+
will actually apply — with image digests, probes, disruption budgets, a migration
|
|
7
|
+
Job, and a confirmation hash:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
podo deploy init --profile production --context production --host app.example.com
|
|
11
|
+
podo deploy render --profile production --release v1.2.3
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Keep these files if you want a starting point for hand-managed manifests, or delete
|
|
15
|
+
the directory if you deploy with `podo deploy`. If you do apply them by hand, replace
|
|
16
|
+
every `example.com` and `ghcr.io/example/...` placeholder first, and note that the
|
|
17
|
+
image tags below are placeholders too: the deployment tooling rejects `latest`,
|
|
18
|
+
branch tags, and empty tags, and so should you.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# Example only. See README.md in this directory; `podo deploy` renders its own.
|
|
1
2
|
apiVersion: apps/v1
|
|
2
3
|
kind: Deployment
|
|
3
4
|
metadata:
|
|
@@ -13,7 +14,8 @@ spec:
|
|
|
13
14
|
spec:
|
|
14
15
|
containers:
|
|
15
16
|
- name: api
|
|
16
|
-
|
|
17
|
+
# Placeholder. Use one immutable, stable SemVer tag for api and web.
|
|
18
|
+
image: ghcr.io/example/podokit-api:v0.0.0
|
|
17
19
|
ports:
|
|
18
20
|
- containerPort: 3000
|
|
19
21
|
envFrom:
|
|
@@ -22,3 +24,6 @@ spec:
|
|
|
22
24
|
readinessProbe:
|
|
23
25
|
httpGet: { path: /health/ready, port: 3000 }
|
|
24
26
|
initialDelaySeconds: 5
|
|
27
|
+
livenessProbe:
|
|
28
|
+
httpGet: { path: /health, port: 3000 }
|
|
29
|
+
initialDelaySeconds: 15
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# Example only. See README.md in this directory; `podo deploy` renders its own.
|
|
1
2
|
apiVersion: traefik.io/v1alpha1
|
|
2
3
|
kind: Middleware
|
|
3
4
|
metadata:
|
|
@@ -14,6 +15,13 @@ metadata:
|
|
|
14
15
|
annotations:
|
|
15
16
|
traefik.ingress.kubernetes.io/router.middlewares: podokit-compression@kubernetescrd
|
|
16
17
|
spec:
|
|
18
|
+
# Name the controller explicitly rather than relying on a default IngressClass.
|
|
19
|
+
ingressClassName: traefik
|
|
20
|
+
# app-config sets an https:// origin, so the route has to terminate TLS. Create the
|
|
21
|
+
# Secret out of band (cert-manager, or an existing certificate) and name it here.
|
|
22
|
+
tls:
|
|
23
|
+
- hosts: [example.com]
|
|
24
|
+
secretName: app-tls
|
|
17
25
|
rules:
|
|
18
26
|
- host: example.com
|
|
19
27
|
http:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# Example only. See README.md in this directory; `podo deploy` renders its own.
|
|
1
2
|
apiVersion: apps/v1
|
|
2
3
|
kind: Deployment
|
|
3
4
|
metadata:
|
|
@@ -13,8 +14,17 @@ spec:
|
|
|
13
14
|
spec:
|
|
14
15
|
containers:
|
|
15
16
|
- name: web
|
|
16
|
-
|
|
17
|
+
# Placeholder. Use one immutable, stable SemVer tag for api and web.
|
|
18
|
+
image: ghcr.io/example/podokit-web:v0.0.0
|
|
17
19
|
ports:
|
|
18
20
|
- containerPort: 3000
|
|
19
21
|
envFrom:
|
|
20
22
|
- configMapRef: { name: app-config }
|
|
23
|
+
# Without these a rolling update sends traffic to a process that has not
|
|
24
|
+
# bound its port yet.
|
|
25
|
+
readinessProbe:
|
|
26
|
+
httpGet: { path: /, port: 3000 }
|
|
27
|
+
initialDelaySeconds: 5
|
|
28
|
+
livenessProbe:
|
|
29
|
+
httpGet: { path: /, port: 3000 }
|
|
30
|
+
initialDelaySeconds: 15
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as Table from "$lib/components/ui/table";
|
|
10
10
|
import * as Dialog from "$lib/components/ui/dialog";
|
|
11
11
|
import DataTable, { type DataTableColumn, type SortState, DEFAULT_PAGE_SIZE } from "$lib/components/data-table.svelte";
|
|
12
|
+
import { deviceLabel } from "$lib/device-label";
|
|
12
13
|
import { toast } from "svelte-sonner";
|
|
13
14
|
import { api } from "$lib/api";
|
|
14
15
|
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
@@ -382,7 +383,7 @@
|
|
|
382
383
|
let sessionsSort = $state<SortState | null>(null);
|
|
383
384
|
let busy = $state(false);
|
|
384
385
|
const sessionsColumns: DataTableColumn<Session>[] = [
|
|
385
|
-
{ key: "userAgent", label: i18n.t.sessions.device, sortable: true, value: (s) => s.userAgent ?? "" },
|
|
386
|
+
{ key: "userAgent", label: i18n.t.sessions.device, sortable: true, value: (s) => deviceLabel(s.userAgent) ?? s.userAgent ?? "" },
|
|
386
387
|
{ key: "ipAddress", label: i18n.t.sessions.ip, sortable: true, value: (s) => s.ipAddress ?? "" },
|
|
387
388
|
{ key: "createdAt", label: i18n.t.sessions.since, sortable: true, value: (s) => new Date(s.createdAt).getTime() },
|
|
388
389
|
{ key: "status", label: i18n.t.sessions.status },
|
|
@@ -793,7 +794,7 @@
|
|
|
793
794
|
>
|
|
794
795
|
{#snippet row(s)}
|
|
795
796
|
{@const isCurrent = s.id === data.currentSessionId}
|
|
796
|
-
<Table.Cell class="max-w-xs truncate">{s.userAgent ?? i18n.t.sessions.unknown}</Table.Cell>
|
|
797
|
+
<Table.Cell class="max-w-xs truncate" title={s.userAgent ?? ""}>{deviceLabel(s.userAgent) ?? s.userAgent ?? i18n.t.sessions.unknown}</Table.Cell>
|
|
797
798
|
<Table.Cell class="text-muted-foreground">{s.ipAddress ?? "—"}</Table.Cell>
|
|
798
799
|
<Table.Cell class="text-muted-foreground">{formatDateTime(s.createdAt)}</Table.Cell>
|
|
799
800
|
<Table.Cell>{#if isCurrent}<Badge>{i18n.t.sessions.current}</Badge>{/if}</Table.Cell>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { deviceLabel } from "./device-label";
|
|
3
|
+
|
|
4
|
+
describe("deviceLabel", () => {
|
|
5
|
+
it("names the browser and the machine", () => {
|
|
6
|
+
expect(
|
|
7
|
+
deviceLabel(
|
|
8
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
|
|
9
|
+
),
|
|
10
|
+
).toBe("Chrome · macOS");
|
|
11
|
+
expect(
|
|
12
|
+
deviceLabel(
|
|
13
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
|
|
14
|
+
),
|
|
15
|
+
).toBe("Chrome · Windows");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Each of these contains an earlier engine's token, so a naive check in the wrong
|
|
19
|
+
// order reports every one of them as Chrome or Safari.
|
|
20
|
+
it("does not mistake a derivative for the engine it embeds", () => {
|
|
21
|
+
expect(deviceLabel("Mozilla/5.0 (Windows NT 10.0) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0")).toBe(
|
|
22
|
+
"Edge · Windows",
|
|
23
|
+
);
|
|
24
|
+
expect(deviceLabel("Mozilla/5.0 (Windows NT 10.0) Chrome/141.0.0.0 Safari/537.36 OPR/121.0.0.0")).toBe(
|
|
25
|
+
"Opera · Windows",
|
|
26
|
+
);
|
|
27
|
+
expect(
|
|
28
|
+
deviceLabel("Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/141.0.0.0 SamsungBrowser/28.0 Safari/537.36"),
|
|
29
|
+
).toBe("Samsung Internet · Android");
|
|
30
|
+
expect(
|
|
31
|
+
deviceLabel("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15"),
|
|
32
|
+
).toBe("Safari · macOS");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("names an iPhone and an iPad rather than the engine's host platform", () => {
|
|
36
|
+
expect(
|
|
37
|
+
deviceLabel("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1"),
|
|
38
|
+
).toBe("Safari · iPhone");
|
|
39
|
+
// iPadOS reports Macintosh in the same string, so order decides this one.
|
|
40
|
+
expect(
|
|
41
|
+
deviceLabel("Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1"),
|
|
42
|
+
).toBe("Safari · iPad");
|
|
43
|
+
expect(deviceLabel("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) CriOS/141.0.0.0 Mobile/15E148 Safari/604.1")).toBe(
|
|
44
|
+
"Chrome · iPhone",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("gives up on something that is not a browser, so the caller can show it raw", () => {
|
|
49
|
+
// The exact value is the whole point for these: "node" is the symptom of a
|
|
50
|
+
// proxy that forgot to forward the header, and a CLI's name identifies it.
|
|
51
|
+
expect(deviceLabel("node")).toBeNull();
|
|
52
|
+
expect(deviceLabel("curl/8.7.1")).toBeNull();
|
|
53
|
+
expect(deviceLabel("")).toBeNull();
|
|
54
|
+
expect(deviceLabel(null)).toBeNull();
|
|
55
|
+
expect(deviceLabel(undefined)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("reports whichever half it can read", () => {
|
|
59
|
+
expect(deviceLabel("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")).toBe("Windows");
|
|
60
|
+
expect(deviceLabel("Firefox/142.0")).toBe("Firefox");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable device name for a session's user agent.
|
|
3
|
+
*
|
|
4
|
+
* The sessions tables label this column "device", and a raw user agent does not
|
|
5
|
+
* answer that question — truncated to fit a cell, every modern browser starts with
|
|
6
|
+
* the same "Mozilla/5.0 (…" and the part that differs is off the end. This reduces
|
|
7
|
+
* it to the two things someone scanning their own sessions is looking for: which
|
|
8
|
+
* browser, on which machine.
|
|
9
|
+
*
|
|
10
|
+
* Returns null when the string is not a browser at all — a CLI, a script, or the
|
|
11
|
+
* proxy's own default. The caller shows the raw value then, because for those the
|
|
12
|
+
* exact string is the only useful information.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Order matters: every one of these also contains an earlier engine's token. */
|
|
16
|
+
const BROWSERS: Array<[RegExp, string]> = [
|
|
17
|
+
[/\bEdg(?:e|A|iOS)?\//, "Edge"],
|
|
18
|
+
[/\bOPR\/|\bOpera\//, "Opera"],
|
|
19
|
+
[/\bSamsungBrowser\//, "Samsung Internet"],
|
|
20
|
+
[/\bFirefox\/|\bFxiOS\//, "Firefox"],
|
|
21
|
+
[/\bCriOS\//, "Chrome"],
|
|
22
|
+
[/\bChrome\/|\bChromium\//, "Chrome"],
|
|
23
|
+
[/\bSafari\//, "Safari"],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** iPadOS reports as Macintosh, so the iPad check has to come before macOS. */
|
|
27
|
+
const PLATFORMS: Array<[RegExp, string]> = [
|
|
28
|
+
[/\biPhone\b/, "iPhone"],
|
|
29
|
+
[/\biPad\b/, "iPad"],
|
|
30
|
+
[/\bAndroid\b/, "Android"],
|
|
31
|
+
[/\bCrOS\b/, "ChromeOS"],
|
|
32
|
+
[/\bWindows NT\b/, "Windows"],
|
|
33
|
+
[/\bMacintosh\b|\bMac OS X\b/, "macOS"],
|
|
34
|
+
[/\bLinux\b/, "Linux"],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function match(candidates: Array<[RegExp, string]>, value: string): string | null {
|
|
38
|
+
for (const [pattern, label] of candidates) {
|
|
39
|
+
if (pattern.test(value)) return label;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function deviceLabel(userAgent: string | null | undefined): string | null {
|
|
45
|
+
if (!userAgent) return null;
|
|
46
|
+
const browser = match(BROWSERS, userAgent);
|
|
47
|
+
const platform = match(PLATFORMS, userAgent);
|
|
48
|
+
if (browser && platform) return `${browser} · ${platform}`;
|
|
49
|
+
return browser ?? platform;
|
|
50
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { toast } from "svelte-sonner";
|
|
10
10
|
import { api } from "$lib/api";
|
|
11
11
|
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
12
|
+
import { deviceLabel } from "$lib/device-label";
|
|
12
13
|
|
|
13
14
|
const i18n = getI18n();
|
|
14
15
|
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
|
|
44
45
|
const columns = $derived<DataTableColumn<Row>[]>([
|
|
45
46
|
{ key: "user", label: i18n.t.adminSessions.user, sortable: true, value: (r) => r.userName },
|
|
46
|
-
{ key: "device", label: i18n.t.adminSessions.device, sortable: true, value: (r) => r.userAgent },
|
|
47
|
+
{ key: "device", label: i18n.t.adminSessions.device, sortable: true, value: (r) => deviceLabel(r.userAgent) ?? r.userAgent ?? "" },
|
|
47
48
|
{ key: "ip", label: i18n.t.adminSessions.ip, sortable: true, value: (r) => r.ipAddress },
|
|
48
49
|
{ key: "createdAt", label: i18n.t.adminSessions.since, sortable: true },
|
|
49
50
|
{ key: "expiresAt", label: i18n.t.adminSessions.expires, sortable: true, value: (r) => (r.expiresAt ? new Date(r.expiresAt).getTime() : 0) },
|
|
@@ -134,7 +135,7 @@
|
|
|
134
135
|
</div>
|
|
135
136
|
<div class="text-muted-foreground text-xs">{r.userEmail}</div>
|
|
136
137
|
</Table.Cell>
|
|
137
|
-
<Table.Cell class="max-w-xs truncate">{r.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
|
|
138
|
+
<Table.Cell class="max-w-xs truncate" title={r.userAgent ?? ""}>{deviceLabel(r.userAgent) ?? r.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
|
|
138
139
|
<Table.Cell class="text-muted-foreground">{r.ipAddress ?? "—"}</Table.Cell>
|
|
139
140
|
<Table.Cell class="text-muted-foreground">{formatDateTime(r.createdAt)}</Table.Cell>
|
|
140
141
|
<Table.Cell class="text-muted-foreground">{r.expiresAt ? formatDateTime(r.expiresAt) : "—"}</Table.Cell>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { api } from "$lib/api";
|
|
18
18
|
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
19
19
|
import { cn } from "$lib/utils";
|
|
20
|
+
import { deviceLabel } from "$lib/device-label";
|
|
20
21
|
import type { PageData } from "./$types";
|
|
21
22
|
|
|
22
23
|
let { data }: { data: PageData } = $props();
|
|
@@ -194,7 +195,7 @@
|
|
|
194
195
|
let mSessionsPage = $state(1);
|
|
195
196
|
let mSessionsSort = $state<SortState | null>(null);
|
|
196
197
|
const mSessionsColumns: DataTableColumn<Session>[] = [
|
|
197
|
-
{ key: "userAgent", label: i18n.t.adminSessions.device, sortable: true },
|
|
198
|
+
{ key: "userAgent", label: i18n.t.adminSessions.device, sortable: true, value: (s) => deviceLabel(s.userAgent) ?? s.userAgent ?? "" },
|
|
198
199
|
{ key: "ipAddress", label: i18n.t.adminSessions.ip, sortable: true },
|
|
199
200
|
{ key: "createdAt", label: i18n.t.adminSessions.since, sortable: true, value: (s) => new Date(s.createdAt).getTime() },
|
|
200
201
|
{ key: "actions", label: "", class: "w-10" },
|
|
@@ -557,7 +558,7 @@
|
|
|
557
558
|
empty={i18n.t.adminSessions.empty}
|
|
558
559
|
>
|
|
559
560
|
{#snippet row(s)}
|
|
560
|
-
<Table.Cell class="max-w-40 truncate">{s.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
|
|
561
|
+
<Table.Cell class="max-w-40 truncate" title={s.userAgent ?? ""}>{deviceLabel(s.userAgent) ?? s.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
|
|
561
562
|
<Table.Cell class="text-muted-foreground">{s.ipAddress ?? "—"}</Table.Cell>
|
|
562
563
|
<Table.Cell class="text-muted-foreground">{formatDateTime(s.createdAt)}</Table.Cell>
|
|
563
564
|
<Table.Cell><Button variant="ghost" size="sm" disabled={busy} onclick={() => revokeSession(s.token)}>{i18n.t.users.revokeSession}</Button></Table.Cell>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# Example only. See infra/k3s/README.md; `podo deploy` renders its own.
|
|
1
2
|
# BullMQ worker — runs the same API image with the worker entrypoint.
|
|
2
3
|
# It has no HTTP server, so there is no Service or Ingress.
|
|
3
4
|
apiVersion: apps/v1
|
|
@@ -15,7 +16,7 @@ spec:
|
|
|
15
16
|
spec:
|
|
16
17
|
containers:
|
|
17
18
|
- name: worker
|
|
18
|
-
image: ghcr.io/example/podokit-api:
|
|
19
|
+
image: ghcr.io/example/podokit-api:v0.0.0
|
|
19
20
|
command: ["node", "dist/main-worker"]
|
|
20
21
|
envFrom:
|
|
21
22
|
- configMapRef: { name: app-config }
|
|
@@ -70,7 +70,8 @@ Useful commands:
|
|
|
70
70
|
- `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
|
|
71
71
|
- `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
|
|
72
72
|
- `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
|
|
73
|
-
- `podo deploy` — plan, apply, verify, or roll back an immutable Kubernetes release. Use `.agents/skills/podokit-deploy`.
|
|
73
|
+
- `podo deploy` — plan, apply, verify, or roll back an immutable release on Kubernetes or one Docker host. Tag `vX.Y.Z` to build and push the images (`.github/workflows/release.yml`; check its runner — GitHub-hosted minutes are free for public repositories only). Use `.agents/skills/podokit-deploy`.
|
|
74
|
+
- `podo deploy sync` — copy local build output into a running Compose deployment and restart it, for iterating without a release. Not a release: no image is published and the next `apply` discards it. Use `.agents/skills/podokit-deploy-fast`.
|
|
74
75
|
|
|
75
76
|
## Commit conventions
|
|
76
77
|
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Development image for the PodoKit monorepo (web + api).
|
|
2
2
|
# Dependencies and source are baked in; `docker compose watch` syncs your
|
|
3
3
|
# edits into the running container so the native file watchers fire.
|
|
4
|
+
|
|
5
|
+
# Every workspace manifest, collected rather than listed.
|
|
6
|
+
#
|
|
7
|
+
# Listing them by hand only works for the workspaces this template starts with. A
|
|
8
|
+
# project that adds `packages/*` then has workspace manifests that never reach the
|
|
9
|
+
# image, so npm does not know those names are local and goes looking for them on the
|
|
10
|
+
# public registry — `npm error 404 '@scope/thing' is not in this registry`, from an
|
|
11
|
+
# install that succeeds on the developer's own machine. The production images
|
|
12
|
+
# collect manifests for exactly this reason; this one was the exception until a
|
|
13
|
+
# generated project with shared packages could not start its dev stack at all.
|
|
14
|
+
FROM node:22-bookworm-slim AS manifests
|
|
15
|
+
WORKDIR /src
|
|
16
|
+
COPY . .
|
|
17
|
+
RUN mkdir -p /app \
|
|
18
|
+
&& cp package.json /app/ \
|
|
19
|
+
&& find . -name package.json -not -path "*/node_modules/*" \
|
|
20
|
+
-exec sh -c 'mkdir -p "/app/$(dirname "$1")" && cp "$1" "/app/$1"' _ {} \;
|
|
21
|
+
|
|
4
22
|
FROM node:22-bookworm-slim
|
|
5
23
|
|
|
6
24
|
# git for the toolchain (editors, AI agents); build tools for native modules.
|
|
@@ -14,9 +32,7 @@ WORKDIR /app
|
|
|
14
32
|
# NB: the host package-lock.json is intentionally NOT copied — a lockfile
|
|
15
33
|
# generated on macOS/Windows prunes platform-specific optional deps and can drop
|
|
16
34
|
# packages (e.g. @swc/cli) on linux. Resolving fresh in-container avoids that.
|
|
17
|
-
COPY
|
|
18
|
-
COPY apps/api/package.json ./apps/api/package.json
|
|
19
|
-
COPY apps/web/package.json ./apps/web/package.json
|
|
35
|
+
COPY --from=manifests /app ./
|
|
20
36
|
ARG PODOKIT_NPM_REGISTRY=""
|
|
21
37
|
RUN if [ -n "$PODOKIT_NPM_REGISTRY" ]; then npm config set registry "$PODOKIT_NPM_REGISTRY"; fi \
|
|
22
38
|
&& npm install
|
|
@@ -24,4 +40,19 @@ RUN if [ -n "$PODOKIT_NPM_REGISTRY" ]; then npm config set registry "$PODOKIT_NP
|
|
|
24
40
|
# Baked source (kept in sync during development by `docker compose watch`).
|
|
25
41
|
COPY . .
|
|
26
42
|
|
|
43
|
+
# Workspace packages compile before the apps that import them. Apps import them by
|
|
44
|
+
# their published entry point (`dist/index.js`), so without this the API starts and
|
|
45
|
+
# dies on MODULE_NOT_FOUND for a package whose symlink is right there — the link
|
|
46
|
+
# resolves, the built file behind it does not exist. `--if-present` keeps a project
|
|
47
|
+
# with no packages, or one whose packages need no build, working unchanged.
|
|
48
|
+
#
|
|
49
|
+
# `docker compose watch` syncs app source only, so a change under packages/* needs
|
|
50
|
+
# this image rebuilt rather than a container restart.
|
|
51
|
+
RUN if [ -d packages ]; then \
|
|
52
|
+
for manifest in packages/*/package.json; do \
|
|
53
|
+
[ -e "$manifest" ] || continue; \
|
|
54
|
+
npm run build --if-present --workspace "$(dirname "$manifest")"; \
|
|
55
|
+
done; \
|
|
56
|
+
fi
|
|
57
|
+
|
|
27
58
|
EXPOSE 5001 5002
|