[{"data":1,"prerenderedAt":2138},["ShallowReactive",2],{"/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io":3,"navigation-en-us":1355,"banner-en-us":1781,"footer-en-us":1791,"blog-post-authors-en-us-Andrew Fontaine":2036,"blog-related-posts-en-us-how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io":2051,"blog-promotions-en-us":2074,"next-steps-en-us":2128},{"id":4,"title":5,"authors":6,"body":8,"category":1332,"date":1333,"description":1334,"extension":1335,"externalUrl":1336,"faq":1336,"featured":1337,"heroImage":1338,"meta":1339,"navigation":130,"path":1340,"seo":1341,"slug":1346,"stem":1347,"tags":1348,"template":1353,"updatedDate":1336,"__hash__":1354},"blogPosts/en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io.md","How to create Review Apps for Android with GitLab, fastlane, and Appetize.io",[7],"Andrew Fontaine",{"type":9,"value":10,"toc":1325},"minimark",[11,35,43,50,55,65,192,205,370,381,385,395,421,546,555,671,674,681,685,692,695,701,933,964,982,992,1054,1058,1069,1270,1301,1307,1311,1321],[12,13,14,15,23,24,26,27,30,31,34],"p",{},"In a ",[16,17,18,19],"span",{},"previous look at GitLab and ",[20,21,22],"em",{},"fastlane",", we discussed how ",[20,25,22],{}," now\nautomatically publishes the Gitter Android app to the Google Play Store, but at\nGitLab, we live on ",[16,28,29],{},"review apps",", and review apps for Android applications didn't\nreally exist... until ",[16,32,33],{},"Appetize.io"," came to our attention.",[12,36,37,38,42],{},"Just a simple extension of our existing ",[39,40,41],"code",{},".gitlab-ci.yml",", we can utilize\nAppetize.io to spin up review apps of our Android application.",[12,44,45,46,49],{},"If you'd rather just skip to the end, you can see\n",[16,47,48],{},"my MR to the Gitter Android project",".",[51,52,54],"h2",{"id":53},"setting-up-fastlane","Setting up Fastlane",[12,56,57,58,60,61,64],{},"Fortunately for us, ",[20,59,22],{}," has integrated support for Appetize.io, so all\nthat's needed to hit Appetize is the addition of a new ",[39,62,63],{},"lane",":",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-diff shiki shiki-themes github-light","diff --git a/fastlane/Fastfile b/fastlane/Fastfile\nindex eb47819..f013a86 100644\n--- a/fastlane/Fastfile\n+++ b/fastlane/Fastfile\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+  lane :review do\n+    appetize(api_token: ENV['APPETIZE_TOKEN'],\n+             path: 'app/build/outputs/apk/debug/app-debug.apk',\n+             platform: 'android')\n+  end\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","diff","",[39,73,74,82,89,96,103,113,119,125,132,138,144,150,156,162,168,174,180,186],{"__ignoreMap":71},[16,75,78],{"class":76,"line":77},"line",1,[16,79,81],{"class":80},"sYu0t","diff --git a/fastlane/Fastfile b/fastlane/Fastfile\n",[16,83,85],{"class":76,"line":84},2,[16,86,88],{"class":87},"sgsFI","index eb47819..f013a86 100644\n",[16,90,92],{"class":76,"line":91},3,[16,93,95],{"class":94},"s9hmT","--- a/fastlane/Fastfile\n",[16,97,99],{"class":76,"line":98},4,[16,100,102],{"class":101},"shJU0","+++ b/fastlane/Fastfile\n",[16,104,106,110],{"class":76,"line":105},5,[16,107,109],{"class":108},"soKTg","@@ -32,6 +32,13 @@",[16,111,112],{"class":87}," platform :android do\n",[16,114,116],{"class":76,"line":115},6,[16,117,118],{"class":87},"     gradle(task: \"test\")\n",[16,120,122],{"class":76,"line":121},7,[16,123,124],{"class":87},"   end\n",[16,126,128],{"class":76,"line":127},8,[16,129,131],{"emptyLinePlaceholder":130},true,"\n",[16,133,135],{"class":76,"line":134},9,[16,136,137],{"class":101},"+  desc 'Pushes the app to Appetize and updates a review app'\n",[16,139,141],{"class":76,"line":140},10,[16,142,143],{"class":101},"+  lane :review do\n",[16,145,147],{"class":76,"line":146},11,[16,148,149],{"class":101},"+    appetize(api_token: ENV['APPETIZE_TOKEN'],\n",[16,151,153],{"class":76,"line":152},12,[16,154,155],{"class":101},"+             path: 'app/build/outputs/apk/debug/app-debug.apk',\n",[16,157,159],{"class":76,"line":158},13,[16,160,161],{"class":101},"+             platform: 'android')\n",[16,163,165],{"class":76,"line":164},14,[16,166,167],{"class":101},"+  end\n",[16,169,171],{"class":76,"line":170},15,[16,172,173],{"class":101},"+\n",[16,175,177],{"class":76,"line":176},16,[16,178,179],{"class":87},"   desc \"Submit a new Internal Build to Play Store\"\n",[16,181,183],{"class":76,"line":182},17,[16,184,185],{"class":87},"   lane :internal do\n",[16,187,189],{"class":76,"line":188},18,[16,190,191],{"class":87},"     upload_to_play_store(track: 'internal', apk: 'app/build/outputs/apk/release/app-release.apk')\n",[12,193,194,197,198,201,202,204],{},[39,195,196],{},"APPETIZE_TOKEN"," is an Appetize.io API token that can be generated on the\n",[16,199,200],{},"Appetize API docs"," after signing up for an account. Once we add a new job and\nstage to our ",[39,203,41],{},", we will be able to deploy our APK to Appetize and\nrun them in the browser!",[66,206,208],{"className":68,"code":207,"language":70,"meta":71,"style":71},"diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\nindex d9863d7..e4d0ce3 100644\n--- a/.gitlab-ci.yml\n+++ b/.gitlab-ci.yml\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+  stage: review\n+  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n+  script:\n+    - bundle exec fastlane review\n+  only:\n+    - branches\n+  except:\n+    - master\n+\n testDebug:\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n   stage: test\n\n",[39,209,210,215,220,225,230,238,243,248,253,258,263,268,273,281,286,291,295,300,305,311,317,323,329,335,341,347,352,358,364],{"__ignoreMap":71},[16,211,212],{"class":76,"line":77},[16,213,214],{"class":80},"diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\n",[16,216,217],{"class":76,"line":84},[16,218,219],{"class":87},"index d9863d7..e4d0ce3 100644\n",[16,221,222],{"class":76,"line":91},[16,223,224],{"class":94},"--- a/.gitlab-ci.yml\n",[16,226,227],{"class":76,"line":98},[16,228,229],{"class":101},"+++ b/.gitlab-ci.yml\n",[16,231,232,235],{"class":76,"line":105},[16,233,234],{"class":108},"@@ -5,6 +5,7 @@",[16,236,237],{"class":87}," stages:\n",[16,239,240],{"class":76,"line":115},[16,241,242],{"class":87},"   - environment\n",[16,244,245],{"class":76,"line":121},[16,246,247],{"class":87},"   - build\n",[16,249,250],{"class":76,"line":127},[16,251,252],{"class":87},"   - test\n",[16,254,255],{"class":76,"line":134},[16,256,257],{"class":101},"+  - review\n",[16,259,260],{"class":76,"line":140},[16,261,262],{"class":87},"   - internal\n",[16,264,265],{"class":76,"line":146},[16,266,267],{"class":87},"   - alpha\n",[16,269,270],{"class":76,"line":152},[16,271,272],{"class":87},"   - beta\n",[16,274,275,278],{"class":76,"line":158},[16,276,277],{"class":108},"@@ -81,6 +82,16 @@",[16,279,280],{"class":87}," buildRelease:\n",[16,282,283],{"class":76,"line":164},[16,284,285],{"class":87},"   environment:\n",[16,287,288],{"class":76,"line":170},[16,289,290],{"class":87},"     name: production\n",[16,292,293],{"class":76,"line":176},[16,294,131],{"emptyLinePlaceholder":130},[16,296,297],{"class":76,"line":182},[16,298,299],{"class":101},"+deployReview:\n",[16,301,302],{"class":76,"line":188},[16,303,304],{"class":101},"+  stage: review\n",[16,306,308],{"class":76,"line":307},19,[16,309,310],{"class":101},"+  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n",[16,312,314],{"class":76,"line":313},20,[16,315,316],{"class":101},"+  script:\n",[16,318,320],{"class":76,"line":319},21,[16,321,322],{"class":101},"+    - bundle exec fastlane review\n",[16,324,326],{"class":76,"line":325},22,[16,327,328],{"class":101},"+  only:\n",[16,330,332],{"class":76,"line":331},23,[16,333,334],{"class":101},"+    - branches\n",[16,336,338],{"class":76,"line":337},24,[16,339,340],{"class":101},"+  except:\n",[16,342,344],{"class":76,"line":343},25,[16,345,346],{"class":101},"+    - master\n",[16,348,350],{"class":76,"line":349},26,[16,351,173],{"class":101},[16,353,355],{"class":76,"line":354},27,[16,356,357],{"class":87}," testDebug:\n",[16,359,361],{"class":76,"line":360},28,[16,362,363],{"class":87},"   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n",[16,365,367],{"class":76,"line":366},29,[16,368,369],{"class":87},"   stage: test\n",[12,371,372,373,376,377,380],{},"Great! Review apps will be deployed when branches other than ",[39,374,375],{},"master"," build.\nUnfortunately, there is no ",[39,378,379],{},"environment"," block, so there's nothing linking these\ndeployed review apps to GitLab. Let's fix that next.",[51,382,384],{"id":383},"dynamic-environment-urls","Dynamic Environment URLs",[12,386,387,388,391,392,49],{},"Previously, GitLab only liked environment URLs that used pre-existing CI\nvariables (like ",[39,389,390],{},"$CI_COMMT_REF_NAME",") in their definition. Since 12.9, however,\na ",[16,393,394],{},"new way of defining environment urls with alternative variables exists",[12,396,397,398,401,402,405,406,409,410,413,414,417,418,420],{},"By creating a ",[39,399,400],{},"dotenv"," file and submitting it as an ",[39,403,404],{},"artifact"," in our build, we\ncan define custom variables to use in our environment's URL. As all Appetize.io\napp URLs take the pattern of ",[39,407,408],{},"https://appetize.io.app/$PUBLIC_KEY",", where\n",[39,411,412],{},"$PUBLIC_KEY"," is randomly generated when the app is created, we need to get the\npublic key from the Appetize response in our ",[39,415,416],{},"Fastfile",", and put it in a\n",[39,419,400],{}," file.",[66,422,424],{"className":68,"code":423,"language":70,"meta":71,"style":71},"diff --git a/fastlane/Fastfile b/fastlane/Fastfile\nindex 7b5f9d1..ae3867c 100644\n--- a/fastlane/Fastfile\n+++ b/fastlane/Fastfile\n@@ -13,6 +13,13 @@\n # Uncomment the line if you want fastlane to automatically update itself\n # update_fastlane\n\n+\n+def update_deployment_url(pub_key)\n+  File.open('../deploy.env', 'w') do |f|\n+    f.write(\"APPETIZE_PUBLIC_KEY=#{pub_key}\")\n+  end\n+end\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",[39,425,426,430,435,439,443,448,453,458,462,466,471,476,481,485,490,494,499,503,507,514,519,524,529,534,538,542],{"__ignoreMap":71},[16,427,428],{"class":76,"line":77},[16,429,81],{"class":80},[16,431,432],{"class":76,"line":84},[16,433,434],{"class":87},"index 7b5f9d1..ae3867c 100644\n",[16,436,437],{"class":76,"line":91},[16,438,95],{"class":94},[16,440,441],{"class":76,"line":98},[16,442,102],{"class":101},[16,444,445],{"class":76,"line":105},[16,446,447],{"class":108},"@@ -13,6 +13,13 @@\n",[16,449,450],{"class":76,"line":115},[16,451,452],{"class":87}," # Uncomment the line if you want fastlane to automatically update itself\n",[16,454,455],{"class":76,"line":121},[16,456,457],{"class":87}," # update_fastlane\n",[16,459,460],{"class":76,"line":127},[16,461,131],{"emptyLinePlaceholder":130},[16,463,464],{"class":76,"line":134},[16,465,173],{"class":101},[16,467,468],{"class":76,"line":140},[16,469,470],{"class":101},"+def update_deployment_url(pub_key)\n",[16,472,473],{"class":76,"line":146},[16,474,475],{"class":101},"+  File.open('../deploy.env', 'w') do |f|\n",[16,477,478],{"class":76,"line":152},[16,479,480],{"class":101},"+    f.write(\"APPETIZE_PUBLIC_KEY=#{pub_key}\")\n",[16,482,483],{"class":76,"line":158},[16,484,167],{"class":101},[16,486,487],{"class":76,"line":164},[16,488,489],{"class":101},"+end\n",[16,491,492],{"class":76,"line":170},[16,493,173],{"class":101},[16,495,496],{"class":76,"line":176},[16,497,498],{"class":87}," default_platform(:android)\n",[16,500,501],{"class":76,"line":182},[16,502,131],{"emptyLinePlaceholder":130},[16,504,505],{"class":76,"line":188},[16,506,112],{"class":87},[16,508,509,512],{"class":76,"line":307},[16,510,511],{"class":108},"@@ -37,6 +44,7 @@",[16,513,112],{"class":87},[16,515,516],{"class":76,"line":313},[16,517,518],{"class":87},"     appetize(api_token: ENV['APPETIZE_TOKEN'],\n",[16,520,521],{"class":76,"line":319},[16,522,523],{"class":87},"              path: 'app/build/outputs/apk/debug/app-debug.apk',\n",[16,525,526],{"class":76,"line":325},[16,527,528],{"class":87},"              platform: 'android')\n",[16,530,531],{"class":76,"line":331},[16,532,533],{"class":101},"+    update_deployment_url(lane_context[SharedValues::APPETIZE_PUBLIC_KEY])\n",[16,535,536],{"class":76,"line":337},[16,537,124],{"class":87},[16,539,540],{"class":76,"line":343},[16,541,131],{"emptyLinePlaceholder":130},[16,543,544],{"class":76,"line":349},[16,545,179],{"class":87},[12,547,548,549,551,552,554],{},"We also need to add an ",[39,550,379],{}," block to our ",[39,553,41],{}," to capture an\nenvironment name and URL.",[66,556,558],{"className":68,"code":557,"language":70,"meta":71,"style":71},"diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\nindex f5a8648..c834077 100644\n--- a/.gitlab-ci.yml\n+++ b/.gitlab-ci.yml\n@@ -85,12 +85,18 @@ buildCreateReleaseNotes:\n deployReview:\n   stage: review\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n+  environment:\n+    name: review/$CI_COMMIT_REF_NAME\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+    reports:\n+      dotenv: deploy.env\n\n testDebug:\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n\n",[39,559,560,564,569,573,577,585,590,595,599,604,609,614,619,624,629,634,639,644,649,654,659,663,667],{"__ignoreMap":71},[16,561,562],{"class":76,"line":77},[16,563,214],{"class":80},[16,565,566],{"class":76,"line":84},[16,567,568],{"class":87},"index f5a8648..c834077 100644\n",[16,570,571],{"class":76,"line":91},[16,572,224],{"class":94},[16,574,575],{"class":76,"line":98},[16,576,229],{"class":101},[16,578,579,582],{"class":76,"line":105},[16,580,581],{"class":108},"@@ -85,12 +85,18 @@",[16,583,584],{"class":87}," buildCreateReleaseNotes:\n",[16,586,587],{"class":76,"line":115},[16,588,589],{"class":87}," deployReview:\n",[16,591,592],{"class":76,"line":121},[16,593,594],{"class":87},"   stage: review\n",[16,596,597],{"class":76,"line":127},[16,598,363],{"class":87},[16,600,601],{"class":76,"line":134},[16,602,603],{"class":101},"+  environment:\n",[16,605,606],{"class":76,"line":140},[16,607,608],{"class":101},"+    name: review/$CI_COMMIT_REF_NAME\n",[16,610,611],{"class":76,"line":146},[16,612,613],{"class":101},"+    url: https://appetize.io/app/$APPETIZE_PUBLIC_KEY\n",[16,615,616],{"class":76,"line":152},[16,617,618],{"class":87},"   script:\n",[16,620,621],{"class":76,"line":158},[16,622,623],{"class":87},"     - bundle exec fastlane review\n",[16,625,626],{"class":76,"line":164},[16,627,628],{"class":87},"   only:\n",[16,630,631],{"class":76,"line":170},[16,632,633],{"class":87},"     - branches\n",[16,635,636],{"class":76,"line":176},[16,637,638],{"class":87},"   except:\n",[16,640,641],{"class":76,"line":182},[16,642,643],{"class":87},"     - master\n",[16,645,646],{"class":76,"line":188},[16,647,648],{"class":101},"+  artifacts:\n",[16,650,651],{"class":76,"line":307},[16,652,653],{"class":101},"+    reports:\n",[16,655,656],{"class":76,"line":313},[16,657,658],{"class":101},"+      dotenv: deploy.env\n",[16,660,661],{"class":76,"line":319},[16,662,131],{"emptyLinePlaceholder":130},[16,664,665],{"class":76,"line":325},[16,666,357],{"class":87},[16,668,669],{"class":76,"line":331},[16,670,363],{"class":87},[12,672,673],{},"Once committed, pushed, and a pipeline runs, we should see our environment\ndeployed!",[12,675,676],{},[677,678],"img",{"alt":679,"src":680},"Our first review environment","https://res.cloudinary.com/about-gitlab-com/image/upload/v1782398503/blog/Content%20Images/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io/first-review-app.png",[51,682,684],{"id":683},"optimizing-updates","Optimizing Updates",[12,686,687,688,691],{},"After running with this for a bit, we realized that we were accidentally\ncreating a new app on Appetize.io with every new build! Their docs\n",[16,689,690],{},"specify how to update existing apps",", so we went about seeing if we could\nsmartly update existing environments.",[12,693,694],{},"Spoiler alert: We could.",[12,696,697,698,700],{},"First, we need to save the public key granted to us by Appetize.io somewhere. We\ndecided to put it in a JSON file and save that as an artifact of the build.\nFortunately, the ",[39,699,416],{}," is just ruby, which allows us to quickly write it\nout to a file with a few lines of code, as well as attempt to fetch the artifact\nfor the last build of the current branch.",[66,702,704],{"className":68,"code":703,"language":70,"meta":71,"style":71},"diff --git a/fastlane/Fastfile b/fastlane/Fastfile\nindex ae3867c..61e9226 100644\n--- a/fastlane/Fastfile\n+++ b/fastlane/Fastfile\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+require 'json'\n+\n+GITLAB_TOKEN = ENV['PRIVATE_TOKEN']\n+PROJECT_ID = ENV['CI_PROJECT_ID']\n+REF = ENV['CI_COMMIT_REF_NAME']\n+JOB = ENV['CI_JOB_NAME']\n+API_ROOT = ENV['CI_API_V4_URL']\n+\n+def public_key\n+  uri = URI(\"#{API_ROOT}/projects/#{PROJECT_ID}/jobs/artifacts/#{REF}/raw/appetize-information.json?job=#{JOB}\")\n+  http = Net::HTTP.new(uri.host, uri.port)\n+  http.use_ssl = true\n+  req = Net::HTTP::Get.new(uri)\n+  req['PRIVATE-TOKEN'] = GITLAB_TOKEN\n+  response = http.request(req)\n+  return '' if response.code.equal?('404')\n+\n+  appetize_info = JSON.parse(response.body)\n+  appetize_info['publicKey']\n+end\n\n def update_deployment_url(pub_key)\n+  File.open('../appetize-information.json', 'w') do |f|\n+    f.write(JSON.generate(publicKey: pub_key))\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",[39,705,706,710,715,719,723,728,732,736,740,745,750,754,759,764,769,774,779,783,788,793,798,803,808,813,818,823,827,832,837,841,846,852,858,864,869,875,881,886,894,900,906,911,917,922,927],{"__ignoreMap":71},[16,707,708],{"class":76,"line":77},[16,709,81],{"class":80},[16,711,712],{"class":76,"line":84},[16,713,714],{"class":87},"index ae3867c..61e9226 100644\n",[16,716,717],{"class":76,"line":91},[16,718,95],{"class":94},[16,720,721],{"class":76,"line":98},[16,722,102],{"class":101},[16,724,725],{"class":76,"line":105},[16,726,727],{"class":108},"@@ -13,8 +13,32 @@\n",[16,729,730],{"class":76,"line":115},[16,731,452],{"class":87},[16,733,734],{"class":76,"line":121},[16,735,457],{"class":87},[16,737,738],{"class":76,"line":127},[16,739,131],{"emptyLinePlaceholder":130},[16,741,742],{"class":76,"line":134},[16,743,744],{"class":101},"+require 'net/http'\n",[16,746,747],{"class":76,"line":140},[16,748,749],{"class":101},"+require 'json'\n",[16,751,752],{"class":76,"line":146},[16,753,173],{"class":101},[16,755,756],{"class":76,"line":152},[16,757,758],{"class":101},"+GITLAB_TOKEN = ENV['PRIVATE_TOKEN']\n",[16,760,761],{"class":76,"line":158},[16,762,763],{"class":101},"+PROJECT_ID = ENV['CI_PROJECT_ID']\n",[16,765,766],{"class":76,"line":164},[16,767,768],{"class":101},"+REF = ENV['CI_COMMIT_REF_NAME']\n",[16,770,771],{"class":76,"line":170},[16,772,773],{"class":101},"+JOB = ENV['CI_JOB_NAME']\n",[16,775,776],{"class":76,"line":176},[16,777,778],{"class":101},"+API_ROOT = ENV['CI_API_V4_URL']\n",[16,780,781],{"class":76,"line":182},[16,782,173],{"class":101},[16,784,785],{"class":76,"line":188},[16,786,787],{"class":101},"+def public_key\n",[16,789,790],{"class":76,"line":307},[16,791,792],{"class":101},"+  uri = URI(\"#{API_ROOT}/projects/#{PROJECT_ID}/jobs/artifacts/#{REF}/raw/appetize-information.json?job=#{JOB}\")\n",[16,794,795],{"class":76,"line":313},[16,796,797],{"class":101},"+  http = Net::HTTP.new(uri.host, uri.port)\n",[16,799,800],{"class":76,"line":319},[16,801,802],{"class":101},"+  http.use_ssl = true\n",[16,804,805],{"class":76,"line":325},[16,806,807],{"class":101},"+  req = Net::HTTP::Get.new(uri)\n",[16,809,810],{"class":76,"line":331},[16,811,812],{"class":101},"+  req['PRIVATE-TOKEN'] = GITLAB_TOKEN\n",[16,814,815],{"class":76,"line":337},[16,816,817],{"class":101},"+  response = http.request(req)\n",[16,819,820],{"class":76,"line":343},[16,821,822],{"class":101},"+  return '' if response.code.equal?('404')\n",[16,824,825],{"class":76,"line":349},[16,826,173],{"class":101},[16,828,829],{"class":76,"line":354},[16,830,831],{"class":101},"+  appetize_info = JSON.parse(response.body)\n",[16,833,834],{"class":76,"line":360},[16,835,836],{"class":101},"+  appetize_info['publicKey']\n",[16,838,839],{"class":76,"line":366},[16,840,489],{"class":101},[16,842,844],{"class":76,"line":843},30,[16,845,131],{"emptyLinePlaceholder":130},[16,847,849],{"class":76,"line":848},31,[16,850,851],{"class":87}," def update_deployment_url(pub_key)\n",[16,853,855],{"class":76,"line":854},32,[16,856,857],{"class":101},"+  File.open('../appetize-information.json', 'w') do |f|\n",[16,859,861],{"class":76,"line":860},33,[16,862,863],{"class":101},"+    f.write(JSON.generate(publicKey: pub_key))\n",[16,865,867],{"class":76,"line":866},34,[16,868,167],{"class":101},[16,870,872],{"class":76,"line":871},35,[16,873,874],{"class":87},"   File.open('../deploy.env', 'w') do |f|\n",[16,876,878],{"class":76,"line":877},36,[16,879,880],{"class":87},"     f.write(\"APPETIZE_PUBLIC_KEY=#{pub_key}\")\n",[16,882,884],{"class":76,"line":883},37,[16,885,124],{"class":87},[16,887,889,892],{"class":76,"line":888},38,[16,890,891],{"class":108},"@@ -42,6 +66,7 @@",[16,893,112],{"class":87},[16,895,897],{"class":76,"line":896},39,[16,898,899],{"class":87},"   desc 'Pushes the app to Appetize and updates a review app'\n",[16,901,903],{"class":76,"line":902},40,[16,904,905],{"class":87},"   lane :review do\n",[16,907,909],{"class":76,"line":908},41,[16,910,518],{"class":87},[16,912,914],{"class":76,"line":913},42,[16,915,916],{"class":101},"+             public_key: public_key,\n",[16,918,920],{"class":76,"line":919},43,[16,921,523],{"class":87},[16,923,925],{"class":76,"line":924},44,[16,926,528],{"class":87},[16,928,930],{"class":76,"line":929},45,[16,931,932],{"class":87},"     update_deployment_url(lane_context[SharedValues::APPETIZE_PUBLIC_KEY])\n",[12,934,935,936,939,940,943,944,947,948,951,952,955,956,959,960,963],{},"When we go to deploy our app to Appetize, we hit the ",[16,937,938],{},"Jobs API"," to see if we\nhave a public key for this branch. If the API returns a ",[39,941,942],{},"404",", we know we are\nbuilding a fresh branch and return an empty string, else we parse the JSON and\nreturn our public key. The ",[16,945,946],{},"Fastlane docs"," state the ",[39,949,950],{},"appetize"," action can take\na ",[39,953,954],{},"public_key"," to update an existing app. Here, ",[39,957,958],{},"''"," is considered the same as\n",[20,961,962],{},"not"," providing a public key, so a new application is still deployed as we expect.",[12,965,966,970,971,973,974,977,978,981],{},[967,968,969],"strong",{},"NOTE:"," If you've read the ",[39,972,70],{}," closely, you'll notice the usage of an\nenvironment variable called ",[39,975,976],{},"PRIVATE_TOKEN",". This is a GitLab private token\ncreated with the ",[39,979,980],{},"read_api"," scope and injected into our build as an environment\nvariable. This is required to authenticate with the GitLab API and fetch\nartifacts.",[12,983,984,985,987,988,991],{},"Once we update ",[39,986,41],{}," to save the new ",[39,989,990],{},"appetize-information.json"," file\nas an artifact, later builds on the same branch will be smart and update the\nexisting Appetize app!",[66,993,995],{"className":68,"code":994,"language":70,"meta":71,"style":71},"diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\nindex c834077..54cf3f6 100644\n--- a/.gitlab-ci.yml\n+++ b/.gitlab-ci.yml\n@@ -95,6 +95,8 @@ deployReview:\n   except:\n     - master\n   artifacts:\n+    paths:\n+      - appetize-information.json\n     reports:\n       dotenv: deploy.env\n\n",[39,996,997,1001,1006,1010,1014,1021,1025,1029,1034,1039,1044,1049],{"__ignoreMap":71},[16,998,999],{"class":76,"line":77},[16,1000,214],{"class":80},[16,1002,1003],{"class":76,"line":84},[16,1004,1005],{"class":87},"index c834077..54cf3f6 100644\n",[16,1007,1008],{"class":76,"line":91},[16,1009,224],{"class":94},[16,1011,1012],{"class":76,"line":98},[16,1013,229],{"class":101},[16,1015,1016,1019],{"class":76,"line":105},[16,1017,1018],{"class":108},"@@ -95,6 +95,8 @@",[16,1020,589],{"class":87},[16,1022,1023],{"class":76,"line":115},[16,1024,638],{"class":87},[16,1026,1027],{"class":76,"line":121},[16,1028,643],{"class":87},[16,1030,1031],{"class":76,"line":127},[16,1032,1033],{"class":87},"   artifacts:\n",[16,1035,1036],{"class":76,"line":134},[16,1037,1038],{"class":101},"+    paths:\n",[16,1040,1041],{"class":76,"line":140},[16,1042,1043],{"class":101},"+      - appetize-information.json\n",[16,1045,1046],{"class":76,"line":146},[16,1047,1048],{"class":87},"     reports:\n",[16,1050,1051],{"class":76,"line":152},[16,1052,1053],{"class":87},"       dotenv: deploy.env\n",[51,1055,1057],{"id":1056},"cleaning-up","Cleaning up",[12,1059,1060,1061,1064,1065,1068],{},"All that's left is to delete old apps from Appetize once we don't need them\nanymore. We can do that by leveraging ",[39,1062,1063],{},"on_stop"," and creating a ",[39,1066,1067],{},"stop"," job that\nwill delete our app from Appetize.io",[66,1070,1072],{"className":68,"code":1071,"language":70,"meta":71,"style":71},"diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\nindex 54cf3f6..f6ecf7e 100644\n--- a/.gitlab-ci.yml\n+++ b/.gitlab-ci.yml\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+  stage: stop\n+  environment:\n+    name: review/$CI_COMMIT_REF_NAME\n+    action: stop\n+  variables:\n+    GIT_STRATEGY: none\n+  when: manual\n+  only:\n+    - branches\n+  except:\n+    - master\n+  script:\n+    - apt-get -y update && apt-get -y upgrade && apt-get -y install jq curl\n+    - curl --request DELETE https://$APPETIZE_TOKEN@api.appetize.io/v1/apps/`jq -r '.publicKey' \u003C appetize-information.json`\n+\n testDebug:\n   image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG\n   stage: test\n\n",[39,1073,1074,1078,1083,1087,1091,1098,1102,1106,1111,1116,1120,1124,1129,1136,1140,1145,1150,1155,1159,1163,1167,1174,1178,1182,1186,1191,1196,1200,1204,1209,1214,1219,1224,1228,1232,1236,1240,1244,1249,1254,1258,1262,1266],{"__ignoreMap":71},[16,1075,1076],{"class":76,"line":77},[16,1077,214],{"class":80},[16,1079,1080],{"class":76,"line":84},[16,1081,1082],{"class":87},"index 54cf3f6..f6ecf7e 100644\n",[16,1084,1085],{"class":76,"line":91},[16,1086,224],{"class":94},[16,1088,1089],{"class":76,"line":98},[16,1090,229],{"class":101},[16,1092,1093,1096],{"class":76,"line":105},[16,1094,1095],{"class":108},"@@ -10,6 +10,7 @@",[16,1097,237],{"class":87},[16,1099,1100],{"class":76,"line":115},[16,1101,267],{"class":87},[16,1103,1104],{"class":76,"line":121},[16,1105,272],{"class":87},[16,1107,1108],{"class":76,"line":127},[16,1109,1110],{"class":87},"   - production\n",[16,1112,1113],{"class":76,"line":134},[16,1114,1115],{"class":101},"+  - stop\n",[16,1117,1118],{"class":76,"line":140},[16,1119,131],{"emptyLinePlaceholder":130},[16,1121,1122],{"class":76,"line":146},[16,1123,131],{"emptyLinePlaceholder":130},[16,1125,1126],{"class":76,"line":152},[16,1127,1128],{"class":87}," .updateContainerJob:\n",[16,1130,1131,1134],{"class":76,"line":158},[16,1132,1133],{"class":108},"@@ -88,6 +89,7 @@",[16,1135,589],{"class":87},[16,1137,1138],{"class":76,"line":164},[16,1139,285],{"class":87},[16,1141,1142],{"class":76,"line":170},[16,1143,1144],{"class":87},"     name: review/$CI_COMMIT_REF_NAME\n",[16,1146,1147],{"class":76,"line":176},[16,1148,1149],{"class":87},"     url: https://appetize.io/app/$APPETIZE_PUBLIC_KEY\n",[16,1151,1152],{"class":76,"line":182},[16,1153,1154],{"class":101},"+    on_stop: stopReview\n",[16,1156,1157],{"class":76,"line":188},[16,1158,618],{"class":87},[16,1160,1161],{"class":76,"line":307},[16,1162,623],{"class":87},[16,1164,1165],{"class":76,"line":313},[16,1166,628],{"class":87},[16,1168,1169,1172],{"class":76,"line":319},[16,1170,1171],{"class":108},"@@ -100,6 +102,22 @@",[16,1173,589],{"class":87},[16,1175,1176],{"class":76,"line":325},[16,1177,1048],{"class":87},[16,1179,1180],{"class":76,"line":331},[16,1181,1053],{"class":87},[16,1183,1184],{"class":76,"line":337},[16,1185,131],{"emptyLinePlaceholder":130},[16,1187,1188],{"class":76,"line":343},[16,1189,1190],{"class":101},"+stopReview:\n",[16,1192,1193],{"class":76,"line":349},[16,1194,1195],{"class":101},"+  stage: stop\n",[16,1197,1198],{"class":76,"line":354},[16,1199,603],{"class":101},[16,1201,1202],{"class":76,"line":360},[16,1203,608],{"class":101},[16,1205,1206],{"class":76,"line":366},[16,1207,1208],{"class":101},"+    action: stop\n",[16,1210,1211],{"class":76,"line":843},[16,1212,1213],{"class":101},"+  variables:\n",[16,1215,1216],{"class":76,"line":848},[16,1217,1218],{"class":101},"+    GIT_STRATEGY: none\n",[16,1220,1221],{"class":76,"line":854},[16,1222,1223],{"class":101},"+  when: manual\n",[16,1225,1226],{"class":76,"line":860},[16,1227,328],{"class":101},[16,1229,1230],{"class":76,"line":866},[16,1231,334],{"class":101},[16,1233,1234],{"class":76,"line":871},[16,1235,340],{"class":101},[16,1237,1238],{"class":76,"line":877},[16,1239,346],{"class":101},[16,1241,1242],{"class":76,"line":883},[16,1243,316],{"class":101},[16,1245,1246],{"class":76,"line":888},[16,1247,1248],{"class":101},"+    - apt-get -y update && apt-get -y upgrade && apt-get -y install jq curl\n",[16,1250,1251],{"class":76,"line":896},[16,1252,1253],{"class":101},"+    - curl --request DELETE https://$APPETIZE_TOKEN@api.appetize.io/v1/apps/`jq -r '.publicKey' \u003C appetize-information.json`\n",[16,1255,1256],{"class":76,"line":902},[16,1257,173],{"class":101},[16,1259,1260],{"class":76,"line":908},[16,1261,357],{"class":87},[16,1263,1264],{"class":76,"line":913},[16,1265,363],{"class":87},[16,1267,1268],{"class":76,"line":919},[16,1269,369],{"class":87},[12,1271,1272,1273,1276,1277,1283,1284,1286,1287,1289,1290,1292,1293,1296,1297,1300],{},"Once your MR is merged and your branch is deleted, the ",[39,1274,1275],{},"stopReview"," job runs,\ncalling the ",[16,1278,1279,1282],{},[39,1280,1281],{},"DELETE"," endpoint of the Appetize.io API"," with the public key that\nis contained in ",[39,1285,990],{},". We don't need to fetch\n",[39,1288,990],{}," because the artifact is already present in our build\ncontext. This is because the ",[39,1291,1067],{}," stage happens ",[20,1294,1295],{},"after"," the ",[39,1298,1299],{},"review"," stage.",[12,1302,1303],{},[677,1304],{"alt":1305,"src":1306},"A merge request with a deployed review app","https://res.cloudinary.com/about-gitlab-com/image/upload/v1782398503/blog/Content%20Images/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io/merge-request-with-review-app.png",[51,1308,1310],{"id":1309},"conclusion","Conclusion",[12,1312,1313,1314,1316,1317,1320],{},"Thanks to some integration with ",[20,1315,22],{}," and the addition of a couple\nenvironment variables, having the ability to create review apps for an Android\nproject was surpsingly simple. GitLab's review apps are not ",[20,1318,1319],{},"just"," for web-based\nprojects, even though it may take a little tinkering to get working. Appetize.io\nalso supports iOS applications, so all mobile native applications can be turned\ninto review apps. I would love to see this strategy be applied to a React Native\nproject as well!",[1322,1323,1324],"style",{},"html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s9hmT, html code.shiki .s9hmT{--shiki-default:#B31D28}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}html pre.shiki code .soKTg, html code.shiki .soKTg{--shiki-default:#6F42C1;--shiki-default-font-weight:bold}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":71,"searchDepth":84,"depth":84,"links":1326},[1327,1328,1329,1330,1331],{"id":53,"depth":84,"text":54},{"id":383,"depth":84,"text":384},{"id":683,"depth":84,"text":684},{"id":1056,"depth":84,"text":1057},{"id":1309,"depth":84,"text":1310},"unfiltered","2020-05-06","See how GitLab and Appetize.io can bring Review Apps to your Android project","md",null,false,"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749664102/Blog/Hero%20Images/gitlab-values-cover.png",{},"/en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io",{"title":1342,"description":1334,"ogTitle":1342,"ogDescription":1334,"noIndex":1337,"ogImage":1338,"ogUrl":1343,"ogSiteName":1344,"ogType":1345,"canonicalUrls":1343},"Review Apps for Android with GitLab, fastlane & Appetize.io","https://about.gitlab.com/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io","https://about.gitlab.com","article","how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io","en-us/blog/how-to-create-review-apps-for-android-with-gitlab-fastlane-and-appetize-dot-io",[1349,1350,1351,1352],"CI/CD","integrations","features","tutorial","BlogPost","97RFzbPqruRqiQcGLibdyZqHCE_xeHqBkGI1hGp1M4o",{"logo":1356,"freeTrial":1361,"sales":1366,"login":1371,"items":1376,"search":1701,"minimal":1732,"duo":1751,"switchNav":1760,"pricingDeployment":1771},{"config":1357},{"href":1358,"dataGaName":1359,"dataGaLocation":1360},"/","gitlab logo","header",{"text":1362,"config":1363},"Get free trial",{"href":1364,"dataGaName":1365,"dataGaLocation":1360},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":1367,"config":1368},"Request a demo",{"href":1369,"dataGaName":1370,"dataGaLocation":1360},"/sales/?contact-topic=request-demo","sales",{"text":1372,"config":1373},"Sign in",{"href":1374,"dataGaName":1375,"dataGaLocation":1360},"https://gitlab.com/users/sign_in/","sign in",[1377,1406,1505,1510,1623,1679],{"text":1378,"config":1379,"menu":1381},"Platform",{"dataNavLevelOne":1380},"platform",{"type":1382,"columns":1383},"cards",[1384,1390,1398],{"title":1378,"description":1385,"link":1386},"The intelligent orchestration platform for DevSecOps",{"text":1387,"config":1388},"Explore our Platform",{"href":1389,"dataGaName":1380,"dataGaLocation":1360},"/platform/",{"title":1391,"description":1392,"link":1393},"GitLab Duo Agent Platform","Agentic AI for the entire software lifecycle",{"text":1394,"config":1395},"Meet GitLab Duo",{"href":1396,"dataGaName":1397,"dataGaLocation":1360},"/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":1399,"description":1400,"link":1401},"Why GitLab","See the top reasons enterprises choose GitLab",{"text":1402,"config":1403},"Learn more",{"href":1404,"dataGaName":1405,"dataGaLocation":1360},"/why-gitlab/","why gitlab",{"text":1407,"left":130,"config":1408,"menu":1410},"Product",{"dataNavLevelOne":1409},"solutions",{"type":1411,"link":1412,"columns":1416,"feature":1484},"lists",{"text":1413,"config":1414},"View all Solutions",{"href":1415,"dataGaName":1409,"dataGaLocation":1360},"/solutions/",[1417,1440,1463],{"title":1418,"description":1419,"link":1420,"items":1425},"Automation","CI/CD and automation to accelerate deployment",{"config":1421},{"icon":1422,"href":1423,"dataGaName":1424,"dataGaLocation":1360},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[1426,1429,1432,1436],{"text":1349,"config":1427},{"href":1428,"dataGaLocation":1360,"dataGaName":1349},"/solutions/continuous-integration/",{"text":1391,"config":1430},{"href":1396,"dataGaLocation":1360,"dataGaName":1431},"gitlab duo agent platform - product menu",{"text":1433,"config":1434},"Source Code Management",{"href":1435,"dataGaLocation":1360,"dataGaName":1433},"/solutions/source-code-management/",{"text":1437,"config":1438},"Automated Software Delivery",{"href":1423,"dataGaLocation":1360,"dataGaName":1439},"Automated software delivery",{"title":1441,"description":1442,"link":1443,"items":1448},"Security","Deliver code faster without compromising security",{"config":1444},{"href":1445,"dataGaName":1446,"dataGaLocation":1360,"icon":1447},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[1449,1453,1458],{"text":1450,"config":1451},"Application Security Testing",{"href":1445,"dataGaName":1452,"dataGaLocation":1360},"Application security testing",{"text":1454,"config":1455},"Software Supply Chain Security",{"href":1456,"dataGaLocation":1360,"dataGaName":1457},"/solutions/supply-chain/","Software supply chain security",{"text":1459,"config":1460},"Software Compliance",{"href":1461,"dataGaName":1462,"dataGaLocation":1360},"/solutions/software-compliance/","software compliance",{"title":1464,"link":1465,"items":1470},"Measurement",{"config":1466},{"icon":1467,"href":1468,"dataGaName":1469,"dataGaLocation":1360},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[1471,1475,1479],{"text":1472,"config":1473},"Visibility & Measurement",{"href":1468,"dataGaLocation":1360,"dataGaName":1474},"Visibility and Measurement",{"text":1476,"config":1477},"Value Stream Management",{"href":1478,"dataGaLocation":1360,"dataGaName":1476},"/solutions/value-stream-management/",{"text":1480,"config":1481},"Analytics & Insights",{"href":1482,"dataGaLocation":1360,"dataGaName":1483},"/solutions/analytics-and-insights/","Analytics and insights",{"title":1485,"type":1411,"items":1486},"GitLab for",[1487,1493,1499],{"text":1488,"config":1489},"Enterprise",{"icon":1490,"href":1491,"dataGaLocation":1360,"dataGaName":1492},"Building","/enterprise/","enterprise",{"text":1494,"config":1495},"Small Business",{"icon":1496,"href":1497,"dataGaLocation":1360,"dataGaName":1498},"Work","/small-business/","small business",{"text":1500,"config":1501},"Public Sector",{"icon":1502,"href":1503,"dataGaLocation":1360,"dataGaName":1504},"Organization","/solutions/public-sector/","public sector",{"text":1506,"config":1507},"Pricing",{"href":1508,"dataGaName":1509,"dataGaLocation":1360,"dataNavLevelOne":1509},"/pricing/","pricing",{"text":1511,"config":1512,"menu":1514},"Resources",{"dataNavLevelOne":1513},"resources",{"type":1411,"link":1515,"columns":1519,"feature":1612},{"text":1516,"config":1517},"View all resources",{"href":1518,"dataGaName":1513,"dataGaLocation":1360},"/resources/",[1520,1552,1579],{"title":1521,"items":1522},"Getting started",[1523,1528,1533,1538,1543,1548],{"text":1524,"config":1525},"Install",{"href":1526,"dataGaName":1527,"dataGaLocation":1360},"/install/","install",{"text":1529,"config":1530},"Quick start guides",{"href":1531,"dataGaName":1532,"dataGaLocation":1360},"/get-started/","quick setup checklists",{"text":1534,"config":1535},"Learn",{"href":1536,"dataGaLocation":1360,"dataGaName":1537},"https://university.gitlab.com/","learn",{"text":1539,"config":1540},"Product documentation",{"href":1541,"dataGaName":1542,"dataGaLocation":1360},"https://docs.gitlab.com/","product documentation",{"text":1544,"config":1545},"Best practice videos",{"href":1546,"dataGaName":1547,"dataGaLocation":1360},"/getting-started-videos/","best practice videos",{"text":1549,"config":1550},"Integrations",{"href":1551,"dataGaName":1350,"dataGaLocation":1360},"/integrations/",{"title":1553,"items":1554},"Discover",[1555,1560,1565,1570,1574],{"text":1556,"config":1557},"Customer success stories",{"href":1558,"dataGaName":1559,"dataGaLocation":1360},"/customers/","customer success stories",{"text":1561,"config":1562},"Blog",{"href":1563,"dataGaName":1564,"dataGaLocation":1360},"/blog/","blog",{"text":1566,"config":1567},"Demo Hub",{"href":1568,"dataGaName":1569,"dataGaLocation":1360},"/demo-hub/","demo hub",{"text":1571,"config":1572},"The Source",{"href":1573,"dataGaName":1564,"dataGaLocation":1360},"/the-source/",{"text":1575,"config":1576},"Remote",{"href":1577,"dataGaName":1578,"dataGaLocation":1360},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":1580,"items":1581},"Connect",[1582,1587,1592,1597,1602,1607],{"text":1583,"config":1584},"GitLab Services",{"href":1585,"dataGaName":1586,"dataGaLocation":1360},"/services/","services",{"text":1588,"config":1589},"Contribute",{"href":1590,"dataGaName":1591,"dataGaLocation":1360},"https://contributors.gitlab.com","contribute",{"text":1593,"config":1594},"Community",{"href":1595,"dataGaName":1596,"dataGaLocation":1360},"/community/","community",{"text":1598,"config":1599},"Forum",{"href":1600,"dataGaName":1601,"dataGaLocation":1360},"https://forum.gitlab.com/","forum",{"text":1603,"config":1604},"Events",{"href":1605,"dataGaName":1606,"dataGaLocation":1360},"/events/","events",{"text":1608,"config":1609},"Partners",{"href":1610,"dataGaName":1611,"dataGaLocation":1360},"/partners/","partners",{"config":1613,"title":1616,"text":1617,"link":1618},{"background":1614,"textColor":1615},"url('https://res.cloudinary.com/about-gitlab-com/image/upload/v1777322348/qpq8yrgn8knii57omj0c.png')","#000","What’s new in GitLab","Stay updated with our latest features and improvements.",{"text":1619,"config":1620},"Read the latest",{"href":1621,"dataGaName":1622,"dataGaLocation":1360},"/whats-new/","whats new",{"text":1624,"config":1625,"menu":1627},"Company",{"dataNavLevelOne":1626},"company",{"type":1411,"columns":1628},[1629],{"items":1630},[1631,1636,1642,1644,1649,1654,1659,1664,1669,1674],{"text":1632,"config":1633},"About",{"href":1634,"dataGaName":1635,"dataGaLocation":1360},"/company/","about",{"text":1637,"config":1638,"footerGa":1641},"Jobs",{"href":1639,"dataGaName":1640,"dataGaLocation":1360},"/jobs/","jobs",{"dataGaName":1640},{"text":1603,"config":1643},{"href":1605,"dataGaName":1606,"dataGaLocation":1360},{"text":1645,"config":1646},"Leadership",{"href":1647,"dataGaName":1648,"dataGaLocation":1360},"/company/team/e-group/","leadership",{"text":1650,"config":1651},"Handbook",{"href":1652,"dataGaName":1653,"dataGaLocation":1360},"https://handbook.gitlab.com/","handbook",{"text":1655,"config":1656},"Investor relations",{"href":1657,"dataGaName":1658,"dataGaLocation":1360},"https://ir.gitlab.com/overview/default.aspx","investor relations",{"text":1660,"config":1661},"Trust Center",{"href":1662,"dataGaName":1663,"dataGaLocation":1360},"/security/","trust center",{"text":1665,"config":1666},"AI Transparency Center",{"href":1667,"dataGaName":1668,"dataGaLocation":1360},"/ai-transparency-center/","ai transparency center",{"text":1670,"config":1671},"Newsletter",{"href":1672,"dataGaName":1673,"dataGaLocation":1360},"/company/contact/#contact-forms","newsletter",{"text":1675,"config":1676},"Press",{"href":1677,"dataGaName":1678,"dataGaLocation":1360},"/press/","press",{"text":1680,"config":1681,"menu":1682},"Contact us",{"dataNavLevelOne":1626},{"type":1411,"columns":1683},[1684],{"items":1685},[1686,1691,1696],{"text":1687,"config":1688},"Talk to sales",{"href":1689,"dataGaName":1690,"dataGaLocation":1360},"/sales/","talk to sales",{"text":1692,"config":1693},"Support portal",{"href":1694,"dataGaName":1695,"dataGaLocation":1360},"https://support.gitlab.com/hc/en-us","support portal",{"text":1697,"config":1698},"Customer portal",{"href":1699,"dataGaName":1700,"dataGaLocation":1360},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":1702,"login":1703,"suggestions":1710},"Close",{"text":1704,"link":1705},"To search repositories and projects, login to",{"text":1706,"config":1707},"gitlab.com",{"href":1374,"dataGaName":1708,"dataGaLocation":1709},"search login","search",{"text":1711,"default":1712},"Suggestions",[1713,1715,1719,1721,1725,1729],{"text":1391,"config":1714},{"href":1396,"dataGaName":1391,"dataGaLocation":1709},{"text":1716,"config":1717},"Code Suggestions (AI)",{"href":1718,"dataGaName":1716,"dataGaLocation":1709},"/solutions/code-suggestions/",{"text":1349,"config":1720},{"href":1428,"dataGaName":1349,"dataGaLocation":1709},{"text":1722,"config":1723},"GitLab on AWS",{"href":1724,"dataGaName":1722,"dataGaLocation":1709},"/partners/technology-partners/aws/",{"text":1726,"config":1727},"GitLab on Google Cloud",{"href":1728,"dataGaName":1726,"dataGaLocation":1709},"/partners/technology-partners/google-cloud-platform/",{"text":1730,"config":1731},"Why GitLab?",{"href":1404,"dataGaName":1730,"dataGaLocation":1709},{"freeTrial":1733,"mobileIcon":1738,"desktopIcon":1743,"secondaryButton":1746},{"text":1734,"config":1735},"Start free trial",{"href":1736,"dataGaName":1365,"dataGaLocation":1737},"https://gitlab.com/-/trials/new/","nav",{"altText":1739,"config":1740},"Gitlab Icon",{"src":1741,"dataGaName":1742,"dataGaLocation":1737},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":1739,"config":1744},{"src":1745,"dataGaName":1742,"dataGaLocation":1737},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":1747,"config":1748},"Get Started",{"href":1749,"dataGaName":1750,"dataGaLocation":1737},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":1752,"mobileIcon":1756,"desktopIcon":1758},{"text":1753,"config":1754},"Learn more about GitLab Duo",{"href":1396,"dataGaName":1755,"dataGaLocation":1737},"gitlab duo",{"altText":1739,"config":1757},{"src":1741,"dataGaName":1742,"dataGaLocation":1737},{"altText":1739,"config":1759},{"src":1745,"dataGaName":1742,"dataGaLocation":1737},{"button":1761,"mobileIcon":1766,"desktopIcon":1768},{"text":1762,"config":1763},"/switch",{"href":1764,"dataGaName":1765,"dataGaLocation":1737},"#contact","switch",{"altText":1739,"config":1767},{"src":1741,"dataGaName":1742,"dataGaLocation":1737},{"altText":1739,"config":1769},{"src":1770,"dataGaName":1742,"dataGaLocation":1737},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":1772,"mobileIcon":1777,"desktopIcon":1779},{"text":1773,"config":1774},"Back to pricing",{"href":1508,"dataGaName":1775,"dataGaLocation":1737,"icon":1776},"back to pricing","GoBack",{"altText":1739,"config":1778},{"src":1741,"dataGaName":1742,"dataGaLocation":1737},{"altText":1739,"config":1780},{"src":1745,"dataGaName":1742,"dataGaLocation":1737},{"title":1782,"titleMobile":1783,"button":1784,"config":1789},"Duo Agent Platform delivers 400% ROI, per new Forrester Consulting study.","400% ROI: Forrester TEI for GitLab Duo",{"text":1402,"config":1785},{"href":1786,"dataGaName":1787,"dataGaLocation":1788},"https://about.gitlab.com/blog/gitlab-duo-agent-platform-delivers-400-percent-roi/","forrester-tei-dap-banner","global-banner",{"layout":1790,"disabled":1337},"release",{"data":1792},{"text":1793,"source":1794,"edit":1800,"contribute":1805,"config":1810,"items":1815,"minimal":2025},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":1795,"config":1796},"View page source",{"href":1797,"dataGaName":1798,"dataGaLocation":1799},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":1801,"config":1802},"Edit this page",{"href":1803,"dataGaName":1804,"dataGaLocation":1799},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":1806,"config":1807},"Please contribute",{"href":1808,"dataGaName":1809,"dataGaLocation":1799},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":1811,"facebook":1812,"youtube":1813,"linkedin":1814},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[1816,1863,1917,1961,1993],{"title":1506,"links":1817,"subMenu":1832},[1818,1822,1827],{"text":1819,"config":1820},"View plans",{"href":1508,"dataGaName":1821,"dataGaLocation":1799},"view plans",{"text":1823,"config":1824},"Why Premium?",{"href":1825,"dataGaName":1826,"dataGaLocation":1799},"/pricing/premium/","why premium",{"text":1828,"config":1829},"Why Ultimate?",{"href":1830,"dataGaName":1831,"dataGaLocation":1799},"/pricing/ultimate/","why ultimate",[1833],{"title":1834,"links":1835},"Contact Us",[1836,1839,1841,1843,1848,1853,1858],{"text":1837,"config":1838},"Contact sales",{"href":1689,"dataGaName":1370,"dataGaLocation":1799},{"text":1692,"config":1840},{"href":1694,"dataGaName":1695,"dataGaLocation":1799},{"text":1697,"config":1842},{"href":1699,"dataGaName":1700,"dataGaLocation":1799},{"text":1844,"config":1845},"Status",{"href":1846,"dataGaName":1847,"dataGaLocation":1799},"https://status.gitlab.com/","status",{"text":1849,"config":1850},"Terms of use",{"href":1851,"dataGaName":1852,"dataGaLocation":1799},"/terms/","terms of use",{"text":1854,"config":1855},"Privacy statement",{"href":1856,"dataGaName":1857,"dataGaLocation":1799},"/privacy/","privacy statement",{"text":1859,"config":1860},"Cookie preferences",{"dataGaName":1861,"dataGaLocation":1799,"id":1862,"isOneTrustButton":130},"cookie preferences","ot-sdk-btn",{"title":1407,"links":1864,"subMenu":1873},[1865,1869],{"text":1866,"config":1867},"DevSecOps platform",{"href":1389,"dataGaName":1868,"dataGaLocation":1799},"devsecops platform",{"text":1870,"config":1871},"AI-Assisted Development",{"href":1396,"dataGaName":1872,"dataGaLocation":1799},"ai-assisted development",[1874],{"title":1875,"links":1876},"Topics",[1877,1882,1887,1892,1897,1902,1907,1912],{"text":1878,"config":1879},"CICD",{"href":1880,"dataGaName":1881,"dataGaLocation":1799},"/topics/ci-cd/","cicd",{"text":1883,"config":1884},"GitOps",{"href":1885,"dataGaName":1886,"dataGaLocation":1799},"/topics/gitops/","gitops",{"text":1888,"config":1889},"DevOps",{"href":1890,"dataGaName":1891,"dataGaLocation":1799},"/topics/devops/","devops",{"text":1893,"config":1894},"Version Control",{"href":1895,"dataGaName":1896,"dataGaLocation":1799},"/topics/version-control/","version control",{"text":1898,"config":1899},"DevSecOps",{"href":1900,"dataGaName":1901,"dataGaLocation":1799},"/topics/devsecops/","devsecops",{"text":1903,"config":1904},"Cloud Native",{"href":1905,"dataGaName":1906,"dataGaLocation":1799},"/topics/cloud-native/","cloud native",{"text":1908,"config":1909},"AI for Coding",{"href":1910,"dataGaName":1911,"dataGaLocation":1799},"/topics/devops/ai-for-coding/","ai for coding",{"text":1913,"config":1914},"Agentic AI",{"href":1915,"dataGaName":1916,"dataGaLocation":1799},"/topics/agentic-ai/","agentic ai",{"title":1918,"links":1919},"Solutions",[1920,1922,1924,1929,1933,1936,1940,1943,1945,1948,1951,1956],{"text":1450,"config":1921},{"href":1445,"dataGaName":1450,"dataGaLocation":1799},{"text":1439,"config":1923},{"href":1423,"dataGaName":1424,"dataGaLocation":1799},{"text":1925,"config":1926},"Agile development",{"href":1927,"dataGaName":1928,"dataGaLocation":1799},"/solutions/agile-delivery/","agile delivery",{"text":1930,"config":1931},"SCM",{"href":1435,"dataGaName":1932,"dataGaLocation":1799},"source code management",{"text":1878,"config":1934},{"href":1428,"dataGaName":1935,"dataGaLocation":1799},"continuous integration & delivery",{"text":1937,"config":1938},"Value stream management",{"href":1478,"dataGaName":1939,"dataGaLocation":1799},"value stream management",{"text":1883,"config":1941},{"href":1942,"dataGaName":1886,"dataGaLocation":1799},"/solutions/gitops/",{"text":1488,"config":1944},{"href":1491,"dataGaName":1492,"dataGaLocation":1799},{"text":1946,"config":1947},"Small business",{"href":1497,"dataGaName":1498,"dataGaLocation":1799},{"text":1949,"config":1950},"Public sector",{"href":1503,"dataGaName":1504,"dataGaLocation":1799},{"text":1952,"config":1953},"Education",{"href":1954,"dataGaName":1955,"dataGaLocation":1799},"/solutions/education/","education",{"text":1957,"config":1958},"Financial services",{"href":1959,"dataGaName":1960,"dataGaLocation":1799},"/solutions/finance/","financial services",{"title":1511,"links":1962},[1963,1965,1967,1969,1972,1974,1977,1979,1981,1983,1985,1987,1989,1991],{"text":1524,"config":1964},{"href":1526,"dataGaName":1527,"dataGaLocation":1799},{"text":1529,"config":1966},{"href":1531,"dataGaName":1532,"dataGaLocation":1799},{"text":1534,"config":1968},{"href":1536,"dataGaName":1537,"dataGaLocation":1799},{"text":1539,"config":1970},{"href":1541,"dataGaName":1971,"dataGaLocation":1799},"docs",{"text":1561,"config":1973},{"href":1563,"dataGaName":1564,"dataGaLocation":1799},{"text":1975,"config":1976},"What's new",{"href":1621,"dataGaName":1622,"dataGaLocation":1799},{"text":1556,"config":1978},{"href":1558,"dataGaName":1559,"dataGaLocation":1799},{"text":1575,"config":1980},{"href":1577,"dataGaName":1578,"dataGaLocation":1799},{"text":1583,"config":1982},{"href":1585,"dataGaName":1586,"dataGaLocation":1799},{"text":1588,"config":1984},{"href":1590,"dataGaName":1591,"dataGaLocation":1799},{"text":1593,"config":1986},{"href":1595,"dataGaName":1596,"dataGaLocation":1799},{"text":1598,"config":1988},{"href":1600,"dataGaName":1601,"dataGaLocation":1799},{"text":1603,"config":1990},{"href":1605,"dataGaName":1606,"dataGaLocation":1799},{"text":1608,"config":1992},{"href":1610,"dataGaName":1611,"dataGaLocation":1799},{"title":1624,"links":1994},[1995,1997,1999,2001,2003,2005,2009,2014,2016,2018,2020],{"text":1632,"config":1996},{"href":1634,"dataGaName":1626,"dataGaLocation":1799},{"text":1637,"config":1998},{"href":1639,"dataGaName":1640,"dataGaLocation":1799},{"text":1645,"config":2000},{"href":1647,"dataGaName":1648,"dataGaLocation":1799},{"text":1650,"config":2002},{"href":1652,"dataGaName":1653,"dataGaLocation":1799},{"text":1655,"config":2004},{"href":1657,"dataGaName":1658,"dataGaLocation":1799},{"text":2006,"config":2007},"Sustainability",{"href":2008,"dataGaName":2006,"dataGaLocation":1799},"/sustainability/",{"text":2010,"config":2011},"Diversity, inclusion and belonging (DIB)",{"href":2012,"dataGaName":2013,"dataGaLocation":1799},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":1660,"config":2015},{"href":1662,"dataGaName":1663,"dataGaLocation":1799},{"text":1670,"config":2017},{"href":1672,"dataGaName":1673,"dataGaLocation":1799},{"text":1675,"config":2019},{"href":1677,"dataGaName":1678,"dataGaLocation":1799},{"text":2021,"config":2022},"Modern Slavery Transparency Statement",{"href":2023,"dataGaName":2024,"dataGaLocation":1799},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":2026},[2027,2030,2033],{"text":2028,"config":2029},"Terms",{"href":1851,"dataGaName":1852,"dataGaLocation":1799},{"text":2031,"config":2032},"Cookies",{"dataGaName":1861,"dataGaLocation":1799,"id":1862,"isOneTrustButton":130},{"text":2034,"config":2035},"Privacy",{"href":1856,"dataGaName":1857,"dataGaLocation":1799},[2037],{"id":2038,"title":7,"body":1336,"config":2039,"content":2041,"description":1336,"extension":2045,"meta":2046,"navigation":130,"path":2047,"seo":2048,"stem":2049,"__hash__":2050},"blogAuthors/en-us/blog/authors/andrew-fontaine.yml",{"template":2040},"BlogAuthor",{"name":7,"config":2042},{"headshot":2043,"ctfId":2044},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749672447/Blog/Author%20Headshots/afontaine-headshot.jpg","afontaine","yml",{},"/en-us/blog/authors/andrew-fontaine",{},"en-us/blog/authors/andrew-fontaine","kN6bHMAg-RIWbJyCicccQWOBGEY6BPs1UGnwX_7i1wA",[2052,2059,2067],{"title":2053,"description":2054,"heroImage":1338,"category":1332,"date":2055,"authors":2056,"slug":2058,"externalUrl":1336},"CEO Shadow Takeaways from Jacie","Recap of my experience in the CEO Shadow Program.","2021-05-18",[2057],"Jacie Bandur","ceo-shadow-recap",{"title":2060,"description":2061,"heroImage":2062,"category":1332,"date":2063,"authors":2064,"slug":2066,"externalUrl":1336},"Why I love contributing to GitLab","Making small meaningful changes is what it's all about.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749679501/Blog/Hero%20Images/new-feature.png","2021-05-11",[2065],"Austin Regnery","why-i-love-contributing-to-gitlab",{"title":2068,"description":2069,"heroImage":2070,"category":1332,"date":2063,"authors":2071,"slug":2073,"externalUrl":1336},"Placebo Lines on the Pipeline Graph","Have you noticed the connecting lines missing on your pipelines lately? Here's why","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749679507/Blog/Hero%20Images/ci-cd.png",[2072],"Sam Beckham","placebo-lines-on-the-pipeline-graph",{"promotions":2075},[2076,2090,2102,2114],{"id":2077,"categories":2078,"header":2080,"text":2081,"button":2082,"image":2087},"ai-modernization",[2079],"ai","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":2083,"config":2084},"Get your AI maturity score",{"href":2085,"dataGaName":2086,"dataGaLocation":1564},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":2088},{"src":2089},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":2091,"categories":2092,"header":2094,"text":2081,"button":2095,"image":2099},"devops-modernization",[2093,1901],"product","Are you just managing tools or shipping innovation?",{"text":2096,"config":2097},"Get your DevOps maturity score",{"href":2098,"dataGaName":2086,"dataGaLocation":1564},"/assessments/devops-modernization-assessment/",{"config":2100},{"src":2101},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":2103,"categories":2104,"header":2106,"text":2081,"button":2107,"image":2111},"security-modernization",[2105],"security","Are you trading speed for security?",{"text":2108,"config":2109},"Get your security maturity score",{"href":2110,"dataGaName":2086,"dataGaLocation":1564},"/assessments/security-modernization-assessment/",{"config":2112},{"src":2113},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":2115,"paths":2116,"header":2119,"text":2120,"button":2121,"image":2126},"github-azure-migration",[2117,2118],"migration-from-azure-devops-to-gitlab","integrating-azure-devops-scm-and-gitlab","Is your team ready for GitHub's Azure move?","GitHub is already rebuilding around Azure. Find out what it means for you.",{"text":2122,"config":2123},"See how GitLab compares to GitHub",{"href":2124,"dataGaName":2125,"dataGaLocation":1564},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":2127},{"src":2101},{"header":2129,"blurb":2130,"button":2131,"secondaryButton":2136},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":2132,"config":2133},"Get your free trial",{"href":2134,"dataGaName":1365,"dataGaLocation":2135},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":1837,"config":2137},{"href":1689,"dataGaName":1370,"dataGaLocation":2135},1786803751735]