Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application Pipelines #2094

Merged
merged 66 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
145bd03
Templated UX in frontend to build upon
joepavitt May 3, 2023
4efaf99
Base Pipeline/Stages API, DB Models & DB Views
joepavitt May 4, 2023
00af280
Add application/api routes
joepavitt May 4, 2023
1251432
Pipelines & "Add Stage" UX Skeleton
joepavitt May 4, 2023
90b45bd
"Add Stage" functionality
joepavitt May 4, 2023
73c047e
Fix instance selection and overlow
joepavitt May 4, 2023
ab628d0
Stage CSS/layout & Add target setting via "source" in API call
joepavitt May 4, 2023
bf471e5
Add manual run action for single step in pipeline
joepavitt May 4, 2023
f6d01d0
Cleaner UX around editing/config of a pipeline
joepavitt May 5, 2023
090503e
Add instance status onload & delete API for pipeline
joepavitt May 5, 2023
4c4f6c0
Merge branch 'main' into 2075-application-pipelines
joepavitt May 5, 2023
e267dc0
Better animation feedback of deployments of a stage - now dependent u…
joepavitt May 5, 2023
564ef78
Add URL link inside pipeline stage
joepavitt May 5, 2023
b11b4a7
Add PH Events
joepavitt May 5, 2023
924bace
Add collection of audit events
joepavitt May 5, 2023
743a106
Add all devops pipeline aduit events & detail icon/verbose descriptio…
joepavitt May 5, 2023
002c483
Add basic empty state
joepavitt May 9, 2023
3863eff
Prevent access without premium license & add test accordingly
joepavitt May 9, 2023
90bbee1
Add Pipelines API unit tests
joepavitt May 9, 2023
652c7c3
Merge pull request #2107 from flowforge/2075-pipelines--auditlogs
knolleary May 9, 2023
8c8d941
Move pipeline routes under ee src tree
knolleary May 9, 2023
bcc9148
Merge pull request #2110 from flowforge/2075-move-pipeliens-to-ee
Pezmc May 10, 2023
f2e151b
Migration to explicitly create pipeline tables
Pezmc May 11, 2023
93fea55
Rework the Pipeline API to use a through table for instances
Pezmc May 11, 2023
9be9e41
Account for multiple instances per pipeline stage
Pezmc May 11, 2023
d398b98
Require pipeline stages to have an instance when added
Pezmc May 11, 2023
05e7378
Validate that all instances on a stage are correct application
Pezmc May 11, 2023
b85bdcc
Prevent unwanted attrs being passed down to pipeline view
Pezmc May 11, 2023
a7b7917
Remove redundant status load
Pezmc May 11, 2023
d2b3174
Guard against schema not being set
Pezmc May 11, 2023
af285e8
:bug:
Pezmc May 11, 2023
bf7f8f2
Update mock API to include pipelines
Pezmc May 16, 2023
e08d7b4
Merge branch 'main' into 2075-application-pipelines
Pezmc May 16, 2023
6c509fa
Require instance and name to be set to submit form
Pezmc May 16, 2023
5ef2dbc
Merge pull request #2125 from flowforge/2075-application-pipelines-db…
knolleary May 16, 2023
732ea98
Major restructure of pipelines to fall under applications
Pezmc May 17, 2023
1d023cc
Disable the instance dropdown if there are no instances
Pezmc May 19, 2023
c4b64c9
Merge pull request #2155 from flowforge/2124-pipeline-ui-tweaks
joepavitt May 19, 2023
20a4275
Update frontend/src/api/pipeline.js
joepavitt May 19, 2023
c814275
Update forge/ee/routes/pipeline/index.js
joepavitt May 19, 2023
d4e65c8
Add edit/update pipeline UX & API endpoint
joepavitt May 19, 2023
072661a
Extract shared logic of edit form to separate component
Pezmc May 20, 2023
2371dec
Add edit page for pipeline stages
Pezmc May 20, 2023
d8e94ac
API support for update and delete
Pezmc May 21, 2023
5f256c6
Pass instances down into pipeline pages
Pezmc May 21, 2023
a9e3bf3
Wire up edit pipeline stage form
Pezmc May 21, 2023
b752014
Deletion of pipelines
Pezmc May 21, 2023
48981d0
Guard against stageID being undefined
Pezmc May 21, 2023
f3fb052
Frontend API coverage of update/delete
Pezmc May 22, 2023
6682d38
Fix noise in unit tests by marking ff- components as custom
Pezmc May 22, 2023
0f0066d
Revert config changes due to issue with stubbed ff-text-input
Pezmc May 22, 2023
a5a0173
Remapping when deleting
Pezmc May 22, 2023
7849eef
Add pending test coverage of the pipelines API
Pezmc May 22, 2023
55ec4e2
Test coverage of CRUD Pipeline Stages
Pezmc May 22, 2023
a47dd3c
Generate a real UUID
Pezmc May 22, 2023
60e1137
Generate a UUID
Pezmc May 22, 2023
d543880
Merge pull request #2158 from flowforge/2124-edit-pipeline
Pezmc May 22, 2023
1221a03
Avoid preventable flash when pipeline cleared
Pezmc May 25, 2023
379e4a2
Prevent error when leaving pipeline stage edit screen
Pezmc May 25, 2023
0ea9e0c
Only allow submit if name is set
Pezmc May 25, 2023
f0212c9
Raise error if stage not found
Pezmc May 25, 2023
1ad56c1
When deleting a stage, ensure the previous stage is reloaded
Pezmc May 25, 2023
1d0372a
Merge pull request #2184 from flowforge/2124-pipeline-polish
knolleary May 26, 2023
0d81d98
Rename target to NextStageId and NextStage for clarity
Pezmc May 26, 2023
81843c1
/s/.targert/.NextStageID in pipelinetests
Pezmc May 26, 2023
441aeb8
Merge pull request #2189 from flowforge/2124-rename-pipeline-target
Pezmc May 26, 2023
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
2 changes: 1 addition & 1 deletion config/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [
Vue()
Vue(),
],
test: {
globals: true,
Expand Down
26 changes: 24 additions & 2 deletions forge/auditLog/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const isObject = (obj) => {
* @param {{ error?, team?, project?, sourceProject?, targetProject?, device?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, role?, projectType?, info? } == {}} objects objects to include in body
* @returns {{ error?, team?, project?, sourceProject?, targetProject?, device?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, role?, projectType? info? }
*/
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, user, stack, billingSession, subscription, license, updates, snapshot, role, projectType, info } = {}) => {
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, user, stack, billingSession, subscription, license, updates, snapshot, pipeline, pipelineStage, role, projectType, info } = {}) => {
const body = {}

if (isObject(error) || typeof error === 'string') {
Expand Down Expand Up @@ -57,6 +57,12 @@ const generateBody = ({ error, team, application, project, sourceProject, target
if (isObject(snapshot)) {
body.snapshot = snapshotObject(snapshot)
}
if (isObject(pipeline)) {
body.pipeline = pipelineObject(pipeline)
}
if (isObject(pipelineStage)) {
body.pipelineStage = pipelineStageObject(pipelineStage)
}
if (isObject(role) || typeof role === 'number') {
body.role = roleObject(role)
}
Expand Down Expand Up @@ -127,7 +133,9 @@ const formatLogEntry = (auditLogDbRow) => {
updates: body?.updates,
device: body?.device,
projectType: body?.projectType,
info: body?.info
info: body?.info,
pipeline: body?.pipeline,
pipelineStage: body?.pipelineStage
})
const roleObj = body?.role && roleObject(body.role)
if (roleObj) {
Expand Down Expand Up @@ -238,6 +246,20 @@ const snapshotObject = (snapshot) => {
name: snapshot?.name || null
}
}
const pipelineObject = (pipeline) => {
return {
id: pipeline?.id || null,
hashid: pipeline?.hashid || null,
name: pipeline?.name || null
}
}
const pipelineStageObject = (stage) => {
return {
id: stage?.id || null,
hashid: stage?.hashid || null,
name: stage?.name || null
}
}
const roleObject = (role) => {
if (typeof role === 'number') {
if (RoleNames[role]) {
Expand Down
3 changes: 3 additions & 0 deletions forge/auditLog/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ module.exports = {
async flowImported (actionedBy, error, project) {
await log('project.flow-imported', actionedBy, project?.id, generateBody({ error, project }))
},
async assignedToPipelineStage (actionedBy, error, project, pipeline, pipelineStage) {
await log('project.assigned-to-pipeline-stage', actionedBy, project?.id, generateBody({ error, project, pipeline, pipelineStage }))
},
device: {
async unassigned (actionedBy, error, project, device) {
await log('project.device.unassigned', actionedBy, project?.id, generateBody({ error, project, device }))
Expand Down
14 changes: 14 additions & 0 deletions forge/auditLog/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ module.exports = {
},
async deleted (actionedBy, error, team, application) {
await log('application.deleted', actionedBy, team?.id, generateBody({ error, team, application }))
},
pipeline: {
async created (actionedBy, error, team, application, pipeline) {
await log('application.pipeline.created', actionedBy, team?.id, generateBody({ error, team, application, pipeline }))
},
async updated (actionedBy, error, team, application, pipeline, updates) {
await log('application.pipeline.updated', actionedBy, team?.id, generateBody({ error, team, application, pipeline, updates }))
},
async deleted (actionedBy, error, team, application, pipeline) {
await log('application.pipeline.deleted', actionedBy, team?.id, generateBody({ error, team, application, pipeline }))
},
async stageAdded (actionedBy, error, team, application, pipeline, pipelineStage) {
await log('application.pipeline.stage-added', actionedBy, team?.id, generateBody({ error, team, application, pipeline, pipelineStage }))
}
}
}

Expand Down
74 changes: 74 additions & 0 deletions forge/db/migrations/20230504-01-add-pipelines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Add the ProjectStacks table
*/
const { DataTypes } = require('sequelize')

module.exports = {
up: async (context) => {
await context.createTable('Pipelines', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: { type: DataTypes.STRING, allowNull: false },

createdAt: { type: DataTypes.DATE },
updatedAt: { type: DataTypes.DATE },

ApplicationId: {
type: DataTypes.INTEGER,
references: { model: 'Applications', key: 'id' },
onDelete: 'cascade',
onUpdate: 'cascade'
}
})

await context.createTable('PipelineStages', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: { type: DataTypes.STRING, allowNull: false },

NextStageId: {
type: DataTypes.INTEGER,
allowNull: true,
references: { model: 'PipelineStages', key: 'id' },
onDelete: 'SET NULL ',
onUpdate: 'SET NULL '
},

PipelineId: {
type: DataTypes.INTEGER,
allowNull: false,
references: { model: 'Pipelines', key: 'id' },
onDelete: 'cascade',
onUpdate: 'cascade'
},

createdAt: { type: DataTypes.DATE },
updatedAt: { type: DataTypes.DATE }
})

await context.createTable('PipelineStageInstances', {
InstanceId: {
type: DataTypes.UUID,
allowNull: false,
references: { model: 'Projects', key: 'id' },
onDelete: 'cascade',
onUpdate: 'cascade'
},
PipelineStageId: {
type: DataTypes.INTEGER,
allowNull: false,
references: { model: 'PipelineStages', key: 'id' },
onDelete: 'cascade',
onUpdate: 'cascade'
}
})
},
down: async (context) => {
}
}
27 changes: 27 additions & 0 deletions forge/ee/db/controllers/Pipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

module.exports = {
addPipelineStage: async function (app, pipeline, options) {
if (!options.instanceId) {
throw new Error('Param instanceId is required when creating a new pipeline stage')
}

let source
options.PipelineId = pipeline.id
if (options.source) {
// this gives us the input stage to this new stage.
// we store "targets", so need to update the source to point to this new stage
source = options.source
delete options.source
}
const stage = await app.db.models.PipelineStage.create(options)
await stage.addInstanceId(options.instanceId)

if (source) {
const sourceStage = await app.db.models.PipelineStage.byId(source)
sourceStage.NextStageId = stage.id
await sourceStage.save()
}

return stage
}
}
3 changes: 2 additions & 1 deletion forge/ee/db/controllers/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const modelTypes = [
'Subscription',
'UserBillingCode'
'UserBillingCode',
'Pipeline'
]

async function init (app) {
Expand Down
48 changes: 48 additions & 0 deletions forge/ee/db/models/Pipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const {
DataTypes
} = require('sequelize')

module.exports = {
name: 'Pipeline',
schema: {
name: {
type: DataTypes.STRING,
allowNull: false
}
},
associations: function (M) {
this.belongsTo(M.Application)
this.hasMany(M.PipelineStage)
},
finders: function (M) {
const self = this
return {
instance: {
stages: async function () {
return await M.PipelineStage.byPipeline(this.id)
}
},
static: {
byId: async function (idOrHash) {
let id = idOrHash
if (typeof idOrHash === 'string') {
id = M.Pipeline.decodeHashid(idOrHash)
}
return this.findOne({
where: { id }
})
},
byApplicationId: async function (applicationId) {
if (typeof applicationId === 'string') {
applicationId = M.Application.decodeHashid(applicationId)
}
return self.findAll({
where: {
ApplicationId: applicationId
}
})
}
}
}
}
}
97 changes: 97 additions & 0 deletions forge/ee/db/models/PipelineStage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const {
DataTypes
} = require('sequelize')

module.exports = {
name: 'PipelineStage',
schema: {
name: {
type: DataTypes.STRING,
allowNull: false
},

NextStageId: {
type: DataTypes.INTEGER,
allowNull: true
}
},
options: {
validate: {
async instancesHaveSameApplication () {
const instancesPromise = this.getInstances()
const pipelinePromise = this.getPipeline()

const instances = await instancesPromise
const pipeline = await pipelinePromise

instances.forEach((instance) => {
if (instance.ApplicationId !== pipeline.ApplicationId) {
throw new Error(`All instances on a pipeline stage, must be a member of the same application as the pipeline. ${instance.name} is not a member of application ${pipeline.ApplicationId}.`)
}
})
}
}
},
associations: function (M) {
this.belongsTo(M.Pipeline)
this.belongsToMany(M.Project, { through: M.PipelineStageInstance, as: 'Instances', otherKey: 'InstanceId' })
this.hasOne(M.PipelineStage, { as: 'NextStage', foreignKey: 'NextStageId', allowNull: true })
},
finders: function (M) {
const self = this
return {
instance: {
async addInstanceId (instanceId) {
const instance = await M.Project.byId(instanceId)
if (!instance) {
throw new Error('instanceId not found')
}

await this.addInstance(instance)
}
},
static: {
byId: async function (idOrHash) {
let id = idOrHash
if (typeof idOrHash === 'string') {
id = M.PipelineStage.decodeHashid(idOrHash)
}
return this.findOne({
where: { id },
include: [
{
association: 'Instances',
attributes: ['hashid', 'id', 'name', 'url', 'updatedAt']
}
]
})
},
byPipeline: async function (pipelineId) {
if (typeof pipelineId === 'string') {
pipelineId = M.Pipeline.decodeHashid(pipelineId)
}
return await self.findAll({
where: {
PipelineId: pipelineId
},
include: [
{
association: 'Instances',
attributes: ['hashid', 'id', 'name', 'url', 'updatedAt']
}
]
})
},
byNextStage: async function (idOrHash) {
let id = idOrHash
if (typeof idOrHash === 'string') {
id = M.PipelineStage.decodeHashid(idOrHash)
}
return this.findOne({
where: { NextStageId: id }
})
}
}
}
}
}
20 changes: 20 additions & 0 deletions forge/ee/db/models/PipelineStageInstance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* This is the many:1 association model between a Project and a PipelineStage
* @namespace forge.db.models.PipelineStageInstance
*/

module.exports = {
name: 'PipelineStageInstance',
options: {
timestamps: false
},
associations: function (M) {
this.belongsTo(M.PipelineStage)
this.belongsTo(M.Project, { as: 'Instance' }) // @TODO: need to guard that the instance is part of the same application that owns the stage
},
meta: {
slug: false,
hashid: false,
links: false
}
}
Loading