diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6befcd555..120e1ab2f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -82,3 +82,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..8a21f4f39 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 diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 90de78fe8..671514939 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 @@ -42,19 +48,32 @@ jobs: 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 }} + POLL_INTERVAL: "10" run: | - WAIT_EXIT=0 - aws ssm wait command-executed \ - --command-id "$CMD_ID" \ - --instance-id "$INSTANCE_ID" || WAIT_EXIT=$? + poll_status() { + aws ssm get-command-invocation \ + --command-id "$CMD_ID" \ + --instance-id "$INSTANCE_ID" \ + --query "Status" \ + --output text 2>/dev/null || echo "Pending" + } + + STATUS=$(poll_status) + until [[ "$STATUS" =~ ^(Success|Failed|Cancelled|TimedOut)$ ]]; do + echo "Status: $STATUS — waiting ${POLL_INTERVAL}s" + sleep "$POLL_INTERVAL" + STATUS=$(poll_status) + done + echo "Final 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 $WAIT_EXIT + [[ "$STATUS" == "Success" ]] diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 000000000..2b7784a98 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,49 @@ +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 _/-]+\))?!?: .+' + + echo "Checking PR title: $PR_TITLE" + if echo "$PR_TITLE" | grep -qE "$PATTERN"; then + echo "PR title is valid." + exit 0 + fi + + echo "::error title=Invalid PR title::'$PR_TITLE' is not in Conventional Commits format" + { + echo "### ❌ PR title is not in Conventional Commits format" + echo "" + echo "**Your title:** \`$PR_TITLE\`" + echo "" + echo "**Required format:** \`type(scope): short summary\` (scope is optional)" + echo "" + echo "**Allowed types:** feat, fix, chore, docs, refactor, perf, test, ci, build, style, revert" + echo "" + echo "**Good examples:**" + echo "- \`feat(collection): Add new fields\`" + echo "- \`fix(auth): Correct token expiry check\`" + echo "- \`chore(ci): Add PR title check workflow\`" + echo "" + echo "Edit the PR title to match and this check re-runs automatically." + } >> "$GITHUB_STEP_SUMMARY" + exit 1