Deploy from GitHub

The deploys-app/build-deploy-action builds your repository on GitHub’s runners and deploys the result in one step. There are no secrets to store: authentication is keyless via GitHub Actions OIDC. Pushing to the default branch deploys to production; every pull request gets a temporary preview deployment with its own URL.

What you get#

  • Production deploys on push — every push to the default branch builds the repo and rolls out a new revision.
  • Preview deployments on pull requests — each PR gets its own deployment (<name>-pr-<number>) with a public URL, a sticky comment on the PR, and a GitHub deployment status. Previews are deleted automatically when the PR closes.
  • No secrets in the repo — the workflow authenticates with its GitHub OIDC token. There is no service-account key to create, store, or rotate.

If you already build and push images elsewhere and only need a deploy step, the simpler deploys-action (deploy-only, service-account secrets) still works fine.

One-time setup#

  1. Create a service account

    The action deploys as a service account in your project. It’s just an identity here — you never create a key for it.

    deploys serviceaccount create \
      --project acme \
      --id ci \
      --name "GitHub Deployer"
    

    Give it a role with the permissions the action uses:

    deploys role create \
      --project acme --role github-deployer --name "GitHub Deployer" \
      --permissions "deployment.deploy,deployment.get,deployment.delete,registry.push"
    
    deploys role grant \
      --project acme --role github-deployer \
      --email ci@acme.serviceaccount.deploys.app
    

    deployment.delete is what lets the action clean up preview deployments when a PR closes; registry.push lets it push the built image to your project’s registry.

  2. Link the repository to the project

    The link is what ties a GitHub repository to a project and a service account — it’s the authorization the token exchange checks against. Open the console’s GitHub page and click Link repository. The flow has two steps:

    1. Install the GitHub App — click the install button, pick the repository (or the whole organization) on GitHub, and GitHub redirects you back to the console automatically. The console remembers the installation, so you won’t need to reinstall next time.
    2. Pick the repository — choose it from a searchable dropdown of the repositories visible to the installed App (just created the repo? hit Refresh to re-fetch the list), choose the service account you created above, pick a Deploy trigger and Production branch (see below), and click Link.

    The App is also how the action posts the preview comment and deployment statuses on pull requests. Prefer the terminal? deploys github link does the same once the App is installed.

Deploy trigger#

Each link has a Deploy trigger that decides which workflow runs deploy:

TriggerPush to the production branchPull-request previews
Branch + PR previews (default)✅ deploys✅ preview per PR
Branch only✅ deploys— no preview
PR previews only— never deploys✅ preview per PR

The platform enforces the trigger at the token exchange, keyed off the verified OIDC token — not just the generated workflow:

  • A push / tag run only gets a token when the trigger deploys branches (Branch + PR previews or Branch only); otherwise it is rejected.
  • A pull-request run only gets a token when the trigger posts previews (Branch + PR previews or PR previews only); otherwise it is rejected.

Production branch#

For the two branch-deploying triggers, the Production branch setting decides which branch deploys. It defaults to main; leave it empty to allow a push to any branch. Pushes to any other ref (including tags) are refused at the token exchange. PR previews only links never deploy a branch, so this setting does not apply.

To change the trigger, production branch, or service account later, open the console’s GitHub page, click the edit (pencil) button on the repository, adjust the fields, and Save — no need to unlink and relink. Changing the trigger or branch only updates the link; recreate the workflow file afterwards (the editor nudges you, and links straight to the generator) so its on: block matches the new setting.

Generated workflow#

The console’s generator writes the matching on: block, so the workflow only runs for the events that can actually deploy:

on:                  # Branch + PR previews
  push:
    branches: [main]
  pull_request:
on:                  # Branch only
  push:
    branches: [main]
on:                  # PR previews only
  pull_request:

Add the workflow#

One file, one step. The id-token: write permission is the credential — the workflow won’t authenticate without it.

.github/workflows/deploy.ymlyaml
name: Deploy
on:
  push:
    branches: [main]
  pull_request:

permissions:
  id-token: write   # required — this is the credential
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: deploys-app/build-deploy-action@v1
      with:
        project: my-project
        location: gke.cluster-rcf2
        name: web
        port: 3000
TipAfter linking, the console’s GitHub page can generate this file for you, pre-filled with the project, location, and deployment name — use the Copy button, or Create on GitHub to open GitHub with the workflow file pre-filled. If you later edit the link’s trigger or production branch, recreate the file the same way: the generated on: block follows the link, and you commit the change yourself.

