chore: crush git history - reborn from consolidation on 2026-03-10
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@5
|
||||
|
||||
jobs:
|
||||
contract-tests:
|
||||
docker:
|
||||
- image: cimg/node:20.0
|
||||
steps:
|
||||
- checkout
|
||||
- node/install-packages:
|
||||
pkg-manager: npm
|
||||
- restore_cache:
|
||||
keys:
|
||||
- apophis-cache-{{ .Branch }}
|
||||
- apophis-cache-main
|
||||
- run:
|
||||
name: Determine changed routes
|
||||
command: |
|
||||
# Extract changed routes from git diff
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD | \
|
||||
grep -E 'src/routes|src/schemas' | \
|
||||
sed 's|src/routes||; s|\.ts$||' | \
|
||||
paste -sd ',' -)
|
||||
echo "export APOPHIS_CHANGED_ROUTES=$CHANGED" >> $BASH_ENV
|
||||
- run:
|
||||
name: Run contract tests
|
||||
command: npm test
|
||||
environment:
|
||||
APOPHIS_LOG_LEVEL: info
|
||||
- save_cache:
|
||||
paths:
|
||||
- .apophis-cache.json
|
||||
key: apophis-cache-{{ .Branch }}-{{ epoch }}
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: test-results
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- contract-tests
|
||||
@@ -0,0 +1,164 @@
|
||||
import Fastify from 'fastify'
|
||||
import apophisPlugin from 'apophis-fastify'
|
||||
|
||||
const fastify = Fastify()
|
||||
|
||||
await fastify.register(apophisPlugin, {
|
||||
runtime: 'error', // Validate contracts on every request
|
||||
cleanup: true, // Auto-cleanup resources on exit
|
||||
})
|
||||
|
||||
// In-memory store for demo
|
||||
const users = new Map<string, { id: string; email: string; name: string }>()
|
||||
|
||||
// CREATE — constructor
|
||||
fastify.post('/users', {
|
||||
schema: {
|
||||
'x-category': 'constructor',
|
||||
'x-ensures': [
|
||||
'status:201',
|
||||
'response_body(this).id != null',
|
||||
'response_body(this).email == request_body(this).email',
|
||||
],
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
name: { type: 'string', minLength: 1 }
|
||||
},
|
||||
required: ['email', 'name']
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
name: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, async (req, reply) => {
|
||||
const id = `usr-${Date.now()}`
|
||||
const user = { id, email: req.body.email, name: req.body.name }
|
||||
users.set(id, user)
|
||||
reply.status(201)
|
||||
return user
|
||||
})
|
||||
|
||||
// READ — observer
|
||||
fastify.get('/users/:id', {
|
||||
schema: {
|
||||
'x-category': 'observer',
|
||||
'x-requires': ['users:id'],
|
||||
'x-ensures': [
|
||||
'status:200',
|
||||
'response_body(this).id == request_params(this).id',
|
||||
],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' }
|
||||
},
|
||||
required: ['id']
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
name: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, async (req) => {
|
||||
const user = users.get(req.params.id)
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
return user
|
||||
})
|
||||
|
||||
// UPDATE — mutator
|
||||
fastify.put('/users/:id', {
|
||||
schema: {
|
||||
'x-category': 'mutator',
|
||||
'x-requires': ['users:id'],
|
||||
'x-ensures': [
|
||||
'status:200',
|
||||
'response_body(this).id == request_params(this).id',
|
||||
],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' }
|
||||
},
|
||||
required: ['id']
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
name: { type: 'string', minLength: 1 }
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
name: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, async (req) => {
|
||||
const user = users.get(req.params.id)
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
const updated = {
|
||||
...user,
|
||||
email: req.body.email ?? user.email,
|
||||
name: req.body.name ?? user.name,
|
||||
}
|
||||
users.set(req.params.id, updated)
|
||||
return updated
|
||||
})
|
||||
|
||||
// DELETE — destructor
|
||||
fastify.delete('/users/:id', {
|
||||
schema: {
|
||||
'x-category': 'destructor',
|
||||
'x-requires': ['users:id'],
|
||||
'x-ensures': ['status:204'],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' }
|
||||
},
|
||||
required: ['id']
|
||||
}
|
||||
}
|
||||
}, async (req, reply) => {
|
||||
users.delete(req.params.id)
|
||||
reply.status(204)
|
||||
})
|
||||
|
||||
await fastify.ready()
|
||||
|
||||
// Run contract tests (all non-utility routes, property-based)
|
||||
const result = await fastify.apophis.contract({ depth: 'standard' })
|
||||
console.log('Contract tests:', result.summary)
|
||||
|
||||
// Run stateful tests (constructor→mutator→destructor sequences)
|
||||
const stateful = await fastify.apophis.stateful({ depth: 'standard', seed: 42 })
|
||||
console.log('Stateful tests:', stateful.summary)
|
||||
|
||||
// Validate a single route
|
||||
const check = await fastify.apophis.check('POST', '/users')
|
||||
console.log('POST /users check:', check.ok ? 'PASS' : 'FAIL')
|
||||
@@ -0,0 +1,63 @@
|
||||
name: API Contract Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Restore APOPHIS cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .apophis-cache.json
|
||||
key: apophis-cache-${{ github.ref }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
apophis-cache-${{ github.ref }}-
|
||||
apophis-cache-main-
|
||||
|
||||
- name: Determine changed routes
|
||||
id: changed
|
||||
run: |
|
||||
# Example: extract changed routes from git diff
|
||||
# Adjust this to match your project's structure
|
||||
CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | \
|
||||
grep -E 'src/routes|src/schemas' | \
|
||||
sed 's|src/routes||; s|\.ts$||' | \
|
||||
paste -sd ',' -)
|
||||
echo "routes=$CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run contract tests
|
||||
run: npm test
|
||||
env:
|
||||
APOPHIS_LOG_LEVEL: info
|
||||
APOPHIS_CHANGED_ROUTES: ${{ steps.changed.outputs.routes }}
|
||||
|
||||
- name: Save APOPHIS cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .apophis-cache.json
|
||||
key: apophis-cache-${{ github.ref }}-${{ github.sha }}
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
*.tap
|
||||
.apophis-cache.json
|
||||
@@ -0,0 +1,50 @@
|
||||
stages:
|
||||
- test
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
NODE_VERSION: "20"
|
||||
APOPHIS_LOG_LEVEL: "info"
|
||||
|
||||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- node_modules/
|
||||
- .apophis-cache.json
|
||||
|
||||
contract_tests:
|
||||
stage: test
|
||||
image: node:${NODE_VERSION}
|
||||
before_script:
|
||||
- npm ci
|
||||
script:
|
||||
# Determine changed routes from merge request diff
|
||||
- |
|
||||
if [ "$CI_MERGE_REQUEST_IID" != "" ]; then
|
||||
CHANGED=$(git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA $CI_COMMIT_SHA | \
|
||||
grep -E 'src/routes|src/schemas' | \
|
||||
sed 's|src/routes||; s|\.ts$||' | \
|
||||
paste -sd ',' -)
|
||||
export APOPHIS_CHANGED_ROUTES="$CHANGED"
|
||||
fi
|
||||
- npm test
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- .apophis-cache.json
|
||||
- "*.tap"
|
||||
reports:
|
||||
junit: junit.xml
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_IID
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
# Optional: Clear cache after deployment to main
|
||||
clear_cache:
|
||||
stage: deploy
|
||||
image: node:${NODE_VERSION}
|
||||
script:
|
||||
- rm -f .apophis-cache.json
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
when: on_success
|
||||
@@ -0,0 +1,26 @@
|
||||
import Fastify from 'fastify'
|
||||
import apophisPlugin from 'apophis-fastify'
|
||||
|
||||
const fastify = Fastify()
|
||||
|
||||
// APOPHIS auto-registers @fastify/swagger
|
||||
await fastify.register(apophisPlugin, {})
|
||||
|
||||
fastify.get('/health', {
|
||||
schema: {
|
||||
'x-category': 'observer',
|
||||
'x-ensures': ['status:200'],
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: { status: { type: 'string' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}, async () => ({ status: 'ok' }))
|
||||
|
||||
await fastify.ready()
|
||||
|
||||
// Run contract tests
|
||||
const result = await fastify.apophis.contract({ depth: 'quick' })
|
||||
console.log(result.summary)
|
||||
Reference in New Issue
Block a user