Learning objectives
After reading this article you will be able to:
- Build a workflow that deploys on every push to the main branch
- Store tokens and API keys in secrets without exposing them in code
- Run tests before the deploy and check that it worked
On this page
- How does an automatic deploy with GitHub Actions work?
- What do you need before you start?
- How do you create the first workflow, step by step?
- How does each part of the file work?
- How do you store tokens and keys safely?
- How do you know the deploy worked?
- What are the most common errors?
- How to do this on Square Cloud
- FAQs
Deploying by hand works the first few times: you zip the folder, upload it, restart. Over time the manual step becomes the place where someone forgets to run the tests, uploads the wrong folder or leaves the server on an old version.
GitHub Actions fixes this by moving the deploy into the repository. A text file describes what happens on every push, and GitHub runs the steps on a temporary machine. If deployment is still a new idea, start with what a deployment is.
How does an automatic deploy with GitHub Actions work?#
A workflow is a YAML file stored in .github/workflows/ in your repository. It declares:
- Triggers (
on): what starts a run, such as a push, a published release or a manual click. - Jobs: units of work. Each job runs on its own virtual machine, called a runner.
- Steps: the actions inside a job. A step either runs a command (
run) or uses a ready-made action (uses).
In an automatic deploy, the trigger is a push to the main branch and the last job sends the code to the server. Everything the job needs (a token, an API key) comes from secrets, never from the code.
What do you need before you start?#
- A GitHub repository with your project.
- An application already created on your server or hosting platform, so there is somewhere to send the code.
- A credential that lets you send code to that target (an API key, a token or an SSH key).
- A script or command that deploys from the terminal. If you can deploy by running one command on your machine, you can automate it.
Test the deploy command locally before putting it in the workflow. Debugging YAML and the deploy at the same time is much slower.
How do you create the first workflow, step by step?#
1. Create the file. In the repository, create .github/workflows/deploy.yml. The file name is up to you, the folder is not.
2. Save the credential as a secret. On GitHub, open Settings > Secrets and variables > Actions, go to the Secrets tab and click New repository secret. Pick a name such as DEPLOY_TOKEN and paste the value. From the command line, gh secret set DEPLOY_TOKEN prompts for the value and stores it.
3. Write the workflow. This example runs the tests of a Node.js project and only then deploys:
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: lts/*
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Send to the server
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./scripts/deploy.sh4. Commit and push to main. The workflow shows up in the repository's Actions tab and starts running.
scripts/deploy.sh is the command you already tested locally. Replace it with whatever your target needs; each platform has its own route: an rsync over SSH, its own CLI or an official action, like the one in the last section.
How does each part of the file work?#
Which branch triggers the deploy?#
branches: [main] limits the push trigger to main. Pushes to other branches do not start the deploy. You can list several branches or use patterns such as release/**. You cannot combine branches and branches-ignore for the same event.
Why add workflow_dispatch?#
It adds a Run workflow button to the Actions tab. It is useful for redeploying without a new commit, for example after fixing an environment variable. The trigger only works when the file exists on the repository's default branch.
What does concurrency do?#
Without it, two quick pushes can start two deploys at the same time, and whichever finishes last wins, even if it is the older one. With a group, only one run of the group executes at a time and, by default, if several are waiting, only the newest stays pending. Keep cancel-in-progress: false for deploys: cancelling an upload halfway can leave the app in an incomplete state.
What does needs do?#
needs: test makes the deploy job wait for the test job and run only if it passes. It is the simplest way to keep broken code out of production.
Which version of each action should you use?#
Pin the major version, such as actions/checkout@v7. Avoid pointing at @main: a change by the action's author can alter your deploy without warning.
How do you store tokens and keys safely?#
Rules that prevent most incidents:
- Never write the credential in the YAML or the code. The file goes into Git history. If a key lands there, treat it as compromised and read what to do if an API key leaks on GitHub.
- Use
${{ secrets.NAME }}. GitHub redacts the value in logs. - Values that are not secret (such as an application ID) can go in Variables, on the same screen, and are read as
${{ vars.NAME }}. - Secrets do not reach workflows triggered from forks. GitHub does not pass them to the runner, which protects you from malicious pull requests.
- Grant the minimum permission. Add
permissions: contents: readto the workflow when it does not need to write to the repository.
For the application's own configuration (database, bot tokens), follow the same logic as environment variables: the value lives in the environment, not in the repository.
How do you know the deploy worked?#
- Open the Actions tab and enter the run. Each step has its own log. A green check on every job means the command finished without an error.
- Confirm at the target: did the app restart? Does the change show up? A green check only says the command exited with code 0, not that the app is up.
- Read the application logs right after the deploy (see how to read application logs). A missing variable or an uninstalled dependency shows up there.
What are the most common errors?#
- Empty secret. A typo in
secrets.NAMEraises no error: the expression returns an empty string, and the command fails later with an authentication error. Check the name against the stored secret. - Forgetting the checkout. The runner starts empty. Without
actions/checkoutthere are no files to send. - File in the wrong folder. The workflow must be in
.github/workflows/at the repository root, with a.ymlor.yamlextension. - YAML indentation. Two spaces per level, no tabs. One wrong space changes the meaning or breaks the file.
- Workflow does not start. Check that the push went to a branch listed under
branchesand that the file is on the branch you pushed. - A command that asks for confirmation. The runner has no interactive terminal. Commands that ask "are you sure?" hang or exit with an error, so use the command's auto-confirm option.
- Deploy from a fork does not run. Without secrets, the deploy job fails on pull requests from forks. Deploy only on push, not on
pull_request.
How to do this on Square Cloud#
If your target is Square Cloud, the official GitHub Action replaces scripts/deploy.sh: it runs a CLI command on every push, release or manual run. Keep the test job and swap only the deploy job:
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: squarecloudofc/github-action@v2
with:
token: ${{ secrets.SQUARECLOUD_API_KEY }}
command: commit ${{ vars.APP_ID }} --restartThe token input takes your account's API key, stored in Secrets as this guide describes, and the application ID can live in Variables, since it is not a secret. commit uploads the files in the working directory (the optional workdir input changes which one) and overwrites only the files it sends; the rest of the application's disk stays as it was. --restart restarts the application so the new version goes live. The platform has no build step: if your project compiles, run the build in a step before commit.
After the push, check the application's logs in the dashboard, as in the section on verifying the deploy. The squarecloud.app file is optional: the dashboard generates it at deploy time, or you write it at the project root. See the action's inputs on the GitHub Action page and the CLI commands.
Deploy
Push from GitHub, the CLI or a ZIP and go live in seconds
FAQs
Is GitHub Actions free?
It depends on the plan and the repository type. GitHub includes a quota of minutes and bills the excess in some cases, so check the GitHub Actions billing page for your situation.
Can I use secrets in pull requests from forks?
No. GitHub does not pass secrets to the runner when a workflow is triggered from a forked repository. This stops a third-party pull request from reading your credentials.
How do I trigger a deploy manually?
Add the workflow_dispatch trigger to the on block. GitHub then shows a Run workflow button in the Actions tab, and the trigger only works if the file is on the default branch.
Does the deploy run if the tests fail?
Not if the deploy job declares needs pointing at the test job. When a job that another depends on fails, the dependent job is skipped.
More in Deploy
See all 33 articles- What are environment variables?Environment variables are key-value pairs the operating system hands to a process at startup. Learn to read them in Node.js and Python and keep tokens out of code.
- What is a deployment?A deployment takes a version of your code to a server, prepares it and starts it. See the stages, the methods and how to roll back when it goes wrong.
- How much RAM and CPU does my app need?Size RAM and CPU by measuring your app's real peak and adding about 30% headroom. Learn how to measure in Node.js, Python and Java and what to do if short.
- Best way to host a Node.js appThe best way to host Node.js depends on the kind of process: always on or per request. Compare a VPS, a container platform and serverless, scenario by scenario.