diff --git a/.github/workflows/deploy-core.yml b/.github/workflows/deploy-core.yml new file mode 100644 index 000000000..65824837a --- /dev/null +++ b/.github/workflows/deploy-core.yml @@ -0,0 +1,219 @@ +name: Deploy Core + +on: + workflow_call: + inputs: + image_tag: + description: 'Docker image tag to deploy' + type: string + required: true + sha: + description: 'Git commit SHA to deploy' + type: string + required: true + description: + description: 'Deployment description' + type: string + required: true + pr_number: + description: 'Pull request number (optional)' + type: string + required: false + outputs: + deployment_id: + description: 'GitHub deployment ID' + value: ${{ jobs.deploy.outputs.deployment_id }} + status: + description: 'Deployment status (success/failure)' + value: ${{ jobs.deploy.outputs.status }} + +concurrency: + group: deploy-staging + cancel-in-progress: false + +permissions: + contents: read + deployments: write + +jobs: + deploy: + name: Deploy to staging + runs-on: ubuntu-latest + + environment: + name: staging + url: ${{ vars.DEPLOY_URL || 'https://dev.opensourcepos.org' }} + deployment: false + + outputs: + deployment_id: ${{ steps.deployment.outputs.deployment_id }} + status: ${{ steps.webhook.outputs.status }} + + steps: + - name: Create GitHub Deployment + id: deployment + env: + GH_TOKEN: ${{ github.token }} + IMAGE_TAG: ${{ inputs.image_tag }} + REF_SHA: ${{ inputs.sha }} + DESCRIPTION: ${{ inputs.description }} + run: | + set -euo pipefail + + DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \ + -X POST \ + -f ref="${REF_SHA}" \ + -f environment="staging" \ + -f description="${DESCRIPTION}" \ + -F auto_merge=false \ + -F required_contexts[] \ + --jq '.id') + + if [ -z "$DEPLOYMENT_ID" ]; then + echo "::error::Failed to create deployment" + exit 1 + fi + + echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT" + echo "Created deployment: $DEPLOYMENT_ID" + + - name: Set deployment status to in_progress + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \ + -X POST \ + -f state="in_progress" \ + -f description="Deployment in progress..." \ + -f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + + - name: Trigger deployment webhook + id: webhook + env: + DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }} + DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }} + DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }} + IMAGE_TAG: ${{ inputs.image_tag }} + REF_SHA: ${{ inputs.sha }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + set -euo pipefail + + if [ -z "$DEPLOY_WEBHOOK_URL" ]; then + echo "::error::DEPLOY_WEBHOOK_URL secret is not configured" + echo "Please add the DEPLOY_WEBHOOK_URL secret in your repository settings" + echo "status=failure" >> "$GITHUB_OUTPUT" + exit 1 + fi + + REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}" + REPO_NAMESPACE="${REPO_NAME%%/*}" + REPO_SHORT_NAME="${REPO_NAME#*/}" + PUSHED_AT=$(date +%s) + + if [ -n "$PR_NUMBER" ]; then + PAYLOAD=$(jq -n \ + --arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --argjson pushed_at "$PUSHED_AT" \ + --arg pusher "$GITHUB_ACTOR" \ + --arg tag "$IMAGE_TAG" \ + --arg repo_name "$REPO_NAME" \ + --arg name "$REPO_SHORT_NAME" \ + --arg namespace "$REPO_NAMESPACE" \ + --arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \ + --arg deployment_id "$DEPLOYMENT_ID" \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg sha "$REF_SHA" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg actor "$GITHUB_ACTOR" \ + --argjson pr_number "$PR_NUMBER" \ + '{ + callback_url: $callback_url, + push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag}, + repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"}, + github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor, pull_request: $pr_number} + }') + else + PAYLOAD=$(jq -n \ + --arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --argjson pushed_at "$PUSHED_AT" \ + --arg pusher "$GITHUB_ACTOR" \ + --arg tag "$IMAGE_TAG" \ + --arg repo_name "$REPO_NAME" \ + --arg name "$REPO_SHORT_NAME" \ + --arg namespace "$REPO_NAMESPACE" \ + --arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \ + --arg deployment_id "$DEPLOYMENT_ID" \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg sha "$REF_SHA" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg actor "$GITHUB_ACTOR" \ + '{ + callback_url: $callback_url, + push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag}, + repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"}, + github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor} + }') + fi + + echo "Sending webhook..." + echo "Image: ${IMAGE_TAG}" + echo "Environment: staging" + + HEADERS=(-H "Content-Type: application/json") + + if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then + SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //') + HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE") + echo "Using HMAC-SHA256 signature verification" + else + echo "::warning::DEPLOY_WEBHOOK_SECRET not set - webhook calls will not be signed" + echo "For security, configure DEPLOY_WEBHOOK_SECRET in your repository settings" + fi + + HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \ + -o response.txt -w "%{http_code}" \ + -X POST \ + "${HEADERS[@]}" \ + -d "$PAYLOAD" \ + "$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000" + + echo "Response code: $HTTP_CODE" + if [ -s response.txt ]; then + cat response.txt + fi + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "status=success" >> "$GITHUB_OUTPUT" + else + echo "status=failure" >> "$GITHUB_OUTPUT" + fi + + - name: Set deployment status + if: always() + env: + GH_TOKEN: ${{ github.token }} + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + set -euo pipefail + + STATE="${{ steps.webhook.outputs.status }}" + + if [ "$STATE" = "success" ]; then + DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" \ + '"Deployed image \($tag) to staging"') + + gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \ + -X POST \ + -f state="success" \ + -f description="$DESCRIPTION" + else + gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \ + -X POST \ + -f state="failure" \ + -f description="Deployment failed" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml new file mode 100644 index 000000000..d13e40c47 --- /dev/null +++ b/.github/workflows/deploy-pr.yml @@ -0,0 +1,79 @@ +name: PR Deploy + +on: + pull_request_review: + types: [submitted] + +concurrency: + group: staging-deploy + cancel-in-progress: false + +permissions: + contents: read + deployments: write + pull-requests: write + +jobs: + prepare: + name: Prepare deployment + runs-on: ubuntu-latest + if: > + github.event.review.state == 'approved' && + github.event.pull_request.head.repo.full_name == github.repository + outputs: + image_tag: ${{ steps.image.outputs.tag }} + sha: ${{ github.event.pull_request.head.sha }} + pr_number: ${{ github.event.pull_request.number }} + + steps: + - name: Checkout PR + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Get image tag + id: image + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_SHA: ${{ github.event.pull_request.head.sha }} + run: | + IMAGE_TAG="pr-${PR_NUMBER}-${PR_SHA:0:7}" + echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT" + + deploy: + name: Deploy to staging + needs: prepare + uses: ./.github/workflows/deploy-core.yml + with: + image_tag: ${{ needs.prepare.outputs.image_tag }} + sha: ${{ needs.prepare.outputs.sha }} + description: Deploy PR #${{ needs.prepare.outputs.pr_number }} to staging + pr_number: ${{ needs.prepare.outputs.pr_number }} + secrets: inherit + + comment: + name: Comment deployment status + needs: [prepare, deploy] + if: always() + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + PR_NUMBER: ${{ needs.prepare.outputs.pr_number }} + REF_SHA: ${{ needs.prepare.outputs.sha }} + STATUS: ${{ needs.deploy.outputs.status }} + + steps: + - name: Comment on PR + run: | + if [ "$STATUS" = "success" ]; then + BODY=$(jq -nr --arg tag "$IMAGE_TAG" --arg sha "$REF_SHA" --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + '"āœ… **Staging deployment completed**\n\nšŸ”— **URL**: https://dev.opensourcepos.org\nšŸ“¦ **Image Tag**: `\($tag)`\nšŸ”Ø **Commit**: \($sha)\n\nView logs: \($url)"') + else + BODY=$(jq -nr --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + '"āŒ **Staging deployment failed**\n\nCheck the [workflow logs](\($url)) for details."') + fi + + gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + -X POST \ + -f body="$BODY" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..a8d9d6af3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,23 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Docker image tag to deploy (e.g., v3.4.0, latest)' + required: true + default: 'latest' + +permissions: + contents: read + deployments: write + +jobs: + deploy: + name: Deploy to staging + uses: ./.github/workflows/deploy-core.yml + with: + image_tag: ${{ inputs.image_tag }} + sha: ${{ github.sha }} + description: Deploy image ${{ inputs.image_tag }} + secrets: inherit \ No newline at end of file