From 6b803513713183682958beda7e1829fd0cac0d14 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 26 Jun 2026 14:24:14 -0400 Subject: [PATCH 1/4] ci: add staging deploy workflow with Flyway DB migration --- .github/composite/db/migrate/action.yml | 61 ++++++++++++++++++ .github/scripts/src/db/migrate.ts | 83 +++++++++++++++++++++++++ .github/workflows/deploy-staging.yml | 60 ++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 .github/composite/db/migrate/action.yml create mode 100644 .github/scripts/src/db/migrate.ts create mode 100644 .github/workflows/deploy-staging.yml diff --git a/.github/composite/db/migrate/action.yml b/.github/composite/db/migrate/action.yml new file mode 100644 index 0000000..23c08fe --- /dev/null +++ b/.github/composite/db/migrate/action.yml @@ -0,0 +1,61 @@ +name: "Validate DB" +description: "Validate the current db/ folder at a current commit against a database" + +inputs: + AZURE_CLIENT_ID: + description: "Azure client ID if azure authentication is required." + required: false + AZURE_CLIENT_SECRET: + description: "Azure client secret if azure authentication is required." + required: false + AZURE_TENANT_ID: + description: "Azure tenant ID if azure authentication is required." + required: false + AZURE_SUBSCRIPTION_ID: + description: "Azure subscription ID if azure authentication is required." + required: false + ENVIRONMENT: + description: '"staging" or "production"' + required: false + default: production + SHA: + description: "(Staging only). Current commit SHA" + required: false + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + AZURE_CLIENT_ID: + description: "Azure client ID if azure authentication is required." + required: false + AZURE_CLIENT_SECRET: + description: "Azure client secret if azure authentication is required." + required: false + AZURE_TENANT_ID: + description: "Azure tenant ID if azure authentication is required." + required: false + AZURE_SUBSCRIPTION_ID: + description: "Azure subscription ID if azure authentication is required." + required: false + + - name: Set up OpenJDK 25 + uses: actions/setup-java@v5 + with: + distribution: "temurin" + java-version: "25" + + - name: Cache Maven packages + uses: actions/cache@v5 + with: + path: | + ~/.m2 + ~/repository + key: ${{ github.job }}-${{ hashFiles('**/pom.xml') }} + + - name: Run script + shell: bash + run: bun run .github/scripts/migrate-db --environment=${{ inputs.ENVIRONMENT }} + --sha=${{ inputs.SHA }} diff --git a/.github/scripts/src/db/migrate.ts b/.github/scripts/src/db/migrate.ts new file mode 100644 index 0000000..ff6da1c --- /dev/null +++ b/.github/scripts/src/db/migrate.ts @@ -0,0 +1,83 @@ +import { $ } from "bun"; +import { + EnvClient, + EnvClientStrategy, +} from "@tahminator/pipeline"; + + +async function main() { + const envClient = EnvClient.create(EnvClientStrategy.SOPS); + const ciEnv = await envClient.readFromEnv(".env.ci"); + const { + DATABASE_NAME, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_USER, + DATABASE_PASSWORD, + } = parseCiEnv(ciEnv); + + await $.env({ + ...process.env, + DATABASE_NAME, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_USER, + DATABASE_PASSWORD, + })`./mvnw flyway:migrate -Dflyway.locations=filesystem:db`; +} + +function parseCiEnv(ciEnv: Record) { + const DATABASE_NAME = (() => { + const v = ciEnv["PG_DATABASE"]; + if (!v) { + throw new Error("Missing PG_DATABASE from .env.ci"); + } + return v; + })(); + + const DATABASE_HOST = (() => { + const v = ciEnv["PG_HOST"]; + if (!v) { + throw new Error("Missing PG_HOST from .env.ci"); + } + return v; + })(); + + const DATABASE_PORT = (() => { + const v = ciEnv["PG_PORT"]; + if (!v) { + throw new Error("Missing PG_PORT from .env.ci"); + } + return v; + })(); + + const DATABASE_USER = (() => { + return "patchats-stg-app" + })(); + + const DATABASE_PASSWORD = (() => { + const v = ciEnv["PG_ROLE_patchats-stg-app"]; + if (!v) { + throw new Error("Missing PG_PASSWORD from .env.ci"); + } + return v; + })(); + + return { + DATABASE_NAME, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_USER, + DATABASE_PASSWORD, + env: ciEnv, + }; +} + +main() + .then(() => { + process.exit(); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..aa83cb4 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,60 @@ +name: Deploy to staging +run-name: Deploying to staging triggered by ${{ github.actor }} + +concurrency: + group: deploy-staging + cancel-in-progress: true + +on: + push: + branches: [main] + +permissions: + contents: write + issues: write + pull-requests: write + statuses: write + checks: write + +jobs: + backend-pretest: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Run workflow + uses: ./.github/composite/test/backend-pretest + + frontend-pretest: + runs-on: ubuntu-latest + defaults: + run: + working-directory: js + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Run workflow + uses: ./.github/composite/test/frontend-pretest + + migrate-database: + name: Migrate staging database + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + + - name: Run workflow + uses: ./.github/composite/db/migrate + with: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + ENVIRONMENT: staging + SHA: ${{ github.sha }} From 74f28dccf15802440042f13a53e6d3d5ff5c1053 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 26 Jun 2026 14:40:29 -0400 Subject: [PATCH 2/4] Fix the migration pipeline - Replaced erroneous schema with values in composite action - Make migrate.ts parse the --environment flag to conditionally render the right role prefix for the database - Fix the path for the migrate.ts script invocation in the composite action --- .github/composite/db/migrate/action.yml | 18 +++------- .github/scripts/src/db/migrate.ts | 44 ++++++++++++++++--------- .github/workflows/deploy-staging.yml | 1 + 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/.github/composite/db/migrate/action.yml b/.github/composite/db/migrate/action.yml index 23c08fe..8a6157d 100644 --- a/.github/composite/db/migrate/action.yml +++ b/.github/composite/db/migrate/action.yml @@ -28,18 +28,10 @@ runs: - name: Setup CI uses: ./.github/composite/setup-ci with: - AZURE_CLIENT_ID: - description: "Azure client ID if azure authentication is required." - required: false - AZURE_CLIENT_SECRET: - description: "Azure client secret if azure authentication is required." - required: false - AZURE_TENANT_ID: - description: "Azure tenant ID if azure authentication is required." - required: false - AZURE_SUBSCRIPTION_ID: - description: "Azure subscription ID if azure authentication is required." - required: false + AZURE_CLIENT_ID: ${{ inputs.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ inputs.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ inputs.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ inputs.AZURE_SUBSCRIPTION_ID }} - name: Set up OpenJDK 25 uses: actions/setup-java@v5 @@ -57,5 +49,5 @@ runs: - name: Run script shell: bash - run: bun run .github/scripts/migrate-db --environment=${{ inputs.ENVIRONMENT }} + run: bun run .github/scripts/src/db/migrate.ts --environment=${{ inputs.ENVIRONMENT }} --sha=${{ inputs.SHA }} diff --git a/.github/scripts/src/db/migrate.ts b/.github/scripts/src/db/migrate.ts index ff6da1c..2e9efb0 100644 --- a/.github/scripts/src/db/migrate.ts +++ b/.github/scripts/src/db/migrate.ts @@ -1,20 +1,30 @@ +import { EnvClient, EnvClientStrategy } from "@tahminator/pipeline"; import { $ } from "bun"; -import { - EnvClient, - EnvClientStrategy, -} from "@tahminator/pipeline"; - +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; async function main() { + const { environment } = await yargs(hideBin(process.argv)) + .option("environment", { + type: "string", + choices: ["staging", "production"], + default: "production", + }) + .option("sha", { + type: "string", + description: "Current commit SHA (staging only)", + }) + .parseAsync(); + const envClient = EnvClient.create(EnvClientStrategy.SOPS); - const ciEnv = await envClient.readFromEnv(".env.ci"); + const ciEnv = await envClient.readFromEnv("secrets-rw.yaml"); const { DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, - } = parseCiEnv(ciEnv); + } = parseCiEnv(ciEnv, environment); await $.env({ ...process.env, @@ -26,11 +36,13 @@ async function main() { })`./mvnw flyway:migrate -Dflyway.locations=filesystem:db`; } -function parseCiEnv(ciEnv: Record) { +function parseCiEnv(ciEnv: Record, environment: string) { + const roleSuffix = environment === "staging" ? "stg" : "prod"; + const DATABASE_NAME = (() => { const v = ciEnv["PG_DATABASE"]; if (!v) { - throw new Error("Missing PG_DATABASE from .env.ci"); + throw new Error("Missing PG_DATABASE from secrets-rw.yaml"); } return v; })(); @@ -38,7 +50,7 @@ function parseCiEnv(ciEnv: Record) { const DATABASE_HOST = (() => { const v = ciEnv["PG_HOST"]; if (!v) { - throw new Error("Missing PG_HOST from .env.ci"); + throw new Error("Missing PG_HOST from secrets-rw.yaml"); } return v; })(); @@ -46,19 +58,19 @@ function parseCiEnv(ciEnv: Record) { const DATABASE_PORT = (() => { const v = ciEnv["PG_PORT"]; if (!v) { - throw new Error("Missing PG_PORT from .env.ci"); + throw new Error("Missing PG_PORT from secrets-rw.yaml"); } return v; })(); - const DATABASE_USER = (() => { - return "patchats-stg-app" - })(); + const DATABASE_USER = `patchats-${roleSuffix}-app`; const DATABASE_PASSWORD = (() => { - const v = ciEnv["PG_ROLE_patchats-stg-app"]; + const v = ciEnv[`PG_ROLE_patchats-${roleSuffix}-app`]; if (!v) { - throw new Error("Missing PG_PASSWORD from .env.ci"); + throw new Error( + `Missing PG_ROLE_patchats-${roleSuffix}-app from secrets-rw.yaml`, + ); } return v; })(); diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index aa83cb4..0cf8e0a 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -41,6 +41,7 @@ jobs: migrate-database: name: Migrate staging database runs-on: ubuntu-latest + needs: [backend-pretest, frontend-pretest] steps: - name: Checkout Repository From 74fabe40435a9df47e3f0b8178011a334964623c Mon Sep 17 00:00:00 2001 From: andrew <52230726+rootandroo@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:40:18 -0400 Subject: [PATCH 3/4] Update action.yml --- .github/composite/db/migrate/action.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/composite/db/migrate/action.yml b/.github/composite/db/migrate/action.yml index 8a6157d..77eae03 100644 --- a/.github/composite/db/migrate/action.yml +++ b/.github/composite/db/migrate/action.yml @@ -49,5 +49,6 @@ runs: - name: Run script shell: bash - run: bun run .github/scripts/src/db/migrate.ts --environment=${{ inputs.ENVIRONMENT }} - --sha=${{ inputs.SHA }} + + run: bun run .github/scripts/src/db/migrate.ts --environment=${{ inputs.ENVIRONMENT }} --sha=${{ inputs.SHA }} + From 474fa3957837d86583d8b77fcea4bb7841699054 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 26 Jun 2026 16:54:44 -0400 Subject: [PATCH 4/4] CI: Use secrets.ci.yaml to migrate staging --- .github/composite/db/migrate/action.yml | 13 +++++-------- .github/scripts/src/db/migrate.ts | 16 ++++++---------- .github/workflows/deploy-staging.yml | 1 - secrets.ci.yaml | 10 ++++++++-- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/composite/db/migrate/action.yml b/.github/composite/db/migrate/action.yml index 77eae03..beb1033 100644 --- a/.github/composite/db/migrate/action.yml +++ b/.github/composite/db/migrate/action.yml @@ -1,5 +1,5 @@ -name: "Validate DB" -description: "Validate the current db/ folder at a current commit against a database" +name: "Migrate database" +description: "Run Flyway migrations against the target environment's database" inputs: AZURE_CLIENT_ID: @@ -17,10 +17,7 @@ inputs: ENVIRONMENT: description: '"staging" or "production"' required: false - default: production - SHA: - description: "(Staging only). Current commit SHA" - required: false + default: staging runs: using: "composite" @@ -50,5 +47,5 @@ runs: - name: Run script shell: bash - run: bun run .github/scripts/src/db/migrate.ts --environment=${{ inputs.ENVIRONMENT }} --sha=${{ inputs.SHA }} - + run: | + bun run .github/scripts/src/db/migrate.ts --environment=${{ inputs.ENVIRONMENT }} diff --git a/.github/scripts/src/db/migrate.ts b/.github/scripts/src/db/migrate.ts index 2e9efb0..ee00b57 100644 --- a/.github/scripts/src/db/migrate.ts +++ b/.github/scripts/src/db/migrate.ts @@ -8,16 +8,12 @@ async function main() { .option("environment", { type: "string", choices: ["staging", "production"], - default: "production", - }) - .option("sha", { - type: "string", - description: "Current commit SHA (staging only)", + require: true, }) .parseAsync(); const envClient = EnvClient.create(EnvClientStrategy.SOPS); - const ciEnv = await envClient.readFromEnv("secrets-rw.yaml"); + const ciEnv = await envClient.readFromEnv("secrets.ci.yaml"); const { DATABASE_NAME, DATABASE_HOST, @@ -42,7 +38,7 @@ function parseCiEnv(ciEnv: Record, environment: string) { const DATABASE_NAME = (() => { const v = ciEnv["PG_DATABASE"]; if (!v) { - throw new Error("Missing PG_DATABASE from secrets-rw.yaml"); + throw new Error("Missing PG_DATABASE from secrets.ci.yaml"); } return v; })(); @@ -50,7 +46,7 @@ function parseCiEnv(ciEnv: Record, environment: string) { const DATABASE_HOST = (() => { const v = ciEnv["PG_HOST"]; if (!v) { - throw new Error("Missing PG_HOST from secrets-rw.yaml"); + throw new Error("Missing PG_HOST from secrets.ci.yaml"); } return v; })(); @@ -58,7 +54,7 @@ function parseCiEnv(ciEnv: Record, environment: string) { const DATABASE_PORT = (() => { const v = ciEnv["PG_PORT"]; if (!v) { - throw new Error("Missing PG_PORT from secrets-rw.yaml"); + throw new Error("Missing PG_PORT from secrets.ci.yaml"); } return v; })(); @@ -69,7 +65,7 @@ function parseCiEnv(ciEnv: Record, environment: string) { const v = ciEnv[`PG_ROLE_patchats-${roleSuffix}-app`]; if (!v) { throw new Error( - `Missing PG_ROLE_patchats-${roleSuffix}-app from secrets-rw.yaml`, + `Missing PG_ROLE_patchats-${roleSuffix}-app from secrets.ci.yaml`, ); } return v; diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 0cf8e0a..442a2d4 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -58,4 +58,3 @@ jobs: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} ENVIRONMENT: staging - SHA: ${{ github.sha }} diff --git a/secrets.ci.yaml b/secrets.ci.yaml index 4110b84..20e1a0b 100644 --- a/secrets.ci.yaml +++ b/secrets.ci.yaml @@ -1,4 +1,10 @@ #ENC[AES256_GCM,data:qE98Q60Bg0l1HXMQrqHo5QogKV8shLkOrm3NyLywCx3duhk9D6C0Wbne2UHD6Bfd7CaScA==,iv:2FuKLItO8y6VqNmwama+nc7PiuK685cT24doSz+EBjM=,tag:F76ao1gb6w4F591qjJHBug==,type:comment] +PG_DATABASE: ENC[AES256_GCM,data:zsdf+sWxyrM=,iv:93V5dWLQ9ybzyMgXVUaVU2HkPIsE7HUqtj62V/dPGkU=,tag:axXdOmG5UF0XvFs385yA4A==,type:str] +PG_HOST: ENC[AES256_GCM,data:bbhXNt3P53F4dcFSLffzx6wdZg+q2X8P,iv:JiMTSgQ1Su/sb++13x4XeEM4QTR28HbDjIkqIQojVLY=,tag:9/7ta2HtFdnI4VZTwUTCSA==,type:str] +PG_PORT: ENC[AES256_GCM,data:2vKQkQ==,iv:mcnYDQ6pAkp3N8vEHpbwFtyvJdMxHujbc+I8LhbbDkA=,tag:ZgwdVJXcIJsR/CDe4jEmAA==,type:str] +PG_ROLE_patchats-stg-app: ENC[AES256_GCM,data:R0LC8Yk4cX5l3OvVYhXo+w4ah7FVa63XIa2zbn3sTw0=,iv:E3Qgwe+uDwG2I+dLBo2H6DI3w6qUYilq48L5bVbkAbQ=,tag:eWezK3M1umTkbIeYACnXeQ==,type:str] +PG_ROLE_patchats-prod-app: ENC[AES256_GCM,data:Dio+n+leocayOfOq5OFfxEs+yV/IOl790KJRnHg1Vec=,iv:jMVDxdZtiWf+8u/zcuFGZCXElOcnGU2rv3zcFxlSIvg=,tag:yvIVJ41OJUDvoGKcQgEi0g==,type:str] + GITHUB_APP_APP_ID: ENC[AES256_GCM,data:/zDLkRlj6w==,iv:wtIraBrVH6BN4VWl+fpqDlPuI28CyIq2Gzp4bQNe+Qs=,tag:YPYYXLa2YzRzSRI6hvlNyg==,type:str] GITHUB_APP_INSTALLATION_ID: ENC[AES256_GCM,data:6thy6wpRMBo9,iv:zQOGXwEOfyZJgso+moCd2EIECQPo3+rLzfOk1b757qs=,tag:BlGBrEo4ezur7Om5F9f4gw==,type:int] GITHUB_APP_PEM_CONTENT: ENC[AES256_GCM,data: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,iv:iBGHZRcesMGP7nQjHAX6hMyJLzebz6MVwfXqK5JXPMs=,tag:fZAdfLUc1+0maMfpd5JyAA==,type:str] @@ -9,7 +15,7 @@ sops: name: sops-ro-key vault_url: https://sops-ro.vault.azure.net version: addd283ca3a54c0cbc4378b37dc4fcf1 - lastmodified: "2026-06-10T00:45:13Z" - mac: ENC[AES256_GCM,data:yDcfyAprtSwKUWN8mOzm6i5QfdqZHs2qyGX4EMGbABTWR7TYwmE+ikjgVDi80WD5jHbAHtDOsTJdWl8eAxGZfikgms1CvXZFArrStSLtEruo3iDgO0pi/J5zJAZ5biEYTv74EJ2FxGfHJ8N3Sw9YfjoWK6Hs4P+CGudK6NE6hQ4=,iv:gjjOWfkNdZdzXg/Ir1zyM7FGqNQ1fIXdTLquxwYP2mA=,tag:x7/i1XxNuAdcPbqa1NZY9w==,type:str] + lastmodified: "2026-06-26T20:32:45Z" + mac: ENC[AES256_GCM,data:ecxHesBDSU88QN8RFxGm6HLOpuvhv017UtkmDJKdGJtWf4ax7DGI7VWb187bDDHRU+hVPws5fYREutQpOhK4P4s1YydX6Jar5Huca/WYlXFX6BsxLDpd+GZNvmx8I6bIEcVWRM/dcvQi2Tr8nniTwK0A9vuzVxZEb5hYiu3KIYI=,iv:O7MSAiuXBb2cF1tY9xRBZdTWJO/h8dAfv1k+g9c6U6I=,tag:XE7F7WnG8tJwNVHYoCaw1Q==,type:str] unencrypted_suffix: _unencrypted version: 3.13.1