diff --git a/.github/composite/db/migrate/action.yml b/.github/composite/db/migrate/action.yml new file mode 100644 index 0000000..beb1033 --- /dev/null +++ b/.github/composite/db/migrate/action.yml @@ -0,0 +1,51 @@ +name: "Migrate database" +description: "Run Flyway migrations against the target environment's 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: staging + +runs: + using: "composite" + steps: + - name: Setup CI + uses: ./.github/composite/setup-ci + with: + 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 + 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/src/db/migrate.ts --environment=${{ inputs.ENVIRONMENT }} diff --git a/.github/scripts/src/db/migrate.ts b/.github/scripts/src/db/migrate.ts new file mode 100644 index 0000000..ee00b57 --- /dev/null +++ b/.github/scripts/src/db/migrate.ts @@ -0,0 +1,91 @@ +import { EnvClient, EnvClientStrategy } from "@tahminator/pipeline"; +import { $ } from "bun"; +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"], + require: true, + }) + .parseAsync(); + + const envClient = EnvClient.create(EnvClientStrategy.SOPS); + const ciEnv = await envClient.readFromEnv("secrets.ci.yaml"); + const { + DATABASE_NAME, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_USER, + DATABASE_PASSWORD, + } = parseCiEnv(ciEnv, environment); + + 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, 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 secrets.ci.yaml"); + } + return v; + })(); + + const DATABASE_HOST = (() => { + const v = ciEnv["PG_HOST"]; + if (!v) { + throw new Error("Missing PG_HOST from secrets.ci.yaml"); + } + return v; + })(); + + const DATABASE_PORT = (() => { + const v = ciEnv["PG_PORT"]; + if (!v) { + throw new Error("Missing PG_PORT from secrets.ci.yaml"); + } + return v; + })(); + + const DATABASE_USER = `patchats-${roleSuffix}-app`; + + const DATABASE_PASSWORD = (() => { + const v = ciEnv[`PG_ROLE_patchats-${roleSuffix}-app`]; + if (!v) { + throw new Error( + `Missing PG_ROLE_patchats-${roleSuffix}-app from secrets.ci.yaml`, + ); + } + 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..442a2d4 --- /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 + needs: [backend-pretest, frontend-pretest] + + 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 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