Skip to content

Commit 0d01150

Browse files
authored
Run migrations on PR environments (#1440)
* Create separate tables for PR envs * Use commit hash for checkout / teardown * Loud error
1 parent f52536d commit 0d01150

14 files changed

Lines changed: 124 additions & 8 deletions

File tree

.github/workflows/ci-app-pr-environment-destroy.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ on:
66
description: "Pull request number"
77
required: true
88
type: string
9+
commit_hash:
10+
description: "Commit hash whose scripts to use for teardown"
11+
required: true
12+
type: string
913
# !! Uncomment the following lines once you've set up the dev environment and are ready to enable PR environments
1014
pull_request:
1115
types: [closed]
@@ -24,3 +28,4 @@ jobs:
2428
app_name: "app"
2529
environment: "dev"
2630
pr_number: ${{ inputs.pr_number || github.event.number }}
31+
commit_hash: ${{ inputs.commit_hash || github.event.pull_request.head.sha }}

.github/workflows/pr-environment-checks.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242

4343
steps:
4444
- uses: actions/checkout@v6
45+
with:
46+
ref: ${{ inputs.commit_hash }}
4547

4648
- name: Set up Terraform
4749
uses: ./.github/actions/setup-terraform

.github/workflows/pr-environment-destroy.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ on:
1212
pr_number:
1313
required: true
1414
type: string
15+
commit_hash:
16+
required: true
17+
type: string
1518
jobs:
1619
destroy:
1720
name: Destroy environment
@@ -27,6 +30,8 @@ jobs:
2730

2831
steps:
2932
- uses: actions/checkout@v6
33+
with:
34+
ref: ${{ inputs.commit_hash }}
3035

3136
- name: Set up Terraform
3237
uses: ./.github/actions/setup-terraform

app/bin/db-drop-schema

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
echo "Dropping schema..."
3+
echo " DB_SCHEMA=$DB_SCHEMA"
4+
5+
./bin/rake db:drop_schema

app/bin/db-migrate

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ echo " DB_USER=$DB_USER"
66
echo " DB_NAME=$DB_NAME"
77
echo " DB_SCHEMA=$DB_SCHEMA"
88

9+
if [ -n "$DB_SCHEMA" ] && [ "$DB_SCHEMA" != "public" ] && [ "$DB_SCHEMA" != "app" ]; then
10+
echo "Creating schema $DB_SCHEMA if it doesn't exist..."
11+
./bin/rake db:create_schema
12+
fi
13+
914
./bin/rake --trace db:migrate

app/config/database.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ queue:
131131
password: <%= ENV.fetch("DB_PASS") { nil } %>
132132
port: <%= ENV.fetch("DB_PORT") { 5432 } %>
133133
host: <%= ENV.fetch("DB_HOST") { "localhost" } %>
134+
schema_search_path: <%= ENV.fetch("DB_SCHEMA", "app") %>
134135
aws_rds_iam_auth_token_generator: default
135136

136137
production:
@@ -140,6 +141,7 @@ production:
140141
password: <%= ENV.fetch("DB_PASS") { nil } %>
141142
port: <%= ENV.fetch("DB_PORT") { 5432 } %>
142143
host: <%= ENV.fetch("DB_HOST") { "localhost" } %>
144+
schema_search_path: <%= ENV.fetch("DB_SCHEMA", "app") %>
143145
aws_rds_iam_auth_token_generator: default
144146
ci:
145147
<<: *default

app/lib/tasks/schema.rake

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace :db do
2+
desc "Create the PostgreSQL schema specified by DB_SCHEMA"
3+
task create_schema: :environment do
4+
schema = ENV["DB_SCHEMA"]
5+
abort("DB_SCHEMA must be set to a non-'public' value (got: #{schema.inspect})") if schema.blank? || schema == "public"
6+
7+
ActiveRecord::Base.connection.execute(
8+
"CREATE SCHEMA IF NOT EXISTS #{ActiveRecord::Base.connection.quote_column_name(schema)}"
9+
)
10+
Rails.logger.info("Schema '#{schema}' created or already exists.")
11+
end
12+
13+
desc "Drop the PostgreSQL schema specified by DB_SCHEMA"
14+
task drop_schema: :environment do
15+
schema = ENV["DB_SCHEMA"]
16+
abort("DB_SCHEMA must be set to a non-'public' value (got: #{schema.inspect})") if schema.blank? || schema == "public"
17+
18+
ActiveRecord::Base.connection.execute(
19+
"DROP SCHEMA IF EXISTS #{ActiveRecord::Base.connection.quote_column_name(schema)} CASCADE"
20+
)
21+
Rails.logger.info("Schema '#{schema}' dropped.")
22+
end
23+
end

bin/destroy-pr-environment

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ if ! terraform -chdir="infra/${app_name}/service" workspace select "${workspace}
2727
exit 0
2828
fi
2929

30+
echo "::group::Drop PR database schema"
31+
terraform -chdir="infra/${app_name}/app-config" init > /dev/null
32+
terraform -chdir="infra/${app_name}/app-config" apply -auto-approve > /dev/null
33+
has_database=$(terraform -chdir="infra/${app_name}/app-config" output -raw has_database)
34+
35+
if [ "${has_database}" = "true" ]; then
36+
migrator_role_arn=$(terraform -chdir="infra/${app_name}/service" output -raw migrator_role_arn)
37+
./bin/run-command \
38+
--workspace "${workspace}" \
39+
--task-role-arn "${migrator_role_arn}" \
40+
"${app_name}" "${environment}" '["db-drop-schema"]'
41+
else
42+
echo "Application does not have a database, skipping schema cleanup"
43+
fi
44+
echo "::endgroup::"
45+
46+
# Re-select workspace after run-command reinitializes terraform
47+
terraform -chdir="infra/${app_name}/service" workspace select "${workspace}"
48+
3049
echo "::group::Destroy resources"
3150
terraform -chdir="infra/${app_name}/service" destroy -var="environment_name=${environment}" -input=false -auto-approve
3251
echo "::endgroup::"

bin/run-command

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ if [ -n "${workspace}" ]; then
8787
terraform -chdir="infra/${app_name}/service" workspace select "${workspace}"
8888
fi
8989

90+
if [ -n "${workspace}" ]; then
91+
echo "Selecting Terraform workspace: ${workspace}"
92+
terraform -chdir="infra/${app_name}/service" workspace select "${workspace}"
93+
fi
94+
9095
# Use the same cluster, task definition, and network configuration that the application service uses
9196
cluster_name=$(terraform -chdir="infra/${app_name}/service" output -raw service_cluster_name)
9297
service_name=$(terraform -chdir="infra/${app_name}/service" output -raw service_name)

bin/run-database-migrations

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# do not update the service
66
# 2. Run the "db-migrate" command in the container as a new task
77
#
8+
# Optional parameters:
9+
# --workspace <name> – Terraform workspace to select (for PR environments)
10+
#
811
# Positional parameters:
912
# app_name (required) – the name of subdirectory of /infra that holds the
1013
# application's infrastructure code.
@@ -15,6 +18,19 @@
1518

1619
set -euo pipefail
1720

21+
workspace=""
22+
while :; do
23+
case "${1:-}" in
24+
--workspace)
25+
workspace="$2"
26+
shift 2
27+
;;
28+
*)
29+
break
30+
;;
31+
esac
32+
done
33+
1834
app_name="$1"
1935
image_tag="$2"
2036
environment="$3"
@@ -40,17 +56,32 @@ fi
4056
db_migrator_user=$(terraform -chdir="infra/${app_name}/app-config" output -json environment_configs | jq -r ".${environment}.database_config.migrator_username")
4157

