@next-friday/create-app 2.0.0 → 2.1.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.
Files changed (86) hide show
  1. package/README.md +16 -15
  2. package/dist/main.mjs +18 -7
  3. package/package.json +2 -2
  4. package/templates/base/pnpm-workspace.yaml +24 -2
  5. package/templates/payload/README.md +1 -1
  6. package/templates/payload/package.json +0 -1
  7. package/templates/payload/src/payload.config.test.ts +31 -0
  8. package/templates/turborepo/.agents/rules/typescript-first.md +7 -0
  9. package/templates/turborepo/.agents/rules.json +41 -0
  10. package/templates/turborepo/.claude/rules/deterministic-testing.md +21 -0
  11. package/templates/turborepo/.claude/rules/typescript-first.md +15 -0
  12. package/templates/turborepo/.cursor/rules/deterministic-testing.mdc +21 -0
  13. package/templates/turborepo/.cursor/rules/typescript-first.mdc +13 -0
  14. package/templates/turborepo/.dockerignore +12 -0
  15. package/templates/turborepo/.editorconfig-checker.json +3 -0
  16. package/templates/turborepo/.env.example +4 -0
  17. package/templates/turborepo/.github/ISSUE_TEMPLATE/bug.yml +66 -0
  18. package/templates/turborepo/.github/ISSUE_TEMPLATE/feature.yml +51 -0
  19. package/templates/turborepo/.github/dependabot.yml +79 -0
  20. package/templates/turborepo/.github/labeler.yml +25 -0
  21. package/templates/turborepo/.github/pull_request_template.md +25 -0
  22. package/templates/turborepo/.github/scripts/affected-apps.sh +31 -0
  23. package/templates/turborepo/.github/workflows/ci.yml +300 -0
  24. package/templates/turborepo/.github/workflows/deploy.yml +200 -0
  25. package/templates/turborepo/.github/workflows/sast.yml +46 -0
  26. package/templates/turborepo/.lintstagedrc.json +11 -0
  27. package/templates/turborepo/.prettierignore +5 -0
  28. package/templates/turborepo/AGENTS.md +20 -0
  29. package/templates/turborepo/CONTRIBUTING.md +58 -0
  30. package/templates/turborepo/README.md +114 -0
  31. package/templates/turborepo/_gitignore +53 -0
  32. package/templates/turborepo/apps/app/.dependency-cruiser.json +34 -0
  33. package/templates/turborepo/apps/app/Dockerfile +25 -0
  34. package/templates/turborepo/apps/app/app/favicon.ico +0 -0
  35. package/templates/turborepo/apps/app/app/globals.css +26 -0
  36. package/templates/turborepo/apps/app/app/health/route.test.ts +20 -0
  37. package/templates/turborepo/apps/app/app/health/route.ts +23 -0
  38. package/templates/turborepo/apps/app/app/layout.tsx +29 -0
  39. package/templates/turborepo/apps/app/app/page.test.ts +11 -0
  40. package/templates/turborepo/apps/app/app/page.tsx +8 -0
  41. package/templates/turborepo/apps/app/next.config.ts +40 -0
  42. package/templates/turborepo/apps/app/package.json +41 -0
  43. package/templates/turborepo/apps/app/playwright.config.ts +27 -0
  44. package/templates/turborepo/apps/app/postcss.config.mjs +7 -0
  45. package/templates/turborepo/apps/app/test/home.e2e.test.ts +11 -0
  46. package/templates/turborepo/apps/app/tsconfig.json +33 -0
  47. package/templates/turborepo/apps/app/vitest.config.ts +12 -0
  48. package/templates/turborepo/apps/backoffice/.dependency-cruiser.json +34 -0
  49. package/templates/turborepo/apps/backoffice/Dockerfile +28 -0
  50. package/templates/turborepo/apps/backoffice/next.config.ts +48 -0
  51. package/templates/turborepo/apps/backoffice/package.json +49 -0
  52. package/templates/turborepo/apps/backoffice/playwright.config.ts +27 -0
  53. package/templates/turborepo/apps/backoffice/src/app/(frontend)/layout.tsx +18 -0
  54. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.test.ts +14 -0
  55. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.tsx +14 -0
  56. package/templates/turborepo/apps/backoffice/src/app/(frontend)/styles.css +15 -0
  57. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
  58. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
  59. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/importMap.ts +6 -0
  60. package/templates/turborepo/apps/backoffice/src/app/(payload)/api/[...slug]/route.ts +19 -0
  61. package/templates/turborepo/apps/backoffice/src/app/(payload)/custom.scss +0 -0
  62. package/templates/turborepo/apps/backoffice/src/app/(payload)/layout.tsx +31 -0
  63. package/templates/turborepo/apps/backoffice/src/app/health/route.test.ts +20 -0
  64. package/templates/turborepo/apps/backoffice/src/app/health/route.ts +23 -0
  65. package/templates/turborepo/apps/backoffice/src/collections/media.test.ts +27 -0
  66. package/templates/turborepo/apps/backoffice/src/collections/media.ts +18 -0
  67. package/templates/turborepo/apps/backoffice/src/collections/users.test.ts +10 -0
  68. package/templates/turborepo/apps/backoffice/src/collections/users.ts +10 -0
  69. package/templates/turborepo/apps/backoffice/src/environment.test.ts +32 -0
  70. package/templates/turborepo/apps/backoffice/src/environment.ts +27 -0
  71. package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.json +969 -0
  72. package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.ts +134 -0
  73. package/templates/turborepo/apps/backoffice/src/migrations/index.ts +9 -0
  74. package/templates/turborepo/apps/backoffice/src/payload-types.ts +339 -0
  75. package/templates/turborepo/apps/backoffice/src/payload.config.test.ts +31 -0
  76. package/templates/turborepo/apps/backoffice/src/payload.config.ts +50 -0
  77. package/templates/turborepo/apps/backoffice/test/home.e2e.test.ts +16 -0
  78. package/templates/turborepo/apps/backoffice/tsconfig.json +34 -0
  79. package/templates/turborepo/apps/backoffice/vitest.config.ts +19 -0
  80. package/templates/turborepo/eslint.config.ts +40 -0
  81. package/templates/turborepo/knip.json +9 -0
  82. package/templates/turborepo/package.json +70 -0
  83. package/templates/turborepo/pnpm-workspace.yaml +46 -0
  84. package/templates/turborepo/prettier.config.ts +11 -0
  85. package/templates/turborepo/tsconfig.json +16 -0
  86. package/templates/turborepo/turbo.json +45 -0
