Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5593f2ac2c | ||
|
|
216d76f1d8 | ||
|
|
499b9678e1 | ||
|
|
78a39ec011 | ||
|
|
d120cc0958 |
+14
-1
@@ -261,7 +261,20 @@ func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.RollbackTraffic()
|
||||
state, snapshotErr := s.lifeboat.Snapshot()
|
||||
if snapshotErr != nil {
|
||||
respond(w, false, snapshotErr)
|
||||
return
|
||||
}
|
||||
// A ReleaseSet must roll back as a whole. The legacy release-only method
|
||||
// would merely flip the traffic mode while retaining a candidate upstream.
|
||||
var ok bool
|
||||
var err error
|
||||
if state.StableReleaseSet != nil {
|
||||
ok, err = s.lifeboat.RollbackSet()
|
||||
} else {
|
||||
ok, err = s.lifeboat.RollbackTraffic()
|
||||
}
|
||||
respond(w, ok, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# This file deliberately owns only one inactive release slot. It joins the
|
||||
# stable project's shared network as an external resource, but has a distinct
|
||||
# Compose project name supplied by run-production-canary.sh. Do not add
|
||||
# durable services or stable services here.
|
||||
services:
|
||||
research-runtime:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/images/runtime.Dockerfile
|
||||
args: { TARGET: research-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: :8087
|
||||
RESEARCH_SOURCE_ROOT: /var/lib/research-sources
|
||||
RESEARCH_HTTP_ALLOWED_HOSTS: assistant.cin.su,git.cin.su
|
||||
RESEARCH_SEARCH_URL_TEMPLATE: ${RESEARCH_SEARCH_URL_TEMPLATE:-https://www.bing.com/search?q=%s}
|
||||
RESEARCH_SEARCH_ALLOWED_HOSTS: ${RESEARCH_SEARCH_ALLOWED_HOSTS:-www.bing.com}
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_research_sources:/var/lib/research-sources:ro
|
||||
networks:
|
||||
shared:
|
||||
aliases: [research-runtime-candidate]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8087/healthz >/dev/null && wget -qO- http://localhost:8087/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
|
||||
core-runtime:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/images/runtime.Dockerfile
|
||||
args: { TARGET: core-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8080"
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
POSTGRES_USER: situation_role
|
||||
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
|
||||
IDENTITY_POSTGRES_USER: identity_role
|
||||
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
|
||||
CREATOR_USERNAME: ${CREATOR_USERNAME}
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
CAPABILITY_POSTGRES_USER: capability_role
|
||||
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
INTERACTION_POSTGRES_USER: interaction_role
|
||||
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
|
||||
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
|
||||
COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR: ${COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR:-60}
|
||||
COGNITIVE_ADMISSION_MAX_INPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_INPUT_BYTES:-81920}
|
||||
COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES:-16384}
|
||||
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
networks:
|
||||
shared:
|
||||
aliases: [core-runtime-candidate]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
|
||||
cognitive-worker:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/images/runtime.Dockerfile
|
||||
args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
|
||||
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-gateway:8080/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-/run/secrets/llm_local_gateway_api_key}
|
||||
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
|
||||
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
|
||||
LOCAL_IMAGE_PROVIDER_KIND: ${LOCAL_IMAGE_PROVIDER_KIND:-comfyui}
|
||||
LOCAL_IMAGE_PROVIDER_URL: ${LOCAL_IMAGE_PROVIDER_URL:-http://comfyui:8188}
|
||||
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
networks:
|
||||
shared:
|
||||
aliases: [cognitive-worker-candidate]
|
||||
llm-local: {}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
|
||||
creator-web:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/images/runtime.Dockerfile
|
||||
args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8084"
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime:8082
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
|
||||
AGENT_WORKER_URL: http://agent-worker:8090
|
||||
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
networks:
|
||||
shared:
|
||||
aliases: [creator-web-candidate]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
assistant_core_secrets:
|
||||
external: true
|
||||
assistant_research_sources:
|
||||
name: assistant_research_sources
|
||||
external: true
|
||||
|
||||
networks:
|
||||
shared:
|
||||
name: assistant_default
|
||||
external: true
|
||||
llm-local:
|
||||
name: llm-local_default
|
||||
external: true
|
||||
@@ -89,17 +89,6 @@ services:
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
|
||||
core-runtime-candidate:
|
||||
profiles: ["assistant-canary"]
|
||||
extends:
|
||||
service: core-runtime
|
||||
restart: "no"
|
||||
environment:
|
||||
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
research-runtime-candidate: { condition: service_started }
|
||||
|
||||
agent-canary:
|
||||
profiles: ["agent-canary"]
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-canary, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
@@ -223,14 +212,6 @@ services:
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
|
||||
research-runtime-candidate:
|
||||
profiles: ["assistant-canary"]
|
||||
extends:
|
||||
service: research-runtime
|
||||
restart: "no"
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
|
||||
continuity-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
@@ -299,17 +280,6 @@ services:
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
|
||||
cognitive-worker-candidate:
|
||||
profiles: ["assistant-canary"]
|
||||
extends:
|
||||
service: cognitive-worker
|
||||
restart: "no"
|
||||
environment:
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
depends_on:
|
||||
core-runtime-candidate: { condition: service_started }
|
||||
cognitive-lifeboat: { condition: service_healthy }
|
||||
|
||||
cognitive-lifeboat:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
restart: unless-stopped
|
||||
@@ -386,28 +356,6 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
creator-web-candidate:
|
||||
profiles: ["assistant-canary"]
|
||||
extends:
|
||||
service: creator-web
|
||||
restart: "no"
|
||||
environment:
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime:8082
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
|
||||
AGENT_WORKER_URL: http://agent-worker:8090
|
||||
depends_on:
|
||||
core-runtime-candidate: { condition: service_started }
|
||||
continuity-runtime: { condition: service_started }
|
||||
commit-runtime: { condition: service_started }
|
||||
cognitive-worker-candidate: { condition: service_healthy }
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
assistant_pg:
|
||||
assistant_nats:
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Stable-only production updater. Candidate slots are managed exclusively by
|
||||
# their isolated release command and Supervisor; this controller never starts
|
||||
# or reconciles a candidate service.
|
||||
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
|
||||
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
|
||||
ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
|
||||
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
|
||||
LOG_FILE="${LOG_FILE:-$BASE_DIR/deploy.log}"
|
||||
LOCK_FILE="${LOCK_FILE:-/tmp/assistant-stable-deploy.lock}"
|
||||
stable_services=(postgres core-runtime continuity-runtime commit-runtime cognitive-worker creator-web notification-receiver research-runtime agent-worker work-graph-runtime)
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || exit 0
|
||||
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$LOG_FILE"; }
|
||||
fail() { log "FAILED: $*"; exit 1; }
|
||||
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||
|
||||
cd "$REPO_DIR"
|
||||
git fetch --prune origin main >>"$LOG_FILE" 2>&1
|
||||
git merge --ff-only origin/main >>"$LOG_FILE" 2>&1 || fail "stable checkout is not a fast-forward to origin/main"
|
||||
revision="$(git rev-parse HEAD)"
|
||||
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || fail "invalid source revision"
|
||||
|
||||
if ! REPO_DIR="$REPO_DIR" ENV_FILE="$ENV_FILE" bash "$REPO_DIR/deploy/apply-migrations.sh" >>"$LOG_FILE" 2>&1; then
|
||||
fail "migration application failed"
|
||||
fi
|
||||
compose config >/dev/null 2>>"$LOG_FILE" || fail "stable compose config validation failed"
|
||||
|
||||
all_current=1
|
||||
for service in "${stable_services[@]}"; do
|
||||
container="assistant-${service}-1"
|
||||
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
|
||||
[[ "$label" == "$revision" ]] || all_current=0
|
||||
done
|
||||
if [[ "$all_current" == 1 ]]; then log "SKIP stable revision=$revision"; exit 0; fi
|
||||
|
||||
log "BUILD stable revision=$revision"
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" "${stable_services[@]}" >>"$LOG_FILE" 2>&1
|
||||
log "UP stable revision=$revision"
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose up -d --no-build "${stable_services[@]}" >>"$LOG_FILE" 2>&1
|
||||
|
||||
for attempt in $(seq 1 30); do
|
||||
if curl -fsS --max-time 5 https://assistant.cin.su/healthz >/dev/null 2>&1; then break; fi
|
||||
[[ "$attempt" == 30 ]] && fail "public health failed"
|
||||
sleep 10
|
||||
done
|
||||
for service in "${stable_services[@]}"; do
|
||||
container="assistant-${service}-1"
|
||||
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
|
||||
[[ "$label" == "$revision" ]] || fail "artifact drift container=$container"
|
||||
done
|
||||
log "OK stable revision=$revision"
|
||||
@@ -1,57 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Candidate services are never part of the stable Compose project. The slot
|
||||
# Compose file may use only declared external networks and durable resources.
|
||||
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
|
||||
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
|
||||
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
|
||||
CANDIDATE_FILE="$BASE_DIR/deploy/docker-compose.candidate-slot.yml"
|
||||
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
|
||||
REVISION="${1:?candidate revision is required}"
|
||||
SLOT="${CANDIDATE_SLOT:-b}"
|
||||
STABLE_PROJECT="${STABLE_PROJECT:-assistant}"
|
||||
CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"
|
||||
HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"
|
||||
PROMOTE_AFTER_CANARY="${PROMOTE_AFTER_CANARY:-true}"
|
||||
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
[[ "$SLOT" =~ ^[a-z0-9-]+$ ]] || { echo "invalid candidate slot" >&2; exit 2; }
|
||||
[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]] || { echo "candidate and stable Compose projects must differ" >&2; exit 2; }
|
||||
|
||||
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||
candidate_compose() { docker compose -p "$CANDIDATE_PROJECT" --env-file "$ENV_FILE" -f "$CANDIDATE_FILE" "$@"; }
|
||||
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
|
||||
fail_rollback() {
|
||||
control /v1/traffic/rollback || true
|
||||
control /v1/releases/rollback || true
|
||||
echo "production ReleaseSet canary failed; stable ReleaseSet rollback requested" >&2
|
||||
echo "isolated candidate ReleaseSet failed; stable ReleaseSet rollback requested" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
services=(creator-web-candidate core-runtime-candidate cognitive-worker-candidate research-runtime-candidate)
|
||||
containers=(assistant-creator-web-candidate-1 assistant-core-runtime-candidate-1 assistant-cognitive-worker-candidate-1 assistant-research-runtime-candidate-1)
|
||||
services=(creator-web core-runtime cognitive-worker research-runtime)
|
||||
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
|
||||
|
||||
stable_ids() {
|
||||
local service container id
|
||||
for service in "${stable_services[@]}"; do
|
||||
container="assistant-${service}-1"
|
||||
container="${STABLE_PROJECT}-${service}-1"
|
||||
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
|
||||
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
|
||||
printf '%s=%s\n' "$service" "$id"
|
||||
done
|
||||
}
|
||||
|
||||
assert_stable_ids_unchanged() {
|
||||
local after
|
||||
after=$(stable_ids) || fail_rollback
|
||||
[[ "$after" == "$stable_ids_before" ]] || {
|
||||
echo "stable container IDs changed during candidate operation" >&2
|
||||
echo "stable container IDs changed during isolated candidate operation" >&2
|
||||
diff -u <(printf '%s\n' "$stable_ids_before") <(printf '%s\n' "$after") >&2 || true
|
||||
fail_rollback
|
||||
}
|
||||
}
|
||||
|
||||
stable_ids_before=$(stable_ids) || exit 1
|
||||
|
||||
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
|
||||
|
||||
candidate_container() { printf '%s-%s-1' "$CANDIDATE_PROJECT" "$1"; }
|
||||
wait_healthy() {
|
||||
local container="$1" status="" ready=""
|
||||
ready=""
|
||||
for attempt in $(seq 1 "$HEALTH_ATTEMPTS"); do
|
||||
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$container" 2>/dev/null || true)
|
||||
if [[ "$status" == healthy ]]; then ready=true; break; fi
|
||||
@@ -60,68 +57,57 @@ wait_healthy() {
|
||||
[[ "$ready" == true ]] || fail_rollback
|
||||
}
|
||||
|
||||
compose --profile assistant-canary up -d --no-build --no-deps research-runtime-candidate core-runtime-candidate
|
||||
wait_healthy assistant-research-runtime-candidate-1
|
||||
wait_healthy assistant-core-runtime-candidate-1
|
||||
compose --profile assistant-canary up -d --no-build --no-deps cognitive-worker-candidate
|
||||
wait_healthy assistant-cognitive-worker-candidate-1
|
||||
compose --profile assistant-canary up -d --no-build --no-deps creator-web-candidate
|
||||
wait_healthy assistant-creator-web-candidate-1
|
||||
stable_ids_before=$(stable_ids) || exit 1
|
||||
candidate_compose config >/dev/null
|
||||
candidate_compose build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
|
||||
assert_stable_ids_unchanged
|
||||
|
||||
for container in "${containers[@]}"; do
|
||||
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$container" 2>/dev/null || true)
|
||||
candidate_compose up -d --no-build --no-deps research-runtime core-runtime
|
||||
wait_healthy "$(candidate_container research-runtime)"
|
||||
wait_healthy "$(candidate_container core-runtime)"
|
||||
candidate_compose up -d --no-build --no-deps cognitive-worker
|
||||
wait_healthy "$(candidate_container cognitive-worker)"
|
||||
candidate_compose up -d --no-build --no-deps creator-web
|
||||
wait_healthy "$(candidate_container creator-web)"
|
||||
assert_stable_ids_unchanged
|
||||
for service in "${services[@]}"; do
|
||||
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$(candidate_container "$service")" 2>/dev/null || true)
|
||||
[[ "$image_revision" == "$REVISION" ]] || fail_rollback
|
||||
done
|
||||
assert_stable_ids_unchanged
|
||||
|
||||
if [[ "${PREPARE_ONLY:-false}" == true ]]; then
|
||||
echo "production ReleaseSet candidate prepared revision=$REVISION"
|
||||
echo "isolated candidate ReleaseSet prepared slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release_json=$(cat <<JSON
|
||||
{
|
||||
"id":"candidate-$REVISION",
|
||||
"revision":"$REVISION",
|
||||
"components":{
|
||||
"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},
|
||||
"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},
|
||||
"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},
|
||||
"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}
|
||||
}
|
||||
}
|
||||
{"id":"candidate-$SLOT-$REVISION","slot":"$SLOT","revision":"$REVISION","components":{"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}}}
|
||||
JSON
|
||||
)
|
||||
|
||||
stable_json=$(curl -fsS --max-time 10 "$SUPERVISOR_CONTROL_URL/v1/state")
|
||||
stable_set=$(printf '%s' "$stable_json" | jq -c '.stable_release_set // empty')
|
||||
if [[ -z "$stable_set" ]]; then
|
||||
stable_revision=$(printf '%s' "$stable_json" | jq -r '.current.version // "unknown"')
|
||||
stable_set=$(cat <<JSON
|
||||
{"id":"stable-$stable_revision","revision":"$stable_revision","components":{
|
||||
"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/readyz"},
|
||||
"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},
|
||||
"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},
|
||||
"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
|
||||
{"id":"stable-$stable_revision","slot":"legacy-stable","revision":"$stable_revision","components":{"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
|
||||
JSON
|
||||
)
|
||||
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/baseline-set" -H 'Content-Type: application/json' --data "$stable_set" >/dev/null || fail_rollback
|
||||
fi
|
||||
|
||||
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/start-set" -H 'Content-Type: application/json' --data "$release_json" >/dev/null || fail_rollback
|
||||
assert_stable_ids_unchanged
|
||||
observation=$(printf '{"release_id":"candidate-%s","ready":true,"reason":"all ReleaseSet healthchecks and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
|
||||
observation=$(printf '{"release_id":"candidate-%s-%s","ready":true,"reason":"isolated slot health and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$SLOT" "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
|
||||
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/observe-set" -H 'Content-Type: application/json' --data "$observation" >/dev/null || fail_rollback
|
||||
assert_stable_ids_unchanged
|
||||
control /v1/traffic/canary || fail_rollback
|
||||
assert_stable_ids_unchanged
|
||||
|
||||
headers=$(mktemp)
|
||||
trap 'rm -f "$headers"' EXIT
|
||||
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
|
||||
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
|
||||
|
||||
if [[ "$PROMOTE_AFTER_CANARY" != true ]]; then
|
||||
echo "isolated candidate ReleaseSet canary ready slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
|
||||
exit 0
|
||||
fi
|
||||
control /v1/releases/promote || fail_rollback
|
||||
assert_stable_ids_unchanged
|
||||
echo "production ReleaseSet canary promoted revision=$REVISION"
|
||||
echo "isolated candidate ReleaseSet promoted slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-COMPAT-OBS-001-production-compatibility-observation.json","task_id":"GEN2-COMPAT-OBS-001","task_revision":2}
|
||||
{"task_file":"dev/tasks/GEN2-DEPLOY-003-isolated-release-slots.json","task_id":"GEN2-DEPLOY-003","task_revision":1}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"task_id": "GEN2-DEPLOY-003",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Изолировать stable и candidate ReleaseSet в независимые Compose deployment projects/slots, чтобы подготовка candidate не могла пересоздать stable containers, сохранив Supervisor как единственный traffic/promotion/rollback control plane.",
|
||||
"requirement_ids": ["REQ-GEN2-DEPLOY-003-SLOT-ISOLATION", "REQ-GEN2-DEPLOY-003-UPDATER-OWNERSHIP", "REQ-GEN2-DEPLOY-003-ROLLBACK"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["deployment control", "runtime supervisor"],
|
||||
"paths": [
|
||||
"deploy/**",
|
||||
"runtime/supervisor/**",
|
||||
"cmd/supervisor/**",
|
||||
"docs/evidence/**",
|
||||
"docs/DEPLOYMENT.md",
|
||||
"tests/deployment/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-DEPLOY-003-isolated-release-slots.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "database/**", "modules/**", "contracts/**", "security/**", "adapters/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {
|
||||
"consume": ["existing Supervisor durable ReleaseSet state", "existing production compose stack", "existing shared durable PostgreSQL/volumes"],
|
||||
"may_change": ["deployment slot topology", "deployment-controller scripts", "Supervisor operational ReleaseSet identity fields"]
|
||||
},
|
||||
"semantic_owner": "Operations/Supervisor; slot and ReleaseSet identity are runtime-only operational control-plane state, not semantic canonical state.",
|
||||
"canonical_state": ["no semantic owner, canonical writer, schema or data migration change", "Supervisor durable release evidence/LKG remains retained outside candidate containers"],
|
||||
"state_transitions": ["stable slot retained -> isolated candidate slot start -> four-component health/revision validation -> Supervisor canary -> promote or traffic rollback -> inactive slot retained for rollback"],
|
||||
"side_effects_and_privileges": ["production operations only through Docker/Compose and Supervisor control plane explicitly authorized for this remediation", "candidate preparation must never directly replace/recreate stable", "no credential reads or outputs", "external updater pause is allowed only as an explicit bounded operation with restore evidence"],
|
||||
"persistence_and_migrations": ["no database migration", "shared PostgreSQL, named volumes and durable services are attached as existing external resources and are never duplicated or deleted by candidate cleanup"],
|
||||
"failure_retry_restart": ["capture stable container IDs before every candidate operation and abort before canary when any changes", "candidate component failure returns traffic to LKG without container recreation", "Supervisor restart reloads ReleaseSet/slot/LKG evidence", "updater ownership must be project-scoped so it cannot reconcile the other slot"],
|
||||
"assumptions": ["current stable may remain in its existing project during the one-time transition; it must not be recreated merely to rename a slot", "external deployment controller can be changed only when its exact owner/process is evidenced", "GEN2-COMPAT-OBS-001, V1 retirement and Gen3 remain stopped until this task passes"],
|
||||
"acceptance_scenarios": [
|
||||
"DEPLOY-003-SCN-01: stable and candidate Compose projects, service/container identities and updater ownership are distinct",
|
||||
"DEPLOY-003-SCN-02: candidate dependencies resolve only to candidate services plus declared shared external resources",
|
||||
"DEPLOY-003-SCN-03: candidate operation leaves every captured stable container ID unchanged; deviation aborts before canary",
|
||||
"DEPLOY-003-SCN-04: all four candidate components report health/readiness and one revision",
|
||||
"DEPLOY-003-SCN-05: Supervisor records ReleaseSet slot identity/revision/LKG and canaries the candidate",
|
||||
"DEPLOY-003-SCN-06: forced candidate component failure rolls all traffic back to previous stable without stable recreate",
|
||||
"DEPLOY-003-SCN-07: Supervisor restart preserves LKG/rollback target and candidate recovery does not alter stable IDs",
|
||||
"DEPLOY-003-EXIT: isolated production ReleaseSet drill passes, after which GEN2-COMPAT-OBS-001 may resume without V1 retirement"
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: compose config for stable and candidate slots",
|
||||
"FAST: targeted go test ./runtime/supervisor ./cmd/supervisor",
|
||||
"FAST: targeted go vet ./runtime/supervisor ./cmd/supervisor",
|
||||
"FAST: deployment shell regression for project/identity isolation and stable ID invariance",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"PRODUCTION: isolated candidate ReleaseSet health, canary, forced rollback, Supervisor restart/LKG and stable-ID drill"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_DEPLOYMENT_CONTROL", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"task": "GEN2-DEPLOY-003",
|
||||
"status": "PASS",
|
||||
"timestamp_utc": "2026-08-30T11:09:35Z",
|
||||
"root_cause": {
|
||||
"stable_and_candidate_project": "assistant",
|
||||
"stable_and_candidate_working_directory": "/home/m/assistant-deploy/repo/deploy",
|
||||
"updater": "/home/m/assistant-deploy/update.sh",
|
||||
"updater_schedule": "cron every five minutes",
|
||||
"unsafe_operation": "the updater ran full docker compose up for the stable project and invoked the old in-project candidate canary"
|
||||
},
|
||||
"remediation": {
|
||||
"stable_project": "assistant",
|
||||
"candidate_project": "assistant-slot-b",
|
||||
"candidate_file": "deploy/docker-compose.candidate-slot.yml",
|
||||
"shared_resources": ["assistant_default external network", "llm-local_default external network", "existing secret and research volumes"],
|
||||
"updater_ownership": "stable-only updater; candidate release command is not invoked by the updater",
|
||||
"supervisor_release_set_slot": "ReleaseSet.slot persisted in durable Supervisor state"
|
||||
},
|
||||
"candidate_revision": "216d76f1d8b4eeba6ee9427da4c87c3e5c728c02",
|
||||
"stable_revision": "2ee5da81145cdad2da1c92c24a91145ad4af61a5",
|
||||
"candidate_components": {
|
||||
"creator-web": "healthy",
|
||||
"core-runtime": "healthy",
|
||||
"cognitive-worker": "healthy",
|
||||
"research-runtime": "healthy"
|
||||
},
|
||||
"drill": {
|
||||
"stable_ids_captured_before_candidate": true,
|
||||
"stable_ids_unchanged_after_candidate_start": true,
|
||||
"header_canary": "candidate slot b",
|
||||
"forced_failure": "candidate cognitive-worker stopped",
|
||||
"rollback": "Supervisor release rollback returned traffic to legacy stable without recreating stable containers",
|
||||
"stable_ids_unchanged_after_rollback": true,
|
||||
"supervisor_restart": "last-known-good legacy stable release set preserved",
|
||||
"candidate_recovery": "candidate cognitive-worker restored healthy",
|
||||
"stable_ids_unchanged_after_candidate_recovery": true
|
||||
},
|
||||
"final_state": {
|
||||
"traffic_mode": "STABLE",
|
||||
"stable_upstream": "http://creator-web:8084",
|
||||
"last_known_good_slot": "legacy-stable",
|
||||
"public_health": 200,
|
||||
"public_ready": 200
|
||||
},
|
||||
"limits": [
|
||||
"The existing stable project remains legacy-stable during the non-disruptive transition; it was not recreated merely to rename it.",
|
||||
"Local MEDIUM discovery was slow/unavailable during candidate cognitive startup; it delayed startup but did not affect stable traffic or the isolated release-set drill.",
|
||||
"GEN2-COMPAT-OBS-001 has not resumed and no V1 path has been retired."
|
||||
]
|
||||
}
|
||||
@@ -46,7 +46,10 @@ type ComponentRelease struct {
|
||||
|
||||
// ReleaseSet is the atomic deployment unit supervised by the control plane.
|
||||
type ReleaseSet struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
// Slot identifies the independently managed deployment slot. An empty value
|
||||
// remains readable for release state recorded before slot isolation.
|
||||
Slot string `json:"slot,omitempty"`
|
||||
Revision string `json:"revision"`
|
||||
Components map[string]ComponentRelease `json:"components"`
|
||||
}
|
||||
@@ -207,6 +210,9 @@ func (l *Lifeboat) PromoteSet() bool {
|
||||
l.Current.Set = &set
|
||||
l.Current.LastKnownGood = true
|
||||
l.CandidateReleaseSet = nil
|
||||
// The promoted ReleaseSet is now the stable target. Leaving header-canary
|
||||
// mode active would keep an obsolete candidate route in the public proxy.
|
||||
l.TrafficMode = TrafficStable
|
||||
return l.setSetUpstreams(&set, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,9 @@ func TestPersistentReleaseSetRollbackPreservesWholeLastKnownGood(t *testing.T) {
|
||||
}
|
||||
first := supervisor.NewPersistentLifeboat(store)
|
||||
good := testReleaseSet("stable-r1", "r1", "http://web", "http://core", "http://cognitive", "http://research")
|
||||
good.Slot = "a"
|
||||
candidate := testReleaseSet("candidate-r2", "r2", "http://web-c", "http://core-c", "http://cognitive-c", "http://research-c")
|
||||
candidate.Slot = "b"
|
||||
if ok, err := first.EstablishBaselineSet(good); err != nil || !ok {
|
||||
t.Fatalf("baseline: %v %v", ok, err)
|
||||
}
|
||||
@@ -135,7 +137,7 @@ func TestPersistentReleaseSetRollbackPreservesWholeLastKnownGood(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.StableReleaseSet == nil || state.StableReleaseSet.Revision != "r1" || state.TrafficMode != supervisor.TrafficStable {
|
||||
if state.StableReleaseSet == nil || state.StableReleaseSet.Revision != "r1" || state.StableReleaseSet.Slot != "a" || state.TrafficMode != supervisor.TrafficStable {
|
||||
t.Fatalf("state=%+v", state)
|
||||
}
|
||||
}
|
||||
@@ -154,6 +156,9 @@ func TestReleaseSetPromotionRetainsPreviousStableForRollback(t *testing.T) {
|
||||
if !l.RecordSet(observation) || !l.PromoteSet() {
|
||||
t.Fatal("promotion failed")
|
||||
}
|
||||
if l.TrafficMode != supervisor.TrafficStable {
|
||||
t.Fatalf("promoted ReleaseSet traffic=%s, want stable", l.TrafficMode)
|
||||
}
|
||||
if l.RollbackTargetReleaseSet == nil || l.RollbackTargetReleaseSet.Revision != "r1" {
|
||||
t.Fatalf("rollback target=%+v", l.RollbackTargetReleaseSet)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,16 @@ set -euo pipefail
|
||||
root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
canary="$root/deploy/run-production-canary.sh"
|
||||
supervisor="$root/deploy/update-supervisor.sh"
|
||||
candidate_compose="$root/deploy/docker-compose.candidate-slot.yml"
|
||||
stable_compose="$root/deploy/docker-compose.prod.yml"
|
||||
stable_updater="$root/deploy/production-stable-updater.sh"
|
||||
|
||||
grep -Fq 'up -d --no-build --no-deps research-runtime-candidate core-runtime-candidate' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps cognitive-worker-candidate' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps creator-web-candidate' "$canary"
|
||||
grep -Fq 'CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"' "$canary"
|
||||
grep -Fq '[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]]' "$canary"
|
||||
grep -Fq 'docker compose -p "$CANDIDATE_PROJECT"' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps research-runtime core-runtime' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps cognitive-worker' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps creator-web' "$canary"
|
||||
grep -Fq 'compose up -d --no-build --no-deps supervisor' "$supervisor"
|
||||
grep -Fq 'stable_ids_before=$(stable_ids)' "$canary"
|
||||
grep -Fq 'assert_stable_ids_unchanged' "$canary"
|
||||
@@ -16,4 +22,11 @@ grep -Fq 'after=$(stable_ids)' "$supervisor"
|
||||
grep -Fq 'stable container IDs changed' "$supervisor"
|
||||
grep -Fq 'HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"' "$canary"
|
||||
grep -Fq 'seq 1 "$HEALTH_ATTEMPTS"' "$canary"
|
||||
grep -Fq 'PROMOTE_AFTER_CANARY="${PROMOTE_AFTER_CANARY:-true}"' "$canary"
|
||||
grep -Fq 'if [[ "$PROMOTE_AFTER_CANARY" != true ]]' "$canary"
|
||||
grep -Fq 'name: assistant_default' "$candidate_compose"
|
||||
grep -Fq 'external: true' "$candidate_compose"
|
||||
! grep -Fq 'candidate:' "$stable_compose"
|
||||
! grep -Fq 'run-production-canary.sh' "$stable_updater"
|
||||
grep -Fq 'compose up -d --no-build "${stable_services[@]}"' "$stable_updater"
|
||||
echo 'stable release-set guard: PASS'
|
||||
|
||||
Reference in New Issue
Block a user