Files
projectbrain-Public/scripts/stage-release-oci.sh
T

96 lines
3.9 KiB
Bash

#!/usr/bin/env sh
set -eu
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <40-character-commit-sha> <release-artifact-directory>" >&2
exit 64
fi
RELEASE_SHA="$1"
ARTIFACT_DIR="${2%/}"
case "${RELEASE_SHA}" in
*[!0-9a-f]*|'') echo "Release SHA must contain exactly 40 lowercase hexadecimal characters." >&2; exit 64 ;;
esac
if [ "${#RELEASE_SHA}" -ne 40 ]; then
echo "Release SHA must contain exactly 40 lowercase hexadecimal characters." >&2
exit 64
fi
API_ARCHIVE="${ARTIFACT_DIR}/projectbrain-api-${RELEASE_SHA}.tar"
WEB_ARCHIVE="${ARTIFACT_DIR}/projectbrain-web-${RELEASE_SHA}.tar"
DIGEST_FILE="${ARTIFACT_DIR}/archive-sha256.txt"
for path in "${API_ARCHIVE}" "${WEB_ARCHIVE}" "${DIGEST_FILE}"; do
if [ ! -f "${path}" ]; then echo "Missing release artifact: ${path}" >&2; exit 66; fi
done
for command in sha256sum skopeo docker curl; do
if ! command -v "${command}" >/dev/null 2>&1; then echo "Required command is unavailable: ${command}" >&2; exit 69; fi
done
verify_digest() {
archive="$1"
filename="$(basename "${archive}")"
expected="$(awk -v file="${filename}" '$2 == file || $2 ~ ("/" file "$") { print $1; exit }' "${DIGEST_FILE}")"
actual="$(sha256sum "${archive}" | awk '{ print $1 }')"
if [ -z "${expected}" ] || [ "${actual}" != "${expected}" ]; then
echo "Digest mismatch for ${filename}." >&2
exit 65
fi
printf '%s' "${actual}"
}
API_DIGEST="$(verify_digest "${API_ARCHIVE}")"
WEB_DIGEST="$(verify_digest "${WEB_ARCHIVE}")"
skopeo copy "oci-archive:${API_ARCHIVE}" "docker-daemon:projectbrain-api:${RELEASE_SHA}"
skopeo copy "oci-archive:${WEB_ARCHIVE}" "docker-daemon:projectbrain-web:${RELEASE_SHA}"
API_IMAGE_ID="$(docker image inspect --format '{{.Id}}' "projectbrain-api:${RELEASE_SHA}")"
WEB_IMAGE_ID="$(docker image inspect --format '{{.Id}}' "projectbrain-web:${RELEASE_SHA}")"
: "${POSTGRES_PASSWORD:?set a non-default POSTGRES_PASSWORD in the environment or .env}"
: "${PROJECTBRAIN_OWNER_PASSWORD:?set PROJECTBRAIN_OWNER_PASSWORD in the environment or .env}"
: "${PROJECTBRAIN_AUTH_SECRET:?set PROJECTBRAIN_AUTH_SECRET in the environment or .env}"
if [ "${POSTGRES_PASSWORD}" = "projectbrain" ]; then echo "Default PostgreSQL password is forbidden." >&2; exit 78; fi
if [ "${#PROJECTBRAIN_OWNER_PASSWORD}" -lt 12 ]; then echo "Owner password must contain at least 12 characters." >&2; exit 78; fi
if [ "${#PROJECTBRAIN_AUTH_SECRET}" -lt 32 ]; then echo "Auth secret must contain at least 32 characters." >&2; exit 78; fi
export PROJECTBRAIN_RELEASE_SHA="${RELEASE_SHA}"
export PROJECTBRAIN_DEPLOY_SLOT="${PROJECTBRAIN_DEPLOY_SLOT:-stage}"
export PROJECTBRAIN_WEB_PORT="${PROJECTBRAIN_STAGE_WEB_PORT:-1210}"
COMPOSE_PROJECT="projectbrain-${PROJECTBRAIN_DEPLOY_SLOT}-${RELEASE_SHA%????????????????????????????????}"
docker compose \
--project-name "${COMPOSE_PROJECT}" \
--file docker-compose.yml \
--file docker-compose.release.yml \
config --quiet
docker compose \
--project-name "${COMPOSE_PROJECT}" \
--file docker-compose.yml \
--file docker-compose.release.yml \
up --detach --no-build --pull never
READINESS_URL="http://127.0.0.1:${PROJECTBRAIN_WEB_PORT}/api/system/readiness"
attempt=0
until READINESS="$(curl --fail --silent --show-error "${READINESS_URL}")"; do
attempt=$((attempt + 1))
if [ "${attempt}" -ge 30 ]; then echo "Staged release did not become ready: ${READINESS_URL}" >&2; exit 70; fi
sleep 2
done
EVIDENCE_FILE="${ARTIFACT_DIR}/staged-deployment-${RELEASE_SHA}.txt"
{
echo "release_sha=${RELEASE_SHA}"
echo "compose_project=${COMPOSE_PROJECT}"
echo "web_port=${PROJECTBRAIN_WEB_PORT}"
echo "api_archive_sha256=${API_DIGEST}"
echo "web_archive_sha256=${WEB_DIGEST}"
echo "api_image_id=${API_IMAGE_ID}"
echo "web_image_id=${WEB_IMAGE_ID}"
echo "readiness_url=${READINESS_URL}"
echo "readiness=${READINESS}"
echo "verified_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "${EVIDENCE_FILE}"
echo "Staged immutable release ${RELEASE_SHA} as ${COMPOSE_PROJECT}."
echo "Evidence: ${EVIDENCE_FILE}"