Deploy a Go app from GitHub Actions

Push to main, your Go app deploys. One workflow file, one secret, and a key scoped to deploying a single app, so a leak stays contained to that app.

The secret is the hard part

Running aiploy deploy from CI is not difficult. It is one command. The difficult part is the thing you paste into your repository settings so that the command is allowed to run.

Until now, the only credential there was to paste was your account API key. That key can create and delete apps, read every environment variable on every app, and restore a backup over a live database. Put it in a CI secret and all of that is now readable by every step of every workflow in the repo: the third-party action you pinned to a tag, the build dependency whose maintainer's laptop got owned, the collaborator who edits a workflow file to "debug something".

A deploy pipeline needs to deploy one app. It does not need the keys to all of them.

Deploy keys

So I added a second kind of key. A deploy key belongs to one app and can do one thing: deploy that app. It cannot list your apps, read or write env vars, stream logs, touch backups, or deploy any other app. Try it on a different app and the server answers App not found, exactly as it would for a token that doesn't exist, so a leaked key can't be used to go looking for your other apps either.

Be clear about what that scope buys you, though. A deploy key can't ask the API for your environment variables, but it can ship code, and the code it ships runs with that app's environment and its databases. Anyone holding the key can deploy a handler that prints os.Environ(). What the key limits is the blast radius: one app, not your account. Treat it as a production secret, which is what the environment: line in the workflow below is for.

You create one from the app's card on the dashboard, under Deploy keys. Give it a name, and it shows you the key once. It starts with aiploy_dk_, which makes it easy to spot in a log you really didn't want it in. The dashboard only keeps a hash, so there is nothing to show you a second time. Lose it and you make another.

Creating one also sends you an email. If a key appears that you didn't make, the email has a link to revoke it, and you should change your password while you're there.

The workflow

Save the key as a repository secret named AIPLOY_KEY. Save the app's token (the dashboard shows it next to the key) as a repository variable named AIPLOY_APP. The token isn't a secret: on its own it grants nothing. If your .aiploy file is committed, skip the variable entirely.

Then add .github/workflows/deploy.yml:

name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsSL https://www.aiploy.dev/install.sh | sh
      - run: aiploy deploy
        env:
          AIPLOY_KEY: ${{ secrets.AIPLOY_KEY }}
          AIPLOY_APP: ${{ vars.AIPLOY_APP }}

The CLI reads AIPLOY_KEY and AIPLOY_APP from the environment, uses them for that run, and never writes them to disk or prints them. aiploy status will tell you where each one came from without telling you what it is.

The job's exit code is the deploy's result. A compile error fails the step and prints the last 40 lines of the build. If the build queue is full, the CLI exits 75 rather than 1, so a retry step can tell "come back in a minute" apart from "your code doesn't build".

Three lines that matter

environment: production. Move the secret out of the repository and into a GitHub environment, then restrict that environment to the main branch, or require a reviewer. Now a workflow on some other branch can't read the key at all, however it was written.

on: push, never pull_request_target. pull_request_target runs with your secrets available, including on pull requests from forks. The moment a workflow like that checks out the pull request's code, a stranger's go build is running next to your deploy key. Deploy from main, after review, and not from anything a stranger can open.

Install the CLI on every run. Don't cache the binary. The server only accepts the current CLI version, so a cached binary works until the next release and then fails every deploy with a polite request to run aiploy update. The installer takes a few seconds; the cache would save you those seconds and cost you a confusing Tuesday.

Rotating

An app can hold five deploy keys, which is more than one for a reason. To rotate without a failed deploy in between: create a new key, update the secret, run the workflow once, then revoke the old key. The dashboard shows when each key was last used and from which IP, so you can see the old one go quiet before you delete it.

If you run aiploy deploy in CI with an account key, the CLI still works. It also prints a warning to stderr every run, asking you to switch. I'd rather nag than break a pipeline.

What it won't do

No keyless login. GitHub can mint a short-lived identity token for a workflow, and exchanging that for a deploy credential would mean no stored secret at all. That's not built. Today a deploy key is a long-lived secret, and it lasts until you revoke it.

No expiry. Keys don't time out. Rotation is on you, which is why the last-used column exists.

No preview deploys. One app per key, one app per deploy. Each pull request doesn't get its own URL.

Still Go only, still no team features, still not free. Seven days on trial with a card, then $2/mo for one app.

What you didn't do

You didn't write a Dockerfile for CI, or a registry login step, or an SSH action with a private key to a box you'd rather not think about. You added one file, one secret and one variable. The key that lives in your CI reaches one app, only by deploying it, which is the thing you put it there for.

Deploy your Go app

7 days free with a card, then $2/mo.