Modularity in CI/CD pipeline

Draft Disclaimer: Please note that this article is currently in draft form and may undergo revisions before final publication. The content, including information, opinions, and recommendations, is subject to change and may not represent the final version. We appreciate your understanding and patience as we work to refine and improve the quality of this article. Your feedback is valuable in shaping the final release.

Language Mismatch Disclaimer: Please be aware that the language of this article may not match the language settings of your browser or device.
Do you want to read articles in English instead ?

A bit of devops content

Intro

  • Some steps are repeated
  • some of those are long enough where it started to make sense to look into code reuse
  • shorter chunks, easier to read and understand even for a beginner
  • outline today
    • blocking action with needs for dependency between workflow
    • how to define local actions for reuse in many workflows
    • auto update with dependabots.yml and auto merge when build passes
    • auto merge PR when patches and minor version updates
    • concurrency: cancel previously running action when new one started (ie cancel previous deployment)
  • gotchas
    • do not use reserve words. github_token is reserved

Single deployment workflow = f(environment) (read that take environment as variable)

jobs:
  ReuseableMatrixJobForDeployment:
    strategy:
      matrix:
        target: [dev, stage, prod]
    uses: octocat/octo-repo/.github/workflows/deployment.yml@main
    with:
      target: ${{ matrix.target }}

Few examples:

# file: .github/workflows/automerge.yml
on:
  pull_request:
    branches:
      - main
      
jobs:
  # Other jobs here
  # secrets.GITHUB_TOKEN exist by default I think, so change required for the following
  
  dependabot_auto_merge:
    runs-on: ubuntu-latest
    name: Automerge Dependabot PR
    needs: [php_lint, php_tests, php_static_analysis]
    if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/[email protected]
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Auto-merge Dependabot PRs for semver-minor updates
        if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Auto-merge Dependabot PRs for semver-patch updates
        if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CI run workflow on push, pull request and release

  • on push
    • build artifact
    • deploy
    • concurrency mode to prevent multiple deployments to same environment
  • on pull request to main branch
    • build
    • tests
      • lint
      • static code analysis
      • php tests
      • validate nginx configuration
    • auto merge
      • when from dependabot but only minor and patch updates
      • and when tests passes

Lint action files

# file: .github/workflows/action_lint.yml
name: Use actionlint to lint workflow files
on: [pull_request]
jobs:
  actionlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: reviewdog/action-actionlint@v1

Validate nginx configuration

name: Validate nginx configuration

on:
  pull_request:

jobs:
  validate_nginx_config:
    runs-on: ubuntu-20.04
    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout code (PR)
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - uses: actions/checkout@v4

      - name: Validate nginx configuration
        run: |
          docker run --rm -t -a stdout --name nginx-validate -v $PWD/:/app -v $PWD/docker/nginx/default.conf:/etc/nginx/sites-enabled/default.conf -v $PWD/docker/nginx.conf:/etc/nginx/nginx.conf nginx:latest nginx -c /etc/nginx/nginx.conf -t

Nginx/Php - files

# file: docker/nginx/default.conf
server {
    listen 80 default_server;

    return 301 https://$host$request_uri;
}

server {
    listen 443 default_server;

    root /home/{{ user }}/app/public/;
    index index.html index.htm index.php;
		
    ssl on;
    ssl_certificate /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;
    ssl_protocols TLSv1.1 TLSv1.2;

    ssl_prefer_server_ciphers on;

    ssl_session_timeout 5m;
    ssl_session_cache shared:SSL:5m;

    charset utf-8;

    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
    server_tokens off;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";
    add_header Expect-CT "enforce; max-age=31536000";

    client_max_body_size 0;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_log /var/log/nginx/error.log;
    error_page 403 =404 /404.html;
    access_log /var/log/nginx/access.log;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 700;
        keepalive_timeout 700;
        types_hash_max_size 2048;
    }

    location ~ /\.ht {
        deny all;
    }
}



# file: docker/nginx/nginx.conf
; Start a new pool named 'www'.
[www]

listen = /var/run/php-fpm.sock

listen.owner = {{ user }}
listen.group = nginx
listen.mode = 0660

user = {{ user }}
group = {{ user }}

pm = dynamic
pm.max_children = 200
pm.start_servers = 50
pm.min_spare_servers = 50
pm.max_spare_servers = 150
pm.max_requests = 500
slowlog = /var/log/php-fpm/www-slow.log
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path]    = /var/lib/php/session
php_value[soap.wsdl_cache_dir]  = /var/lib/php/wsdlcache




# file: docker/php/php.ini
[PHP]
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions =
disable_classes =
zend.enable_gc = On
expose_php = On
max_execution_time = 30
max_input_time = 60
memory_limit = -1
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
track_errors = Off
html_errors = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 3G
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 3G
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60
cli_server.color = On
pcre.jit=0
pdo_mysql.cache_size = 2000
pdo_mysql.default_socket=
sendmail_path = /usr/sbin/sendmail -t -i
mail.add_x_header = On
sql.safe_mode = Off
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1
ibase.allow_persistent = 1
ibase.max_persistent = -1
ibase.max_links = -1
ibase.timestampformat = "%Y-%m-%d %H:%M:%S"
ibase.dateformat = "%Y-%m-%d"
ibase.timeformat = "%H:%M:%S"
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.cache_size = 2000
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off
mysqlnd.collect_statistics = On
mysqlnd.collect_memory_statistics = Off
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0
bcmath.scale = 0
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.cookie_secure = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly = 1
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.sid_length = 26
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
zend.assertions = -1
tidy.clean_output = Off
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400
soap.wsdl_cache_limit = 5
ldap.max_links = -1