Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 8 additions & 22 deletions .github/workflows/cd_dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,18 @@ on:
pull_request:
branches: main
jobs:
merge-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Merge with main
uses: devmasx/merge-branch@master
with:
type: now
from_branch: main
target_branch: ${{ github.head_ref }}
github_token: ${{ secrets.OPENAPI }}
message: Merge main into this branch to deploy to dev for testing.
test:
needs: merge-branch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Create .env from secrets
run: echo "${{ secrets.DEV_FULL_ENV }}" > .env
- name: Setup Node.js
uses: actions/setup-node@master
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Cache node modules
uses: actions/cache@master
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
Expand All @@ -38,14 +25,13 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install dependencies and run the test
run: |
npm install
npm run runtest
- name: Install dependencies
run: npm install
- name: Generate coverage report
run: npm run coverage:ci
deploy:
if: github.event.pull_request.draft == false
needs:
- merge-branch
- test
strategy:
matrix:
Expand All @@ -55,7 +41,7 @@ jobs:
- vlcdhp02
runs-on: ${{ matrix.machines }}
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Deploy the app on the server
run: |
if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/cd_prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Create .env from secrets
run: echo "${{ secrets.PROD_FULL_ENV }}" > .env
- name: Setup Node.js
uses: actions/setup-node@master
uses: actions/setup-node@v4
with:
node-version: "24"

# Speed up subsequent runs with caching
- name: Cache node modules
uses: actions/cache@master
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
Expand All @@ -27,10 +27,12 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install dependencies and run the test
run: |
npm install
npm run runtest
- name: Install dependencies
run: npm install
- name: Run tests
run: npm run test:ci
- name: Generate coverage report
run: npm run coverage:ci
deploy:
needs: test
strategy:
Expand All @@ -41,7 +43,7 @@ jobs:
- vlcdhprdp02
runs-on: ${{ matrix.machines }}
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Deploy the app on the server
run: |
if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then
Expand Down
78 changes: 57 additions & 21 deletions __tests__/core_provider_contract.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import { describe, it } from 'node:test'
import assert from 'node:assert'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

const here = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(here, "..")
const apiRoutesPath = path.join(repoRoot, "routes", "api-routes.js")
const contractPath = path.join(repoRoot, "contracts", "core-provider.openapi.yaml")
const repoRoot = path.resolve(here, '..')
const apiRoutesPath = path.join(repoRoot, 'routes', 'api-routes.js')
const contractPath = path.join(repoRoot, 'contracts', 'core-provider.openapi.yaml')

const skippedMountedRouters = new Set([
"./static.js",
"./compatability.js"
'./static.js',
'./compatability.js'
])

/**
* Normalize route paths from Express format to OpenAPI format
* /id/:id -> /id/{id}
*/
function normalizeRoutePath(routePath) {
return routePath.replace(/\/:([A-Za-z0-9_]+)/g, "/{id}")
return routePath
.replace(/\/:([A-Za-z0-9_]+)/g, '/{$1}')
.replace(/\{_?id\}/g, '{id}')
}

/**
* Join mounted prefix with route path
*/
function joinMountedPath(prefix, routePath) {
const suffix = routePath === "/" ? "" : routePath
return normalizeRoutePath(`${prefix}${suffix}`.replace(/\/+/g, "/"))
const suffix = routePath === '/' ? '' : routePath
return normalizeRoutePath(`${prefix}${suffix}`.replace(/\/+/g, '/'))
}

/**
* Parse ES6 import statements from source
*/
function parseImports(source) {
const imports = new Map()
const importPattern = /^import\s+(\w+)\s+from\s+'(\.\/[^']+)';?$/gm
Expand All @@ -30,6 +44,9 @@ function parseImports(source) {
return imports
}

/**
* Parse router.use() mounted subrouters
*/
function parseMountedRouters(source, imports) {
const mounted = []
const usePattern = /router\.use\('([^']+)',\s*(\w+)\)/g
Expand All @@ -40,14 +57,17 @@ function parseMountedRouters(source, imports) {
}
mounted.push({
prefix: match[1],
filePath: path.join(repoRoot, "routes", importPath.replace("./", ""))
filePath: path.join(repoRoot, 'routes', importPath.replace('./', ''))
})
}
return mounted
}

