{"id":20613213,"url":"https://github.com/robiningelbrecht/continuous-integration-example","last_synced_at":"2025-04-15T07:11:37.095Z","repository":{"id":42472859,"uuid":"471465668","full_name":"robiningelbrecht/continuous-integration-example","owner":"robiningelbrecht","description":"This repository aims to build a fairly complete CI/CD example using GitHub workflows and actions.","archived":false,"fork":false,"pushed_at":"2022-04-04T08:27:46.000Z","size":1819,"stargazers_count":4,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-15T07:11:30.754Z","etag":null,"topics":["ci-cd","test-automation","testing"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/robiningelbrecht.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-03-18T17:52:48.000Z","updated_at":"2025-01-02T15:24:35.000Z","dependencies_parsed_at":"2022-09-13T15:10:57.449Z","dependency_job_id":null,"html_url":"https://github.com/robiningelbrecht/continuous-integration-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robiningelbrecht%2Fcontinuous-integration-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robiningelbrecht%2Fcontinuous-integration-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robiningelbrecht%2Fcontinuous-integration-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robiningelbrecht%2Fcontinuous-integration-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/robiningelbrecht","download_url":"https://codeload.github.com/robiningelbrecht/continuous-integration-example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249023731,"owners_count":21199960,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ci-cd","test-automation","testing"],"created_at":"2024-11-16T11:09:09.669Z","updated_at":"2025-04-15T07:11:37.079Z","avatar_url":"https://github.com/robiningelbrecht.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eCI/CD example\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/ci.png\" alt=\"CI/CD\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://github.com/robiningelbrecht/continuous-integration-example/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/actions/workflows/ci.yml/badge.svg\" alt=\"CI/CD\"\u003e\u003c/a\u003e\n\u003ca href=\"https://codecov.io/gh/robiningelbrecht/continuous-integration-example\"\u003e\u003cimg src=\"https://codecov.io/gh/robiningelbrecht/continuous-integration-example/branch/master/graph/badge.svg?token=9FEMHIZTZ0\" alt=\"codecov.io\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/robiningelbrecht/continuous-integration-example/blob/master/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/robiningelbrecht/continuous-integration-example?color=428f7e\u0026logo=open%20source%20initiative\u0026logoColor=white\" alt=\"License\"\u003e\u003c/a\u003e\n\u003ca href=\"https://phpstan.org/\"\u003e\u003cimg src=\"https://img.shields.io/badge/PHPStan-level%209-succes.svg?logo=php\u0026logoColor=white\u0026color=31C652\" alt=\"PHPStan Enabled\"\u003e\u003c/a\u003e\n\u003ca href=\"https://php.net/\"\u003e\u003cimg src=\"https://img.shields.io/packagist/php-v/robiningelbrecht/continuous-integration-example/dev-master?color=777bb3\u0026logo=php\u0026logoColor=white\" alt=\"PHP\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n------\n\nThis repository aims to build a fairly complete \n\u003ca href=\"https://en.wikipedia.org/wiki/CI/CD\" target=\"_blanks\"\u003eCI/CD\u003c/a\u003e \nexample using GitHub workflows and actions.\n\nKeep in mind that the toolset used in this repository is not the only solution \nto build a solid workflow. I'm sure there are many tools I have never heard of \nthat can get the job done as wel 💅.\n\nIf you liked this tutorial, please consider giving it a ⭐\n\n__Note__: This tutorial won't explain the complete [inner workings](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) of GitHub workflows and actions, so some basic knowledge is required. \n\n__Note 2__: Since I'm a PHP developer, all examples in this tutorial are PHP based.\nIt should be fairly easy to convert the workflows to be used with a \"non PHP\" code base.\n\n------\n\n\u003ch2\u003e🐣 Setting up the repository\u003c/h2\u003e\n\nBefore we get into the technical stuff, we first need to set up our repository.\nThe main thing we want to do is setting up the _default branch_\nand the _branch protection rules_. \n\n\u003ch3\u003eThe default branch\u003c/h3\u003e\n\nThe default branch is considered the “base” branch in your repository, \nagainst which all pull requests and code commits are automatically made, \nunless you specify a different branch. \n\nYou can configure the default branch by navigating to \n`https://github.com/username/repository/settings/branches`. You can set the default \nbranch to whatever you want, but usually \"_main_\" or \"_master_\" are used.\n\n\u003ch3\u003eBranch protection rules\u003c/h3\u003e\n\nBranch protection rules allow you to disable force pushing, prevent branches from being deleted, \nand optionally require status checks before merging. These checks are important to ensure\ncode quality and have a solid CI. For now, we will configure the bare minimum, \nbut we will get back to this.\n\nNavigate to `https://github.com/username/repository/settings/branches` and \nadd a new branch protection rule with following settings:\n\n* Branch name pattern: _the name of your default branch_\n* ✅ Require a pull request before merging\n* ✅ Require approvals\n* Required number of approvals before merging: _1_\n* ✅ Require status checks to pass before merging\n* ✅ Require branches to be up-to-date before merging\n\nAll other options should stay unchecked.\n\nThese rules will basically disable the ability to push to your default branch\nand force you to work with pull requests and code reviews.\n\n\u003ch3\u003eConfiguring issue \u0026 PR templates\u003c/h3\u003e\n\nWith [issue and pull request templates](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository), \nyou can customize and standardize the information you'd like contributors to include when \nthey open issues and pull requests in your repository.\n\nAs this as not a required step to set up your workflows, it's always a good idea to \nstandardize how users provide you with feedback about new features and bugs. It's up to \nyou (and your team) to decide if you want to use this feature.\n\n\u003ch2\u003e💎 Configuring the CI workflow\u003c/h2\u003e\n\nThe next step is configuring the CI workflow. The [workflow](https://github.com/robiningelbrecht/continuous-integration-example/blob/master/.github/workflows/ci.yml) \nused in this example contains two jobs that __should__ ensure code quality. It is triggered\nfor all pull requests:\n\n```yaml\non:\n  pull_request:\n  workflow_dispatch:\n```\n\nSince we configured that codes changes can only end up on the default branch via pull requests,\nwe are sure that the test suite will run for every new/changed line of code.\n\n\u003ch3\u003eRunning the test suite\u003c/h3\u003e\n\nLet's take a closer look at all steps configured in this job.\n\nFor the unit tests to be able to run, we need to install PHP (deuh). Later on \nwe'll need Xdebug as well to check and ensure code coverage.\n\n```yaml\n  # https://github.com/marketplace/actions/setup-php-action\n  - name: Setup PHP 8.1 with Xdebug 3.x\n    uses: shivammathur/setup-php@v2\n    with:\n      php-version: '8.1'\n      coverage: xdebug\n```\n\n\u003ch4\u003e🔥 PRO tip 🔥\u003c/h4\u003e\n\nIf you want to run your test suite against multiple PHP versions and/or \noperating systems you can do this by using a [matrix setup](https://github.com/robiningelbrecht/continuous-integration-example/blob/master/.github/workflows/ci-cd-matrix.yml):\n\n```yaml\n  name: Test suite PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }}\n  runs-on: ${{ matrix.operating-system }}\n  strategy:\n    matrix:\n      operating-system: ['ubuntu-latest', 'ubuntu-18.04']\n      php-versions: [ '7.4', '8.0', '8.1' ]\n  steps:\n    - name: Setup PHP ${{ matrix.php-versions }} with Xdebug 3.x\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: ${{ matrix.php-versions }}\n        coverage: xdebug\n```\n\nThis should result in a workflow run for all possible combinations in the matrix:\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/ci-matrix.png\" alt=\"CI matrix\" width=\"250\"\u003e\n\u003c/p\u003e\n\nThe next step is to pull in the code and install all dependencies\n\n```yaml\n  # https://github.com/marketplace/actions/checkout\n  - name: Checkout code\n    uses: actions/checkout@v2\n\n  - name: Install dependencies\n    run: composer install --prefer-dist\n```\n\nAfter which the tests can finally run\n\n```yaml\n  - name: Run test suite\n    run: vendor/bin/phpunit --testsuite unit --fail-on-incomplete  --log-junit junit.xml --coverage-clover clover.xml\n```\n\nYou probably noticed that the command to run the test contains some options.\nEach of them have a purpose:\n\n* __--fail-on-incomplete__: forces PHPUnit to fail on incomplete tests\n* __--log-junit junit.xml__: generates an XML file to publish the test results later on\n* __--coverage-clover clover.xml__: generates an XML file to check the test coverage later on\n\nAfter running the tests, we can visualize and publish them as a comment on the pull request.\n\n```yaml\n  # https://github.com/marketplace/actions/publish-unit-test-results\n  - name: Publish test results\n    uses: EnricoMi/publish-unit-test-result-action@v1.31\n    if: always()\n    with:\n      files: \"junit.xml\"\n      check_name: \"Unit test results\"\n```\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/unit-test-results.png\" alt=\"Unit test results\" width=\"500\"\u003e\n\u003c/p\u003e\n\nWe'll also send the generated `clover.xml` report to [codecov.io](https://about.codecov.io/)\n\n\u003e Codecov gives companies actionable coverage insights when and where \n\u003e they need them to ensure they are shipping quality code.\n\nCodecov.io basically allows you to check your code coverage and find untested code.\nIt does so by providing [fancy graphs and charts](https://app.codecov.io/gh/robiningelbrecht/continuous-integration-example).\n\n```yaml\n  # https://github.com/marketplace/actions/codecov\n  - name: Send test coverage to codecov.io\n    uses: codecov/codecov-action@v2.1.0\n    with:\n      files: clover.xml\n      fail_ci_if_error: true # optional (default = false)\n      verbose: true # optional (default = false)\n```\n\nThe codecov action also adds a comment on each pull request.\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/codecov-results.png\" alt=\"Codecov.io results\" width=\"500\"\u003e\n\u003c/p\u003e\n\nLast but not least we ensure a minimum test coverage of 90% across the project. \nIf the minimum coverage isn't reached, the job will fail.\nThis is done using [this test coverage checker](https://github.com/robiningelbrecht/phpunit-coverage-check).\n\n```yaml\n  - name: Check minimum required test coverage\n    run: |\n      CODE_COVERAGE=$(vendor/bin/coverage-checker clover.xml 90 --processor=clover-coverage)\n      echo ${CODE_COVERAGE}\n      if [[ ${CODE_COVERAGE} == *\"test coverage, got\"* ]] ; then\n        exit 1;\n      fi\n```\n\n\u003ch3\u003eStatic code analysis \u0026 coding standards\u003c/h3\u003e\n\nRunning static code analysis and applying coding standards are configured in a separate job\nbecause these don't need Xdebug or other fancy dependencies.\n\nTo run these tasks we'll use [PHPStan](https://phpstan.org/) \nand [PHP Coding Standards Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer)\n\n\u003e PHPStan focuses on finding errors in your code without actually running it. \n\u003e It catches whole classes of bugs even before you write tests for the code.\n\n\u003e The PHP Coding Standards Fixer (PHP CS Fixer) tool fixes your code to follow standards; \n\u003e whether you want to follow PHP coding standards as defined in the PSR-1, PSR-2, etc., \n\u003e or other community driven ones like the Symfony one.\n\nOnce again we need to install PHP, checkout the code and install dependencies\n\n```yaml\n  # https://github.com/marketplace/actions/setup-php-action\n  - name: Setup PHP 8.1\n    uses: shivammathur/setup-php@v2\n    with:\n      php-version: '8.1'\n\n  # https://github.com/marketplace/actions/checkout\n  - name: Checkout code\n    uses: actions/checkout@v2\n    \n  - name: Install dependencies\n    run: composer install --prefer-dist\n```\n\nAfter which we run the static code analyser\n\n```yaml\n  - name: Run PHPStan\n    run: vendor/bin/phpstan analyse\n```\n\nAnd check coding standards\n\n```yaml\n  - name: Run PHPcs fixer dry-run\n    run: vendor/bin/php-cs-fixer fix --dry-run --stop-on-violation --config=.php-cs-fixer.dist.php\n```\n\nThe job will fail if one of both tasks does not succeed.\n\nNow that the CI workflow has been configured, we can go back to the \nrepository branch protection rules and tighten them up by configuring extra \nrequired status checks:\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/protected-branch-settings.png\" alt=\"Protected branch settings\" width=\"500\"\u003e\n\u003c/p\u003e\n\nThese settings require both jobs in the CI workflow to succeed before the PR can be merged.\n\n\u003ch3\u003eExample pull requests\u003c/h3\u003e\n\nThere are some example pull requests to show the different reasons why a PR\ncan fail and what it takes for one to pass.\n\n* ❌ [Failed PR because of PHPStan](https://github.com/robiningelbrecht/continuous-integration-example/pull/3)\n* ❌ [Failed PR because of PHP coding standards](https://github.com/robiningelbrecht/continuous-integration-example/pull/4)\n* ❌ [Failed PR because of UnitTest](https://github.com/robiningelbrecht/continuous-integration-example/pull/5)\n* ❌ [Failed PR because of low test coverage](https://github.com/robiningelbrecht/continuous-integration-example/pull/6)\n* ✅ [A successful pull request](https://github.com/robiningelbrecht/continuous-integration-example/pull/7)\n\n\u003ch2\u003e🚀 Configuring the build \u0026 deploy workflow\u003c/h2\u003e\n\nAt this point new features and bug fixes can be \"safely\" merged to the main branch,\nbut they still need to be deployed to a remote server. The [workflow](https://github.com/robiningelbrecht/continuous-integration-example/blob/master/.github/workflows/build-deploy.yml)\nused in this example contains two jobs that take care of the deploy. It will be triggered manually:\n\n```yaml\n  on:\n    workflow_dispatch:\n```\n\n\u003ch3\u003eCreating a build\u003c/h3\u003e\n\nWe'll start of with creating a build by using artifacts. Before starting, we first need\nto check if the selected branch is allowed to be deployed:\n\n```yaml\n  build:\n    if: github.ref_name == 'master' || github.ref_name == 'development'\n    name: Create build ${{ github.run_number }} for ${{ github.ref_name }}\n```\n\nIf any other branch than `master` or `development` is selected the workflow will be aborted.\n\nTo create the build with the necessary files we first have to pull the dependencies again\n\n```yaml\n  # https://github.com/marketplace/actions/checkout\n  - name: Checkout code\n    uses: actions/checkout@v2\n\n  # https://github.com/marketplace/actions/setup-php-action\n  - name: Setup PHP 8.1\n    uses: shivammathur/setup-php@v2\n    with:\n      php-version: '8.1'\n\n  - name: Install dependencies\n    run: composer install --prefer-dist --no-dev\n```\n\nAfter which we can create an artifact that contains the files needed for a deploy.\n\n```yaml\n  # https://github.com/marketplace/actions/upload-a-build-artifact\n  - name: Create artifact\n    uses: actions/upload-artifact@v3\n    with:\n      name: release-${{ github.run_number }}\n      path: |\n        src/**\n        vendor/**\n```\n\nAll artifacts created during a workflow can be downloaded from the workflow summary page.\nThis can come in handy to \"debug\" your artifact and to check which files are actually included.\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/artifacts.png\" alt=\"Artifacts\" width=\"500\"\u003e\n\u003c/p\u003e\n\n\u003ch3\u003eDeploying to a remote server\u003c/h3\u003e\n\nThe next and final step is to deploy the build we created in the previous step. \nBefore we can do this, we first need to configure an [environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment). \n\nNavigate to `https://github.com/username/repository/settings/environments` to do this.\nIn this example we'll have an environment for `master` and `development` \non which we'll configure the following: \n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/environment-settings.png\" alt=\"Environment settings\" width=\"500\"\u003e\n\u003c/p\u003e\n\nThese settings will enforce that only the `development` branch can be deployed \nto the development environment. The secrets configured on the environment will be used\nto connect to the remote server during deploy.\n\nNow we're ready to start configuring the deploy job. We start off by\n\n* Referencing the build step. We cannot deploy before the build has been finished.\n* Referencing the environment we are deploying. This will\n  1. allow us to use the secrets configured on that environment\n  2. allow GitHub to validate that the correct branch is deployed to that environment \n  3. allow GitHub to indicate if a PR has been deployed (or not):\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://github.com/robiningelbrecht/continuous-integration-example/raw/master/readme/branch-deploy-info.png\" alt=\"Branch deploy info\"\u003e\n\u003c/p\u003e\n\nFYI: `${{ github.ref_name }}` contains the branch or tag the workflow is initialised with.\n\n```yaml\n  needs: build\n  environment:\n    name: ${{ github.ref_name }}\n    url: https://${{ github.ref_name }}.env\n```\n\nBy setting the `concurrency` we make sure only one deploy (per environment) at a time can be run.\n\n```yaml\n  concurrency: ${{ github.ref_name }}\n```\n\nThe first step in this job will download the artifact we created in the previous job.\nIt contains all the files that need to be transferred to the remote server.\n\n```yaml\n  # https://github.com/marketplace/actions/download-a-build-artifact\n  - name: Download artifact\n    uses: actions/download-artifact@v3\n    with:\n      name: release-${{ github.run_number }}\n```\n\nNext we'll use `rsync` to transfer al downloaded file to the server. This step uses \nthe secrets we have configured on our repository's [environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)\nto authenticate.\n\n```yaml\n  # https://github.com/marketplace/actions/rsync-deployments-action\n  - name: Rsync build to server\n    uses: burnett01/rsync-deployments@5.2\n    with:\n      switches: -avzr --delete\n      path: .\n      remote_path: /var/www/release-${{ github.run_number }}/\n      remote_host: ${{ secrets.SSH_HOST }}\n      remote_user: ${{ secrets.SSH_USERNAME }}\n      remote_key: ${{ secrets.SSH_KEY }}\n```\n\nOnce the files have been transferred, the last thing we need to do is run a deploy script.\nThis script can do a number of things depending on the stack you are using. In this example\nwe'll run some database updates and install a new crontab.\n\n```yaml\n  # https://github.com/marketplace/actions/ssh-remote-commands\n  - name: Run remote SSH commands\n    uses: appleboy/ssh-action@master\n    with:\n      host: ${{ secrets.HOST }}\n      username: ${{ secrets.USERNAME }}\n      key: ${{ secrets.KEY }}\n      port: ${{ secrets.PORT }}\n      script: |\n        RELEASE_DIRECTORY=/var/www/release-${{ github.run_number }}\n        CURRENT_DIRECTORY=/var/www/app\n        \n        # Remove symlink.\n        rm -r \"${CURRENT_DIRECTORY}\"\n        \n        # Create symlink to new release.\n        ls -s \"${RELEASE_DIRECTORY}\" \"${CURRENT_DIRECTORY}\"\n        \n        # Run database migrations\n        ${CURRENT_DIRECTORY}/bin/console doctrine:migrations:migrate\n        \n        # Install updated crontab\n        crontab ${RELEASE_DIRECTORY}/crontab\n        \n        # Clear cache\n        ${CURRENT_DIRECTORY}/bin/console cache:clear\n```\n\nAt this point new features and/or bug fixes are deployed to your remote server. You should be \ngood to go to repeat this cycle over and over and over again 😌\n\n\u003ch2\u003e🍔 Hungry for more?\u003c/h2\u003e\n\nThis example touches only a few aspects of continuous integration and continuous development.\nThere are lots of extra things I could have covered, but I wanted to keep this clean and simple.\n\n\u003ch3\u003eIntegration tests\u003c/h3\u003e\n\n\u003e Integration testing is the phase in software testing in which individual software modules \nare combined and tested as a group. \n\nThere are multiple frameworks out there that provide\na toolset to implement your integration tests, [codeception](https://codeception.com/) is one of them.\n\n\u003ch3\u003eEnd-to-end tests\u003c/h3\u003e\n\n\u003e End-to-end testing is a technique that tests the entire software product from beginning \nto end to ensure the application flow behaves as expected.\n\n[https://codecept.io/](https://codecept.io/) is one of many tools that provde a e2e testing framework.\n\n\u003ch3\u003eVisual regression tests\u003c/h3\u003e\n\n\u003e A visual regression test checks what the user will see after any code changes have \nbeen executed by comparing screenshots taken before and after deploys. \n\n[BackstopJS](https://github.com/garris/BackstopJS) is an open-source tool that allows \nyou to implement such checks.\n\n\u003ch3\u003eAuto deploy on merging\u003c/h3\u003e\n\nThis example handles deploys as a manual action, but it's possible to automate this.\nLet's assume you want to deploy every time something is merged, you can configure your \nworkflow to be triggered as following:\n\n```yaml\n  on:\n    push:\n      branches:\n        - master\n        - develop\n```\n\n\u003ch3\u003eSpeed up your test suite\u003c/h3\u003e\n\nAs your application and thus test suite grows, your workflows will take longer and longer \nto complete. There are several nifty tricks to speed up you test suite:\n\n* Use [Paratest](https://github.com/paratestphp/paratest) to run test in parallel\n* [Cache](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) your vendor dependencies\n* Use an in-memory SQLite database for tests that hit your database\n* Disable Xdebug, if you don't need test coverage\n\n\u003ch3\u003eComposite actions\u003c/h3\u003e\n\n[Composite actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action)\ncan be used to split workflows into smaller, reusable components. I could tell you all\nabout them, but [this blogpost](https://dev.to/jameswallis/using-github-composite-actions-to-make-your-workflows-smaller-and-more-reusable-476l) \ndoes a perfect job at explaining how to define and use them. Big up to the author James Wallis 👌\n\n\u003ch2\u003e🌈 Feedback and questions\u003c/h2\u003e\n\nAs I stated in the beginning, this is only one approach on how you could set up your CI/CD and\ndeploy flow. It's just an example to get you going. If you have any feedback or suggestions to \nimprove this tutorial, please let me know. I'm always open to learning new approaches and \ngetting to know new tools.\n\nIf you have any questions, feel free to 📭 [contact me](https://www.linkedin.com/in/robin-ingelbrecht/), \nI'll be glad to help you out.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobiningelbrecht%2Fcontinuous-integration-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobiningelbrecht%2Fcontinuous-integration-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobiningelbrecht%2Fcontinuous-integration-example/lists"}