Run CI steps in parallel (#9760)

* Parallel CI

* Add build to integration test

* Add checkout to top of each run

* Revert branch update for ci job

* Experiment with --runInBand for CI

* Fix intermittent backend-core migration test failure

* Fix hanging worker redis connection

* Update naming from reset to newTenant
This commit is contained in:
Rory Powell 2023-02-21 17:13:24 +00:00 committed by GitHub
parent 1742055c3c
commit 940de8b6a0
22 changed files with 227 additions and 210 deletions

View File

@ -11,7 +11,6 @@ on:
branches: branches:
- master - master
- develop - develop
- release
workflow_dispatch: workflow_dispatch:
env: env:
@ -20,9 +19,53 @@ env:
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
jobs: jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: yarn
- run: yarn lint
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Install Pro
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn
- run: yarn bootstrap
- run: yarn build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Install Pro
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn
- run: yarn bootstrap
- run: yarn test
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml
name: codecov-umbrella
verbose: true
integration-test:
runs-on: ubuntu-latest
services: services:
couchdb: couchdb:
image: ibmcom/couchdb3 image: ibmcom/couchdb3
@ -31,39 +74,18 @@ jobs:
COUCHDB_USER: budibase COUCHDB_USER: budibase
ports: ports:
- 4567:5984 - 4567:5984
strategy:
matrix:
node-version: [14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js 14.x
- name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1
uses: actions/setup-node@v1 with:
with: node-version: 14.x
node-version: ${{ matrix.node-version }} - name: Install Pro
run: yarn install:pro $BRANCH $BASE_BRANCH
- name: Install Pro - run: yarn
run: yarn install:pro $BRANCH $BASE_BRANCH - run: yarn bootstrap
- run: yarn build
- run: yarn - run: |
- run: yarn bootstrap cd qa-core
- run: yarn lint yarn
- run: yarn build yarn api:test:ci
- run: yarn test
env:
CI: true
name: Budibase CI
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml
name: codecov-umbrella
verbose: true
- name: QA Core Integration Tests
run: |
cd qa-core
yarn
yarn api:test:ci

View File

@ -1,4 +1,2 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
yarn run lint

View File

@ -9,15 +9,9 @@ const baseConfig: Config.InitialProjectOptions = {
transform: { transform: {
"^.+\\.ts?$": "@swc/jest", "^.+\\.ts?$": "@swc/jest",
}, },
} moduleNameMapper: {
if (!process.env.CI) {
// use sources when not in CI
baseConfig.moduleNameMapper = {
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
} },
} else {
console.log("Running tests with compiled dependency sources")
} }
const config: Config.InitialOptions = { const config: Config.InitialOptions = {

View File

@ -18,7 +18,7 @@
"build:pro": "../../scripts/pro/build.sh", "build:pro": "../../scripts/pro/build.sh",
"postbuild": "yarn run build:pro", "postbuild": "yarn run build:pro",
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"test": "jest --coverage", "test": "jest --coverage --runInBand",
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {

View File

@ -19,7 +19,7 @@ describe("google", () => {
const callbackUrl = generator.url() const callbackUrl = generator.url()
it("should create successfully create a google strategy", async () => { it("should create successfully create a google strategy", async () => {
await google.strategyFactory(googleConfig, callbackUrl) await google.strategyFactory(googleConfig, callbackUrl, mockSaveUserFn)
const expectedOptions = { const expectedOptions = {
clientID: googleConfig.clientID, clientID: googleConfig.clientID,

View File

@ -4,7 +4,7 @@ import {
StaticDatabases, StaticDatabases,
getAllApps, getAllApps,
getGlobalDBName, getGlobalDBName,
doWithDB, getDB,
} from "../db" } from "../db"
import environment from "../environment" import environment from "../environment"
import * as platform from "../platform" import * as platform from "../platform"
@ -86,66 +86,65 @@ export const runMigration = async (
count++ count++
const lengthStatement = length > 1 ? `[${count}/${length}]` : "" const lengthStatement = length > 1 ? `[${count}/${length}]` : ""
await doWithDB(dbName, async (db: any) => { const db = getDB(dbName)
try { try {
const doc = await getMigrationsDoc(db) const doc = await getMigrationsDoc(db)
// the migration has already been run // the migration has already been run
if (doc[migrationName]) { if (doc[migrationName]) {
// check for force // check for force
if ( if (
options.force && options.force &&
options.force[migrationType] && options.force[migrationType] &&
options.force[migrationType].includes(migrationName) options.force[migrationType].includes(migrationName)
) { ) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
)
} else {
// no force, exit
return
}
}
// check if the migration is not a no-op
if (!options.noOp) {
log( log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}` `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
)
if (migration.preventRetry) {
// eagerly set the completion date
// so that we never run this migration twice even upon failure
doc[migrationName] = Date.now()
const response = await db.put(doc)
doc._rev = response.rev
}
// run the migration
if (migrationType === MigrationType.APP) {
await context.doInAppContext(db.name, async () => {
await migration.fn(db)
})
} else {
await migration.fn(db)
}
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
) )
} else {
// no force, exit
return
} }
// mark as complete
doc[migrationName] = Date.now()
await db.put(doc)
} catch (err) {
console.error(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
err
)
throw err
} }
})
// check if the migration is not a no-op
if (!options.noOp) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
)
if (migration.preventRetry) {
// eagerly set the completion date
// so that we never run this migration twice even upon failure
doc[migrationName] = Date.now()
const response = await db.put(doc)
doc._rev = response.rev
}
// run the migration
if (migrationType === MigrationType.APP) {
await context.doInAppContext(db.name, async () => {
await migration.fn(db)
})
} else {
await migration.fn(db)
}
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
)
}
// mark as complete
doc[migrationName] = Date.now()
await db.put(doc)
} catch (err) {
console.error(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
err
)
throw err
}
} }
} }
@ -185,7 +184,10 @@ export const runMigrations = async (
// for all migrations // for all migrations
for (const migration of migrations) { for (const migration of migrations) {
// run the migration // run the migration
await context.doInTenant(tenantId, () => runMigration(migration, options)) await context.doInTenant(
tenantId,
async () => await runMigration(migration, options)
)
} }
} }
console.log("Migrations complete") console.log("Migrations complete")

