diff --git a/admin-client/src/Router.svelte b/admin-client/src/Router.svelte index 1e01b2d35..eb3287ada 100644 --- a/admin-client/src/Router.svelte +++ b/admin-client/src/Router.svelte @@ -68,6 +68,7 @@ page('/organizations/:id/edit', queryString, render(Pages.Organization.Edit)); page('/organizations-report', queryString, render(Pages.Organization.Report)); page('/reports', queryString, render(Pages.Reports)); + page('/tasks', queryString, render(Pages.Tasks)); page('*', render(Pages.NotFound)); page(); diff --git a/admin-client/src/components/TaskTable.svelte b/admin-client/src/components/TaskTable.svelte new file mode 100644 index 000000000..fad746073 --- /dev/null +++ b/admin-client/src/components/TaskTable.svelte @@ -0,0 +1,49 @@ + + + + + Status + Id + Build + Task Type + Name + Artifact + Created + Updated + + + + {task.status} + + {task.id} + {task.buildId} + {task.buildTaskTypeId} + {task.name} + {task.artifact} + {formatDateTime(task.createdAt, true)} + {formatDateTime(task.udpatedAt, true)} + +

No build tasks found

+
+ + \ No newline at end of file diff --git a/admin-client/src/components/index.js b/admin-client/src/components/index.js index a108b4ade..5f29cb964 100644 --- a/admin-client/src/components/index.js +++ b/admin-client/src/components/index.js @@ -29,6 +29,7 @@ export { default as SiteForm } from './SiteForm.svelte'; export { default as SiteFormOrganization } from './SiteFormOrganization.svelte'; export { default as SiteFormWebhook } from './SiteFormWebhook.svelte'; export { default as SiteMetadata } from './SiteMetadata.svelte'; +export { default as TaskTable } from './TaskTable.svelte'; export { default as TextInput } from './TextInput.svelte'; export { default as UserTable } from './UserTable.svelte'; export { default as UserTableRoles } from './UserTableRoles.svelte'; diff --git a/admin-client/src/lib/api.js b/admin-client/src/lib/api.js index 27195df77..476a46a74 100644 --- a/admin-client/src/lib/api.js +++ b/admin-client/src/lib/api.js @@ -282,6 +282,10 @@ async function logout() { return get('/logout').catch(() => null); } +async function fetchTasks(query = {}) { + return get('/tasks', query).catch(() => []); +} + export { destroySite, fetchMe, @@ -323,4 +327,5 @@ export { resendInvite, logout, updateSite, + fetchTasks, }; diff --git a/admin-client/src/pages/Site.svelte b/admin-client/src/pages/Site.svelte index 9f867c90a..4597a1fb0 100644 --- a/admin-client/src/pages/Site.svelte +++ b/admin-client/src/pages/Site.svelte @@ -6,6 +6,7 @@ fetchOrganizations, fetchSite, fetchSiteWebhooks, + fetchTasks, fetchUserEnvironmentVariables, fetchUsers, updateSite, @@ -23,6 +24,7 @@ SiteFormOrganization, SiteFormWebhook, SiteMetadata, + TaskTable, UserTable, } from '../components'; import { destroySite } from '../flows'; @@ -32,6 +34,7 @@ $: sitePromise = fetchSite(id); $: siteWebhookPromise = fetchSiteWebhooks(id); $: buildsPromise = fetchBuilds({ site: id, limit: 10 }); + $: buildTasksPromise = fetchTasks({ site: id, limit: 10 }); $: orgsPromise = fetchOrganizations({ limit: 100 }); $: usersPromise = fetchUsers({ site: id }); $: uevsPromise = fetchUserEnvironmentVariables({ site: id }); @@ -193,6 +196,27 @@ + +

Registered Build Tasks

+ + + BuildTaskTypeId + Branch + Metadata + Created At + + + {sbt.buildTaskTypeId} + {sbt.branch} + {JSON.stringify(sbt.metadata)} + {sbt.createdAt} + +

No build tasks registered

