From b8899e06a31f5615ef41d8a4ab251bcc96717837 Mon Sep 17 00:00:00 2001 From: lemusthelroy Date: Wed, 29 May 2024 11:24:32 +0100 Subject: [PATCH] feat: retrieve integration meta data from new endpoint (#5647) * chore: add flag to split logic - in progress * chore: link * chore: in progress with ff split * chore: correct flag * chore: debug tests * chore: debug tests * chore: test coverage added * chore: remove logs * chore: change local dep * chore: remove flag comment * chore: user v2 as flag var name * chore: add link to linear issue * chore: make error func return never * chore: remove else * chore: fix typing by return error func that returns never --------- Co-authored-by: Lewis Thorley Co-authored-by: Karin Hendrikse <30577427+khendrikse@users.noreply.github.com> --- packages/config/src/api/site_info.ts | 60 +++++++++++--- packages/config/src/error.ts | 2 +- packages/config/src/main.ts | 1 - packages/config/tests/api/tests.js | 114 +++++++++++++++++++++++++-- 4 files changed, 159 insertions(+), 18 deletions(-) diff --git a/packages/config/src/api/site_info.ts b/packages/config/src/api/site_info.ts index 93d08df1ee..d5c67371b1 100644 --- a/packages/config/src/api/site_info.ts +++ b/packages/config/src/api/site_info.ts @@ -14,7 +14,6 @@ type GetSiteInfoOpts = { offline?: boolean api?: NetlifyAPI context?: string - featureFlags?: Record testOpts?: TestOptions } /** @@ -37,22 +36,49 @@ export const getSiteInfo = async function ({ }: GetSiteInfoOpts) { const { env: testEnv = false } = testOpts - if (api === undefined || mode === 'buildbot' || testEnv) { + if (api === undefined || testEnv || offline) { const siteInfo = siteId === undefined ? {} : { id: siteId } - const integrations = mode === 'buildbot' && !offline ? await getIntegrations({ siteId, testOpts, offline }) : [] + return { siteInfo, accounts: [], addons: [], integrations: [] } + } + + const siteInfo = await getSite(api, siteId, siteFeatureFlagPrefix) + const featureFlags = siteInfo.feature_flags + + const useV2Endpoint = featureFlags?.cli_integration_installations_meta + + if (useV2Endpoint) { + const promises = [ + getAccounts(api), + getAddons(api, siteId), + getIntegrations({ siteId, testOpts, offline, accountId: siteInfo.account_id, featureFlags }), + ] + + const [accounts, addons, integrations] = await Promise.all(promises) + + if (siteInfo.use_envelope) { + const envelope = await getEnvelope({ api, accountId: siteInfo.account_slug, siteId, context }) + + siteInfo.build_settings.env = envelope + } + + return { siteInfo, accounts, addons, integrations } + } + if (mode === 'buildbot') { + const siteInfo = siteId === undefined ? {} : { id: siteId } + + const integrations = await getIntegrations({ siteId, testOpts, offline, featureFlags }) return { siteInfo, accounts: [], addons: [], integrations } } const promises = [ - getSite(api, siteId, siteFeatureFlagPrefix), getAccounts(api), getAddons(api, siteId), - getIntegrations({ siteId, testOpts, offline }), + getIntegrations({ siteId, testOpts, offline, featureFlags }), ] - const [siteInfo, accounts, addons, integrations] = await Promise.all(promises) + const [accounts, addons, integrations] = await Promise.all(promises) if (siteInfo.use_envelope) { const envelope = await getEnvelope({ api, accountId: siteInfo.account_slug, siteId, context }) @@ -72,7 +98,7 @@ const getSite = async function (api: NetlifyAPI, siteId: string, siteFeatureFlag const site = await (api as any).getSite({ siteId, feature_flags: siteFeatureFlagPrefix }) return { ...site, id: siteId } } catch (error) { - throwUserError(`Failed retrieving site data for site ${siteId}: ${error.message}. ${ERROR_CALL_TO_ACTION}`) + return throwUserError(`Failed retrieving site data for site ${siteId}: ${error.message}. ${ERROR_CALL_TO_ACTION}`) } } @@ -81,7 +107,7 @@ const getAccounts = async function (api: NetlifyAPI) { const accounts = await (api as any).listAccountsForUser() return Array.isArray(accounts) ? accounts : [] } catch (error) { - throwUserError(`Failed retrieving user account: ${error.message}. ${ERROR_CALL_TO_ACTION}`) + return throwUserError(`Failed retrieving user account: ${error.message}. ${ERROR_CALL_TO_ACTION}`) } } @@ -94,20 +120,24 @@ const getAddons = async function (api: NetlifyAPI, siteId: string) { const addons = await (api as any).listServiceInstancesForSite({ siteId }) return Array.isArray(addons) ? addons : [] } catch (error) { - throwUserError(`Failed retrieving addons for site ${siteId}: ${error.message}. ${ERROR_CALL_TO_ACTION}`) + return throwUserError(`Failed retrieving addons for site ${siteId}: ${error.message}. ${ERROR_CALL_TO_ACTION}`) } } type GetIntegrationsOpts = { siteId?: string + accountId?: string testOpts: TestOptions offline: boolean + featureFlags?: Record } const getIntegrations = async function ({ siteId, + accountId, testOpts, offline, + featureFlags, }: GetIntegrationsOpts): Promise { if (!siteId || offline) { return [] @@ -117,13 +147,21 @@ const getIntegrations = async function ({ const baseUrl = new URL(host ? `http://${host}` : `https://api.netlifysdk.com`) + const useV2Endpoint = featureFlags?.cli_integration_installations_meta + + const url = useV2Endpoint + ? `${baseUrl}team/${accountId}/integrations/installations/meta` + : `${baseUrl}site/${siteId}/integrations/safe` + try { - const response = await fetch(`${baseUrl}site/${siteId}/integrations/safe`) + const response = await fetch(url) const integrations = await response.json() return Array.isArray(integrations) ? integrations : [] } catch (error) { - // for now, we'll just ignore errors, as this is early days + // Integrations should not block the build if they fail to load + // TODO: We should consider blocking the build as integrations are a critical part of the build process + // https://linear.app/netlify/issue/CT-1214/implement-strategy-in-builds-to-deal-with-integrations-that-we-fail-to return [] } } diff --git a/packages/config/src/error.ts b/packages/config/src/error.ts index 343dc84a95..75bdf4fa1c 100644 --- a/packages/config/src/error.ts +++ b/packages/config/src/error.ts @@ -1,6 +1,6 @@ // We distinguish between errors thrown intentionally and uncaught exceptions // (such as bugs) with a `customErrorInfo.type` property. -export const throwUserError = function (messageOrError: string | Error, error?: Error) { +export const throwUserError = function (messageOrError: string | Error, error?: Error): never { const errorA = getError(messageOrError, error) errorA[CUSTOM_ERROR_KEY] = { type: USER_ERROR_TYPE } throw errorA diff --git a/packages/config/src/main.ts b/packages/config/src/main.ts index 1971acb574..8440061e85 100644 --- a/packages/config/src/main.ts +++ b/packages/config/src/main.ts @@ -73,7 +73,6 @@ export const resolveConfig = async function (opts) { mode, offline, siteFeatureFlagPrefix, - featureFlags, testOpts, }) diff --git a/packages/config/tests/api/tests.js b/packages/config/tests/api/tests.js index 61f9a2a113..67464ab446 100644 --- a/packages/config/tests/api/tests.js +++ b/packages/config/tests/api/tests.js @@ -26,11 +26,37 @@ const SITE_INTEGRATIONS_RESPONSE = { ], } +const TEAM_INSTALLATIONS_META_RESPONSE = { + path: '/team/account1/integrations/installations/meta', + response: [ + { + slug: 'test', + version: 'so-cool', + has_build: true, + }, + ], +} + const SITE_INTEGRATIONS_EMPTY_RESPONSE = { path: '/site/test/integrations/safe', response: [], } +const siteInfoWithFeatureFlag = (flag) => { + return { + path: SITE_INFO_PATH, + response: { + ssl_url: 'test', + name: 'test-name', + build_settings: { repo_url: 'test' }, + account_id: 'account1', + feature_flags: { + [flag]: true, + }, + }, + } +} + const SITE_INFO_BUILD_SETTINGS = { path: SITE_INFO_PATH, response: { @@ -307,9 +333,10 @@ test('In integration dev mode, integration specified in config is returned and b t.assert(config.integrations[0].version === undefined) }) -test('Integrations are returned if feature flag is true, mode buildbot', async (t) => { +test('Integrations are not returned if offline', async (t) => { const { output } = await new Fixture('./fixtures/base') .withFlags({ + offline: true, siteId: 'test', mode: 'buildbot', }) @@ -317,6 +344,35 @@ test('Integrations are returned if feature flag is true, mode buildbot', async ( const config = JSON.parse(output) + t.assert(config.integrations) + t.assert(config.integrations.length === 0) +}) + +test('Integrations are not returned if no api', async (t) => { + const { output } = await new Fixture('./fixtures/base') + .withFlags({ + siteId: 'test', + mode: 'buildbot', + }) + .runConfigServer([SITE_INTEGRATIONS_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) + + const config = JSON.parse(output) + + t.assert(config.integrations) + t.assert(config.integrations.length === 0) +}) + +test('Integrations are returned if feature flag is false and mode is buildbot', async (t) => { + const { output } = await new Fixture('./fixtures/base') + .withFlags({ + siteId: 'test', + mode: 'buildbot', + token: 'test', + }) + .runConfigServer([SITE_INFO_DATA, SITE_INTEGRATIONS_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) + + const config = JSON.parse(output) + t.assert(config.integrations) t.assert(config.integrations.length === 1) t.assert(config.integrations[0].slug === 'test') @@ -324,19 +380,67 @@ test('Integrations are returned if feature flag is true, mode buildbot', async ( t.assert(config.integrations[0].has_build === true) }) -test('Integrations are not returned if offline', async (t) => { +test('Integrations are returned if feature flag is false and mode is dev', async (t) => { + const { output } = await new Fixture('./fixtures/base') + .withFlags({ + siteId: 'test', + mode: 'dev', + token: 'test', + }) + .runConfigServer([SITE_INFO_DATA, SITE_INTEGRATIONS_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) + + const config = JSON.parse(output) + + t.assert(config.integrations) + t.assert(config.integrations.length === 1) + t.assert(config.integrations[0].slug === 'test') + t.assert(config.integrations[0].version === 'so-cool') + t.assert(config.integrations[0].has_build === true) +}) + +// new tests +test('Integrations are returned if flag is true for site and mode is buildbot', async (t) => { const { output } = await new Fixture('./fixtures/base') .withFlags({ - offline: true, siteId: 'test', mode: 'buildbot', + token: 'test', }) - .runConfigServer([SITE_INTEGRATIONS_RESPONSE, FETCH_INTEGRATIONS_EMPTY_RESPONSE]) + .runConfigServer([ + siteInfoWithFeatureFlag('cli_integration_installations_meta'), + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + ]) const config = JSON.parse(output) t.assert(config.integrations) - t.assert(config.integrations.length === 0) + t.assert(config.integrations.length === 1) + t.assert(config.integrations[0].slug === 'test') + t.assert(config.integrations[0].version === 'so-cool') + t.assert(config.integrations[0].has_build === true) +}) + +test('Integrations are returned if flag is true for site and mode is dev', async (t) => { + const { output } = await new Fixture('./fixtures/base') + .withFlags({ + siteId: 'test', + mode: 'dev', + token: 'test', + }) + .runConfigServer([ + siteInfoWithFeatureFlag('cli_integration_installations_meta'), + TEAM_INSTALLATIONS_META_RESPONSE, + FETCH_INTEGRATIONS_EMPTY_RESPONSE, + ]) + + const config = JSON.parse(output) + + t.assert(config.integrations) + t.assert(config.integrations.length === 1) + t.assert(config.integrations[0].slug === 'test') + t.assert(config.integrations[0].version === 'so-cool') + t.assert(config.integrations[0].has_build === true) }) test('baseRelDir is true if build.base is overridden', async (t) => {