[{"data":1,"prerenderedAt":720},["ShallowReactive",2],{"/en-us/blog/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd/":3,"navigation-en-us":36,"banner-en-us":465,"footer-en-us":482,"Michael Friedrich":692,"next-steps-en-us":705},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":26,"_id":29,"_type":30,"title":31,"_source":32,"_file":33,"_stem":34,"_extension":35},"/en-us/blog/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"How to continuously test web apps and APIs with Hurl and GitLab CI/CD","Hurl as a CLI tool can be integrated into the DevSecOps platform to continuously verify, test, and monitor targets. It also offers integrated unit test reports in GitLab CI/CD.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659883/Blog/Hero%20Images/post-cover-image.jpg","https://about.gitlab.com/blog/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"How to continuously test web apps and APIs with Hurl and GitLab CI/CD\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Michael Friedrich\"}],\n        \"datePublished\": \"2022-12-14\",\n      }",{"title":9,"description":10,"authors":17,"heroImage":11,"date":19,"body":20,"category":21,"tags":22},[18],"Michael Friedrich","2022-12-14","Testing websites, web applications, or generally everything reachable with\nthe HTTP protocol, can be a challenging exercise. Thanks to tools like\n`curl` and `jq`, [DevOps workflows have become more\nproductive](/blog/devops-workflows-json-format-jq-ci-cd-lint/)\nand even simple monitoring tasks can be automated with CI/CD pipeline\nschedules. Sometimes, use cases require specialized tooling with custom HTTP\nheaders, parsing expected responses, and building end-to-end test pipelines.\nStressful incidents also need good and fast tools that help analyze the root\ncause and quickly mitigate and fix problems.\n\n\n[Hurl](https://hurl.dev) is an open-source project developed and maintained\nby Orange, and uses libcurl from curl to provide HTTP test capabilities. It\naims to tackle complex HTTP test challenges by providing a simple plain text\nconfiguration to describe HTTP requests. It can chain requests, capture\nvalues, and evaluate queries on headers and body responses. So far, so good:\nHurl does not only support fetching data, it can be used to test HTTP\nsessions and XML (SOAP) and JSON (REST) APIs.\n\n\n## Getting Started\n\n\nHurl comes in various package formats to\n[install](https://hurl.dev/docs/installation.html). On macOS, a Homebrew\npackage is available.\n\n\n```sh\n\n$ brew install hurl\n\n```\n\n\n## First steps with Hurl\n\n\nHurl proposes to start with the configuration file format first, which is a\ngreat way to learn the syntax step by step. The following example creates a\nnew `gitlab-contribute.hurl` configuration file that will do two things:\nexecute a GET HTTP request on\n`https://about.gitlab.com/community/contribute/` and check whether its HTTP\nresponse contains the HTTP protocol `2` and status code `200` (OK).\n\n\n```sh\n\n$ vim gitlab-contribute.hurl\n\n\nGET https://about.gitlab.com/community/contribute/\n\n\nHTTP/2 200\n\n$ hurl --test gitlab-contribute.hurl\n\ngitlab-contribute.hurl: Running [1/1]\n\ngitlab-contribute.hurl: Success (1 request(s) in 413 ms)\n\n--------------------------------------------------------------------------------\n\nExecuted files:  1\n\nSucceeded files: 1 (100.0%)\n\nFailed files:    0 (0.0%)\n\nDuration:        415 ms\n\n```\n\n\nInstead of creating configuration files, you can also use the `echo “...” |\nhurl` command pattern. The following command tests against about.gitlab.com\nand checks whether the HTTP response protocol is 1.1 and the status is OK\n(200). The two newline characters `\\n` are required for separation.\n\n\n```sh\n\n$ echo \"GET https://about.gitlab.com\\n\\nHTTP/1.1 200\" | hurl --test\n\n```\n\n\n![hurl CLI run against about.gitlab.com, failed\nrequest](https://about.gitlab.com/images/blogimages/hurl-continuous-website-testing/hurl_assert_failure.png)\n\n\nThe command failed, and it says that the response protocol version is\nactually `2`. Let's adjust the test run to expect `HTTP/2`:\n\n\n```sh\n\necho \"GET https://about.gitlab.com\\n\\nHTTP/2 200\" | hurl --test\n\n```\n\n## Asserting HTTP responses\n\n\nHurl allows defining\n[assertions](https://hurl.dev/docs/asserting-response.html) to control when\nthe tests fail. These can be defined for different HTTP response types:\n\n\n- Expected HTTP protocol version and status\n\n- Headers\n\n- Body\n\n\nThe configuration language allows users to define queries with predicates\nthat allow to compare, chain, and execute different assertions.\n\n\nThis is the easiest way to verify that the HTTP response contains what is\nexpected to be a string or sentence on the website, for example. If the\nstring does not exist, this can indicate that it was changed unexpectedly,\nor that the website is down. Let's revisit the example with testing GET\nhttps://about.gitlab.com/community/contribute/ and add an expected string\n`Everyone can contribute` as a new assertion, `body contains \u003Cstring>` is\nthe expected configuration syntax for [body\nasserts](https://hurl.dev/docs/asserting-response.html#body-assert).\n\n\n```sh\n\n$ vim gitlab-contribute.hurl\n\n\nGET https://about.gitlab.com/community/contribute/\n\n\nHTTP/2 200\n\n\n[Asserts]\n\nbody contains \"Everyone should contribute\"\n\n\n$ hurl --test gitlab-contribute.hurl\n\n```\n\n\n**Exercise:** Fix the test by updating the asserts line to `Everyone can\ncontribute` and run Hurl again.\n\n\n### Asserting responses: JSON and XML\n\n\n[JSONPath](https://hurl.dev/docs/asserting-response.html#jsonpath-assert)\nautomatically parses the JSON response (a built-in `jq with curl` parser so\nto speak), and allows users to compare the value to verify the asserts (more\nbelow). The XML format can be found in an [RSS feed on\nabout.gitlab.com](https://about.gitlab.com/atom.xml) and parsed using\n[XPath](https://hurl.dev/docs/asserting-response.html#xpath-assert). The\nfollowing example from `atom.xml` should be verified with Hurl:\n\n\n```xml\n\n\u003Cfeed xmlns=\"http://www.w3.org/2005/Atom\">\n\n\u003Ctitle>GitLab\u003C/title>\n\n\u003Cid>https://about.gitlab.com/blog\u003C/id>\n\n\u003Clink href=\"https://about.gitlab.com/blog/\"/>\n\n\u003Cupdated>2022-11-21T00:00:00+00:00\u003C/updated>\n\n\u003Cauthor>\n\n\u003Cname>The GitLab Team\u003C/name>\n\n\u003C/author>\n\n\u003Centry>\n\n...\n\n\u003C/entry>\n\n\u003Centry>\n\n...\n\n\u003C/entry>\n\n\u003Centry>\n\n…\n\n```\n\n\nIt is important to note that XML namespaces need to be specified for\nparsing. Hurl allows users to replace the first default namespace with the\n`_` character to avoid adding `http://www.w3.org/2005/Atom` everywhere, the\nXPath is now shorter with `string(//_:feed/_:entry)` to get a list of all\nentries. This value is captured in the `entries` variable, which can be\ncompared to match a specific string, `GitLab` in this example. Additionally,\nthe feed id and author name is checked.\n\n\n```\n\n$ vim gitlab-rss.hurl\n\n\nGET https://about.gitlab.com/atom.xml\n\n\nHTTP/2 200\n\n\n[Captures]\n\nentries: xpath \"string(//_:feed/_:entry)\"\n\n\n[Asserts]\n\nvariable \"entries\" matches \"GitLab\"\n\n\nxpath \"string(//_:feed/_:id)\" == \"https://about.gitlab.com/blog\"\n\nxpath \"string(//_:feed/_:author/_:name)\" == \"The GitLab Team\"\n\n\n$ hurl –test gitlab-rss.hurl\n\n```\n\n\nHurl allows users to capture the value from responses into\n[variables](https://hurl.dev/docs/templates.html#variables) that can be used\nlater. This method can also be helpful to model end-to-end testing\nworkflows: First, check the website health status and retrieve a CSRF token,\nand then try to log into the website by sending the token again.\n\n\nREST APIs that are expected to always return a specified field, or\nmonitoring a website health state [becomes a breeze using\nHurl](https://hurl.dev/docs/tutorial/chaining-requests.html#test-rest-api).\n\n\n## Use Hurl in GitLab CI/CD jobs\n\n\nThe easiest way to integrate Hurl into GitLab CI/CD is to use the official\ncontainer image. The Hurl project provides a [container image on Docker\nHub](https://hub.docker.com/r/orangeopensource/hurl), which did not work in\nCI/CD at first glance. After talking with the maintainers, the [entrypoint\noverride](https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#override-the-entrypoint-of-an-image)\nwas identified as a solution for using the image in GitLab CI/CD. Note that\nthe Alpine based image uses the libcurl library that does not support HTTP/2\nyet - the test results are different to a Debian base image (follow [this\nissue report](https://github.com/Orange-OpenSource/hurl/issues/1082) for the\nproblem analysis).\n\n\nThe following example is kept short to run the container image, override the\nentrypoint, and run Hurl with passing in the test using the `echo` CLI\ncommand.\n\n\n```yaml\n\nhurl-standalone:\n  image:\n    name: ghcr.io/orange-opensource/hurl:latest\n    entrypoint: [\"\"]\n  script:\n    - echo -e \"GET https://about.gitlab.com/community/contribute/\\n\\nHTTP/1.1 200\" | hurl --test --color\n```\n\n\nThe Hurl test report is printed into the CI/CD job trace log, and returns\nsuccesfully.\n\n\n```sh\n\n$ echo -e \"GET https://about.gitlab.com/community/contribute/\\n\\nHTTP/1.1\n200\" | hurl --test --color\n\n-: Running [1/1]\n\n-: Success (1 request(s) in 280 ms)\n\n--------------------------------------------------------------------------------\n\nExecuted files:  1\n\nSucceeded files: 1 (100.0%)\n\nFailed files:    0 (0.0%)\n\nDuration:        283 ms\n\nCleaning up project directory and file based variables\n\n00:00\n\nJob succeeded\n\n```\n\n\nThe next iteration is to create a CI/CD job template that provides generic\nattributes, and allows users to dynamically run the job with an environment\nvariable called `HURL_URL`.\n\n\n```yaml\n\n# Hurl job template\n\n.hurl-tmpl:\n  # Use the upstream container image and override the ENTRYPOINT to run CI/CD script\n  # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#override-the-entrypoint-of-an-image\n  image:\n    name: ghcr.io/orange-opensource/hurl:1.8.0\n    entrypoint: [\"\"]\n  variables:\n    HURL_URL: \"about.gitlab.com/community/contribute/\"\n  script:\n    - echo -e \"GET https://${HURL_URL}\\n\\nHTTP/1.1 200\" | hurl --test --color\n\nhurl-about-gitlab-com:\n  extends: .hurl-tmpl\n  variables:\n    HURL_URL: \"about.gitlab.com/jobs/\"\n```\n\n\nRunning GET commands with expected HTTP results is not the only use case,\nand the Hurl maintainers thought about this already. The next section\nexplains how to create a custom container image; you can skip to the\n[DevSecOps workflows](#devSecOps-workflows-with-hurl) section to learn more\nabout efficient Hurl configuration use cases.\n\n\n### Custom container image with Hurl\n\n\nMaintaining and building a custom container image adds more work, but also\nhelps with avoiding running unknown container images in CI/CD pipelines. The\nlatter is often a requirement for compliance and security. _Since the Hurl\nDebian package supports detecting HTTP/2 as a protocol, this blog post will\nfocus on building a custom image, and run all tests using this image. If you\nplan on using the upstream container image, make sure to review the test\nconfiguration for the HTTP protocol version detection._\n\n\nThe Hurl documentation provides multiple ways to install Hurl. For this\nexample, Debian 11 Bullseye (slim) is used. Hurl comes with a package\ndependency on `libxml2` which can either be installed manually with then\nrunning the `dpkg` command, or by using `apt install` to install a local\npackage and automatically resolve the dependencies.\n\n\nThe following CI/CD example uses a job template which defines the Hurl\nversion as environment variable to avoid repetitive use, and downloads and\ninstalls the Hurl Debian package. The `hurl-gitlab-com` job extends the\nCI/CD job template and runs a one-line test against `https://gitlab.com` and\nexpects to return `HTTP/2` as HTTP protocol version, and `200` as status.\n\n\n```yaml\n\n# CI/CD job template\n\n.hurl-tmpl:\n  variables:\n    HURL_VERSION: 1.8.0\n  before_script:\n    - DEBIAN_FRONTEND=noninteractive apt update && apt -y install jq curl ca-certificates\n    - curl -LO \"https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}/hurl_${HURL_VERSION}_amd64.deb\"\n    - DEBIAN_FRONTEND=noninteractive apt -y install \"./hurl_${HURL_VERSION}_amd64.deb\"\n\nhurl-gitlab-com:\n  extends: .hurl-tmpl\n  script:\n    - echo -e \"GET https://gitlab.com\\n\\nHTTP/2 200\" | hurl --test --color\n```\n\n\nThe next section describes how to optimize the CI/CD pipelines for more\nefficient schedules and runs to monitor websites and not waste too many\nresources and CI/CD minutes. You can also skip it and [scroll down to more\nadvanced Hurl examples in GitLab CI/CD](#devsecops-workflows-with-hurl).\n\n\n### CI/CD efficiency: Hurl container image\n\n\nThe installation steps for Hurl, and its dependencies, can waste resources\nand increase the pipeline job runtime every time. To make the CI/CD\npipelines more efficient, we want to use a container image that already\nprovides Hurl pre-installed. The following steps are required for creating a\ncontainer image:\n\n\n- Use Debian 11 Slim (FROM).\n\n- Install dependencies to download Hurl (`curl`, `ca-certificates`). `jq` is\ninstalled for convenience to access it from CI/CD commands when needed\nlater.\n\n- Download the Hurl Debian package, and use `apt install` to install its\ndependencies automatically.\n\n- Clear the apt lists cache to enforce apt update again, and avoid security\nissues.\n\n- Hurl is installed into the PATH, specify the default command being run.\nThis allows running the container without having to specify a command.\n\n\nThe steps to install the packages are separated for better readability; an\noptimization for the `docker-build` job can happen by chaining the `RUN`\ncommands into one long command.\n\n\n`Dockerfile`\n\n```\n\nFROM debian:11-slim\n\n\nENV DEBIAN_FRONTEND noninteractive\n\n\nARG HURL_VERSION=1.8.0\n\n\nRUN apt update && apt install -y curl jq ca-certificates\n\nRUN curl -LO\n\"https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}/hurl_${HURL_VERSION}_amd64.deb\"\n\n# Use apt install to determine package dependencies instead of dpkg\n\nRUN apt -y install \"./hurl_${HURL_VERSION}_amd64.deb\"\n\nRUN rm -rf /var/lib/apt/lists/*\n\n\nCMD [\"hurl\"]\n\n```\n\n\nNote that the `HURL_VERSION` variable can be overridden by passing the\nvariable and value into the container build job later. It is intentionally\nnot using an automated script that always uses the [latest\nrelease](https://github.com/Orange-OpenSource/hurl/releases) to avoid\nbreaking the behavior, and enforces a controlled upgrade cycle for container\nimages in production.\n\n\nOn GitLab.com SaaS, you can include the `Docker.gitlab-ci.yml` CI/CD\ntemplate which will automatically detect the `Dockerfile` file and start\nbuilding the image using the shared runners, and push it to the [GitLab\ncontainer\nregistry](https://docs.gitlab.com/ee/user/packages/container_registry/). For\nself-managed instances or own runners on GitLab.com SaaS, it is recommended\nto decide whether to use and setup\n[Docker-in-Docker](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)\nor [Kaniko](https://docs.gitlab.com/ee/ci/docker/using_kaniko.html), Podman,\nor other container image build tools.\n\n\n```yaml\n\ninclude:\n  - template: Docker.gitlab-ci.yml\n```\n\n\nTo avoid running the Docker image build job every time, the job override\ndefinition specifies to [run it\nmanually](https://docs.gitlab.com/ee/ci/yaml/#when). You can also use rules\nto [choose when to run the\njob](https://docs.gitlab.com/ee/ci/jobs/job_control.html), only when a Git\ntag is pushed for example.\n\n\n```yaml\n\ninclude:\n  - template: Docker.gitlab-ci.yml\n\n# Change Docker build to manual non-blocking\n\ndocker-build:\n  rules:\n    - if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'\n      when: manual\n      allow_failure: true\n```\n\n\nOnce the container image is pushed to the registry, navigate into `Packages\nand Registries > Container Registries` and inspect the tagged image. Copy\nthe image path for the latest tagged version and use it for the `image`\nattribute in the CI/CD job configuration.\n\n\n### Hurl container image in GitLab CI/CD example\n\n\nThe full example uses the previously built container image, and specifies\nthe default `HURL_URL` variable. This can later be overridden by job\ndefinitions.\n\n\n_Please note that the image URL\n`registry.gitlab.com/everyonecancontribute/dev/hurl-playground:latest` is\nonly used for demo purposes and not actively maintained or updated._\n\n\n```yaml\n\ninclude:\n  - template: Docker.gitlab-ci.yml\n\n# Change Docker build to manual non-blocking\n\ndocker-build:\n  rules:\n    - if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'\n      when: manual\n      allow_failure: true\n\n# Hurl job template\n\n.hurl-tmpl:\n  image: registry.gitlab.com/everyonecancontribute/dev/hurl-playground:latest\n  variables:\n    HURL_URL: gitlab.com\n\n# Hurl jobs that check websites\n\nhurl-dnsmichi-at:\n  extends: .hurl-tmpl\n  variables:\n    HURL_URL: dnsmichi.at\n  script:\n    - echo -e \"GET https://${HURL_URL}\\n\\nHTTP/1.1 200\" | hurl --test --color\n\nhurl-opsindev-news:\n  extends: .hurl-tmpl\n  variables:\n    HURL_URL: opsindev.news\n  script:\n    - echo -e \"GET https://${HURL_URL}\\n\\nHTTP/2 200\" | hurl --test --color\n```\n\n\nThe CI/CD configuration can further be optimized:\n\n\n- Create job templates that execute the same scripts and only differ in the\n`HURL_URL` variable.\n\n- Use Hurl configuration files that allow specifying variables on the CLI or\nas environment variables. More on this in the next section.\n\n\n## DevSecOps workflows with Hurl\n\n\nHurl allows users to describe HTTP instructions in a configuration file with\nthe `.hurl` suffix. You can add the configuration files to Git, and review\nand approve changes in merge requests - with the changes run in CI/CD and\nreporting back any failures before merging.\n\n\nInspect the `use-cases/` directory in the [example\nproject](https://gitlab.com/everyonecancontribute/dev/hurl-playground), and\nfork it to make changes and commit and run the CI/CD pipelines and reports.\nYou can also clone the project and run the `tree` command in the terminal.\n\n\n```sh\n\n$ tree use-cases\n\nuse-cases\n\n├── dnsmichi.at.hurl\n\n├── gitlab-com-api.hurl\n\n├── gitlab-contribute.hurl\n\n└── hackernews.hurl\n\n```\n\n\nHurl supports the glob option which collects all configuration files\nmatching a specific pattern.\n\n\n![Hurl configuration file\nrun](https://about.gitlab.com/images/blogimages/hurl-continuous-website-testing/hurl_multiple_config_files_run.png)\n\n\n### Chaining requests\n\n\nSimilar to CI/CD pipelines, jobs, and stages, testing HTTP endpoints with\nHurl can require multiple steps. First, ping the website for being\nreachable, and then try parsing expected results. Separating the\nrequirements into two steps helps to analyze errors.\n\n\n- HTTP endpoint reachable, but expected string not in response - static\nwebsite was changed, REST API misses a field, etc.\n\n- HTTP endpoint is unreachable, don’t try to understand why the follow-up\ntests fail.\n\n\nThe following example first sends a ping probe to the dev instance, and a\ncheck towards the production environment in the second request.\n\n\n```sh\n\n$ vim use-cases/everyonecancontribute-com.hurl\n\n\nGET https://everyonecancontribute.dev\n\n\nHTTP/2 200\n\n\nGET https://everyonecancontribute.com\n\n\nHTTP/2 200\n\n$ hurl --test use-cases/everyonecancontribute-com.hurl\n\n```\n\n\nIn this scenario, the TLS certificate of the dev instance expired, and Hurl\nhalts the test immediately.\n\n\n![Hurl chained requests, failing the first test with TLS certificate\nproblems](https://about.gitlab.com/images/blogimages/hurl-continuous-website-testing/hurl_chained_request_fail.png)\n\n\n### Hurl reports as JUnit test reports\n\n\nTreat website monitoring and web app tests as unit and end-to-end tests. The\nHurl developers thought of that too - the CLI command provides different\noutput options for the report: `--report-junit \u003Coutputpath>` integrates with\n[GitLab JUnit\nreport](https://docs.gitlab.com/ee/ci/testing/unit_test_reports.html)\nsupport into merge requests and pipeline views.\n\n\nThe following configuration generates a JUnit report file into the value of\nthe `HURL_JUNIT_REPORT` variable. It exists to avoid typing the path three\ntimes. The Hurl tests are run from the `use-cases/` directory using a glob\npattern.\n\n\n```yaml\n\n# Hurl job template\n\n.hurl-tmpl:\n    image: registry.gitlab.com/everyonecancontribute/dev/hurl-playground:latest\n    variables:\n        HURL_URL: gitlab.com\n        HURL_JUNIT_REPORT: hurl_junit_report.xml\n\n# Hurl tests from configuration file, generating JUnit report integration in\nGitLab CI/CD\n\nhurl-report:\n    extends: .hurl-tmpl\n    script:\n      - hurl --test use-cases/*.hurl --report-junit $HURL_JUNIT_REPORT\n    after_script:\n      # Hack: Workaround for 'id' instead of 'name' in JUnit report from Hurl. https://gitlab.com/gitlab-org/gitlab/-/issues/299086\n      - sed -i 's/id/name/g' $HURL_JUNIT_REPORT\n    artifacts:\n      when: always\n      paths:\n        - $HURL_JUNIT_REPORT\n      reports:\n        junit: $HURL_JUNIT_REPORT\n```\n\n\nThe JUnit format returned by Hurl 1.8.0 defines the `id` attribute, but the\nGitLab JUnit integration expects the `name` attribute to be present. While\nwriting this blog post, [the problem was\ndiscussed](https://github.com/Orange-OpenSource/hurl/issues/1067#issuecomment-1343264751)\nwith the maintainers, and [the `name` attribute was\nimplemented](https://github.com/Orange-OpenSource/hurl/issues/1078) and will\nbe available in future releases. As a workaround with Hurl 1.8.0, the CI/CD\n[after_script](https://docs.gitlab.com/ee/ci/yaml/#after_script) section\nuses `sed` to replace the attributes after generating the report.\n\n\nThe [following\nexample](https://gitlab.com/everyonecancontribute/dev/hurl-playground/-/merge_requests/10)\nfails on purpose with checking a different HTTP protocol version.\n\n\n```\n\nGET https://opsindev.news\n\n\n# This will fail on purpose\n\nHTTP/1.1 200\n\n\n[Asserts]\n\nbody contains \"Michael Friedrich\"\n\n```\n\n\n![Hurl test report in JUnit format integrated into\nGitLab](https://about.gitlab.com/images/blogimages/hurl-continuous-website-testing/hurl_gitlab_junit_integration_merge_request_widget_overlay.png)\n\n\nOnce the JUnit integration with Hurl tests from a glob pattern work, you can\ncontinue adding new `.hurl` configuration files to the GitLab repository and\nstart testing in MRs, which will require review and approval workflows for\nproduction then.\n\n\n### Web review apps\n\n\nWebsite monitoring is only one aspect of using Hurl: Testing web\napplications deployed in review environments in the cloud, and in\ncloud-native clusters provides a native integration into\n[DevSecOps](https://about.gitlab.com/topics/devsecops/) workflows. The CI/CD\npipelines will fail when Hurl tests are failing, and more insights are\nprovided using merge request widgets reports.\n\n\n[Cloud Seed](https://docs.gitlab.com/ee/cloud_seed/index.html) provides the\nability to deploy a web application to a major cloud provider, for example\nGoogle Cloud. After the deployment is successful, additional CI/CD jobs can\nbe configured that verify that the deployed web app version does not\nintroduce a regression, and provides all required data elements, API\nendpoints, etc. A similar workflow can be achieved by using review app\nenvironments with [webservers (Nginx, etc.), Docker, AWS, and\nKubernetes](https://docs.gitlab.com/ee/ci/review_apps/#review-apps-examples).\nThe review app [environment\nURL](https://docs.gitlab.com/ee/ci/environments/#create-a-dynamic-environment)\nis important for instrumenting the Hurl tests dynamically. The CI/CD\nvariable\n[`CI_ENVIRONMENT_URL`](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)\nis available when `environment:url` is specified in the review app\nconfiguration.\n\n\nThe following example tests the review app for [this blog post when written\nin a merge\nrequest](https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/115548):\n\n\n```yaml\n\n# Test review apps with hurl for this blog post.\n\nhurl-review-test:\n  extends: .review-environment # inherits the environment settings\n  needs: [uncategorized-build-and-review-deploy] # waits until the website (sites/uncategorized) is deployed\n  stage: test\n  rules: # YAML anchor that runs the job only on merge requests\n    - \u003C\u003C: *if-merge-request-original-repo\n  image:\n    name: ghcr.io/orange-opensource/hurl:1.8.0\n    entrypoint: [\"\"]\n  script:\n    - echo -e \"GET ${CI_ENVIRONMENT_URL}\\n\\nHTTP/1.1 200\" | hurl --test --color\n```\n\n\nThe environment is specified in the [.review-environment job\ntemplate](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/91d6fd72a424a3d913e79ebc2aefb23bbab85863/.gitlab-ci.yml#L332)\nand used to [deploy the website review\njob](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/91d6fd72a424a3d913e79ebc2aefb23bbab85863/.gitlab-ci.yml#L532).\nThe relevant configuration snippet is shown here:\n\n\n```yaml\n\n.review-environment:\n  variables:\n    DEPLOY_TYPE: review\n  environment:\n    name: review/$CI_COMMIT_REF_SLUG\n    url: https://$CI_COMMIT_REF_SLUG.about.gitlab-review.app\n    on_stop: review-stop\n    auto_stop_in: 30 days\n```\n\n\nThe deployment of the www-gitlab-com project [uses buckets in Google\nCloud](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/91d6fd72a424a3d913e79ebc2aefb23bbab85863/scripts/deploy)\nthat serve the website content in the review app. There are different types\nof web applications that require different deployment methods - as long as\nthe environment URL variable is available in CI/CD and the deployment URL is\naccessible from the GitLab Runner executing the CI/CD job, you can\ncontinously test web apps with Hurl!\n\n\n![Hurl test in GitLab CI/CD for review app\nenvironments](https://about.gitlab.com/images/blogimages/hurl-continuous-website-testing/hurl_gitlab_cicd_review_app_environment_tests_www-gitlab-com.png)\n\n\n## Development tips\n\n\nUse the [`--verbose`\nparameter](https://hurl.dev/docs/tutorial/debug-tips.html) to see the full\nrequest and response flow. Hurl also provides tips which `curl` command\ncould be run to fetch more data. This can be helpful when starting to use or\ndevelop a new REST API, or learning to understand the JSON structure of HTTP\nresponses. Chaining the `curl` command with `jq` (the `curl ... | jq`\npattern) can still be helpful to fetch data, and build the HTTP tests in a\nsecond terminal or editor window.\n\n\n```sh\n\n$ curl -s 'https://gitlab.com/api/v4/projects' | jq\n\n$ curl -s 'https://gitlab.com/api/v4/projects' | jq -c '.[]' | jq\n\n\n{\"id\":41375401,\"description\":\"An example project for a GitLab\npipeline.\",\"name\":\"Calculator\",\"name_with_namespace\":\"Iva Tee /\nCalculator\",\"path\":\"calculator\",\"path_with_namespace\":\"snufkins_hat/calculator\",\"created_at\":\"2022-11-26T00:32:33.825Z\",\"default_branch\":\"master\",\"tag_list\":[],\"topics\":[],\"ssh_url_to_repo\":\"git@gitlab.com:snufkins_hat/calculator.git\",\"http_url_to_repo\":\"https://gitlab.com/snufkins_hat/calculator.git\",\"web_url\":\"https://gitlab.com/snufkins_hat/calculator\",\"readme_url\":\"https://gitlab.com/snufkins_hat/calculator/-/blob/master/README.md\",\"avatar_url\":null,\"forks_count\":0,\"star_count\":0,\"last_activity_at\":\"2022-11-26T00:32:33.825Z\",\"namespace\":{\"id\":58849237,\"name\":\"Iva\nTee\",\"path\":\"snufkins_hat\",\"kind\":\"user\",\"full_path\":\"snufkins_hat\",\"parent_id\":null,\"avatar_url\":\"https://secure.gravatar.com/avatar/a3efe834950275380d5f19c9b17c922c?s=80&d=identicon\",\"web_url\":\"https://gitlab.com/snufkins_hat\"}}\n\n```\n\n\nThe GitLab projects API returns an array of elements, where we can inspect\nthe `id` and `name` attributes for a simple test - the first element’s name\nmust not be empty, the second element’s id needs to be greater than 0.\n\n\n```sh\n\n$ vim gitlab-com-api.hurl\n\n\nGET https://gitlab.com/api/v4/projects\n\n\nHTTP/2 200\n\n\n[Asserts]\n\njsonpath \"$[0].name\" != \"\"\n\njsonpath \"$[1].id\" > 0\n\n\n$ hurl --test gitlab-com-api.hurl\n\n\ngitlab-com-api.hurl: Running [1/1]\n\ngitlab-com-api.hurl: Success (1 request(s) in 728 ms)\n\n--------------------------------------------------------------------------------\n\nExecuted files:  1\n\nSucceeded files: 1 (100.0%)\n\nFailed files:    0 (0.0%)\n\nDuration:        730 ms\n\n```\n\n\n## More use cases\n\n\n- Work with HTTP sessions and\n[cookies](https://hurl.dev/docs/request.html#cookies), test [forms with\nparameters](https://hurl.dev/docs/request.html#form-parameters).\n\n- Review existing API tests of your applications.\n\n- Build advanced chained workflows with GET, POST, PUT, DELETE, and more\nHTTP methods.\n\n- Integrate simple ping/HTTP monitoring health checks into the DevSecOps\nPlatform using alerts and incident management.\n\n\nIf the Hurl checks cannot be integrated directly inside the project where\nthe application is developed and deployed, another idea could be to create a\nstandalone GitLab project that has CI/CD pipeline schedules enabled. It can\ncontinuously run the Hurl tests, and parse the reports or trigger an event\nwhen the pipeline is failing, and [create an\nalert](https://docs.gitlab.com/ee/operations/incident_management/alerts.html)\nby sending a JSON payload from the Hurl results to the [HTTP\nendpoint](https://docs.gitlab.com/ee/operations/incident_management/integrations.html#single-http-endpoint).\nDevelopers can send MRs to update the Hurl tests, and maintainers review and\napprove the new test suites being rolled out into production. Alternatively,\nmove the complete CI/CD configuration into a group/project with different\npermissions, and specify the CI/CD configuration as remote URL in the web\napplication project. This compliance level helps to control who can make\nchanges to important tests and CI/CD configuration.\n\n\nHurl supports `--json` as parameter to only return the JSON formatted test\nresult and build own custom reports and integrations.\n\n\n```sh\n\n$ echo -e \"GET https://about.gitlab.com/teamops/\\n\\nHTTP/2 200\" | hurl\n--json | jq\n\n```\n\n\nFor folks in DevRel, monitoring certain websites for keywords or checking\nAPIs whether values increase a certain threshold can be interesting. Here is\nan example for monitoring Hacker News using the Algolia search API, inspired\nby the [Zapier integration used for GitLab\nSlack](/handbook/marketing/developer-relations/workflows-tools/zapier/#zaps-for-hacker-news).\nThe `QueryStringParams` section allows users to define the query parameters\nas a readable list, which is easier to modify. The `jsonpath` checks\nsearches for the `hits` key and its count being zero (not on the Hacker News\nfront page means OK in this example).\n\n\n```\n\n$ vim hackernews.hurl\n\n\nGET https://hn.algolia.com/api/v1/search\n\n[QueryStringParams]\n\nquery: gitlab\n\n#query: hurl\n\ntags: front_page\n\n\nHTTP/2 200\n\n\n[Asserts]\n\njsonpath \"$.hits\" count == 0\n\n\n$ hurl --test hackernews.hurl\n\n```\n\n\n## Limitations\n\n\nHurl works great for testing websites and web applications that serve static\ncontent, and by sending different HTTP request types, data, etc., and\nensuring that responses match expectations. Compared to other end-to-end\ntesting solutions (Selenium, etc.), Hurl does not provide a JavaScript\nengine and only can parse the raw DOM or JSON response. It does not support\na DOM managed and rendered by JavaScript front-end frameworks. UI\nintegration tests also need to be performed with different tools, similar to\nfull end-to-end test workflows. Other examples are [accessibility\ntesting](https://docs.gitlab.com/ee/ci/testing/accessibility_testing.html)\nand [browser performance\ntesting](https://docs.gitlab.com/ee/ci/testing/browser_performance_testing.html).\nIf you are curious how end-to-end testing is done for GitLab, the product,\npeek into the [development\ndocumentation](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/).\n\n\n## Conclusion\n\n\nHurl provides an easy way to test HTTP endpoints (such as websites and APIs)\nin a fast and reliable way. The CLI commands can be integrated into CI/CD\nworkflows, and the configuration syntax and files provide a single source of\ntruth for everything. Additional support for JUnit report formats ensure\nthat website testing is fully integrated into the\n[DevSecOps](https://about.gitlab.com/topics/devsecops/) platform, and\nincreases visibility and extensibility with automating tests, and\nmonitoring. There are known limitations with dynamic JavaScript websites and\nadvanced UI/end-to-end testing workflows.\n\n\nHurl is open source, [created and maintained by\nOrange](https://opensource.orange.com/en/open-source-orange/), and written\nin Rust. This blog post inspired contributions to the [Debian/Ubuntu\ninstallation\ndocumentation](https://github.com/Orange-OpenSource/hurl/pull/1084) and\n[default issue\ntemplates](https://github.com/Orange-OpenSource/hurl/pull/1083).\n\n\n**Tip:** Practice using Hurl on the command line, and remember it when the\nnext production incident shows a strange API behavior with POST requests.\n\n\nThanks to [Lee Tickett](/company/team/#leetickett-gitlab) who inspired me to\ntest Hurl in GitLab CI/CD and write this blog post after seeing huge\ninterest in a [Twitter\nshare](https://twitter.com/dnsmichi/status/1595820546062778369).\n\n\nCover image by [Aaron Burden](https://unsplash.com/@aaronburden) on\n[Unsplash](https://unsplash.com)\n\n{: .note}\n","engineering",[23,24,25],"testing","CI","DevOps",{"slug":27,"featured":6,"template":28},"how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd","BlogPost","content:en-us:blog:how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd.yml","yaml","How To Continously Test Web Apps Apis With Hurl And Gitlab Ci Cd","content","en-us/blog/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd.yml","en-us/blog/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd","yml",{"_path":37,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"data":39,"_id":461,"_type":30,"title":462,"_source":32,"_file":463,"_stem":464,"_extension":35},"/shared/en-us/main-navigation","en-us",{"logo":40,"freeTrial":45,"sales":50,"login":55,"items":60,"search":392,"minimal":423,"duo":442,"pricingDeployment":451},{"config":41},{"href":42,"dataGaName":43,"dataGaLocation":44},"/","gitlab logo","header",{"text":46,"config":47},"Get free trial",{"href":48,"dataGaName":49,"dataGaLocation":44},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":51,"config":52},"Talk to sales",{"href":53,"dataGaName":54,"dataGaLocation":44},"/sales/","sales",{"text":56,"config":57},"Sign in",{"href":58,"dataGaName":59,"dataGaLocation":44},"https://gitlab.com/users/sign_in/","sign in",[61,105,203,208,313,373],{"text":62,"config":63,"cards":65,"footer":88},"Platform",{"dataNavLevelOne":64},"platform",[66,72,80],{"title":62,"description":67,"link":68},"The most comprehensive AI-powered DevSecOps Platform",{"text":69,"config":70},"Explore our Platform",{"href":71,"dataGaName":64,"dataGaLocation":44},"/platform/",{"title":73,"description":74,"link":75},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":76,"config":77},"Meet GitLab Duo",{"href":78,"dataGaName":79,"dataGaLocation":44},"/gitlab-duo/","gitlab duo ai",{"title":81,"description":82,"link":83},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":84,"config":85},"Learn more",{"href":86,"dataGaName":87,"dataGaLocation":44},"/why-gitlab/","why gitlab",{"title":89,"items":90},"Get started with",[91,96,101],{"text":92,"config":93},"Platform Engineering",{"href":94,"dataGaName":95,"dataGaLocation":44},"/solutions/platform-engineering/","platform engineering",{"text":97,"config":98},"Developer Experience",{"href":99,"dataGaName":100,"dataGaLocation":44},"/developer-experience/","Developer experience",{"text":102,"config":103},"MLOps",{"href":104,"dataGaName":102,"dataGaLocation":44},"/topics/devops/the-role-of-ai-in-devops/",{"text":106,"left":107,"config":108,"link":110,"lists":114,"footer":185},"Product",true,{"dataNavLevelOne":109},"solutions",{"text":111,"config":112},"View all Solutions",{"href":113,"dataGaName":109,"dataGaLocation":44},"/solutions/",[115,140,164],{"title":116,"description":117,"link":118,"items":123},"Automation","CI/CD and automation to accelerate deployment",{"config":119},{"icon":120,"href":121,"dataGaName":122,"dataGaLocation":44},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[124,128,132,136],{"text":125,"config":126},"CI/CD",{"href":127,"dataGaLocation":44,"dataGaName":125},"/solutions/continuous-integration/",{"text":129,"config":130},"AI-Assisted Development",{"href":78,"dataGaLocation":44,"dataGaName":131},"AI assisted development",{"text":133,"config":134},"Source Code Management",{"href":135,"dataGaLocation":44,"dataGaName":133},"/solutions/source-code-management/",{"text":137,"config":138},"Automated Software Delivery",{"href":121,"dataGaLocation":44,"dataGaName":139},"Automated software delivery",{"title":141,"description":142,"link":143,"items":148},"Security","Deliver code faster without compromising security",{"config":144},{"href":145,"dataGaName":146,"dataGaLocation":44,"icon":147},"/solutions/security-compliance/","security and compliance","ShieldCheckLight",[149,154,159],{"text":150,"config":151},"Application Security Testing",{"href":152,"dataGaName":153,"dataGaLocation":44},"/solutions/application-security-testing/","Application security testing",{"text":155,"config":156},"Software Supply Chain Security",{"href":157,"dataGaLocation":44,"dataGaName":158},"/solutions/supply-chain/","Software supply chain security",{"text":160,"config":161},"Software Compliance",{"href":162,"dataGaName":163,"dataGaLocation":44},"/solutions/software-compliance/","software compliance",{"title":165,"link":166,"items":171},"Measurement",{"config":167},{"icon":168,"href":169,"dataGaName":170,"dataGaLocation":44},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[172,176,180],{"text":173,"config":174},"Visibility & Measurement",{"href":169,"dataGaLocation":44,"dataGaName":175},"Visibility and Measurement",{"text":177,"config":178},"Value Stream Management",{"href":179,"dataGaLocation":44,"dataGaName":177},"/solutions/value-stream-management/",{"text":181,"config":182},"Analytics & Insights",{"href":183,"dataGaLocation":44,"dataGaName":184},"/solutions/analytics-and-insights/","Analytics and insights",{"title":186,"items":187},"GitLab for",[188,193,198],{"text":189,"config":190},"Enterprise",{"href":191,"dataGaLocation":44,"dataGaName":192},"/enterprise/","enterprise",{"text":194,"config":195},"Small Business",{"href":196,"dataGaLocation":44,"dataGaName":197},"/small-business/","small business",{"text":199,"config":200},"Public Sector",{"href":201,"dataGaLocation":44,"dataGaName":202},"/solutions/public-sector/","public sector",{"text":204,"config":205},"Pricing",{"href":206,"dataGaName":207,"dataGaLocation":44,"dataNavLevelOne":207},"/pricing/","pricing",{"text":209,"config":210,"link":212,"lists":216,"feature":300},"Resources",{"dataNavLevelOne":211},"resources",{"text":213,"config":214},"View all resources",{"href":215,"dataGaName":211,"dataGaLocation":44},"/resources/",[217,250,272],{"title":218,"items":219},"Getting started",[220,225,230,235,240,245],{"text":221,"config":222},"Install",{"href":223,"dataGaName":224,"dataGaLocation":44},"/install/","install",{"text":226,"config":227},"Quick start guides",{"href":228,"dataGaName":229,"dataGaLocation":44},"/get-started/","quick setup checklists",{"text":231,"config":232},"Learn",{"href":233,"dataGaLocation":44,"dataGaName":234},"https://university.gitlab.com/","learn",{"text":236,"config":237},"Product documentation",{"href":238,"dataGaName":239,"dataGaLocation":44},"https://docs.gitlab.com/","product documentation",{"text":241,"config":242},"Best practice videos",{"href":243,"dataGaName":244,"dataGaLocation":44},"/getting-started-videos/","best practice videos",{"text":246,"config":247},"Integrations",{"href":248,"dataGaName":249,"dataGaLocation":44},"/integrations/","integrations",{"title":251,"items":252},"Discover",[253,258,262,267],{"text":254,"config":255},"Customer success stories",{"href":256,"dataGaName":257,"dataGaLocation":44},"/customers/","customer success stories",{"text":259,"config":260},"Blog",{"href":261,"dataGaName":5,"dataGaLocation":44},"/blog/",{"text":263,"config":264},"Remote",{"href":265,"dataGaName":266,"dataGaLocation":44},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":268,"config":269},"TeamOps",{"href":270,"dataGaName":271,"dataGaLocation":44},"/teamops/","teamops",{"title":273,"items":274},"Connect",[275,280,285,290,295],{"text":276,"config":277},"GitLab Services",{"href":278,"dataGaName":279,"dataGaLocation":44},"/services/","services",{"text":281,"config":282},"Community",{"href":283,"dataGaName":284,"dataGaLocation":44},"/community/","community",{"text":286,"config":287},"Forum",{"href":288,"dataGaName":289,"dataGaLocation":44},"https://forum.gitlab.com/","forum",{"text":291,"config":292},"Events",{"href":293,"dataGaName":294,"dataGaLocation":44},"/events/","events",{"text":296,"config":297},"Partners",{"href":298,"dataGaName":299,"dataGaLocation":44},"/partners/","partners",{"backgroundColor":301,"textColor":302,"text":303,"image":304,"link":308},"#2f2a6b","#fff","Insights for the future of software development",{"altText":305,"config":306},"the source promo card",{"src":307},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758208064/dzl0dbift9xdizyelkk4.svg",{"text":309,"config":310},"Read the latest",{"href":311,"dataGaName":312,"dataGaLocation":44},"/the-source/","the source",{"text":314,"config":315,"lists":317},"Company",{"dataNavLevelOne":316},"company",[318],{"items":319},[320,325,331,333,338,343,348,353,358,363,368],{"text":321,"config":322},"About",{"href":323,"dataGaName":324,"dataGaLocation":44},"/company/","about",{"text":326,"config":327,"footerGa":330},"Jobs",{"href":328,"dataGaName":329,"dataGaLocation":44},"/jobs/","jobs",{"dataGaName":329},{"text":291,"config":332},{"href":293,"dataGaName":294,"dataGaLocation":44},{"text":334,"config":335},"Leadership",{"href":336,"dataGaName":337,"dataGaLocation":44},"/company/team/e-group/","leadership",{"text":339,"config":340},"Team",{"href":341,"dataGaName":342,"dataGaLocation":44},"/company/team/","team",{"text":344,"config":345},"Handbook",{"href":346,"dataGaName":347,"dataGaLocation":44},"https://handbook.gitlab.com/","handbook",{"text":349,"config":350},"Investor relations",{"href":351,"dataGaName":352,"dataGaLocation":44},"https://ir.gitlab.com/","investor relations",{"text":354,"config":355},"Trust Center",{"href":356,"dataGaName":357,"dataGaLocation":44},"/security/","trust center",{"text":359,"config":360},"AI Transparency Center",{"href":361,"dataGaName":362,"dataGaLocation":44},"/ai-transparency-center/","ai transparency center",{"text":364,"config":365},"Newsletter",{"href":366,"dataGaName":367,"dataGaLocation":44},"/company/contact/","newsletter",{"text":369,"config":370},"Press",{"href":371,"dataGaName":372,"dataGaLocation":44},"/press/","press",{"text":374,"config":375,"lists":376},"Contact us",{"dataNavLevelOne":316},[377],{"items":378},[379,382,387],{"text":51,"config":380},{"href":53,"dataGaName":381,"dataGaLocation":44},"talk to sales",{"text":383,"config":384},"Get help",{"href":385,"dataGaName":386,"dataGaLocation":44},"/support/","get help",{"text":388,"config":389},"Customer portal",{"href":390,"dataGaName":391,"dataGaLocation":44},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":393,"login":394,"suggestions":401},"Close",{"text":395,"link":396},"To search repositories and projects, login to",{"text":397,"config":398},"gitlab.com",{"href":58,"dataGaName":399,"dataGaLocation":400},"search login","search",{"text":402,"default":403},"Suggestions",[404,406,410,412,416,420],{"text":73,"config":405},{"href":78,"dataGaName":73,"dataGaLocation":400},{"text":407,"config":408},"Code Suggestions (AI)",{"href":409,"dataGaName":407,"dataGaLocation":400},"/solutions/code-suggestions/",{"text":125,"config":411},{"href":127,"dataGaName":125,"dataGaLocation":400},{"text":413,"config":414},"GitLab on AWS",{"href":415,"dataGaName":413,"dataGaLocation":400},"/partners/technology-partners/aws/",{"text":417,"config":418},"GitLab on Google Cloud",{"href":419,"dataGaName":417,"dataGaLocation":400},"/partners/technology-partners/google-cloud-platform/",{"text":421,"config":422},"Why GitLab?",{"href":86,"dataGaName":421,"dataGaLocation":400},{"freeTrial":424,"mobileIcon":429,"desktopIcon":434,"secondaryButton":437},{"text":425,"config":426},"Start free trial",{"href":427,"dataGaName":49,"dataGaLocation":428},"https://gitlab.com/-/trials/new/","nav",{"altText":430,"config":431},"Gitlab Icon",{"src":432,"dataGaName":433,"dataGaLocation":428},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":430,"config":435},{"src":436,"dataGaName":433,"dataGaLocation":428},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":438,"config":439},"Get Started",{"href":440,"dataGaName":441,"dataGaLocation":428},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":443,"mobileIcon":447,"desktopIcon":449},{"text":444,"config":445},"Learn more about GitLab Duo",{"href":78,"dataGaName":446,"dataGaLocation":428},"gitlab duo",{"altText":430,"config":448},{"src":432,"dataGaName":433,"dataGaLocation":428},{"altText":430,"config":450},{"src":436,"dataGaName":433,"dataGaLocation":428},{"freeTrial":452,"mobileIcon":457,"desktopIcon":459},{"text":453,"config":454},"Back to pricing",{"href":206,"dataGaName":455,"dataGaLocation":428,"icon":456},"back to pricing","GoBack",{"altText":430,"config":458},{"src":432,"dataGaName":433,"dataGaLocation":428},{"altText":430,"config":460},{"src":436,"dataGaName":433,"dataGaLocation":428},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":466,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"title":467,"button":468,"image":473,"config":477,"_id":479,"_type":30,"_source":32,"_file":480,"_stem":481,"_extension":35},"/shared/en-us/banner","is now in public beta!",{"text":469,"config":470},"Try the Beta",{"href":471,"dataGaName":472,"dataGaLocation":44},"/gitlab-duo/agent-platform/","duo banner",{"altText":474,"config":475},"GitLab Duo Agent Platform",{"src":476},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1753720689/somrf9zaunk0xlt7ne4x.svg",{"layout":478},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":483,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"data":484,"_id":688,"_type":30,"title":689,"_source":32,"_file":690,"_stem":691,"_extension":35},"/shared/en-us/main-footer",{"text":485,"source":486,"edit":492,"contribute":497,"config":502,"items":507,"minimal":680},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":487,"config":488},"View page source",{"href":489,"dataGaName":490,"dataGaLocation":491},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":493,"config":494},"Edit this page",{"href":495,"dataGaName":496,"dataGaLocation":491},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":498,"config":499},"Please contribute",{"href":500,"dataGaName":501,"dataGaLocation":491},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":503,"facebook":504,"youtube":505,"linkedin":506},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[508,531,587,616,650],{"title":62,"links":509,"subMenu":514},[510],{"text":511,"config":512},"DevSecOps platform",{"href":71,"dataGaName":513,"dataGaLocation":491},"devsecops platform",[515],{"title":204,"links":516},[517,521,526],{"text":518,"config":519},"View plans",{"href":206,"dataGaName":520,"dataGaLocation":491},"view plans",{"text":522,"config":523},"Why Premium?",{"href":524,"dataGaName":525,"dataGaLocation":491},"/pricing/premium/","why premium",{"text":527,"config":528},"Why Ultimate?",{"href":529,"dataGaName":530,"dataGaLocation":491},"/pricing/ultimate/","why ultimate",{"title":532,"links":533},"Solutions",[534,539,541,543,548,553,557,560,564,569,571,574,577,582],{"text":535,"config":536},"Digital transformation",{"href":537,"dataGaName":538,"dataGaLocation":491},"/topics/digital-transformation/","digital transformation",{"text":150,"config":540},{"href":152,"dataGaName":150,"dataGaLocation":491},{"text":139,"config":542},{"href":121,"dataGaName":122,"dataGaLocation":491},{"text":544,"config":545},"Agile development",{"href":546,"dataGaName":547,"dataGaLocation":491},"/solutions/agile-delivery/","agile delivery",{"text":549,"config":550},"Cloud transformation",{"href":551,"dataGaName":552,"dataGaLocation":491},"/topics/cloud-native/","cloud transformation",{"text":554,"config":555},"SCM",{"href":135,"dataGaName":556,"dataGaLocation":491},"source code management",{"text":125,"config":558},{"href":127,"dataGaName":559,"dataGaLocation":491},"continuous integration & delivery",{"text":561,"config":562},"Value stream management",{"href":179,"dataGaName":563,"dataGaLocation":491},"value stream management",{"text":565,"config":566},"GitOps",{"href":567,"dataGaName":568,"dataGaLocation":491},"/solutions/gitops/","gitops",{"text":189,"config":570},{"href":191,"dataGaName":192,"dataGaLocation":491},{"text":572,"config":573},"Small business",{"href":196,"dataGaName":197,"dataGaLocation":491},{"text":575,"config":576},"Public sector",{"href":201,"dataGaName":202,"dataGaLocation":491},{"text":578,"config":579},"Education",{"href":580,"dataGaName":581,"dataGaLocation":491},"/solutions/education/","education",{"text":583,"config":584},"Financial services",{"href":585,"dataGaName":586,"dataGaLocation":491},"/solutions/finance/","financial services",{"title":209,"links":588},[589,591,593,595,598,600,602,604,606,608,610,612,614],{"text":221,"config":590},{"href":223,"dataGaName":224,"dataGaLocation":491},{"text":226,"config":592},{"href":228,"dataGaName":229,"dataGaLocation":491},{"text":231,"config":594},{"href":233,"dataGaName":234,"dataGaLocation":491},{"text":236,"config":596},{"href":238,"dataGaName":597,"dataGaLocation":491},"docs",{"text":259,"config":599},{"href":261,"dataGaName":5,"dataGaLocation":491},{"text":254,"config":601},{"href":256,"dataGaName":257,"dataGaLocation":491},{"text":263,"config":603},{"href":265,"dataGaName":266,"dataGaLocation":491},{"text":276,"config":605},{"href":278,"dataGaName":279,"dataGaLocation":491},{"text":268,"config":607},{"href":270,"dataGaName":271,"dataGaLocation":491},{"text":281,"config":609},{"href":283,"dataGaName":284,"dataGaLocation":491},{"text":286,"config":611},{"href":288,"dataGaName":289,"dataGaLocation":491},{"text":291,"config":613},{"href":293,"dataGaName":294,"dataGaLocation":491},{"text":296,"config":615},{"href":298,"dataGaName":299,"dataGaLocation":491},{"title":314,"links":617},[618,620,622,624,626,628,630,634,639,641,643,645],{"text":321,"config":619},{"href":323,"dataGaName":316,"dataGaLocation":491},{"text":326,"config":621},{"href":328,"dataGaName":329,"dataGaLocation":491},{"text":334,"config":623},{"href":336,"dataGaName":337,"dataGaLocation":491},{"text":339,"config":625},{"href":341,"dataGaName":342,"dataGaLocation":491},{"text":344,"config":627},{"href":346,"dataGaName":347,"dataGaLocation":491},{"text":349,"config":629},{"href":351,"dataGaName":352,"dataGaLocation":491},{"text":631,"config":632},"Sustainability",{"href":633,"dataGaName":631,"dataGaLocation":491},"/sustainability/",{"text":635,"config":636},"Diversity, inclusion and belonging (DIB)",{"href":637,"dataGaName":638,"dataGaLocation":491},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":354,"config":640},{"href":356,"dataGaName":357,"dataGaLocation":491},{"text":364,"config":642},{"href":366,"dataGaName":367,"dataGaLocation":491},{"text":369,"config":644},{"href":371,"dataGaName":372,"dataGaLocation":491},{"text":646,"config":647},"Modern Slavery Transparency Statement",{"href":648,"dataGaName":649,"dataGaLocation":491},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"title":651,"links":652},"Contact Us",[653,656,658,660,665,670,675],{"text":654,"config":655},"Contact an expert",{"href":53,"dataGaName":54,"dataGaLocation":491},{"text":383,"config":657},{"href":385,"dataGaName":386,"dataGaLocation":491},{"text":388,"config":659},{"href":390,"dataGaName":391,"dataGaLocation":491},{"text":661,"config":662},"Status",{"href":663,"dataGaName":664,"dataGaLocation":491},"https://status.gitlab.com/","status",{"text":666,"config":667},"Terms of use",{"href":668,"dataGaName":669,"dataGaLocation":491},"/terms/","terms of use",{"text":671,"config":672},"Privacy statement",{"href":673,"dataGaName":674,"dataGaLocation":491},"/privacy/","privacy statement",{"text":676,"config":677},"Cookie preferences",{"dataGaName":678,"dataGaLocation":491,"id":679,"isOneTrustButton":107},"cookie preferences","ot-sdk-btn",{"items":681},[682,684,686],{"text":666,"config":683},{"href":668,"dataGaName":669,"dataGaLocation":491},{"text":671,"config":685},{"href":673,"dataGaName":674,"dataGaLocation":491},{"text":676,"config":687},{"dataGaName":678,"dataGaLocation":491,"id":679,"isOneTrustButton":107},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[693],{"_path":694,"_dir":695,"_draft":6,"_partial":6,"_locale":7,"content":696,"config":700,"_id":702,"_type":30,"title":18,"_source":32,"_file":703,"_stem":704,"_extension":35},"/en-us/blog/authors/michael-friedrich","authors",{"name":18,"config":697},{"headshot":698,"ctfId":699},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659879/Blog/Author%20Headshots/dnsmichi-headshot.jpg","dnsmichi",{"template":701},"BlogAuthor","content:en-us:blog:authors:michael-friedrich.yml","en-us/blog/authors/michael-friedrich.yml","en-us/blog/authors/michael-friedrich",{"_path":706,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"header":707,"eyebrow":708,"blurb":709,"button":710,"secondaryButton":714,"_id":716,"_type":30,"title":717,"_source":32,"_file":718,"_stem":719,"_extension":35},"/shared/en-us/next-steps","Start shipping better software faster","50%+ of the Fortune 100 trust GitLab","See what your team can do with the intelligent\n\n\nDevSecOps platform.\n",{"text":46,"config":711},{"href":712,"dataGaName":49,"dataGaLocation":713},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":51,"config":715},{"href":53,"dataGaName":54,"dataGaLocation":713},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",1758326238467]