@next-friday/create-app 9.6.0 → 9.7.1
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 +11 -10
- package/dist/main.mjs +110 -14
- package/package.json +1 -1
- package/templates/base/.agents/hooks/agent-production-gate.sh +8 -3
- package/templates/base/SETUP.md +9 -1
- package/templates/base/scripts/check-agent-gates.ts +40 -35
- package/templates/deploy-railway/turborepo/turbo.json +66 -0
- package/templates/nestjs/.github/workflows/ci.yml +3 -1
- package/templates/nestjs/AGENTS.md +5 -0
- package/templates/nestjs/Dockerfile +1 -1
- package/templates/nestjs/README.md +26 -0
- package/templates/nextjs/.github/workflows/ci.yml +3 -1
- package/templates/nextjs/AGENTS.md +5 -0
- package/templates/nextjs/Dockerfile +1 -1
- package/templates/nextjs/README.md +26 -0
- package/templates/nodejs/.github/workflows/ci.yml +3 -1
- package/templates/nodejs/AGENTS.md +5 -0
- package/templates/nodejs/Dockerfile +1 -1
- package/templates/nodejs/README.md +26 -0
- package/templates/payload/.github/workflows/ci.yml +3 -1
- package/templates/payload/AGENTS.md +5 -0
- package/templates/payload/Dockerfile +1 -1
- package/templates/payload/README.md +26 -0
- package/templates/turborepo/.github/workflows/ci.yml +3 -1
- package/templates/turborepo/AGENTS.md +5 -0
- package/templates/turborepo/README.md +26 -0
- package/templates/turborepo/apps/app/Dockerfile +1 -1
- package/templates/turborepo/apps/backoffice/Dockerfile +1 -1
- package/templates/turborepo/turbo.json +1 -7
- /package/templates/{base → deploy-railway/base}/.github/workflows/deploy.yml +0 -0
- /package/templates/{base → deploy-railway/base}/railway.json +0 -0
- /package/templates/{base → deploy-railway/base}/runbooks/rollback-deploy.md +0 -0
- /package/templates/{turborepo → deploy-railway/turborepo}/.github/workflows/deploy.yml +0 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
|
|
|
31
31
|
- **Code quality:** TypeScript, ESLint and Prettier with the Next Friday configs, Vitest with coverage, fast-check, Knip, dependency-cruiser, editorconfig-checker, and sort-package-json.
|
|
32
32
|
- **Git hooks:** Husky runs gitleaks, `pnpm rules:check`, and lint-staged before each commit, commitlint on each message, and `pnpm check` before each push. After the first commit, commits to `main` are blocked, and local merges into `main` always are; pushes to `main` are blocked once it exists on the remote.
|
|
33
33
|
- **CI:** GitHub Actions run the static checks, tests with patch coverage, the build, end-to-end tests for Next.js and Payload, a container build and vulnerability scan, Semgrep, OSV-Scanner, CodeQL, dependency review, OpenSSF Scorecard, gitleaks, TruffleHog, zizmor, and pull request title validation. Dependabot keeps dependencies current.
|
|
34
|
-
- **Deployment:** a `Dockerfile` that runs as the `node` user
|
|
34
|
+
- **Deployment:** a `Dockerfile` that runs as the `node` user. With `--deploy railway`, also a Railway workflow that promotes one commit through `dev`, `uat`, and `prod`.
|
|
35
35
|
- **Agent rules:** `CLAUDE.md` imports `AGENTS.md`. Shared rules live in `.agents/rules`, with hooks that enforce them in Claude Code and Cursor.
|
|
36
36
|
|
|
37
37
|
## Requirements
|
|
@@ -44,17 +44,18 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
|
|
|
44
44
|
## Options
|
|
45
45
|
|
|
46
46
|
```text
|
|
47
|
-
pnpm create @next-friday/app [name] [--stack nodejs|nestjs|nextjs|payload|turborepo]
|
|
47
|
+
pnpm create @next-friday/app [name] [--stack nodejs|nestjs|nextjs|payload|turborepo] [--deploy none|railway]
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
| Option | Description
|
|
51
|
-
| :--------------- |
|
|
52
|
-
| `name` | Project directory and package name. Asked when left out.
|
|
53
|
-
| `--stack` | `nodejs`, `nestjs`, `nextjs`, `payload`, or `turborepo`. Asked when left out.
|
|
54
|
-
| `--
|
|
55
|
-
| `--skip-
|
|
56
|
-
|
|
|
57
|
-
| `-
|
|
50
|
+
| Option | Description |
|
|
51
|
+
| :--------------- | :---------------------------------------------------------------------------------------- |
|
|
52
|
+
| `name` | Project directory and package name. Asked when left out. |
|
|
53
|
+
| `--stack` | `nodejs`, `nestjs`, `nextjs`, `payload`, or `turborepo`. Asked when left out. |
|
|
54
|
+
| `--deploy` | Deploy target: `none` or `railway`. The default is `none`, which adds no deploy workflow. |
|
|
55
|
+
| `--skip-git` | Do not run `git init`. |
|
|
56
|
+
| `--skip-install` | Do not run `pnpm install`. |
|
|
57
|
+
| `-v, --version` | Print the version. |
|
|
58
|
+
| `-h, --help` | Print the usage. |
|
|
58
59
|
|
|
59
60
|
The name must:
|
|
60
61
|
|
package/dist/main.mjs
CHANGED
|
@@ -117,6 +117,16 @@ function validateProjectName(name, cwd) {
|
|
|
117
117
|
function ownEntries(directory) {
|
|
118
118
|
return readdirSync(directory).filter((entry) => entry !== ".git");
|
|
119
119
|
}
|
|
120
|
+
// src/deploy-targets.ts
|
|
121
|
+
const deployTargets = ["none", "railway"];
|
|
122
|
+
/**
|
|
123
|
+
* Check whether a string names a supported deploy target.
|
|
124
|
+
* @param value - Deploy target from the command line.
|
|
125
|
+
* @returns Whether `value` is a supported deploy target.
|
|
126
|
+
*/
|
|
127
|
+
function isDeployTarget(value) {
|
|
128
|
+
return deployTargets.some((target) => target === value);
|
|
129
|
+
}
|
|
120
130
|
// src/stacks.ts
|
|
121
131
|
const stacks = [
|
|
122
132
|
{
|
|
@@ -173,10 +183,11 @@ function templateName(stack) {
|
|
|
173
183
|
return `next-friday-template-${stack}`;
|
|
174
184
|
}
|
|
175
185
|
// src/arguments.ts
|
|
176
|
-
const usage = `Usage: pnpm create @next-friday/app [name] [--stack ${stacks.map((stack) => stack.id).join("|")}]
|
|
186
|
+
const usage = `Usage: pnpm create @next-friday/app [name] [--stack ${stacks.map((stack) => stack.id).join("|")}] [--deploy ${deployTargets.join("|")}]
|
|
177
187
|
|
|
178
188
|
name Project directory and package name, or . for the current directory. Asked when left out.
|
|
179
189
|
--stack Template stack. Asked when left out.
|
|
190
|
+
--deploy Deploy target: ${deployTargets.join(" or ")}. The default is none.
|
|
180
191
|
--skip-git Do not run git init.
|
|
181
192
|
--skip-install Do not run pnpm install.
|
|
182
193
|
-v, --version Show the version.
|
|
@@ -192,6 +203,10 @@ function parseCliArguments(cliArguments) {
|
|
|
192
203
|
allowPositionals: true,
|
|
193
204
|
options: {
|
|
194
205
|
stack: { type: "string" },
|
|
206
|
+
deploy: {
|
|
207
|
+
type: "string",
|
|
208
|
+
default: "none"
|
|
209
|
+
},
|
|
195
210
|
"skip-git": {
|
|
196
211
|
type: "boolean",
|
|
197
212
|
default: false
|
|
@@ -212,23 +227,33 @@ function parseCliArguments(cliArguments) {
|
|
|
212
227
|
}
|
|
213
228
|
}
|
|
214
229
|
});
|
|
215
|
-
const { help, stack, version } = values;
|
|
230
|
+
const { deploy, help, stack, version } = values;
|
|
216
231
|
const isInformational = help || version;
|
|
217
232
|
if (!isInformational && positionals.length > 1) throw new Error(`Pass one project name, not ${positionals.length}: ${positionals.join(" ")}.`);
|
|
218
233
|
const [name] = positionals;
|
|
219
|
-
if (!isInformational
|
|
220
|
-
const ids = stacks.map((supported) => supported.id).join(", ");
|
|
221
|
-
throw new Error(`Unknown stack "${stack}". Use one of: ${ids}.`);
|
|
222
|
-
}
|
|
234
|
+
if (!isInformational) assertKnownChoices(stack, deploy);
|
|
223
235
|
return {
|
|
224
236
|
help,
|
|
225
237
|
name,
|
|
226
238
|
version,
|
|
239
|
+
deploy: isDeployTarget(deploy) ? deploy : "none",
|
|
227
240
|
stack: stack !== void 0 && isStackId(stack) ? stack : void 0,
|
|
228
241
|
skipGit: values["skip-git"],
|
|
229
242
|
skipInstall: values["skip-install"]
|
|
230
243
|
};
|
|
231
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Throw when the stack or the deploy target is not one the CLI supports.
|
|
247
|
+
* @param stack - Stack from the command line, or `undefined` when left out.
|
|
248
|
+
* @param deploy - Deploy target from the command line.
|
|
249
|
+
*/
|
|
250
|
+
function assertKnownChoices(stack, deploy) {
|
|
251
|
+
if (stack !== void 0 && !isStackId(stack)) {
|
|
252
|
+
const ids = stacks.map((supported) => supported.id).join(", ");
|
|
253
|
+
throw new Error(`Unknown stack "${stack}". Use one of: ${ids}.`);
|
|
254
|
+
}
|
|
255
|
+
if (!isDeployTarget(deploy)) throw new Error(`Unknown deploy target "${deploy}". Use one of: ${deployTargets.join(", ")}.`);
|
|
256
|
+
}
|
|
232
257
|
// src/rename.ts
|
|
233
258
|
/**
|
|
234
259
|
* Replace the template name with the project name in every text file.
|
|
@@ -245,6 +270,46 @@ async function renameProject(directory, from, to) {
|
|
|
245
270
|
await writeFile(file, text.replaceAll(from, () => to));
|
|
246
271
|
}));
|
|
247
272
|
}
|
|
273
|
+
// src/deploy-sections.ts
|
|
274
|
+
const marker = /^\s*(?:#|\/\/|<!--)\s*deploy:(?<name>[a-z]+)\s*(?:-->\s*)?$/u;
|
|
275
|
+
const endName = "end";
|
|
276
|
+
/**
|
|
277
|
+
* Keep the sections of a template file marked for the chosen deploy target and remove the others.
|
|
278
|
+
* A section starts at a comment line that holds `deploy:<target>` and ends at one that holds `deploy:end`.
|
|
279
|
+
* Every marker line is removed, with a blank line that directly follows it.
|
|
280
|
+
* @param content - Text of a copied template file.
|
|
281
|
+
* @param target - Chosen deploy target.
|
|
282
|
+
* @returns The text for the chosen deploy target.
|
|
283
|
+
*/
|
|
284
|
+
function resolveDeploySections(content, target) {
|
|
285
|
+
const kept = [];
|
|
286
|
+
let section;
|
|
287
|
+
let isAfterMarker = false;
|
|
288
|
+
const lines = content.split("\n");
|
|
289
|
+
for (const [index, line] of lines.entries()) {
|
|
290
|
+
const name = marker.exec(line)?.groups?.name;
|
|
291
|
+
if (name === void 0) {
|
|
292
|
+
if (!(isAfterMarker && line.trim() === "" && index < lines.length - 1) && (section ?? target) === target) kept.push(line);
|
|
293
|
+
} else section = sectionAfterMarker(name, section);
|
|
294
|
+
isAfterMarker = name !== void 0;
|
|
295
|
+
}
|
|
296
|
+
if (section !== void 0) throw new Error(`The deploy:${section} section has no deploy:end marker.`);
|
|
297
|
+
return kept.join("\n");
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Give the section a marker line leaves open, or throw when the marker is out of place.
|
|
301
|
+
* @param name - Deploy target the marker names, or `end`.
|
|
302
|
+
* @param section - Deploy target of the section the marker sits in, or `undefined` outside a section.
|
|
303
|
+
* @returns The deploy target of the section now open, or `undefined` after an end marker.
|
|
304
|
+
*/
|
|
305
|
+
function sectionAfterMarker(name, section) {
|
|
306
|
+
if (name === endName) {
|
|
307
|
+
if (section === void 0) throw new Error("A deploy:end marker has no deploy target section to close.");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (section !== void 0) throw new Error(`A deploy:${name} marker starts inside the deploy:${section} section.`);
|
|
311
|
+
return name;
|
|
312
|
+
}
|
|
248
313
|
// src/create.ts
|
|
249
314
|
const templatesDirectory = fileURLToPath(new URL("../templates", import.meta.url));
|
|
250
315
|
const interruptSignals = [
|
|
@@ -276,7 +341,7 @@ async function create(cliArguments) {
|
|
|
276
341
|
const target = path.resolve(cwd, name);
|
|
277
342
|
const directory = existsSync(target) ? realpathSync(target) : target;
|
|
278
343
|
log.step(`Copying the ${stack} template.`);
|
|
279
|
-
await writeProject(stack, directory, manifestName);
|
|
344
|
+
await writeProject(stack, options.deploy, directory, manifestName);
|
|
280
345
|
log.success(`Created ${manifestName} from the ${stack} template.`);
|
|
281
346
|
const { git, isInstalled } = setUpProject(directory, options);
|
|
282
347
|
outro(nextSteps(name, stack, git, isInstalled));
|
|
@@ -294,10 +359,11 @@ async function create(cliArguments) {
|
|
|
294
359
|
* Copy, rename, and write `.env`, removing what was written when a step fails or Ctrl+C, SIGTERM, or SIGHUP arrives.
|
|
295
360
|
* An interrupt waits for the running step, so the removal does not race the copy.
|
|
296
361
|
* @param stack - Supported stack id.
|
|
362
|
+
* @param deploy - Supported deploy target.
|
|
297
363
|
* @param directory - Project directory.
|
|
298
364
|
* @param manifestName - Package name for `package.json`.
|
|
299
365
|
*/
|
|
300
|
-
async function writeProject(stack, directory, manifestName) {
|
|
366
|
+
async function writeProject(stack, deploy, directory, manifestName) {
|
|
301
367
|
const kept = existsSync(directory) ? readdirSync(directory) : void 0;
|
|
302
368
|
let isInterrupted = false;
|
|
303
369
|
const interrupt = () => {
|
|
@@ -305,7 +371,7 @@ async function writeProject(stack, directory, manifestName) {
|
|
|
305
371
|
};
|
|
306
372
|
for (const signal of interruptSignals) process.on(signal, interrupt);
|
|
307
373
|
try {
|
|
308
|
-
await copyTemplate(stack, directory);
|
|
374
|
+
await copyTemplate(stack, deploy, directory);
|
|
309
375
|
await renameProject(directory, templateName(stack), manifestName);
|
|
310
376
|
await writeEnvironmentFile(directory);
|
|
311
377
|
if (isInterrupted) throw new CancelledError();
|
|
@@ -382,19 +448,49 @@ function existingGit(directory) {
|
|
|
382
448
|
return existsSync(path.join(directory, ".git")) ? "existing" : void 0;
|
|
383
449
|
}
|
|
384
450
|
/**
|
|
385
|
-
* Copy the shared template files,
|
|
451
|
+
* Copy the shared template files, the stack's files, and the deploy target's files into the project directory.
|
|
386
452
|
* npm leaves `.gitignore` out of published packages, so the template keeps it as `_gitignore`.
|
|
387
453
|
* @param stack - Supported stack id.
|
|
454
|
+
* @param deploy - Supported deploy target.
|
|
388
455
|
* @param directory - Project directory.
|
|
389
456
|
*/
|
|
390
|
-
async function copyTemplate(stack, directory) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
457
|
+
async function copyTemplate(stack, deploy, directory) {
|
|
458
|
+
const deployLayer = deploy === "none" ? void 0 : `deploy-${deploy}`;
|
|
459
|
+
const required = [
|
|
460
|
+
"base",
|
|
461
|
+
stack,
|
|
462
|
+
...deployLayer === void 0 ? [] : [`${deployLayer}/base`]
|
|
463
|
+
];
|
|
464
|
+
for (const template of required) if (!existsSync(path.join(templatesDirectory, template))) throw new Error(`The ${template} template is missing from ${templatesDirectory}. Reinstall @next-friday/create-app.`);
|
|
465
|
+
const copyLayer = async (layer) => {
|
|
466
|
+
await cp(path.join(templatesDirectory, layer), directory, { recursive: true });
|
|
467
|
+
};
|
|
468
|
+
await copyLayer("base");
|
|
469
|
+
await copyLayer(stack);
|
|
470
|
+
if (deployLayer !== void 0) {
|
|
471
|
+
await copyLayer(`${deployLayer}/base`);
|
|
472
|
+
if (existsSync(path.join(templatesDirectory, deployLayer, stack))) await copyLayer(`${deployLayer}/${stack}`);
|
|
473
|
+
}
|
|
394
474
|
await rename(path.join(directory, "_gitignore"), path.join(directory, ".gitignore"));
|
|
475
|
+
await resolveDeployTarget(directory, deploy);
|
|
395
476
|
await markScriptsExecutable(directory);
|
|
396
477
|
}
|
|
397
478
|
/**
|
|
479
|
+
* Resolve the deploy target sections of every copied file that holds a marker.
|
|
480
|
+
* @param directory - Project directory.
|
|
481
|
+
* @param deploy - Supported deploy target.
|
|
482
|
+
*/
|
|
483
|
+
async function resolveDeployTarget(directory, deploy) {
|
|
484
|
+
const files = await listProjectFiles(directory);
|
|
485
|
+
await Promise.all(files.map(async (file) => {
|
|
486
|
+
const content = await readFile(file);
|
|
487
|
+
if (!content.includes("deploy:")) return;
|
|
488
|
+
const template = content.toString();
|
|
489
|
+
const resolved = resolveDeploySections(template, deploy);
|
|
490
|
+
if (resolved !== template) await writeFile(file, resolved);
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
398
494
|
* Make every file that starts with a shebang executable. `pnpm pack` drops the executable bit,
|
|
399
495
|
* and the Git and agent hooks run these scripts directly.
|
|
400
496
|
* @param directory - Project directory.
|
package/package.json
CHANGED
|
@@ -45,9 +45,9 @@ release_gate="A release manager runs prod deploys, commands against prod, and re
|
|
|
45
45
|
|
|
46
46
|
# The gate reads the command line as text, joining continued lines and dropping quotes,
|
|
47
47
|
# backslashes, and fd redirections, so wrappers such as bash -c, eval, or npx and split words such
|
|
48
|
-
# as pr''od do not hide the common forms. A dispatch input
|
|
49
|
-
#
|
|
50
|
-
#
|
|
48
|
+
# as pr''od do not hide the common forms. A dispatch input the gate cannot read (a variable, a
|
|
49
|
+
# command substitution, a file, or stdin) counts as prod. It is a convenience, not a security
|
|
50
|
+
# boundary: the prod environment's required reviewers and the deploy credentials are.
|
|
51
51
|
text=${command//'\\'/}
|
|
52
52
|
text=${text//$'\\\n'/}
|
|
53
53
|
text=$(sed -E 's/([0-9]*[<>]&[0-9-]*|&>>?)//g' <<< "${text//[\"\'\\]/}")
|
|
@@ -56,16 +56,21 @@ prod="(prod([^[:alnum:]_-]|$)|[\$\`@])"
|
|
|
56
56
|
dispatch="(workflow[[:space:]]+run|/dispatches)"
|
|
57
57
|
environment_field="environment[]]*[=:][[:space:]]*$prod"
|
|
58
58
|
unreadable_inputs="--(json|input)([[:space:]=]|$)"
|
|
59
|
+
# deploy:railway
|
|
60
|
+
# A Railway environment the gate cannot read counts as prod too.
|
|
59
61
|
railway="(^|[^[:alnum:]_.-])(railway|@railway/cli(@[^[:space:]]*)?)([[:space:]]$simple)?"
|
|
62
|
+
# deploy:end
|
|
60
63
|
git_push="(^|[^[:alnum:]_.-])git([[:space:]]+(-[Cc][[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|config-env)[[:space:]]+[^[:space:]]+|--?[^[:space:]]+))*[[:space:]]+push([[:space:]]$simple)?"
|
|
61
64
|
tag_ref="([[:space:]:+]((refs/)?tags/|v[0-9])|[[:space:]]--((follow-)?tags|mirror)([[:space:]=]|$))"
|
|
62
65
|
|
|
63
66
|
if [[ $text =~ $dispatch$simple($environment_field|$unreadable_inputs) || $text =~ $environment_field$simple$dispatch ]]; then
|
|
64
67
|
block "$release_gate"
|
|
65
68
|
fi
|
|
69
|
+
# deploy:railway
|
|
66
70
|
if [[ $text =~ $railway[[:space:]](-[[:alpha:]]*e([[:space:]]+|=)?|--environment([[:space:]]+|=))$prod || $text =~ $railway[[:space:]](env|environment)([[:space:]]+[[:alpha:]]+)?[[:space:]]+$prod ]]; then
|
|
67
71
|
block "$release_gate"
|
|
68
72
|
fi
|
|
73
|
+
# deploy:end
|
|
69
74
|
if [[ $text =~ $git_push$tag_ref || $(tr '[:upper:]' '[:lower:]' <<< "$text") == *push.followtags* ]]; then
|
|
70
75
|
block "$release_gate"
|
|
71
76
|
fi
|
package/templates/base/SETUP.md
CHANGED
|
@@ -64,7 +64,10 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
|
|
|
64
64
|
|
|
65
65
|
Rulesets in a private repository need GitHub Pro, Team, or Enterprise; on GitHub Free, make the repository public or upgrade.
|
|
66
66
|
|
|
67
|
-
- **Secrets and variables → Actions → Variables:** in a private repository, set `CODE_SCANNING` to `true` to run CodeQL, Scorecard, and Dependency Review, which need GitHub Code Security there; public repositories run them without it. Set `
|
|
67
|
+
- **Secrets and variables → Actions → Variables:** in a private repository, set `CODE_SCANNING` to `true` to run CodeQL, Scorecard, and Dependency Review, which need GitHub Code Security there; public repositories run them without it. Set `CONTAINER_IMAGE` to `false` to skip the job that builds and scans the container image, for a project that ships no image.
|
|
68
|
+
<!-- deploy:railway -->
|
|
69
|
+
- **Secrets and variables → Actions → Variables:** set `DEPLOY_ENABLED` to `true` when each push to `main` that passes CI should deploy to `dev`; the README's deploy section sets up the environments.
|
|
70
|
+
<!-- deploy:end -->
|
|
68
71
|
- Create the `needs triage` label, which the issue forms apply: `gh label create "needs triage"`.
|
|
69
72
|
|
|
70
73
|
**Check:** a test pull request shows the two required checks and asks for a code owner's review.
|
|
@@ -94,7 +97,12 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
|
|
|
94
97
|
- **Sources of truth (engineering lead):** for each artifact, name one system as the source of truth: this repository, an existing tool such as Jira, or, as the minimum, a link both ways (the record id in the file and the commit SHA in the record).
|
|
95
98
|
- **Higher risk (engineering lead):** decide which changes count as higher risk. A tech lead or architect approves their specs and plans.
|
|
96
99
|
- **Policies (each policy owner, with an engineer):** the plugin ships a default for each area, `plugins/sdlc/claude-code/skills/<area>-policy/SKILL.md` (security: OWASP ASVS 5.0.0 Level 1; UX: WCAG 2.2 Level AA; compliance: the OECD privacy principles; brand: one declared source for every brand value). Review it. To replace a default, add the organization's own rules in `.agents/skills/<area>-policy/SKILL.md` in the project: that file wins over the plugin's default, and the `CODEOWNERS` line of step 2 for that area makes the policy owner its code owner. Do not edit the plugin's copy. Then run `pnpm rules:sync`, which renders the project's own policy skill for Claude Code and Cursor. Test that the skill triggers: ask for the relevant task in different ways and confirm the skill loads each time.
|
|
100
|
+
<!-- deploy:railway -->
|
|
97
101
|
- **Production gate (engineering leadership, with change management and compliance):** name the release managers and add them as required reviewers on the `prod` environment, as the README's deploy section describes. `.agents/hooks/agent-production-gate.sh` blocks an agent's shell commands that dispatch a `prod` deploy, run a Railway command with `prod` as its environment, or push a release tag. It does not read MCP tools. A release manager whose own agent session must do one of these starts the agent with `RELEASE_APPROVAL` set to the approval record, for example `RELEASE_APPROVAL=CHG-123 claude`. The value lifts the gate only when it matches the extended regular expression on the first line of `.agents/release-approval-form`, shipped as `^[A-Z]+-[0-9]+$`; set that line to the form of your organization's approval records. Each gate hook appends its decisions to `.agents/logs/gate-decisions.jsonl`, which git ignores: the time, the hook, the decision, the session, and the reason for a block, and, when the production gate is lifted, the approval value and the Git user name. A record holds no command text and no path.
|
|
102
|
+
<!-- deploy:end -->
|
|
103
|
+
<!-- deploy:none -->
|
|
104
|
+
- **Production gate (engineering leadership, with change management and compliance):** name the release managers and add them as required reviewers on the `prod` environment, as the README's deploy section describes. `.agents/hooks/agent-production-gate.sh` blocks an agent's shell commands that dispatch a `prod` deploy or push a release tag. It does not read MCP tools. A release manager whose own agent session must do one of these starts the agent with `RELEASE_APPROVAL` set to the approval record, for example `RELEASE_APPROVAL=CHG-123 claude`. The value lifts the gate only when it matches the extended regular expression on the first line of `.agents/release-approval-form`, shipped as `^[A-Z]+-[0-9]+$`; set that line to the form of your organization's approval records. Each gate hook appends its decisions to `.agents/logs/gate-decisions.jsonl`, which git ignores: the time, the hook, the decision, the session, and the reason for a block, and, when the production gate is lifted, the approval value and the Git user name. A record holds no command text and no path.
|
|
105
|
+
<!-- deploy:end -->
|
|
98
106
|
- **Review (tech lead):** `.agents/review.md` names this stack's generated files to skip; add a path when the project gains a generated file, then run `pnpm rules:sync`, which renders it to `REVIEW.md` for Claude Code Review. Once a month, rate the review findings and tune `.agents/review.md`.
|
|
99
107
|
- **People without Git experience:** connect GitHub to Claude (claude.ai or Cowork), so Claude commits `intent.md` for them.
|
|
100
108
|
|
|
@@ -137,7 +137,7 @@ const allowedCommands = [
|
|
|
137
137
|
'gh api repos/next-friday/example/issues/1/comments -f body="APPROVED by QA"',
|
|
138
138
|
];
|
|
139
139
|
|
|
140
|
-
const
|
|
140
|
+
const sampleProductionCommand = "gh workflow run deploy.yml -f environment=prod";
|
|
141
141
|
|
|
142
142
|
const productionCommands = [
|
|
143
143
|
"gh workflow run deploy.yml -f environment=prod -f commit_sha=abc",
|
|
@@ -158,8 +158,30 @@ const productionCommands = [
|
|
|
158
158
|
"gh api repos/o/r/actions/workflows/1/dispatches --input body.json",
|
|
159
159
|
"gh api repos/o/r/actions/workflows/1/dispatches -f inputs[environment]=$E",
|
|
160
160
|
"bash -c 'gh workflow run deploy.yml -f environment=prod'",
|
|
161
|
+
sampleProductionCommand,
|
|
162
|
+
"git push origin v1.0.0",
|
|
163
|
+
"git push origin HEAD:refs/tags/v1.0.0",
|
|
164
|
+
"git push --tags",
|
|
165
|
+
"git push --follow-tags origin main",
|
|
166
|
+
"git -C . push origin v2.3.4",
|
|
167
|
+
"gh workflow run deploy.yml -f environment=pr''od",
|
|
168
|
+
"gh workflow run deploy.yml -f environment=p\\rod",
|
|
169
|
+
'gh workflow run deploy.yml -f "envi"ronment=prod',
|
|
170
|
+
"git push origin 'refs/tags/*'",
|
|
171
|
+
'git push origin "refs/tags/v*"',
|
|
172
|
+
"git push --mirror",
|
|
173
|
+
"git -c push.followTags=true push origin main",
|
|
174
|
+
"git config push.followTags true && git push",
|
|
175
|
+
"gh workflow run deploy.yml 2>&1 -f environment=prod",
|
|
176
|
+
"git push origin tags/v1.0.0",
|
|
177
|
+
"git --git-dir .git push origin v1.0.0",
|
|
178
|
+
"gh workflow run deploy.yml \\\n -f environment=prod \\\n -f commit_sha=abc",
|
|
179
|
+
"git push \\\n origin v1.0.0",
|
|
180
|
+
"git push &>/dev/null origin v1.0.0",
|
|
181
|
+
"cd a\\\\\ngit push origin v1.0.0",
|
|
182
|
+
// deploy:railway
|
|
161
183
|
"railway up --ci --environment prod",
|
|
162
|
-
|
|
184
|
+
"railway up -e prod",
|
|
163
185
|
"railway up -e=prod",
|
|
164
186
|
"railway up -eprod",
|
|
165
187
|
"railway up -ce prod",
|
|
@@ -181,32 +203,13 @@ const productionCommands = [
|
|
|
181
203
|
"/usr/local/bin/railway up -e prod",
|
|
182
204
|
"timeout 60 railway up -e prod",
|
|
183
205
|
"eval railway up -e prod",
|
|
184
|
-
"git push origin v1.0.0",
|
|
185
|
-
"git push origin HEAD:refs/tags/v1.0.0",
|
|
186
|
-
"git push --tags",
|
|
187
|
-
"git push --follow-tags origin main",
|
|
188
|
-
"git -C . push origin v2.3.4",
|
|
189
206
|
"npx @railway/cli@5.71.0 up -e prod",
|
|
190
207
|
"npx @railway/cli@latest up --environment prod",
|
|
191
208
|
"railway up -e pr''od",
|
|
192
|
-
"gh workflow run deploy.yml -f environment=pr''od",
|
|
193
|
-
"gh workflow run deploy.yml -f environment=p\\rod",
|
|
194
|
-
'gh workflow run deploy.yml -f "envi"ronment=prod',
|
|
195
|
-
"git push origin 'refs/tags/*'",
|
|
196
|
-
'git push origin "refs/tags/v*"',
|
|
197
|
-
"git push --mirror",
|
|
198
|
-
"git -c push.followTags=true push origin main",
|
|
199
|
-
"git config push.followTags true && git push",
|
|
200
|
-
"gh workflow run deploy.yml 2>&1 -f environment=prod",
|
|
201
209
|
"railway up 2>&1 -e prod",
|
|
202
|
-
"git push origin tags/v1.0.0",
|
|
203
|
-
"git --git-dir .git push origin v1.0.0",
|
|
204
|
-
"gh workflow run deploy.yml \\\n -f environment=prod \\\n -f commit_sha=abc",
|
|
205
210
|
"railway up \\\n --environment prod",
|
|
206
|
-
"git push \\\n origin v1.0.0",
|
|
207
211
|
"railway up &>/dev/null -e prod",
|
|
208
|
-
|
|
209
|
-
"cd a\\\\\ngit push origin v1.0.0",
|
|
212
|
+
// deploy:end
|
|
210
213
|
];
|
|
211
214
|
|
|
212
215
|
const approvalLiftsGate = ["CHG-123", "CHG-1"];
|
|
@@ -229,10 +232,6 @@ const approvalKeepsGate = [
|
|
|
229
232
|
const nonProductionCommands = [
|
|
230
233
|
"gh workflow run deploy.yml -f environment=uat -f commit_sha=abc",
|
|
231
234
|
"gh api repos/o/r/actions/workflows/deploy.yml/dispatches -f 'inputs[environment]=uat'",
|
|
232
|
-
"railway up -e uat",
|
|
233
|
-
"railway up -e prod-eu",
|
|
234
|
-
"railway environment uat",
|
|
235
|
-
"railway status",
|
|
236
235
|
"gh workflow list",
|
|
237
236
|
"gh run list --workflow deploy.yml",
|
|
238
237
|
"gh api repos/o/r/environments/prod",
|
|
@@ -241,14 +240,20 @@ const nonProductionCommands = [
|
|
|
241
240
|
'git commit -m "release v1.0" && git push',
|
|
242
241
|
'git commit -m "docs(x): deploy to production"',
|
|
243
242
|
'gh pr create --title "ci(x): promote to prod" --body "run gh workflow run later"',
|
|
244
|
-
"echo railway",
|
|
245
243
|
"git tag v1.0.0",
|
|
246
244
|
'git stash push -m "wip v2"',
|
|
247
245
|
'git commit -m "fix(ci): push v2 images"',
|
|
248
|
-
'git commit -m "fix: railway env uat and prod split"',
|
|
249
246
|
"git push -u origin HEAD",
|
|
250
247
|
"git push origin feature/push-v2",
|
|
251
248
|
"pnpm verify 2>&1 | tail && git push",
|
|
249
|
+
// deploy:railway
|
|
250
|
+
"railway up -e uat",
|
|
251
|
+
"railway up -e prod-eu",
|
|
252
|
+
"railway environment uat",
|
|
253
|
+
"railway status",
|
|
254
|
+
"echo railway",
|
|
255
|
+
'git commit -m "fix: railway env uat and prod split"',
|
|
256
|
+
// deploy:end
|
|
252
257
|
];
|
|
253
258
|
|
|
254
259
|
const mcpTools = {
|
|
@@ -461,11 +466,11 @@ const cases: GateCase[] = [
|
|
|
461
466
|
expectedApproval: releaseApproval,
|
|
462
467
|
input: {
|
|
463
468
|
tool_input: {
|
|
464
|
-
command:
|
|
469
|
+
command: sampleProductionCommand,
|
|
465
470
|
},
|
|
466
471
|
},
|
|
467
472
|
expectedPermission: "allow" as const,
|
|
468
|
-
name: `production with RELEASE_APPROVAL="${releaseApproval}": ${
|
|
473
|
+
name: `production with RELEASE_APPROVAL="${releaseApproval}": ${sampleProductionCommand}`,
|
|
469
474
|
expectedExit: 0 as const,
|
|
470
475
|
})),
|
|
471
476
|
...approvalKeepsGate.map(releaseApproval => ({
|
|
@@ -473,10 +478,10 @@ const cases: GateCase[] = [
|
|
|
473
478
|
releaseApproval,
|
|
474
479
|
input: {
|
|
475
480
|
tool_input: {
|
|
476
|
-
command:
|
|
481
|
+
command: sampleProductionCommand,
|
|
477
482
|
},
|
|
478
483
|
},
|
|
479
|
-
name: `production with RELEASE_APPROVAL="${releaseApproval}": ${
|
|
484
|
+
name: `production with RELEASE_APPROVAL="${releaseApproval}": ${sampleProductionCommand}`,
|
|
480
485
|
expectedExit: 2 as const,
|
|
481
486
|
})),
|
|
482
487
|
...nonProductionCommands.map(command => ({
|
|
@@ -590,7 +595,7 @@ const cases: GateCase[] = [
|
|
|
590
595
|
name: "record: the conversation of a Cursor hook input",
|
|
591
596
|
expectedExit: 2,
|
|
592
597
|
input: {
|
|
593
|
-
command:
|
|
598
|
+
command: sampleProductionCommand,
|
|
594
599
|
conversation_id: "cursor-conversation",
|
|
595
600
|
},
|
|
596
601
|
},
|
|
@@ -617,7 +622,7 @@ const cases: GateCase[] = [
|
|
|
617
622
|
releaseApproval: "CHG-123",
|
|
618
623
|
expectedExit: 2,
|
|
619
624
|
input: {
|
|
620
|
-
command:
|
|
625
|
+
command: sampleProductionCommand,
|
|
621
626
|
},
|
|
622
627
|
},
|
|
623
628
|
{
|
|
@@ -636,7 +641,7 @@ const cases: GateCase[] = [
|
|
|
636
641
|
expectedExit: 2,
|
|
637
642
|
isRecordUnwritable: true,
|
|
638
643
|
input: {
|
|
639
|
-
command:
|
|
644
|
+
command: sampleProductionCommand,
|
|
640
645
|
},
|
|
641
646
|
},
|
|
642
647
|
];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turborepo.dev/schema.json",
|
|
3
|
+
"futureFlags": {
|
|
4
|
+
"affectedUsingTaskInputs": true
|
|
5
|
+
},
|
|
6
|
+
"globalDependencies": [
|
|
7
|
+
".dockerignore",
|
|
8
|
+
".env",
|
|
9
|
+
"package.json",
|
|
10
|
+
"pnpm-workspace.yaml",
|
|
11
|
+
"railway.json"
|
|
12
|
+
],
|
|
13
|
+
"globalPassThroughEnv": [
|
|
14
|
+
"APP_URL",
|
|
15
|
+
"CI",
|
|
16
|
+
"DATABASE_URL",
|
|
17
|
+
"NEXT_TELEMETRY_DISABLED",
|
|
18
|
+
"PAYLOAD_SECRET"
|
|
19
|
+
],
|
|
20
|
+
"tasks": {
|
|
21
|
+
"build": {
|
|
22
|
+
"dependsOn": ["^build"],
|
|
23
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"],
|
|
24
|
+
"outputs": [".next/**", "!.next/cache/**", "!.next/dev/**"]
|
|
25
|
+
},
|
|
26
|
+
"depcruise": {
|
|
27
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
28
|
+
},
|
|
29
|
+
"dev": {
|
|
30
|
+
"cache": false,
|
|
31
|
+
"persistent": true,
|
|
32
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
33
|
+
},
|
|
34
|
+
"start": {
|
|
35
|
+
"cache": false,
|
|
36
|
+
"persistent": true,
|
|
37
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
38
|
+
},
|
|
39
|
+
"test": {
|
|
40
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
41
|
+
},
|
|
42
|
+
"test:changed": {
|
|
43
|
+
"cache": false,
|
|
44
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
45
|
+
},
|
|
46
|
+
"test:coverage": {
|
|
47
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"],
|
|
48
|
+
"outputs": ["coverage/**"]
|
|
49
|
+
},
|
|
50
|
+
"test:e2e": {
|
|
51
|
+
"cache": false,
|
|
52
|
+
"dependsOn": ["build"],
|
|
53
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
54
|
+
},
|
|
55
|
+
"test:watch": {
|
|
56
|
+
"cache": false,
|
|
57
|
+
"persistent": true,
|
|
58
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"]
|
|
59
|
+
},
|
|
60
|
+
"typecheck": {
|
|
61
|
+
"dependsOn": ["^typecheck"],
|
|
62
|
+
"inputs": ["$TURBO_DEFAULT$", "!README.md"],
|
|
63
|
+
"outputs": [".next/types/**", "next-env.d.ts"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -118,7 +118,7 @@ jobs:
|
|
|
118
118
|
image:
|
|
119
119
|
name: Container Image
|
|
120
120
|
needs: changes
|
|
121
|
-
if: needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule'
|
|
121
|
+
if: (needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule') && vars.CONTAINER_IMAGE != 'false'
|
|
122
122
|
runs-on: ubuntu-latest
|
|
123
123
|
timeout-minutes: 15
|
|
124
124
|
env:
|
|
@@ -157,6 +157,7 @@ jobs:
|
|
|
157
157
|
tar --extract --gzip --file "$archive" trivy
|
|
158
158
|
./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:ci
|
|
159
159
|
|
|
160
|
+
# deploy:railway
|
|
160
161
|
deploy-dev:
|
|
161
162
|
name: Deploy to dev
|
|
162
163
|
needs:
|
|
@@ -171,6 +172,7 @@ jobs:
|
|
|
171
172
|
environment: dev
|
|
172
173
|
commit_sha: ${{ github.sha }}
|
|
173
174
|
|
|
175
|
+
# deploy:end
|
|
174
176
|
triage:
|
|
175
177
|
name: Triage failed build
|
|
176
178
|
needs:
|
|
@@ -45,7 +45,12 @@ The `verifier` subagent starts the app with `pnpm start:dev`. It exercises the c
|
|
|
45
45
|
|
|
46
46
|
When Claude makes a mistake twice, add the correction here.
|
|
47
47
|
|
|
48
|
+
<!-- deploy:railway -->
|
|
48
49
|
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
|
|
50
|
+
<!-- deploy:end -->
|
|
51
|
+
<!-- deploy:none -->
|
|
52
|
+
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
|
|
53
|
+
<!-- deploy:end -->
|
|
49
54
|
- **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
|
|
50
55
|
- **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
|
|
51
56
|
- **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
|
|
@@ -34,13 +34,25 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
|
|
|
34
34
|
|
|
35
35
|
## Container
|
|
36
36
|
|
|
37
|
+
<!-- deploy:railway -->
|
|
38
|
+
|
|
37
39
|
The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`; Railway sets it automatically. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
|
|
38
40
|
|
|
41
|
+
<!-- deploy:end -->
|
|
42
|
+
|
|
43
|
+
<!-- deploy:none -->
|
|
44
|
+
|
|
45
|
+
The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
|
|
46
|
+
|
|
47
|
+
<!-- deploy:end -->
|
|
48
|
+
|
|
39
49
|
```sh
|
|
40
50
|
docker build -t app .
|
|
41
51
|
docker run --env PORT=3000 --publish 3000:3000 app
|
|
42
52
|
```
|
|
43
53
|
|
|
54
|
+
<!-- deploy:railway -->
|
|
55
|
+
|
|
44
56
|
## Deploy
|
|
45
57
|
|
|
46
58
|
`.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
|
|
@@ -58,4 +70,18 @@ docker run --env PORT=3000 --publish 3000:3000 app
|
|
|
58
70
|
|
|
59
71
|
Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
|
|
60
72
|
|
|
73
|
+
<!-- deploy:end -->
|
|
74
|
+
|
|
75
|
+
<!-- deploy:none -->
|
|
76
|
+
|
|
77
|
+
## Deploy
|
|
78
|
+
|
|
79
|
+
The project has no deploy step yet. A deploy step needs:
|
|
80
|
+
|
|
81
|
+
- a workflow under `.github/workflows/` that deploys;
|
|
82
|
+
- the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
|
|
83
|
+
- the image the `Dockerfile` builds.
|
|
84
|
+
|
|
85
|
+
<!-- deploy:end -->
|
|
86
|
+
|
|
61
87
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
|
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
image:
|
|
125
125
|
name: Container Image
|
|
126
126
|
needs: changes
|
|
127
|
-
if: needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule'
|
|
127
|
+
if: (needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule') && vars.CONTAINER_IMAGE != 'false'
|
|
128
128
|
runs-on: ubuntu-latest
|
|
129
129
|
timeout-minutes: 15
|
|
130
130
|
env:
|
|
@@ -163,6 +163,7 @@ jobs:
|
|
|
163
163
|
tar --extract --gzip --file "$archive" trivy
|
|
164
164
|
./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:ci
|
|
165
165
|
|
|
166
|
+
# deploy:railway
|
|
166
167
|
deploy-dev:
|
|
167
168
|
name: Deploy to dev
|
|
168
169
|
needs:
|
|
@@ -177,6 +178,7 @@ jobs:
|
|
|
177
178
|
environment: dev
|
|
178
179
|
commit_sha: ${{ github.sha }}
|
|
179
180
|
|
|
181
|
+
# deploy:end
|
|
180
182
|
triage:
|
|
181
183
|
name: Triage failed build
|
|
182
184
|
needs:
|
|
@@ -46,7 +46,12 @@ The `verifier` subagent starts the app with `pnpm dev`. It exercises the changed
|
|
|
46
46
|
When Claude makes a mistake twice, add the correction here.
|
|
47
47
|
|
|
48
48
|
- **First end-to-end run**: run `pnpm exec playwright install chromium` once. `pnpm test:e2e` starts the development server unless one is already running.
|
|
49
|
+
<!-- deploy:railway -->
|
|
49
50
|
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
|
|
51
|
+
<!-- deploy:end -->
|
|
52
|
+
<!-- deploy:none -->
|
|
53
|
+
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
|
|
54
|
+
<!-- deploy:end -->
|
|
50
55
|
- **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
|
|
51
56
|
- **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
|
|
52
57
|
- **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Runs on any OCI platform
|
|
1
|
+
# Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
|
|
2
2
|
FROM \
|
|
3
3
|
node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
|
|
4
4
|
AS build
|
|
@@ -32,8 +32,18 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
|
|
|
32
32
|
|
|
33
33
|
## Container
|
|
34
34
|
|
|
35
|
+
<!-- deploy:railway -->
|
|
36
|
+
|
|
35
37
|
The `Dockerfile` builds a standalone production image that runs as the `node` user, for platforms such as Railway. The server listens on `PORT`, or 3000 when it is unset.
|
|
36
38
|
|
|
39
|
+
<!-- deploy:end -->
|
|
40
|
+
|
|
41
|
+
<!-- deploy:none -->
|
|
42
|
+
|
|
43
|
+
The `Dockerfile` builds a standalone production image that runs as the `node` user for a container platform. The server listens on `PORT`, or 3000 when it is unset.
|
|
44
|
+
|
|
45
|
+
<!-- deploy:end -->
|
|
46
|
+
|
|
37
47
|
```sh
|
|
38
48
|
docker build -t app .
|
|
39
49
|
docker run --publish 3000:3000 app
|
|
@@ -41,6 +51,8 @@ docker run --publish 3000:3000 app
|
|
|
41
51
|
|
|
42
52
|
On Vercel, deploy without the image; the Next.js preset takes precedence over the `Dockerfile`.
|
|
43
53
|
|
|
54
|
+
<!-- deploy:railway -->
|
|
55
|
+
|
|
44
56
|
## Deploy
|
|
45
57
|
|
|
46
58
|
`.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
|
|
@@ -58,4 +70,18 @@ On Vercel, deploy without the image; the Next.js preset takes precedence over th
|
|
|
58
70
|
|
|
59
71
|
Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
|
|
60
72
|
|
|
73
|
+
<!-- deploy:end -->
|
|
74
|
+
|
|
75
|
+
<!-- deploy:none -->
|
|
76
|
+
|
|
77
|
+
## Deploy
|
|
78
|
+
|
|
79
|
+
The project has no deploy step yet. A deploy step needs:
|
|
80
|
+
|
|
81
|
+
- a workflow under `.github/workflows/` that deploys;
|
|
82
|
+
- the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
|
|
83
|
+
- the image the `Dockerfile` builds.
|
|
84
|
+
|
|
85
|
+
<!-- deploy:end -->
|
|
86
|
+
|
|
61
87
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
|
|
@@ -116,7 +116,7 @@ jobs:
|
|
|
116
116
|
image:
|
|
117
117
|
name: Container Image
|
|
118
118
|
needs: changes
|
|
119
|
-
if: needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule'
|
|
119
|
+
if: (needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule') && vars.CONTAINER_IMAGE != 'false'
|
|
120
120
|
runs-on: ubuntu-latest
|
|
121
121
|
timeout-minutes: 15
|
|
122
122
|
env:
|
|
@@ -155,6 +155,7 @@ jobs:
|
|
|
155
155
|
tar --extract --gzip --file "$archive" trivy
|
|
156
156
|
./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:ci
|
|
157
157
|
|
|
158
|
+
# deploy:railway
|
|
158
159
|
deploy-dev:
|
|
159
160
|
name: Deploy to dev
|
|
160
161
|
needs:
|
|
@@ -169,6 +170,7 @@ jobs:
|
|
|
169
170
|
environment: dev
|
|
170
171
|
commit_sha: ${{ github.sha }}
|
|
171
172
|
|
|
173
|
+
# deploy:end
|
|
172
174
|
triage:
|
|
173
175
|
name: Triage failed build
|
|
174
176
|
needs:
|
|
@@ -44,7 +44,12 @@ The `verifier` subagent starts the app with `pnpm start:dev`. It exercises the c
|
|
|
44
44
|
|
|
45
45
|
When Claude makes a mistake twice, add the correction here.
|
|
46
46
|
|
|
47
|
+
<!-- deploy:railway -->
|
|
47
48
|
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
|
|
49
|
+
<!-- deploy:end -->
|
|
50
|
+
<!-- deploy:none -->
|
|
51
|
+
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
|
|
52
|
+
<!-- deploy:end -->
|
|
48
53
|
- **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
|
|
49
54
|
- **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
|
|
50
55
|
- **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
|
|
@@ -32,13 +32,25 @@ CI also runs the build, the container image scan, and the security scans, includ
|
|
|
32
32
|
|
|
33
33
|
## Container
|
|
34
34
|
|
|
35
|
+
<!-- deploy:railway -->
|
|
36
|
+
|
|
35
37
|
The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`; Railway sets it automatically. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
|
|
36
38
|
|
|
39
|
+
<!-- deploy:end -->
|
|
40
|
+
|
|
41
|
+
<!-- deploy:none -->
|
|
42
|
+
|
|
43
|
+
The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
|
|
44
|
+
|
|
45
|
+
<!-- deploy:end -->
|
|
46
|
+
|
|
37
47
|
```sh
|
|
38
48
|
docker build -t app .
|
|
39
49
|
docker run --env PORT=3000 --publish 3000:3000 app
|
|
40
50
|
```
|
|
41
51
|
|
|
52
|
+
<!-- deploy:railway -->
|
|
53
|
+
|
|
42
54
|
## Deploy
|
|
43
55
|
|
|
44
56
|
`.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
|
|
@@ -56,4 +68,18 @@ docker run --env PORT=3000 --publish 3000:3000 app
|
|
|
56
68
|
|
|
57
69
|
Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
|
|
58
70
|
|
|
71
|
+
<!-- deploy:end -->
|
|
72
|
+
|
|
73
|
+
<!-- deploy:none -->
|
|
74
|
+
|
|
75
|
+
## Deploy
|
|
76
|
+
|
|
77
|
+
The project has no deploy step yet. A deploy step needs:
|
|
78
|
+
|
|
79
|
+
- a workflow under `.github/workflows/` that deploys;
|
|
80
|
+
- the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
|
|
81
|
+
- the image the `Dockerfile` builds.
|
|
82
|
+
|
|
83
|
+
<!-- deploy:end -->
|
|
84
|
+
|
|
59
85
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
|
|
@@ -141,7 +141,7 @@ jobs:
|
|
|
141
141
|
image:
|
|
142
142
|
name: Container Image
|
|
143
143
|
needs: changes
|
|
144
|
-
if: needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule'
|
|
144
|
+
if: (needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule') && vars.CONTAINER_IMAGE != 'false'
|
|
145
145
|
runs-on: ubuntu-latest
|
|
146
146
|
timeout-minutes: 15
|
|
147
147
|
services:
|
|
@@ -196,6 +196,7 @@ jobs:
|
|
|
196
196
|
tar --extract --gzip --file "$archive" trivy
|
|
197
197
|
./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:ci
|
|
198
198
|
|
|
199
|
+
# deploy:railway
|
|
199
200
|
deploy-dev:
|
|
200
201
|
name: Deploy to dev
|
|
201
202
|
needs:
|
|
@@ -210,6 +211,7 @@ jobs:
|
|
|
210
211
|
environment: dev
|
|
211
212
|
commit_sha: ${{ github.sha }}
|
|
212
213
|
|
|
214
|
+
# deploy:end
|
|
213
215
|
triage:
|
|
214
216
|
name: Triage failed build
|
|
215
217
|
needs:
|
|
@@ -49,7 +49,12 @@ The `verifier` subagent starts the app with `pnpm dev`, which needs a PostgreSQL
|
|
|
49
49
|
When Claude makes a mistake twice, add the correction here.
|
|
50
50
|
|
|
51
51
|
- **Database for `pnpm dev` and end-to-end tests**: both need a PostgreSQL database in `DATABASE_URL` and `PAYLOAD_SECRET` in `.env`; the README lists every variable. Run `pnpm exec playwright install chromium` once before the first `pnpm test:e2e`.
|
|
52
|
+
<!-- deploy:railway -->
|
|
52
53
|
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
|
|
54
|
+
<!-- deploy:end -->
|
|
55
|
+
<!-- deploy:none -->
|
|
56
|
+
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
|
|
57
|
+
<!-- deploy:end -->
|
|
53
58
|
- **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
|
|
54
59
|
- **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
|
|
55
60
|
- **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Runs on any OCI platform
|
|
1
|
+
# Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
|
|
2
2
|
FROM \
|
|
3
3
|
node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
|
|
4
4
|
AS build
|
|
@@ -48,8 +48,18 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
|
|
|
48
48
|
|
|
49
49
|
## Container
|
|
50
50
|
|
|
51
|
+
<!-- deploy:railway -->
|
|
52
|
+
|
|
51
53
|
The `Dockerfile` builds a standalone production image that runs as the `node` user, for platforms such as Railway. The server listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`, the public URL that Payload accepts cookie-authenticated and cross-origin requests from.
|
|
52
54
|
|
|
55
|
+
<!-- deploy:end -->
|
|
56
|
+
|
|
57
|
+
<!-- deploy:none -->
|
|
58
|
+
|
|
59
|
+
The `Dockerfile` builds a standalone production image that runs as the `node` user for a container platform. The server listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`, the public URL that Payload accepts cookie-authenticated and cross-origin requests from.
|
|
60
|
+
|
|
61
|
+
<!-- deploy:end -->
|
|
62
|
+
|
|
53
63
|
```sh
|
|
54
64
|
docker build -t app .
|
|
55
65
|
docker run --publish 3000:3000 --env DATABASE_URL --env PAYLOAD_SECRET --env APP_URL app
|
|
@@ -61,6 +71,8 @@ The config sets no `email` adapter, so Payload writes every email, including pas
|
|
|
61
71
|
|
|
62
72
|
On Vercel, deploy without the image; the Next.js preset takes precedence over the `Dockerfile`.
|
|
63
73
|
|
|
74
|
+
<!-- deploy:railway -->
|
|
75
|
+
|
|
64
76
|
## Deploy
|
|
65
77
|
|
|
66
78
|
`.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
|
|
@@ -84,4 +96,18 @@ On Vercel, deploy without the image; the Next.js preset takes precedence over th
|
|
|
84
96
|
|
|
85
97
|
Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
|
|
86
98
|
|
|
99
|
+
<!-- deploy:end -->
|
|
100
|
+
|
|
101
|
+
<!-- deploy:none -->
|
|
102
|
+
|
|
103
|
+
## Deploy
|
|
104
|
+
|
|
105
|
+
The project has no deploy step yet. A deploy step needs:
|
|
106
|
+
|
|
107
|
+
- a workflow under `.github/workflows/` that deploys;
|
|
108
|
+
- the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
|
|
109
|
+
- the image the `Dockerfile` builds.
|
|
110
|
+
|
|
111
|
+
<!-- deploy:end -->
|
|
112
|
+
|
|
87
113
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
|
|
@@ -212,7 +212,7 @@ jobs:
|
|
|
212
212
|
needs:
|
|
213
213
|
- changes
|
|
214
214
|
- affected-apps
|
|
215
|
-
if: (needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule') && needs.affected-apps.outputs.apps != '[]'
|
|
215
|
+
if: (needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule') && needs.affected-apps.outputs.apps != '[]' && vars.CONTAINER_IMAGE != 'false'
|
|
216
216
|
runs-on: ubuntu-latest
|
|
217
217
|
timeout-minutes: 20
|
|
218
218
|
strategy:
|
|
@@ -272,6 +272,7 @@ jobs:
|
|
|
272
272
|
tar --extract --gzip --file "$archive" trivy
|
|
273
273
|
./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress "$APP:ci"
|
|
274
274
|
|
|
275
|
+
# deploy:railway
|
|
275
276
|
deploy-dev:
|
|
276
277
|
name: Deploy to dev
|
|
277
278
|
needs:
|
|
@@ -286,6 +287,7 @@ jobs:
|
|
|
286
287
|
environment: dev
|
|
287
288
|
commit_sha: ${{ github.sha }}
|
|
288
289
|
|
|
290
|
+
# deploy:end
|
|
289
291
|
triage:
|
|
290
292
|
name: Triage failed build
|
|
291
293
|
needs:
|
|
@@ -52,7 +52,12 @@ When Claude makes a mistake twice, add the correction here.
|
|
|
52
52
|
|
|
53
53
|
- **Next.js docs live per app**: this Next.js version differs from your training data. Read the guide in `apps/app/node_modules/next/dist/docs/` or `apps/backoffice/node_modules/next/dist/docs/` before writing Next.js code; the workspace root has no `next` package.
|
|
54
54
|
- **Database for `pnpm dev` and end-to-end tests**: the backoffice needs a PostgreSQL database in `DATABASE_URL` and `PAYLOAD_SECRET` in `.env`; the README lists every variable. Run `pnpm --filter ./apps/app exec playwright install chromium` once before the first `pnpm test:e2e`.
|
|
55
|
+
<!-- deploy:railway -->
|
|
55
56
|
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
|
|
57
|
+
<!-- deploy:end -->
|
|
58
|
+
<!-- deploy:none -->
|
|
59
|
+
- **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool passes it.
|
|
60
|
+
<!-- deploy:end -->
|
|
56
61
|
- **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
|
|
57
62
|
- **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
|
|
58
63
|
- **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
|
|
@@ -61,6 +61,8 @@ Uploaded images, up to 10 MB each, are written to the backoffice container files
|
|
|
61
61
|
|
|
62
62
|
On Vercel, deploy an app without its image. The Next.js preset takes precedence over the Dockerfile.
|
|
63
63
|
|
|
64
|
+
<!-- deploy:railway -->
|
|
65
|
+
|
|
64
66
|
## Deploy
|
|
65
67
|
|
|
66
68
|
`.github/workflows/deploy.yml` deploys both apps and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. Create one Railway service per app, with that service's Dockerfile path set to `apps/app/Dockerfile` or `apps/backoffice/Dockerfile`. The workflow stays off until you configure it:
|
|
@@ -86,6 +88,20 @@ On Vercel, deploy an app without its image. The Next.js preset takes precedence
|
|
|
86
88
|
|
|
87
89
|
Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
|
|
88
90
|
|
|
91
|
+
<!-- deploy:end -->
|
|
92
|
+
|
|
93
|
+
<!-- deploy:none -->
|
|
94
|
+
|
|
95
|
+
## Deploy
|
|
96
|
+
|
|
97
|
+
The project has no deploy step yet. A deploy step needs:
|
|
98
|
+
|
|
99
|
+
- a workflow under `.github/workflows/` that deploys;
|
|
100
|
+
- the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
|
|
101
|
+
- the image the `Dockerfile` builds.
|
|
102
|
+
|
|
103
|
+
<!-- deploy:end -->
|
|
104
|
+
|
|
89
105
|
## Mobile app
|
|
90
106
|
|
|
91
107
|
To add an Expo app, create it under `apps/mobile`:
|
|
@@ -94,6 +110,16 @@ To add an Expo app, create it under `apps/mobile`:
|
|
|
94
110
|
2. Use `@expo/fingerprint` with `expo-github-action/fingerprint` so a pull request requests a native build only when the native layer changed ([Expo fingerprint](https://docs.expo.dev/versions/latest/sdk/fingerprint/)).
|
|
95
111
|
3. Keep a single `react-native` version in the workspace. Duplicate React Native versions in one monorepo are not supported ([Expo: Monorepos](https://docs.expo.dev/guides/monorepos/)).
|
|
96
112
|
|
|
113
|
+
<!-- deploy:railway -->
|
|
114
|
+
|
|
97
115
|
EAS Build and Submit replace Railway for that app. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
|
|
98
116
|
|
|
117
|
+
<!-- deploy:end -->
|
|
118
|
+
|
|
119
|
+
<!-- deploy:none -->
|
|
120
|
+
|
|
121
|
+
EAS Build and Submit deploy that app, in place of the project's deploy step. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
|
|
122
|
+
|
|
123
|
+
<!-- deploy:end -->
|
|
124
|
+
|
|
99
125
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Runs on any OCI platform
|
|
1
|
+
# Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
|
|
2
2
|
FROM \
|
|
3
3
|
node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
|
|
4
4
|
AS build
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Runs on any OCI platform
|
|
1
|
+
# Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
|
|
2
2
|
FROM \
|
|
3
3
|
node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
|
|
4
4
|
AS build
|
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
"futureFlags": {
|
|
4
4
|
"affectedUsingTaskInputs": true
|
|
5
5
|
},
|
|
6
|
-
"globalDependencies": [
|
|
7
|
-
".dockerignore",
|
|
8
|
-
".env",
|
|
9
|
-
"package.json",
|
|
10
|
-
"pnpm-workspace.yaml",
|
|
11
|
-
"railway.json"
|
|
12
|
-
],
|
|
6
|
+
"globalDependencies": [".dockerignore", ".env", "package.json", "pnpm-workspace.yaml"],
|
|
13
7
|
"globalPassThroughEnv": [
|
|
14
8
|
"APP_URL",
|
|
15
9
|
"CI",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|