f in x
GitHub Actions from Zero: Workflow, Job, Step and Trigger for Automated Deploy
> cd .. / HUB_EDITORIALE
Analisi dei dati e metriche

GitHub Actions from Zero: Workflow, Job, Step and Trigger for Automated Deploy

[2026-06-07] Author: Ing. Calogero Bono

Have you ever kicked off a manual deploy on Friday at 6 PM and found out on Monday that a test had failed? Or worse: you forgot to run the database migration and the site was down for half a day? We, at Meteora Web, have seen these scenarios dozens of times. The solution is CI/CD automation, and GitHub Actions is the most accessible tool to start—especially if your code is already on GitHub.

This guide starts from zero but is not for absolute beginners: you already know some Git and YAML? Good. Here you’ll truly understand how workflow, job, step and trigger work, not just how to copy a template YAML file. You’ll learn what belongs in a job vs a step, when to use a push trigger versus a pull_request trigger, and why getting the structure wrong can cost you time and resources.

What is a Workflow? The Black Box of Your Automation

A workflow is an automated process defined in a YAML file inside .github/workflows/. Each repository can have multiple workflows, each independent. Think of a workflow as a recipe that says: “when X happens, run this sequence of operations.”

The minimal structure looks like this:

name: Deploy to production
on:
  push:
    branches: ["main"]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Run deploy
        run: echo "Simulated deploy"

Key components:

  • name: human-readable name (optional but useful).
  • on: the trigger — when to start the workflow.
  • jobs: a set of jobs that run in parallel by default.
  • runs-on: the runner environment (e.g., ubuntu-latest, windows-latest).
  • steps: commands executed one after another inside the job.

YAML is indentation-sensitive. We, at Meteora Web, have seen workflows break because a tab was used instead of two spaces. Always use a YAML-aware editor.

Triggers: When and Why to Run the Workflow

The on trigger is the decision heart. You can use simple events like push, pull_request, schedule (cron), workflow_dispatch (manual), or events from external services via repository_dispatch.

A common mistake is using push on all branches. For CI you usually want to run only on main branches and pull requests. Example:

on:
  push:
    branches: ["main", "develop"]
  pull_request:
    branches: ["main"]

Note that pull_request triggers the workflow when the PR is opened or updated, not when pushing to the PR branch (unless it’s against main). This avoids duplicate runs.

Other useful triggers:

  • schedule: for nightly backups or log cleanup.
  • workflow_dispatch: to run the workflow manually from the Actions tab.
  • release: to automatically publish to npm or Docker Hub on release creation.

Path filters matter: you can limit triggers to specific files with paths. For example, trigger deploy only if docker-compose.yml changes. We use this to avoid restarting the entire pipeline when you only change the README.

Jobs: The Separate Rooms of Your Automation

A workflow can have one or more jobs. Each job runs on an independent runner and, by default, in parallel. This is powerful: you can, for instance, run tests on three Node.js versions simultaneously without waiting.

Example matrix strategy:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [16, 18, 20]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test

Each job is isolated: if the “test on Node 16” job fails, the others continue. You can also define dependencies between jobs with needs. Example:

jobs:
  test:
    runs-on: ubuntu-latest
    steps: [/* tests */]
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps: [/* deploy */]

Here deploy only starts if test completes successfully (by default, if a job fails, dependent jobs don’t run).

Job Runners and Environments

Each job runs on a runner. Official runners are ubuntu-latest, windows-latest, macos-latest (each with a predefined set of tools). You can also use self-hosted runners for specific needs (e.g., GPU, internal databases). We, at Meteora Web, set up a self-hosted runner for a client who needed to compile code on an on-premise server with special licenses. Security is paramount: the runner has access to your code, so only use trusted runners.

Steps: The Atomic Building Blocks

Inside each job you have steps. Each step performs an atomic action: it can be a shell command (run) or a predefined action (uses). Steps are sequential: if one fails (exit code ≠ 0), subsequent steps are skipped unless you use if: always() or if: failure().

Example using community actions:

steps:
  - name: Checkout code
    uses: actions/checkout@v4
  - name: Setup PHP
    uses: shivammathur/setup-php@v2
    with:
      php-version: '8.2'
  - name: Copy .env
    run: cp .env.example .env
  - name: Run Composer
    run: composer install --no-interaction --prefer-dist
  - name: Run tests
    run: php artisan test

Each step has a name that appears in logs. Don’t underestimate clear names: when a workflow fails, a descriptive name saves you minutes of debugging.

Using Predefined Actions vs Raw Commands

Actions (from the Marketplace) are reusable packages. For common operations (checkout, language setup, cloud deployment) it’s better to use them. For custom logic (e.g., migration scripts), write a run command directly. We advise against writing hundreds of lines of inline shell: move the logic into a script in the repository and call it with run: bash scripts/deploy.sh.

Common Mistakes and How to Avoid Them

1. Malformed YAML: an indentation error breaks everything. Use yaml-lint or GitHub’s built-in validation. We run a workflow that lints all YAML files before every deploy.

2. Too broad triggers: if you run the workflow on every push to every branch, every commit triggers the pipeline — including a typo fix in the README. Filter with branches and paths-ignore.

3. Clear-text environment variables: never write passwords or tokens directly in the YAML file. Use GitHub secrets: ${{ secrets.MY_SECRET }}.

4. Wrong job dependencies: if you use needs, remember you cannot share data between jobs without artifacts or cache. To pass a file from one job to another, use actions/upload-artifact and actions/download-artifact.

Complete Example: CI + Conditional Deploy

Let’s put everything together in a realistic workflow for a Laravel app with tests and deploy to a VPS via SSH:

name: CI/CD Laravel

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_DATABASE: app_test
          MYSQL_USER: test
          MYSQL_PASSWORD: test
          MYSQL_ROOT_PASSWORD: root
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: pdo, pdo_mysql
      - run: cp .env.example .env
      - run: composer install -q --no-interaction --prefer-dist
      - run: php artisan key:generate
      - run: php artisan migrate --force
      - run: php artisan test

  deploy:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy via SSH
        uses: easingthemes/ssh-deploy@v4
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
          remote-host: ${{ secrets.SSH_HOST }}
          remote-user: ${{ secrets.SSH_USER }}
          source: "."
          target: "/var/www/app"
      - name: Run migrations and optimizations
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/app
            php artisan migrate --force
            php artisan cache:clear
            php artisan config:cache

Note: deploy only runs when push is on main and after tests pass. This is the pattern we recommend to every client starting with CI/CD.

In Summary — What to Do Now

  1. Create the file .github/workflows/ci.yml in your repository. That’s all you need to start.
  2. Choose the right triggers: at least push on the main branch and pull_request on that same branch.
  3. Define at least one job that runs tests. If you use multiple language versions, leverage the matrix strategy.
  4. Separate CI and CD: one job for tests, one for deploy with needs: test and a conditional if on the branch.
  5. Never expose secrets: always use ${{ secrets.NAME }} for passwords, tokens and SSH keys.
  6. Check the logs after the first run: GitHub shows each step in real time. If something fails, the output tells you why.

If you need help setting up your pipeline or want us to clean up an existing workflow, talk to us. At Meteora Web we work on this stuff every day — from domain to revenue, a single point of contact.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()