Skip to content

Commit 94e07d3

Browse files
committed
Support migrations from a local checkout
1 parent 26a21a1 commit 94e07d3

3 files changed

Lines changed: 105 additions & 7 deletions

File tree

robotics_integration_tests/custom_containers/migrations_runner.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,57 @@
11
from pathlib import Path
22

33
from docker.models.networks import Network
4+
from loguru import logger
45

56
from robotics_integration_tests.custom_containers.image_builder import build_image_once
67
from robotics_integration_tests.custom_containers.stream_logging_docker_container import (
78
StreamLoggingDockerContainer,
89
)
910
from robotics_integration_tests.settings.settings import settings
1011

12+
# Where a local checkout is mounted inside the migrations runner.
13+
_LOCAL_REPO_MOUNT = "/src"
14+
15+
16+
def _with_migrations_source(
17+
container: StreamLoggingDockerContainer,
18+
source_dir: str,
19+
project_folder: str,
20+
setting_name: str,
21+
) -> StreamLoggingDockerContainer:
22+
"""Take migrations from a local checkout instead of cloning from GitHub.
23+
24+
Mounted read-only; the entrypoint copies it into the container so that nothing
25+
can be written back into the working tree. The copy means uncommitted and
26+
untracked migrations are picked up, which is the reason to use this at all.
27+
28+
Validation is deliberately strict and happens here, before the container
29+
starts: silently falling back to the GitHub clone would leave you believing
30+
you had tested a local migration when you had not.
31+
"""
32+
if not source_dir:
33+
return container
34+
35+
resolved: Path = Path(source_dir).expanduser().resolve()
36+
if not resolved.is_dir():
37+
raise ValueError(
38+
f"{setting_name} is set to '{source_dir}' (resolved to '{resolved}'), "
39+
"which is not a directory."
40+
)
41+
42+
project_dir: Path = resolved / project_folder
43+
if not list(project_dir.glob("*.csproj")):
44+
raise ValueError(
45+
f"{setting_name} is set to '{resolved}', but no .csproj was found in "
46+
f"'{project_dir}'. Point it at the repository root of the service, not "
47+
"at the project folder."
48+
)
49+
50+
logger.info(f"Using migrations from local checkout: {resolved}")
51+
return container.with_volume_mapping(
52+
str(resolved), _LOCAL_REPO_MOUNT, "ro"
53+
).with_env("LOCAL_REPO_PATH", _LOCAL_REPO_MOUNT)
54+
1155

1256
def _with_design_time_database_config(
1357
container: StreamLoggingDockerContainer, postgres_connection_string: str
@@ -54,6 +98,12 @@ def create_migrations_runner_container(
5498
.with_env("EF_PROJECT_PATH", settings.BACKEND_PROJECT_FILE_FOLDER)
5599
.with_env("EF_STARTUP_PATH", settings.BACKEND_PROJECT_FILE_FOLDER)
56100
)
101+
container = _with_migrations_source(
102+
container,
103+
source_dir=settings.MIGRATIONS_SOURCE_DIR,
104+
project_folder=settings.BACKEND_PROJECT_FILE_FOLDER,
105+
setting_name="MIGRATIONS_SOURCE_DIR",
106+
)
57107
return _with_design_time_database_config(container, postgres_connection_string)
58108

59109

@@ -78,4 +128,10 @@ def create_sara_migrations_runner_container(
78128
.with_env("EF_PROJECT_PATH", settings.SARA_BACKEND_PROJECT_FILE_FOLDER)
79129
.with_env("EF_STARTUP_PATH", settings.SARA_BACKEND_PROJECT_FILE_FOLDER)
80130
)
131+
container = _with_migrations_source(
132+
container,
133+
source_dir=settings.SARA_MIGRATIONS_SOURCE_DIR,
134+
project_folder=settings.SARA_BACKEND_PROJECT_FILE_FOLDER,
135+
setting_name="SARA_MIGRATIONS_SOURCE_DIR",
136+
)
81137
return _with_design_time_database_config(container, postgres_connection_string)

robotics_integration_tests/custom_images/migrations_runner/entrypoint.sh

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,50 @@ AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-}"
2323
AZURE_TENANT_ID="${AZURE_TENANT_ID:-}"
2424
export AZURE_CLIENT_SECRET AZURE_CLIENT_ID AZURE_TENANT_ID
2525

