Skip to content

Commit 8181471

Browse files
committed
fix: make CI environment sync explicit
1 parent bf76230 commit 8181471

20 files changed

Lines changed: 833 additions & 164 deletions

File tree

.agent/skills/shipnode/REFERENCE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Most commands accept `--config <path>` and `--app <name>` (multi-app). `rollback
7777
### CI/CD
7878
| Command | Description |
7979
|---|---|
80-
| `ci github` | Write `.github/workflows/shipnode-deploy.yml` |
81-
| `ci env-sync [--all]` | Push .env → GitHub repo secrets |
80+
| `ci github [--app name] [--environment name] [--sync-env]` | Write the deploy workflow; app env stays server-managed unless opted in |
81+
| `ci env-sync [--app name] [--environment name] [--file path] [--dry-run] [--all]` | Store one complete dotenv file as a GitHub Environment secret |
8282

8383
### Config
8484
| Command | Description |

.agent/skills/shipnode/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ Target one app: `shipnode deploy --app api`, `shipnode logs --app web`. `rollbac
9090
3. `shipnode env` (per app if multi-app: `--app api`)
9191
4. `shipnode deploy`
9292

93+
### CI/CD
94+
95+
Runtime env stays on the VPS by default: upload it with `shipnode env`, then generate a code-only workflow with `shipnode ci github`. To opt one app into GitHub-managed env, run `shipnode ci env-sync --app api --environment production`, then generate with the matching `--app api --environment production --sync-env` flags. Shipnode stores the complete dotenv file as one Environment secret and uploads it with `--no-reload` immediately before deployment.
96+
9397
### Releases
9498
Every deploy uses release dirs + atomic `current` symlink. Failed deploys revert `current` (when a previous release exists) and record `status: 'failed'`.
9599

.claude/skills/shipnode/REFERENCE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Most commands accept `--config <path>` and `--app <name>` (multi-app). `rollback
7777
### CI/CD
7878
| Command | Description |
7979
|---|---|
80-
| `ci github` | Write `.github/workflows/shipnode-deploy.yml` |
81-
| `ci env-sync [--all]` | Push .env → GitHub repo secrets |
80+
| `ci github [--app name] [--environment name] [--sync-env]` | Write the deploy workflow; app env stays server-managed unless opted in |
81+
| `ci env-sync [--app name] [--environment name] [--file path] [--dry-run] [--all]` | Store one complete dotenv file as a GitHub Environment secret |
8282

8383
### Config
8484
| Command | Description |

.claude/skills/shipnode/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ Target one app: `shipnode deploy --app api`, `shipnode logs --app web`. `rollbac
9090
3. `shipnode env` (per app if multi-app: `--app api`)
9191
4. `shipnode deploy`
9292

93+
### CI/CD
94+
95+
Runtime env stays on the VPS by default: upload it with `shipnode env`, then generate a code-only workflow with `shipnode ci github`. To opt one app into GitHub-managed env, run `shipnode ci env-sync --app api --environment production`, then generate with the matching `--app api --environment production --sync-env` flags. Shipnode stores the complete dotenv file as one Environment secret and uploads it with `--no-reload` immediately before deployment.
96+
9397
### Releases
9498
Every deploy uses release dirs + atomic `current` symlink. Failed deploys revert `current` (when a previous release exists) and record `status: 'failed'`.
9599

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,10 +425,16 @@ Falls back to installing `cloudflared` from the upstream GitHub `.deb` if `pkg.c
425425
### CI/CD
426426

427427
```bash
428-
shipnode ci github # Generate .github/workflows/shipnode-deploy.yml
429-
shipnode ci env-sync # Push .env vars to GitHub repository secrets
428+
shipnode ci github # Generate a server-managed-env deploy workflow
429+
shipnode env --app api # Upload runtime env directly to the VPS (default)
430+
431+
# Opt in to GitHub-managed env for one app/environment
432+
shipnode ci env-sync --app api --environment production --all
433+
shipnode ci github --app api --environment production --sync-env
430434
```
431435

436+
The default workflow deploys code without copying application secrets through GitHub. With `--sync-env`, Shipnode stores the complete dotenv file as one GitHub Environment secret, uploads it with `--no-reload`, deploys, then removes the runner copy. Production and staging use distinct environments and secret names.
437+
432438
### Configuration
433439

434440
```bash
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Keep runtime env on the VPS by default; make GitHub ownership explicit
2+
3+
Shipnode deployments need application configuration during remote backend builds and process startup. Static frontend builds may need the same configuration on the CI runner. Treating every dotenv key as an independent repository secret created partial-update risks, lost dotenv syntax, secret-name collisions between apps, and a workflow that did not actually consume the synchronized values.
4+
5+
## Decision
6+
7+
Runtime env remains server-managed by default. Operators upload it directly with `shipnode env`; ordinary generated workflows deploy code without transferring application secrets through GitHub.
8+
9+
GitHub-managed env is an explicit per-app, per-environment mode:
10+
11+
- `ci env-sync` stores the complete dotenv file as one GitHub Environment secret named `SHIPNODE_ENV_<ENVIRONMENT>_<APP>`.
12+
- The secret value is sent to `gh secret set` over standard input, never as a process argument.
13+
- `ci github --sync-env` requires an app target in multi-app workspaces and generates a job bound to the selected GitHub Environment.
14+
- The workflow rejects a missing secret, materializes the configured file under a restrictive umask, uploads it with `shipnode env --no-reload`, deploys the app, and removes the runner copy with `if: always()`.
15+
- Files larger than GitHub's 48 KB secret limit are rejected before invoking GitHub.
16+
17+
SSH host, user, and port stay in committed Shipnode configuration. CI secrets are limited to the SSH private key, a trusted known-hosts entry, and the optional app environment blob.
18+
19+
## Deployment behavior
20+
21+
Uploading with `--no-reload` is required for release safety. A running old process retains its existing process environment while the new blue-green colour starts with the newly uploaded shared env. Health checks and the Caddy switch still decide whether the release goes live.
22+
23+
Generated workflows do not run a generic repository-root build. Frontend strategies already build locally during `shipnode deploy`, while backend strategies build remotely after sourcing the shared env. The workflow only installs dependencies needed to load project configuration and execute Shipnode.
24+
25+
Production jobs use GitHub Environment protection, least-privilege repository permissions, a timeout, and serialized concurrency with `cancel-in-progress: false`. Root-level monorepo changes continue to trigger deployments; no `paths` filter is generated.
26+
27+
## Trade-offs
28+
29+
- Server-managed env minimizes secret exposure but requires an out-of-band env upload or rotation step.
30+
- GitHub-managed env makes CI reproducible but exposes the complete application environment to GitHub and its ephemeral runner.
31+
- One opaque secret provides atomic file replacement and exact dotenv preservation, but GitHub cannot rotate or audit individual keys independently.
32+
- Environments larger than 48 KB need to remain server-managed or move to an external secret manager; Shipnode does not introduce an encrypted-file format.

0 commit comments

Comments
 (0)