{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreidovmj7yxvfum2l7t3mbhs4uyxzjxwangp2jiiat6vjpvim3ppm5a",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpubivfq4ng2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiaaiqimm6zelw5uj3y57qyb7krcu7rr7he4qflxpllny4g4iijnwm"
    },
    "mimeType": "image/webp",
    "size": 81710
  },
  "path": "/dayan_elvisjahuirapilco/cicd-testing-tools-compared-the-same-pipeline-in-github-actions-gitlab-ci-and-jenkins-53",
  "publishedAt": "2026-07-04T23:54:20.000Z",
  "site": "https://dev.to",
  "tags": [
    "cicd",
    "devops",
    "testing",
    "tutorial",
    "github.com/Dayan-18/ci-tools-demo →",
    "SAST scans",
    "IaC scans",
    "API tests",
    "repo",
    "github.com/Dayan-18/ci-tools-demo",
    "SAST with Bandit",
    "IaC scanning with Checkov",
    "API testing with pytest",
    "GitHub Actions docs",
    "GitLab CI docs",
    "Jenkins Pipeline docs",
    "Tekton",
    "CircleCI",
    "@pytest.mark.parametrize"
  ],
  "textContent": "> **TL;DR:** I compared the major CI/CD tools for test automation (GitHub Actions, GitLab CI, Jenkins, CircleCI, Travis CI, TeamCity, Bitbucket Pipelines, Tekton, Harness) — and instead of stopping at a table, I wrote the **exact same test pipeline three times** : once for GitHub Actions (running live), once for GitLab CI, and once as a Jenkinsfile, all in one public repo so you can compare the syntax line by line. Repo: **github.com/Dayan-18/ci-tools-demo →**\n\n##  Why CI is where your tests actually live\n\nA test suite that runs only on your laptop protects exactly one person: you. The moment tests run automatically on every push and pull request, they protect the whole team — and that's the job of a CI/CD tool. In my previous articles I used GitHub Actions to automate SAST scans, IaC scans and API tests. But GitHub Actions is one option among many. How do the alternatives compare?\n\n##  The landscape\n\nTool | Hosting | Config | Pricing (open source) | Standout feature | Watch out for\n---|---|---|---|---|---\n**GitHub Actions** | SaaS (+ self-hosted runners) | YAML in repo | Free for public repos | Marketplace with 20k+ reusable actions | Vendor lock-in to GitHub\n**GitLab CI** | SaaS or self-hosted | YAML in repo | Free tier + minutes | Single tool: repo, CI, registry, environments | Complex YAML at scale\n**Jenkins** | Self-hosted | Jenkinsfile (Groovy) | 100% free, open source | Total control, 1,900+ plugins | You maintain the server, plugins break\n**CircleCI** | SaaS | YAML | Free tier | Very fast, first-class Docker | Credits model gets pricey\n**Travis CI** | SaaS | YAML | Limited free | Historic pioneer for OSS | Lost most mindshare after pricing changes\n**TeamCity** | Self-hosted or Cloud | UI or Kotlin DSL | Free tier | Deep JetBrains IDE integration | Heavier setup\n**Bitbucket Pipelines** | SaaS | YAML | Free tier | Native Jira/Bitbucket integration | Only useful inside Atlassian stack\n**Tekton** | Kubernetes | CRDs (YAML) | Free, CNCF | Cloud-native building blocks | Steep learning curve; it's a framework, not a product\n**Harness** | SaaS | YAML/UI | Free tier | AI-assisted pipelines, deployment verification | Enterprise-oriented complexity\n\nTables are nice, but syntax is what you live with every day. So let's compare the three most representative tools **with real code** : the same pipeline — install dependencies, run a pytest suite, publish a JUnit report — written three times.\n\n##  The test suite (identical for all three)\n\nA small shopping-cart calculator with 10 tests (happy paths, error paths, parametrized cases):\n\n\n\n    @pytest.mark.parametrize(\"amount,percent,expected\", [\n        (100.0, 0, 100.0),\n        (100.0, 10, 90.0),\n        (100.0, 100, 0.0),\n        (59.99, 25, 44.99),\n    ])\n    def test_apply_discount(amount, percent, expected):\n        assert apply_discount(amount, percent) == expected\n\n\nRunning locally:\n\n\n\n    test_calculator.py::test_subtotal_sums_price_times_quantity PASSED       [ 10%]\n    test_calculator.py::test_subtotal_empty_cart_is_zero PASSED              [ 20%]\n    test_calculator.py::test_subtotal_rejects_negative_values PASSED         [ 30%]\n    test_calculator.py::test_apply_discount[100.0-0-100.0] PASSED            [ 40%]\n    test_calculator.py::test_apply_discount[100.0-10-90.0] PASSED            [ 50%]\n    test_calculator.py::test_apply_discount[100.0-100-0.0] PASSED            [ 60%]\n    test_calculator.py::test_apply_discount[59.99-25-44.99] PASSED           [ 70%]\n    test_calculator.py::test_apply_discount_rejects_invalid_percent PASSED   [ 80%]\n    test_calculator.py::test_total_with_tax_default_18_percent PASSED        [ 90%]\n    test_calculator.py::test_total_with_tax_custom_rate PASSED               [100%]\n\n    ============================== 10 passed ==============================\n\n\nNow, the same automation in three dialects.\n\n##  Round 1: GitHub Actions (`.github/workflows/tests.yml`)\n\n\n    name: Tests (GitHub Actions)\n\n    on:\n      push:\n        branches: [main]\n      pull_request:\n        branches: [main]\n\n    jobs:\n      test:\n        runs-on: ubuntu-latest\n        strategy:\n          matrix:\n            python-version: [\"3.11\", \"3.12\"]\n        steps:\n          - uses: actions/checkout@v4\n          - uses: actions/setup-python@v5\n            with:\n              python-version: ${{ matrix.python-version }}\n          - name: Install dependencies\n            run: pip install -r requirements.txt\n          - name: Run tests\n            run: pytest test_calculator.py -v --junitxml=report-${{ matrix.python-version }}.xml\n          - uses: actions/upload-artifact@v4\n            if: always()\n            with:\n              name: test-report-${{ matrix.python-version }}\n              path: report-${{ matrix.python-version }}.xml\n\n\nNotice the **matrix** : two Python versions tested in parallel with four extra lines. Reusable actions (`actions/setup-python`) replace what would be manual scripting elsewhere. This one is **running live** in the repo — check the Actions tab.\n\n##  Round 2: GitLab CI (`.gitlab-ci.yml`)\n\n\n    stages:\n      - test\n\n    .test_template: &test_template\n      stage: test\n      before_script:\n        - pip install -r requirements.txt\n      script:\n        - pytest test_calculator.py -v --junitxml=report.xml\n      artifacts:\n        when: always\n        reports:\n          junit: report.xml\n\n    test:python3.11:\n      <<: *test_template\n      image: python:3.11\n\n    test:python3.12:\n      <<: *test_template\n      image: python:3.12\n\n\nTwo things stand out. First, jobs declare a Docker **image** directly — the container _is_ the environment, no setup action needed. Second, `reports: junit` is built into the platform: failed tests appear annotated in the merge request UI. The version matrix, though, requires YAML anchors (`&template`/`<<:`) — more manual than Actions' `matrix`.\n\n##  Round 3: Jenkins (`Jenkinsfile`)\n\n\n    pipeline {\n        agent {\n            docker { image 'python:3.12' }\n        }\n\n        stages {\n            stage('Install') {\n                steps {\n                    sh 'pip install -r requirements.txt'\n                }\n            }\n\n            stage('Test') {\n                steps {\n                    sh 'pytest test_calculator.py -v --junitxml=report.xml'\n                }\n            }\n        }\n\n        post {\n            always {\n                junit 'report.xml'\n            }\n        }\n    }\n\n\nJenkins is the only one here that is **not YAML** — it's a Groovy DSL, which means real programming (variables, conditionals, shared libraries) when you need it. The `post { always { junit ... } }` block is Jenkins' classic test-report integration. The trade-off is operational: GitHub and GitLab run your pipeline on their servers; with Jenkins, _you_ run the server, the agents, the plugins, and the upgrades.\n\n##  What the comparison actually teaches\n\nReading the three files side by side (they're all in the repo), the pattern is obvious: **every CI tool automates the same four verbs — trigger, environment, steps, report.** The differences are dialect and philosophy: Actions optimizes for reusable building blocks, GitLab for platform integration, Jenkins for control and extensibility. The niche players follow the same verbs too: Tekton expresses them as Kubernetes resources, Bitbucket Pipelines as Atlassian-flavored YAML, Harness wraps them in an enterprise UI.\n\nMy recommendation for choosing: if your code lives on GitHub, use Actions; on GitLab, use GitLab CI — fighting your platform's native tool is rarely worth it. Choose Jenkins when you need on-premise control or heavy customization, and Tekton only if your team already breathes Kubernetes.\n\n##  Conclusion\n\n\"Which CI tool should I learn?\" is less important than understanding the shared model: trigger → environment → steps → report. Learn it once, and every tool becomes a syntax lookup. The full working example — one suite, three pipelines — is public here: **github.com/Dayan-18/ci-tools-demo** , with GitHub Actions running it live on every push.\n\n_Which CI tool does your team use — and would you switch? Tell me in the comments!_ 👇\n\n_Previous articles in this series: SAST with Bandit · IaC scanning with Checkov · API testing with pytest_\n\n_References: GitHub Actions docs · GitLab CI docs · Jenkins Pipeline docs · Tekton · CircleCI_",
  "title": "CI/CD Testing Tools Compared: the Same Pipeline in GitHub Actions, GitLab CI and Jenkins"
}