mirror of
https://github.com/coder/coder.git
synced 2026-06-06 22:48:19 +00:00
395 lines
16 KiB
YAML
395 lines
16 KiB
YAML
# 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"
|
|
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: "*"
|
|
|
|
env:
|
|
REPO: ghcr.io/coder/coder-preview
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
pull-requests: write
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.ref }}
|
|
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'
|
|
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: "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 and try again"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Add DNS record to Cloudflare
|
|
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}'
|
|
)
|
|
|
|
- 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: Check and Create Certificate
|
|
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
|
|
else
|
|
echo "Certificate exists. Skipping certificate creation."
|
|
echo "Copy certificate from pr-deployment-certs to pr${{ env.PR_NUMBER }} namespace"
|
|
(
|
|
kubectl get secret pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs -o json |
|
|
jq 'del(.metadata.namespace,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid,.metadata.managedFields)' |
|
|
kubectl -n pr${{ env.PR_NUMBER }} apply -f -
|
|
)
|
|
fi
|
|
|
|
- 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: Get experiments
|
|
id: get_experiments
|
|
run: |
|
|
set -euxo pipefail
|
|
if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
|
|
experiments=${{ github.event.inputs.experiments }}
|
|
else
|
|
experiments=$(echo "${{ github.event.comment.body }}" | grep -oP '(?<=--experiments )[^ ]+')
|
|
fi
|
|
echo "experiments=$experiments" >> $GITHUB_OUTPUT
|
|
|
|
- name: Create values.yaml
|
|
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: "*,${{ steps.get_experiments.outputs.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
|
|
|
|
- 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="https://${{ 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
|
|
mv "${DEST}" /usr/local/bin/coder
|
|
|
|
- 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
|
|
|
|
coder login \
|
|
--first-user-username test \
|
|
--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 }}
|
|
|
|
# Create template
|
|
coder templates init --id kubernetes && cd ./kubernetes/ && coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }}
|
|
|
|
# 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
|
|
|
|
- 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": "'"https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'",
|
|
"pr_username": "'"test"'",
|
|
"pr_email": "'"pr${{ 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](https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}).
|
|
:warning: This deployment will be deleted when the PR is closed.
|
|
reactions: rocket
|