From b47bd7ccb5ec6f7ef170742a767f97406755bd2e Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Thu, 3 Aug 2023 18:11:07 +0300 Subject: [PATCH] ci: implement automatic upgrade of PR deployment (#8876) --- .github/workflows/pr-cleanup.yaml | 10 +- .github/workflows/pr-deploy.yaml | 230 ++++++++++++++++++++---------- docs/CONTRIBUTING.md | 21 +-- docs/images/pr-deploy-manual.png | Bin 0 -> 27155 bytes 4 files changed, 174 insertions(+), 87 deletions(-) create mode 100644 docs/images/pr-deploy-manual.png diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index cb3e6429a2..1ec7e4a731 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -1,7 +1,7 @@ name: Cleanup PR deployment and image on: pull_request: - types: [closed] + types: closed workflow_dispatch: inputs: pr_number: @@ -63,5 +63,11 @@ jobs: ( curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records/$record_id" \ -H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \ - -H "Content-Type:application/json" + -H "Content-Type:application/json" | jq -r '.success' ) || echo "DNS record not found" + + - name: "Delete certificate" + if: ${{ github.event.pull_request.merged == true }} + run: | + set -euxo pipefail + kuebctl delete certificate "pr${{ steps.pr_number.outputs.PR_NUMBER }}-tls" -n pr-deployment-certs || echo "certificate not found" diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 7b8b3bea69..3202326b91 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -1,8 +1,11 @@ -# This action will trigger when a PR is commented on with `/deploy-pr` or when the workflow is manually triggered. +# This action will trigger when +# 1. when the workflow is manually triggered +# 2. ./scripts/deploy_pr.sh is run locally +# 3. when a PR is updated name: Deploy PR on: - issue_comment: - types: [created, edited] + pull_request: + types: synchronize workflow_dispatch: inputs: pr_number: @@ -29,12 +32,12 @@ permissions: pull-requests: write concurrency: - group: ${{ github.workflow }}-PR-${{ (github.event.issue.number) || (github.event.inputs.pr_number) }} + group: ${{ github.workflow }}-PR-${{ github.event.pull_request.number || github.event.inputs.pr_number }} cancel-in-progress: true jobs: - pr_commented: - if: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/deploy-pr') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'OWNER')) || github.event_name == 'workflow_dispatch' + get_info: + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' outputs: PR_NUMBER: ${{ steps.pr_info.outputs.PR_NUMBER }} PR_TITLE: ${{ steps.pr_info.outputs.PR_TITLE }} @@ -42,6 +45,8 @@ jobs: PR_BRANCH: ${{ steps.pr_info.outputs.PR_BRANCH }} CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }} CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }} + NEW: ${{ steps.check_deployment.outputs.new }} + BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }} runs-on: "ubuntu-latest" steps: @@ -49,11 +54,7 @@ jobs: id: pr_info run: | set -euxo pipefail - if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then - PR_NUMBER=${{ github.event.inputs.pr_number }} - else - PR_NUMBER=${{ github.event.issue.number }} - fi + PR_NUMBER=${{ github.event.inputs.pr_number || github.event.pull_request.number }} PR_TITLE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.title') PR_BRANCH=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.head.ref') echo "PR_URL=https://github.com/coder/coder/pull/$PR_NUMBER" >> $GITHUB_OUTPUT @@ -71,28 +72,98 @@ jobs: CODER_BASE_IMAGE_TAG: ghcr.io/coder/coder-preview-base:pr${{ steps.pr_info.outputs.PR_NUMBER }} CODER_IMAGE_TAG: ghcr.io/coder/coder-preview:pr${{ steps.pr_info.outputs.PR_NUMBER }} - - name: Comment on PR - id: comment_id - if: github.event_name == 'issue_comment' - uses: peter-evans/create-or-update-comment@v3 + - name: Set up kubeconfig + run: | + set -euxo pipefail + mkdir -p ~/.kube + echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config + export KUBECONFIG=~/.kube/config + + - name: Check if the helm deployment already exists + id: check_deployment + run: | + set -euxo pipefail + if helm status "pr${{ steps.pr_info.outputs.PR_NUMBER }}" --namespace "pr${{ steps.pr_info.outputs.PR_NUMBER }}" > /dev/null 2>&1; then + echo "Deployment already exists. Skipping deployment." + new=false + else + echo "Deployment doesn't exist. Creating a new one." + new=true + fi + echo "new=$new" >> $GITHUB_OUTPUT + + - name: Find Comment + uses: peter-evans/find-comment@v2 + id: fc with: issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }} + comment-author: "github-actions[bot]" + body-includes: ":rocket:" + direction: last + + - name: Comment on PR + id: comment_id + uses: peter-evans/create-or-update-comment@v3 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }} + edit-mode: replace body: | + --- :rocket: Deploying PR ${{ steps.pr_info.outputs.PR_NUMBER }} ... - :warning: This deployment will be deleted when the PR is closed. - reactions: "+1" + --- + reactions: eyes + reactions-edit-mode: replace + + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ steps.pr_info.outputs.PR_BRANCH }} + fetch-depth: 0 + + - name: Check changed files + uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + all: + - "**" + ignored: + - "docs/**" + - "README.md" + - "examples/web-server/**" + - "examples/monitoring/**" + - "examples/lima/**" + - ".github/**" + - "offlinedocs/**" + - ".devcontainer/**" + - "helm/**" + - "*[^g][^o][^.][^s][^u][^m]*" + - "*[^g][^o][^.][^m][^o][^d]*" + - "*[^M][^a][^k][^e][^f][^i][^l][^e]*" + - "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e]*" + - "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e][.][b][^a][^s][^e]*" + + - name: Print number of changed files + run: | + set -euxo pipefail + echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}" + echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}" build: - needs: pr_commented + needs: get_info # Skips the build job if the workflow was triggered by a workflow_dispatch event and the skip_build input is set to true # or if the workflow was triggered by an issue_comment event and the comment body contains --skip-build - if: (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_build == 'false') || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '--skip-build') != true) + # alwyas run the build job if the workflow was triggered by a pull_request event + if: | + (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_build == 'false') || + (github.event_name == 'pull_request' && needs.get_info.outputs.NEW == 'false') runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} env: DOCKER_CLI_EXPERIMENTAL: "enabled" - CODER_IMAGE_TAG: ${{ needs.pr_commented.outputs.CODER_IMAGE_TAG }} - PR_NUMBER: ${{ needs.pr_commented.outputs.PR_NUMBER }} - PR_BRANCH: ${{ needs.pr_commented.outputs.PR_BRANCH }} + CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} + PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }} + PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }} steps: - name: Checkout uses: actions/checkout@v3 @@ -101,15 +172,19 @@ jobs: fetch-depth: 0 - name: Setup Node + if: needs.get_info.outputs.BUILD == 'true' uses: ./.github/actions/setup-node - name: Setup Go + if: needs.get_info.outputs.BUILD == 'true' uses: ./.github/actions/setup-go - name: Setup sqlc + if: needs.get_info.outputs.BUILD == 'true' uses: ./.github/actions/setup-sqlc - name: GHCR Login + if: needs.get_info.outputs.BUILD == 'true' uses: docker/login-action@v2 with: registry: ghcr.io @@ -117,6 +192,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Linux amd64 Docker image + if: needs.get_info.outputs.BUILD == 'true' run: | set -euxo pipefail go mod download @@ -133,19 +209,27 @@ jobs: build/coder_linux_amd64 deploy: - needs: [build, pr_commented] + needs: [build, get_info] # Run deploy job only if build job was successful or skipped - if: always() && (needs.build.result == 'success' || needs.build.result == 'skipped') && needs.pr_commented.result == 'success' + if: always() && (needs.build.result == 'success' || needs.build.result == 'skipped') && needs.get_info.result == 'success' runs-on: "ubuntu-latest" env: - CODER_IMAGE_TAG: ${{ needs.pr_commented.outputs.CODER_IMAGE_TAG }} - PR_NUMBER: ${{ needs.pr_commented.outputs.PR_NUMBER }} - PR_TITLE: ${{ needs.pr_commented.outputs.PR_TITLE }} - PR_URL: ${{ needs.pr_commented.outputs.PR_URL }} - PR_BRANCH: ${{ needs.pr_commented.outputs.PR_BRANCH }} - PR_DEPLOYMENT_ACCESS_URL: "pr${{ needs.pr_commented.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" + CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} + PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }} + PR_TITLE: ${{ needs.get_info.outputs.PR_TITLE }} + PR_URL: ${{ needs.get_info.outputs.PR_URL }} + PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }} + PR_DEPLOYMENT_ACCESS_URL: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" steps: + - name: Set up kubeconfig + run: | + set -euxo pipefail + mkdir -p ~/.kube + echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config + export KUBECONFIG=~/.kube/config + - name: Check if image exists + if: needs.get_info.outputs.NEW == 'true' run: | set -euxo pipefail foundTag=$(curl -fsSL https://github.com/coder/coder/pkgs/container/coder-preview | grep -o ${{ env.CODER_IMAGE_TAG }} | head -n 1) @@ -157,27 +241,20 @@ jobs: fi - name: Add DNS record to Cloudflare + if: needs.get_info.outputs.NEW == 'true' run: | - ( - curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \ - -H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \ - -H "Content-Type:application/json" \ - --data '{"type":"CNAME","name":"*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}","content":"${{ env.PR_DEPLOYMENT_ACCESS_URL }}","ttl":1,"proxied":false}' - ) + curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \ + -H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type:application/json" \ + --data '{"type":"CNAME","name":"*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}","content":"${{ env.PR_DEPLOYMENT_ACCESS_URL }}","ttl":1,"proxied":false}' - name: Checkout uses: actions/checkout@v3 with: ref: ${{ env.PR_BRANCH }} - - name: Set up kubeconfig - run: | - set -euxo pipefail - mkdir -p ~/.kube - echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config - export KUBECONFIG=~/.kube/config - - name: Create PR namespace + if: needs.get_info.outputs.NEW == 'true' run: | set -euxo pipefail # try to delete the namespace, but don't fail if it doesn't exist @@ -185,6 +262,7 @@ jobs: kubectl create namespace "pr${{ env.PR_NUMBER }}" - name: Check and Create Certificate + if: needs.get_info.outputs.NEW == 'true' run: | # Using kubectl to check if a Certificate resource already exists # we are doing this to avoid letsenrypt rate limits @@ -216,6 +294,7 @@ jobs: ) - name: Set up PostgreSQL database + if: needs.get_info.outputs.NEW == 'true' run: | helm repo add bitnami https://charts.bitnami.com/bitnami helm install coder-db bitnami/postgresql \ @@ -227,28 +306,8 @@ jobs: kubectl create secret generic coder-db-url -n pr${{ env.PR_NUMBER }} \ --from-literal=url="postgres://coder:coder@coder-db-postgresql.pr${{ env.PR_NUMBER }}.svc.cluster.local:5432/coder?sslmode=disable" - - name: Get experiments - id: get_experiments - run: | - set -euxo pipefail - if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then - experiments=${{ github.event.inputs.experiments }} - else - # extract experiments part - extracted_experiments=$(echo "$COMMENT_BODY" | grep -oP '(?<=--experiments )[^ ]+') - # Validate that the experiments is a comma-separated list of alphanumeric strings, "*", or "-" - if [[ $extracted_experiments =~ ^[a-zA-Z0-9_*,\"-]+$ ]]; then - experiments=$extracted_experiments - else - echo "Invalid input: $extracted_experiments" - exit 1 - fi - fi - echo "experiments=$experiments" >> $GITHUB_OUTPUT - env: - COMMENT_BODY: ${{ github.event.comment.body || '' }} - - name: Create values.yaml + if: github.event_name == 'workflow_dispatch' run: | cat < pr-deploy-values.yaml coder: @@ -273,7 +332,7 @@ jobs: - name: "CODER_WILDCARD_ACCESS_URL" value: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}" - name: "CODER_EXPERIMENTS" - value: "*,${{ steps.get_experiments.outputs.experiments }}" + value: "${{ github.event.inputs.experiments }}" - name: CODER_PG_CONNECTION_URL valueFrom: secretKeyRef: @@ -289,14 +348,27 @@ jobs: value: "coder" EOF - - name: Install Helm chart + - name: Install/Upgrade Helm chart run: | - helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \ - --namespace "pr${{ env.PR_NUMBER }}" \ - --values ./pr-deploy-values.yaml \ - --force + set -euxo pipefail + if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then + helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \ + --namespace "pr${{ env.PR_NUMBER }}" \ + --values ./pr-deploy-values.yaml \ + --force + else + if [[ ${{ needs.get_info.outputs.BUILD }} == "true" ]]; then + helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \ + --namespace "pr${{ env.PR_NUMBER }}" \ + --reuse-values \ + --force + else + echo "Skipping helm upgrade, as there is no new image to deploy" + fi + fi - name: Install coder-logstream-kube + if: needs.get_info.outputs.NEW == 'true' run: | helm repo add coder-logstream-kube https://helm.coder.com/logstream-kube helm upgrade --install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \ @@ -304,6 +376,7 @@ jobs: --set url="https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" - name: Get Coder binary + if: needs.get_info.outputs.NEW == 'true' run: | set -euxo pipefail @@ -329,6 +402,7 @@ jobs: mv "${DEST}" /usr/local/bin/coder - name: Create first user, template and workspace + if: needs.get_info.outputs.NEW == 'true' id: setup_deployment run: | set -euxo pipefail @@ -364,6 +438,7 @@ jobs: coder stop test -y - name: Send Slack notification + if: needs.get_info.outputs.NEW == 'true' run: | curl -s -o /dev/null -X POST -H 'Content-type: application/json' \ -d \ @@ -382,23 +457,26 @@ jobs: - name: Find Comment uses: peter-evans/find-comment@v2 - if: github.event_name == 'issue_comment' id: fc with: issue-number: ${{ env.PR_NUMBER }} comment-author: "github-actions[bot]" - body-includes: This deployment will be deleted when the PR is closed + body-includes: ":rocket:" direction: last - name: Comment on PR uses: peter-evans/create-or-update-comment@v3 - if: github.event_name == 'issue_comment' + env: + STATUS: ${{ needs.get_info.outputs.NEW == 'true' && 'Created' || 'Updated' }} with: issue-number: ${{ env.PR_NUMBER }} edit-mode: replace comment-id: ${{ steps.fc.outputs.comment-id }} body: | - :heavy_check_mark: Deployed PR ${{ env.PR_NUMBER }} successfully. - :rocket: Access the deployment link [here](https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}). - :warning: This deployment will be deleted when the PR is closed. + --- + :heavy_check_mark: PR ${{ env.PR_NUMBER }} ${{ env.STATUS }} successfully. + :rocket: Access the credentials [here](${{ secrets.PR_DEPLOYMENTS_SLACK_CHANNEL_URL }}). + --- + cc: @${{ github.actor }} reactions: rocket + reactions-edit-mode: replace diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0bdaaa5be7..e794828520 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -81,17 +81,20 @@ Use the following `make` commands and scripts in development: ### Deploying a PR -You can test your changes by creating a PR deployment. A PR deployment can be triggered in two ways: +You can test your changes by creating a PR deployment. There are two ways to do this: -1. By commenting on the PR with `/deploy-pr` -2. By running `./scripts/deploy-pr.sh` -3. Available options - - `-s` or `--skip-build`, the image will not be built again, and the last image will be used. - - `-e EXPERIMENT1,EXPERIMENT2` or `--experiments EXPERIMENT1,EXPERIMENT2`, will enable the specified experiments. - - `-n` or `--dry-run` will display the context without deployment. e.g., branch name and PR number, etc. - - `-y` or `--yes`, will skip the CLI confirmation (only valid for the `./scripts/deploy-pr.sh`) +1. By running `./scripts/deploy-pr.sh` +2. By manually triggering the [`pr-deploy.yaml`](https://github.com/coder/coder/actions/workflows/pr-deploy.yaml) GitHub Action workflow + ![Deploy PR manually](./images/pr-deploy-manual.png) -> Note: all flags can be used with both `./scripts/deploy-pr.sh` and `/deploy-pr` comment on the PR. +#### Available options + +- `-s` or `--skip-build`, force prevents the build of the Docker image.(generally not needed as we are intelligently checking if the image needs to be built) +- `-e EXPERIMENT1,EXPERIMENT2` or `--experiments EXPERIMENT1,EXPERIMENT2`, will enable the specified experiments. (defaults to `*`) +- `-n` or `--dry-run` will display the context without deployment. e.g., branch name and PR number, etc. +- `-y` or `--yes`, will skip the CLI confirmation prompt. + +> Note: PR deployment will be re-deployed automatically when the PR is updated. It will use the last values automatically for redeployment. > You need to be a member or collaborator of the of [coder](github.com/coder) GitHub organization to be able to deploy a PR. diff --git a/docs/images/pr-deploy-manual.png b/docs/images/pr-deploy-manual.png new file mode 100644 index 0000000000000000000000000000000000000000..eab92cc2249e75e08119bc1d1e4897545f6f75e2 GIT binary patch literal 27155 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