Github actions I
October, 18, 2025I’ve used CI/CD pipelines on all my jobs on GitLab; this web, however, has its code located on GitHub. I’m too lazy to move it to GitLab, and I’m also too lazy to log in to my host server, pull my changes from the main branch, and run the update script (which I’ve written) manually. It occurred to me that this is a great opportunity to get started with GitHub Actions and automate the deployment process.
As af first MVP, I want to create a CI pipeline that deploys the Django app if some code is pushed to the main branch.
First of all, I've generated SSH public and private keys:
ssh-keygen -t rsa -b 4096 -f .
Make sure no passphrase is passed. Add SSH private key, host user name, and IP to the GitHub Actions secrets of the project:

Cat the public key, SSH to your server, and add it to the end of ~/.ssh/authorized_keys.
In my project, the deployment process on the server is handled by restart.sh script that I've written. So to keep it as an exe file after pulls, and keep access to all the files in the project for the user:
# make it executable locally and commit the mode, so future pulls keep it
git update-index --chmod=+x restart.sh
git commit -m "Make restart.sh executable"
git push
#make the user owner of the root_folder and grant rights
sudo chown -R user:user ~/root_folder
sudo chmod -R u+rwX ~/root_folder
Also, to avoid calling the sudo password call in the script and calling git pull, add these lines to /etc/sudoers :
user ALL=(root) NOPASSWD: /home/user/root_folder/restart.sh
user ALL=(root) NOPASSWD: /usr/bin/git -C /home/user/root_folder pull --ff-only
I've created in my root folder .github/workflows/main.yml file, and in the next order added things:
Top-level metadata
name: Continuous Integration
Label you’ll see in the Actions UI for this workflow.
on: → push: → branches: [ master ]
Only runs when commits are pushed to the master branch.
concurrency:
Prevents overlapping runs for the same “group.”
-
group: master→ all runs share the same group namemaster. -
cancel-in-progress: true→ if a new push arrives while an older run is still executing, the older run is canceled so only the latest deploy proceeds.name: Continuous Integration on: push: branches: - master concurrency: group: master cancel-in-progress: true
Job definitions
jobs: → deploy:
Defines a single job named deploy.
name: Deploy
Human-friendly name for the job in the UI.
runs-on: ubuntu-latest
Allocates a fresh Ubuntu runner VM to execute the steps.
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
css:
- 'en_legin_abroad/static/css/**/*.css'
- 'css/**/*.css'
- 'static/**/*.css'
Steps:
Check out the code:
-
Fetches your repository into the runner.
-
fetch-depth: 0pulls full history (not just the last commit). That keepsgitfeatures and some diff-based tools are happy and are generally safer when other steps might rely on history.
Detect if CSS changed:
-
Runs the
dorny/paths-filteraction to compute whether files matching the given globs changed in this push. -
Gives you an output boolean
steps.changes.outputs.cssequal to'true'if any of those patterns were touched. -
Important indentation: under
filters: |the list items must be indented beneathcss.
Configure SSH
- name: Configure SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
cat >>~/.ssh/config <<END
Host target
HostName $SSH_HOST
User $SSH_USER
IdentityFile ~/.ssh/id_rsa
LogLevel ERROR
StrictHostKeyChecking no
END
-
Pulls connection details from Actions secrets (never hardcode keys).
-
Writes the private key to
~/.ssh/id_rsawith strict permissions (600). -
Creates an SSH config alias
targetso later steps can just runssh target .... -
StrictHostKeyChecking noskips host key verification (convenient but insecure).
Deploy path when CSS changed (do collectstatic path)
- name: Deploy with collect static
if: steps.changes.outputs.css == 'true'
run: |
ssh target 'cd ~/root_folder && git pull --ff-only && yes yes | sudo ./restart.sh -c'
-
Only runs if the filter says CSS changed.
-
Remote command does three things, left to right:
-
cd ~/root_folder→ go to your app directory on the server. -
git pull --ff-only→ update to the latest commit (will fail if a merge would be required). -
yes yes | sudo ./restart.sh -c→ runs your restart script with the-coption (your “collect static” path), and auto-answers the prompt that requires typing the full wordyes.-
yes yesoutputsyesrepeatedly; the pipe feeds it to the script’s stdin.
-
-
Deploy path when CSS did not change
- name: Deploy without changes in static files
if: steps.changes.outputs.css != 'true'
run: |
ssh target 'cd ~/root_folder && git pull --ff-only && sudo ./restart.sh'
-
Runs in the opposite case.
-
Same as above, but calls the regular restart path (no
-c, noyespiped).
And it worked ![]()