@@ -0,0 +1,300 @@
1
+ name: Verify
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
6
+ push:
7
+ branches:
8
+ - main
9
+ schedule:
10
+ - cron: "0 3 * * 1"
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ changes:
21
+ name: Detect Changes
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 5
24
+ permissions:
25
+ contents: read
26
+ pull-requests: read # Needed by paths-filter to inspect pull request changes.
27
+ outputs:
28
+ heavy: ${{ steps.filter.outputs.heavy }}
29
+ ci_policy: ${{ steps.filter.outputs.ci_policy }}
30
+ full_verification: ${{ steps.mode.outputs.full_verification }}
31
+ steps:
32
+ - name: Checkout repository
33
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
34
+ with:
35
+ persist-credentials: false
36
+ - name: Detect relevant changes
37
+ id: filter
38
+ if: github.event_name == 'pull_request'
39
+ uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
40
+ with:
41
+ predicate-quantifier: some-with-excludes
42
+ filters: .github/file-filters.yml
43
+ - name: Select verification mode
44
+ id: mode
45
+ shell: bash
46
+ env:
47
+ EVENT_NAME: ${{ github.event_name }}
48
+ DRAFT: ${{ github.event.pull_request.draft }}
49
+ HEAVY: ${{ steps.filter.outputs.heavy }}
50
+ CI_POLICY: ${{ steps.filter.outputs.ci_policy }}
51
+ run: |
52
+ full_verification=false
53
+ if [[ "$EVENT_NAME" == "push" || ( "$EVENT_NAME" == "pull_request" && "$DRAFT" != "true" && ( "$HEAVY" == "true" || "$CI_POLICY" == "true" ) ) ]]; then
54
+ full_verification=true
55
+ fi
56
+
57
+ echo "full_verification=$full_verification" >> "$GITHUB_OUTPUT"
58
+
59
+ checks:
60
+ name: Static Checks, Tests & Build
61
+ needs: changes
62
+ if: needs.changes.outputs.full_verification == 'true'
63
+ runs-on: ubuntu-latest
64
+ timeout-minutes: 20
65
+ permissions:
66
+ contents: read
67
+ id-token: write # Needed to exchange GitHub OIDC for a Turborepo Remote Cache token.
68
+ services:
69
+ postgres:
70
+ image: postgres:17.11-alpine@sha256:266d3c257594e195b309ae9d1805e54c788f1d7b375f56bf6c367aac6c917e7b
71
+ env:
72
+ POSTGRES_PASSWORD: postgres
73
+ POSTGRES_DB: payload
74
+ ports:
75
+ - 5432:5432
76
+ options: >-
77
+ --health-cmd "pg_isready --username postgres"
78
+ --health-interval 5s
79
+ --health-timeout 5s
80
+ --health-retries 10
81
+ env:
82
+ DATABASE_URL: postgres://postgres:postgres@localhost:5432/payload
83
+ PAYLOAD_SECRET: ci-only-secret
84
+ APP_URL: http://localhost:3001
85
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
86
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
87
+ steps:
88
+ - name: Checkout repository
89
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
90
+ with:
91
+ fetch-depth: 0
92
+ persist-credentials: false
93
+ - name: Fetch the pull request base
94
+ if: github.event_name == 'pull_request'
95
+ env:
96
+ BASE_REF: ${{ github.base_ref }}
97
+ run: git fetch --no-tags origin "$BASE_REF"
98
+ - name: Set up Node.js and pnpm
99
+ uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
100
+ with:
101
+ cache: true
102
+ require-lockfile: true
103
+ - name: Set up Turborepo Remote Cache
104
+ if: vars.TURBO_TEAM != ''
105
+ uses: vercel/setup-turborepo-remote-cache-action@49d7b1b46ba4c9251e1977986bfe18336feabc8f # v1.1.0
106
+ with:
107
+ team: ${{ vars.TURBO_TEAM }}
108
+ - name: Scope Turborepo to affected packages
109
+ if: github.event_name == 'pull_request'
110
+ env:
111
+ BASE_REF: ${{ github.base_ref }}
112
+ run: |
113
+ echo "TURBO_FILTER=--affected" >> "$GITHUB_ENV"
114
+ echo "TURBO_SCM_BASE=origin/$BASE_REF" >> "$GITHUB_ENV"
115
+ - name: Reject suppression directives
116
+ run: pnpm suppressions:check
117
+ - name: Check Cursor rules match .agents/rules
118
+ run: pnpm rules:check
119
+ - name: Check agent gates
120
+ run: pnpm gates:check
121
+ - name: Check lint
122
+ run: pnpm lint:check
123
+ - name: Check formatting
124
+ run: pnpm format:check
125
+ env:
126
+ GITHUB_TOKEN: ${{ github.token }} # editorconfig-checker downloads its binary from GitHub releases; the token avoids the anonymous rate limit.
127
+ - name: Check package.json ordering
128
+ run: pnpm sort-package-json:check
129
+ - name: Validate peer dependency contracts
130
+ run: pnpm peers:check
131
+ - name: Check types
132
+ run: pnpm typecheck
133
+ - name: Run Knip
134
+ run: pnpm knip
135
+ - name: Check dependency graph
136
+ run: pnpm depcruise
137
+ - name: Run tests with coverage
138
+ run: pnpm test:coverage
139
+ - name: Build
140
+ run: pnpm build
141
+ env:
142
+ NEXT_TELEMETRY_DISABLED: "1"
143
+ - name: Install Playwright browsers
144
+ run: |
145
+ if [[ -n "${TURBO_FILTER-}" ]]; then
146
+ mapfile -t apps < <(bash .github/scripts/affected-apps.sh "$TURBO_SCM_BASE" HEAD)
147
+ else
148
+ mapfile -t apps < <(bash .github/scripts/affected-apps.sh)
149
+ fi
150
+ for app in "${apps[@]}"; do
151
+ pnpm --filter "./apps/$app" exec playwright install --with-deps chromium
152
+ done
153
+ - name: Run end-to-end tests
154
+ run: pnpm test:e2e
155
+ env:
156
+ NEXT_TELEMETRY_DISABLED: "1"
157
+ - name: Check patch coverage
158
+ if: github.event_name == 'pull_request'
159
+ env:
160
+ BASE_REF: ${{ github.base_ref }}
161
+ run: |
162
+ python3 -m venv "$RUNNER_TEMP/diff-cover"
163
+ "$RUNNER_TEMP/diff-cover/bin/pip" install --quiet --require-hashes --only-binary=:all: -r .github/requirements/diff-cover.txt
164
+ if [[ -f apps/app/coverage/cobertura-coverage.xml ]]; then
165
+ "$RUNNER_TEMP/diff-cover/bin/diff-cover" apps/app/coverage/cobertura-coverage.xml --compare-branch "origin/$BASE_REF" --fail-under 95 --branch-coverage
166
+ fi
167
+ if [[ -f apps/backoffice/coverage/cobertura-coverage.xml ]]; then
168
+ "$RUNNER_TEMP/diff-cover/bin/diff-cover" apps/backoffice/coverage/cobertura-coverage.xml --compare-branch "origin/$BASE_REF" --fail-under 95 --branch-coverage
169
+ fi
170
+
171
+ image:
172
+ name: Container Image
173
+ needs: changes
174
+ if: needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule'
175
+ runs-on: ubuntu-latest
176
+ timeout-minutes: 20
177
+ env:
178
+ HADOLINT_VERSION: 2.15.1
179
+ HADOLINT_SHA256: c7187db94eeeeca956519a6af171adc31453941a1e777961f6e680f697c8c507
180
+ TRIVY_VERSION: 0.76.1
181
+ TRIVY_SHA256: c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
182
+ steps:
183
+ - name: Checkout repository
184
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
185
+ with:
186
+ fetch-depth: 0
187
+ persist-credentials: false
188
+ - name: Fetch the pull request base
189
+ if: github.event_name == 'pull_request'
190
+ env:
191
+ BASE_REF: ${{ github.base_ref }}
192
+ run: git fetch --no-tags origin "$BASE_REF"
193
+ - name: Set up Node.js and pnpm
194
+ uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
195
+ with:
196
+ cache: true
197
+ require-lockfile: true
198
+ - name: List affected apps
199
+ id: affected
200
+ env:
201
+ EVENT_NAME: ${{ github.event_name }}
202
+ BASE_REF: ${{ github.base_ref }}
203
+ run: |
204
+ if [[ "$EVENT_NAME" == "pull_request" ]]; then
205
+ mapfile -t apps < <(bash .github/scripts/affected-apps.sh "origin/$BASE_REF" HEAD)
206
+ else
207
+ mapfile -t apps < <(bash .github/scripts/affected-apps.sh)
208
+ fi
209
+ {
210
+ echo "app=false"
211
+ echo "backoffice=false"
212
+ for app in "${apps[@]}"; do
213
+ echo "$app=true"
214
+ done
215
+ } >> "$GITHUB_OUTPUT"
216
+ - name: Lint Dockerfile
217
+ run: |
218
+ curl --silent --show-error --fail --location --output hadolint \
219
+ "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-linux-x86_64"
220
+ echo "${HADOLINT_SHA256} hadolint" | sha256sum --check --strict
221
+ chmod +x hadolint
222
+ files=()
223
+ if [[ "${{ steps.affected.outputs.app }}" == "true" ]]; then
224
+ files+=(apps/app/Dockerfile)
225
+ fi
226
+ if [[ "${{ steps.affected.outputs.backoffice }}" == "true" ]]; then
227
+ files+=(apps/backoffice/Dockerfile)
228
+ fi
229
+ if [[ "${#files[@]}" -gt 0 ]]; then
230
+ ./hadolint "${files[@]}"
231
+ fi
232
+ - name: Build images
233
+ run: |
234
+ if [[ "${{ steps.affected.outputs.app }}" == "true" ]]; then
235
+ docker build --file apps/app/Dockerfile --tag app:ci .
236
+ fi
237
+ if [[ "${{ steps.affected.outputs.backoffice }}" == "true" ]]; then
238
+ docker build --file apps/backoffice/Dockerfile --tag backoffice:ci .
239
+ fi
240
+ - name: Smoke test images
241
+ id: smoke
242
+ run: |
243
+ if [[ "${{ steps.affected.outputs.app }}" == "true" ]]; then
244
+ docker run --detach --name app --env PORT=3000 --publish 3000:3000 app:ci
245
+ curl --silent --show-error --fail --retry 15 --retry-delay 1 --retry-all-errors --output /dev/null http://localhost:3000/health
246
+ fi
247
+ if [[ "${{ steps.affected.outputs.backoffice }}" == "true" ]]; then
248
+ docker run --detach --name backoffice --env PORT=3001 --env DATABASE_URL=postgres://build-only@localhost/build-only --env PAYLOAD_SECRET=ci-only-secret --env APP_URL=http://localhost:3001 --publish 3001:3001 backoffice:ci
249
+ curl --silent --show-error --fail --retry 15 --retry-delay 1 --retry-all-errors --output /dev/null http://localhost:3001/health
250
+ fi
251
+ - name: Show container logs
252
+ if: failure() && steps.smoke.outcome == 'failure'
253
+ run: |
254
+ if docker container inspect app >/dev/null 2>&1; then
255
+ docker logs app
256
+ fi
257
+ if docker container inspect backoffice >/dev/null 2>&1; then
258
+ docker logs backoffice
259
+ fi
260
+ - name: Scan images for vulnerabilities
261
+ run: |
262
+ archive="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
263
+ curl --silent --show-error --fail --location --output "$archive" \
264
+ "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${archive}"
265
+ echo "${TRIVY_SHA256} ${archive}" | sha256sum --check --strict
266
+ tar --extract --gzip --file "$archive" trivy
267
+ if [[ "${{ steps.affected.outputs.app }}" == "true" ]]; then
268
+ ./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:ci
269
+ fi
270
+ if [[ "${{ steps.affected.outputs.backoffice }}" == "true" ]]; then
271
+ ./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress backoffice:ci
272
+ fi
273
+
274
+ deploy-dev:
275
+ name: Deploy to dev
276
+ needs:
277
+ - checks
278
+ - image
279
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.DEPLOY_ENABLED == 'true'
280
+ permissions:
281
+ contents: read
282
+ actions: read # Needed by deploy.yml to find the CI run of the deployed commit.
283
+ uses: ./.github/workflows/deploy.yml
284
+ with:
285
+ environment: dev
286
+ commit_sha: ${{ github.sha }}
287
+
288
+ ci:
289
+ name: All Checks
290
+ if: always()
291
+ timeout-minutes: 5
292
+ needs:
293
+ - changes
294
+ - checks
295
+ - image
296
+ runs-on: ubuntu-latest
297
+ steps:
298
+ - name: Require all verification jobs
299
+ if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
300
+ run: exit 1
@@ -0,0 +1,200 @@
1
+ name: Deploy
2
+ run-name: Deploy ${{ inputs.commit_sha }} to ${{ inputs.environment }}
3
+
4
+ on:
5
+ workflow_call:
6
+ inputs:
7
+ environment:
8
+ required: true
9
+ type: string
10
+ commit_sha:
11
+ required: true
12
+ type: string
13
+ workflow_dispatch:
14
+ inputs:
15
+ environment:
16
+ description: Railway environment to promote to
17
+ required: true
18
+ type: choice
19
+ options:
20
+ - uat
21
+ - prod
22
+ commit_sha:
23
+ description: Full commit SHA on main that passed CI
24
+ required: true
25
+ type: string
26
+
27
+ permissions: {}
28
+
29
+ concurrency:
30
+ group: deploy-${{ inputs.environment }}
31
+ cancel-in-progress: false
32
+
33
+ jobs:
34
+ verify:
35
+ name: Verify the requested commit
36
+ runs-on: ubuntu-latest
37
+ timeout-minutes: 5
38
+ permissions:
39
+ contents: read
40
+ actions: read # Needed to find the CI and deploy runs of the requested commit.
41
+ env:
42
+ COMMIT_SHA: ${{ inputs.commit_sha }}
43
+ TARGET: ${{ inputs.environment }}
44
+ steps:
45
+ - name: Checkout the requested revision
46
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
47
+ with:
48
+ ref: ${{ inputs.commit_sha }}
49
+ fetch-depth: 0
50
+ persist-credentials: false
51
+ - name: Require the commit on main with passing CI and a deployment to the previous environment
52
+ if: github.event_name == 'workflow_dispatch'
53
+ env:
54
+ GH_TOKEN: ${{ github.token }}
55
+ REPOSITORY: ${{ github.repository }}
56
+ run: |
57
+ [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Expected a full lowercase commit SHA"; exit 1; }
58
+ git merge-base --is-ancestor "$COMMIT_SHA" origin/main || { echo "::error::$COMMIT_SHA is not on main"; exit 1; }
59
+ ci_run="$(gh api "repos/$REPOSITORY/actions/workflows/ci.yml/runs?head_sha=$COMMIT_SHA&event=push&branch=main&status=success" --jq '.workflow_runs[0].id // empty')"
60
+ [[ -n "$ci_run" ]] || { echo "::error::No successful CI run on main for $COMMIT_SHA"; exit 1; }
61
+ case "$TARGET" in
62
+ uat)
63
+ previous=dev
64
+ deployed="$(gh api "repos/$REPOSITORY/actions/runs/$ci_run/jobs" --jq '[.jobs[] | select(.name == "Deploy to dev / Deploy to dev" and .conclusion == "success")] | length')"
65
+ ;;
66
+ prod)
67
+ previous=uat
68
+ export EXPECTED_TITLE="Deploy $COMMIT_SHA to uat"
69
+ deployed="$(gh api --paginate "repos/$REPOSITORY/actions/workflows/deploy.yml/runs?event=workflow_dispatch&status=success&per_page=100" --jq '.workflow_runs[] | select(.display_title == env.EXPECTED_TITLE) | .id' | wc -l)"
70
+ ;;
71
+ esac
72
+ [[ "$deployed" -gt 0 ]] || { echo "::error::$COMMIT_SHA has no successful $previous deployment; promote through $previous first"; exit 1; }
73
+
74
+ deploy:
75
+ name: Deploy to ${{ inputs.environment }}
76
+ needs: verify
77
+ runs-on: ubuntu-latest
78
+ timeout-minutes: 40
79
+ environment:
80
+ name: ${{ inputs.environment }}
81
+ url: ${{ vars.APP_URL }}
82
+ permissions:
83
+ contents: read
84
+ actions: read # Needed to find the last successful deploy of this environment.
85
+ env:
86
+ APP_URL: ${{ vars.APP_URL }}
87
+ BACKOFFICE_URL: ${{ vars.BACKOFFICE_URL }}
88
+ RAILWAY_CLI_SHA256: 2e682fd2ddf8d6fdf9e46d9d99d99646ff0022a6abb0c2bca93011348a269fb9
89
+ RAILWAY_CLI_VERSION: 5.71.0
90
+ RAILWAY_ENVIRONMENT: ${{ inputs.environment }}
91
+ RAILWAY_SERVICE_APP: ${{ vars.RAILWAY_SERVICE_APP }}
92
+ RAILWAY_SERVICE_BACKOFFICE: ${{ vars.RAILWAY_SERVICE_BACKOFFICE }}
93
+ steps:
94
+ - name: Require environment settings
95
+ env:
96
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
97
+ run: |
98
+ [[ -n "$RAILWAY_TOKEN" ]] || { echo "::error::Set the RAILWAY_TOKEN secret in the $RAILWAY_ENVIRONMENT environment"; exit 1; }
99
+ [[ -n "$RAILWAY_SERVICE_APP" ]] || { echo "::error::Set the RAILWAY_SERVICE_APP variable in the $RAILWAY_ENVIRONMENT environment"; exit 1; }
100
+ [[ -n "$RAILWAY_SERVICE_BACKOFFICE" ]] || { echo "::error::Set the RAILWAY_SERVICE_BACKOFFICE variable in the $RAILWAY_ENVIRONMENT environment"; exit 1; }
101
+ [[ "$APP_URL" == https://* ]] || { echo "::error::Set the APP_URL variable to an https URL in the $RAILWAY_ENVIRONMENT environment"; exit 1; }
102
+ [[ "$BACKOFFICE_URL" == https://* ]] || { echo "::error::Set the BACKOFFICE_URL variable to an https URL in the $RAILWAY_ENVIRONMENT environment"; exit 1; }
103
+ - name: Checkout the requested revision
104
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
105
+ with:
106
+ ref: ${{ inputs.commit_sha }}
107
+ fetch-depth: 0
108
+ persist-credentials: false
109
+ - name: Set up Node.js and pnpm
110
+ uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
111
+ with:
112
+ cache: true
113
+ require-lockfile: true
114
+ - name: List affected apps
115
+ id: affected
116
+ env:
117
+ COMMIT_SHA: ${{ inputs.commit_sha }}
118
+ GH_TOKEN: ${{ github.token }}
119
+ RAILWAY_ENVIRONMENT: ${{ inputs.environment }}
120
+ REPOSITORY: ${{ github.repository }}
121
+ run: |
122
+ previous=""
123
+ latest=""
124
+ while IFS=$'\t' read -r run_id title; do
125
+ [[ -z "$run_id" ]] && continue
126
+ if [[ "$title" =~ ^Deploy\ ([0-9a-f]{40})\ to\ ${RAILWAY_ENVIRONMENT}$ ]]; then
127
+ candidate="${BASH_REMATCH[1]}"
128
+ else
129
+ continue
130
+ fi
131
+ [[ "$candidate" == "$COMMIT_SHA" ]] && continue
132
+ completed="$(gh api "repos/$REPOSITORY/actions/runs/$run_id/jobs" | jq -r --arg env "$RAILWAY_ENVIRONMENT" '[.jobs[] | select(.conclusion == "success" and (.name == "Deploy to \($env)" or .name == "Deploy to \($env) / Deploy to \($env)")) | .completed_at] | max // empty')"
133
+ if [[ -n "$completed" && ( -z "$latest" || "$completed" > "$latest" ) ]]; then
134
+ latest="$completed"
135
+ previous="$candidate"
136
+ fi
137
+ done < <(gh api --paginate "repos/$REPOSITORY/actions/workflows/deploy.yml/runs?status=success&per_page=50" --jq '.workflow_runs[] | [.id, .display_title] | @tsv')
138
+ while IFS=$'\t' read -r run_id head_sha; do
139
+ [[ -z "$run_id" || "$head_sha" == "$COMMIT_SHA" ]] && continue
140
+ completed="$(gh api "repos/$REPOSITORY/actions/runs/$run_id/jobs" | jq -r --arg env "$RAILWAY_ENVIRONMENT" '[.jobs[] | select(.conclusion == "success" and .name == "Deploy to \($env) / Deploy to \($env)") | .completed_at] | max // empty')"
141
+ if [[ -n "$completed" && ( -z "$latest" || "$completed" > "$latest" ) ]]; then
142
+ latest="$completed"
143
+ previous="$head_sha"
144
+ fi
145
+ done < <(gh api "repos/$REPOSITORY/actions/workflows/ci.yml/runs?event=push&branch=main&status=success&per_page=50" --jq '.workflow_runs[] | [.id, .head_sha] | @tsv')
146
+ if [[ -n "$previous" ]]; then
147
+ git cat-file -e "$previous^{commit}" 2>/dev/null || git fetch --no-tags origin "$previous"
148
+ mapfile -t apps < <(bash .github/scripts/affected-apps.sh "$previous" "$COMMIT_SHA")
149
+ else
150
+ mapfile -t apps < <(bash .github/scripts/affected-apps.sh)
151
+ fi
152
+ {
153
+ echo "app=false"
154
+ echo "backoffice=false"
155
+ for app in "${apps[@]}"; do
156
+ echo "$app=true"
157
+ done
158
+ } >> "$GITHUB_OUTPUT"
159
+ - name: Install verified Railway CLI
160
+ run: |
161
+ archive="railway-v${RAILWAY_CLI_VERSION}-x86_64-unknown-linux-musl.tar.gz"
162
+ curl --silent --show-error --fail --location --retry 3 --output "$RUNNER_TEMP/$archive" \
163
+ "https://github.com/railwayapp/cli/releases/download/v${RAILWAY_CLI_VERSION}/${archive}"
164
+ echo "${RAILWAY_CLI_SHA256} $RUNNER_TEMP/$archive" | sha256sum --check --strict
165
+ tar --extract --gzip --file "$RUNNER_TEMP/$archive" --directory "$RUNNER_TEMP"
166
+ echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
167
+ - name: Deploy
168
+ env:
169
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
170
+ DEPLOY_APP: ${{ steps.affected.outputs.app }}
171
+ DEPLOY_BACKOFFICE: ${{ steps.affected.outputs.backoffice }}
172
+ run: |
173
+ if [[ "$DEPLOY_APP" == "true" ]]; then
174
+ railway up --ci --service "$RAILWAY_SERVICE_APP" --environment "$RAILWAY_ENVIRONMENT"
175
+ fi
176
+ if [[ "$DEPLOY_BACKOFFICE" == "true" ]]; then
177
+ railway up --ci --service "$RAILWAY_SERVICE_BACKOFFICE" --environment "$RAILWAY_ENVIRONMENT"
178
+ fi
179
+ - name: Require a healthy deployment
180
+ env:
181
+ DEPLOY_APP: ${{ steps.affected.outputs.app }}
182
+ DEPLOY_BACKOFFICE: ${{ steps.affected.outputs.backoffice }}
183
+ run: |
184
+ wait_health() {
185
+ url="$1"
186
+ for _ in $(seq 30); do
187
+ if curl --silent --fail --output /dev/null "$url/health"; then
188
+ return 0
189
+ fi
190
+ sleep 10
191
+ done
192
+ echo "::error::$url/health did not respond"
193
+ return 1
194
+ }
195
+ if [[ "$DEPLOY_APP" == "true" ]]; then
196
+ wait_health "$APP_URL"
197
+ fi
198
+ if [[ "$DEPLOY_BACKOFFICE" == "true" ]]; then
199
+ wait_health "$BACKOFFICE_URL"
200
+ fi
@@ -0,0 +1,46 @@
1
+ name: SAST
2
+
3
+ on:
4
+ pull_request: {}
5
+ push:
6
+ branches:
7
+ - main
8
+ paths-ignore:
9
+ - "**/*.md"
10
+ - ".agents/**"
11
+ - ".claude/**"
12
+ - ".cursor/**"
13
+ schedule:
14
+ - cron: "17 2 * * 1"
15
+
16
+ permissions: {}
17
+
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ semgrep:
24
+ name: Semgrep (OWASP)
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 15
27
+ permissions:
28
+ contents: read
29
+ container:
30
+ image: semgrep/semgrep:1.179.0@sha256:93963d9295a366f59e4850127b1550400ee7b388f04fe144e4a1f6325d96e01b
31
+ steps:
32
+ - name: Checkout repository
33
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
34
+ with:
35
+ persist-credentials: false
36
+ - name: Scan source
37
+ run: >-
38
+ semgrep scan
39
+ --config p/owasp-top-ten
40
+ --config p/typescript
41
+ --config p/nodejsscan
42
+ --config p/react
43
+ --severity ERROR --severity WARNING
44
+ --error --metrics=off
45
+ --timeout 60 --timeout-threshold 0
46
+ .
@@ -0,0 +1,11 @@
1
+ {
2
+ "*.{ts,tsx,js,jsx,mjs,cjs}": [
3
+ "pnpm exec eslint --fix --cache --max-warnings=0 --no-warn-ignored --no-inline-config",
4
+ "prettier --write --cache"
5
+ ],
6
+ "!(package).json": ["prettier --write --cache"],
7
+ "*.{css,scss,md,yml,yaml}": ["prettier --write --cache"],
8
+ "package.json": ["sort-package-json", "prettier --write --cache"],
9
+ ".husky/{commit-msg,pre-commit,pre-merge-commit,pre-push}": ["prettier --write --cache"],
10
+ "*.sh": ["prettier --write --cache"]
11
+ }
@@ -0,0 +1,5 @@
1
+ .husky/_
2
+ pnpm-lock.yaml
3
+ apps/backoffice/src/app/(payload)/
4
+ apps/backoffice/src/migrations/
5
+ apps/backoffice/src/payload-types.ts
@@ -0,0 +1,20 @@
1
+ # Agent guide
2
+
3
+ Turborepo workspace on pnpm. `apps/app` is the Next.js site. `apps/backoffice` is the Payload CMS. Root scripts run the same checks as the other Next Friday templates and delegate framework commands with Turborepo.
4
+
5
+ - Follow the rules in `.agents/rules/`; `.agents/rules.json` gives each rule's scope. Claude Code and Cursor load them automatically.
6
+ - Rule files, agent hooks, and agent settings are company policy: propose a change to the user, who edits them.
7
+ - Put unit tests next to the code as `*.test.ts`. Put Playwright end-to-end tests in `apps/app/test/` and `apps/backoffice/test/` as `*.e2e.test.ts`. Component tests need a DOM test environment, which is not set up yet; ask the user before adding one.
8
+ - The Next.js site lives in `apps/app/app/`. The backoffice site lives in `apps/backoffice/src/app/(frontend)/`, collections in `apps/backoffice/src/collections/`, and the Payload config in `apps/backoffice/src/payload.config.ts`.
9
+ - Payload generates `apps/backoffice/src/app/(payload)/`, `apps/backoffice/src/payload-types.ts`, and `apps/backoffice/src/migrations/`. After changing a collection, regenerate them with `pnpm generate:types`, `pnpm generate:importmap`, and `pnpm migrate:create <name>`, and commit the results.
10
+ - Read backoffice configuration through `requireEnvironment` in `apps/backoffice/src/environment.ts`. Document each variable in `.env.example`.
11
+
12
+ <!-- BEGIN:nextjs-agent-rules -->
13
+
14
+ ## This is NOT the Next.js you know
15
+
16
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
17
+
18
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
19
+
20
+ <!-- END:nextjs-agent-rules -->
@@ -0,0 +1,58 @@
1
+ # Contributing
2
+
3
+ Use this guide to prepare changes to this repository.
4
+
5
+ ## Prerequisites
6
+
7
+ - Use the Node.js version declared by `.nvmrc` and `package.json#engines`.
8
+ - Change the Node.js version in `.nvmrc` and both Dockerfiles together. Dependabot updates only the Dockerfiles.
9
+ - Install pnpm once with `npm install --global pnpm`. It switches to the version declared by `package.json#packageManager` automatically.
10
+ - Use Claude Code v2.1.295 or later. It reads `AGENTS.md` directly and supports the `onFailure` field the agent hooks use.
11
+ - Install gitleaks 8.19 or later, which the `pre-commit` hook runs, and jq, which the agent hooks and the `pre-push` hook (`pnpm gates:check`) need: `brew install gitleaks jq` on macOS, or install them from each project's releases on Linux.
12
+
13
+ From the repository root:
14
+
15
+ ```sh
16
+ npm install --global pnpm
17
+ pnpm install
18
+ ```
19
+
20
+ ## Quality standards
21
+
22
+ - Fix lint, type, and test failures at their root cause.
23
+ - Put unit tests next to the code as `*.test.ts` and Playwright end-to-end tests in each app's `test/` as `*.e2e.test.ts`.
24
+ - Change the database schema only through collections, then run `pnpm generate:types`, `pnpm generate:importmap`, and `pnpm migrate:create <name>` and commit the generated files. Production applies pending migrations when the server starts.
25
+ - Treat `coverage/` as generated output.
26
+ - Do not commit `.env` files or credentials.
27
+
28
+ ## Verification
29
+
30
+ Run the narrowest check that covers the change while you work. Git hooks run these checks:
31
+
32
+ | Hook | Checks |
33
+ | :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `pre-commit` | blocks commits on `main` except the first commit of a new repository, gitleaks on staged changes, `pnpm rules:check`, and lint-staged on staged files |
35
+ | `commit-msg` | commitlint |
36
+ | `pre-merge-commit` | blocks merge commits on `main` |
37
+ | `pre-push` | blocks pushes to `main` except the push that creates it, then `pnpm check`: the static checks and the unit tests related to your changes |
38
+
39
+ - **Scans:** CI also runs coverage, the build, the container image, and the secret, SAST, and OSV scans. Dependabot and OSV-Scanner report vulnerable dependencies.
40
+ - **End-to-end tests:** run `pnpm test:e2e` when you change pages or the admin panel. It starts both development servers unless they are already running, and the backoffice needs `DATABASE_URL` and `PAYLOAD_SECRET` from `.env`. Run `pnpm --filter ./apps/app exec playwright install chromium` once before the first run. CI runs it against the production build and a PostgreSQL service on every pull request that is ready for review and changes code.
41
+ - **Coverage:** CI requires 95% branch coverage on changed lines in `apps/app/app/` and `apps/backoffice/src/`, except layouts, the Payload config, and generated files.
42
+
43
+ Run `pnpm verify` for repository changes.
44
+
45
+ Agent rules live in `.agents/rules/` with their scope in `.agents/rules.json`. After you change either, run `pnpm rules:sync` to regenerate `.claude/rules/` and `.cursor/rules/`.
46
+
47
+ ## Pull requests
48
+
49
+ We use trunk-based development. `main` is the only long-lived branch, and every deploy promotes a commit from `main` (see the Deploy section in the README).
50
+
51
+ - Branch from `main` and merge back within a day or two. Do not create `develop` or release branches.
52
+ - Merge unfinished work behind a feature flag or keep it unreachable instead of holding a long-lived branch.
53
+ - Pull requests are squash-merged, so the pull request title becomes the commit on `main`.
54
+ - Write commit messages in the format that `@next-friday/commitlint-config-friday` defines. Pull request titles use `<type>(<scope>): <subject>`, which `.github/workflows/validate-pr-title.yml` checks.
55
+ - Keep each pull request focused on one cohesive change.
56
+ - Describe observable page, collection, API, or configuration effects.
57
+ - Report only checks that actually ran and include their results.
58
+ - Add or update tests only when they protect a distinct observable contract.