View File

@ -1,57 +0,0 @@
require("../../../tests")
const { runMigrations, getMigrationsDoc } = require("../index")
const { getGlobalDBName, getDB } = require("../../db")
const { structures, testEnv } = require("../../../tests")
testEnv.multiTenant()
let db
describe("migrations", () => {
const migrationFunction = jest.fn()
const MIGRATIONS = [{
type: "global",
name: "test",
fn: migrationFunction
}]
let tenantId
beforeEach(() => {
tenantId = structures.tenant.id()
db = getDB(getGlobalDBName(tenantId))
})
afterEach(async () => {
jest.clearAllMocks()
await db.destroy()
})
const migrate = () => {
return runMigrations(MIGRATIONS, { tenantIds: [tenantId]})
}
it("should run a new migration", async () => {
await migrate()
expect(migrationFunction).toHaveBeenCalled()
const doc = await getMigrationsDoc(db)
expect(doc.test).toBeDefined()
})
it("should match snapshot", async () => {
await migrate()
const doc = await getMigrationsDoc(db)
expect(doc).toMatchSnapshot()
})
it("should skip a previously run migration", async () => {
await migrate()
const previousMigrationTime = await getMigrationsDoc(db).test
await migrate()
const currentMigrationTime = await getMigrationsDoc(db).test
expect(migrationFunction).toHaveBeenCalledTimes(1)
expect(currentMigrationTime).toBe(previousMigrationTime)
})
})

View File

