diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index cb3e6429a2233..1ec7e4a73154f 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 7b8b3bea69bc9..3202326b919dc 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: 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 - if: github.event_name == 'issue_comment' 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 0bdaaa5be78ff..e794828520e81 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 0000000000000..eab92cc2249e7 Binary files /dev/null and b/docs/images/pr-deploy-manual.png differ