ci: upgrade pr deployments workflow (#8924)

This commit is contained in:
Muhammad Atif Ali
2023-08-11 13:49:23 +03:00
committed by GitHub
parent 4d8152d543
commit 114ad4624e
9 changed files with 575 additions and 207 deletions
+162 -185
View File
@@ -4,24 +4,28 @@
# 3. when a PR is updated
name: Deploy PR
on:
pull_request:
types: synchronize
push:
workflow_dispatch:
inputs:
pr_number:
description: "PR number"
type: number
required: true
skip_build:
description: "Skip build job"
required: false
type: boolean
default: false
experiments:
description: "Experiments to enable"
required: false
type: string
default: "*"
build:
description: "Force new build"
required: false
type: boolean
default: false
deploy:
description: "Force new deployment"
required: false
type: boolean
default: false
env:
REPO: ghcr.io/coder/coder-preview
@@ -29,43 +33,70 @@ env:
permissions:
contents: read
packages: write
pull-requests: write
pull-requests: write # needed for commenting on PRs
concurrency:
group: ${{ github.workflow }}-PR-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check_pr:
runs-on: ubuntu-latest
outputs:
PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Check if PR is open
id: check_pr
run: |
set -euo pipefail
pr_open=true
if [[ "$(gh pr view --json state | jq -r '.state')" != "OPEN" ]]; then
echo "PR doesn't exist or is closed."
pr_open=false
fi
echo "pr_open=$pr_open" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
get_info:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request'
needs: check_pr
if: ${{ needs.check_pr.outputs.PR_OPEN == 'true' }}
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 }}
NEW: ${{ steps.check_deployment.outputs.new }}
BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.new }}
NEW: ${{ steps.check_deployment.outputs.NEW }}
BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.NEW }}
runs-on: "ubuntu-latest"
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get PR number, title, and branch name
id: pr_info
run: |
set -euxo pipefail
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
set -euo pipefail
PR_NUMBER=$(gh pr view --json number | jq -r '.number')
PR_TITLE=$(gh pr view --json title | jq -r '.title')
PR_URL=$(gh pr view --json url | jq -r '.url')
echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "PR_TITLE=$PR_TITLE" >> $GITHUB_OUTPUT
echo "PR_BRANCH=$PR_BRANCH" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set required tags
id: set_tags
run: |
set -euxo pipefail
set -euo pipefail
echo "CODER_BASE_IMAGE_TAG=$CODER_BASE_IMAGE_TAG" >> $GITHUB_OUTPUT
echo "CODER_IMAGE_TAG=$CODER_IMAGE_TAG" >> $GITHUB_OUTPUT
env:
@@ -74,7 +105,7 @@ jobs:
- name: Set up kubeconfig
run: |
set -euxo pipefail
set -euo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
@@ -82,53 +113,21 @@ jobs:
- name: Check if the helm deployment already exists
id: check_deployment
run: |
set -euxo pipefail
set -euo 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
NEW=false
else
echo "Deployment doesn't exist."
new=true
NEW=true
fi
echo "new=$new" >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v2
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
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 == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
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 }} ...
---
reactions: eyes
reactions-edit-mode: replace
- name: Checkout
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
uses: actions/checkout@v3
with:
ref: ${{ steps.pr_info.outputs.PR_BRANCH }}
fetch-depth: 0
echo "NEW=$NEW" >> $GITHUB_OUTPUT
- name: Check changed files
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
uses: dorny/paths-filter@v2
id: filter
with:
base: ${{ github.ref }}
filters: |
all:
- "**"
@@ -149,47 +148,76 @@ jobs:
- "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e][.][b][^a][^s][^e]*"
- name: Print number of changed files
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
run: |
set -euxo pipefail
set -euo pipefail
echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}"
echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}"
- name: Print job outputs
run: |
set -euo pipefail
# Print all outputs of this job
echo "PR_NUMBER=${{ steps.pr_info.outputs.PR_NUMBER }}"
echo "PR_TITLE=${{ steps.pr_info.outputs.PR_TITLE }}"
echo "PR_URL=${{ steps.pr_info.outputs.PR_URL }}"
echo "CODER_BASE_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}"
echo "CODER_IMAGE_TAG=${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}"
echo "NEW=${{ steps.check_deployment.outputs.NEW }}"
echo "BUILD=${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.NEW || github.event.inputs.build == 'true' }}"
echo "GITHUB_REF=${{ github.ref }}"
comment-pr:
needs: [check_pr, get_info]
if: ${{ needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true' || github.event.inputs.build == 'true' }}
runs-on: "ubuntu-latest"
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ needs.get_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: ${{ needs.get_info.outputs.PR_NUMBER }}
edit-mode: replace
body: |
---
:rocket: Deploying PR ${{ needs.get_info.outputs.PR_NUMBER }} ...
---
reactions: eyes
reactions-edit-mode: replace
build:
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
# always run the build job if a pull_request event triggered the workflow
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.skip_build == 'false') ||
(github.event_name == 'pull_request' && needs.get_info.result == 'success' && needs.get_info.outputs.NEW == 'false')
# Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag
if: needs.get_info.outputs.BUILD == 'true'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
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
with:
ref: ${{ env.PR_BRANCH }}
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
@@ -197,9 +225,8 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Linux amd64 Docker image
if: needs.get_info.outputs.BUILD == 'true'
run: |
set -euxo pipefail
set -euo pipefail
go mod download
make gen/mark-fresh
export DOCKER_IMAGE_NO_PREREQUISITES=true
@@ -217,35 +244,42 @@ jobs:
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') &&
(github.event_name == 'workflow_dispatch' || needs.get_info.outputs.NEW == 'false')
always() && (needs.build.result == 'success' || needs.build.result == 'skipped') &&
(needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true')
runs-on: "ubuntu-latest"
env:
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 }}"
PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
steps:
- name: Set up kubeconfig
run: |
set -euxo pipefail
set -euo 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)
set -euo pipefail
foundTag=$(
gh api /orgs/coder/packages/container/coder-preview/versions |
jq -r --arg tag "pr${{ env.PR_NUMBER }}" '.[] |
select(.metadata.container.tags == [$tag]) |
.metadata.container.tags[0]'
)
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 and try again"
exit 1
else
echo "Image found"
echo "$foundTag tag found in ghcr.io/coder/coder-preview"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add DNS record to Cloudflare
if: needs.get_info.outputs.NEW == 'true'
@@ -253,43 +287,27 @@ jobs:
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 }}
--data '{"type":"CNAME","name":"*.${{ env.PR_HOSTNAME }}","content":"${{ env.PR_HOSTNAME }}","ttl":1,"proxied":false}'
- name: Create PR namespace
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
set -euxo pipefail
set -euo 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: Checkout
uses: actions/checkout@v3
- name: Check and Create Certificate
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
# Using kubectl to check if a Certificate resource already exists
# we are doing this to avoid letsenrypt rate limits
if ! kubectl get certificate pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs > /dev/null 2>&1; then
echo "Certificate doesn't exist. Creating a new one."
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pr${{ env.PR_NUMBER }}-tls
namespace: pr-deployment-certs
spec:
secretName: pr${{ env.PR_NUMBER }}-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
dnsNames:
- "${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
EOF
envsubst < ./.github/pr-deployments/certificate.yaml | kubectl apply -f -
else
echo "Certificate exists. Skipping certificate creation."
fi
@@ -306,7 +324,7 @@ jobs:
)
- name: Set up PostgreSQL database
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install coder-db bitnami/postgresql \
@@ -318,82 +336,45 @@ 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: Create values.yaml
if: github.event_name == 'workflow_dispatch'
- name: Create a service account, role, and rolebinding for the PR namespace
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
cat <<EOF > pr-deploy-values.yaml
coder:
image:
repo: ${{ env.REPO }}
tag: pr${{ env.PR_NUMBER }}
pullPolicy: Always
service:
type: ClusterIP
ingress:
enable: true
className: traefik
host: ${{ env.PR_DEPLOYMENT_ACCESS_URL }}
wildcardHost: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
tls:
enable: true
secretName: pr${{ env.PR_NUMBER }}-tls
wildcardSecretName: pr${{ env.PR_NUMBER }}-tls
env:
- name: "CODER_ACCESS_URL"
value: "https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- name: "CODER_WILDCARD_ACCESS_URL"
value: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- name: "CODER_EXPERIMENTS"
value: "${{ github.event.inputs.experiments }}"
- 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
set -euo pipefail
# Create service account, role, rolebinding
envsubst < ./.github/pr-deployments/rbac.yaml | kubectl apply -f -
- name: Create values.yaml
env:
EXPERIMENTS: ${{ github.event.inputs.experiments }}
PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID }}
PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET }}
run: |
set -euo pipefail
envsubst < ./.github/pr-deployments/values.yaml > ./pr-deploy-values.yaml
- name: Install/Upgrade Helm chart
run: |
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
set -euo pipefail
helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm/coder \
--namespace "pr${{ env.PR_NUMBER }}" \
--values ./pr-deploy-values.yaml \
--force
- name: Install coder-logstream-kube
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == '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 \
--namespace "pr${{ env.PR_NUMBER }}" \
--set url="https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
--set url="https://${{ env.PR_HOSTNAME }}"
- name: Get Coder binary
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
set -euxo pipefail
set -euo pipefail
DEST="${HOME}/coder"
URL="https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64"
URL="https://${{ env.PR_HOSTNAME }}/bin/coder-linux-amd64"
mkdir -p "$(dirname ${DEST})"
@@ -414,10 +395,10 @@ jobs:
mv "${DEST}" /usr/local/bin/coder
- name: Create first user, template and workspace
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
id: setup_deployment
run: |
set -euxo pipefail
set -euo pipefail
# Create first user
@@ -429,28 +410,24 @@ jobs:
echo "password=$password" >> $GITHUB_OUTPUT
coder login \
--first-user-username test \
--first-user-username coder \
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
--first-user-password $password \
--first-user-trial \
--use-token-as-session \
https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}
https://${{ env.PR_HOSTNAME }}
# Create template
coder templates init --id kubernetes && cd ./kubernetes/ && coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }}
cd ./.github/pr-deployments/template
terraform init
coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
# Create workspace
cat <<EOF > workspace.yaml
cpu: "2"
memory: "4"
home_disk_size: "2"
EOF
coder create --template="kubernetes" test --rich-parameter-file ./workspace.yaml -y
coder stop test -y
coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y
coder stop kube -y
- name: Send Slack notification
if: needs.get_info.outputs.NEW == 'true'
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
curl -s -o /dev/null -X POST -H 'Content-type: application/json' \
-d \
@@ -458,7 +435,7 @@ jobs:
"pr_number": "'"${{ env.PR_NUMBER }}"'",
"pr_url": "'"${{ env.PR_URL }}"'",
"pr_title": "'"${{ env.PR_TITLE }}"'",
"pr_access_url": "'"https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'",
"pr_access_url": "'"https://${{ env.PR_HOSTNAME }}"'",
"pr_username": "'"test"'",
"pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'",
"pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'",