From 85d4ee961eb83f905929565fdf15420c043df959 Mon Sep 17 00:00:00 2001 From: andypalmi Date: Wed, 21 Oct 2026 11:59:57 +0200 Subject: [PATCH 1/4] Let users keep the instance's env vars when rolling back to a snapshot The rollback endpoint accepts an optional mergeEnvVars flag, defaulting to false. The rollback dialog has a checkbox to keep the instance's env vars, ticked by default, and the rolled-back audit entry records the option used. --- forge/auditLog/project.js | 4 +- forge/routes/api/projectActions.js | 7 +- frontend/src/api/instances.js | 5 +- frontend/src/api/projectSnapshots.js | 4 +- .../audit-log/AuditEntryVerbose.vue | 2 +- .../dialogs/RollbackSnapshotDialog.vue | 35 ++++++++++ frontend/src/mixins/Snapshots.js | 17 +++-- .../version-history/snapshots.spec.js | 2 + test/unit/forge/auditLog/project_spec.js | 8 +++ .../forge/routes/api/projectSnapshots_spec.js | 64 +++++++++++++++++++ 10 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/dialogs/RollbackSnapshotDialog.vue diff --git a/forge/auditLog/project.js b/forge/auditLog/project.js index 21752632a7..cba775239c 100644 --- a/forge/auditLog/project.js +++ b/forge/auditLog/project.js @@ -84,8 +84,8 @@ module.exports = { async updated (actionedBy, error, project, snapshot, updates) { await log('project.snapshot.updated', actionedBy, project?.id, generateBody({ error, project, snapshot, updates })) }, - async rolledBack (actionedBy, error, project, snapshot) { - await log('project.snapshot.rolled-back', actionedBy, project?.id, generateBody({ error, project, snapshot })) + async rolledBack (actionedBy, error, project, snapshot, info) { + await log('project.snapshot.rolled-back', actionedBy, project?.id, generateBody({ error, project, snapshot, info })) }, async deleted (actionedBy, error, project, snapshot) { await log('project.snapshot.deleted', actionedBy, project?.id, generateBody({ error, project, snapshot })) diff --git a/forge/routes/api/projectActions.js b/forge/routes/api/projectActions.js index 5357bd8a97..16d8131aed 100644 --- a/forge/routes/api/projectActions.js +++ b/forge/routes/api/projectActions.js @@ -225,7 +225,8 @@ module.exports = async function (app) { body: { type: 'object', properties: { - snapshot: { type: 'string' } + snapshot: { type: 'string' }, + mergeEnvVars: { type: 'boolean', default: false } }, required: ['snapshot'] }, @@ -258,8 +259,8 @@ module.exports = async function (app) { restartProject = true } await app.db.controllers.Project.setInflightState(request.project, 'rollback') - await app.db.controllers.Project.importProjectSnapshot(request.project, snapshot) - await app.auditLog.Project.project.snapshot.rolledBack(request.session.User, null, request.project, snapshot) + await app.db.controllers.Project.importProjectSnapshot(request.project, snapshot, { mergeEnvVars: request.body.mergeEnvVars }) + await app.auditLog.Project.project.snapshot.rolledBack(request.session.User, null, request.project, snapshot, { mergeEnvVars: request.body.mergeEnvVars }) if (restartProject) { await app.db.controllers.Project.setInflightState(request.project, 'restarting') await app.containers.restartFlows(request.project) diff --git a/frontend/src/api/instances.js b/frontend/src/api/instances.js index ca106fac68..cf9548cd3b 100644 --- a/frontend/src/api/instances.js +++ b/frontend/src/api/instances.js @@ -134,9 +134,10 @@ const getInstanceDeviceSettings = async (instanceId) => { const updateInstanceDeviceSettings = async (instanceId, settings) => { return client.post(`/api/v1/projects/${instanceId}/devices/settings`, settings).then(res => res.data) } -const rollbackInstance = async (instanceId, snapshotId) => { +const rollbackInstance = async (instanceId, snapshotId, { mergeEnvVars } = {}) => { const data = { - snapshot: snapshotId + snapshot: snapshotId, + mergeEnvVars } return client.post(`/api/v1/projects/${instanceId}/actions/rollback`, data).then(res => res.data) } diff --git a/frontend/src/api/projectSnapshots.js b/frontend/src/api/projectSnapshots.js index a2748dd66d..89ac1a10cb 100644 --- a/frontend/src/api/projectSnapshots.js +++ b/frontend/src/api/projectSnapshots.js @@ -25,13 +25,13 @@ const create = async (projectId, options) => { }) } -const rollbackSnapshot = async (instanceId, snapshotId) => { +const rollbackSnapshot = async (instanceId, snapshotId, options) => { product.capture('ff-snapshot-rollback', { 'snapshot-id': snapshotId }, { instance: instanceId }) - return instanceApi.rollbackInstance(instanceId, snapshotId) + return instanceApi.rollbackInstance(instanceId, snapshotId, options) } /** diff --git a/frontend/src/components/audit-log/AuditEntryVerbose.vue b/frontend/src/components/audit-log/AuditEntryVerbose.vue index ad9d1f1261..e39dc464bc 100644 --- a/frontend/src/components/audit-log/AuditEntryVerbose.vue +++ b/frontend/src/components/audit-log/AuditEntryVerbose.vue @@ -691,7 +691,7 @@ @@ -20,15 +19,27 @@