Skip to content

Commit

Permalink
feat: write metadata file (#5875)
Browse files Browse the repository at this point in the history
* feat: write metadata file

* refactor: use exported types

* refactor: use `FunctionResult` type

* chore: add JSDoc

* feat: pass branch to zip-it-and-ship-it

* chore: remove dep

* chore: add util back

---------

Co-authored-by: Philippe Serhal <[email protected]>
  • Loading branch information
eduardoboucas and serhalp authored Oct 10, 2024
1 parent 5860e72 commit 7dfac4c
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 30 deletions.
12 changes: 8 additions & 4 deletions packages/build/src/plugins_core/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolve } from 'path'

import { NodeBundlerName, RUNTIME, zipFunctions } from '@netlify/zip-it-and-ship-it'
import { NodeBundlerName, RUNTIME, zipFunctions, FunctionResult } from '@netlify/zip-it-and-ship-it'
import { pathExists } from 'path-exists'

import { addErrorInfo } from '../../error/info.js'
Expand All @@ -13,7 +13,7 @@ import { getUserAndInternalFunctions, validateFunctionsSrc } from './utils.js'
import { getZisiParameters } from './zisi.js'

// Get a list of all unique bundlers in this run
const getBundlers = (results: Awaited<ReturnType<typeof zipFunctions>> = []) =>
const getBundlers = (results: FunctionResult[] = []) =>
// using a Set to filter duplicates
new Set(
results
Expand All @@ -38,7 +38,7 @@ const eventTriggeredFunctions = new Set([
'identity-login',
])

const validateCustomRoutes = function (functions: Awaited<ReturnType<typeof zipFunctions>>) {
const validateCustomRoutes = function (functions: FunctionResult[]) {
for (const { routes, name, schedule } of functions) {
if (!routes || routes.length === 0) continue

Expand All @@ -61,6 +61,7 @@ const validateCustomRoutes = function (functions: Awaited<ReturnType<typeof zipF
}

const zipFunctionsAndLogResults = async ({
branch,
buildDir,
childEnv,
featureFlags,
Expand All @@ -76,6 +77,7 @@ const zipFunctionsAndLogResults = async ({
systemLog,
}) => {
const zisiParameters = getZisiParameters({
branch,
buildDir,
childEnv,
featureFlags,
Expand Down Expand Up @@ -118,6 +120,7 @@ const coreStep = async function ({
FUNCTIONS_DIST: relativeFunctionsDist,
},
buildDir,
branch,
packagePath,
logs,
netlifyConfig,
Expand Down Expand Up @@ -166,6 +169,7 @@ const coreStep = async function ({
}

const { bundlers } = await zipFunctionsAndLogResults({
branch,
buildDir,
childEnv,
featureFlags,
Expand Down Expand Up @@ -237,7 +241,7 @@ export const bundleFunctions = {
// `zip-it-and-ship-it` methods. Therefore, we need to use an intermediary
// function and export them so tests can use it.
export const zipItAndShipIt = {
async zipFunctions(...args: Parameters<typeof zipFunctions>) {
async zipFunctions(...args: Parameters<typeof zipFunctions>): Promise<FunctionResult[]> {
return await zipFunctions(...args)
},
}
Expand Down
3 changes: 3 additions & 0 deletions packages/build/src/plugins_core/functions/zisi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { FeatureFlags } from '../../core/feature_flags.js'
import { getZisiFeatureFlags } from './feature_flags.js'

type GetZisiParametersType = {
branch?: string
buildDir: string
childEnv: Record<string, string>
featureFlags: FeatureFlags
Expand Down Expand Up @@ -40,6 +41,7 @@ const getLambdaNodeVersion = (childEnv: Record<string, string>, userNodeVersion:
}

export const getZisiParameters = ({
branch,
buildDir,
childEnv,
featureFlags,
Expand All @@ -65,6 +67,7 @@ export const getZisiParameters = ({

return {
basePath: buildDir,
branch,
config,
manifest,
featureFlags: zisiFeatureFlags,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async () => new Response("Hello")

export const config = { path: "/hello" }
40 changes: 38 additions & 2 deletions packages/build/tests/functions/tests.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { readdir, readFile, rm, stat, writeFile } from 'fs/promises'
import { resolve } from 'path'
import { join, resolve } from 'path'
import { version as nodeVersion } from 'process'
import { fileURLToPath } from 'url'

import { Fixture, normalizeOutput, removeDir, getTempName } from '@netlify/testing'
import { Fixture, normalizeOutput, removeDir, getTempName, unzipFile } from '@netlify/testing'
import test from 'ava'
import { pathExists } from 'path-exists'
import semver from 'semver'
Expand Down Expand Up @@ -204,3 +204,39 @@ if (semver.gte(nodeVersion, '16.9.0')) {
t.true(app2FunctionsDist.includes('worker.zip'))
})
}

test('Functions: creates metadata file', async (t) => {
const fixture = await new Fixture('./fixtures/v2').withCopyRoot({ git: false })
const build = await fixture
.withFlags({
branch: 'my-branch',
cwd: fixture.repositoryRoot,
featureFlags: { zisi_add_metadata_file: true },
})
.runWithBuildAndIntrospect()

t.true(build.success)

const functionsDistPath = resolve(fixture.repositoryRoot, '.netlify/functions')
const functionsDistFiles = await readdir(functionsDistPath)

t.true(functionsDistFiles.includes('manifest.json'))
t.true(functionsDistFiles.includes('test.zip'))

const unzipPath = join(functionsDistPath, `.netlify-test-${Date.now()}`)

await unzipFile(join(functionsDistPath, 'test.zip'), unzipPath)

const functionFiles = await readdir(unzipPath)

t.true(functionFiles.includes('___netlify-bootstrap.mjs'))
t.true(functionFiles.includes('___netlify-entry-point.mjs'))
t.true(functionFiles.includes('___netlify-metadata.json'))
t.true(functionFiles.includes('test.mjs'))

const metadata = JSON.parse(await readFile(join(unzipPath, '___netlify-metadata.json'), 'utf8'))

t.is(semver.valid(metadata.bootstrap_version), metadata.bootstrap_version)
t.is(metadata.branch, 'my-branch')
t.is(metadata.version, 1)
})
14 changes: 14 additions & 0 deletions packages/testing/src/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { mkdir } from 'fs/promises'
import { platform } from 'process'

import { execa } from 'execa'

export const unzipFile = async function (path: string, dest: string): Promise<void> {
await mkdir(dest, { recursive: true })

if (platform === 'win32') {
await execa('tar', ['-xf', path, '-C', dest])
} else {
await execa('unzip', ['-o', path, '-d', dest])
}
}
1 change: 1 addition & 0 deletions packages/testing/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './dir.js'
export * from './fixture.js'
export * from './fs.js'
export * from './normalize.js'
export * from './server.js'
export * from './tcp_server.js'
Expand Down
4 changes: 2 additions & 2 deletions packages/zip-it-and-ship-it/src/feature_flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export const defaultFlags = {
// Adds the `___netlify-telemetry.mjs` file to the function bundle.
zisi_add_instrumentation_loader: true,

// Adds a `___netlify-bootstrap-version` file to the function bundle.
zisi_add_version_file: false,
// Adds a `___netlify-metadata.json` file to the function bundle.
zisi_add_metadata_file: false,
} as const

export type FeatureFlags = Partial<Record<keyof typeof defaultFlags, boolean>>
Expand Down
2 changes: 2 additions & 0 deletions packages/zip-it-and-ship-it/src/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const getSrcFilesWithBundler: GetSrcFilesFunction = async (parameters) => {
const zipFunction: ZipFunction = async function ({
archiveFormat,
basePath,
branch,
cache,
config = {},
destFolder,
Expand Down Expand Up @@ -113,6 +114,7 @@ const zipFunction: ZipFunction = async function ({
aliases,
archiveFormat,
basePath: finalBasePath,
branch,
cache,
destFolder,
extension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { normalizeFilePath } from './normalize_path.js'
export const ENTRY_FILE_NAME = '___netlify-entry-point'
export const BOOTSTRAP_FILE_NAME = '___netlify-bootstrap.mjs'
export const BOOTSTRAP_VERSION_FILE_NAME = '___netlify-bootstrap-version'
export const METADATA_FILE_NAME = '___netlify-metadata.json'
export const TELEMETRY_FILE_NAME = '___netlify-telemetry.mjs'

const require = createRequire(import.meta.url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface MetadataFile {
bootstrap_version?: string
branch?: string
version: number
}

export const getMetadataFile = (bootstrapVersion?: string, branch?: string): MetadataFile => ({
bootstrap_version: bootstrapVersion,
branch,
version: 1,
})
12 changes: 7 additions & 5 deletions packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import { cachedLstat, mkdirAndWriteFile } from '../../../utils/fs.js'

import {
BOOTSTRAP_FILE_NAME,
BOOTSTRAP_VERSION_FILE_NAME,
METADATA_FILE_NAME,
conflictsWithEntryFile,
EntryFile,
getEntryFile,
getTelemetryFile,
isNamedLikeEntryFile,
} from './entry_file.js'
import { getMetadataFile } from './metadata_file.js'
import { ModuleFormat } from './module_format.js'
import { normalizeFilePath } from './normalize_path.js'
import { getPackageJsonIfAvailable } from './package_json.js'
Expand All @@ -44,6 +45,7 @@ const DEFAULT_USER_SUBDIRECTORY = 'src'
interface ZipNodeParameters {
aliases?: Map<string, string>
basePath: string
branch?: string
cache: RuntimeCache
destFolder: string
extension: string
Expand Down Expand Up @@ -186,6 +188,7 @@ const createDirectory = async function ({
const createZipArchive = async function ({
aliases = new Map(),
basePath,
branch,
cache,
destFolder,
extension,
Expand Down Expand Up @@ -251,12 +254,11 @@ const createZipArchive = async function ({
if (runtimeAPIVersion === 2) {
const bootstrapPath = addBootstrapFile(srcFiles, aliases)

if (featureFlags.zisi_add_version_file === true) {
if (featureFlags.zisi_add_metadata_file === true) {
const { version: bootstrapVersion } = await getPackageJsonIfAvailable(bootstrapPath)
const payload = JSON.stringify(getMetadataFile(bootstrapVersion, branch))

if (bootstrapVersion) {
addZipContent(archive, bootstrapVersion, BOOTSTRAP_VERSION_FILE_NAME)
}
addZipContent(archive, payload, METADATA_FILE_NAME)
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/zip-it-and-ship-it/src/runtimes/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type ZipFunction = (
args: {
archiveFormat: ArchiveFormat
basePath?: string
branch?: string
cache: RuntimeCache
config: FunctionConfig
destFolder: string
Expand Down
9 changes: 6 additions & 3 deletions packages/zip-it-and-ship-it/src/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import { getFunctionsFromPaths } from './runtimes/index.js'
import { MODULE_FORMAT } from './runtimes/node/utils/module_format.js'
import { addArchiveSize } from './utils/archive_size.js'
import { RuntimeCache } from './utils/cache.js'
import { formatZipResult } from './utils/format_result.js'
import { formatZipResult, FunctionResult } from './utils/format_result.js'
import { listFunctionsDirectories, resolveFunctionsDirectories } from './utils/fs.js'
import { getLogger, LogFunction } from './utils/logger.js'
import { nonNullable } from './utils/non_nullable.js'

export interface ZipFunctionOptions {
archiveFormat?: ArchiveFormat
basePath?: string
branch?: string
config?: Config
featureFlags?: FeatureFlags
repositoryRoot?: string
Expand Down Expand Up @@ -53,6 +54,7 @@ export const zipFunctions = async function (
{
archiveFormat = ARCHIVE_FORMAT.ZIP,
basePath,
branch,
config = {},
configFileDirectories,
featureFlags: inputFeatureFlags,
Expand All @@ -63,7 +65,7 @@ export const zipFunctions = async function (
debug,
internalSrcFolder,
}: ZipFunctionsOptions = {},
) {
): Promise<FunctionResult[]> {
validateArchiveFormat(archiveFormat)

const logger = getLogger(systemLog, debug)
Expand Down Expand Up @@ -94,6 +96,7 @@ export const zipFunctions = async function (
const zipResult = await func.runtime.zipFunction({
archiveFormat,
basePath,
branch,
cache,
config: func.config,
destFolder,
Expand Down Expand Up @@ -145,7 +148,7 @@ export const zipFunction = async function (
debug,
internalSrcFolder,
}: ZipFunctionOptions = {},
) {
): Promise<FunctionResult | undefined> {
validateArchiveFormat(archiveFormat)

const logger = getLogger(systemLog, debug)
Expand Down
54 changes: 40 additions & 14 deletions packages/zip-it-and-ship-it/tests/v2api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,22 +709,48 @@ describe.runIf(semver.gte(nodeVersion, '18.13.0'))('V2 functions API', () => {
expect(files[0].runtimeVersion).toBe('nodejs20.x')
})

test('Adds a file with the bootstrap version to the ZIP archive', async () => {
const fixtureName = 'v2-api'
const { files } = await zipFixture(fixtureName, {
fixtureDir: FIXTURES_ESM_DIR,
opts: {
featureFlags: {
zisi_add_version_file: true,
describe('Adds a file with metadata', () => {
test('Without a branch', async () => {
const fixtureName = 'v2-api'
const { files } = await zipFixture(fixtureName, {
fixtureDir: FIXTURES_ESM_DIR,
opts: {
featureFlags: {
zisi_add_metadata_file: true,
},
},
},
})
const [unzippedFunction] = await unzipFiles(files)
const bootstrapPath = getBootstrapPath()
const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8')
const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson)
const versionFileContents = await readFile(join(unzippedFunction.unzipPath, '___netlify-metadata.json'), 'utf8')

expect(JSON.parse(versionFileContents)).toEqual({ bootstrap_version: bootstrapVersion, version: 1 })
})
const [unzippedFunction] = await unzipFiles(files)
const bootstrapPath = getBootstrapPath()
const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8')
const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson)
const versionFileContents = await readFile(join(unzippedFunction.unzipPath, '___netlify-bootstrap-version'), 'utf8')

expect(versionFileContents).toBe(bootstrapVersion)
test('With a branch', async () => {
const fixtureName = 'v2-api'
const { files } = await zipFixture(fixtureName, {
fixtureDir: FIXTURES_ESM_DIR,
opts: {
branch: 'main',
featureFlags: {
zisi_add_metadata_file: true,
},
},
})
const [unzippedFunction] = await unzipFiles(files)
const bootstrapPath = getBootstrapPath()
const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8')
const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson)
const versionFileContents = await readFile(join(unzippedFunction.unzipPath, '___netlify-metadata.json'), 'utf8')

expect(JSON.parse(versionFileContents)).toEqual({
bootstrap_version: bootstrapVersion,
branch: 'main',
version: 1,
})
})
})
})

0 comments on commit 7dfac4c

Please sign in to comment.