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#
- 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.appdeployment.deleteis what lets the action clean up preview deployments when a PR closes;registry.pushlets it push the built image to your project’s registry. - 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:
- 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.
- 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 linkdoes the same once the App is installed.
Deploy trigger#
Each link has a Deploy trigger that decides which workflow runs deploy:
| Trigger | Push to the production branch | Pull-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.
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: 3000on: 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-42for 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:
| Input | Required | Description |
|---|---|---|
project | yes | Project ID |
location | yes | Location ID (e.g. gke.cluster-rcf2) |
name | yes | Deployment name |
mode | no | dockerfile (default) or static |
workingDirectory | no | Root of the app to build, for monorepos (default .) |
previewTtl | no | How long an idle preview lives (default 7d) |
apiEndpoint | no | Override the API endpoint |
registry | no | Override the registry to push to |
Container build (mode: dockerfile):
| Input | Required | Description |
|---|---|---|
context | no | Build context directory, relative to workingDirectory (default .) |
dockerfile | no | Path to the Dockerfile |
buildArgs | no | Docker build args, one KEY=VALUE per line |
port | no | Port the container listens on (default 8080) |
type | no | Deployment type — WebService (default), Worker, InternalTCPService |
protocol | no | WebService protocol — http (default), https, or h2c |
env | no | Environment variables, one KEY=VALUE per line |
envGroups | no | Env groups to attach, one per line or comma-separated |
pullSecret | no | Pull secret name for a private base image |
Static build (mode: static) — see Static sites:
| Input | Default | Description |
|---|---|---|
framework | auto | auto, hugo, node, or none |
buildCommand | per framework | Build command (required when framework: none) |
outputDir | public | Folder of built files to publish |
nodeVersion | .nvmrc else 20 | Node version for the node framework |
spa | false | Serve index.html for unknown routes |
notFound | 404.html | Custom 404 document when spa: false |
baseUrl | the deploy URL | Build-time base URL |
Access — see Deployment access (applies to WebService and Static):
| Input | Default | Description |
|---|---|---|
requireGoogleLogin | false | Gate the deployment behind Google sign-in |
allowedEmails | — | Allowed emails, one per line or comma-separated |
allowedDomains | — | Allowed 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:
| Output | Description |
|---|---|
deployment | Deployment name (including the -pr-<n> suffix for previews) |
environment | production, or pr-<n> for previews |
artifact | The deployed artifact — the pushed image digest, or the static release SHA |
url | URL 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 havepermissions: 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 basenameshort enough to leave room.