Add CD workflow: build-push, deploy Test, smoke suite, promote Int
Some checks failed
CD / Build and push images (push) Failing after 5s
CD / Deploy to Test (push) Has been skipped
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Failing after 7s
CI / Build container images (push) Has been skipped
Some checks failed
CD / Build and push images (push) Failing after 5s
CD / Deploy to Test (push) Has been skipped
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Failing after 7s
CI / Build container images (push) Has been skipped
On every push to main: build both images once (SHA + moving `test` tag), push to the Gitea registry, SSH-deploy the Test stage, wait for readiness, run the new Playwright smoke suite (SPA shell, web liveness, api healthz/readyz) against https://test.dorfteich.cloud, and on green retag the identical SHA images as `int` and deploy Int. The CI image-build job becomes PR-only to avoid double builds on main. Part of #8 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f870c3855d
commit
fb1422f56f
124
.gitea/workflows/cd.yml
Normal file
124
.gitea/workflows/cd.yml
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# CD: every push to main builds images once, deploys them to Test, runs
|
||||||
|
# the smoke suite against the live Test stage, and promotes the identical
|
||||||
|
# images to Int on success (ADR 0014). Prod deploys are a separate,
|
||||||
|
# manually gated release workflow (issue #89).
|
||||||
|
|
||||||
|
name: CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_BASE: gitea.101010.cloud/stwaidele/dorfteich
|
||||||
|
DEPLOY_HOST: 188.245.116.44
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-push:
|
||||||
|
name: Build and push images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Gitea registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.101010.cloud -u fable-5 --password-stdin
|
||||||
|
|
||||||
|
# Tags: the immutable SHA (promotion + rollback target) and the
|
||||||
|
# moving `test` tag that the Test stage's compose file pulls.
|
||||||
|
- name: Build and push web image
|
||||||
|
run: |
|
||||||
|
docker build -f apps/web/Dockerfile --build-arg APP_VERSION=${{ github.sha }} \
|
||||||
|
-t $IMAGE_BASE-web:${{ github.sha }} -t $IMAGE_BASE-web:test .
|
||||||
|
docker push $IMAGE_BASE-web:${{ github.sha }}
|
||||||
|
docker push $IMAGE_BASE-web:test
|
||||||
|
|
||||||
|
- name: Build and push api image
|
||||||
|
run: |
|
||||||
|
docker build -f apps/api/Dockerfile --build-arg APP_VERSION=${{ github.sha }} \
|
||||||
|
-t $IMAGE_BASE-api:${{ github.sha }} -t $IMAGE_BASE-api:test .
|
||||||
|
docker push $IMAGE_BASE-api:${{ github.sha }}
|
||||||
|
docker push $IMAGE_BASE-api:test
|
||||||
|
|
||||||
|
deploy-test:
|
||||||
|
name: Deploy to Test
|
||||||
|
needs: build-push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
|
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY_TEST }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
printf '%s\n' "${{ secrets.DEPLOY_HOST_KEY }}" > ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Pull and restart the Test stack
|
||||||
|
run: |
|
||||||
|
ssh deploy@$DEPLOY_HOST 'cd /home/DOCKER/dorfteich-test \
|
||||||
|
&& docker compose pull --quiet && docker compose up -d --remove-orphans \
|
||||||
|
&& docker compose ps'
|
||||||
|
|
||||||
|
smoke-test:
|
||||||
|
name: Smoke tests against Test
|
||||||
|
needs: deploy-test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 11
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright browser
|
||||||
|
run: pnpm --filter @dorfteich/web exec playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Wait for the Test stage to be ready
|
||||||
|
run: |
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' https://test.dorfteich.cloud/api/v1/readyz || true)
|
||||||
|
[ "$code" = "200" ] && exit 0
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "Test stage did not become ready" >&2; exit 1
|
||||||
|
|
||||||
|
- name: Run smoke suite
|
||||||
|
run: E2E_BASE_URL=https://test.dorfteich.cloud pnpm --filter @dorfteich/web exec playwright test
|
||||||
|
|
||||||
|
promote-int:
|
||||||
|
name: Promote to Int
|
||||||
|
needs: smoke-test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Log in to the Gitea registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.101010.cloud -u fable-5 --password-stdin
|
||||||
|
|
||||||
|
# Registry-side retag of the SHA images that just passed on Test —
|
||||||
|
# Int always runs bit-identical images, never a rebuild.
|
||||||
|
- name: Retag SHA images as :int
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t $IMAGE_BASE-web:int $IMAGE_BASE-web:${{ github.sha }}
|
||||||
|
docker buildx imagetools create -t $IMAGE_BASE-api:int $IMAGE_BASE-api:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
|
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY_INT }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
printf '%s\n' "${{ secrets.DEPLOY_HOST_KEY }}" > ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Pull and restart the Int stack
|
||||||
|
run: |
|
||||||
|
ssh deploy@$DEPLOY_HOST 'cd /home/DOCKER/dorfteich-int \
|
||||||
|
&& docker compose pull --quiet && docker compose up -d --remove-orphans \
|
||||||
|
&& docker compose ps'
|
||||||
@ -53,6 +53,9 @@ jobs:
|
|||||||
|
|
||||||
images:
|
images:
|
||||||
name: Build container images
|
name: Build container images
|
||||||
|
# PR-only: on main the CD workflow builds and pushes the same images —
|
||||||
|
# building twice would waste the runner (ADR 0014: build once, promote).
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
|
|||||||
25
apps/web/e2e/smoke.spec.ts
Normal file
25
apps/web/e2e/smoke.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
// Walking-skeleton smoke suite: proves the deployed stage serves the SPA,
|
||||||
|
// the reverse proxy routes /api, and the api is ready (db + migrations).
|
||||||
|
// Feature e2e packs (issues #20, #32, …) build on this file's patterns.
|
||||||
|
|
||||||
|
test('SPA shell loads', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page).toHaveTitle(/Dorfteich/);
|
||||||
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('web liveness endpoint responds', async ({ request }) => {
|
||||||
|
const res = await request.get('/healthz');
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('api is live and ready', async ({ request }) => {
|
||||||
|
const healthz = await request.get('/api/v1/healthz');
|
||||||
|
expect(healthz.ok()).toBeTruthy();
|
||||||
|
expect((await healthz.json()).status).toBe('ok');
|
||||||
|
|
||||||
|
const readyz = await request.get('/api/v1/readyz');
|
||||||
|
expect(readyz.ok(), `readyz: ${await readyz.text()}`).toBeTruthy();
|
||||||
|
});
|
||||||
@ -10,7 +10,8 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests"
|
"test": "vitest run --passWithNoTests",
|
||||||
|
"e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dorfteich/shared": "workspace:*",
|
"@dorfteich/shared": "workspace:*",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
"react-router-dom": "^7.1.0"
|
"react-router-dom": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
|||||||
13
apps/web/playwright.config.ts
Normal file
13
apps/web/playwright.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
// Smoke/e2e tests run against a deployed stage (CD pipeline) or a local
|
||||||
|
// stack. The target comes from E2E_BASE_URL; no dev server is started here.
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
timeout: 30_000,
|
||||||
|
retries: 1,
|
||||||
|
reporter: [['list']],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:5173',
|
||||||
|
},
|
||||||
|
});
|
||||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@ -130,6 +130,9 @@ importers:
|
|||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: ^1.61.1
|
||||||
|
version: 1.61.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.17
|
version: 19.2.17
|
||||||
@ -1063,6 +1066,11 @@ packages:
|
|||||||
'@pinojs/redact@0.4.0':
|
'@pinojs/redact@0.4.0':
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||||
|
|
||||||
|
'@playwright/test@1.61.1':
|
||||||
|
resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@prisma/client@6.19.3':
|
'@prisma/client@6.19.3':
|
||||||
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
@ -2211,6 +2219,11 @@ packages:
|
|||||||
fs-monkey@1.1.0:
|
fs-monkey@1.1.0:
|
||||||
resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==}
|
resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@ -2719,6 +2732,16 @@ packages:
|
|||||||
pkg-types@2.3.1:
|
pkg-types@2.3.1:
|
||||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||||
|
|
||||||
|
playwright-core@1.61.1:
|
||||||
|
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.61.1:
|
||||||
|
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pluralize@8.0.0:
|
pluralize@8.0.0:
|
||||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -4215,6 +4238,10 @@ snapshots:
|
|||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
|
|
||||||
|
'@playwright/test@1.61.1':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.61.1
|
||||||
|
|
||||||
'@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)':
|
'@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
prisma: 6.19.3(typescript@5.9.3)
|
prisma: 6.19.3(typescript@5.9.3)
|
||||||
@ -5453,6 +5480,9 @@ snapshots:
|
|||||||
|
|
||||||
fs-monkey@1.1.0: {}
|
fs-monkey@1.1.0: {}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -5917,6 +5947,14 @@ snapshots:
|
|||||||
exsolve: 1.1.0
|
exsolve: 1.1.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
playwright-core@1.61.1: {}
|
||||||
|
|
||||||
|
playwright@1.61.1:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.61.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
pluralize@8.0.0: {}
|
pluralize@8.0.0: {}
|
||||||
|
|
||||||
postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.16)(tsx@4.23.0):
|
postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.16)(tsx@4.23.0):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user