@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.
- package/README.md +16 -15
- package/dist/main.mjs +18 -7
- package/package.json +2 -2
- package/templates/base/pnpm-workspace.yaml +24 -2
- package/templates/payload/README.md +1 -1
- package/templates/payload/package.json +0 -1
- package/templates/payload/src/payload.config.test.ts +31 -0
- package/templates/turborepo/.agents/rules/typescript-first.md +7 -0
- package/templates/turborepo/.agents/rules.json +41 -0
- package/templates/turborepo/.claude/rules/deterministic-testing.md +21 -0
- package/templates/turborepo/.claude/rules/typescript-first.md +15 -0
- package/templates/turborepo/.cursor/rules/deterministic-testing.mdc +21 -0
- package/templates/turborepo/.cursor/rules/typescript-first.mdc +13 -0
- package/templates/turborepo/.dockerignore +12 -0
- package/templates/turborepo/.editorconfig-checker.json +3 -0
- package/templates/turborepo/.env.example +4 -0
- package/templates/turborepo/.github/ISSUE_TEMPLATE/bug.yml +66 -0
- package/templates/turborepo/.github/ISSUE_TEMPLATE/feature.yml +51 -0
- package/templates/turborepo/.github/dependabot.yml +79 -0
- package/templates/turborepo/.github/labeler.yml +25 -0
- package/templates/turborepo/.github/pull_request_template.md +25 -0
- package/templates/turborepo/.github/scripts/affected-apps.sh +31 -0
- package/templates/turborepo/.github/workflows/ci.yml +300 -0
- package/templates/turborepo/.github/workflows/deploy.yml +200 -0
- package/templates/turborepo/.github/workflows/sast.yml +46 -0
- package/templates/turborepo/.lintstagedrc.json +11 -0
- package/templates/turborepo/.prettierignore +5 -0
- package/templates/turborepo/AGENTS.md +20 -0
- package/templates/turborepo/CONTRIBUTING.md +58 -0
- package/templates/turborepo/README.md +114 -0
- package/templates/turborepo/_gitignore +53 -0
- package/templates/turborepo/apps/app/.dependency-cruiser.json +34 -0
- package/templates/turborepo/apps/app/Dockerfile +25 -0
- package/templates/turborepo/apps/app/app/favicon.ico +0 -0
- package/templates/turborepo/apps/app/app/globals.css +26 -0
- package/templates/turborepo/apps/app/app/health/route.test.ts +20 -0
- package/templates/turborepo/apps/app/app/health/route.ts +23 -0
- package/templates/turborepo/apps/app/app/layout.tsx +29 -0
- package/templates/turborepo/apps/app/app/page.test.ts +11 -0
- package/templates/turborepo/apps/app/app/page.tsx +8 -0
- package/templates/turborepo/apps/app/next.config.ts +40 -0
- package/templates/turborepo/apps/app/package.json +41 -0
- package/templates/turborepo/apps/app/playwright.config.ts +27 -0
- package/templates/turborepo/apps/app/postcss.config.mjs +7 -0
- package/templates/turborepo/apps/app/test/home.e2e.test.ts +11 -0
- package/templates/turborepo/apps/app/tsconfig.json +33 -0
- package/templates/turborepo/apps/app/vitest.config.ts +12 -0
- package/templates/turborepo/apps/backoffice/.dependency-cruiser.json +34 -0
- package/templates/turborepo/apps/backoffice/Dockerfile +28 -0
- package/templates/turborepo/apps/backoffice/next.config.ts +48 -0
- package/templates/turborepo/apps/backoffice/package.json +49 -0
- package/templates/turborepo/apps/backoffice/playwright.config.ts +27 -0
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/layout.tsx +18 -0
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.test.ts +14 -0
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.tsx +14 -0
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/styles.css +15 -0
- package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
- package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
- package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/importMap.ts +6 -0
- package/templates/turborepo/apps/backoffice/src/app/(payload)/api/[...slug]/route.ts +19 -0
- package/templates/turborepo/apps/backoffice/src/app/(payload)/custom.scss +0 -0
- package/templates/turborepo/apps/backoffice/src/app/(payload)/layout.tsx +31 -0
- package/templates/turborepo/apps/backoffice/src/app/health/route.test.ts +20 -0
- package/templates/turborepo/apps/backoffice/src/app/health/route.ts +23 -0
- package/templates/turborepo/apps/backoffice/src/collections/media.test.ts +27 -0
- package/templates/turborepo/apps/backoffice/src/collections/media.ts +18 -0
- package/templates/turborepo/apps/backoffice/src/collections/users.test.ts +10 -0
- package/templates/turborepo/apps/backoffice/src/collections/users.ts +10 -0
- package/templates/turborepo/apps/backoffice/src/environment.test.ts +32 -0
- package/templates/turborepo/apps/backoffice/src/environment.ts +27 -0
- package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.json +969 -0
- package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.ts +134 -0
- package/templates/turborepo/apps/backoffice/src/migrations/index.ts +9 -0
- package/templates/turborepo/apps/backoffice/src/payload-types.ts +339 -0
- package/templates/turborepo/apps/backoffice/src/payload.config.test.ts +31 -0
- package/templates/turborepo/apps/backoffice/src/payload.config.ts +50 -0
- package/templates/turborepo/apps/backoffice/test/home.e2e.test.ts +16 -0
- package/templates/turborepo/apps/backoffice/tsconfig.json +34 -0
- package/templates/turborepo/apps/backoffice/vitest.config.ts +19 -0
- package/templates/turborepo/eslint.config.ts +40 -0
- package/templates/turborepo/knip.json +9 -0
- package/templates/turborepo/package.json +70 -0
- package/templates/turborepo/pnpm-workspace.yaml +46 -0
- package/templates/turborepo/prettier.config.ts +11 -0
- package/templates/turborepo/tsconfig.json +16 -0
- 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,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.
|