2026-07-27 19:26:07 -03:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
2026-09-03 10:12:06 -04:00
|
|
|
# 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.
|
2026-07-27 19:26:07 -03:00
|
|
|
#
|
|
|
|
|
# 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"
|
2026-08-31 11:51:18 -04:00
|
|
|
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
|
2026-07-27 19:26:07 -03:00
|
|
|
|
|
|
|
|
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" \
|
2026-08-31 11:51:18 -04:00
|
|
|
-p:ContinuousIntegrationBuild=true \
|
|
|
|
|
-p:UseAppHost=true
|
2026-07-27 19:26:07 -03:00
|
|
|
|
|
|
|
|
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 \
|
2026-08-31 11:51:18 -04:00
|
|
|
--target-runtime "$RUNTIME" \
|
2026-07-27 19:26:07 -03:00
|
|
|
--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)"
|
2026-08-31 11:51:18 -04:00
|
|
|
if [[ "$ARCHIVER" == "zip" ]]; then
|
|
|
|
|
(
|
|
|
|
|
cd "$STAGING_DIR"
|
2026-09-03 10:12:06 -04:00
|
|
|
# ZIP stores file mtimes. Normalize archive metadata; release immutability
|
|
|
|
|
# comes from uploading this one build under a unique version label.
|
2026-08-31 11:51:18 -04:00
|
|
|
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
|
2026-07-27 19:26:07 -03:00
|
|
|
|
|
|
|
|
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)"
|