# This action will trigger when a PR is commented on with `/deploy-pr` or when the workflow is manually triggered. name: Deploy PR on: issue_comment: types: [created, edited] workflow_dispatch: inputs: pr_number: description: "PR number" required: true skip_build: description: "Skip build job" required: false default: false env: REPO: ghcr.io/coder/coder-preview permissions: contents: read packages: write pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }} cancel-in-progress: false 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' outputs: PR_NUMBER: ${{ steps.pr_info.outputs.PR_NUMBER }} PR_TITLE: ${{ steps.pr_info.outputs.PR_TITLE }} PR_URL: ${{ steps.pr_info.outputs.PR_URL }} 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 }} runs-on: "ubuntu-latest" steps: - name: Get PR number, title, and branch name 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_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 echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT echo "PR_TITLE=$PR_TITLE" >> $GITHUB_OUTPUT echo "PR_BRANCH=$PR_BRANCH" >> $GITHUB_OUTPUT - name: Set required tags id: set_tags run: | set -euxo pipefail echo "CODER_BASE_IMAGE_TAG=$CODER_BASE_IMAGE_TAG" >> $GITHUB_OUTPUT echo "CODER_IMAGE_TAG=$CODER_IMAGE_TAG" >> $GITHUB_OUTPUT env: 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 with: issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }} body: | :rocket: Deploying PR ${{ steps.pr_info.outputs.PR_NUMBER }} ... :warning: This deployment will be deleted when the PR is closed. reactions: "+1" build: needs: pr_commented # 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) 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 }} steps: - name: Checkout uses: actions/checkout@v3 with: ref: ${{ env.PR_BRANCH }} fetch-depth: 0 - name: Setup Node uses: ./.github/actions/setup-node - name: Setup Go uses: ./.github/actions/setup-go - name: Setup sqlc uses: ./.github/actions/setup-sqlc - name: GHCR Login uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Linux amd64 Docker image run: | set -euxo pipefail go mod download make gen/mark-fresh export DOCKER_IMAGE_NO_PREREQUISITES=true version="$(./scripts/version.sh)" export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")" make -j build/coder_linux_amd64 ./scripts/build_docker.sh \ --arch amd64 \ --target ${{ env.CODER_IMAGE_TAG }} \ --version $version \ --push \ build/coder_linux_amd64 deploy: needs: [build, pr_commented] # 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' 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: "https://pr${{ needs.pr_commented.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" steps: - name: Check if image exists 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) if [ -z "$foundTag" ]; then echo "Image not found" echo "${{ env.CODER_IMAGE_TAG }} not found in ghcr.io/coder/coder-preview" echo "Please remove --skip-build from the comment or ./scripts/deploy-pr.sh" exit 1 fi - 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 run: | set -euxo pipefail # try to delete the namespace, but don't fail if it doesn't exist kubectl delete namespace "pr${{ env.PR_NUMBER }}" || true kubectl create namespace "pr${{ env.PR_NUMBER }}" - name: Setup ingress run: | cat < ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: pr${{ env.PR_NUMBER }} namespace: pr${{ env.PR_NUMBER }} annotations: cert-manager.io/cluster-issuer: letsencrypt spec: tls: - hosts: - "${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" - "*.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" secretName: pr${{ env.PR_NUMBER }}-tls rules: - host: "pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" http: paths: - pathType: Prefix path: "/" backend: service: name: coder port: number: 80 EOF kubectl apply -f ingress.yaml - name: Set up PostgreSQL database run: | helm repo add bitnami https://charts.bitnami.com/bitnami helm install coder-db bitnami/postgresql \ --namespace pr${{ env.PR_NUMBER }} \ --set auth.username=coder \ --set auth.password=coder \ --set auth.database=coder \ --set persistence.size=10Gi 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: Create values.yaml run: | cat < pr-deploy-values.yaml coder: image: repo: ${{ env.REPO }} tag: pr${{ env.PR_NUMBER }} pullPolicy: Always service: type: ClusterIP env: - name: "CODER_ACCESS_URL" value: "https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" - name: "CODER_WILDCARD_ACCESS_URL" value: "*--pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" - name: "CODER_EXPERIMENTS" value: "*" - name: CODER_PG_CONNECTION_URL valueFrom: secretKeyRef: name: coder-db-url key: url - name: "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS" value: "true" - name: "CODER_OAUTH2_GITHUB_CLIENT_ID" value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID }}" - name: "CODER_OAUTH2_GITHUB_CLIENT_SECRET" value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET }}" - name: "CODER_OAUTH2_GITHUB_ALLOWED_ORGS" value: "coder" EOF - name: Install Helm chart run: | helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \ --namespace "pr${{ env.PR_NUMBER }}" \ --values ./pr-deploy-values.yaml \ --force - name: Install coder-logstream-kube 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 \ --namespace "pr${{ env.PR_NUMBER }}" \ --set url="https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" - name: Get Coder binary run: | set -euxo pipefail DEST="${HOME}/coder" URL="${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64" mkdir -p "$(dirname ${DEST})" COUNT=0 until $(curl --output /dev/null --silent --head --fail "$URL"); do printf '.' sleep 5 COUNT=$((COUNT+1)) if [ $COUNT -ge 60 ]; then echo "Timed out waiting for URL to be available" exit 1 fi done curl -fsSL "$URL" -o "${DEST}" chmod +x "${DEST}" "${DEST}" version - name: Create first user, template and workspace id: setup_deployment run: | set -euxo pipefail # Create first user # create a masked random password 12 characters long password=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-12) # add mask so that the password is not printed to the logs echo "::add-mask::$password" echo "password=$password" >> $GITHUB_OUTPUT /home/runner/coder login \ --first-user-username pr${{ env.PR_NUMBER }} \ --first-user-email ${{ env.PR_NUMBER }}@coder.com \ --first-user-password $password \ --first-user-trial \ --use-token-as-session \ ${{ env.PR_DEPLOYMENT_ACCESS_URL }} # Create template /home/runner/coder templates init --id kubernetes && cd ./kubernetes/ && /home/runner/coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} # Create workspace cat < workspace.yaml cpu: "2" memory: "4" home_disk_size: "2" EOF /home/runner/coder create --template="kubernetes" pr${{ env.PR_NUMBER }} --rich-parameter-file ./workspace.yaml -y /home/runner/coder stop pr${{ env.PR_NUMBER }} -y - name: Send Slack notification run: | curl -s -o /dev/null -X POST -H 'Content-type: application/json' \ -d \ '{ "pr_number": "'"${{ env.PR_NUMBER }}"'", "pr_url": "'"${{ env.PR_URL }}"'", "pr_title": "'"${{ env.PR_TITLE }}"'", "pr_access_url": "'"${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'", "pr_username": "'"pr${{ env.PR_NUMBER }}"'", "pr_email": "'"${{ env.PR_NUMBER }}@coder.com"'", "pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'", "pr_actor": "'"${{ github.actor }}"'" }' \ ${{ secrets.PR_DEPLOYMENTS_SLACK_WEBHOOK }} echo "Slack notification sent" - 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 direction: last - name: Comment on PR uses: peter-evans/create-or-update-comment@v3 if: github.event_name == 'issue_comment' 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](${{ env.PR_DEPLOYMENT_ACCESS_URL }}). :warning: This deployment will be deleted when the PR is closed. reactions: rocket