diff --git a/src/lib/addons/slack-definition.js b/src/lib/addons/slack-definition.js index 9a81a4bdd5..527474a37f 100644 --- a/src/lib/addons/slack-definition.js +++ b/src/lib/addons/slack-definition.js @@ -5,6 +5,8 @@ const { FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, } = require('../event-type'); module.exports = { @@ -52,6 +54,8 @@ module.exports = { FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, ], tagTypes: [ { diff --git a/src/lib/addons/slack.js b/src/lib/addons/slack.js index d61157e68e..712ea74a4a 100644 --- a/src/lib/addons/slack.js +++ b/src/lib/addons/slack.js @@ -8,6 +8,8 @@ const { FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, } = require('../event-type'); const definition = require('./slack-definition'); @@ -32,7 +34,15 @@ class SlackAddon extends Addon { slackChannels.push(defaultChannel); } - const text = this.generateText(event); + let text; + + if (event.type === FEATURE_STALE_ON) { + text = this.generateStaleText(event, true); + } else if (event.type === FEATURE_STALE_OFF) { + text = this.generateStaleText(event, false); + } else { + text = this.generateText(event); + } const requests = slackChannels.map(channel => { const body = { @@ -74,6 +84,16 @@ class SlackAddon extends Addon { return tags.filter(tag => tag.type === 'slack').map(t => t.value); } + generateStaleText({ createdBy, data }, isStale) { + const feature = `<${this.unleashUrl}/#/features/strategies/${data.name}|${data.name}>`; + + if (isStale) { + return `The feature toggle *${feature}* is now *ready to be removed* from the code. :technologist: +This was changed by ${createdBy}.`; + } + return `The feature toggle *${feature}* was is *unmarked as stale*. This was changed by ${createdBy}.`; + } + generateText({ createdBy, data, type }) { const eventName = this.eventName(type); const feature = `<${this.unleashUrl}/#/features/strategies/${data.name}|${data.name}>`; diff --git a/src/lib/addons/webhook-definition.js b/src/lib/addons/webhook-definition.js index 3bbc11e463..898a30d79d 100644 --- a/src/lib/addons/webhook-definition.js +++ b/src/lib/addons/webhook-definition.js @@ -3,6 +3,8 @@ const { FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, } = require('../event-type'); module.exports = { @@ -50,5 +52,7 @@ module.exports = { FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, ], }; diff --git a/src/lib/event-differ.js b/src/lib/event-differ.js index d9ae2d6742..00f65c0fe6 100644 --- a/src/lib/event-differ.js +++ b/src/lib/event-differ.js @@ -30,6 +30,8 @@ const { TAG_TYPE_CREATED, TAG_TYPE_DELETED, APPLICATION_CREATED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, } = require('./event-type'); const strategyTypes = [ @@ -51,6 +53,8 @@ const featureTypes = [ FEATURE_TAGGED, FEATURE_UNTAGGED, DROP_FEATURES, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, ]; const contextTypes = [ diff --git a/src/lib/event-type.js b/src/lib/event-type.js index 49a431455a..b3bb215d20 100644 --- a/src/lib/event-type.js +++ b/src/lib/event-type.js @@ -9,6 +9,8 @@ module.exports = { FEATURE_IMPORT: 'feature-import', FEATURE_TAGGED: 'feature-tagged', FEATURE_UNTAGGED: 'feature-untagged', + FEATURE_STALE_ON: 'feature-stale-on', + FEATURE_STALE_OFF: 'feature-stale-off', DROP_FEATURES: 'drop-features', STRATEGY_CREATED: 'strategy-created', STRATEGY_DELETED: 'strategy-deleted', diff --git a/src/lib/routes/admin-api/feature.js b/src/lib/routes/admin-api/feature.js index 1ae809ea55..2ef28e326d 100644 --- a/src/lib/routes/admin-api/feature.js +++ b/src/lib/routes/admin-api/feature.js @@ -169,11 +169,33 @@ class FeatureController extends Controller { } async staleOn(req, res) { - await this._updateField('stale', true, req, res); + try { + const { featureName } = req.params; + const userName = extractUser(req); + const feature = await this.featureService.updateStale( + featureName, + true, + userName, + ); + res.json(feature).end(); + } catch (error) { + handleErrors(res, this.logger, error); + } } async staleOff(req, res) { - await this._updateField('stale', false, req, res); + try { + const { featureName } = req.params; + const userName = extractUser(req); + const feature = await this.featureService.updateStale( + featureName, + false, + userName, + ); + res.json(feature).end(); + } catch (error) { + handleErrors(res, this.logger, error); + } } async _updateField(field, value, req, res) { diff --git a/src/lib/routes/admin-api/feature.test.js b/src/lib/routes/admin-api/feature.test.js index 7de6f56d48..84eb57146d 100644 --- a/src/lib/routes/admin-api/feature.test.js +++ b/src/lib/routes/admin-api/feature.test.js @@ -620,3 +620,42 @@ test('Trying to get features while database is down should yield 500', t => { const { request, base } = getSetup(false); return request.get(`${base}/api/admin/features`).expect(500); }); + +test('should mark toggle as stale', t => { + t.plan(1); + const toggleName = 'toggle-stale'; + const { request, featureToggleStore, base, perms } = getSetup(); + perms.withPermissions(UPDATE_FEATURE, DELETE_FEATURE); + featureToggleStore.createFeature({ + name: toggleName, + strategies: [{ name: 'default' }], + }); + + return request + .post(`${base}/api/admin/features/${toggleName}/stale/on`) + .set('Content-Type', 'application/json') + .expect(200) + .expect(res => { + t.true(res.body.stale); + }); +}); + +test('should mark toggle as NOT stale', t => { + t.plan(1); + const toggleName = 'toggle-stale'; + const { request, featureToggleStore, base, perms } = getSetup(); + perms.withPermissions(UPDATE_FEATURE, DELETE_FEATURE); + featureToggleStore.createFeature({ + name: toggleName, + strategies: [{ name: 'default' }], + stale: true, + }); + + return request + .post(`${base}/api/admin/features/${toggleName}/stale/off`) + .set('Content-Type', 'application/json') + .expect(200) + .expect(res => { + t.false(res.body.stale); + }); +}); diff --git a/src/lib/services/feature-toggle-service.js b/src/lib/services/feature-toggle-service.js index 9bae03b879..3f92088ed9 100644 --- a/src/lib/services/feature-toggle-service.js +++ b/src/lib/services/feature-toggle-service.js @@ -8,6 +8,8 @@ const { FEATURE_CREATED, FEATURE_REVIVED, FEATURE_UPDATED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, TAG_CREATED, } = require('../event-type'); @@ -119,7 +121,7 @@ class FeatureToggleService { return this.updateField(feature.name, 'enabled', toggle, userName); } - /** Tag releated */ + /** Tag related */ async listTags(featureName) { return this.featureToggleStore.getAllTagsForFeature(featureName); } @@ -205,6 +207,23 @@ class FeatureToggleService { }); return feature; } + + async updateStale(featureName, isStale, userName) { + const feature = await this.featureToggleStore.getFeature(featureName); + feature.stale = isStale; + await this.featureToggleStore.updateFeature(feature); + const tags = + (await this.featureToggleStore.getAllTagsForFeature(featureName)) || + []; + + await this.eventStore.store({ + type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, + createdBy: userName, + data: feature, + tags, + }); + return feature; + } } module.exports = FeatureToggleService; diff --git a/src/test/e2e/api/admin/feature.e2e.test.js b/src/test/e2e/api/admin/feature.e2e.test.js index 4aa06dea5e..fb06308840 100644 --- a/src/test/e2e/api/admin/feature.e2e.test.js +++ b/src/test/e2e/api/admin/feature.e2e.test.js @@ -518,3 +518,15 @@ test.serial( }); }, ); + +test.serial('marks feature toggle as stale', async t => { + t.plan(1); + const request = await setupApp(stores); + await request + .post('/api/admin/features/featureZ/stale/on') + .set('Content-Type', 'application/json'); + + return request.get('/api/admin/features/featureZ').expect(res => { + t.true(res.body.stale); + }); +});