- Prerequisites
- Using
npm
across the project - Manually creating workflow files
- Generating SSH keys for GitHub Actions with Trellis CLI
- Configuring secrets in GitHub
- File:
.github/workflows/deploy-staging.yml
- Adding SSH keys to the global Vault
- Modifying the
deploy
role tasks to write the key on the server - Adding a
Makefile
for convenience commands - Best practices
- Final result
This guide outlines, step by step, how to configure a continuous deployment workflow for a project based on the Roots stack (Trellis + Bedrock + Sage 11) using GitHub Actions. It includes secure SSH key handling for staging and production environments.
-
A Git repository organized following the Roots structure:
project-name/ ├── site/ # WordPress via Bedrock │ └── web/app/themes/sage ├── trellis/ # Ansible + server config └── .github/ # deployment workflows
-
GitHub CLI installed and authenticated:
gh auth login
-
A VPS or droplet provisioned with Trellis (Ubuntu, DigitalOcean, etc.)
-
A deploy SSH key generated and added to the GitHub repository as a "read-only deploy key"
While the project might have originally used yarn
, it is recommended to fully migrate to npm
, for the following reasons:
@wordpress/scripts
and other modern WordPress tools are designed and tested withnpm
.- Sage 11 has removed explicit references to
yarn
in its documentation. - Yarn 2+ (Berry) introduces a Plug'n'Play system incompatible with many tools.
cd site/web/app/themes/sage
rm yarn.lock
npm install
Then update the workflow and Makefile commands accordingly.
Instead of cloning external templates like trellis-github-deployment
, this guide defines custom GitHub Actions workflows from scratch.
- Create the following folder structure:
mkdir -p .github/workflows
- Inside
.github/workflows/
, create two files:
deploy-staging.yml
deploy-production.yml
- Use the examples provided in this guide as a base and customize them as needed:
- Sage theme path
- Build commands
- Deployment target (
staging
orproduction
)
This approach keeps workflows clean, minimal, and fully tailored to the project.
From the trellis
directory:
trellis key generate
This command will:
-
Generate a private SSH key in
~/.ssh/trellis_<slug>_ed25519
-
Create the corresponding public key in
trellis/public_keys/
-
Add the deploy key to the GitHub repo (if you have access)
-
Automatically create these GitHub secrets:
TRELLIS_DEPLOY_SSH_PRIVATE_KEY
TRELLIS_DEPLOY_SSH_KNOWN_HOSTS
You can also upload the key to the server:
trellis provision --tags=users staging
Each project should have its own key. Trellis handles naming to avoid SSH conflicts.
In your GitHub repository: Settings → Secrets and variables → Actions → New repository secret
Add the following secrets if they weren’t created automatically by trellis key generate
:
ANSIBLE_VAULT_PASSWORD
: content oftrellis/.vault_pass
TRELLIS_DEPLOY_SSH_PRIVATE_KEY
: your private SSH keyTRELLIS_DEPLOY_SSH_KNOWN_HOSTS
: output ofssh-keyscan -H github.com
GITHUB_TOKEN
: this is provided by default in GitHub Actions, no need to add manually
💡 Note: Secret values are hidden once saved. If unsure, re-uploading them is safe.
name: 🚀 Deploy to Staging
on:
workflow_dispatch:
jobs:
deploy:
name: Deploy to Staging
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Install SSH key and known_hosts
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.TRELLIS_DEPLOY_SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.TRELLIS_DEPLOY_SSH_KNOWN_HOSTS }}
if_key_exists: replace
- name: Setup Trellis CLI
uses: roots/setup-trellis-cli@v1
with:
ansible-vault-password: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Sage 11 Theme
run: |
cd site/web/app/themes/sage
npm install
npm run build
- name: Install Sage PHP dependencies
run: |
cd site/web/app/themes/sage
composer install --no-dev --no-interaction --optimize-autoloader
- name: Deploy to staging
run: trellis deploy staging
Create a similar file for
deploy-production.yml
, replacingstaging
withproduction
.
Edit:
ansible-vault edit trellis/group_vars/all/vault.yml
Add:
vault_github_deploy_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
your-key-here...
-----END OPENSSH PRIVATE KEY-----
Edit trellis/roles/deploy/tasks/main.yml
:
Insert right before - import_tasks: initialize.yml
:
- name: Ensure .ssh directory exists
file:
path: "/home/{{ web_user }}/.ssh"
state: directory
owner: "{{ web_user }}"
group: "{{ web_group }}"
mode: "0700"
- name: Write GitHub deploy key from Vault
copy:
content: "{{ vault_github_deploy_key }}"
dest: "/home/{{ web_user }}/.ssh/github_deploy_key"
owner: "{{ web_user }}"
group: "{{ web_group }}"
mode: "0600"
- name: Configure SSH to use GitHub deploy key
copy:
content: |
Host github.com
IdentityFile /home/{{ web_user }}/.ssh/github_deploy_key
IdentitiesOnly yes
dest: "/home/{{ web_user }}/.ssh/config"
owner: "{{ web_user }}"
group: "{{ web_group }}"
mode: "0644"
# Variables
BRANCH=main
deploy-staging:
gh workflow run deploy-staging.yml --ref $(BRANCH)
deploy-production:
gh workflow run deploy-production.yml --ref $(BRANCH)
build-theme:
cd site/web/app/themes/sage && npm install && npm run build
- Never store private keys in plain files (use Vault)
- Avoid agent-forwarding with GitHub Actions
- Prefer
npm
for best compatibility with WordPress ecosystem - Always ensure staging is running before deploying
When running:
make deploy-staging
GitHub Actions will:
- Clone the repo
- Build the Sage 11 theme
- Install PHP dependencies
- Upload the SSH key to the server
- Deploy the site to staging via Trellis
All of this happens securely, reproducibly, and automatically.
Hi @aitormendez, great doc! But I have some questions:
Maybe I'm doing things the wrong way, but I think you are overdoing some tasks... 😄