Last updated on 6th May 2026

Migrating from GitHub Actions to DeployHQ

Most teams do not need to migrate off GitHub Actions. Actions is excellent at what it was designed for — running tests, building artifacts, gating PRs. Where it falls short is the deploy step itself: there is no atomic release model, no audit log scoped to deployments (just generic workflow runs), no one-click rollback, no per-environment role separation beyond environments: rules, and no first-class concept of "the previous release on the server."

That is what DeployHQ replaces. The rest of your Actions setup stays exactly where it is.

This guide is about the hybrid pattern: keep GitHub Actions as your CI runner, hand the deploy step to DeployHQ, and gain the deployment-specific features (rollback, audit, atomic symlink swaps, role-based deploy permissions, AI error explanation) that Actions was never built to provide. For a side-by-side feature comparison, see DeployHQ vs GitHub Actions.

If you are migrating from a script-based deploy tool that predates GitHub Actions, see Migrating from Capistrano to DeployHQ or Migrating from Mina to DeployHQ — same destination, different shape.

What stays, what moves, what goes away

Today, in GitHub Actions After migration
pull_request lint, test, type-check jobs Stays in GitHub Actions — unchanged
Build artifacts (compiled assets, Docker images, binaries) Stays in GitHub Actions or moves to DeployHQ build pipelines — your call
appleboy/ssh-action, raw ssh, rsync, scp deploy steps Moves to DeployHQ — replaced by SSH commands and atomic upload
Symlink-flipping bash inside YAML Goes away — DeployHQ does atomic releases natively
secrets.SSH_PRIVATE_KEY, secrets.HOST, secrets.USERNAME Goes away — DeployHQ holds per-server keys, audit-logged
Manual rollback (commit revert, re-run workflow) Goes away — replaced by one-click rollback
deployhq-action (webhook-trigger) Optional — replace with the DeployHQ CLI for richer status reporting

Why move the deploy step at all

The honest answer: because Actions treats a deploy like any other CI step, and a deploy is not like any other CI step.

  • SSH server management is genuinely painful in Actions. Every workflow needs webfactory/ssh-agent or appleboy/ssh-action, a KNOWN_HOSTS block (or StrictHostKeyChecking=no, which you should not be doing), an SSH_PRIVATE_KEY secret, an SSH_HOST secret, and an SSH_USER secret. Rotate the key and you are editing GitHub Secrets across every repository that deploys to that server. Add a server to the rotation and you are copy-pasting host blocks into YAML. There is no inventory of which keys reach which servers, no record of which deploys used which keys, and no way to revoke without hunting through repos. DeployHQ holds the keys in a dashboard with per-server scope, an audit trail on every use, and one place to rotate.
  • Audit log of every deploy. Workflow runs are generic. DeployHQ records who deployed what, from which branch, at which commit, and what each step output — separately from tests, lint, and build runs.
  • Atomic releases on the target server. No half-deployed state if a step fails mid-upload. The new release is built in a separate directory, then the symlink swaps in one operation.
  • One-click rollback to any prior release. Not a git revert plus a re-run. The previous release stays on disk; rollback flips the symlink back.
  • Per-environment deploy permissions. Grant a junior engineer the right to deploy to staging without giving them write access to .github/workflows/ or production environment secrets.
  • AI deploy error explanation. When a step fails, the failure is summarised in plain English with a likely fix.
  • Deploy from anywhere. Browser, terminal (dhq deploy), API, or — yes — a GitHub Actions step. Same atomic semantics either way.

If none of those matter to you, an SSH-based Actions deploy is fine. If even one of them matters, the deploy step is the wrong job for Actions to do.

The migration in three steps

1. Set up the DeployHQ project

Connect the same repository, configure your servers, and set shared paths. Whatever your current deploy YAML does over SSH on the target server — composer install --no-dev, npm ci && npm run build, php artisan migrate, service restarts — translate it into:

  • Build pipeline commands for anything that produces artifacts (composer install, npm ci, asset compilation). These run once on our build infrastructure and the output is uploaded with the release. You stop running npm install on every target server, every deploy.
  • SSH commands for anything that runs on the target after upload (php artisan migrate, systemctl restart php-fpm, cache warmups). Each command is bound to a deploy stage: before upload, after upload, before symlink change, or after deploy.

2. Replace your deploy job

A typical "before" looks like one of the patterns below. The "after" is the same in every case: a thin DeployHQ CLI call.

Before — SSH from Actions:

deploy:
  needs: [test, build]
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: webfactory/ssh-agent@v0.9.0
      with:
        ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
    - run: |
        rsync -avz --delete ./build/ deploy@${{ secrets.HOST }}:/var/www/app/releases/${{ github.sha }}/
        ssh deploy@${{ secrets.HOST }} '
          cd /var/www/app/releases/${{ github.sha }} &&
          ln -sfn /var/www/app/shared/.env .env &&
          php artisan migrate --force &&
          ln -sfn /var/www/app/releases/${{ github.sha }} /var/www/app/current &&
          sudo systemctl reload php-fpm
        '

