Init repo
Master Production Deploy / production (push) Failing after 7m51s

This commit is contained in:
2026-05-16 23:51:49 +03:00
parent 9f4f99f0f9
commit df60a7b74c
41 changed files with 5107 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -Eeuo pipefail
require_var() {
local name="$1"
if [ -z "${!name:-}" ]; then
printf 'Required environment variable %s is missing\n' "$name" >&2
exit 2
fi
}
quote() {
printf '%q' "$1"
}
require_var SWARM_MANAGER_HOST
require_var SWARM_SSH_USER
require_var ENVIRONMENT
require_var STACK_NAME
require_var RELEASE_ID
require_var COMMIT_SHA
require_var WEB_IMAGE
require_var PUBLIC_HOST
require_var TRAEFIK_NETWORK
SSH_PORT="${SWARM_SSH_PORT:-22}"
SSH_TARGET="${SWARM_SSH_USER}@${SWARM_MANAGER_HOST}"
REMOTE_DEPLOY_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
ssh -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p $(quote "$REMOTE_DEPLOY_DIR")"
scp -P "$SSH_PORT" deploy/swarm/docker-stack.yml "$SSH_TARGET:$REMOTE_DEPLOY_DIR/docker-stack.yml"
scp -P "$SSH_PORT" deploy/remote/deploy-stack.sh "$SSH_TARGET:$REMOTE_DEPLOY_DIR/deploy-stack.sh"
remote_vars=(
ENVIRONMENT STACK_NAME RELEASE_ID COMMIT_SHA WEB_IMAGE PUBLIC_HOST TRAEFIK_NETWORK
REGION TEAM_ID WEB_REPLICAS WEB_PLACEMENT_CONSTRAINT TRAEFIK_ENTRYPOINTS TRAEFIK_TLS REGISTRY REGISTRY_USERNAME REGISTRY_PASSWORD
RELEASE_LOCK_FILE REMOTE_DEPLOY_DIR DEPLOY_MIN_FREE_MB DEPLOY_PRUNE_BEFORE_PULL DEPLOY_PRUNE_UNTIL
DEPLOY_CLEANUP_FAILED_IMAGES VIDEOREG_DB_MEMORY_TTL_MS VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS
)
remote_command=""
for name in "${remote_vars[@]}"; do
if [ -n "${!name:-}" ]; then
remote_command+="$name=$(quote "${!name}") "
fi
done
remote_command+="bash $(quote "$REMOTE_DEPLOY_DIR/deploy-stack.sh")"
ssh -p "$SSH_PORT" "$SSH_TARGET" "$remote_command"
+32
View File
@@ -0,0 +1,32 @@
const required = [
'ENVIRONMENT',
'STACK_NAME',
'RELEASE_ID',
'COMMIT_SHA',
'WEB_IMAGE',
'PUBLIC_HOST',
'TRAEFIK_NETWORK',
'SWARM_MANAGER_HOST',
'SWARM_SSH_USER',
];
const missing = required.filter((name) => !process.env[name]);
if (missing.length > 0) {
console.error(`Missing required deploy variables: ${missing.join(', ')}`);
process.exit(1);
}
if (!/@sha256:[a-f0-9]{64}$/i.test(process.env.WEB_IMAGE ?? '')) {
console.error(`WEB_IMAGE must be pinned by immutable digest, got: ${process.env.WEB_IMAGE}`);
process.exit(1);
}
console.log(
JSON.stringify({
event: 'predeploy_check_passed',
environment: process.env.ENVIRONMENT,
stack: process.env.STACK_NAME,
releaseId: process.env.RELEASE_ID,
commitSha: process.env.COMMIT_SHA,
}),
);
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -Eeuo pipefail
require_var() {
local name="$1"
if [ -z "${!name:-}" ]; then
printf 'Required environment variable %s is missing\n' "$name" >&2
exit 2
fi
}
quote() {
printf '%q' "$1"
}
require_var SWARM_MANAGER_HOST
require_var SWARM_SSH_USER
require_var ENVIRONMENT
require_var STACK_NAME
SSH_PORT="${SWARM_SSH_PORT:-22}"
SSH_TARGET="${SWARM_SSH_USER}@${SWARM_MANAGER_HOST}"
REMOTE_DEPLOY_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
ssh -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p $(quote "$REMOTE_DEPLOY_DIR")"
scp -P "$SSH_PORT" deploy/remote/rollback-stack.sh "$SSH_TARGET:$REMOTE_DEPLOY_DIR/rollback-stack.sh"
remote_command="ENVIRONMENT=$(quote "$ENVIRONMENT") STACK_NAME=$(quote "$STACK_NAME") "
if [ -n "${RELEASE_LOCK_FILE:-}" ]; then
remote_command+="RELEASE_LOCK_FILE=$(quote "$RELEASE_LOCK_FILE") "
fi
remote_command+="bash $(quote "$REMOTE_DEPLOY_DIR/rollback-stack.sh")"
ssh -p "$SSH_PORT" "$SSH_TARGET" "$remote_command"
+138
View File
@@ -0,0 +1,138 @@
import { writeFileSync } from 'node:fs';
const baseUrl = process.env.BASE_URL?.replace(/\/$/, '');
const expectedReleaseId = process.env.EXPECTED_RELEASE_ID;
const smokeWrite = process.env.SMOKE_WRITE === 'true';
const outputPath = process.env.SMOKE_RESULT_PATH;
const timeoutMs = Number(process.env.SMOKE_TIMEOUT_MS ?? 5000);
const retryAttempts = Number(process.env.SMOKE_RETRY_ATTEMPTS ?? 12);
const retryDelayMs = Number(process.env.SMOKE_RETRY_DELAY_MS ?? 5000);
const retryableStatuses = new Set([404, 408, 425, 429, 500, 502, 503, 504]);
if (!baseUrl) {
console.error('BASE_URL is required for smoke checks');
process.exit(2);
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function requestOnce(path, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: {
'content-type': 'application/json',
...options.headers,
},
signal: AbortSignal.timeout(timeoutMs),
});
const text = await response.text();
let body;
try {
body = text ? JSON.parse(text) : undefined;
} catch {
body = text;
}
if (!response.ok) {
const error = new Error(`${path} returned ${response.status}: ${text}`);
error.status = response.status;
throw error;
}
return body;
}
async function request(path, options = {}) {
let lastError;
for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
try {
return await requestOnce(path, options);
} catch (error) {
lastError = error;
const status = error?.status;
const retryableStatus = typeof status === 'number' && retryableStatuses.has(status);
const retryableNetworkError = error?.name === 'TimeoutError' || error?.cause?.code;
if (attempt >= retryAttempts || (!retryableStatus && !retryableNetworkError)) {
throw error;
}
await sleep(retryDelayMs);
}
}
throw lastError;
}
function assertRelease(body) {
if (!expectedReleaseId) return;
const actual = body?.metadata?.releaseId;
if (actual !== expectedReleaseId) {
throw new Error(`service reports release ${actual}, expected ${expectedReleaseId}`);
}
}
async function run() {
const startedAt = Date.now();
const checks = [];
const live = await request('/health/live');
assertRelease(live);
checks.push({ name: 'web_live', status: 'ok' });
const ready = await request('/health/ready');
assertRelease(ready);
checks.push({ name: 'web_ready', status: 'ok' });
const registry = await request('/api/videos');
if (!registry.stats || !Array.isArray(registry.videos)) {
throw new Error('Video registry API returned an invalid shape');
}
checks.push({ name: 'registry_read', status: 'ok' });
if (smokeWrite) {
await request('/api/videos', {
method: 'POST',
body: JSON.stringify({
title: `Smoke video ${Date.now()}`,
owner: 'Deploy smoke',
sourceUrl: 'https://example.com/smoke',
status: 'draft',
}),
});
checks.push({ name: 'registry_write', status: 'ok' });
}
const result = {
status: 'ok',
baseUrl,
expectedReleaseId,
checks,
durationMs: Date.now() - startedAt,
};
if (outputPath) {
writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
}
console.log(JSON.stringify({ event: 'smoke_check_success', ...result }));
}
run().catch((error) => {
const result = {
status: 'error',
baseUrl,
expectedReleaseId,
error: error instanceof Error ? error.message : String(error),
};
if (outputPath) {
writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
}
console.error(JSON.stringify({ event: 'smoke_check_failure', ...result }));
process.exit(1);
});
+30
View File
@@ -0,0 +1,30 @@
import { readFileSync } from 'node:fs';
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required`);
}
return value;
}
function readJson(path) {
return JSON.parse(readFileSync(path, 'utf8'));
}
const smoke = readJson(requireEnv('SMOKE_RESULT_PATH'));
const manifest = {
commitSha: requireEnv('COMMIT_SHA'),
releaseId: requireEnv('RELEASE_ID'),
webImageDigest: requireEnv('WEB_IMAGE_DIGEST'),
deployTimestamp: new Date().toISOString(),
smokeResult: smoke,
workflowRunId: requireEnv('WORKFLOW_RUN_ID'),
};
if (!/@sha256:[a-f0-9]{64}$/i.test(manifest.webImageDigest)) {
throw new Error('webImageDigest must be digest-pinned');
}
console.log(JSON.stringify(manifest, null, 2));