ci: optimera pipelines + automatisk produktionssmoke #447
@@ -18,6 +18,14 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
# Never let two deploys race the same Coolify app. Unlike CI we do NOT cancel an
|
||||
# in-flight deploy (it may already be restarting the container); the newest
|
||||
# commit still wins because Coolify builds from the branch head.
|
||||
concurrency:
|
||||
group: deploy-0pt-main
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+11
-1
@@ -17,10 +17,16 @@ name: CI (SIAX Cloud)
|
||||
# push/PR/weekly scanner (this file is push/PR-only).
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
# One run per ref; a branch push that also updates a PR used to start two full
|
||||
# quality runs (and, on main, two deploy chains). Cancel the superseded one.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -30,6 +36,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- name: Enable corepack + pin pnpm@9 (pnpm-lock.yaml is lockfileVersion 9.0)
|
||||
run: |
|
||||
corepack enable
|
||||
@@ -126,6 +134,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- name: Enable corepack + pin pnpm@9
|
||||
run: |
|
||||
corepack enable
|
||||
|
||||
@@ -8,14 +8,21 @@ on:
|
||||
schedule:
|
||||
- cron: "0 4 * * *" # daily 04:00 UTC
|
||||
|
||||
concurrency:
|
||||
group: nightly-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- name: Enable corepack + pin pnpm@9
|
||||
run: |
|
||||
corepack enable
|
||||
@@ -34,6 +41,7 @@ jobs:
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Trivy
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Production smoke (0pt.app)
|
||||
|
||||
# Automated end-to-end delivery verification. Every 30 minutes (and on demand)
|
||||
# this checks that the live service is actually ready and that the running
|
||||
# container is the commit that main points at — so a green deploy is never
|
||||
# confused with a healthy, current service.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "*/30 * * * *"
|
||||
|
||||
concurrency:
|
||||
group: prod-smoke
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Readiness (retry on a fresh deploy)
|
||||
run: |
|
||||
set -eu
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
body="$(curl -fsS --max-time 15 https://0pt.app/api/ready || true)"
|
||||
echo "attempt $attempt: ${body:-<no response>}"
|
||||
if printf '%s' "$body" | grep -q '"ready":true'; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 20
|
||||
done
|
||||
echo "::error::0pt.app/api/ready never reported ready:true"
|
||||
exit 1
|
||||
|
||||
- name: Health envelope
|
||||
run: |
|
||||
set -eu
|
||||
body="$(curl -fsS --max-time 15 https://0pt.app/api/health)"
|
||||
echo "$body"
|
||||
printf '%s' "$body" | grep -q '"db":"ok"' || { echo "::error::database is not ok"; exit 1; }
|
||||
printf '%s' "$body" | grep -q '"stripe":"ok"' || { echo "::error::stripe is not ok"; exit 1; }
|
||||
|
||||
- name: Deployed commit equals main
|
||||
env:
|
||||
SIAX_CI_TOKEN: ${{ secrets.SIAX_CI_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
version="$(curl -fsS --max-time 15 https://0pt.app/api/version)"
|
||||
echo "$version"
|
||||
running="$(printf '%s' "$version" | sed -n 's/.*"commit":"\([0-9a-f]*\)".*/\1/p')"
|
||||
[ -n "$running" ] || { echo "::error::/api/version returned no commit"; exit 1; }
|
||||
if [ -z "${SIAX_CI_TOKEN:-}" ]; then
|
||||
echo "::warning::SIAX_CI_TOKEN is not set — cannot compare with main; commit $running is live"
|
||||
exit 0
|
||||
fi
|
||||
expected="$(curl -fsS --max-time 15 \
|
||||
-H "Authorization: token ${SIAX_CI_TOKEN}" \
|
||||
https://git.siax.io/api/v1/repos/admin/0pt/commits/main \
|
||||
| sed -n 's/.*"sha":"\([0-9a-f]*\)".*/\1/p')"
|
||||
echo "running=$running expected(main)=$expected"
|
||||
[ -n "$expected" ] || { echo "::error::could not read main SHA from Gitea"; exit 1; }
|
||||
[ "$running" = "$expected" ] || {
|
||||
echo "::error::production is $running but main is $expected — deploy did not land"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Document pipeline surface responds (auth required, never 404/5xx)
|
||||
run: |
|
||||
set -eu
|
||||
for path in /api/0pt/documents/upload /api/0pt/documents/structure /api/0pt/documents/models /api/0pt/credits/balance; do
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "https://0pt.app${path}")"
|
||||
echo "$path -> $code"
|
||||
case "$code" in
|
||||
401|405) ;; # unauthenticated or method guard: the route exists
|
||||
*) echo "::error::$path returned $code (expected 401/405)"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
@@ -39,6 +39,11 @@ on:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
IMAGE_REPO: git.cloud.siax.io/sax3l/0pt
|
||||
DOCKERFILE: Dockerfile
|
||||
|
||||
@@ -19,12 +19,16 @@ name: Security Scan (SIAX Cloud)
|
||||
# and docs/compliance/GOLDEN-STANDARD-REPORT.md control C6 for full context.
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 5 * * 1" # weekly Monday re-scan
|
||||
|
||||
|
||||
concurrency:
|
||||
group: security-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/version — deployment identity for automated verification.
|
||||
*
|
||||
* Returns only non-sensitive build identity (no env values, no config). The
|
||||
* scheduled prod-smoke workflow compares `commit` with the current main SHA so
|
||||
* "the container is up" is never confused with "the intended commit is live".
|
||||
*/
|
||||
export async function GET() {
|
||||
const commit =
|
||||
process.env.SOURCE_COMMIT ?? process.env.GIT_COMMIT_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? null;
|
||||
return NextResponse.json(
|
||||
{
|
||||
service: '0pt-web',
|
||||
commit,
|
||||
built_at: process.env.BUILD_TIME ?? null,
|
||||
},
|
||||
{ headers: { 'cache-control': 'no-store' } },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user