26-
echo "Cloning $GIT_REPO @ $GIT_REF ..."
2726
rm -rf /work/repo
28-
if [ "$GIT_REF" = "latest" ]; then
29-
BRANCH=$(curl -s ${GITHUB_TOKEN:+-H "Authorization: token $GITHUB_TOKEN"} \
30-
"https://api.github.com/repos/$GIT_REPO/releases/latest" | jq -r .tag_name)
31-
echo "Resolved latest to $BRANCH"
27+
mkdir -p /work/repo
28+
29+
if [ -n "${LOCAL_REPO_PATH:-}" ]; then
30+
# Migrations come from a local checkout mounted read-only, so that locally
31+
# built service images and the database schema come from the same source.
32+
# Copied rather than used in place: the build writes bin/ and obj/ into the
33+
# project, and the mount is read-only precisely so the caller's working tree
34+
# cannot be modified.
35+
#
36+
# bin and obj are excluded not to save space but for correctness: they are
37+
# host-architecture build output, and obj/project.assets.json embeds absolute
38+
# host paths, both of which break restore inside this container.
39+
echo "Copying migrations source from $LOCAL_REPO_PATH ..."
40+
[ -d "$LOCAL_REPO_PATH" ] || { echo "LOCAL_REPO_PATH '$LOCAL_REPO_PATH' is not a directory."; exit 1; }
41+
tar -C "$LOCAL_REPO_PATH" \
42+
--exclude=bin \
43+
--exclude=obj \
44+
--exclude=node_modules \
45+
--exclude=.git \
46+
--exclude=TestResults \
47+
-cf - . | tar -C /work/repo -xf -
3248
else
33-
BRANCH="main"
49+
echo "Cloning $GIT_REPO @ $GIT_REF ..."
50+
if [ "$GIT_REF" = "latest" ]; then
51+
BRANCH=$(curl -s ${GITHUB_TOKEN:+-H "Authorization: token $GITHUB_TOKEN"} \
52+
"https://api.github.com/repos/$GIT_REPO/releases/latest" | jq -r .tag_name)
53+
echo "Resolved latest to $BRANCH"
54+
else
55+
BRANCH="main"
56+
fi
57+
rm -rf /work/repo
58+
git clone --depth 1 --branch "$BRANCH" "https://github.com/$GIT_REPO" /work/repo
3459
fi
35-
git clone --depth 1 --branch "$BRANCH" "https://github.com/$GIT_REPO" /work/repo
3660

3761
cd /work/repo
3862

63+
# Guard against a source that does not contain what we expect, rather than
64+
# letting it surface later as an opaque dotnet-ef failure.
65+
if ! ls "$EF_PROJECT_PATH"/*.csproj >/dev/null 2>&1; then
66+
echo "No .csproj found at '$EF_PROJECT_PATH' in the migrations source."
67+
exit 1
68+
fi
69+
3970
echo "Restoring projects for EF design-time..."
4071
dotnet restore "$EF_STARTUP_PATH" || dotnet restore "$EF_PROJECT_PATH" || true
4172

robotics_integration_tests/settings/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ class Settings(BaseSettings):
5151
GIT_REPOSITORY_FOR_MIGRATIONS_REF: str = Field(default="latest")
5252
BACKEND_PROJECT_FILE_FOLDER: str = Field(default="backend/api")
5353

54+
# Path to a local flotilla checkout to take migrations from. When set, it
55+
# takes precedence over cloning GIT_REPOSITORY_FOR_MIGRATIONS from GitHub, and
56+
# GIT_REPOSITORY_FOR_MIGRATIONS_REF is ignored. Use this together with locally
57+
# built images (see scripts/build_local_images.sh) so that the schema and the
58+
# application code come from the same source. Uncommitted and untracked
59+
# migrations are included, since the directory is copied rather than cloned.
60+
MIGRATIONS_SOURCE_DIR: str = Field(default="")
61+
5462
# PostgreSQL Sara Database environment
5563
POSTGRESQL_IMAGE: str = Field(default="postgres:16")
5664
SARA_DB_USER: str = Field(default="sara")
@@ -62,6 +70,9 @@ class Settings(BaseSettings):
6270

6371
SARA_BACKEND_PROJECT_FILE_FOLDER: str = Field(default="api")
6472

73+
# See MIGRATIONS_SOURCE_DIR.
74+
SARA_MIGRATIONS_SOURCE_DIR: str = Field(default="")
75+
6576
# Migrations runner environment
6677
RELATIVE_PATH_TO_DOCKERFILE: str = Field(
6778
default="./robotics_integration_tests/custom_images/migrations_runner/"

0 commit comments

Comments
 (0)