@ -0,0 +1,64 @@
import { testEnv, DBTestConfiguration } from "../../../tests"
import * as migrations from "../index"
import * as context from "../../context"
import { MigrationType } from "@budibase/types"
testEnv.multiTenant()
describe("migrations", () => {
const config = new DBTestConfiguration()
const migrationFunction = jest.fn()
const MIGRATIONS = [
{
type: MigrationType.GLOBAL,
name: "test" as any,
fn: migrationFunction,
},
]
beforeEach(() => {
config.newTenant()
})
afterEach(async () => {
jest.clearAllMocks()
})
const migrate = () => {
return migrations.runMigrations(MIGRATIONS, {
tenantIds: [config.tenantId],
})
}
it("should run a new migration", async () => {
await config.doInTenant(async () => {
await migrate()
expect(migrationFunction).toHaveBeenCalled()
const db = context.getGlobalDB()
const doc = await migrations.getMigrationsDoc(db)
expect(doc.test).toBeDefined()
})
})
it("should match snapshot", async () => {
await config.doInTenant(async () => {
await migrate()
const doc = await migrations.getMigrationsDoc(context.getGlobalDB())
expect(doc).toMatchSnapshot()
})
})
it("should skip a previously run migration", async () => {
await config.doInTenant(async () => {
const db = context.getGlobalDB()
await migrate()
const previousDoc = await migrations.getMigrationsDoc(db)
await migrate()
const currentDoc = await migrations.getMigrationsDoc(db)
expect(migrationFunction).toHaveBeenCalledTimes(1)
expect(currentDoc.test).toBe(previousDoc.test)
})
})
})

View File