The example is the Branch + PR previews shape with main as the branch; the generated workflow’s on: block matches your link’s deploy trigger and production branch.

Push to main and the action builds the image from the repo’s Dockerfile, pushes it to the project registry, and deploys it as web.

What happens on a pull request#

When the workflow runs for a pull request, the action deploys a preview instead of touching production:

  • The deployment is named <name>-pr-<number>web-pr-42 for PR #42 — and gets its own URL.
  • The action posts a sticky comment on the PR with the preview URL, the image, and the commit it was built from. Repeated pushes update the same comment, not a new one per push.
  • A GitHub deployment status is created, so the PR shows a View deployment button.
  • Each push redeploys the same preview in place and re-rolls its TTL.

The PR also reflects post-deploy failures, not just the CI-time outcome. A preview that deploys green but then crash-loops, gets OOM-killed, or whose apply fails afterwards is detected by the platform (the same deployment.health signal): the sticky comment updates to a failure state with the reason, and the GitHub deployment status flips to failure — so a preview that started green but is actually broken stops looking green. (Production runs get the deployment-status update but no PR comment.)

Previews are temporary by design. They’re deleted automatically when the PR is closed or merged; the TTL (default 7 days since the last push, configurable with previewTtl) is the backstop for previews that never get cleaned up.

Static sites#

Set mode: static and the action builds your site (Hugo, a Node SPA, or any build command) and publishes it as a Static deployment served from the edge — no Dockerfile, no container. The setup above (service account, App, link) is identical. See Static sites for the build inputs and examples.

Inputs and outputs#

Common — apply to every build:

InputRequiredDescription
projectyesProject ID
locationyesLocation ID (e.g. gke.cluster-rcf2)
nameyesDeployment name
modenodockerfile (default) or static
workingDirectorynoRoot of the app to build, for monorepos (default .)
previewTtlnoHow long an idle preview lives (default 7d)
apiEndpointnoOverride the API endpoint
registrynoOverride the registry to push to

Container build (mode: dockerfile):

InputRequiredDescription
contextnoBuild context directory, relative to workingDirectory (default .)
dockerfilenoPath to the Dockerfile
buildArgsnoDocker build args, one KEY=VALUE per line
portnoPort the container listens on (default 8080)
typenoDeployment typeWebService (default), Worker, InternalTCPService
protocolnoWebService protocol — http (default), https, or h2c
envnoEnvironment variables, one KEY=VALUE per line
envGroupsnoEnv groups to attach, one per line or comma-separated
pullSecretnoPull secret name for a private base image

Static build (mode: static) — see Static sites:

InputDefaultDescription
frameworkautoauto, hugo, node, or none
buildCommandper frameworkBuild command (required when framework: none)
outputDirpublicFolder of built files to publish
nodeVersion.nvmrc else 20Node version for the node framework
spafalseServe index.html for unknown routes
notFound404.htmlCustom 404 document when spa: false
baseUrlthe deploy URLBuild-time base URL

Access — see Deployment access (applies to WebService and Static):

InputDefaultDescription
requireGoogleLoginfalseGate the deployment behind Google sign-in
allowedEmailsAllowed emails, one per line or comma-separated
allowedDomainsAllowed email domains, one per line or comma-separated

The action exposes outputs you can use in later steps — handy for smoke tests against the deployed URL:

OutputDescription
deploymentDeployment name (including the -pr-<n> suffix for previews)
environmentproduction, or pr-<n> for previews
artifactThe deployed artifact — the pushed image digest, or the static release SHA
urlURL of the deployment that was created or updated

image is still emitted as a deprecated alias of artifact; prefer artifact.

How authentication works#

The workflow requests a GitHub OIDC token with audience https://deploys.app and exchanges it at the Deploys.app API for a short-lived (1 hour) token scoped to the service account the repository is linked to. The same token authenticates the registry push. The build runs entirely on GitHub’s runners — only the built image reaches Deploys.app.

Limitations and troubleshooting#

  • No previews from forks. GitHub does not issue OIDC tokens to workflows triggered by pull requests from forks, so fork-opened PRs can’t authenticate. This is a GitHub restriction, not a setting.
  • missing OIDC token support — the workflow doesn’t have permissions: id-token: write. Add it at the workflow (or job) level.
  • token exchange failed … is this repository linked — the repository isn’t linked to a project. Create the link (step 3 above).
  • github app is not installed on the repository — install the Deploys.app GitHub App on the repo first, then link it.
  • Name too long — deployment names cap at 63 characters including the -pr-<n> suffix on previews. Keep the base name short enough to leave room.