#!/usr/bin/env bash # # package-elastic-beanstalk.sh — build a normalized Elastic Beanstalk source # bundle for the shoc-backend .NET 8 application. Generated .NET/EF binaries # are not guaranteed to be byte-reproducible between separate builds. # # Layout of the resulting ZIP (the Beanstalk application root): # ./ published Api.SeaHavenIndustries (self-contained, linux-x64) # ./efbundle self-contained EF Core 8.0.8 migrations bundle (linux-x64, +x) # ./.ebextensions/* leader-only migration container command # # The bundle reads ConnectionStrings__DefaultConnection from the runtime # environment (Elastic Beanstalk property). No connection string or secret is # written into the package. # # The script only ever removes its own generated directory (.artifacts/elastic-beanstalk) # and validates that path before doing so. # # Usage: # bash scripts/package-elastic-beanstalk.sh [output.zip] # # Environment: # DOTNET_BIN optional path to the dotnet executable # RUNTIME optional target RID for local validation (default: linux-x64) set -euo pipefail OUTPUT_ZIP="${1:-.artifacts/elastic-beanstalk/site.zip}" STAGING_DIR=".artifacts/elastic-beanstalk/staging" TOOLS_DIR=".artifacts/dotnet-tools" log() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; } die() { printf '\033[31mERR\033[0m %s\n' "$1" >&2; exit 1; } REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" case "$OUTPUT_ZIP" in .artifacts/elastic-beanstalk/*.zip) : ;; *) die "output must be a .zip beneath .artifacts/elastic-beanstalk" ;; esac if [[ -n "${DOTNET_BIN:-}" ]]; then DOTNET="$DOTNET_BIN" elif command -v dotnet >/dev/null 2>&1; then DOTNET="$(command -v dotnet)" elif [[ -x "$HOME/.dotnet/dotnet" ]]; then DOTNET="$HOME/.dotnet/dotnet" else die "dotnet is unavailable; set DOTNET_BIN or install the .NET 8 SDK." fi DOTNET_DIR="$(cd "$(dirname "$DOTNET")" && pwd)" export PATH="$DOTNET_DIR:$PATH" if [[ -d "$DOTNET_DIR/host/fxr" ]]; then export DOTNET_ROOT="$DOTNET_DIR" fi export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_NOLOGO=1 export TZ=UTC export SOURCE_DATE_EPOCH="315532800" API_PROJECT="Api.SeaHavenIndustries/Api.SeaHavenIndustries.csproj" MIGRATIONS_PROJECT="Data.SeaHavenIndustries/Data.SeaHavenIndustries.csproj" EF_VERSION="8.0.8" RUNTIME="${RUNTIME:-linux-x64}" [[ -f "$API_PROJECT" ]] || die "missing API project: $API_PROJECT" [[ -f "$MIGRATIONS_PROJECT" ]] || die "missing migrations project: $MIGRATIONS_PROJECT" log "resolve commit SHA for release metadata" COMMIT_SHA="${APP_COMMIT_SHA:-${GITHUB_SHA:-}}" if [[ ! "$COMMIT_SHA" =~ ^[0-9a-fA-F]{40}$ ]] && command -v git >/dev/null 2>&1; then GIT_SHA="$(git rev-parse HEAD 2>/dev/null || true)" if [[ "$GIT_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then COMMIT_SHA="$GIT_SHA" fi fi if [[ ! "$COMMIT_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then if [[ "${CI:-false}" == "true" ]]; then die "CI build could not resolve a valid 40-hex commit SHA (APP_COMMIT_SHA/GITHUB_SHA/git)." fi die "could not resolve a valid 40-hex commit SHA (APP_COMMIT_SHA/GITHUB_SHA/git rev-parse HEAD)." fi COMMIT_SHA="$(printf '%s' "$COMMIT_SHA" | tr '[:upper:]' '[:lower:]')" printf ' commit: %s\n' "$COMMIT_SHA" if command -v zip >/dev/null 2>&1; then ARCHIVER="zip" elif command -v python >/dev/null 2>&1; then ARCHIVER="python" else die "zip or python is required to build the source bundle." fi GENERATED_ROOT="$(dirname "$STAGING_DIR")" case "$GENERATED_ROOT" in .artifacts/elastic-beanstalk) : ;; *) die "refusing to remove unexpected generated dir: $GENERATED_ROOT" ;; esac log "clean generated artifacts" rm -rf "$GENERATED_ROOT" "$TOOLS_DIR" mkdir -p "$STAGING_DIR" "$TOOLS_DIR" log "publish $API_PROJECT (Release, self-contained, $RUNTIME)" "$DOTNET" publish "$API_PROJECT" \ -c Release \ --self-contained \ --runtime "$RUNTIME" \ -o "$STAGING_DIR" \ -p:ContinuousIntegrationBuild=true \ -p:UseAppHost=true \ -p:SourceRevisionId="$COMMIT_SHA" \ -p:InformationalVersion="shoc-backend@$COMMIT_SHA" \ -p:IncludeSourceRevisionInInformationalVersion=false \ -p:DebugType=portable \ -p:DebugSymbols=true log "install dotnet-ef $EF_VERSION (local tool path)" if ! "$DOTNET" tool install dotnet-ef --version "$EF_VERSION" --tool-path "$TOOLS_DIR" 2>/dev/null; then "$DOTNET" tool update dotnet-ef --version "$EF_VERSION" --tool-path "$TOOLS_DIR" fi EF="$TOOLS_DIR/dotnet-ef" log "build EF migrations bundle (self-contained, $RUNTIME)" "$EF" migrations bundle \ --project "$MIGRATIONS_PROJECT" \ --startup-project "$API_PROJECT" \ --configuration Release \ --self-contained \ --target-runtime "$RUNTIME" \ --output "$STAGING_DIR/efbundle" chmod 0755 "$STAGING_DIR/efbundle" log "copy .ebextensions into bundle root" mkdir -p "$STAGING_DIR/.ebextensions" cp -R .ebextensions/. "$STAGING_DIR/.ebextensions/" log "verify no committed secret placeholders survived publish" if grep -rIEl -- 'Server=.*;.*Password=|AccountKey=|aws_secret|AKIA[0-9A-Z]{16}' "$STAGING_DIR" 2>/dev/null; then die "potential secret detected in publish output; refusing to package." fi log "assemble source bundle (contents, not the containing directory)" if [[ "$ARCHIVER" == "zip" ]]; then ( cd "$STAGING_DIR" # ZIP stores file mtimes. Normalize archive metadata; release immutability # comes from uploading this one build under a unique version label. find . -type f -exec touch -t 198001010000 {} + find . -type f -print | LC_ALL=C sort \ | zip -q -X -@ "$REPO_ROOT/$OUTPUT_ZIP" ) else python - "$STAGING_DIR" "$REPO_ROOT/$OUTPUT_ZIP" <<'PY' import pathlib import stat import sys import zipfile root = pathlib.Path(sys.argv[1]) output = pathlib.Path(sys.argv[2]) with zipfile.ZipFile( output, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9, ) as archive: for path in sorted(item for item in root.rglob("*") if item.is_file()): info = zipfile.ZipInfo( path.relative_to(root).as_posix(), date_time=(1980, 1, 1, 0, 0, 0), ) info.compress_type = zipfile.ZIP_DEFLATED mode = path.stat().st_mode if path.name == "efbundle": mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH info.external_attr = (mode & 0xFFFF) << 16 archive.writestr(info, path.read_bytes(), compresslevel=9) PY fi log "package written: $OUTPUT_ZIP" printf ' contents: %d files\n' "$(find "$STAGING_DIR" -type f | wc -l | tr -d ' ')" printf ' size: %s bytes\n' "$(wc -c < "$REPO_ROOT/$OUTPUT_ZIP" | tr -d ' ')" printf ' efbundle: %s\n' "$(file -b "$STAGING_DIR/efbundle" 2>/dev/null || echo present)"