4258
./bin/terraform-init "infra/${app_name}/service" "${environment}"
59+
60+
if [ -n "${workspace}" ]; then
61+
echo "Selecting Terraform workspace: ${workspace}"
62+
terraform -chdir="infra/${app_name}/service" workspace select "${workspace}"
63+
fi
64+
4365
migrator_role_arn=$(terraform -chdir="infra/${app_name}/service" output -raw migrator_role_arn)
4466

4567
echo
46-
echo "::group::Step 1. Update task definition without updating service"
4768

48-
TF_CLI_ARGS_apply="-input=false -auto-approve -var=image_tag=${image_tag}
49-
-target=module.service.aws_ecs_task_definition.app
50-
-target=module.service.aws_iam_role_policy.task_executor" \
51-
make infra-update-app-service APP_NAME="${app_name}" ENVIRONMENT="${environment}"
69+
if [ -n "${workspace}" ]; then
70+
# For PR environments, the task definition was already updated by the caller's
71+
# terraform apply, so skip the targeted apply (which would reinitialize terraform
72+
# and reset the workspace).
73+
echo "Skipping task definition update (already applied in workspace ${workspace})"
74+
else
75+
echo "::group::Step 1. Update task definition without updating service"
76+
77+
TF_CLI_ARGS_apply="-input=false -auto-approve -var=image_tag=${image_tag}
78+
-target=module.service.aws_ecs_task_definition.app
79+
-target=module.service.aws_iam_role_policy.task_executor" \
80+
make infra-update-app-service APP_NAME="${app_name}" ENVIRONMENT="${environment}"
81+
82+
echo "::endgroup::"
83+
fi
5284

53-
echo "::endgroup::"
5485
echo
5586
echo 'Step 2. Run "db-migrate" command'
5687

@@ -64,4 +95,4 @@ environment_variables=$(cat << EOF
6495
EOF
6596
)
6697

67-
./bin/run-command --task-role-arn "${migrator_role_arn}" --environment-variables "${environment_variables}" "${app_name}" "${environment}" "${command}"
98+
./bin/run-command ${workspace:+--workspace "${workspace}"} --task-role-arn "${migrator_role_arn}" --environment-variables "${environment_variables}" "${app_name}" "${environment}" "${command}"

0 commit comments

Comments
 (0)