/**
* Parse route operations from a route file
*/
function parseRouteOperations(filePath, prefix) {
const source = fs.readFileSync(filePath, "utf8")
const source = fs.readFileSync(filePath, 'utf8')
const operations = new Set()
const routeBlockPattern = /router\.route\('([^']+)'\)([\s\S]*?)(?=\nrouter\.route\(|\nexport default)/g
for (const match of source.matchAll(routeBlockPattern)) {
Expand All @@ -63,19 +83,25 @@ function parseRouteOperations(filePath, prefix) {
return operations
}

/**
* Parse direct router.METHOD() operations
*/
function parseDirectOperations(source) {
const operations = new Set()
const directPattern = /router\.(get|post|put|patch|delete|head)\('([^']+)'/g
for (const match of source.matchAll(directPattern)) {
if (match[2] === "/api") {
if (match[2] === '/api') {
operations.add(`${match[1].toUpperCase()} ${match[2]}`)
}
}
return operations
}

/**
* Get all mounted core provider operations
*/
function getMountedCoreProviderOperations() {
const source = fs.readFileSync(apiRoutesPath, "utf8")
const source = fs.readFileSync(apiRoutesPath, 'utf8')
const imports = parseImports(source)
const operations = new Set(parseDirectOperations(source))
for (const mountedRouter of parseMountedRouters(source, imports)) {
Expand All @@ -86,10 +112,13 @@ function getMountedCoreProviderOperations() {
return Array.from(operations).sort()
}

/**
* Parse operations from OpenAPI contract
*/
function getContractOperations() {
const lines = fs.readFileSync(contractPath, "utf8").split("\n")
const lines = fs.readFileSync(contractPath, 'utf8').split('\n')
const operations = []
let currentPath = ""
let currentPath = ''
for (const line of lines) {
const pathMatch = line.match(/^ (\/[^:]+):\s*$/)
if (pathMatch) {
Expand All @@ -98,14 +127,21 @@ function getContractOperations() {
}
const methodMatch = line.match(/^ (get|post|put|patch|delete|head):\s*$/)
if (methodMatch && currentPath) {
operations.push(`${methodMatch[1].toUpperCase()} ${currentPath}`)
operations.push(`${methodMatch[1].toUpperCase()} ${normalizeRoutePath(currentPath)}`)
}
}
return operations.sort()
}

describe("core provider contract", () => {
it("matches the mounted core provider route surface", () => {
expect(getContractOperations()).toEqual(getMountedCoreProviderOperations())
describe('Core Provider Contract', () => {
it('Mounted routes match the core provider contract', () => {
const contractOps = getContractOperations()
const implementedOps = getMountedCoreProviderOperations()

assert.deepEqual(
implementedOps,
contractOps,
'Implemented routes do not match contract specification'
)
})
})
21 changes: 12 additions & 9 deletions __tests__/openapi_sync_artifacts.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
Expand All @@ -14,21 +16,22 @@ describe("Shared OpenAPI artifact sync scaffolding", () => {
const targetArtifact = fs.readFileSync(targetArtifactPath, "utf8")

for (const artifact of [providerArtifact, targetArtifact]) {
expect(artifact).toContain("openapi: 3.0.3")
expect(artifact).toContain("title: RERUM Shared Components")
expect(artifact).toContain("version: 0.1.0")
expect(artifact).toContain("components:")
expect(artifact).toContain("schemas: {}")
assert.match(artifact, /openapi: 3\.0\.3/)
assert.match(artifact, /title: RERUM Shared Components/)
assert.match(artifact, /version: 0\.1\.0/)
assert.match(artifact, /components:/)
assert.match(artifact, /schemas: \{\}/)
}
})

it("verifies the shared artifact sync workflow configuration", () => {
const workflowPath = path.join(repoRoot, ".github/workflows/sync-rerum-shared-openapi.yml")
const workflow = fs.readFileSync(workflowPath, "utf8")

expect(workflow).toContain("openapi/components/rerum-shared-components.openapi.yaml")
expect(workflow).toContain("sync-provider-artifact.yml")
expect(workflow).toContain("repo: 'rerum_openapi'")
expect(workflow).toContain("target_artifact_path: 'schemas/openapi/rerum-shared-components.openapi.yaml'")
assert.match(workflow, /openapi\/components\/rerum-shared-components\.openapi\.yaml/)
assert.match(workflow, /repository:\s*cubap\/rerum_openapi/)
assert.match(workflow, /path:\s*rerum_openapi/)
assert.match(workflow, /peter-evans\/create-pull-request@v7/)
assert.match(workflow, /schemas\/openapi\/rerum-shared-components\.openapi\.yaml/)
})
})
4 changes: 3 additions & 1 deletion __tests__/provider_sync_artifacts.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
Expand All @@ -11,6 +13,6 @@ describe("provider sync artifacts", () => {
const workflowPath = path.join(repoRoot, ".github", "workflows", "sync-core-provider-contract.yml")
const workflow = fs.readFileSync(workflowPath, "utf8")

expect(workflow).toContain("contracts/core-provider.openapi.yaml")
assert.match(workflow, /contracts\/core-provider\.openapi\.yaml/)
})
})
Loading
Loading