GitHub Action

The official deploys-app/deploys-action lets a GitHub Actions workflow roll out a new revision once your image is built and pushed. It’s the deploys CLI wrapped in a step.

TipWant Deploys.app to build the image too — keyless, with preview deployments on every PR? See Deploy from GitHub. This page covers the deploy-only action for teams that build and push images elsewhere.

A complete workflow#

The shape most teams use: build the image, push it to a registry, deploy by digest.

.github/workflows/deploy.ymlyaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

      - name: Deploy
        uses: deploys-app/deploys-action@v1
        with:
          project: acme
          location: gke.cluster-rcf2
          name: web
          image: ghcr.io/${{ github.repository }}@${{ steps.docker_build.outputs.digest }}
          port: 8080
          type: WebService
          minReplicas: 2
          maxReplicas: 6
        env:
          DEPLOYS_AUTH_USER: ${{ secrets.DEPLOYS_AUTH_USER }}
          DEPLOYS_AUTH_PASS: ${{ secrets.DEPLOYS_AUTH_PASS }}

Deploying by digest (@sha256:…) instead of by tag makes the rollout reproducible — the same SHA always means the same bits.

Inputs#

The action calls deployment.deploy with these fields. Fields you don’t pass are preserved from the deployment’s current revision when possible — so a CI deploy can pass just image and leave the rest as configured.

Target and artifact:

InputRequiredDescription
projectyesProject ID
locationyesLocation ID (e.g. gke.cluster-rcf2)
nameyesDeployment name
imagefor containersContainer image with tag or digest
typenoDeployment typeWebService, Worker, CronJob, InternalTCPService, Static
sitefor StaticStatic release reference (site://…@<sha>); leave image empty
siteManifestDigestnoManifest digest for the static release

Networking and scaling:

InputDescription
portPort the container listens on (WebService/InternalTCPService)
protocolWebService protocol — http, https, or h2c
internalRun a WebService as internal-only (true/false)
minReplicas / maxReplicasAutoscale bounds (0–20)
scheduleCron schedule for a CronJob (5 fields)
ttlAuto-delete TTL (7d, 12h, seconds…); 0 clears it

Environment and container:

InputDescription
envEnv vars (KEY=VALUE per line) — replaces the whole set
addEnv / removeEnvAdd / remove individual env vars without replacing the set
envGroupsEnv groups to attach (replaces)
addEnvGroups / removeEnvGroupsAdd / remove individual env groups
command / argsOverride the image entrypoint / arguments (one token per line)
workloadIdentityWorkload identity to bind
pullSecretPull secret for a private registry

Storage and resources:

InputDescription
diskName / diskMountPath / diskSubPathAttach a persistent disk
mountDataFiles to mount, as a JSON map of path → contents
cpuRequest / memoryRequest / cpuLimit / memoryLimitResource requests and limits

Access control:

InputDescription
accessRequireGoogleLoginGate the deployment behind Google sign-in (true/false)
accessAllowedEmailsAllowed emails, one per line or comma-separated
accessAllowedDomainsAllowed email domains, one per line or comma-separated

Auth and escape hatch:

InputDescription
token / authUser / authPassCredentials (default to the DEPLOYS_* env vars below)
apiEndpointOverride the API endpoint
extraArgsA JSON object merged into the deployment.deploy request — use for anything not yet a dedicated input

A Cloud SQL Proxy sidecar can be attached with cloudSqlProxyInstance, cloudSqlProxyPort, and cloudSqlProxyCredentials.

Outputs: url (the deployed URL) and deployment (the deployment name).

Authentication#

Two equivalent ways to authenticate; pick one:

  • Service account (recommended for CI)DEPLOYS_AUTH_USER + DEPLOYS_AUTH_PASS.
  • Bearer tokenDEPLOYS_TOKEN. Useful for short-lived scripts; rotate often.

Generate the service-account key from Service accounts → Create key, then add both values to the repo’s secrets (Settings → Secrets and variables → Actions).

A minimal example#

If you already have an image somewhere and just want a deploy step:

- uses: deploys-app/deploys-action@v1
  with:
    project: acme
    location: gke.cluster-rcf2
    name: web
    image: registry.deploys.app/acme/web:v2.4.1
  env:
    DEPLOYS_AUTH_USER: ${{ secrets.DEPLOYS_AUTH_USER }}
    DEPLOYS_AUTH_PASS: ${{ secrets.DEPLOYS_AUTH_PASS }}

The action will run deployment.deploy and return success only once the API accepts the call — the actual rollout continues in the background. To gate the workflow on the new revision being healthy, follow up with a small script that polls deployment.get until the revision and status look right.

Permissions the CI service account needs#

For deploy-only CI, the minimum useful role is:

deploys role create \
  --project acme --role deployer --name "Deployer" \
  --permissions "project.get,deployment.list,deployment.get,deployment.deploy,registry.list"

Then grant it to the CI service account:

deploys role grant --project acme --role deployer \
  --email ci@acme.deploys.app

registry.list is only needed if you also push images to registry.deploys.app from the same step.

Troubleshooting#

  • 403 forbidden — the service account is missing deployment.deploy. Run deploys me authorized --project acme --permissions deployment.deploy in a quick debug step to confirm.
  • Stuck at “Pending” — the API accepted the call but the new revision isn’t rolling out. Open the deployment, switch to Events, and look for ImagePullBackOff (wrong image / missing pull secret) or readiness check failures.
  • Image not found — the platform pulled the previous tag and the current pull metadata is stale. Deploy by digest (@sha256:…) to dodge it entirely.