@netlify/build 29.33.6 → 29.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core/dry.js +3 -0
- package/lib/plugins_core/deploy_config/index.d.ts +2 -0
- package/lib/plugins_core/deploy_config/index.js +70 -0
- package/lib/plugins_core/deploy_config/util.d.ts +12 -0
- package/lib/plugins_core/deploy_config/util.js +32 -0
- package/lib/plugins_core/types.d.ts +4 -2
- package/lib/steps/get.d.ts +3 -2
- package/lib/steps/get.js +3 -1
- package/lib/steps/run_step.d.ts +2 -1
- package/lib/steps/run_step.js +6 -4
- package/lib/steps/run_steps.js +2 -1
- package/package.json +6 -6
package/lib/core/dry.js
CHANGED
|
@@ -8,6 +8,9 @@ export const doDryRun = async function ({ buildDir, steps, netlifyConfig, consta
|
|
|
8
8
|
const stepsCount = successSteps.length;
|
|
9
9
|
logDryRunStart({ logs, eventWidth, stepsCount });
|
|
10
10
|
successSteps.forEach((step, index) => {
|
|
11
|
+
if (step.quiet) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
11
14
|
logDryRunStep({ logs, step, index, netlifyConfig, eventWidth, stepsCount });
|
|
12
15
|
});
|
|
13
16
|
logDryRunEnd(logs);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { mergeConfigs } from '@netlify/config';
|
|
4
|
+
import { filterConfig } from './util.js';
|
|
5
|
+
// The properties that can be set using this API. Each element represents a
|
|
6
|
+
// path using dot-notation — e.g. `["build", "functions"]` represents the
|
|
7
|
+
// `build.functions` property.
|
|
8
|
+
const ALLOWED_PROPERTIES = [
|
|
9
|
+
['build', 'edge_functions'],
|
|
10
|
+
['build', 'functions'],
|
|
11
|
+
['build', 'publish'],
|
|
12
|
+
['functions', '*'],
|
|
13
|
+
['functions', '*', '*'],
|
|
14
|
+
['headers'],
|
|
15
|
+
['images', 'remote_images'],
|
|
16
|
+
['redirects'],
|
|
17
|
+
];
|
|
18
|
+
// For array properties, any values set in this API will be merged with the
|
|
19
|
+
// main configuration file in such a way that user-defined values always take
|
|
20
|
+
// precedence. The exception are these properties that let frameworks set
|
|
21
|
+
// values that should be evaluated before any user-defined values. They use
|
|
22
|
+
// a special notation where `headers!` represents "forced headers", etc.
|
|
23
|
+
const OVERRIDE_PROPERTIES = new Set(['headers!', 'redirects!']);
|
|
24
|
+
const coreStep = async function ({ buildDir, netlifyConfig, systemLog = () => {
|
|
25
|
+
// no-op
|
|
26
|
+
}, }) {
|
|
27
|
+
const configPath = resolve(buildDir, '.netlify/deploy/v1/config.json');
|
|
28
|
+
let config = {};
|
|
29
|
+
try {
|
|
30
|
+
const data = await fs.readFile(configPath, 'utf8');
|
|
31
|
+
config = JSON.parse(data);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
// If the file doesn't exist, this is a non-error.
|
|
35
|
+
if (err.code === 'ENOENT') {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
systemLog(`Failed to read Deploy Configuration API: ${err.message}`);
|
|
39
|
+
throw new Error('An error occured while processing the platform configurarion defined by your framework');
|
|
40
|
+
}
|
|
41
|
+
const configOverrides = {};
|
|
42
|
+
for (const key in config) {
|
|
43
|
+
// If the key uses the special notation for defining mutations that should
|
|
44
|
+
// take precedence over user-defined properties, extract the canonical
|
|
45
|
+
// property, set it on a different object, and delete it from the main one.
|
|
46
|
+
if (OVERRIDE_PROPERTIES.has(key)) {
|
|
47
|
+
const canonicalKey = key.slice(0, -1);
|
|
48
|
+
configOverrides[canonicalKey] = config[key];
|
|
49
|
+
delete config[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Filtering out any properties that can't be mutated using this API.
|
|
53
|
+
config = filterConfig(config, [], ALLOWED_PROPERTIES, systemLog);
|
|
54
|
+
// Merging the different configuration sources. The order here is important.
|
|
55
|
+
// Leftmost elements of the array take precedence.
|
|
56
|
+
const newConfig = mergeConfigs([config, netlifyConfig, configOverrides], { concatenateArrays: true });
|
|
57
|
+
for (const key in newConfig) {
|
|
58
|
+
netlifyConfig[key] = newConfig[key];
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
};
|
|
62
|
+
export const applyDeployConfig = {
|
|
63
|
+
event: 'onBuild',
|
|
64
|
+
coreStep,
|
|
65
|
+
coreStepId: 'deploy_config',
|
|
66
|
+
coreStepName: 'Applying Deploy Configuration',
|
|
67
|
+
coreStepDescription: () => '',
|
|
68
|
+
condition: ({ featureFlags }) => featureFlags?.netlify_build_deploy_configuration_api,
|
|
69
|
+
quiet: true,
|
|
70
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SystemLogger } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks whether a property can be defined using the Deploy Configuration API.
|
|
4
|
+
*/
|
|
5
|
+
export declare const isAllowedProperty: (property: string[], allowedProperties: string[][]) => boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Takes a candidate configuration object and returns a normalized version that
|
|
8
|
+
* includes only the properties that are present in an allow-list. It does so
|
|
9
|
+
* recursively, so `allowedProperties` can contain the full list of properties
|
|
10
|
+
* that the Deploy Configuration API can interact with.
|
|
11
|
+
*/
|
|
12
|
+
export declare const filterConfig: (obj: Record<string, unknown>, path: string[], allowedProperties: string[][], systemLog: SystemLogger) => Record<string, unknown>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import isPlainObject from 'is-plain-obj';
|
|
2
|
+
import mapObject, { mapObjectSkip } from 'map-obj';
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a property matches a template that may contain wildcards.
|
|
5
|
+
* Both the property and the template use a dot-notation represented as an
|
|
6
|
+
* array of strings.
|
|
7
|
+
* For example, the property `["build", "command"]` matches the templates
|
|
8
|
+
* `["build", "command"]` and `["build", "*"]`, but not `["functions"]`.
|
|
9
|
+
*/
|
|
10
|
+
const propertyMatchesTemplate = (property, template) => property.every((node, index) => template[index] === node || template[index] === '*');
|
|
11
|
+
/**
|
|
12
|
+
* Checks whether a property can be defined using the Deploy Configuration API.
|
|
13
|
+
*/
|
|
14
|
+
export const isAllowedProperty = (property, allowedProperties) => allowedProperties.some((template) => propertyMatchesTemplate(property, template));
|
|
15
|
+
/**
|
|
16
|
+
* Takes a candidate configuration object and returns a normalized version that
|
|
17
|
+
* includes only the properties that are present in an allow-list. It does so
|
|
18
|
+
* recursively, so `allowedProperties` can contain the full list of properties
|
|
19
|
+
* that the Deploy Configuration API can interact with.
|
|
20
|
+
*/
|
|
21
|
+
export const filterConfig = (obj, path, allowedProperties, systemLog) => mapObject(obj, (key, value) => {
|
|
22
|
+
const keyPath = [...path, key];
|
|
23
|
+
if (!isAllowedProperty(keyPath, allowedProperties)) {
|
|
24
|
+
systemLog(`Discarding property that is not supported by the Deploy Configuration API: ${keyPath.join('.')}`);
|
|
25
|
+
return mapObjectSkip;
|
|
26
|
+
}
|
|
27
|
+
if (!isPlainObject(value)) {
|
|
28
|
+
systemLog(`Loading property from Deploy Configuration API: ${keyPath.join('.')}`);
|
|
29
|
+
return [key, value];
|
|
30
|
+
}
|
|
31
|
+
return [key, filterConfig(value, keyPath, allowedProperties, systemLog)];
|
|
32
|
+
});
|
|
@@ -28,13 +28,15 @@ export type CoreStepFunctionArgs = {
|
|
|
28
28
|
};
|
|
29
29
|
export type CoreStepFunction = (args: CoreStepFunctionArgs) => Promise<object>;
|
|
30
30
|
export type CoreStepCondition = (args: CoreStepFunctionArgs) => Promise<boolean> | boolean;
|
|
31
|
-
type Event = 'onPreBuild' | 'onBuild' | 'onPostBuild' | 'onPreDev' | 'onDev' | 'onPostDev';
|
|
31
|
+
export type Event = 'onPreBuild' | 'onBuild' | 'onPostBuild' | 'onPreDev' | 'onDev' | 'onPostDev';
|
|
32
32
|
export type CoreStep = {
|
|
33
33
|
event: Event;
|
|
34
34
|
coreStep: CoreStepFunction;
|
|
35
35
|
coreStepId: string;
|
|
36
36
|
coreStepName: string;
|
|
37
37
|
coreStepDescription: () => string;
|
|
38
|
-
condition
|
|
38
|
+
condition?: CoreStepCondition;
|
|
39
|
+
quiet?: boolean;
|
|
39
40
|
};
|
|
41
|
+
export type SystemLogger = (...args: any[]) => void;
|
|
40
42
|
export {};
|
package/lib/steps/get.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { CoreStep } from '../plugins_core/types.js';
|
|
1
2
|
export declare const getSteps: (steps: any, eventHandlers?: any[]) => {
|
|
2
|
-
steps:
|
|
3
|
+
steps: CoreStep[];
|
|
3
4
|
events: unknown[];
|
|
4
5
|
};
|
|
5
6
|
export declare const getDevSteps: (command: any, steps: any, eventHandlers?: any[]) => {
|
|
6
|
-
steps:
|
|
7
|
+
steps: CoreStep[];
|
|
7
8
|
events: unknown[];
|
|
8
9
|
};
|
package/lib/steps/get.js
CHANGED
|
@@ -3,6 +3,7 @@ import { DEV_EVENTS, EVENTS } from '../plugins/events.js';
|
|
|
3
3
|
import { uploadBlobs } from '../plugins_core/blobs_upload/index.js';
|
|
4
4
|
import { buildCommandCore } from '../plugins_core/build_command.js';
|
|
5
5
|
import { deploySite } from '../plugins_core/deploy/index.js';
|
|
6
|
+
import { applyDeployConfig } from '../plugins_core/deploy_config/index.js';
|
|
6
7
|
import { bundleEdgeFunctions } from '../plugins_core/edge_functions/index.js';
|
|
7
8
|
import { bundleFunctions } from '../plugins_core/functions/index.js';
|
|
8
9
|
import { preCleanup } from '../plugins_core/pre_cleanup/index.js';
|
|
@@ -44,7 +45,7 @@ const getEventSteps = function (eventHandlers) {
|
|
|
44
45
|
handler = eventHandler.handler;
|
|
45
46
|
}
|
|
46
47
|
return {
|
|
47
|
-
event,
|
|
48
|
+
event: event,
|
|
48
49
|
coreStep: (args) => {
|
|
49
50
|
const { constants, event } = args;
|
|
50
51
|
const utils = getUtils({ event, constants, runState: {} });
|
|
@@ -60,6 +61,7 @@ const addCoreSteps = function (steps) {
|
|
|
60
61
|
return [
|
|
61
62
|
preCleanup,
|
|
62
63
|
buildCommandCore,
|
|
64
|
+
applyDeployConfig,
|
|
63
65
|
...steps,
|
|
64
66
|
bundleFunctions,
|
|
65
67
|
bundleEdgeFunctions,
|
package/lib/steps/run_step.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const runStep: ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }: {
|
|
1
|
+
export declare const runStep: ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, coreStepQuiet, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }: {
|
|
2
2
|
event: any;
|
|
3
3
|
childProcess: any;
|
|
4
4
|
packageName: any;
|
|
@@ -6,6 +6,7 @@ export declare const runStep: ({ event, childProcess, packageName, coreStep, cor
|
|
|
6
6
|
coreStepId: any;
|
|
7
7
|
coreStepName: any;
|
|
8
8
|
coreStepDescription: any;
|
|
9
|
+
coreStepQuiet: any;
|
|
9
10
|
pluginPackageJson: any;
|
|
10
11
|
loadedFrom: any;
|
|
11
12
|
origin: any;
|
package/lib/steps/run_step.js
CHANGED
|
@@ -10,7 +10,7 @@ import { firePluginStep } from './plugin.js';
|
|
|
10
10
|
import { getStepReturn } from './return.js';
|
|
11
11
|
const tracer = trace.getTracer('steps');
|
|
12
12
|
// Run a step (core, build command or plugin)
|
|
13
|
-
export const runStep = async function ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }) {
|
|
13
|
+
export const runStep = async function ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, coreStepQuiet, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }) {
|
|
14
14
|
// Add relevant attributes to the upcoming span context
|
|
15
15
|
const attributes = {
|
|
16
16
|
'build.execution.step.name': coreStepName,
|
|
@@ -41,13 +41,14 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
|
|
|
41
41
|
saveConfig,
|
|
42
42
|
explicitSecretKeys,
|
|
43
43
|
deployId,
|
|
44
|
+
featureFlags,
|
|
44
45
|
});
|
|
45
46
|
span.setAttribute('build.execution.step.should_run', shouldRun);
|
|
46
47
|
if (!shouldRun) {
|
|
47
48
|
span.end();
|
|
48
49
|
return {};
|
|
49
50
|
}
|
|
50
|
-
if (!quiet) {
|
|
51
|
+
if (!quiet && !coreStepQuiet) {
|
|
51
52
|
logStepStart({ logs, event, packageName, coreStepDescription, error, netlifyConfig });
|
|
52
53
|
}
|
|
53
54
|
const fireStep = getFireStep(packageName, coreStepId, event);
|
|
@@ -118,7 +119,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
|
|
|
118
119
|
durationNs,
|
|
119
120
|
testOpts,
|
|
120
121
|
systemLog,
|
|
121
|
-
quiet,
|
|
122
|
+
quiet: quiet || coreStepQuiet,
|
|
122
123
|
metrics,
|
|
123
124
|
});
|
|
124
125
|
span.end();
|
|
@@ -156,7 +157,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
|
|
|
156
157
|
// or available. However, one might be created by a build plugin, in which case,
|
|
157
158
|
// those core plugins should be triggered. We use a dynamic `condition()` to
|
|
158
159
|
// model this behavior.
|
|
159
|
-
const shouldRunStep = async function ({ event, packageName, error, packagePath, failedPlugins, netlifyConfig, condition, constants, buildbotServerSocket, buildDir, saveConfig, explicitSecretKeys, deployId, }) {
|
|
160
|
+
const shouldRunStep = async function ({ event, packageName, error, packagePath, failedPlugins, netlifyConfig, condition, constants, buildbotServerSocket, buildDir, saveConfig, explicitSecretKeys, deployId, featureFlags = {}, }) {
|
|
160
161
|
if (failedPlugins.includes(packageName) ||
|
|
161
162
|
(condition !== undefined &&
|
|
162
163
|
!(await condition({
|
|
@@ -168,6 +169,7 @@ const shouldRunStep = async function ({ event, packageName, error, packagePath,
|
|
|
168
169
|
saveConfig,
|
|
169
170
|
explicitSecretKeys,
|
|
170
171
|
deployId,
|
|
172
|
+
featureFlags,
|
|
171
173
|
})))) {
|
|
172
174
|
return false;
|
|
173
175
|
}
|
package/lib/steps/run_steps.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runStep } from './run_step.js';
|
|
|
8
8
|
// If an error arises, runs `onError` events.
|
|
9
9
|
// Runs `onEnd` events at the end, whether an error was thrown or not.
|
|
10
10
|
export const runSteps = async function ({ steps, buildbotServerSocket, events, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, childEnv, context, branch, constants, mode, api, errorMonitor, deployId, errorParams, netlifyConfig, configOpts, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }) {
|
|
11
|
-
const { index: stepsCount, error: errorA, netlifyConfig: netlifyConfigC, statuses: statusesB, failedPlugins: failedPluginsA, timers: timersC, configMutations: configMutationsB, metrics: metricsC, } = await pReduce(steps, async ({ index, error, failedPlugins, envChanges, netlifyConfig: netlifyConfigA, configMutations, headersPath: headersPathA, redirectsPath: redirectsPathA, statuses, timers: timersA, metrics: metricsA, }, { event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, }) => {
|
|
11
|
+
const { index: stepsCount, error: errorA, netlifyConfig: netlifyConfigC, statuses: statusesB, failedPlugins: failedPluginsA, timers: timersC, configMutations: configMutationsB, metrics: metricsC, } = await pReduce(steps, async ({ index, error, failedPlugins, envChanges, netlifyConfig: netlifyConfigA, configMutations, headersPath: headersPathA, redirectsPath: redirectsPathA, statuses, timers: timersA, metrics: metricsA, }, { event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, quiet: coreStepQuiet, }) => {
|
|
12
12
|
const { newIndex = index, newError = error, failedPlugin = [], newEnvChanges = {}, netlifyConfig: netlifyConfigB = netlifyConfigA, configMutations: configMutationsA = configMutations, headersPath: headersPathB = headersPathA, redirectsPath: redirectsPathB = redirectsPathA, newStatus, timers: timersB = timersA, metrics: metricsB = [], } = await runStep({
|
|
13
13
|
event,
|
|
14
14
|
childProcess,
|
|
@@ -17,6 +17,7 @@ export const runSteps = async function ({ steps, buildbotServerSocket, events, c
|
|
|
17
17
|
coreStepId,
|
|
18
18
|
coreStepName,
|
|
19
19
|
coreStepDescription,
|
|
20
|
+
coreStepQuiet,
|
|
20
21
|
pluginPackageJson,
|
|
21
22
|
loadedFrom,
|
|
22
23
|
origin,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/build",
|
|
3
|
-
"version": "29.
|
|
3
|
+
"version": "29.34.0",
|
|
4
4
|
"description": "Netlify build module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/index.js",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@bugsnag/js": "^7.0.0",
|
|
71
71
|
"@netlify/blobs": "^6.5.0",
|
|
72
72
|
"@netlify/cache-utils": "^5.1.5",
|
|
73
|
-
"@netlify/config": "^20.
|
|
73
|
+
"@netlify/config": "^20.12.0",
|
|
74
74
|
"@netlify/edge-bundler": "11.2.2",
|
|
75
75
|
"@netlify/framework-info": "^9.8.10",
|
|
76
76
|
"@netlify/functions-utils": "^5.2.50",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"@netlify/nock-udp": "^3.1.2",
|
|
131
131
|
"@opentelemetry/sdk-trace-base": "^1.18.1",
|
|
132
132
|
"@types/node": "^14.18.53",
|
|
133
|
-
"@vitest/coverage-c8": "^0.
|
|
133
|
+
"@vitest/coverage-c8": "^0.33.0",
|
|
134
134
|
"atob": "^2.1.2",
|
|
135
135
|
"ava": "^4.0.0",
|
|
136
136
|
"c8": "^7.12.0",
|
|
@@ -148,8 +148,8 @@
|
|
|
148
148
|
"process-exists": "^5.0.0",
|
|
149
149
|
"sinon": "^13.0.0",
|
|
150
150
|
"tmp-promise": "^3.0.2",
|
|
151
|
-
"tsd": "^0.
|
|
152
|
-
"vitest": "^0.
|
|
151
|
+
"tsd": "^0.30.0",
|
|
152
|
+
"vitest": "^0.34.0",
|
|
153
153
|
"yarn": "^1.22.4"
|
|
154
154
|
},
|
|
155
155
|
"peerDependencies": {
|
|
@@ -163,5 +163,5 @@
|
|
|
163
163
|
"engines": {
|
|
164
164
|
"node": "^14.16.0 || >=16.0.0"
|
|
165
165
|
},
|
|
166
|
-
"gitHead": "
|
|
166
|
+
"gitHead": "24f130e0d2e27091cd5998cb23f19718e3785824"
|
|
167
167
|
}
|