GitHub Action: RunR deploy
actions/deploy in this repository deploys a RunR project from a GitHub Actions workflow. It installs the runr CLI, runs runr deploy and waits until the new version serves. The first run creates the project from runr.yaml; later runs apply config changes, upload the code as a new version and deploy it.
Quick start#
Create a token that can deploy and store it as the repository secret
RUNR_TOKEN:runr tokens create github-actions --preset deploy --expires 90dUse
--preset deploy-createwhen the workflow should create the project on its first run.Add the workflow:
# .github/workflows/deploy.yml name: deploy on: push: branches: [main] concurrency: deploy-production # one deployment at a time jobs: deploy: runs-on: ubuntu-latest environment: name: production url: ${{ steps.runr.outputs.url }} steps: - uses: actions/checkout@v4 - id: runr uses: brf-tech/runr/actions/deploy@v0.5.7 with: api-url: https://runr.example.com token: ${{ secrets.RUNR_TOKEN }} path: ./functions/orders # directory with runr.yaml - name: Smoke test run: curl -fsS "${{ steps.runr.outputs.url }}"
Container projects#
Build and push the image, then deploy that tag:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/acme/web:${{ github.sha }}
- uses: brf-tech/runr/actions/deploy@v0.5.7
with:
api-url: https://runr.example.com
token: ${{ secrets.RUNR_TOKEN }}
project: web
image: ghcr.io/acme/web:${{ github.sha }}
The region's cluster must be able to pull the image (a public image, or the project's image pull secret).
Several projects#
strategy:
matrix:
service: [orders, billing, notify]
steps:
- uses: actions/checkout@v4
- uses: brf-tech/runr/actions/deploy@v0.5.7
with:
api-url: ${{ vars.RUNR_API_URL }}
token: ${{ secrets.RUNR_TOKEN }}
path: services/${{ matrix.service }}
Secret variables#
secrets takes one KEY=VALUE per line and stores them as secret environment variables of the project before the deploy, so the new deployment runs with them. Take the values from repository or environment secrets:
- uses: brf-tech/runr/actions/deploy@v0.5.7
with:
api-url: https://runr.example.com
token: ${{ secrets.RUNR_TOKEN }}
secrets: |
DATABASE_URL=${{ secrets.DATABASE_URL }}
STRIPE_KEY=${{ secrets.STRIPE_KEY }}
- Every value is masked in the log (
::add-mask::) and handed torunr env set --secret --no-deploy KEYon stdin, never as a command-line argument, so it shows up in no process list and no trace. - Blank lines and
#comments are skipped; spaces around the key and the value are removed; an empty value or an invalid name fails the step (exit code 2). - Secrets are write-only: RunR lists their names (
runr env ls), never their values. Secrets set by earlier runs and not listed any more stay; remove them withrunr env unset NAME. - When the first run creates the project (
runr env setfinds no project, exit code 4), the action sets the secrets right after that deploy and deploys once more, so the live deployment has them from the first run on; the outputs are those of the second deploy.
Preview deployments of pull requests#
RunR has preview environments built in: connect the project to the repository through the GitHub App and turn previews on, and every pull request gets its own service and URL, rebuilt on each commit and deleted when the request is closed or merged — no workflow needed (git deploy):
runr git connect --provider github --repo acme/orders --previews -p orders-api
RunR reports each preview on the pull request (commit status, a GitHub deployment preview/pr-<n> with the URL, one comment kept up to date).
When the code reaches RunR through this action instead (a build step in the workflow, a repository that is not connected), a workflow can deploy each pull request as a project of its own and remove it when the request closes. The preview project is named after the request (orders-pr-<number>): the workflow writes that name into runr.yaml, so the first run creates the project and later commits deploy new versions of it.
# .github/workflows/preview.yml
name: preview
on:
pull_request:
types: [opened, reopened, synchronize, closed]
concurrency: preview-${{ github.event.number }}
env:
PREVIEW: orders-pr-${{ github.event.number }}
jobs:
deploy:
# pull requests from forks get no secrets in GitHub Actions
if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
environment:
name: pr-${{ github.event.number }}
url: ${{ steps.runr.outputs.url }}
steps:
- uses: actions/checkout@v4
- name: Name the preview project
run: sed -i "s/^name: .*/name: ${PREVIEW}/" functions/orders/runr.yaml
- id: runr
uses: brf-tech/runr/actions/deploy@v0.5.7
with:
api-url: ${{ vars.RUNR_API_URL }}
token: ${{ secrets.RUNR_PREVIEW_TOKEN }}
path: ./functions/orders
message: "PR #${{ github.event.number }} ${{ github.event.pull_request.head.sha }}"
secrets: |
DATABASE_URL=${{ secrets.PREVIEW_DATABASE_URL }}
remove:
if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
RUNR_API_URL: ${{ vars.RUNR_API_URL }}
RUNR_TOKEN: ${{ secrets.RUNR_PREVIEW_TOKEN }}
RUNR_VERSION: v0.5.7
steps:
- name: Install the runr CLI
run: |
curl -fsSL "https://runr.sh/dl/cli/${RUNR_VERSION}/runr_linux_amd64.tar.gz" \
| sudo tar -xz -C /usr/local/bin runr
- name: Delete the preview project
run: runr projects delete "$PREVIEW" --yes || [ $? -eq 4 ] # 4: it was never created
- The token creates and deletes the preview projects:
runr tokens create github-previews --preset deploy-create --ability projects.delete --expires 90d. - A preview project starts with the variables of
runr.yamlonly; secret variables come from thesecretsinput (preview values, not production ones). concurrencykeeps the deploys of one request in order, and a close after a running deploy waits for it.
Inputs#
| Input | Default | Description |
|---|---|---|
api-url |
(required) | RunR console or API URL |
token |
(required) | RunR API token |
project |
name in runr.yaml |
Project name or id |
path |
. |
Project directory (holds runr.yaml and the code) |
image |
Container image to deploy (caas projects) | |
region |
Region of a project the first deploy creates | |
message |
<workflow> <short sha> |
Version message |
wait |
true |
Wait until the deployment is ready; false returns once it started |
timeout |
10m |
How long to wait for the build and the deployment |
version |
latest |
CLI release: latest or a tag such as v0.5.7 (pin it for reproducible runs) |
cli-base-url |
https://runr.sh/dl/cli |
Download base of the CLI archives, for a mirror: <base>/latest.txt (tag of the latest release), <base>/<tag>/runr_<os>_<arch>.tar.gz and <base>/<tag>/checksums.txt |
secrets |
Secret environment variables, one KEY=VALUE per line, stored write-only before the deploy |
|
args |
Extra arguments for runr deploy, split on whitespace |
Outputs#
| Output | Description |
|---|---|
url |
Public URL of the project (empty for schedule and queue triggers) |
project |
Project name |
project-id |
Project id |
version |
Deployed version label, e.g. v7 |
version-id |
Deployed version id |
deployment-id |
Deployment id |
status |
active, or pending with wait: false |
The action also adds a summary (project, version, URL) to the job page.
How it works#
- Resolve the CLI release:
latestis the tag in<cli-base-url>/latest.txt(https://runr.sh/dl/cli/latest.txt), a tag is used as given; the runner's OS (Linux, macOS, Windows) and architecture (x64, ARM64) pick the archive. - Cache: the CLI is cached with
actions/cacheper release, OS and architecture, so later runs skip the download. - Install: the archive and the release's
checksums.txtare downloaded from<cli-base-url>/<tag>/; the archive's SHA-256 must match before it is unpacked. - Secrets (with
secrets): eachKEY=VALUEis stored withrunr env set --secret --no-deploy(values masked, passed on stdin); a project the first deploy is about to create gets them right after that deploy, followed by a second deploy. - Deploy:
runr deployruns withRUNR_API_URLandRUNR_TOKENfrom the inputs (the token is masked in the log) and--output-env "$GITHUB_OUTPUT", which writes the outputs. The CLI's exit code fails the step: 3 for a rejected token or a permission the token lacks, 5 for invalid input, 6 for a failed build or deployment, 7 for a timeout (see exit codes).
The scripts need bash, curl and tar (unzip or PowerShell on Windows); GitHub-hosted runners have them. Self-hosted runners behind a proxy use cli-base-url with a mirror of the release archives.
Token scopes#
A token with the deploy preset holds exactly what runr deploy calls:
| Permission | Used for |
|---|---|
projects.view |
find the project, read versions and deployment status |
regions.view |
resolve region of runr.yaml |
projects.update |
apply runr.yaml config changes and image |
env.edit |
apply the env of runr.yaml and the secrets input (includes reading the variables, env.view) |
code.edit |
upload the code as a new version |
projects.deploy |
start the deployment |
projects.create |
(deploy-create) create the project on the first run |
A caas-only workflow (image input) works without code.edit; a runr.yaml without env and a workflow without secrets work without env.edit. Keep the token in a secret, give it an expiry and rotate it with runr tokens create / runr tokens rm.
Other CI systems#
The action is a thin wrapper around runr deploy; any CI can do the same with RUNR_API_URL, RUNR_TOKEN and --output-env.
GitLab CI/CD#
Store RUNR_API_URL and RUNR_TOKEN as CI/CD variables (Settings → CI/CD → Variables; mask the token), then add a job to .gitlab-ci.yml:
deploy:
stage: deploy
image: alpine:3.21
variables:
RUNR_VERSION: v0.5.7
before_script:
- apk add --no-cache curl tar
- curl -fsSL https://runr.sh/install.sh | sh # installs $RUNR_VERSION
script:
- runr deploy services/orders --message "$CI_COMMIT_SHORT_SHA" --output-env deploy.env
artifacts:
reports:
dotenv: deploy.env
environment:
name: production
url: $url
Testing the action#
bash actions/deploy/test/run.sh runs the action's scripts against a fake release served from file:// (archive, checksums.txt) and a stub CLI: release resolution, install with checksum verification (a tampered archive is refused), the arguments of runr deploy, the outputs and the job summary.