@ -20,13 +20,17 @@ async function init() {
).init() ).init()
} }
process.on("exit", async () => { export async function shutdown() {
if (userClient) await userClient.finish() if (userClient) await userClient.finish()
if (sessionClient) await sessionClient.finish() if (sessionClient) await sessionClient.finish()
if (appClient) await appClient.finish() if (appClient) await appClient.finish()
if (cacheClient) await cacheClient.finish() if (cacheClient) await cacheClient.finish()
if (writethroughClient) await writethroughClient.finish() if (writethroughClient) await writethroughClient.finish()
if (lockClient) await lockClient.finish() if (lockClient) await lockClient.finish()
}
process.on("exit", async () => {
await shutdown()
}) })
export async function getUserClient() { export async function getUserClient() {

View File

@ -91,6 +91,11 @@ function init(selectDb = DEFAULT_SELECT_DB) {
} }
// attach handlers // attach handlers
client.on("end", (err: Error) => { client.on("end", (err: Error) => {
if (env.isTest()) {
// don't try to re-connect in test env
// allow the process to exit
return
}
connectionError(selectDb, timeout, err) connectionError(selectDb, timeout, err)
}) })
client.on("error", (err: Error) => { client.on("error", (err: Error) => {

View File

@ -12,6 +12,10 @@ class DBTestConfiguration {
this.tenantId = structures.tenant.id() this.tenantId = structures.tenant.id()
} }
newTenant() {
this.tenantId = structures.tenant.id()
}
// TENANCY // TENANCY
doInTenant(task: any) { doInTenant(task: any) {

View File

@ -11,22 +11,17 @@ const baseConfig: Config.InitialProjectOptions = {
transform: { transform: {
"^.+\\.ts?$": "@swc/jest", "^.+\\.ts?$": "@swc/jest",
}, },
} moduleNameMapper: {
if (!process.env.CI) {
// use sources when not in CI
baseConfig.moduleNameMapper = {
"@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1", "@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1",
"@budibase/backend-core": "<rootDir>/../backend-core/src", "@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
} },
// add pro sources if they exist }
if (fs.existsSync("../../../budibase-pro")) {
baseConfig.moduleNameMapper["@budibase/pro"] = // add pro sources if they exist
"<rootDir>/../../../budibase-pro/packages/pro/src" if (fs.existsSync("../../../budibase-pro")) {
} baseConfig.moduleNameMapper["@budibase/pro"] =
} else { "<rootDir>/../../../budibase-pro/packages/pro/src"
console.log("Running tests with compiled dependency sources")
} }
const config: Config.InitialOptions = { const config: Config.InitialOptions = {

View File

@ -14,7 +14,7 @@
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js", "debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/", "postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "jest --coverage --maxWorkers=2", "test": "jest --coverage --runInBand",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client", "predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
"build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION", "build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",

View File

@ -1,6 +1,6 @@
const Resource = require("./utils/Resource") const Resource = require("./utils/Resource")
const { object } = require("./utils") const { object } = require("./utils")
const { BaseQueryVerbs } = require("../../dist/constants") const { BaseQueryVerbs } = require("../../src/constants")
const query = { const query = {
_id: "query_datasource_plus_4d8be0c506b9465daf4bf84d890fdab6_454854487c574d45bc4029b1e153219e", _id: "query_datasource_plus_4d8be0c506b9465daf4bf84d890fdab6_454854487c574d45bc4029b1e153219e",

View File

@ -2,7 +2,7 @@ const {
FieldTypes, FieldTypes,
RelationshipTypes, RelationshipTypes,
FormulaTypes, FormulaTypes,
} = require("../../dist/constants") } = require("../../src/constants")
const { object } = require("./utils") const { object } = require("./utils")
const Resource = require("./utils/Resource") const Resource = require("./utils/Resource")

View File

@ -13,18 +13,6 @@ describe("/static", () => {
app = await config.init() app = await config.init()
}) })
describe("/builder", () => {
it("should serve the builder", async () => {
const res = await request
.get("/builder/portal")
.set(config.defaultHeaders())
.expect("Content-Type", /text\/html/)
.expect(200)
expect(res.text).toContain("<title>Budibase</title>")
})
})
describe("/app", () => { describe("/app", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()

View File

@ -1,4 +1,4 @@
const { roles, utils } = require("@budibase/backend-core") const { roles } = require("@budibase/backend-core")
const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") const { checkPermissionsEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities") const setup = require("./utilities")
const { BUILTIN_ROLE_IDS } = roles const { BUILTIN_ROLE_IDS } = roles
@ -21,8 +21,7 @@ describe("/users", () => {
afterAll(setup.afterAll) afterAll(setup.afterAll)
// For some reason this cannot be a beforeAll or the test "should be able to update the user" fail beforeAll(async () => {
beforeEach(async () => {
await config.init() await config.init()
}) })

View File

@ -21,6 +21,8 @@ export async function shutdown() {
if (devAppClient) await devAppClient.finish() if (devAppClient) await devAppClient.finish()
if (debounceClient) await debounceClient.finish() if (debounceClient) await debounceClient.finish()
if (flagClient) await flagClient.finish() if (flagClient) await flagClient.finish()
// shutdown core clients
await redis.clients.shutdown()
console.log("Redis shutdown") console.log("Redis shutdown")
} }

View File

@ -12,24 +12,19 @@ const config: Config.InitialOptions = {
transform: { transform: {
"^.+\\.ts?$": "@swc/jest", "^.+\\.ts?$": "@swc/jest",
}, },
} moduleNameMapper: {
if (!process.env.CI) {
// use sources when not in CI
config.moduleNameMapper = {
"@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1", "@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1",
"@budibase/backend-core": "<rootDir>/../backend-core/src", "@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
} },
// add pro sources if they exist }
if (fs.existsSync("../../../budibase-pro")) {
config.moduleNameMapper["@budibase/pro/(.*)"] = // add pro sources if they exist
"<rootDir>/../../../budibase-pro/packages/pro/$1" if (fs.existsSync("../../../budibase-pro")) {
config.moduleNameMapper["@budibase/pro"] = config.moduleNameMapper["@budibase/pro/(.*)"] =
"<rootDir>/../../../budibase-pro/packages/pro/src" "<rootDir>/../../../budibase-pro/packages/pro/$1"
} config.moduleNameMapper["@budibase/pro"] =
} else { "<rootDir>/../../../budibase-pro/packages/pro/src"
console.log("Running tests with compiled dependency sources")
} }
export default config export default config

View File

@ -22,7 +22,7 @@
"build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION", "build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
"dev:stack:init": "node ./scripts/dev/manage.js init", "dev:stack:init": "node ./scripts/dev/manage.js init",
"dev:builder": "npm run dev:stack:init && nodemon", "dev:builder": "npm run dev:stack:init && nodemon",
"test": "jest --coverage --maxWorkers=2", "test": "jest --coverage --runInBand",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"env:multi:enable": "node scripts/multiTenancy.js enable", "env:multi:enable": "node scripts/multiTenancy.js enable",
"env:multi:disable": "node scripts/multiTenancy.js disable", "env:multi:disable": "node scripts/multiTenancy.js disable",

View File

@ -54,6 +54,8 @@ export async function init() {
export async function shutdown() { export async function shutdown() {
if (pwResetClient) await pwResetClient.finish() if (pwResetClient) await pwResetClient.finish()
if (invitationClient) await invitationClient.finish() if (invitationClient) await invitationClient.finish()
// shutdown core clients
await redis.clients.shutdown()
console.log("Redis shutdown") console.log("Redis shutdown")
} }