[{"data":1,"prerenderedAt":725},["ShallowReactive",2],{"/en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io":3,"navigation-en-us":38,"banner-en-us":464,"footer-en-us":481,"Andrew Fontaine":691,"next-steps-en-us":704,"footer-source-/en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io/":719},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":28,"_id":31,"_type":32,"title":33,"_source":34,"_file":35,"_stem":36,"_extension":37},"/en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"Review Apps for Android with GitLab, fastlane & Appetize.io","See how GitLab and Appetize.io can bring Review Apps to your Android project","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749664102/Blog/Hero%20Images/gitlab-values-cover.png","https://about.gitlab.com/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"How to create Review Apps for Android with GitLab, fastlane, and Appetize.io\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Andrew Fontaine\"}],\n        \"datePublished\": \"2020-05-06\",\n      }",{"title":17,"description":10,"authors":18,"heroImage":11,"date":20,"body":21,"category":22,"tags":23},"How to create Review Apps for Android with GitLab, fastlane, and Appetize.io",[19],"Andrew Fontaine","2020-05-06","{::options parse_block_html=\"true\" /}\n\n\n\n\nIn a [previous look at GitLab and _fastlane_], we discussed how _fastlane_\nnow\n\nautomatically publishes the Gitter Android app to the Google Play Store, but\nat\n\nGitLab, we live on [review apps], and review apps for Android applications\ndidn't\n\nreally exist... until [Appetize.io] came to our attention.\n\n\nJust a simple extension of our existing `.gitlab-ci.yml`, we can utilize\n\nAppetize.io to spin up review apps of our Android application.\n\n\nIf you'd rather just skip to the end, you can see\n\n[my MR to the Gitter Android project].\n\n\n## Setting up Fastlane\n\n\nFortunately for us, _fastlane_ has integrated support for Appetize.io, so\nall\n\nthat's needed to hit Appetize is the addition of a new `lane`:\n\n\n```diff\n\ndiff --git a/fastlane/Fastfile b/fastlane/Fastfile\n\nindex eb47819..f013a86 100644\n\n--- a/fastlane/Fastfile\n\n+++ b/fastlane/Fastfile\n\n@@ -32,6 +32,13 @@ platform :android do\n     gradle(task: \"test\")\n   end\n\n+  desc 'Pushes the app to Appetize and updates a review app'\n\n+  lane :review do\n\n+    appetize(api_token: ENV['APPETIZE_TOKEN'],\n\n+             path: 'app/build/outputs/apk/debug/app-debug.apk',\n\n+             platform: 'android')\n\n+  end\n\n+\n   desc \"Submit a new Internal Build to Play Store\"\n   lane :internal do\n     upload_to_play_store(track: 'internal', apk: 'app/build/outputs/apk/release/app-release.apk')\n```\n\n\n`APPETIZE_TOKEN` is an Appetize.io API token that can be generated on the\n\n[Appetize API docs] after signing up for an account. Once we add a new job\nand\n\nstage to our `.gitlab-ci.yml`, we will be able to deploy our APK to Appetize\nand\n\nrun them in the browser!\n\n\n```diff\n\ndiff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\n\nindex d9863d7..e4d0ce3 100644\n\n--- a/.gitlab-ci.yml\n\n+++ b/.gitlab-ci.yml\n\n@@ -5,6 +5,7 @@ stages:\n   - environment\n   - build\n   - test\n+  - review\n   - internal\n   - alpha\n   - beta\n@@ -81,6 +82,16 @@ buildRelease:\n   environment:\n     name: production\n\n+deployReview:\n\n+  stage: review\n\n+  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n\n+  script:\n\n+    - bundle exec fastlane review\n\n+  only:\n\n+    - branches\n\n+  except:\n\n+    - master\n\n+\n testDebug:\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n   stage: test\n```\n\n\nGreat! Review apps will be deployed when branches other than `master` build.\n\nUnfortunately, there is no `environment` block, so there's nothing linking\nthese\n\ndeployed review apps to GitLab. Let's fix that next.\n\n\n## Dynamic Environment URLs\n\n\nPreviously, GitLab only liked environment URLs that used pre-existing CI\n\nvariables (like `$CI_COMMT_REF_NAME`) in their definition. Since 12.9,\nhowever,\n\na [new way of defining environment urls with alternative variables exists].\n\n\nBy creating a `dotenv` file and submitting it as an `artifact` in our build,\nwe\n\ncan define custom variables to use in our environment's URL. As all\nAppetize.io\n\napp URLs take the pattern of `https://appetize.io.app/$PUBLIC_KEY`, where\n\n`$PUBLIC_KEY` is randomly generated when the app is created, we need to get\nthe\n\npublic key from the Appetize response in our `Fastfile`, and put it in a\n\n`dotenv` file.\n\n\n```diff\n\ndiff --git a/fastlane/Fastfile b/fastlane/Fastfile\n\nindex 7b5f9d1..ae3867c 100644\n\n--- a/fastlane/Fastfile\n\n+++ b/fastlane/Fastfile\n\n@@ -13,6 +13,13 @@\n # Uncomment the line if you want fastlane to automatically update itself\n # update_fastlane\n\n+\n\n+def update_deployment_url(pub_key)\n\n+  File.open('../deploy.env', 'w') do |f|\n\n+    f.write(\"APPETIZE_PUBLIC_KEY=#{pub_key}\")\n\n+  end\n\n+end\n\n+\n default_platform(:android)\n\n platform :android do\n@@ -37,6 +44,7 @@ platform :android do\n     appetize(api_token: ENV['APPETIZE_TOKEN'],\n              path: 'app/build/outputs/apk/debug/app-debug.apk',\n              platform: 'android')\n+    update_deployment_url(lane_context[SharedValues::APPETIZE_PUBLIC_KEY])\n   end\n\n   desc \"Submit a new Internal Build to Play Store\"\n```\n\n\nWe also need to add an `environment` block to our `.gitlab-ci.yml` to\ncapture an\n\nenvironment name and URL.\n\n\n```diff\n\ndiff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\n\nindex f5a8648..c834077 100644\n\n--- a/.gitlab-ci.yml\n\n+++ b/.gitlab-ci.yml\n\n@@ -85,12 +85,18 @@ buildCreateReleaseNotes:\n deployReview:\n   stage: review\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n+  environment:\n\n+    name: review/$CI_COMMIT_REF_NAME\n\n+    url: https://appetize.io/app/$APPETIZE_PUBLIC_KEY\n   script:\n     - bundle exec fastlane review\n   only:\n     - branches\n   except:\n     - master\n+  artifacts:\n\n+    reports:\n\n+      dotenv: deploy.env\n\n testDebug:\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n```\n\n\nOnce committed, pushed, and a pipeline runs, we should see our environment\n\ndeployed!\n\n\n![Our first review environment][first-review-app]\n\n\n## Optimizing Updates\n\n\nAfter running with this for a bit, we realized that we were accidentally\n\ncreating a new app on Appetize.io with every new build! Their docs\n\n[specify how to update existing apps], so we went about seeing if we could\n\nsmartly update existing environments.\n\n\nSpoiler alert: We could.\n\n\nFirst, we need to save the public key granted to us by Appetize.io\nsomewhere. We\n\ndecided to put it in a JSON file and save that as an artifact of the build.\n\nFortunately, the `Fastfile` is just ruby, which allows us to quickly write\nit\n\nout to a file with a few lines of code, as well as attempt to fetch the\nartifact\n\nfor the last build of the current branch.\n\n\n```diff\n\ndiff --git a/fastlane/Fastfile b/fastlane/Fastfile\n\nindex ae3867c..61e9226 100644\n\n--- a/fastlane/Fastfile\n\n+++ b/fastlane/Fastfile\n\n@@ -13,8 +13,32 @@\n # Uncomment the line if you want fastlane to automatically update itself\n # update_fastlane\n\n+require 'net/http'\n\n+require 'json'\n\n+\n\n+GITLAB_TOKEN = ENV['PRIVATE_TOKEN']\n\n+PROJECT_ID = ENV['CI_PROJECT_ID']\n\n+REF = ENV['CI_COMMIT_REF_NAME']\n\n+JOB = ENV['CI_JOB_NAME']\n\n+API_ROOT = ENV['CI_API_V4_URL']\n\n+\n\n+def public_key\n\n+  uri =\nURI(\"#{API_ROOT}/projects/#{PROJECT_ID}/jobs/artifacts/#{REF}/raw/appetize-information.json?job=#{JOB}\")\n\n+  http = Net::HTTP.new(uri.host, uri.port)\n\n+  http.use_ssl = true\n\n+  req = Net::HTTP::Get.new(uri)\n\n+  req['PRIVATE-TOKEN'] = GITLAB_TOKEN\n\n+  response = http.request(req)\n\n+  return '' if response.code.equal?('404')\n\n+\n\n+  appetize_info = JSON.parse(response.body)\n\n+  appetize_info['publicKey']\n\n+end\n\n def update_deployment_url(pub_key)\n+  File.open('../appetize-information.json', 'w') do |f|\n\n+    f.write(JSON.generate(publicKey: pub_key))\n\n+  end\n   File.open('../deploy.env', 'w') do |f|\n     f.write(\"APPETIZE_PUBLIC_KEY=#{pub_key}\")\n   end\n@@ -42,6 +66,7 @@ platform :android do\n   desc 'Pushes the app to Appetize and updates a review app'\n   lane :review do\n     appetize(api_token: ENV['APPETIZE_TOKEN'],\n+             public_key: public_key,\n              path: 'app/build/outputs/apk/debug/app-debug.apk',\n              platform: 'android')\n     update_deployment_url(lane_context[SharedValues::APPETIZE_PUBLIC_KEY])\n```\n\n\nWhen we go to deploy our app to Appetize, we hit the [Jobs API] to see if we\n\nhave a public key for this branch. If the API returns a `404`, we know we\nare\n\nbuilding a fresh branch and return an empty string, else we parse the JSON\nand\n\nreturn our public key. The [Fastlane docs] state the `appetize` action can\ntake\n\na `public_key` to update an existing app. Here, `''` is considered the same\nas\n\n_not_ providing a public key, so a new application is still deployed as we\nexpect.\n\n\n**NOTE:** If you've read the `diff` closely, you'll notice the usage of an\n\nenvironment variable called `PRIVATE_TOKEN`. This is a GitLab private token\n\ncreated with the `read_api` scope and injected into our build as an\nenvironment\n\nvariable. This is required to authenticate with the GitLab API and fetch\n\nartifacts.\n\n\nOnce we update `.gitlab-ci.yml` to save the new `appetize-information.json`\nfile\n\nas an artifact, later builds on the same branch will be smart and update the\n\nexisting Appetize app!\n\n\n```diff\n\ndiff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\n\nindex c834077..54cf3f6 100644\n\n--- a/.gitlab-ci.yml\n\n+++ b/.gitlab-ci.yml\n\n@@ -95,6 +95,8 @@ deployReview:\n   except:\n     - master\n   artifacts:\n+    paths:\n\n+      - appetize-information.json\n     reports:\n       dotenv: deploy.env\n```\n\n\n## Cleaning up\n\n\nAll that's left is to delete old apps from Appetize once we don't need them\n\nanymore. We can do that by leveraging `on_stop` and creating a `stop` job\nthat\n\nwill delete our app from Appetize.io\n\n\n```diff\n\ndiff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\n\nindex 54cf3f6..f6ecf7e 100644\n\n--- a/.gitlab-ci.yml\n\n+++ b/.gitlab-ci.yml\n\n@@ -10,6 +10,7 @@ stages:\n   - alpha\n   - beta\n   - production\n+  - stop\n\n\n .updateContainerJob:\n@@ -88,6 +89,7 @@ deployReview:\n   environment:\n     name: review/$CI_COMMIT_REF_NAME\n     url: https://appetize.io/app/$APPETIZE_PUBLIC_KEY\n+    on_stop: stopReview\n   script:\n     - bundle exec fastlane review\n   only:\n@@ -100,6 +102,22 @@ deployReview:\n     reports:\n       dotenv: deploy.env\n\n+stopReview:\n\n+  stage: stop\n\n+  environment:\n\n+    name: review/$CI_COMMIT_REF_NAME\n\n+    action: stop\n\n+  variables:\n\n+    GIT_STRATEGY: none\n\n+  when: manual\n\n+  only:\n\n+    - branches\n\n+  except:\n\n+    - master\n\n+  script:\n\n+    - apt-get -y update && apt-get -y upgrade && apt-get -y install jq curl\n\n+    - curl --request DELETE\nhttps://$APPETIZE_TOKEN@api.appetize.io/v1/apps/`jq -r '.publicKey' \u003C\nappetize-information.json`\n\n+\n testDebug:\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n   stage: test\n```\n\n\nOnce your MR is merged and your branch is deleted, the `stopReview` job\nruns,\n\ncalling the [`DELETE` endpoint of the Appetize.io API] with the public key\nthat\n\nis contained in `appetize-information.json`. We don't need to fetch\n\n`appetize-information.json` because the artifact is already present in our\nbuild\n\ncontext. This is because the `stop` stage happens _after_ the `review`\nstage.\n\n\n![A merge request with a deployed review app][merge-request-with-review-app]\n\n\n## Conclusion\n\n\nThanks to some integration with _fastlane_ and the addition of a couple\n\nenvironment variables, having the ability to create review apps for an\nAndroid\n\nproject was surpsingly simple. GitLab's review apps are not _just_ for\nweb-based\n\nprojects, even though it may take a little tinkering to get working.\nAppetize.io\n\nalso supports iOS applications, so all mobile native applications can be\nturned\n\ninto review apps. I would love to see this strategy be applied to a React\nNative\n\nproject as well!\n\n\n[previous look at gitlab and _fastlane_]:\n/blog/android-publishing-with-gitlab-and-fastlane/\n\n[my mr to the gitter android project]:\nhttps://gitlab.com/gitlab-org/gitter/gitter-android-app/-/merge_requests/167\n\n[review apps]: https://docs.gitlab.com/ee/ci/review_apps/#review-apps\n\n[appetize.io]: https://appetize.io\n\n[appetize api docs]: https://appetize.io/docs#request-api-token\n\n[new way of defining environment urls with alternative variables exists]:\nhttps://docs.gitlab.com/ee/ci/environments/index.html#set-dynamic-environment-urls-after-a-job-finishes\n\n[first-review-app]:\n/images/blogimages/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io/first-review-app.png\n\n[specify how to update existing apps]:\nhttps://appetize.io/docs#updating-apps\n\n[jobs api]:\nhttps://docs.gitlab.com/ee/api/jobs.html#download-a-single-artifact-file-from-specific-tag-or-branch\n\n[fastlane docs]: https://docs.fastlane.tools/actions/appetize/\n\n[`delete` endpoint of the appetize.io api]:\nhttps://appetize.io/docs#deleting-apps\n\n[merge-request-with-review-app]:\n/images/blogimages/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io/merge-request-with-review-app.png\n","unfiltered",[24,25,26,27],"CI/CD","integrations","features","tutorial",{"slug":29,"featured":6,"template":30},"how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io","BlogPost","content:en-us:blog:how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io.yml","yaml","How To Create Review Apps For Android With Gitlab Fastlane And Appetize Dot Io","content","en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io.yml","en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io","yml",{"_path":39,"_dir":40,"_draft":6,"_partial":6,"_locale":7,"data":41,"_id":460,"_type":32,"title":461,"_source":34,"_file":462,"_stem":463,"_extension":37},"/shared/en-us/main-navigation","en-us",{"logo":42,"freeTrial":47,"sales":52,"login":57,"items":62,"search":391,"minimal":422,"duo":441,"pricingDeployment":450},{"config":43},{"href":44,"dataGaName":45,"dataGaLocation":46},"/","gitlab logo","header",{"text":48,"config":49},"Get free trial",{"href":50,"dataGaName":51,"dataGaLocation":46},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":53,"config":54},"Talk to sales",{"href":55,"dataGaName":56,"dataGaLocation":46},"/sales/","sales",{"text":58,"config":59},"Sign in",{"href":60,"dataGaName":61,"dataGaLocation":46},"https://gitlab.com/users/sign_in/","sign in",[63,107,203,208,312,372],{"text":64,"config":65,"cards":67,"footer":90},"Platform",{"dataNavLevelOne":66},"platform",[68,74,82],{"title":64,"description":69,"link":70},"The most comprehensive AI-powered DevSecOps Platform",{"text":71,"config":72},"Explore our Platform",{"href":73,"dataGaName":66,"dataGaLocation":46},"/platform/",{"title":75,"description":76,"link":77},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":78,"config":79},"Meet GitLab Duo",{"href":80,"dataGaName":81,"dataGaLocation":46},"/gitlab-duo/","gitlab duo ai",{"title":83,"description":84,"link":85},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":86,"config":87},"Learn more",{"href":88,"dataGaName":89,"dataGaLocation":46},"/why-gitlab/","why gitlab",{"title":91,"items":92},"Get started with",[93,98,103],{"text":94,"config":95},"Platform Engineering",{"href":96,"dataGaName":97,"dataGaLocation":46},"/solutions/platform-engineering/","platform engineering",{"text":99,"config":100},"Developer Experience",{"href":101,"dataGaName":102,"dataGaLocation":46},"/developer-experience/","Developer experience",{"text":104,"config":105},"MLOps",{"href":106,"dataGaName":104,"dataGaLocation":46},"/topics/devops/the-role-of-ai-in-devops/",{"text":108,"left":109,"config":110,"link":112,"lists":116,"footer":185},"Product",true,{"dataNavLevelOne":111},"solutions",{"text":113,"config":114},"View all Solutions",{"href":115,"dataGaName":111,"dataGaLocation":46},"/solutions/",[117,141,164],{"title":118,"description":119,"link":120,"items":125},"Automation","CI/CD and automation to accelerate deployment",{"config":121},{"icon":122,"href":123,"dataGaName":124,"dataGaLocation":46},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[126,129,133,137],{"text":24,"config":127},{"href":128,"dataGaLocation":46,"dataGaName":24},"/solutions/continuous-integration/",{"text":130,"config":131},"AI-Assisted Development",{"href":80,"dataGaLocation":46,"dataGaName":132},"AI assisted development",{"text":134,"config":135},"Source Code Management",{"href":136,"dataGaLocation":46,"dataGaName":134},"/solutions/source-code-management/",{"text":138,"config":139},"Automated Software Delivery",{"href":123,"dataGaLocation":46,"dataGaName":140},"Automated software delivery",{"title":142,"description":143,"link":144,"items":149},"Security","Deliver code faster without compromising security",{"config":145},{"href":146,"dataGaName":147,"dataGaLocation":46,"icon":148},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[150,154,159],{"text":151,"config":152},"Application Security Testing",{"href":146,"dataGaName":153,"dataGaLocation":46},"Application security testing",{"text":155,"config":156},"Software Supply Chain Security",{"href":157,"dataGaLocation":46,"dataGaName":158},"/solutions/supply-chain/","Software supply chain security",{"text":160,"config":161},"Software Compliance",{"href":162,"dataGaName":163,"dataGaLocation":46},"/solutions/software-compliance/","software compliance",{"title":165,"link":166,"items":171},"Measurement",{"config":167},{"icon":168,"href":169,"dataGaName":170,"dataGaLocation":46},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[172,176,180],{"text":173,"config":174},"Visibility & Measurement",{"href":169,"dataGaLocation":46,"dataGaName":175},"Visibility and Measurement",{"text":177,"config":178},"Value Stream Management",{"href":179,"dataGaLocation":46,"dataGaName":177},"/solutions/value-stream-management/",{"text":181,"config":182},"Analytics & Insights",{"href":183,"dataGaLocation":46,"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":46,"dataGaName":192},"/enterprise/","enterprise",{"text":194,"config":195},"Small Business",{"href":196,"dataGaLocation":46,"dataGaName":197},"/small-business/","small business",{"text":199,"config":200},"Public Sector",{"href":201,"dataGaLocation":46,"dataGaName":202},"/solutions/public-sector/","public sector",{"text":204,"config":205},"Pricing",{"href":206,"dataGaName":207,"dataGaLocation":46,"dataNavLevelOne":207},"/pricing/","pricing",{"text":209,"config":210,"link":212,"lists":216,"feature":299},"Resources",{"dataNavLevelOne":211},"resources",{"text":213,"config":214},"View all resources",{"href":215,"dataGaName":211,"dataGaLocation":46},"/resources/",[217,249,271],{"title":218,"items":219},"Getting started",[220,225,230,235,240,245],{"text":221,"config":222},"Install",{"href":223,"dataGaName":224,"dataGaLocation":46},"/install/","install",{"text":226,"config":227},"Quick start guides",{"href":228,"dataGaName":229,"dataGaLocation":46},"/get-started/","quick setup checklists",{"text":231,"config":232},"Learn",{"href":233,"dataGaLocation":46,"dataGaName":234},"https://university.gitlab.com/","learn",{"text":236,"config":237},"Product documentation",{"href":238,"dataGaName":239,"dataGaLocation":46},"https://docs.gitlab.com/","product documentation",{"text":241,"config":242},"Best practice videos",{"href":243,"dataGaName":244,"dataGaLocation":46},"/getting-started-videos/","best practice videos",{"text":246,"config":247},"Integrations",{"href":248,"dataGaName":25,"dataGaLocation":46},"/integrations/",{"title":250,"items":251},"Discover",[252,257,261,266],{"text":253,"config":254},"Customer success stories",{"href":255,"dataGaName":256,"dataGaLocation":46},"/customers/","customer success stories",{"text":258,"config":259},"Blog",{"href":260,"dataGaName":5,"dataGaLocation":46},"/blog/",{"text":262,"config":263},"Remote",{"href":264,"dataGaName":265,"dataGaLocation":46},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":267,"config":268},"TeamOps",{"href":269,"dataGaName":270,"dataGaLocation":46},"/teamops/","teamops",{"title":272,"items":273},"Connect",[274,279,284,289,294],{"text":275,"config":276},"GitLab Services",{"href":277,"dataGaName":278,"dataGaLocation":46},"/services/","services",{"text":280,"config":281},"Community",{"href":282,"dataGaName":283,"dataGaLocation":46},"/community/","community",{"text":285,"config":286},"Forum",{"href":287,"dataGaName":288,"dataGaLocation":46},"https://forum.gitlab.com/","forum",{"text":290,"config":291},"Events",{"href":292,"dataGaName":293,"dataGaLocation":46},"/events/","events",{"text":295,"config":296},"Partners",{"href":297,"dataGaName":298,"dataGaLocation":46},"/partners/","partners",{"backgroundColor":300,"textColor":301,"text":302,"image":303,"link":307},"#2f2a6b","#fff","Insights for the future of software development",{"altText":304,"config":305},"the source promo card",{"src":306},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758208064/dzl0dbift9xdizyelkk4.svg",{"text":308,"config":309},"Read the latest",{"href":310,"dataGaName":311,"dataGaLocation":46},"/the-source/","the source",{"text":313,"config":314,"lists":316},"Company",{"dataNavLevelOne":315},"company",[317],{"items":318},[319,324,330,332,337,342,347,352,357,362,367],{"text":320,"config":321},"About",{"href":322,"dataGaName":323,"dataGaLocation":46},"/company/","about",{"text":325,"config":326,"footerGa":329},"Jobs",{"href":327,"dataGaName":328,"dataGaLocation":46},"/jobs/","jobs",{"dataGaName":328},{"text":290,"config":331},{"href":292,"dataGaName":293,"dataGaLocation":46},{"text":333,"config":334},"Leadership",{"href":335,"dataGaName":336,"dataGaLocation":46},"/company/team/e-group/","leadership",{"text":338,"config":339},"Team",{"href":340,"dataGaName":341,"dataGaLocation":46},"/company/team/","team",{"text":343,"config":344},"Handbook",{"href":345,"dataGaName":346,"dataGaLocation":46},"https://handbook.gitlab.com/","handbook",{"text":348,"config":349},"Investor relations",{"href":350,"dataGaName":351,"dataGaLocation":46},"https://ir.gitlab.com/","investor relations",{"text":353,"config":354},"Trust Center",{"href":355,"dataGaName":356,"dataGaLocation":46},"/security/","trust center",{"text":358,"config":359},"AI Transparency Center",{"href":360,"dataGaName":361,"dataGaLocation":46},"/ai-transparency-center/","ai transparency center",{"text":363,"config":364},"Newsletter",{"href":365,"dataGaName":366,"dataGaLocation":46},"/company/contact/","newsletter",{"text":368,"config":369},"Press",{"href":370,"dataGaName":371,"dataGaLocation":46},"/press/","press",{"text":373,"config":374,"lists":375},"Contact us",{"dataNavLevelOne":315},[376],{"items":377},[378,381,386],{"text":53,"config":379},{"href":55,"dataGaName":380,"dataGaLocation":46},"talk to sales",{"text":382,"config":383},"Get help",{"href":384,"dataGaName":385,"dataGaLocation":46},"/support/","get help",{"text":387,"config":388},"Customer portal",{"href":389,"dataGaName":390,"dataGaLocation":46},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":392,"login":393,"suggestions":400},"Close",{"text":394,"link":395},"To search repositories and projects, login to",{"text":396,"config":397},"gitlab.com",{"href":60,"dataGaName":398,"dataGaLocation":399},"search login","search",{"text":401,"default":402},"Suggestions",[403,405,409,411,415,419],{"text":75,"config":404},{"href":80,"dataGaName":75,"dataGaLocation":399},{"text":406,"config":407},"Code Suggestions (AI)",{"href":408,"dataGaName":406,"dataGaLocation":399},"/solutions/code-suggestions/",{"text":24,"config":410},{"href":128,"dataGaName":24,"dataGaLocation":399},{"text":412,"config":413},"GitLab on AWS",{"href":414,"dataGaName":412,"dataGaLocation":399},"/partners/technology-partners/aws/",{"text":416,"config":417},"GitLab on Google Cloud",{"href":418,"dataGaName":416,"dataGaLocation":399},"/partners/technology-partners/google-cloud-platform/",{"text":420,"config":421},"Why GitLab?",{"href":88,"dataGaName":420,"dataGaLocation":399},{"freeTrial":423,"mobileIcon":428,"desktopIcon":433,"secondaryButton":436},{"text":424,"config":425},"Start free trial",{"href":426,"dataGaName":51,"dataGaLocation":427},"https://gitlab.com/-/trials/new/","nav",{"altText":429,"config":430},"Gitlab Icon",{"src":431,"dataGaName":432,"dataGaLocation":427},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":429,"config":434},{"src":435,"dataGaName":432,"dataGaLocation":427},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":437,"config":438},"Get Started",{"href":439,"dataGaName":440,"dataGaLocation":427},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":442,"mobileIcon":446,"desktopIcon":448},{"text":443,"config":444},"Learn more about GitLab Duo",{"href":80,"dataGaName":445,"dataGaLocation":427},"gitlab duo",{"altText":429,"config":447},{"src":431,"dataGaName":432,"dataGaLocation":427},{"altText":429,"config":449},{"src":435,"dataGaName":432,"dataGaLocation":427},{"freeTrial":451,"mobileIcon":456,"desktopIcon":458},{"text":452,"config":453},"Back to pricing",{"href":206,"dataGaName":454,"dataGaLocation":427,"icon":455},"back to pricing","GoBack",{"altText":429,"config":457},{"src":431,"dataGaName":432,"dataGaLocation":427},{"altText":429,"config":459},{"src":435,"dataGaName":432,"dataGaLocation":427},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":465,"_dir":40,"_draft":6,"_partial":6,"_locale":7,"title":466,"button":467,"image":472,"config":476,"_id":478,"_type":32,"_source":34,"_file":479,"_stem":480,"_extension":37},"/shared/en-us/banner","is now in public beta!",{"text":468,"config":469},"Try the Beta",{"href":470,"dataGaName":471,"dataGaLocation":46},"/gitlab-duo/agent-platform/","duo banner",{"altText":473,"config":474},"GitLab Duo Agent Platform",{"src":475},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1753720689/somrf9zaunk0xlt7ne4x.svg",{"layout":477},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":482,"_dir":40,"_draft":6,"_partial":6,"_locale":7,"data":483,"_id":687,"_type":32,"title":688,"_source":34,"_file":689,"_stem":690,"_extension":37},"/shared/en-us/main-footer",{"text":484,"source":485,"edit":491,"contribute":496,"config":501,"items":506,"minimal":679},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":486,"config":487},"View page source",{"href":488,"dataGaName":489,"dataGaLocation":490},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":492,"config":493},"Edit this page",{"href":494,"dataGaName":495,"dataGaLocation":490},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":497,"config":498},"Please contribute",{"href":499,"dataGaName":500,"dataGaLocation":490},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":502,"facebook":503,"youtube":504,"linkedin":505},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[507,530,586,615,649],{"title":64,"links":508,"subMenu":513},[509],{"text":510,"config":511},"DevSecOps platform",{"href":73,"dataGaName":512,"dataGaLocation":490},"devsecops platform",[514],{"title":204,"links":515},[516,520,525],{"text":517,"config":518},"View plans",{"href":206,"dataGaName":519,"dataGaLocation":490},"view plans",{"text":521,"config":522},"Why Premium?",{"href":523,"dataGaName":524,"dataGaLocation":490},"/pricing/premium/","why premium",{"text":526,"config":527},"Why Ultimate?",{"href":528,"dataGaName":529,"dataGaLocation":490},"/pricing/ultimate/","why ultimate",{"title":531,"links":532},"Solutions",[533,538,540,542,547,552,556,559,563,568,570,573,576,581],{"text":534,"config":535},"Digital transformation",{"href":536,"dataGaName":537,"dataGaLocation":490},"/topics/digital-transformation/","digital transformation",{"text":151,"config":539},{"href":146,"dataGaName":151,"dataGaLocation":490},{"text":140,"config":541},{"href":123,"dataGaName":124,"dataGaLocation":490},{"text":543,"config":544},"Agile development",{"href":545,"dataGaName":546,"dataGaLocation":490},"/solutions/agile-delivery/","agile delivery",{"text":548,"config":549},"Cloud transformation",{"href":550,"dataGaName":551,"dataGaLocation":490},"/topics/cloud-native/","cloud transformation",{"text":553,"config":554},"SCM",{"href":136,"dataGaName":555,"dataGaLocation":490},"source code management",{"text":24,"config":557},{"href":128,"dataGaName":558,"dataGaLocation":490},"continuous integration & delivery",{"text":560,"config":561},"Value stream management",{"href":179,"dataGaName":562,"dataGaLocation":490},"value stream management",{"text":564,"config":565},"GitOps",{"href":566,"dataGaName":567,"dataGaLocation":490},"/solutions/gitops/","gitops",{"text":189,"config":569},{"href":191,"dataGaName":192,"dataGaLocation":490},{"text":571,"config":572},"Small business",{"href":196,"dataGaName":197,"dataGaLocation":490},{"text":574,"config":575},"Public sector",{"href":201,"dataGaName":202,"dataGaLocation":490},{"text":577,"config":578},"Education",{"href":579,"dataGaName":580,"dataGaLocation":490},"/solutions/education/","education",{"text":582,"config":583},"Financial services",{"href":584,"dataGaName":585,"dataGaLocation":490},"/solutions/finance/","financial services",{"title":209,"links":587},[588,590,592,594,597,599,601,603,605,607,609,611,613],{"text":221,"config":589},{"href":223,"dataGaName":224,"dataGaLocation":490},{"text":226,"config":591},{"href":228,"dataGaName":229,"dataGaLocation":490},{"text":231,"config":593},{"href":233,"dataGaName":234,"dataGaLocation":490},{"text":236,"config":595},{"href":238,"dataGaName":596,"dataGaLocation":490},"docs",{"text":258,"config":598},{"href":260,"dataGaName":5,"dataGaLocation":490},{"text":253,"config":600},{"href":255,"dataGaName":256,"dataGaLocation":490},{"text":262,"config":602},{"href":264,"dataGaName":265,"dataGaLocation":490},{"text":275,"config":604},{"href":277,"dataGaName":278,"dataGaLocation":490},{"text":267,"config":606},{"href":269,"dataGaName":270,"dataGaLocation":490},{"text":280,"config":608},{"href":282,"dataGaName":283,"dataGaLocation":490},{"text":285,"config":610},{"href":287,"dataGaName":288,"dataGaLocation":490},{"text":290,"config":612},{"href":292,"dataGaName":293,"dataGaLocation":490},{"text":295,"config":614},{"href":297,"dataGaName":298,"dataGaLocation":490},{"title":313,"links":616},[617,619,621,623,625,627,629,633,638,640,642,644],{"text":320,"config":618},{"href":322,"dataGaName":315,"dataGaLocation":490},{"text":325,"config":620},{"href":327,"dataGaName":328,"dataGaLocation":490},{"text":333,"config":622},{"href":335,"dataGaName":336,"dataGaLocation":490},{"text":338,"config":624},{"href":340,"dataGaName":341,"dataGaLocation":490},{"text":343,"config":626},{"href":345,"dataGaName":346,"dataGaLocation":490},{"text":348,"config":628},{"href":350,"dataGaName":351,"dataGaLocation":490},{"text":630,"config":631},"Sustainability",{"href":632,"dataGaName":630,"dataGaLocation":490},"/sustainability/",{"text":634,"config":635},"Diversity, inclusion and belonging (DIB)",{"href":636,"dataGaName":637,"dataGaLocation":490},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":353,"config":639},{"href":355,"dataGaName":356,"dataGaLocation":490},{"text":363,"config":641},{"href":365,"dataGaName":366,"dataGaLocation":490},{"text":368,"config":643},{"href":370,"dataGaName":371,"dataGaLocation":490},{"text":645,"config":646},"Modern Slavery Transparency Statement",{"href":647,"dataGaName":648,"dataGaLocation":490},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"title":650,"links":651},"Contact Us",[652,655,657,659,664,669,674],{"text":653,"config":654},"Contact an expert",{"href":55,"dataGaName":56,"dataGaLocation":490},{"text":382,"config":656},{"href":384,"dataGaName":385,"dataGaLocation":490},{"text":387,"config":658},{"href":389,"dataGaName":390,"dataGaLocation":490},{"text":660,"config":661},"Status",{"href":662,"dataGaName":663,"dataGaLocation":490},"https://status.gitlab.com/","status",{"text":665,"config":666},"Terms of use",{"href":667,"dataGaName":668,"dataGaLocation":490},"/terms/","terms of use",{"text":670,"config":671},"Privacy statement",{"href":672,"dataGaName":673,"dataGaLocation":490},"/privacy/","privacy statement",{"text":675,"config":676},"Cookie preferences",{"dataGaName":677,"dataGaLocation":490,"id":678,"isOneTrustButton":109},"cookie preferences","ot-sdk-btn",{"items":680},[681,683,685],{"text":665,"config":682},{"href":667,"dataGaName":668,"dataGaLocation":490},{"text":670,"config":684},{"href":672,"dataGaName":673,"dataGaLocation":490},{"text":675,"config":686},{"dataGaName":677,"dataGaLocation":490,"id":678,"isOneTrustButton":109},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[692],{"_path":693,"_dir":694,"_draft":6,"_partial":6,"_locale":7,"content":695,"config":699,"_id":701,"_type":32,"title":19,"_source":34,"_file":702,"_stem":703,"_extension":37},"/en-us/blog/authors/andrew-fontaine","authors",{"name":19,"config":696},{"headshot":697,"ctfId":698},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749672447/Blog/Author%20Headshots/afontaine-headshot.jpg","afontaine",{"template":700},"BlogAuthor","content:en-us:blog:authors:andrew-fontaine.yml","en-us/blog/authors/andrew-fontaine.yml","en-us/blog/authors/andrew-fontaine",{"_path":705,"_dir":40,"_draft":6,"_partial":6,"_locale":7,"header":706,"eyebrow":707,"blurb":708,"button":709,"secondaryButton":713,"_id":715,"_type":32,"title":716,"_source":34,"_file":717,"_stem":718,"_extension":37},"/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":48,"config":710},{"href":711,"dataGaName":51,"dataGaLocation":712},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":53,"config":714},{"href":55,"dataGaName":56,"dataGaLocation":712},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":720,"content":721,"config":724,"_id":31,"_type":32,"title":33,"_source":34,"_file":35,"_stem":36,"_extension":37},{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},{"title":17,"description":10,"authors":722,"heroImage":11,"date":20,"body":21,"category":22,"tags":723},[19],[24,25,26,27],{"slug":29,"featured":6,"template":30},1759956832325]