Github actions I

October, 18, 2025

I’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 name master.

  • 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: 0 pulls full history (not just the last commit). That keeps git features 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-filter action to compute whether files matching the given globs changed in this push.

  • Gives you an output boolean steps.changes.outputs.css equal to 'true' if any of those patterns were touched.

  • Important indentation: under filters: | the list items must be indented beneath css

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_rsa with strict permissions (600).

  • Creates an SSH config alias target so later steps can just run ssh target ....

  • StrictHostKeyChecking no skips 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:

    1. cd ~/root_folder→ go to your app directory on the server.

    2. git pull --ff-only → update to the latest commit (will fail if a merge would be required).

    3. yes yes | sudo ./restart.sh -c → runs your restart script with the -c option (your “collect static” path), and auto-answers the prompt that requires typing the full word yes.

      • yes yes outputs yes repeatedly; 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, no yes piped).

And it worked smiley

Here is the link for YML.


Similar articles:

Learning Modern Linux

Learning Python

Rust course for begginers

Reddit