Files
deployments/main.js

169 lines
4.6 KiB
JavaScript

const Sentry = require("@sentry/node");
const {
SERVICE = "unknown-service",
COMPONENT = "deployment",
SENTRY_DSN = "",
AUTH = "",
DEPLOY_IMAGE = "hello-world",
DEPLOY_VERSION = "qa",
DEPLOY_WEBHOOK_URL = "https://n8n.ops.makecodes.dev/webhook/deployments",
DEPLOY_TIMEOUT_MS = "3600000",
} = process.env;
const fetchFn = global.fetch;
const { AbortSignal } = global;
if (typeof fetchFn !== "function" || !AbortSignal?.timeout) {
throw new Error("Required web APIs (fetch, AbortSignal.timeout) are unavailable.");
}
Sentry.init({
dsn: SENTRY_DSN,
sendDefaultPii: false,
enableLogs: true,
environment: "ci",
_experiments: {
enableMetrics: true,
},
});
const logger = Sentry.logger;
const baseMetricAttributes = {
environment: "ci",
service: SERVICE,
component: COMPONENT,
};
const METRIC_NAMES = {
WEBHOOK_ATTEMPT: "deploy.webhook_attempt",
WEBHOOK_FAILURE: "deploy.webhook_failure",
WEBHOOK_SUCCESS: "deploy.webhook_success",
WEBHOOK_DURATION: "deploy.webhook_duration_ms",
RUN_STARTED: "deploy.run_started",
RUN_SUCCEEDED: "deploy.run_succeeded",
RUN_FAILED: "deploy.run_failed",
RUN_DURATION: "deploy.run_duration_ms",
};
const metrics = {
count: (name, value = 1, attributes = {}) => {
Sentry.metrics?.count?.(name, value, {
attributes: { ...baseMetricAttributes, ...attributes },
});
},
distribution: (name, value, { unit, attributes = {} } = {}) => {
Sentry.metrics?.distribution?.(name, value, {
...(unit && { unit }),
attributes: { ...baseMetricAttributes, ...attributes },
});
},
};
const requireEnv = (value, name) => {
if (!value) {
logger.error(logger.fmt`Missing required environment variable '${name}'.`);
throw new Error(`Missing required environment variable '${name}'.`);
}
return value;
};
const triggerDeployment = async () => {
requireEnv(AUTH, "AUTH");
requireEnv(DEPLOY_WEBHOOK_URL, "DEPLOY_WEBHOOK_URL");
const payload = {
service: SERVICE,
image: DEPLOY_IMAGE,
version: DEPLOY_VERSION,
component: COMPONENT,
};
const signal = AbortSignal.timeout(Number(DEPLOY_TIMEOUT_MS) || 3600000);
logger.info(
logger.fmt`Starting deployment for '${SERVICE}' using version '${DEPLOY_VERSION}'.`,
payload
);
metrics.count(METRIC_NAMES.WEBHOOK_ATTEMPT, 1);
const webhookStart = Date.now();
const response = await fetchFn(DEPLOY_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${AUTH}`,
},
body: JSON.stringify(payload),
signal,
});
metrics.distribution(METRIC_NAMES.WEBHOOK_DURATION, Date.now() - webhookStart, {
unit: "millisecond",
attributes: { status: response.status },
});
const bodyText = await response.text();
logger.debug("Deployment webhook response body", {
service: SERVICE,
status: response.status,
body: bodyText,
});
if (!response.ok) {
metrics.count(METRIC_NAMES.WEBHOOK_FAILURE, 1, { status: response.status });
logger.error(
logger.fmt`Deployment failed for '${SERVICE}' with status '${response.status}'.`,
{ body: bodyText }
);
throw new Error(
`Deployment failed for '${SERVICE}' with status '${response.status}'.`
);
}
metrics.count(METRIC_NAMES.WEBHOOK_SUCCESS, 1, { status: response.status });
logger.info(
logger.fmt`Deployment succeeded for '${SERVICE}' with status '${response.status}'.`
);
};
async function main() {
if (!SENTRY_DSN) {
throw new Error("Missing required environment variable 'SENTRY_DSN'.");
}
metrics.count(METRIC_NAMES.RUN_STARTED, 1);
const startedAt = Date.now();
let success = false;
try {
await triggerDeployment();
success = true;
metrics.count(METRIC_NAMES.RUN_SUCCEEDED, 1);
logger.info(logger.fmt`Deployment script completed for '${SERVICE}'.`);
} catch (error) {
metrics.count(METRIC_NAMES.RUN_FAILED, 1, {
reason: error?.name || "Error",
});
logger.error("Failed to run deployment script", {
error: error.stack || error,
});
} finally {
metrics.distribution(METRIC_NAMES.RUN_DURATION, Date.now() - startedAt, {
unit: "millisecond",
attributes: { status: success ? "success" : "failure" },
});
await Sentry.flush(5000);
if (!success) {
process.exit(1);
}
}
}
main();