+
+ + + +
diff --git a/admin-client/src/pages/Tasks.svelte b/admin-client/src/pages/Tasks.svelte new file mode 100644 index 000000000..cb371b839 --- /dev/null +++ b/admin-client/src/pages/Tasks.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/admin-client/src/pages/index.js b/admin-client/src/pages/index.js index 1e21e9d6b..49e26da69 100644 --- a/admin-client/src/pages/index.js +++ b/admin-client/src/pages/index.js @@ -7,6 +7,7 @@ export { default as NotFound } from './NotFound.svelte'; export { default as Reports } from './Reports.svelte'; export { default as Site } from './Site.svelte'; export { default as Sites } from './Sites.svelte'; +export { default as Tasks } from './Tasks.svelte'; export * as Domain from './domain'; export * as Organization from './organization'; export * as User from './user'; diff --git a/api/admin/controllers/index.js b/api/admin/controllers/index.js index 2a9c4f0a1..789f39194 100644 --- a/api/admin/controllers/index.js +++ b/api/admin/controllers/index.js @@ -5,6 +5,7 @@ const Organization = require('./organization'); const OrganizationRole = require('./organization-role'); const Role = require('./role'); const Site = require('./site'); +const Task = require('./task'); const UserEnvironmentVariable = require('./user-environment-variable'); const User = require('./user'); @@ -16,6 +17,7 @@ module.exports = { OrganizationRole, Role, Site, + Task, UserEnvironmentVariable, User, }; diff --git a/api/admin/controllers/site.js b/api/admin/controllers/site.js index e98a273a3..230230f0f 100644 --- a/api/admin/controllers/site.js +++ b/api/admin/controllers/site.js @@ -4,6 +4,7 @@ const { Organization, Site, SiteBranchConfig, + SiteBuildTask, } = require('../../models'); const SiteDestroyer = require('../../services/SiteDestroyer'); const GithubBuildHelper = require('../../services/GithubBuildHelper'); @@ -87,7 +88,7 @@ module.exports = wrapHandlers({ } = req; const site = await fetchModelById(id, Site, { - include: [SiteBranchConfig, Domain], + include: [SiteBranchConfig, Domain, SiteBuildTask], }); if (!site) return res.notFound(); diff --git a/api/admin/controllers/task.js b/api/admin/controllers/task.js new file mode 100644 index 000000000..35634f2ad --- /dev/null +++ b/api/admin/controllers/task.js @@ -0,0 +1,30 @@ +/* eslint-disable no-await-in-loop */ + +const { BuildTask } = require('../../models'); +const { paginate, wrapHandlers } = require('../../utils'); + +module.exports = wrapHandlers({ + async list(req, res) { + const { + limit, page, site, + } = req.query; + + const scopes = []; + + if (site) { + scopes.push(BuildTask.siteScope(site)); + } + + const pagination = await paginate( + BuildTask.scope(scopes), + a => a, + { limit, page } + ); + + const json = { + ...pagination, + }; + + return res.json(json); + }, +}); diff --git a/api/admin/routers/api.js b/api/admin/routers/api.js index 1ad5e6d90..a6759a249 100644 --- a/api/admin/routers/api.js +++ b/api/admin/routers/api.js @@ -46,6 +46,7 @@ apiRouter.put('/sites/:id', AdminControllers.Site.update); apiRouter.get('/sites/:id/webhooks', AdminControllers.Site.listWebhooks); apiRouter.post('/sites/:id/webhooks', AdminControllers.Site.createWebhook); apiRouter.delete('/sites/:id', authorize(['pages.admin']), AdminControllers.Site.destroy); +apiRouter.get('/tasks', AdminControllers.Task.list); apiRouter.get('/me', AdminControllers.User.me); apiRouter.get('/user-environment-variables', AdminControllers.UserEnvironmentVariable.list); apiRouter.get('/users', AdminControllers.User.list); diff --git a/api/bull-board/app.js b/api/bull-board/app.js index 4736d9cb5..fa5b507d7 100644 --- a/api/bull-board/app.js +++ b/api/bull-board/app.js @@ -11,6 +11,7 @@ const slowDown = require('express-slow-down'); const { ArchiveBuildLogsQueue, + BuildTasksQueue, DomainQueue, FailStuckBuildsQueue, MailQueue, @@ -44,6 +45,7 @@ createBullBoard({ queues: [ new BullAdapter(createQueue('site-build-queue')), new BullMQAdapter(new ArchiveBuildLogsQueue(connection)), + new BullMQAdapter(new BuildTasksQueue(connection)), new BullMQAdapter(new DomainQueue(connection)), new BullMQAdapter(new FailStuckBuildsQueue(connection)), new BullMQAdapter(new MailQueue(connection)), diff --git a/api/models/build-task-type.js b/api/models/build-task-type.js index 4aa55dc61..6bb9eccd6 100644 --- a/api/models/build-task-type.js +++ b/api/models/build-task-type.js @@ -1,3 +1,5 @@ +const { buildEnum } = require('../utils'); + const associate = ({ BuildTaskType, BuildTask, @@ -12,6 +14,16 @@ const associate = ({ }); }; +const Runners = buildEnum([ + 'cf_task', + 'worker', +]); + +const StartsWhens = buildEnum([ + 'build', + 'complete', +]); + module.exports = (sequelize, DataTypes) => { const BuildTaskType = sequelize.define( 'BuildTaskType', @@ -27,11 +39,29 @@ module.exports = (sequelize, DataTypes) => { metadata: { type: DataTypes.JSON, }, + runner: { + type: DataTypes.ENUM, + values: Runners.values, + allowNull: false, + validate: { + isIn: [Runners.values], + }, + }, + startsWhen: { + type: DataTypes.ENUM, + values: StartsWhens.values, + allowNull: false, + validate: { + isIn: [StartsWhens.values], + }, + }, }, { tableName: 'build_task_type', } ); BuildTaskType.associate = associate; + BuildTaskType.Runners = Runners; + BuildTaskType.StartsWhens = StartsWhens; return BuildTaskType; }; diff --git a/api/models/build-task.js b/api/models/build-task.js index 028be987e..c8e56fe9c 100644 --- a/api/models/build-task.js +++ b/api/models/build-task.js @@ -13,11 +13,23 @@ const Statuses = buildEnum([ 'success', ]); -const associate = ({ BuildTask, Build }) => { +const associate = ({ BuildTask, Build, BuildTaskType }) => { BuildTask.belongsTo(Build, { foreignKey: 'buildId', allowNull: false, }); + BuildTask.belongsTo(BuildTaskType, { + foreignKey: 'buildTaskTypeId', + allowNull: false, + }); + BuildTask.addScope('bySite', id => ({ + where: { + '$Build.site$': id, + }, + include: [{ + model: Build, + }], + })); }; const generateToken = () => URLSafeBase64.encode(crypto.randomBytes(32)); @@ -69,6 +81,6 @@ module.exports = (sequelize, DataTypes) => { BuildTask.generateToken = generateToken; BuildTask.associate = associate; - + BuildTask.siteScope = id => ({ method: ['bySite', id] }); return BuildTask; }; diff --git a/api/models/build.js b/api/models/build.js index 652ff582f..760d8e2c4 100644 --- a/api/models/build.js +++ b/api/models/build.js @@ -150,6 +150,34 @@ const jobStateUpdate = (buildStatus, build, site, timestamp) => { return build.update(atts); }; +const afterCreate = async (build) => { + // create relevant build tasks on build create + const { SiteBuildTask, BuildTaskType } = build.sequelize.models; + try { + await SiteBuildTask.startBuildTasks({ + build, + startsWhen: BuildTaskType.StartsWhens.Build, + }); + } catch (err) { + console.error(err); + } +}; + +const afterUpdate = async (build) => { + // we start certain build tasks on build completion + const { SiteBuildTask, BuildTaskType } = build.sequelize.models; + if (build.state === States.Success) { + try { + await SiteBuildTask.startBuildTasks({ + build, + startsWhen: BuildTaskType.StartsWhens.Complete, + }); + } catch (err) { + console.error(err); + } + } +}; + async function enqueue() { const build = this; @@ -298,6 +326,8 @@ module.exports = (sequelize, DataTypes) => { tableName: 'build', hooks: { beforeValidate, + afterCreate, + afterUpdate, }, } ); diff --git a/api/models/site-build-task.js b/api/models/site-build-task.js index 477c8785d..70d7123e6 100644 --- a/api/models/site-build-task.js +++ b/api/models/site-build-task.js @@ -1,3 +1,6 @@ +const { Op } = require('sequelize'); +const BuildTaskQueue = require('../services/BuildTaskQueue'); + const associate = ({ BuildTaskType, Site, SiteBuildTask }) => { SiteBuildTask.belongsTo(BuildTaskType, { foreignKey: 'buildTaskTypeId', @@ -8,6 +11,62 @@ const associate = ({ BuildTaskType, Site, SiteBuildTask }) => { }); }; +async function startBuildTask(buildId) { + const siteBuildTask = this; + const { + BuildTask, + BuildTaskType, + Build, + Site, + } = siteBuildTask.sequelize.models; + + const buildTask = await BuildTask.create({ + buildTaskTypeId: siteBuildTask.buildTaskTypeId, + buildId, + name: `build: ${buildId}, type: ${siteBuildTask.buildTaskTypeId}`, + }); + + const createdBuildTask = await BuildTask.findByPk(buildTask.id, { + include: [ + { model: BuildTaskType, required: true }, + { model: Build, required: true, include: [{ model: Site, required: true }] }, + ], + }); + + BuildTaskQueue.sendTaskMessage(createdBuildTask); +} + +async function findTasksToStart({ build, startsWhen }) { + const { + BuildTaskType, + } = this.sequelize.models; + return this.findAll({ + where: { + [Op.and]: [ + { + [Op.or]: [ + { branch: build.branch }, + { branch: null }, + ], + }, + { + siteId: build.site, + }, + ], + }, + include: { + model: BuildTaskType, + where: { startsWhen }, + required: true, + }, + }); +} + +async function startBuildTasks({ build, startsWhen }) { + const tasks = await this.findTasksToStart({ build, startsWhen }); + await Promise.allSettled(tasks.map(sbt => sbt.startBuildTask(build.id))); +} + module.exports = (sequelize, DataTypes) => { const SiteBuildTask = sequelize.define( 'SiteBuildTask', @@ -28,6 +87,8 @@ module.exports = (sequelize, DataTypes) => { ); SiteBuildTask.associate = associate; - + SiteBuildTask.startBuildTasks = startBuildTasks; + SiteBuildTask.findTasksToStart = findTasksToStart; + SiteBuildTask.prototype.startBuildTask = startBuildTask; return SiteBuildTask; }; diff --git a/api/queues/BuildTasksQueue.js b/api/queues/BuildTasksQueue.js new file mode 100644 index 000000000..cd50ee610 --- /dev/null +++ b/api/queues/BuildTasksQueue.js @@ -0,0 +1,23 @@ +const { Queue } = require('bullmq'); + +const BuildTasksQueueName = 'build-tasks'; + +class BuildTasksQueue extends Queue { + constructor(connection) { + super(BuildTasksQueueName, { + connection, + defaultJobOptions: { + attempts: 3, + backoff: { + type: 'exponential', + delay: 3000, + }, + }, + }); + } +} + +module.exports = { + BuildTasksQueue, + BuildTasksQueueName, +}; diff --git a/api/queues/index.js b/api/queues/index.js index fab058b70..decb40309 100644 --- a/api/queues/index.js +++ b/api/queues/index.js @@ -1,4 +1,5 @@ const { ArchiveBuildLogsQueue, ArchiveBuildLogsQueueName } = require('./ArchiveBuildLogsQueue'); +const { BuildTasksQueue, BuildTasksQueueName } = require('./BuildTasksQueue'); const { DomainQueue, DomainQueueName } = require('./DomainQueue'); const { FailStuckBuildsQueue, FailStuckBuildsQueueName } = require('./FailStuckBuildsQueue'); const { MailQueue, MailQueueName } = require('./MailQueue'); @@ -11,6 +12,8 @@ const { TimeoutBuildTasksQueue, TimeoutBuildTasksQueueName } = require('./Timeou module.exports = { ArchiveBuildLogsQueue, ArchiveBuildLogsQueueName, + BuildTasksQueue, + BuildTasksQueueName, DomainQueue, DomainQueueName, FailStuckBuildsQueue, diff --git a/api/serializers/site.js b/api/serializers/site.js index 4f133935a..3c04aa829 100644 --- a/api/serializers/site.js +++ b/api/serializers/site.js @@ -26,6 +26,7 @@ const allowedAttributes = [ 'Domains', 'Organization', 'SiteBranchConfigs', + 'SiteBuildTasks', 'Users', ]; diff --git a/api/services/BuildTaskQueue.js b/api/services/BuildTaskQueue.js new file mode 100644 index 000000000..c983ea16b --- /dev/null +++ b/api/services/BuildTaskQueue.js @@ -0,0 +1,68 @@ +const path = require('path'); +const config = require('../../config'); +const CloudFoundryAPIClient = require('../utils/cfApiClient'); +const BullQueueClient = require('../utils/bullQueueClient'); +const S3Helper = require('./S3Helper'); + +const apiClient = new CloudFoundryAPIClient(); + +const statusCallbackURL = buildTask => new URL( + path.join('/v0/tasks', String(buildTask.id), buildTask.token), + config.app.hostname +).href; + +const generateDefaultCredentials = async buildTask => ({ + STATUS_CALLBACK: statusCallbackURL(buildTask), + TASK_ID: buildTask.id, +}); + +const buildContainerEnvironment = async (buildTask) => { + const defaultCredentials = await generateDefaultCredentials(buildTask); + + return apiClient + .fetchServiceInstanceCredentials(buildTask.Build.Site.s3ServiceName) + .then(credentials => ({ + ...defaultCredentials, + AWS_DEFAULT_REGION: credentials.region, + AWS_ACCESS_KEY_ID: credentials.access_key_id, + AWS_SECRET_ACCESS_KEY: credentials.secret_access_key, + BUCKET: credentials.bucket, + })); +}; + +const setupBucket = async (build) => { + const credentials = await apiClient.fetchServiceInstanceCredentials(build.Site.s3ServiceName); + const { + access_key_id, // eslint-disable-line + bucket, + region, + secret_access_key, // eslint-disable-line + } = credentials; + + const s3Client = new S3Helper.S3Client({ + accessKeyId: access_key_id, + secretAccessKey: secret_access_key, + bucket, + region, + }); + + // Wait until AWS credentials are usable in case we had to + // provision new ones. This may take up to 10 seconds. + await s3Client.waitForCredentials(); + + return true; +}; + +const BuildTaskQueue = { + bullClient: new BullQueueClient('build-tasks'), +}; + +BuildTaskQueue.messageBodyForBuild = buildTask => buildContainerEnvironment(buildTask); + +BuildTaskQueue.sendTaskMessage = async (buildTask) => { + const message = await BuildTaskQueue.messageBodyForBuild(buildTask); + await setupBucket(buildTask.Build); + return BuildTaskQueue.bullClient.add(message); +}; + +module.exports = BuildTaskQueue; diff --git a/api/utils/cfApiClient.js b/api/utils/cfApiClient.js index 80f449055..5a5eff48b 100644 --- a/api/utils/cfApiClient.js +++ b/api/utils/cfApiClient.js @@ -1,4 +1,6 @@ const _ = require('underscore'); +const parse = require('json-templates'); + const CloudFoundryAuthClient = require('./cfAuthClient'); const HttpClient = require('./httpClient'); @@ -114,6 +116,17 @@ class CloudFoundryAPIClient { ); } + async startBuildTask(task, job) { + // construct the task parameter template by filling in values from the BuildTaskType metadata + // TODO: link to template documentation + const template = parse(task.BuildTaskType.metadata.template); + + const taskParams = template({ task, job }); + + const appGUID = await this.fetchTaskAppGUID(task.BuildTaskType.metadata.appName); + return this.authRequest('POST', `/v3/apps/${appGUID}/tasks`, taskParams); + } + /** * @param {Object} params * @param {string} params.domains Comma-delimited list of domains @@ -252,6 +265,15 @@ class CloudFoundryAPIClient { .then(service => service.guid); } + async fetchTaskAppGUID(appName) { + return this.accessToken().then(token => this.request( + 'GET', + `/v3/apps/?names=${appName}`, + token + )).then(res => firstEntity(res)) + .then(app => app.guid); + } + // Private methods accessToken() { return this.authClient.accessToken(); diff --git a/api/workers/index.js b/api/workers/index.js index 1da8adc93..6bfd6ca22 100644 --- a/api/workers/index.js +++ b/api/workers/index.js @@ -15,6 +15,8 @@ const DomainService = require('../services/Domain'); const { ArchiveBuildLogsQueue, ArchiveBuildLogsQueueName, + BuildTasksQueue, + BuildTasksQueueName, DomainQueueName, FailStuckBuildsQueue, FailStuckBuildsQueueName, @@ -59,6 +61,8 @@ function pagesWorker(connection) { } }; + const buildTasksProcessor = job => Processors.buildTaskRunner(job); + const failBuildsProcessor = job => Processors.failStuckBuilds(job); const siteDeletionProcessor = job => Processors.destroySiteInfra(job.data); @@ -81,6 +85,7 @@ function pagesWorker(connection) { const workers = [ new QueueWorker(ArchiveBuildLogsQueueName, connection, path.join(__dirname, 'jobProcessors', 'archiveBuildLogsDaily.js')), + new QueueWorker(BuildTasksQueueName, connection, buildTasksProcessor), new QueueWorker(DomainQueueName, connection, domainJobProcessor), new QueueWorker(FailStuckBuildsQueueName, connection, failBuildsProcessor), new QueueWorker(MailQueueName, connection, mailJobProcessor), @@ -93,6 +98,7 @@ function pagesWorker(connection) { const schedulers = [ new QueueScheduler(ArchiveBuildLogsQueueName, { connection }), + new QueueScheduler(BuildTasksQueueName, { connection }), new QueueScheduler(DomainQueueName, { connection }), new QueueScheduler(FailStuckBuildsQueueName, { connection }), new QueueScheduler(MailQueueName, { connection }), @@ -104,12 +110,14 @@ function pagesWorker(connection) { ]; const archiveBuildLogsQueue = new ArchiveBuildLogsQueue(connection); + const buildTasksQueue = new BuildTasksQueue(connection); const failStuckBuildsQueue = new FailStuckBuildsQueue(connection); const nightlyBuildsQueue = new NightlyBuildsQueue(connection); const scheduledQueue = new ScheduledQueue(connection); const timeoutBuildTasksQueue = new TimeoutBuildTasksQueue(connection); const queues = [ archiveBuildLogsQueue, + buildTasksQueue, failStuckBuildsQueue, nightlyBuildsQueue, scheduledQueue, diff --git a/api/workers/jobProcessors/buildTaskRunner.js b/api/workers/jobProcessors/buildTaskRunner.js new file mode 100644 index 000000000..614bac58d --- /dev/null +++ b/api/workers/jobProcessors/buildTaskRunner.js @@ -0,0 +1,45 @@ +const { + BuildTask, BuildTaskType, Build, Site, +} = require('../../models'); + +const { createJobLogger } = require('./utils'); +const CloudFoundryAPIClient = require('../../utils/cfApiClient'); + +async function buildTaskRunner(job) { + const logger = createJobLogger(job); + const taskId = job.data.TASK_ID; + + logger.log(`Running build task id: ${taskId}`); + try { + const task = await BuildTask.findByPk(taskId, { + include: [ + { model: BuildTaskType, required: true }, + { model: Build, required: true, include: [{ model: Site, required: true }] }, + ], + raw: true, + nest: true, + }); + + const taskTypeRunner = task.BuildTaskType.runner; + const apiClient = new CloudFoundryAPIClient(); + switch (taskTypeRunner) { + case BuildTaskType.Runners.Cf_task: + await apiClient.startBuildTask(task, job); + return true; + case BuildTaskType.Runners.Worker: + // TODO: temporary switch for JS worker code + return true; + default: + logger.log(`Unknown task runner: ${taskTypeRunner}`); + return true; + } + } catch (err) { + logger.log(err); + // TODO: should this hit the update endpoint instead? + const errorTask = await BuildTask.findByPk(taskId); + await errorTask.update({ status: 'error' }); + return err; + } +} + +module.exports = buildTaskRunner; diff --git a/api/workers/jobProcessors/index.js b/api/workers/jobProcessors/index.js index dd42e0c2c..de1855122 100644 --- a/api/workers/jobProcessors/index.js +++ b/api/workers/jobProcessors/index.js @@ -1,4 +1,5 @@ const archiveBuildLogsDaily = require('./archiveBuildLogsDaily'); +const buildTaskRunner = require('./buildTaskRunner'); const destroySiteInfra = require('./destroySiteInfra'); const failStuckBuilds = require('./failStuckBuilds'); const multiJobProcessor = require('./multiJobProcessor'); @@ -9,6 +10,7 @@ const cleanSandboxOrganizations = require('./cleanSandboxOrganizations'); module.exports = { archiveBuildLogsDaily, + buildTaskRunner, destroySiteInfra, failStuckBuilds, multiJobProcessor, diff --git a/migrations/20230922201843-build-task-type-more-columns.js b/migrations/20230922201843-build-task-type-more-columns.js new file mode 100644 index 000000000..f4b1a7dd6 --- /dev/null +++ b/migrations/20230922201843-build-task-type-more-columns.js @@ -0,0 +1,11 @@ +const TABLE = 'build_task_type'; + +exports.up = db => Promise.all([ + db.addColumn(TABLE, 'runner', { type: 'string', allowNull: false }), + db.addColumn(TABLE, 'startsWhen', { type: 'string', allowNull: true } ) +]); + +exports.down = db => Promise.all([ + db.removeColumn(TABLE, 'runner'), + db.removeColumn(TABLE, 'startsWhen'), +]); \ No newline at end of file diff --git a/package.json b/package.json index ca0e0dfe2..2e08e189c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "inflection": "^1.13.1", "ioredis": "^4.27.6", "js-yaml": "^4.1.0", + "json-templates": "^5.0.0", "json2csv": "juanjoDiaz/json2csv#v7.0.1", "jsonwebtoken": "^8.5.1", "moment": "^2.29.2", diff --git a/scripts/create-dev-data.js b/scripts/create-dev-data.js index 3b02eb264..09622e77c 100644 --- a/scripts/create-dev-data.js +++ b/scripts/create-dev-data.js @@ -443,6 +443,8 @@ async function createData() { metadata: { foo: 'bar', }, + runner: 'cf_task', + startsWhen: 'build', }); await BuildTask.create({ buildId: nodeSiteBuilds[0].id, diff --git a/test/api/support/factory/build-task-type.js b/test/api/support/factory/build-task-type.js index 1c390c239..367d0bfe7 100644 --- a/test/api/support/factory/build-task-type.js +++ b/test/api/support/factory/build-task-type.js @@ -2,11 +2,13 @@ const { BuildTaskType } = require('../../../../api/models'); // eslint-disable-next-line no-underscore-dangle const _attributes = ({ - name, description, metadata, + name, description, metadata, runner, startsWhen, } = {}) => ({ name: name || 'build task type name', description: description || 'build task type description', metadata: metadata || { some: 'metadata' }, + runner: runner || 'cf_task', + startsWhen: startsWhen || 'build', }); const buildTaskType = overrides => Promise.props(_attributes(overrides)) diff --git a/yarn.lock b/yarn.lock index 4ec1259b4..d4bfbecf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7255,6 +7255,13 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== +json-templates@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/json-templates/-/json-templates-5.0.0.tgz#142542c68429964612e70ecbab6938506371b7b2" + integrity sha512-oQ9FrrwX1GmACI1iXZpUEaM+32NS5K9NtQDeQc6e1N950w/3D5VNjpq/otXwNqdv3DfLK85AdYWQZ6oBwqA1aw== + dependencies: + object-path "^0.11.8" + json2csv@juanjoDiaz/json2csv#v7.0.1: version "7.0.1" resolved "https://codeload.github.com/juanjoDiaz/json2csv/tar.gz/b43c7dd556b7afc9728ecc31ccc96c8c76056f09" @@ -8310,6 +8317,11 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object-path@^0.11.8: + version "0.11.8" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" + integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== + object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"