From 7dfac4cc38058e135c4952d53ef6b63c8034f962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 10 Oct 2024 12:16:08 +0100 Subject: [PATCH] feat: write metadata file (#5875) * 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 --- .../build/src/plugins_core/functions/index.ts | 12 +++-- .../build/src/plugins_core/functions/zisi.ts | 3 ++ .../fixtures/v2/netlify/functions/test.mjs | 3 ++ packages/build/tests/functions/tests.js | 40 +++++++++++++- packages/testing/src/fs.ts | 14 +++++ packages/testing/src/index.ts | 1 + .../zip-it-and-ship-it/src/feature_flags.ts | 4 +- .../src/runtimes/node/index.ts | 2 + .../src/runtimes/node/utils/entry_file.ts | 1 + .../src/runtimes/node/utils/metadata_file.ts | 11 ++++ .../src/runtimes/node/utils/zip.ts | 12 +++-- .../src/runtimes/runtime.ts | 1 + packages/zip-it-and-ship-it/src/zip.ts | 9 ++-- .../zip-it-and-ship-it/tests/v2api.test.ts | 54 ++++++++++++++----- 14 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 packages/build/tests/functions/fixtures/v2/netlify/functions/test.mjs create mode 100644 packages/testing/src/fs.ts create mode 100644 packages/zip-it-and-ship-it/src/runtimes/node/utils/metadata_file.ts diff --git a/packages/build/src/plugins_core/functions/index.ts b/packages/build/src/plugins_core/functions/index.ts index 29beaec90f..a5819f8179 100644 --- a/packages/build/src/plugins_core/functions/index.ts +++ b/packages/build/src/plugins_core/functions/index.ts @@ -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' @@ -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> = []) => +const getBundlers = (results: FunctionResult[] = []) => // using a Set to filter duplicates new Set( results @@ -38,7 +38,7 @@ const eventTriggeredFunctions = new Set([ 'identity-login', ]) -const validateCustomRoutes = function (functions: Awaited>) { +const validateCustomRoutes = function (functions: FunctionResult[]) { for (const { routes, name, schedule } of functions) { if (!routes || routes.length === 0) continue @@ -61,6 +61,7 @@ const validateCustomRoutes = function (functions: Awaited { const zisiParameters = getZisiParameters({ + branch, buildDir, childEnv, featureFlags, @@ -118,6 +120,7 @@ const coreStep = async function ({ FUNCTIONS_DIST: relativeFunctionsDist, }, buildDir, + branch, packagePath, logs, netlifyConfig, @@ -166,6 +169,7 @@ const coreStep = async function ({ } const { bundlers } = await zipFunctionsAndLogResults({ + branch, buildDir, childEnv, featureFlags, @@ -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) { + async zipFunctions(...args: Parameters): Promise { return await zipFunctions(...args) }, } diff --git a/packages/build/src/plugins_core/functions/zisi.ts b/packages/build/src/plugins_core/functions/zisi.ts index 41651e3cd3..d6204b3094 100644 --- a/packages/build/src/plugins_core/functions/zisi.ts +++ b/packages/build/src/plugins_core/functions/zisi.ts @@ -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 featureFlags: FeatureFlags @@ -40,6 +41,7 @@ const getLambdaNodeVersion = (childEnv: Record, userNodeVersion: } export const getZisiParameters = ({ + branch, buildDir, childEnv, featureFlags, @@ -65,6 +67,7 @@ export const getZisiParameters = ({ return { basePath: buildDir, + branch, config, manifest, featureFlags: zisiFeatureFlags, diff --git a/packages/build/tests/functions/fixtures/v2/netlify/functions/test.mjs b/packages/build/tests/functions/fixtures/v2/netlify/functions/test.mjs new file mode 100644 index 0000000000..11d0241321 --- /dev/null +++ b/packages/build/tests/functions/fixtures/v2/netlify/functions/test.mjs @@ -0,0 +1,3 @@ +export default async () => new Response("Hello") + +export const config = { path: "/hello" } diff --git a/packages/build/tests/functions/tests.js b/packages/build/tests/functions/tests.js index 3d7d505bf9..a23893c647 100644 --- a/packages/build/tests/functions/tests.js +++ b/packages/build/tests/functions/tests.js @@ -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' @@ -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) +}) diff --git a/packages/testing/src/fs.ts b/packages/testing/src/fs.ts new file mode 100644 index 0000000000..93d06f1838 --- /dev/null +++ b/packages/testing/src/fs.ts @@ -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 { + await mkdir(dest, { recursive: true }) + + if (platform === 'win32') { + await execa('tar', ['-xf', path, '-C', dest]) + } else { + await execa('unzip', ['-o', path, '-d', dest]) + } +} diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index ebb6511823..4b25185540 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -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' diff --git a/packages/zip-it-and-ship-it/src/feature_flags.ts b/packages/zip-it-and-ship-it/src/feature_flags.ts index c8198d07e1..9f1a39ab25 100644 --- a/packages/zip-it-and-ship-it/src/feature_flags.ts +++ b/packages/zip-it-and-ship-it/src/feature_flags.ts @@ -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> diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/index.ts b/packages/zip-it-and-ship-it/src/runtimes/node/index.ts index 3ed6e10eb8..aedf137231 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/index.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/index.ts @@ -37,6 +37,7 @@ const getSrcFilesWithBundler: GetSrcFilesFunction = async (parameters) => { const zipFunction: ZipFunction = async function ({ archiveFormat, basePath, + branch, cache, config = {}, destFolder, @@ -113,6 +114,7 @@ const zipFunction: ZipFunction = async function ({ aliases, archiveFormat, basePath: finalBasePath, + branch, cache, destFolder, extension, diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts b/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts index 5e1cf7ab17..837cbe8def 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts @@ -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) diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/utils/metadata_file.ts b/packages/zip-it-and-ship-it/src/runtimes/node/utils/metadata_file.ts new file mode 100644 index 0000000000..ad4edf327c --- /dev/null +++ b/packages/zip-it-and-ship-it/src/runtimes/node/utils/metadata_file.ts @@ -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, +}) diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts b/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts index 657e0583e5..5712460712 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts @@ -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' @@ -44,6 +45,7 @@ const DEFAULT_USER_SUBDIRECTORY = 'src' interface ZipNodeParameters { aliases?: Map basePath: string + branch?: string cache: RuntimeCache destFolder: string extension: string @@ -186,6 +188,7 @@ const createDirectory = async function ({ const createZipArchive = async function ({ aliases = new Map(), basePath, + branch, cache, destFolder, extension, @@ -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) } } diff --git a/packages/zip-it-and-ship-it/src/runtimes/runtime.ts b/packages/zip-it-and-ship-it/src/runtimes/runtime.ts index e527c97adc..2361eb5b55 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/runtime.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/runtime.ts @@ -66,6 +66,7 @@ export type ZipFunction = ( args: { archiveFormat: ArchiveFormat basePath?: string + branch?: string cache: RuntimeCache config: FunctionConfig destFolder: string diff --git a/packages/zip-it-and-ship-it/src/zip.ts b/packages/zip-it-and-ship-it/src/zip.ts index c805e2a934..995ccaa2e0 100644 --- a/packages/zip-it-and-ship-it/src/zip.ts +++ b/packages/zip-it-and-ship-it/src/zip.ts @@ -13,7 +13,7 @@ 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' @@ -21,6 +21,7 @@ import { nonNullable } from './utils/non_nullable.js' export interface ZipFunctionOptions { archiveFormat?: ArchiveFormat basePath?: string + branch?: string config?: Config featureFlags?: FeatureFlags repositoryRoot?: string @@ -53,6 +54,7 @@ export const zipFunctions = async function ( { archiveFormat = ARCHIVE_FORMAT.ZIP, basePath, + branch, config = {}, configFileDirectories, featureFlags: inputFeatureFlags, @@ -63,7 +65,7 @@ export const zipFunctions = async function ( debug, internalSrcFolder, }: ZipFunctionsOptions = {}, -) { +): Promise { validateArchiveFormat(archiveFormat) const logger = getLogger(systemLog, debug) @@ -94,6 +96,7 @@ export const zipFunctions = async function ( const zipResult = await func.runtime.zipFunction({ archiveFormat, basePath, + branch, cache, config: func.config, destFolder, @@ -145,7 +148,7 @@ export const zipFunction = async function ( debug, internalSrcFolder, }: ZipFunctionOptions = {}, -) { +): Promise { validateArchiveFormat(archiveFormat) const logger = getLogger(systemLog, debug) diff --git a/packages/zip-it-and-ship-it/tests/v2api.test.ts b/packages/zip-it-and-ship-it/tests/v2api.test.ts index 1cddf65d41..ff20297f21 100644 --- a/packages/zip-it-and-ship-it/tests/v2api.test.ts +++ b/packages/zip-it-and-ship-it/tests/v2api.test.ts @@ -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, + }) + }) }) })