Skip to content

Commit

Permalink
feat: retrieve integration meta data from new endpoint (#5647)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Karin Hendrikse <[email protected]>
  • Loading branch information
3 people authored May 29, 2024
1 parent 7bfd4ea commit b8899e0
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 18 deletions.
60 changes: 49 additions & 11 deletions packages/config/src/api/site_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type GetSiteInfoOpts = {
offline?: boolean
api?: NetlifyAPI
context?: string
featureFlags?: Record<string, boolean>
testOpts?: TestOptions
}
/**
Expand All @@ -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<any[]>(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 })
Expand All @@ -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}`)
}
}

Expand All @@ -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}`)
}
}

Expand All @@ -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<string, boolean>
}

const getIntegrations = async function ({
siteId,
accountId,
testOpts,
offline,
featureFlags,
}: GetIntegrationsOpts): Promise<IntegrationResponse[]> {
if (!siteId || offline) {
return []
Expand All @@ -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 []
}
}
2 changes: 1 addition & 1 deletion packages/config/src/error.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion packages/config/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export const resolveConfig = async function (opts) {
mode,
offline,
siteFeatureFlagPrefix,
featureFlags,
testOpts,
})

Expand Down
114 changes: 109 additions & 5 deletions packages/config/tests/api/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -307,36 +333,114 @@ 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',
})
.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 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')
t.assert(config.integrations[0].version === 'so-cool')
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) => {
Expand Down

0 comments on commit b8899e0

Please sign in to comment.