# GitHub (Preview) Deployments with Google Cloud Platform

Create and managed GitHub Deployments and Environments with Google Cloud Build and Cloud Run

- **Author:** Patrick Heneise
- **Published:** 2023-05-25
- **Updated:** 2023-05-25
- **Tags:** google cloud platform, google cloud build, github, github actions, automation

---

import Image from '../../src/components/Image.astro'
import Figure from '../../src/components/Figure.astro'

## Motivation and Introduction

Build previews aren't anything new or innovative. Heroku had them for years. If
you're building on Google Cloud Platform however, things are a little more
difficult. Google Cloud Build can be used to deploy to Cloud Functions, Cloud
Run, Kubernetes, App Engine etc. etc. This is what makes previews slightly
complicated, as Cloud Build doesn't know where you're deploying your application
or service to. They have a GitHub App to connect the repositories and show the
build status in GitHub, but that's it. No previews, comments or any of the
"normal" stuff we're used to from products like Heroku, Vercel or Fly.

There's an "easy" way to post a comment on a GitHub Pull Request from Cloud
Build with a simple HTTP request. But why easy when you can use the
[GitHub Deployments API](https://docs.github.com/en/rest/deployments/deployments?apiVersion=2022-11-28).

With this API, you can create Environments, have different variables per
environment and show build & deployment status directly on a Pull Request. And
once the deployment is done on your CD platform, you can add the URL to the
deployment status. This is what we're going to do in this article.

<Figure
  src="/images/articles/2023/gh-deployment-progress.png"
  alt="screenshot showing the github deployment status in progress"
  width={1776}
  height={502}
  title="GitHub Deployment Status"
/>

Again, there's another way of doing this by using the Deployments API directly
with HTTP requests. This can be tricky sometimes though and you'll need to
define a Personal Access Token to authenticate with GitHub. Instead, we decided
to put together a little Node.js service that uses GitHub Apps to generate an
app token and interact with the Deployments API. This allows a little finer
control over the deployment process.

### Disclaimer

This article is exclusively written for Google Cloud Platform and GitHub. Basic
understanding of Google Cloud Platform, Google Cloud Build and IAM permissions
is assumed. You should have a Google Cloud Platform project set up.

## GitHub Deployer (Node.js & Docker)

GitHub Deployer is a tiny Node.js service that authorized with a GitHub App and
interacts with the GitHub Deployments API. Commands are predefined and some
information needs to be built in to the Docker image, as otherwise the runtime
configuration in Cloud Build would get too complex.

The Docker image is not ready built; you'll need to build it yourself and push
it to your registry. The best place is Google Artifact Registry where the images
for your application are stored as well (Container Registry is deprecated):

1. If not done already, create an Artifact Registry repository
2. Create a GitHub App and install it on your organization
   - [Create a GitHub App](https://docs.github.com/en/developers/apps/creating-a-github-app)
   - Required permissions: 'pull_requests', 'deployments', 'metadata'
3. Copy the App ID (from the App Settings screen), and the App Installation ID
   (after clicking "Configure" you'll find the installation ID in the URL. No
   idea where else that can be found)
4. Create & download a private key and convert it to base64
   (`base64 -i your.private-key.pem`)

<Image
  src="/images/articles/2023/github-app-settings.png"
  alt="screenshot showing the GitHub App with 'Configure' button"
  width={1890}
  height={164}
/>

### Build the Docker image

[Source](https://github.com/zentered/gh-deployer/tree/main/docker)

Cloud Build runs on `amd64`, so we need `buildx` to build the image for the x86
platform:

```bash
docker buildx build --platform linux/amd64 . -t us-central1-docker.pkg.dev/[PROJECT]/docker/gh-deployer \
  --build-arg GH_APP_ID=[GITHUB_APP_ID] \
  --build-arg GH_APP_PRIVATE_KEY=[GITHUB_PRIVATE_KEY_BASE_64] \
  --build-arg GH_APP_INSTALLATION_ID=[GITHUB_INSTALLATION_ID] \
  --build-arg GH_OWNER=[GITHUB_ORG]
```

Push the image to your registry:

```bash
docker push us-central1-docker.pkg.dev/[PROJECT]/docker/gh-deployer
```

### Run the Docker image locally

```bash
docker run -it -e REPO_NAME=test -e REF=main -e ENVIRONMENT=test us-central1-docker.pkg.dev/[PROJECT]/docker/gh-deployer:latest create
```

Required environment variables:

- `REPO_NAME`: **required** the name of the repository (e.g. `gh-deployer`)
- `REF`: **required** the branch, tag or SHA to deploy (e.g. `main`)
- `ENVIRONMENT`: **required** the environment to deploy to (e.g. `preview`)
- `TRANSIENT_ENVIRONMENT`: **optional/false** if set to `true`, the deployment
  will be deleted after a certain time
- `DESCRIPTION`: **optional** a description of the deployment

The following commands are available as first argument:

- `create` - create a new deployment
- `pending` - the build is pending
- `in_progress` - the build is in progress
- `queued` - the build is queued
- `success` - the deployment was successful
- `error` - something went wrong
- `failure` - the deployment failed

With Cloud Build the options are currently a bit limited. There's no `pending`
state as Cloud Build needs to start in order to create the initial deployment.
`queued` doesn't make much sense either, so in our own setup, we're using the
following:

1. `gh-deployer/create` to create the transient deployment for a commit
2. `pgh-deployer/ending` directly after create
3. Run kaniko build to build the Docker image for deployment
4. `gh-deployer/in_progress` after the build is done and before the image is
   deployed
5. `gh-deployer/success` after the image is deployed

Again, with Cloud Build we don't know where the deployment is going to be, so
there are two ways to pass the deployment URL to the `success` step:

1. In a build step, write the URL into `/workspace/deployer_environment_url`
2. Pass a second argument to the Docker image after `success`

We use Cloud Run for our deployments, so here's the build step to retrieve the
deployment URL for a given Pull Request number:

```yaml
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
    env:
      - PR_NUMBER=$_PR_NUMBER
    script: >-
      gcloud run services list --project [project] --filter preview-$PR_NUMBER
      --format "value(status.address.url)" > /workspace/deployer_environment_url
```

## Cloud Build Configuration / Terraform

### cloudbuild.yaml

[cloudbuild.yaml](https://github.com/zentered/gh-deployer/tree/main/cloudbuild)

Here's a complete exmaple using the `cloudbuild.yaml` configuration file. We use
Kaniko to build and Cloud Run as deplyoment target.

If you work with Terraform, there's a Terraform file afailable as well:
[preview.tf](https://github.com/zentered/gh-deployer/tree/main/terraform)

```yaml
steps:
  - name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
    env:
      - REPO_NAME=$REPO_NAME
      - TRANSIENT_ENVIRONMENT=true
      - DESCRIPTION=Deploying to Google Cloud Run
      - ENVIRONMENT=preview
      - REF=$REF_NAME
    args:
      - create
  - name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
    env:
      - REPO_NAME=$REPO_NAME
      - ENVIRONMENT=preview
      - REF=$REF_NAME
    args:
      - pending
  - name: 'gcr.io/kaniko-project/executor:latest'
    args:
      - '--destination=us-central1-docker.pkg.dev/[project]/docker/[name]:$SHORT_SHA'
      - '--use-new-run'
  - name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
    env:
      - REPO_NAME=$REPO_NAME
      - ENVIRONMENT=preview
      - REF=$REF_NAME
    args:
      - in_progress
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    args:
      - run
      - deploy
      - preview-$_PR_NUMBER
      - '--image=us-central1-docker.pkg.dev/[project]/docker/[name]:$SHORT_SHA'
      - '--region=us-central1'
      - '--memory=512Mi'
      - '--platform=managed'
      - '--allow-unauthenticated'
    entrypoint: gcloud
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    env:
      - PR_NUMBER=$_PR_NUMBER
    script: >-
      gcloud run services list --project [project] --filter preview-$PR_NUMBER
      --format "value(status.address.url)" > /workspace/deployer_environment_url
  - name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
    env:
      - REPO_NAME=$REPO_NAME
      - ENVIRONMENT=preview
      - REF=$REF_NAME
    args:
      - success
```

## GitHub Actions

By using the Deployments API, you can trigger GitHub Actions on
`deployment_status`. This makes it easy to run quality assurance checks,
end-to-end tests etc. on each preview build. Here's an example for running
[Playwright](https://playwright.dev/) on each preview using the
`environment_url` that is passed in for the `success` state:

```yaml
name: preview-qa

on: deployment_status

jobs:
  playwright-qa:
    if: ${{ github.event.deployment_status.state == 'success' }}
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - name: Install Playwright Browsers
        run: npm exec playwright install --with-deps -y
      - name: Run Playwright tests
        run: pnpm exec playwright test
        env:
          BASE_URL: ${{github.event.deployment_status.environment_url}}
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
```

## Optimizations

- the basic image `gcr.io/google.com/cloudsdktool/cloud-sdk` is relatively
  large. You can
  [build your own small gcloud image with Cloud Run components](https://cloud.google.com/sdk/docs/downloads-docker#alpine-based_images)
  to speed up the builds.

## Conclusion

<Figure
  src="/images/articles/2023/gh-deployment-done.png"
  alt="screenshot showing the github deployment status done"
  width={1776}
  height={502}
  title="GitHub Deployment Status Done"
/>

Comments on GitHub Pull Requests are easy at first, but when you have multiple
deployments per Pull Request, failing builds etc. they don't offer a lot of
flexibility and are hard to maintain / update. The GitHub Deployments API offers
an elegant way to create and manage deployments from any third party CI/CD
system. With the `GitHub Deployer` we tried to streamline the interaction with
the deployments API and take care of some side effects or pitfalls that are
impossible to solve with just the HTTP API.

Another benefit of using the GitHub Deployments API is that you can use the
`deployment_status` trigger for GitHub Actions and get the `environment_url`
directly with the event payload.

You can find a little more detailled installation / build instructions and all
the code on the
[GitHub repository](https://github.com/zentered/gh-deployer/tree/main#github-deployer)

**If you have any questions or comments**, please
[reach out on Bluesky](https://bsky.app/profile/zentered.co) or
[join the discussion on GitHub](https://github.com/zentered/feedback/discussions/).