After — DeployHQ CLI:

deploy:
  needs: [test, build]
  runs-on: ubuntu-latest
  steps:
    - name: Install DeployHQ CLI
      run: curl -fsSL https://raw.githubusercontent.com/deployhq/deployhq-cli/main/install.sh | sh
    - name: Deploy
      run: dhq deploy --server production --revision ${{ github.sha }} --wait --json
      env:
        DEPLOYHQ_ACCOUNT: ${{ secrets.DEPLOYHQ_ACCOUNT }}
        DEPLOYHQ_EMAIL: ${{ secrets.DEPLOYHQ_EMAIL }}
        DEPLOYHQ_API_KEY: ${{ secrets.DEPLOYHQ_API_KEY }}
        DEPLOYHQ_PROJECT: my-app

The atomic symlink swap, the shared-file linking, the migration step, and the service reload all move into DeployHQ where they are visible, auditable, and rollback-aware. The Actions YAML becomes one step instead of forty.

For more workflow recipes — staging-on-push and production-on-release with auto-rollback, deploy-on-PR-merge with a status comment, GitLab CI, and Bitbucket Pipelines — see Using the CLI in CI/CD Pipelines.

3. Delete the SSH secrets

Once the new pipeline is verified, remove SSH_PRIVATE_KEY, HOST, USERNAME, and any other deploy-specific secrets from your repository. They are no longer needed — DeployHQ holds the per-server keys, scoped to the project, audit-logged on use.

Already using deployhq-action?

The webhook-triggered deployhq-action is the older integration pattern: it fires a POST to a project webhook URL and DeployHQ runs the deploy. It still works and is fine for simple cases.

The CLI-based pattern shown above is the recommended path for new setups because it gives you:

  • Synchronous status in the Actions log (the workflow step waits for the deploy to finish, with --wait).
  • Structured JSON output you can parse with jq to gate downstream steps, post PR comments, or trigger rollback on failure.
  • No double-deploy risk from a forgotten GitHub-side webhook (deployhq-action requires you to delete the auto-installed webhook to avoid this).

If you are happy with fire-and-forget triggering, keep deployhq-action. If you want the deploy to surface failures back into Actions, switch to the CLI.

What teams ask before they switch

Do I have to give up GitHub Actions entirely? No. The opposite. Keep Actions for everything it does well (tests, lint, type-check, artifact builds). Move only the deploy step.

What if my Actions workflow builds a Docker image and pushes to a registry? Keep the image build in Actions if it is already working there. The actual deploy (whatever runs after the image is pushed — kubectl set image, aws ecs update-service, gcloud run deploy, terraform apply) can also move to DeployHQ via Custom Actions (currently in beta). Custom Actions run any CLI tool — aws, kubectl, gcloud, terraform, or your own Docker image — inside DeployHQ's deploy pipeline, with the same audit log, role-based permissions, and rollback story as SSH deploys. So the choice for cloud-API deploys is not "Actions or nothing" — it is "Actions YAML, or a DeployHQ Custom Action with the same dashboard, audit, and approvals as your SSH targets." If your CI/CD already runs cleanly in Actions and you do not need those features for that step, leaving it in Actions is fine.

Can I trigger DeployHQ from pull_request events for preview deploys? Yes. dhq deploy accepts a branch and a server, so a PR-opened workflow can deploy that branch to a dedicated preview server. See the deploy-on-PR-merge example and adapt the trigger.

What about deployment environments and approvals? GitHub's environments: with required reviewers still works in front of the dhq deploy step, so approval gates remain in Actions. DeployHQ adds its own per-environment role-based permissions on top — useful if you want approval policy outside of GitHub.

What if my deploy needs files only present in the Actions workspace (e.g. compiled assets, generated files)? Two options. Either run the equivalent build step inside DeployHQ's build pipeline (recommended — runs once, included in every deploy), or upload the artifact from Actions to a release tarball / object store and have a DeployHQ SSH command pull it down before symlink change. Most teams move the build into DeployHQ.

Can I roll back from inside an Actions workflow? Yes. dhq rollback DEPLOYMENT_ID is scriptable with the same auth pattern as deploy. The CI/CD article shows a failure-triggered rollback example.

Start your migration

Set up a DeployHQ project pointing at the same repository, copy your deploy YAML's SSH steps into DeployHQ as build pipeline commands and SSH commands, and replace the deploy job with the four-line CLI step shown above. Most teams have the new path working in staging in under an hour.

For setup details, see Using the CLI in CI/CD Pipelines — it has working examples for Actions (basic, multi-environment with auto-rollback, and PR-merge with status comments) plus equivalents for GitLab CI and Bitbucket Pipelines.

GitHub Actions is a trademark of GitHub, Inc. DeployHQ is not affiliated with GitHub. We just know what the deploy step looks like when it stops being a CI concern.