diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index bcb60b86b..77efeb019 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -85,3 +85,25 @@ jobs: source .venv/bin/activate coverage report --fail-under=70 working-directory: backend + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Build backend image + run: docker build -t kaapi-backend:ci ./backend + + - name: Prepare env for compose validation + run: cp .env.test.example .env + + - name: Validate compose files + env: + RABBITMQ_USER: ci + RABBITMQ_PASSWORD: ci + RABBITMQ_VHOST: ci + run: | + for f in docker-compose.yml docker-compose.dev.yml docker-compose.staging.yml; do + echo "Validating $f" + docker compose -f "$f" config -q + done diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 856fa8b05..d094405ad 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -6,7 +6,32 @@ on: - "v[0-9]+.[0-9]+.[0-9]+" # Deploy only when tags like v1.0.0, v2.1.0, etc., are created jobs: + verify-ci: + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - name: Confirm CI passed on the tagged commit + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + SHA="${{ github.sha }}" + echo "Checking Kaapi CI conclusion for ${SHA}" + CONCLUSION=$(gh api \ + "repos/${{ github.repository }}/actions/runs?head_sha=${SHA}" \ + --jq 'first(.workflow_runs[] + | select(.name == "Kaapi CI" and .status == "completed") + | .conclusion) // "missing"') + echo "Kaapi CI conclusion: ${CONCLUSION}" + if [ "${CONCLUSION}" != "success" ]; then + echo "::error::Kaapi CI has not passed for ${SHA} (conclusion: ${CONCLUSION}). Aborting release." + exit 1 + fi + build: + needs: verify-ci runs-on: ubuntu-latest environment: AWS_ENV_VARS @@ -83,9 +108,11 @@ jobs: aws ecs update-service \ --cluster ${{ vars.AWS_RESOURCE_PREFIX }}-cluster \ --service ${{ vars.AWS_RESOURCE_PREFIX }}-service \ + --task-definition ${{ vars.AWS_RESOURCE_PREFIX }}-task \ --force-new-deployment aws ecs update-service \ --cluster ${{ vars.AWS_RESOURCE_PREFIX }}-cluster \ --service ${{ vars.AWS_RESOURCE_PREFIX }}-celery-task \ + --task-definition ${{ vars.AWS_RESOURCE_PREFIX }}-celery-task \ --force-new-deployment diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 90de78fe8..39c788eef 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -1,8 +1,10 @@ name: Deploy Kaapi staging to EC2 on: - push: + workflow_run: + workflows: ["Kaapi CI"] branches: [main] + types: [completed] workflow_dispatch: concurrency: @@ -13,6 +15,10 @@ jobs: deploy: runs-on: ubuntu-latest environment: AWS_STAGING_ENV + # workflow_dispatch runs unconditionally; workflow_run only on a green CI. + if: >- + ${{ github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' }} permissions: id-token: write @@ -35,26 +41,47 @@ jobs: --instance-ids "$INSTANCE_ID" \ --document-name "AWS-RunShellScript" \ --comment "Deploy kaapi-backend kaapi-staging" \ - --parameters commands='["set -eux","chown -R ubuntu:ubuntu /data/kaapi-backend","sudo -iu ubuntu bash -lc \"cd /data/kaapi-backend && git fetch --all && git pull origin main && SECRET_ID='"$SECRET_ID"' sh scripts/fetch-secrets.sh && docker compose -f docker-compose.staging.yml build && docker compose -f docker-compose.staging.yml --profile migrate run --rm migrate && docker compose -f docker-compose.staging.yml up -d --remove-orphans && docker image prune -f\""]' \ + --parameters commands='["set -eux","chown -R ubuntu:ubuntu /data/kaapi-backend","sudo -iu ubuntu bash -lc \"cd /data/kaapi-backend && git fetch --all && git pull origin main && SECRET_ID='"$SECRET_ID"' sh scripts/fetch-secrets.sh && docker compose -f docker-compose.staging.yml build && docker compose -f docker-compose.staging.yml --profile migrate run --rm migrate && docker compose -f docker-compose.staging.yml up -d --wait --remove-orphans && docker image prune -f\""]' \ --cloud-watch-output-config CloudWatchOutputEnabled=true \ --query "Command.CommandId" --output text) echo "cmd_id=$CMD_ID" >> "$GITHUB_OUTPUT" echo "Sent SSM command: $CMD_ID" - name: Wait for SSM command to finish + timeout-minutes: 10 env: INSTANCE_ID: ${{ secrets.STAGING_EC2_INSTANCE_ID }} CMD_ID: ${{ steps.ssm.outputs.cmd_id }} run: | - WAIT_EXIT=0 - aws ssm wait command-executed \ - --command-id "$CMD_ID" \ - --instance-id "$INSTANCE_ID" || WAIT_EXIT=$? + while true; do + if aws ssm wait command-executed \ + --command-id "$CMD_ID" \ + --instance-id "$INSTANCE_ID"; then + break # waiter succeeds only on Status == Success + fi + + STATUS=$(aws ssm get-command-invocation \ + --command-id "$CMD_ID" \ + --instance-id "$INSTANCE_ID" \ + --query "Status" --output text 2>/dev/null || echo "Pending") + case "$STATUS" in + Success) break ;; + Failed|Cancelled|Cancelling|TimedOut) + echo "Deployment ended with status: $STATUS" + aws ssm get-command-invocation \ + --command-id "$CMD_ID" \ + --instance-id "$INSTANCE_ID" \ + --query '{Status:Status,Stdout:StandardOutputContent,Stderr:StandardErrorContent}' \ + --output json + exit 1 ;; + *) echo "Still running (status: $STATUS) — waiter capped out, re-waiting" ;; + esac + done + + echo "Deployment completed successfully." aws ssm get-command-invocation \ --command-id "$CMD_ID" \ --instance-id "$INSTANCE_ID" \ --query '{Status:Status,Stdout:StandardOutputContent,Stderr:StandardErrorContent}' \ --output json - - exit $WAIT_EXIT diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 000000000..b7da485e4 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,32 @@ +name: PR Title Check + +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +permissions: + pull-requests: read + +concurrency: + group: pr-title-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Validate PR title follows Conventional Commits + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + # type(scope): summary — scope optional, `!` marks a breaking change. + # Keep types in sync with what the PR formatter is allowed to emit. + PATTERN='^(feat|fix|chore|docs|refactor|perf|test|ci|build|style|revert)(\([a-z0-9 _/-]+\))?!?: .+' + + if echo "$PR_TITLE" | grep -qE "$PATTERN"; then + echo "PR title is valid." + exit 0 + fi + + echo "::error title=Invalid PR title::'$PR_TITLE' must follow Conventional Commits — 'type(scope): summary' (scope optional). Allowed types: feat, fix, chore, docs, refactor, perf, test, ci, build, style, revert." + exit 1