[{"data":1,"prerenderedAt":2175},["ShallowReactive",2],{"/blog/open-sourcing-the-gitter-mobile-apps":3,"navigation-en-us":1390,"banner-en-us":1816,"footer-en-us":1826,"blog-post-authors-en-us-Eric Eastwood":2071,"blog-related-posts-en-us-open-sourcing-the-gitter-mobile-apps":2085,"blog-promotions-en-us":2111,"next-steps-en-us":2165},{"id":4,"title":5,"authors":6,"body":8,"category":1368,"date":1369,"description":1370,"extension":1371,"externalUrl":1372,"faq":1372,"featured":1373,"heroImage":1374,"meta":1375,"navigation":327,"path":1376,"seo":1377,"slug":1381,"stem":1382,"tags":1383,"template":1388,"updatedDate":1372,"__hash__":1389},"blogPosts/en-us/blog/open-sourcing-the-gitter-mobile-apps.md","Open-sourcing the Gitter mobile apps",[7],"Eric Eastwood",{"type":9,"value":10,"toc":1353},"minimark",[11,34,37,59,62,76,80,94,108,113,123,155,159,162,206,221,226,292,297,305,308,344,349,369,381,385,397,404,407,446,452,456,463,491,498,501,512,523,526,531,568,578,581,592,601,604,620,628,633,841,849,854,860,865,1187,1190,1264,1268,1275,1278,1284,1288,1325,1335,1349],[12,13,14,15,21,22,27,28,33],"p",{},"Before we acquired Gitter most every part of Gitter was private/closed-source. The main ",[16,17,20],"a",{"href":18,"rel":19},"https://gitlab.com/gitlab-org/gitter/webapp",[],"webapp"," was open-sourced in June 2017 and got both mobile ",[16,23,26],{"href":24,"rel":25},"https://gitlab.com/gitlab-org/gitter/gitter-android-app",[],"Android","/",[16,29,32],{"href":30,"rel":31},"https://gitlab.com/gitlab-org/gitter/gitter-ios-app",[],"iOS"," apps open sourced in September 2018. If you would like to come help out, feel free to send us a merge request! This blog post will go over some the technical details of making the projects available for anyone to contribute.",[12,35,36],{},"Here is the basic overview:",[38,39,40,44,47,50],"ol",{},[41,42,43],"li",{},"Find secrets in the current state of the project (don't worry about the commit history) and move to some config that isn't tracked in the repo.",[41,45,46],{},"Find/remove secrets throughout the whole repo commit history.",[41,48,49],{},"Make the project public 🎉",[41,51,52,53],{},"Caveats:\n",[54,55,56],"ul",{},[41,57,58],{},"Because we are rewriting the git history, I don't know of a way to keep merge requests/pull requests because the MRs reference the old commit hashes.",[12,60,61],{},"Quick navigation:",[54,63,64,70],{},[41,65,66],{},[16,67,69],{"href":68},"#android","Jump to open sourcing Android",[41,71,72],{},[16,73,75],{"href":74},"#ios","Jump to open sourcing iOS",[77,78,26],"h2",{"id":79},"android",[12,81,82,83,87,88,93],{},"If you want to check out the full project and final result, you can check out the ",[16,84,86],{"href":24,"rel":85},[],"project on GitLab"," (",[16,89,92],{"href":90,"rel":91},"https://twitter.com/gitchat/status/1027293167471812611",[],"open-sourced 2018-8-8",").",[12,95,96,97,102,103,107],{},"To start out, we used the ",[16,98,101],{"href":99,"rel":100},"https://docs.gitlab.com/user/project/import/github/",[],"GitHub to GitLab project import"," to move the private GitHub project over to GitLab. We named it ",[104,105,106],"code",{},"gitter-android-app2"," so that later on we could create the actual clean public project without any of the orphaned git references that may potentially leak.",[109,110,112],"h3",{"id":111},"finding-secrets","Finding secrets",[12,114,115,122],{},[16,116,119],{"href":117,"rel":118},"https://github.com/dxa4481/truffleHog",[],[104,120,121],{},"truffleHog"," will search for high entropy strings (like tokens/passwords) through the entire git repo history. It's also useful to find all the potential areas where secrets may still exist in the current state of the project. Some sticky points we encountered while using include:",[54,124,125,138],{},[41,126,127,128,131,132,137],{},"\"I wish we could just search the current state of the project instead of all git history (the ",[104,129,130],{},"--max_depth=2"," argument will just make it search the diff of the latest commit)\" ",[16,133,136],{"href":134,"rel":135},"https://github.com/dxa4481/truffleHog/issues/92",[],"dxa4481/truffleHog#92",".",[41,139,140,141,144,145,149,150,137],{},"\"The output will show the entire diff for the triggered commit which is a bit burdensome to see exactly what is wrong. The JSON output ",[104,142,143],{},"--json"," is sometimes easier to understand\" ",[16,146,147],{"href":147,"rel":148},"https://github.com/dxa4481/truffleHog/issues/58",[]," or ",[16,151,154],{"href":152,"rel":153},"https://github.com/dxa4481/truffleHog/issues/102",[],"dxa4481/truffleHog#102",[109,156,158],{"id":157},"moving-secrets-to-untracked-config","Moving secrets to untracked config",[12,160,161],{},"Once we figure out where all of the secrets are we need a config/variable solution that isn't tracked by git but still lets them be available when building. We also wanted the solution to work in GitLab CI for some sanity builds/testing. There are lots of good articles on this topic:",[54,163,164,171,178,185,192,199],{},[41,165,166],{},[16,167,170],{"href":168,"rel":169},"https://developer.android.com/studio/build/gradle-tips#remove-private-signing-information-from-your-project",[],"Remove private signing information from your project",[41,172,173],{},[16,174,177],{"href":175,"rel":176},"https://medium.com/@geocohn/keeping-your-android-projects-secrets-secret-393b8855765d",[],"Keeping Your Android Project’s Secrets Secret",[41,179,180],{},[16,181,184],{"href":182,"rel":183},"https://rammic.github.io/2015/07/28/hiding-secrets-in-android-apps/",[],"Hiding Secrets in Android Apps",[41,186,187],{},[16,188,191],{"href":189,"rel":190},"https://joshmcarthur.com/2014/02/16/keeping-secrets-in-an-android-application.html",[],"Keeping secrets in an Android Application",[41,193,194],{},[16,195,198],{"href":196,"rel":197},"https://gist.github.com/curioustechizen/9f7d745f9f5f51355bd6",[],"Android: Loading API Keys and other secrets from properties file using gradle",[41,200,201],{},[16,202,205],{"href":203,"rel":204},"https://arstechnica.com/information-technology/2013/12/how-can-i-keep-api-keys-out-of-source-control/",[],"How can I keep API keys out of source control?",[12,207,208,209,212,213,215,216,137],{},"Our solution is completely based on the information in these articles. We chose to go the route of defining things in a ",[104,210,211],{},"secrets.properties"," file which can easily be read in the Gradle build script which handles the build even when using Android Studio. If the ",[104,214,211],{}," file doesn't exist (like in CI), it will try to read the secrets from ",[16,217,220],{"href":218,"rel":219},"https://docs.gitlab.com/ci/variables/",[],"environment variables which can easily be supplied in the project settings",[12,222,223],{},[104,224,225],{},"secerts.properties",[227,228,233],"pre",{"className":229,"code":230,"language":231,"meta":232,"style":232},"language-properties shiki shiki-themes github-light","# Visit https://developer.gitter.im/apps (sign in) and create a new app\n# Name: my-gitter-android-app (can be anything)\n# Redirect URL: https://gitter.im/login/oauth/callback\noauth_client_id=\"...\"\noauth_client_secret=\"...\"\noauth_redirect_uri=\"https://gitter.im/login/oauth/callback\"\n","properties","",[104,234,235,244,250,256,271,281],{"__ignoreMap":232},[236,237,240],"span",{"class":238,"line":239},"line",1,[236,241,243],{"class":242},"sAwPA","# Visit https://developer.gitter.im/apps (sign in) and create a new app\n",[236,245,247],{"class":238,"line":246},2,[236,248,249],{"class":242},"# Name: my-gitter-android-app (can be anything)\n",[236,251,253],{"class":238,"line":252},3,[236,254,255],{"class":242},"# Redirect URL: https://gitter.im/login/oauth/callback\n",[236,257,259,263,267],{"class":238,"line":258},4,[236,260,262],{"class":261},"sD7c4","oauth_client_id",[236,264,266],{"class":265},"sgsFI","=",[236,268,270],{"class":269},"sYBdl","\"...\"\n",[236,272,274,277,279],{"class":238,"line":273},5,[236,275,276],{"class":261},"oauth_client_secret",[236,278,266],{"class":265},[236,280,270],{"class":269},[236,282,284,287,289],{"class":238,"line":283},6,[236,285,286],{"class":261},"oauth_redirect_uri",[236,288,266],{"class":265},[236,290,291],{"class":269},"\"https://gitter.im/login/oauth/callback\"\n",[12,293,294],{},[104,295,296],{},"build.gradle",[227,298,303],{"className":299,"code":301,"language":302,"meta":232},[300],"language-text","apply plugin: 'com.android.application'\n\n// Try reading secrets from file\ndef secretsPropertiesFile = rootProject.file(\"secrets.properties\")\ndef secretProperties = new Properties()\nif (secretsPropertiesFile.exists()) {\n    secretProperties.load(new FileInputStream(secretsPropertiesFile))\n}\n// Otherwise read from environment variables, this happens in CI\nelse {\n    secretProperties.setProperty(\"oauth_client_id\", \"\\\"${System.getenv('oauth_client_id')}\\\"\")\n    secretProperties.setProperty(\"oauth_client_secret\", \"\\\"${System.getenv('oauth_client_secret')}\\\"\")\n    secretProperties.setProperty(\"oauth_redirect_uri\", \"\\\"${System.getenv('oauth_redirect_uri')}\\\"\")\n}\n\nandroid {\n    ...\n\n    defaultConfig {\n        ...\n\n        buildConfigField(\"String\", \"oauth_client_id\", \"${secretProperties['oauth_client_id']}\")\n        buildConfigField(\"String\", \"oauth_client_secret\", \"${secretProperties['oauth_client_secret']}\")\n        buildConfigField(\"String\", \"oauth_redirect_uri\", \"${secretProperties['oauth_redirect_uri']}\")\n    }\n    ...\n}\n","text",[104,304,301],{"__ignoreMap":232},[12,306,307],{},"Use the config variables in the Java app:",[227,309,313],{"className":310,"code":311,"language":312,"meta":232,"style":232},"language-java shiki shiki-themes github-light","import im.gitter.gitter.BuildConfig;\n\nBuildConfig.oauth_client_id;\nBuildConfig.oauth_client_secret;\nBuildConfig.oauth_redirect_uri;\n","java",[104,314,315,323,329,334,339],{"__ignoreMap":232},[236,316,317,320],{"class":238,"line":239},[236,318,319],{"class":261},"import",[236,321,322],{"class":265}," im.gitter.gitter.BuildConfig;\n",[236,324,325],{"class":238,"line":246},[236,326,328],{"emptyLinePlaceholder":327},true,"\n",[236,330,331],{"class":238,"line":252},[236,332,333],{"class":265},"BuildConfig.oauth_client_id;\n",[236,335,336],{"class":238,"line":258},[236,337,338],{"class":265},"BuildConfig.oauth_client_secret;\n",[236,340,341],{"class":238,"line":273},[236,342,343],{"class":265},"BuildConfig.oauth_redirect_uri;\n",[345,346,348],"h4",{"id":347},"removing-compiled-assets","Removing compiled assets",[12,350,351,352,355,356,362,363,368],{},"We use a ",[104,353,354],{},"WebView"," to display the HTML markdown messages in the chat room. This view uses assets built from the main ",[16,357,359,361],{"href":18,"rel":358},[],[104,360,20],{}," project",". Because these assets had some inlined production ",[16,364,366],{"href":18,"rel":365},[],[104,367,20],{}," secrets that whole directory needed to be removed.",[12,370,371,372,377,378,380],{},"Initially, we opted to have the developer build these assets with their own secrets and symlink the build output directory. The ",[16,373,376],{"href":374,"rel":375},"https://gitlab.com/gitlab-org/gitter/gitter-android-app/merge_requests/113",[],"community made this even simpler",", so now there is just a Gradle task to run which fetches the latest build we have available from the ",[104,379,20],{}," GitLab CI.",[109,382,384],{"id":383},"removing-secrets-from-the-repo-history","Removing secrets from the repo history",[12,386,387,388,390,391,396],{},"From your ",[104,389,121],{}," results earlier, you should know where secrets were stored throughout the history. We can use ",[16,392,395],{"href":393,"rel":394},"https://rtyley.github.io/bfg-repo-cleaner/",[],"BFG Repo-Cleaner"," to remove and rewrite the repo history quickly.",[12,398,399,400,403],{},"When using BFG, I wanted just to rewrite all of the sensitive values in ",[104,401,402],{},"app/src/main/res/values/settings.xml"," instead of completely removing them, but rewriting isn't an option with BFG so I went ahead with deleting it and recreated it in a commit afterwards. 🤷",[12,405,406],{},"For the Android app, here are the BFG commands I used,",[54,408,409,422,433],{},[41,410,411,412,415],{},"Remove ",[104,413,414],{},"app/src/main/assets/www/",[54,416,417],{},[41,418,419],{},[104,420,421],{},"java -jar \"bfg.jar\" --delete-folders www",[41,423,411,424,426],{},[104,425,402],{},[54,427,428],{},[41,429,430],{},[104,431,432],{},"java -jar \"bfg.jar\" --delete-files settings.xml",[41,434,435,436,438,439],{},"Remove sensitive strings where we can't just remove the whole file (collected from ",[104,437,121],{}," results)\n",[54,440,441],{},[41,442,443],{},[104,444,445],{},"java -jar \"bfg.jar\" --replace-text \"gitter-android-bad-words.txt\"",[12,447,448,449,451],{},"After you think you removed all the secrets, it's best to run ",[104,450,121],{}," again just to make sure no secrets are leftover. 😉",[109,453,455],{"id":454},"make-it-public","Make it public",[12,457,458,459,462],{},"Now it's time to update your ",[104,460,461],{},"readme"," with some setup instruction so the community knows how to contribute.",[12,464,465,466,470,471,470,474,477,478,481,482,485,486,490],{},"This is the scary part 😅. Go to ",[467,468,469],"strong",{},"Project settings"," > ",[467,472,473],{},"General",[467,475,476],{},"Permissions"," > set ",[467,479,480],{},"Project visibility"," as ",[467,483,484],{},"Public",". You can read more about project access in the GitLab documentation.\nCurious about how to setup builds in GitLab CI? ",[16,487,489],{"href":488},"/blog/setting-up-gitlab-ci-for-android-projects/","Learn more from this blog post",", which was what we used to set it up for our projects.",[12,492,493,494,137],{},"You can even learn how we ",[16,495,497],{"href":496},"/blog/android-publishing-with-gitlab-and-fastlane/","automated the release process so we can publish straight to the Google Play Store from GitLab CI via fastlane 🚀",[77,499,32],{"id":500},"ios",[12,502,503,504,87,507,93],{},"If you want to see the full project and final result, you can check out the ",[16,505,86],{"href":30,"rel":506},[],[16,508,511],{"href":509,"rel":510},"https://twitter.com/gitchat/status/1041795909103898625",[],"open-sourced 2018-9-18",[12,513,514,515,518,519,522],{},"The same concepts apply from the Android section. We create a separate private project, ",[104,516,517],{},"gitter-ios-app2",", where we can work and later on, we can create the actual clean public project(",[104,520,521],{},"gitter-ios-app",") without any of the orphaned git references that could leak.",[109,524,112],{"id":525},"finding-secrets-1",[12,527,528,530],{},[104,529,121],{}," didn't work well in the iOS project because there was a bunch of generated XCode files that had file hashes (high entropy strings which truffleHog looks for) – which meant every commit was listed. 🤦‍ Instead of trying to find something to filter the results down or get another tool, I decided just search manually. Here is the list of things we looked for:",[54,532,533,538,543,548,553,558,563],{},[41,534,535],{},[104,536,537],{},"token",[41,539,540],{},[104,541,542],{},"secret",[41,544,545],{},[104,546,547],{},"key",[41,549,550],{},[104,551,552],{},"cert",[41,554,555],{},[104,556,557],{},"api",[41,559,560],{},[104,561,562],{},"pw",[41,564,565],{},[104,566,567],{},"password",[12,569,570,571,574,575],{},"I used this directory filter when ",[104,572,573],{},"Ctrl + f"," those strings above to avoid finding things outside of the repo itself (copy-paste for Atom editor): ",[104,576,577],{},"!Common/,!Libraries,!Gitter/www,!Pods/,!xctool",[109,579,158],{"id":580},"moving-secrets-to-untracked-config-1",[12,582,583,584,591],{},"The iOS app uses a few git sub-modules which we also had to check for secrets before making them public. It turned out only one of the sub-modules – ",[16,585,588],{"href":586,"rel":587},"https://gitlab.com/gitlab-org/gitter/troupeobjccommon",[],[104,589,590],{},"troupeobjccommon"," – had secrets of it's own so I ran through the same secret removal process.",[12,593,594,595,597,598,600],{},"We had the same OAuth secrets in the main part of the iOS app, but since ",[104,596,590],{}," was also trying to handle OAuth secret settings, we opted for putting the new logic in ",[104,599,590],{}," to avoid having to refactor whatever other downstream code that uses the same submodule (like the macOS desktop app).",[12,602,603],{},"Here are some articles around handling secrets in an iOS project,",[54,605,606,613],{},[41,607,608],{},[16,609,612],{"href":610,"rel":611},"https://medium.com/flawless-app-stories/secret-variables-in-xcode-and-your-ci-for-fun-and-profit-d387a50475d7",[],"Secret variables in Xcode AND your CI for fun and profit 💌",[41,614,615],{},[16,616,619],{"href":617,"rel":618},"https://medium.com/@jules2689/secrets-management-in-ios-applications-52795c254ec1",[],"Secrets Management in iOS Applications",[12,621,622,623,137],{},"Since iOS apps can only be built on macOS and we don't have any macOS GitLab CI runners, our solution doesn't have to be CI compatible. You can track ",[16,624,627],{"href":625,"rel":626},"https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5720",[],"this issue for shared macOS GitLab CI runners",[12,629,630],{},[104,631,632],{},"Gitter/GitterSecrets-Dev.plist",[227,634,638],{"className":635,"code":636,"language":637,"meta":232,"style":232},"language-xml shiki shiki-themes github-light","\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003C!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\u003Cplist version=\"1.0\">\n\u003Cdict>\n  \u003C!--\n  Visit https://developer.gitter.im/apps (sign in) and create a new app\n  Name: my-gitter-ios-app (can be anything)\n  Redirect URL: https://gitter.im/login/oauth/callback\n  -->\n  \u003Ckey>OAuthClientId\u003C/key>\n  \u003Cstring>\u003C/string>\n  \u003Ckey>OAuthClientSecret\u003C/key>\n  \u003Cstring>\u003C/string>\n  \u003Ckey>OAuthCallback\u003C/key>\n  \u003Cstring>https://gitter.im/login/oauth/callback\u003C/string>\n\u003C/dict>\n\u003C/plist>\n","xml",[104,639,640,668,683,700,709,714,719,725,731,737,752,767,781,794,808,822,832],{"__ignoreMap":232},[236,641,642,645,648,652,654,657,660,662,665],{"class":238,"line":239},[236,643,644],{"class":265},"\u003C?",[236,646,637],{"class":647},"shJU0",[236,649,651],{"class":650},"s7eDp"," version",[236,653,266],{"class":265},[236,655,656],{"class":269},"\"1.0\"",[236,658,659],{"class":650}," encoding",[236,661,266],{"class":265},[236,663,664],{"class":269},"\"UTF-8\"",[236,666,667],{"class":265},"?>\n",[236,669,670,673,676,680],{"class":238,"line":246},[236,671,672],{"class":265},"\u003C!",[236,674,675],{"class":261},"DOCTYPE",[236,677,679],{"class":678},"sYu0t"," plist",[236,681,682],{"class":265}," PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n",[236,684,685,688,691,693,695,697],{"class":238,"line":252},[236,686,687],{"class":265},"\u003C",[236,689,690],{"class":647},"plist",[236,692,651],{"class":650},[236,694,266],{"class":265},[236,696,656],{"class":269},[236,698,699],{"class":265},">\n",[236,701,702,704,707],{"class":238,"line":258},[236,703,687],{"class":265},[236,705,706],{"class":647},"dict",[236,708,699],{"class":265},[236,710,711],{"class":238,"line":273},[236,712,713],{"class":242},"  \u003C!--\n",[236,715,716],{"class":238,"line":283},[236,717,718],{"class":242},"  Visit https://developer.gitter.im/apps (sign in) and create a new app\n",[236,720,722],{"class":238,"line":721},7,[236,723,724],{"class":242},"  Name: my-gitter-ios-app (can be anything)\n",[236,726,728],{"class":238,"line":727},8,[236,729,730],{"class":242},"  Redirect URL: https://gitter.im/login/oauth/callback\n",[236,732,734],{"class":238,"line":733},9,[236,735,736],{"class":242},"  -->\n",[236,738,740,743,745,748,750],{"class":238,"line":739},10,[236,741,742],{"class":265},"  \u003C",[236,744,547],{"class":647},[236,746,747],{"class":265},">OAuthClientId\u003C/",[236,749,547],{"class":647},[236,751,699],{"class":265},[236,753,755,757,760,763,765],{"class":238,"line":754},11,[236,756,742],{"class":265},[236,758,759],{"class":647},"string",[236,761,762],{"class":265},">\u003C/",[236,764,759],{"class":647},[236,766,699],{"class":265},[236,768,770,772,774,777,779],{"class":238,"line":769},12,[236,771,742],{"class":265},[236,773,547],{"class":647},[236,775,776],{"class":265},">OAuthClientSecret\u003C/",[236,778,547],{"class":647},[236,780,699],{"class":265},[236,782,784,786,788,790,792],{"class":238,"line":783},13,[236,785,742],{"class":265},[236,787,759],{"class":647},[236,789,762],{"class":265},[236,791,759],{"class":647},[236,793,699],{"class":265},[236,795,797,799,801,804,806],{"class":238,"line":796},14,[236,798,742],{"class":265},[236,800,547],{"class":647},[236,802,803],{"class":265},">OAuthCallback\u003C/",[236,805,547],{"class":647},[236,807,699],{"class":265},[236,809,811,813,815,818,820],{"class":238,"line":810},15,[236,812,742],{"class":265},[236,814,759],{"class":647},[236,816,817],{"class":265},">https://gitter.im/login/oauth/callback\u003C/",[236,819,759],{"class":647},[236,821,699],{"class":265},[236,823,825,828,830],{"class":238,"line":824},16,[236,826,827],{"class":265},"\u003C/",[236,829,706],{"class":647},[236,831,699],{"class":265},[236,833,835,837,839],{"class":238,"line":834},17,[236,836,827],{"class":265},[236,838,690],{"class":647},[236,840,699],{"class":265},[12,842,843,848],{},[16,844,846],{"href":586,"rel":845},[],[104,847,590],{}," is in Objective-C",[12,850,851],{},[104,852,853],{},"TRAppSettings.h",[227,855,858],{"className":856,"code":857,"language":302,"meta":232},[300],"#import \u003CFoundation/Foundation.h>\n\n@interface TRAppSettings : NSObject\n\n+ (TRAppSettings *) sharedInstance;\n\n- (NSString *) clientID;\n\n- (NSString *) clientSecret;\n\n- (NSString *) oauthScope;\n\n@end\n",[104,859,857],{"__ignoreMap":232},[12,861,862],{},[104,863,864],{},"TRAppSettings.m",[227,866,870],{"className":867,"code":868,"language":869,"meta":232,"style":232},"language-objc shiki shiki-themes github-light","@interface TRAppSettings ()\n\n@property (strong, nonatomic) NSUserDefaults *secrets;\n\n@end\n\nstatic TRAppSettings *sharedAppSettingsSingleton;\n\n@implementation TRAppSettings {\n    int firstRunPostUpdate;\n}\n\n+ (void)initialize\n{\n    static BOOL initialized = NO;\n    if(!initialized)\n    {\n        initialized = YES;\n        sharedAppSettingsSingleton = [[TRAppSettings alloc] init];\n    }\n\n    NSLog(@\"Pulling secrets from SECRETS_PLIST = %@.plist\", SECRETS_PLIST);\n}\n\n+ (TRAppSettings *) sharedInstance\n{\n    return sharedAppSettingsSingleton;\n}\n\n- (id)init {\n    NSString *troupeSecretsPath = [[NSBundle mainBundle] pathForResource:\"GitterSecrets-Dev\" ofType:@\"plist\"];\n    if(troupeSecretsPath == nil) {\n        NSString *failureReason = [NSString stringWithFormat:@\"Gitter secrets file not found in bundle: %@.plist. You probably need to add it to the `Gitter/Supporting Files` in Xcode navigator\", SECRETS_PLIST];\n        NSException* exception = [NSException\n            exceptionWithName:@\"FileNotFoundException\"\n            reason:failureReason\n            userInfo:nil];\n\n        NSLog(@\"%@\", failureReason);\n\n        [exception raise];\n    }\n    NSDictionary *troupeSecrets = [NSDictionary dictionaryWithContentsOfFile:troupeSecretsPath];\n\n    self.secrets = [NSUserDefaults standardUserDefaults];\n    [self.secrets registerDefaults:troupeSecrets];\n}\n\n- (NSString *) clientID {\n    return [self.secrets stringForKey:@\"OAuthClientId\"];\n}\n\n- (NSString *) clientSecret {\n    return [self.secrets stringForKey:@\"OAuthClientSecret\"];\n}\n\n- (NSString *)oauthScope {\n    return [self.secrets stringForKey:@\"OAuthCallback\"];\n}\n","objc",[104,871,872,877,881,886,890,895,899,904,908,913,918,923,927,932,937,942,947,952,958,964,970,975,981,986,991,997,1002,1008,1013,1018,1024,1030,1036,1042,1048,1054,1060,1066,1071,1077,1082,1088,1093,1099,1104,1110,1116,1121,1126,1132,1138,1143,1148,1154,1160,1165,1170,1176,1182],{"__ignoreMap":232},[236,873,874],{"class":238,"line":239},[236,875,876],{},"@interface TRAppSettings ()\n",[236,878,879],{"class":238,"line":246},[236,880,328],{"emptyLinePlaceholder":327},[236,882,883],{"class":238,"line":252},[236,884,885],{},"@property (strong, nonatomic) NSUserDefaults *secrets;\n",[236,887,888],{"class":238,"line":258},[236,889,328],{"emptyLinePlaceholder":327},[236,891,892],{"class":238,"line":273},[236,893,894],{},"@end\n",[236,896,897],{"class":238,"line":283},[236,898,328],{"emptyLinePlaceholder":327},[236,900,901],{"class":238,"line":721},[236,902,903],{},"static TRAppSettings *sharedAppSettingsSingleton;\n",[236,905,906],{"class":238,"line":727},[236,907,328],{"emptyLinePlaceholder":327},[236,909,910],{"class":238,"line":733},[236,911,912],{},"@implementation TRAppSettings {\n",[236,914,915],{"class":238,"line":739},[236,916,917],{},"    int firstRunPostUpdate;\n",[236,919,920],{"class":238,"line":754},[236,921,922],{},"}\n",[236,924,925],{"class":238,"line":769},[236,926,328],{"emptyLinePlaceholder":327},[236,928,929],{"class":238,"line":783},[236,930,931],{},"+ (void)initialize\n",[236,933,934],{"class":238,"line":796},[236,935,936],{},"{\n",[236,938,939],{"class":238,"line":810},[236,940,941],{},"    static BOOL initialized = NO;\n",[236,943,944],{"class":238,"line":824},[236,945,946],{},"    if(!initialized)\n",[236,948,949],{"class":238,"line":834},[236,950,951],{},"    {\n",[236,953,955],{"class":238,"line":954},18,[236,956,957],{},"        initialized = YES;\n",[236,959,961],{"class":238,"line":960},19,[236,962,963],{},"        sharedAppSettingsSingleton = [[TRAppSettings alloc] init];\n",[236,965,967],{"class":238,"line":966},20,[236,968,969],{},"    }\n",[236,971,973],{"class":238,"line":972},21,[236,974,328],{"emptyLinePlaceholder":327},[236,976,978],{"class":238,"line":977},22,[236,979,980],{},"    NSLog(@\"Pulling secrets from SECRETS_PLIST = %@.plist\", SECRETS_PLIST);\n",[236,982,984],{"class":238,"line":983},23,[236,985,922],{},[236,987,989],{"class":238,"line":988},24,[236,990,328],{"emptyLinePlaceholder":327},[236,992,994],{"class":238,"line":993},25,[236,995,996],{},"+ (TRAppSettings *) sharedInstance\n",[236,998,1000],{"class":238,"line":999},26,[236,1001,936],{},[236,1003,1005],{"class":238,"line":1004},27,[236,1006,1007],{},"    return sharedAppSettingsSingleton;\n",[236,1009,1011],{"class":238,"line":1010},28,[236,1012,922],{},[236,1014,1016],{"class":238,"line":1015},29,[236,1017,328],{"emptyLinePlaceholder":327},[236,1019,1021],{"class":238,"line":1020},30,[236,1022,1023],{},"- (id)init {\n",[236,1025,1027],{"class":238,"line":1026},31,[236,1028,1029],{},"    NSString *troupeSecretsPath = [[NSBundle mainBundle] pathForResource:\"GitterSecrets-Dev\" ofType:@\"plist\"];\n",[236,1031,1033],{"class":238,"line":1032},32,[236,1034,1035],{},"    if(troupeSecretsPath == nil) {\n",[236,1037,1039],{"class":238,"line":1038},33,[236,1040,1041],{},"        NSString *failureReason = [NSString stringWithFormat:@\"Gitter secrets file not found in bundle: %@.plist. You probably need to add it to the `Gitter/Supporting Files` in Xcode navigator\", SECRETS_PLIST];\n",[236,1043,1045],{"class":238,"line":1044},34,[236,1046,1047],{},"        NSException* exception = [NSException\n",[236,1049,1051],{"class":238,"line":1050},35,[236,1052,1053],{},"            exceptionWithName:@\"FileNotFoundException\"\n",[236,1055,1057],{"class":238,"line":1056},36,[236,1058,1059],{},"            reason:failureReason\n",[236,1061,1063],{"class":238,"line":1062},37,[236,1064,1065],{},"            userInfo:nil];\n",[236,1067,1069],{"class":238,"line":1068},38,[236,1070,328],{"emptyLinePlaceholder":327},[236,1072,1074],{"class":238,"line":1073},39,[236,1075,1076],{},"        NSLog(@\"%@\", failureReason);\n",[236,1078,1080],{"class":238,"line":1079},40,[236,1081,328],{"emptyLinePlaceholder":327},[236,1083,1085],{"class":238,"line":1084},41,[236,1086,1087],{},"        [exception raise];\n",[236,1089,1091],{"class":238,"line":1090},42,[236,1092,969],{},[236,1094,1096],{"class":238,"line":1095},43,[236,1097,1098],{},"    NSDictionary *troupeSecrets = [NSDictionary dictionaryWithContentsOfFile:troupeSecretsPath];\n",[236,1100,1102],{"class":238,"line":1101},44,[236,1103,328],{"emptyLinePlaceholder":327},[236,1105,1107],{"class":238,"line":1106},45,[236,1108,1109],{},"    self.secrets = [NSUserDefaults standardUserDefaults];\n",[236,1111,1113],{"class":238,"line":1112},46,[236,1114,1115],{},"    [self.secrets registerDefaults:troupeSecrets];\n",[236,1117,1119],{"class":238,"line":1118},47,[236,1120,922],{},[236,1122,1124],{"class":238,"line":1123},48,[236,1125,328],{"emptyLinePlaceholder":327},[236,1127,1129],{"class":238,"line":1128},49,[236,1130,1131],{},"- (NSString *) clientID {\n",[236,1133,1135],{"class":238,"line":1134},50,[236,1136,1137],{},"    return [self.secrets stringForKey:@\"OAuthClientId\"];\n",[236,1139,1141],{"class":238,"line":1140},51,[236,1142,922],{},[236,1144,1146],{"class":238,"line":1145},52,[236,1147,328],{"emptyLinePlaceholder":327},[236,1149,1151],{"class":238,"line":1150},53,[236,1152,1153],{},"- (NSString *) clientSecret {\n",[236,1155,1157],{"class":238,"line":1156},54,[236,1158,1159],{},"    return [self.secrets stringForKey:@\"OAuthClientSecret\"];\n",[236,1161,1163],{"class":238,"line":1162},55,[236,1164,922],{},[236,1166,1168],{"class":238,"line":1167},56,[236,1169,328],{"emptyLinePlaceholder":327},[236,1171,1173],{"class":238,"line":1172},57,[236,1174,1175],{},"- (NSString *)oauthScope {\n",[236,1177,1179],{"class":238,"line":1178},58,[236,1180,1181],{},"    return [self.secrets stringForKey:@\"OAuthCallback\"];\n",[236,1183,1185],{"class":238,"line":1184},59,[236,1186,922],{},[12,1188,1189],{},"Usage in the Swift app:",[227,1191,1195],{"className":1192,"code":1193,"language":1194,"meta":232,"style":232},"language-swift shiki shiki-themes github-light","private let appSettings = TRAppSettings.sharedInstance()\n\nappSettings!.clientID()\nappSettings!.clientSecret()\nappSettings!.oauthScope()\n","swift",[104,1196,1197,1219,1223,1238,1251],{"__ignoreMap":232},[236,1198,1199,1202,1205,1208,1210,1213,1216],{"class":238,"line":239},[236,1200,1201],{"class":261},"private",[236,1203,1204],{"class":261}," let",[236,1206,1207],{"class":265}," appSettings ",[236,1209,266],{"class":261},[236,1211,1212],{"class":265}," TRAppSettings.",[236,1214,1215],{"class":678},"sharedInstance",[236,1217,1218],{"class":265},"()\n",[236,1220,1221],{"class":238,"line":246},[236,1222,328],{"emptyLinePlaceholder":327},[236,1224,1225,1228,1231,1233,1236],{"class":238,"line":252},[236,1226,1227],{"class":265},"appSettings",[236,1229,1230],{"class":261},"!",[236,1232,137],{"class":265},[236,1234,1235],{"class":678},"clientID",[236,1237,1218],{"class":265},[236,1239,1240,1242,1244,1246,1249],{"class":238,"line":258},[236,1241,1227],{"class":265},[236,1243,1230],{"class":261},[236,1245,137],{"class":265},[236,1247,1248],{"class":678},"clientSecret",[236,1250,1218],{"class":265},[236,1252,1253,1255,1257,1259,1262],{"class":238,"line":273},[236,1254,1227],{"class":265},[236,1256,1230],{"class":261},[236,1258,137],{"class":265},[236,1260,1261],{"class":678},"oauthScope",[236,1263,1218],{"class":265},[109,1265,1267],{"id":1266},"adding-in-gitlab-ci","Adding in GitLab CI",[12,1269,1270,1271,137],{},"If you're interested in setting up automated builds and publish releases to the Apple App Store from GitLab CI, you can learn how ",[16,1272,1274],{"href":1273},"/blog/ios-publishing-with-gitlab-and-fastlane/","blog post about using fastlane",[109,1276,384],{"id":1277},"removing-secrets-from-the-repo-history-1",[12,1279,1280,1281,1283],{},"We didn't have a complete picture of what to remove because ",[104,1282,121],{}," didn't work well, so we didn't use BFG Repo-Cleaner. To remove secrets from the git repo history, we just squashed all of the history into a single commit.",[77,1285,1287],{"id":1286},"life-after-open-sourcing-apps","Life after open sourcing apps",[12,1289,1290,1291,1296,1297,1302,1303,1308,1309,1316,1317,1324],{},"We have some ",[16,1292,1295],{"href":1293,"rel":1294},"https://gitlab.com/gitlab-org/gitter/webapp/issues/2281",[],"thoughts of deprecating the Android/iOS apps"," but the community has been great to keep the apps alive so far. We released a couple versions of each app including ",[16,1298,1301],{"href":1299,"rel":1300},"https://gitlab.com/gitlab-org/gitter/gitter-android-app/merge_requests/2",[],"dark theme"," and ",[16,1304,1307],{"href":1305,"rel":1306},"https://gitlab.com/gitlab-org/gitter/gitter-android-app/merge_requests/112",[],"GitLab sign-in"," for Android and a bunch of technical debt and fixes for iOS, including removing the deprecated ",[16,1310,1313],{"href":1311,"rel":1312},"https://gitlab.com/gitlab-org/gitter/gitter-ios-app/merge_requests/8",[],[104,1314,1315],{},"SlackTextViewController"," (and we are intensely working on incorporating the new ",[16,1318,1321],{"href":1319,"rel":1320},"https://goo.gl/7NDM3x",[],[104,1322,1323],{},"SlackWysiwygInputController"," 😜).",[12,1326,1327,1328,27,1331,1334],{},"The ",[16,1329,26],{"href":24,"rel":1330},[],[16,1332,32],{"href":30,"rel":1333},[]," apps could benefit from a lot of polish and fixes, so if you see anything particularly annoying, we would love to review and merge your updates!",[12,1336,1337,1338,1343,1344,137],{},"Cover image by ",[16,1339,1342],{"href":1340,"rel":1341},"https://unsplash.com/@natejohnston",[],"Nate Johnston"," on ",[16,1345,1348],{"href":1346,"rel":1347},"https://unsplash.com/photos/DkCydKeaLV8",[],"Unsplash",[1350,1351,1352],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}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);}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}",{"title":232,"searchDepth":246,"depth":246,"links":1354},[1355,1361,1367],{"id":79,"depth":246,"text":26,"children":1356},[1357,1358,1359,1360],{"id":111,"depth":252,"text":112},{"id":157,"depth":252,"text":158},{"id":383,"depth":252,"text":384},{"id":454,"depth":252,"text":455},{"id":500,"depth":246,"text":32,"children":1362},[1363,1364,1365,1366],{"id":525,"depth":252,"text":112},{"id":580,"depth":252,"text":158},{"id":1266,"depth":252,"text":1267},{"id":1277,"depth":252,"text":384},{"id":1286,"depth":246,"text":1287},"engineering","2019-11-22","Learn how we open sourced the Android and iOS Gitter apps.","md",null,false,"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749666717/Blog/Hero%20Images/cover-image.jpg",{},"/en-us/blog/open-sourcing-the-gitter-mobile-apps",{"title":5,"description":1370,"ogTitle":5,"ogDescription":1370,"noIndex":1373,"ogImage":1374,"ogUrl":1378,"ogSiteName":1379,"ogType":1380,"canonicalUrls":1378},"https://about.gitlab.com/blog/open-sourcing-the-gitter-mobile-apps","https://about.gitlab.com","article","open-sourcing-the-gitter-mobile-apps","en-us/blog/open-sourcing-the-gitter-mobile-apps",[1384,1385,1386,1387],"open source","collaboration","contributors","CI/CD","BlogPost","DRNPb-YQkEhyh_g8khfSUIg1KAtcquPaiY08-9a7-zo",{"logo":1391,"freeTrial":1395,"sales":1400,"login":1405,"items":1410,"search":1736,"minimal":1767,"duo":1786,"switchNav":1795,"pricingDeployment":1806},{"config":1392},{"href":27,"dataGaName":1393,"dataGaLocation":1394},"gitlab logo","header",{"text":1396,"config":1397},"Get free trial",{"href":1398,"dataGaName":1399,"dataGaLocation":1394},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":1401,"config":1402},"Request a demo",{"href":1403,"dataGaName":1404,"dataGaLocation":1394},"/sales/?contact-topic=request-demo","sales",{"text":1406,"config":1407},"Sign in",{"href":1408,"dataGaName":1409,"dataGaLocation":1394},"https://gitlab.com/users/sign_in/","sign in",[1411,1440,1539,1544,1658,1714],{"text":1412,"config":1413,"menu":1415},"Platform",{"dataNavLevelOne":1414},"platform",{"type":1416,"columns":1417},"cards",[1418,1424,1432],{"title":1412,"description":1419,"link":1420},"The intelligent orchestration platform for DevSecOps",{"text":1421,"config":1422},"Explore our Platform",{"href":1423,"dataGaName":1414,"dataGaLocation":1394},"/platform/",{"title":1425,"description":1426,"link":1427},"GitLab Duo Agent Platform","Agentic AI for the entire software lifecycle",{"text":1428,"config":1429},"Meet GitLab Duo",{"href":1430,"dataGaName":1431,"dataGaLocation":1394},"/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":1433,"description":1434,"link":1435},"Why GitLab","See the top reasons enterprises choose GitLab",{"text":1436,"config":1437},"Learn more",{"href":1438,"dataGaName":1439,"dataGaLocation":1394},"/why-gitlab/","why gitlab",{"text":1441,"left":327,"config":1442,"menu":1444},"Product",{"dataNavLevelOne":1443},"solutions",{"type":1445,"link":1446,"columns":1450,"feature":1518},"lists",{"text":1447,"config":1448},"View all Solutions",{"href":1449,"dataGaName":1443,"dataGaLocation":1394},"/solutions/",[1451,1474,1497],{"title":1452,"description":1453,"link":1454,"items":1459},"Automation","CI/CD and automation to accelerate deployment",{"config":1455},{"icon":1456,"href":1457,"dataGaName":1458,"dataGaLocation":1394},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[1460,1463,1466,1470],{"text":1387,"config":1461},{"href":1462,"dataGaLocation":1394,"dataGaName":1387},"/solutions/continuous-integration/",{"text":1425,"config":1464},{"href":1430,"dataGaLocation":1394,"dataGaName":1465},"gitlab duo agent platform - product menu",{"text":1467,"config":1468},"Source Code Management",{"href":1469,"dataGaLocation":1394,"dataGaName":1467},"/solutions/source-code-management/",{"text":1471,"config":1472},"Automated Software Delivery",{"href":1457,"dataGaLocation":1394,"dataGaName":1473},"Automated software delivery",{"title":1475,"description":1476,"link":1477,"items":1482},"Security","Deliver code faster without compromising security",{"config":1478},{"href":1479,"dataGaName":1480,"dataGaLocation":1394,"icon":1481},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[1483,1487,1492],{"text":1484,"config":1485},"Application Security Testing",{"href":1479,"dataGaName":1486,"dataGaLocation":1394},"Application security testing",{"text":1488,"config":1489},"Software Supply Chain Security",{"href":1490,"dataGaLocation":1394,"dataGaName":1491},"/solutions/supply-chain/","Software supply chain security",{"text":1493,"config":1494},"Software Compliance",{"href":1495,"dataGaName":1496,"dataGaLocation":1394},"/solutions/software-compliance/","software compliance",{"title":1498,"link":1499,"items":1504},"Measurement",{"config":1500},{"icon":1501,"href":1502,"dataGaName":1503,"dataGaLocation":1394},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[1505,1509,1513],{"text":1506,"config":1507},"Visibility & Measurement",{"href":1502,"dataGaLocation":1394,"dataGaName":1508},"Visibility and Measurement",{"text":1510,"config":1511},"Value Stream Management",{"href":1512,"dataGaLocation":1394,"dataGaName":1510},"/solutions/value-stream-management/",{"text":1514,"config":1515},"Analytics & Insights",{"href":1516,"dataGaLocation":1394,"dataGaName":1517},"/solutions/analytics-and-insights/","Analytics and insights",{"title":1519,"type":1445,"items":1520},"GitLab for",[1521,1527,1533],{"text":1522,"config":1523},"Enterprise",{"icon":1524,"href":1525,"dataGaLocation":1394,"dataGaName":1526},"Building","/enterprise/","enterprise",{"text":1528,"config":1529},"Small Business",{"icon":1530,"href":1531,"dataGaLocation":1394,"dataGaName":1532},"Work","/small-business/","small business",{"text":1534,"config":1535},"Public Sector",{"icon":1536,"href":1537,"dataGaLocation":1394,"dataGaName":1538},"Organization","/solutions/public-sector/","public sector",{"text":1540,"config":1541},"Pricing",{"href":1542,"dataGaName":1543,"dataGaLocation":1394,"dataNavLevelOne":1543},"/pricing/","pricing",{"text":1545,"config":1546,"menu":1548},"Resources",{"dataNavLevelOne":1547},"resources",{"type":1445,"link":1549,"columns":1553,"feature":1647},{"text":1550,"config":1551},"View all resources",{"href":1552,"dataGaName":1547,"dataGaLocation":1394},"/resources/",[1554,1587,1614],{"title":1555,"items":1556},"Getting started",[1557,1562,1567,1572,1577,1582],{"text":1558,"config":1559},"Install",{"href":1560,"dataGaName":1561,"dataGaLocation":1394},"/install/","install",{"text":1563,"config":1564},"Quick start guides",{"href":1565,"dataGaName":1566,"dataGaLocation":1394},"/get-started/","quick setup checklists",{"text":1568,"config":1569},"Learn",{"href":1570,"dataGaLocation":1394,"dataGaName":1571},"https://university.gitlab.com/","learn",{"text":1573,"config":1574},"Product documentation",{"href":1575,"dataGaName":1576,"dataGaLocation":1394},"https://docs.gitlab.com/","product documentation",{"text":1578,"config":1579},"Best practice videos",{"href":1580,"dataGaName":1581,"dataGaLocation":1394},"/getting-started-videos/","best practice videos",{"text":1583,"config":1584},"Integrations",{"href":1585,"dataGaName":1586,"dataGaLocation":1394},"/integrations/","integrations",{"title":1588,"items":1589},"Discover",[1590,1595,1600,1605,1609],{"text":1591,"config":1592},"Customer success stories",{"href":1593,"dataGaName":1594,"dataGaLocation":1394},"/customers/","customer success stories",{"text":1596,"config":1597},"Blog",{"href":1598,"dataGaName":1599,"dataGaLocation":1394},"/blog/","blog",{"text":1601,"config":1602},"Demo Hub",{"href":1603,"dataGaName":1604,"dataGaLocation":1394},"/demo-hub/","demo hub",{"text":1606,"config":1607},"The Source",{"href":1608,"dataGaName":1599,"dataGaLocation":1394},"/the-source/",{"text":1610,"config":1611},"Remote",{"href":1612,"dataGaName":1613,"dataGaLocation":1394},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":1615,"items":1616},"Connect",[1617,1622,1627,1632,1637,1642],{"text":1618,"config":1619},"GitLab Services",{"href":1620,"dataGaName":1621,"dataGaLocation":1394},"/services/","services",{"text":1623,"config":1624},"Contribute",{"href":1625,"dataGaName":1626,"dataGaLocation":1394},"https://contributors.gitlab.com","contribute",{"text":1628,"config":1629},"Community",{"href":1630,"dataGaName":1631,"dataGaLocation":1394},"/community/","community",{"text":1633,"config":1634},"Forum",{"href":1635,"dataGaName":1636,"dataGaLocation":1394},"https://forum.gitlab.com/","forum",{"text":1638,"config":1639},"Events",{"href":1640,"dataGaName":1641,"dataGaLocation":1394},"/events/","events",{"text":1643,"config":1644},"Partners",{"href":1645,"dataGaName":1646,"dataGaLocation":1394},"/partners/","partners",{"config":1648,"title":1651,"text":1652,"link":1653},{"background":1649,"textColor":1650},"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":1654,"config":1655},"Read the latest",{"href":1656,"dataGaName":1657,"dataGaLocation":1394},"/whats-new/","whats new",{"text":1659,"config":1660,"menu":1662},"Company",{"dataNavLevelOne":1661},"company",{"type":1445,"columns":1663},[1664],{"items":1665},[1666,1671,1677,1679,1684,1689,1694,1699,1704,1709],{"text":1667,"config":1668},"About",{"href":1669,"dataGaName":1670,"dataGaLocation":1394},"/company/","about",{"text":1672,"config":1673,"footerGa":1676},"Jobs",{"href":1674,"dataGaName":1675,"dataGaLocation":1394},"/jobs/","jobs",{"dataGaName":1675},{"text":1638,"config":1678},{"href":1640,"dataGaName":1641,"dataGaLocation":1394},{"text":1680,"config":1681},"Leadership",{"href":1682,"dataGaName":1683,"dataGaLocation":1394},"/company/team/e-group/","leadership",{"text":1685,"config":1686},"Handbook",{"href":1687,"dataGaName":1688,"dataGaLocation":1394},"https://handbook.gitlab.com/","handbook",{"text":1690,"config":1691},"Investor relations",{"href":1692,"dataGaName":1693,"dataGaLocation":1394},"https://ir.gitlab.com/overview/default.aspx","investor relations",{"text":1695,"config":1696},"Trust Center",{"href":1697,"dataGaName":1698,"dataGaLocation":1394},"/security/","trust center",{"text":1700,"config":1701},"AI Transparency Center",{"href":1702,"dataGaName":1703,"dataGaLocation":1394},"/ai-transparency-center/","ai transparency center",{"text":1705,"config":1706},"Newsletter",{"href":1707,"dataGaName":1708,"dataGaLocation":1394},"/company/contact/#contact-forms","newsletter",{"text":1710,"config":1711},"Press",{"href":1712,"dataGaName":1713,"dataGaLocation":1394},"/press/","press",{"text":1715,"config":1716,"menu":1717},"Contact us",{"dataNavLevelOne":1661},{"type":1445,"columns":1718},[1719],{"items":1720},[1721,1726,1731],{"text":1722,"config":1723},"Talk to sales",{"href":1724,"dataGaName":1725,"dataGaLocation":1394},"/sales/","talk to sales",{"text":1727,"config":1728},"Support portal",{"href":1729,"dataGaName":1730,"dataGaLocation":1394},"https://support.gitlab.com/hc/en-us","support portal",{"text":1732,"config":1733},"Customer portal",{"href":1734,"dataGaName":1735,"dataGaLocation":1394},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":1737,"login":1738,"suggestions":1745},"Close",{"text":1739,"link":1740},"To search repositories and projects, login to",{"text":1741,"config":1742},"gitlab.com",{"href":1408,"dataGaName":1743,"dataGaLocation":1744},"search login","search",{"text":1746,"default":1747},"Suggestions",[1748,1750,1754,1756,1760,1764],{"text":1425,"config":1749},{"href":1430,"dataGaName":1425,"dataGaLocation":1744},{"text":1751,"config":1752},"Code Suggestions (AI)",{"href":1753,"dataGaName":1751,"dataGaLocation":1744},"/solutions/code-suggestions/",{"text":1387,"config":1755},{"href":1462,"dataGaName":1387,"dataGaLocation":1744},{"text":1757,"config":1758},"GitLab on AWS",{"href":1759,"dataGaName":1757,"dataGaLocation":1744},"/partners/technology-partners/aws/",{"text":1761,"config":1762},"GitLab on Google Cloud",{"href":1763,"dataGaName":1761,"dataGaLocation":1744},"/partners/technology-partners/google-cloud-platform/",{"text":1765,"config":1766},"Why GitLab?",{"href":1438,"dataGaName":1765,"dataGaLocation":1744},{"freeTrial":1768,"mobileIcon":1773,"desktopIcon":1778,"secondaryButton":1781},{"text":1769,"config":1770},"Start free trial",{"href":1771,"dataGaName":1399,"dataGaLocation":1772},"https://gitlab.com/-/trials/new/","nav",{"altText":1774,"config":1775},"Gitlab Icon",{"src":1776,"dataGaName":1777,"dataGaLocation":1772},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":1774,"config":1779},{"src":1780,"dataGaName":1777,"dataGaLocation":1772},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":1782,"config":1783},"Get Started",{"href":1784,"dataGaName":1785,"dataGaLocation":1772},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":1787,"mobileIcon":1791,"desktopIcon":1793},{"text":1788,"config":1789},"Learn more about GitLab Duo",{"href":1430,"dataGaName":1790,"dataGaLocation":1772},"gitlab duo",{"altText":1774,"config":1792},{"src":1776,"dataGaName":1777,"dataGaLocation":1772},{"altText":1774,"config":1794},{"src":1780,"dataGaName":1777,"dataGaLocation":1772},{"button":1796,"mobileIcon":1801,"desktopIcon":1803},{"text":1797,"config":1798},"/switch",{"href":1799,"dataGaName":1800,"dataGaLocation":1772},"#contact","switch",{"altText":1774,"config":1802},{"src":1776,"dataGaName":1777,"dataGaLocation":1772},{"altText":1774,"config":1804},{"src":1805,"dataGaName":1777,"dataGaLocation":1772},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":1807,"mobileIcon":1812,"desktopIcon":1814},{"text":1808,"config":1809},"Back to pricing",{"href":1542,"dataGaName":1810,"dataGaLocation":1772,"icon":1811},"back to pricing","GoBack",{"altText":1774,"config":1813},{"src":1776,"dataGaName":1777,"dataGaLocation":1772},{"altText":1774,"config":1815},{"src":1780,"dataGaName":1777,"dataGaLocation":1772},{"title":1817,"titleMobile":1818,"button":1819,"config":1824},"Duo Agent Platform delivers 400% ROI, per new Forrester Consulting study.","400% ROI: Forrester TEI for GitLab Duo",{"text":1436,"config":1820},{"href":1821,"dataGaName":1822,"dataGaLocation":1823},"https://about.gitlab.com/blog/gitlab-duo-agent-platform-delivers-400-percent-roi/","forrester-tei-dap-banner","global-banner",{"layout":1825,"disabled":1373},"release",{"data":1827},{"text":1828,"source":1829,"edit":1835,"contribute":1840,"config":1845,"items":1850,"minimal":2060},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":1830,"config":1831},"View page source",{"href":1832,"dataGaName":1833,"dataGaLocation":1834},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":1836,"config":1837},"Edit this page",{"href":1838,"dataGaName":1839,"dataGaLocation":1834},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":1841,"config":1842},"Please contribute",{"href":1843,"dataGaName":1844,"dataGaLocation":1834},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":1846,"facebook":1847,"youtube":1848,"linkedin":1849},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[1851,1898,1952,1996,2028],{"title":1540,"links":1852,"subMenu":1867},[1853,1857,1862],{"text":1854,"config":1855},"View plans",{"href":1542,"dataGaName":1856,"dataGaLocation":1834},"view plans",{"text":1858,"config":1859},"Why Premium?",{"href":1860,"dataGaName":1861,"dataGaLocation":1834},"/pricing/premium/","why premium",{"text":1863,"config":1864},"Why Ultimate?",{"href":1865,"dataGaName":1866,"dataGaLocation":1834},"/pricing/ultimate/","why ultimate",[1868],{"title":1869,"links":1870},"Contact Us",[1871,1874,1876,1878,1883,1888,1893],{"text":1872,"config":1873},"Contact sales",{"href":1724,"dataGaName":1404,"dataGaLocation":1834},{"text":1727,"config":1875},{"href":1729,"dataGaName":1730,"dataGaLocation":1834},{"text":1732,"config":1877},{"href":1734,"dataGaName":1735,"dataGaLocation":1834},{"text":1879,"config":1880},"Status",{"href":1881,"dataGaName":1882,"dataGaLocation":1834},"https://status.gitlab.com/","status",{"text":1884,"config":1885},"Terms of use",{"href":1886,"dataGaName":1887,"dataGaLocation":1834},"/terms/","terms of use",{"text":1889,"config":1890},"Privacy statement",{"href":1891,"dataGaName":1892,"dataGaLocation":1834},"/privacy/","privacy statement",{"text":1894,"config":1895},"Cookie preferences",{"dataGaName":1896,"dataGaLocation":1834,"id":1897,"isOneTrustButton":327},"cookie preferences","ot-sdk-btn",{"title":1441,"links":1899,"subMenu":1908},[1900,1904],{"text":1901,"config":1902},"DevSecOps platform",{"href":1423,"dataGaName":1903,"dataGaLocation":1834},"devsecops platform",{"text":1905,"config":1906},"AI-Assisted Development",{"href":1430,"dataGaName":1907,"dataGaLocation":1834},"ai-assisted development",[1909],{"title":1910,"links":1911},"Topics",[1912,1917,1922,1927,1932,1937,1942,1947],{"text":1913,"config":1914},"CICD",{"href":1915,"dataGaName":1916,"dataGaLocation":1834},"/topics/ci-cd/","cicd",{"text":1918,"config":1919},"GitOps",{"href":1920,"dataGaName":1921,"dataGaLocation":1834},"/topics/gitops/","gitops",{"text":1923,"config":1924},"DevOps",{"href":1925,"dataGaName":1926,"dataGaLocation":1834},"/topics/devops/","devops",{"text":1928,"config":1929},"Version Control",{"href":1930,"dataGaName":1931,"dataGaLocation":1834},"/topics/version-control/","version control",{"text":1933,"config":1934},"DevSecOps",{"href":1935,"dataGaName":1936,"dataGaLocation":1834},"/topics/devsecops/","devsecops",{"text":1938,"config":1939},"Cloud Native",{"href":1940,"dataGaName":1941,"dataGaLocation":1834},"/topics/cloud-native/","cloud native",{"text":1943,"config":1944},"AI for Coding",{"href":1945,"dataGaName":1946,"dataGaLocation":1834},"/topics/devops/ai-for-coding/","ai for coding",{"text":1948,"config":1949},"Agentic AI",{"href":1950,"dataGaName":1951,"dataGaLocation":1834},"/topics/agentic-ai/","agentic ai",{"title":1953,"links":1954},"Solutions",[1955,1957,1959,1964,1968,1971,1975,1978,1980,1983,1986,1991],{"text":1484,"config":1956},{"href":1479,"dataGaName":1484,"dataGaLocation":1834},{"text":1473,"config":1958},{"href":1457,"dataGaName":1458,"dataGaLocation":1834},{"text":1960,"config":1961},"Agile development",{"href":1962,"dataGaName":1963,"dataGaLocation":1834},"/solutions/agile-delivery/","agile delivery",{"text":1965,"config":1966},"SCM",{"href":1469,"dataGaName":1967,"dataGaLocation":1834},"source code management",{"text":1913,"config":1969},{"href":1462,"dataGaName":1970,"dataGaLocation":1834},"continuous integration & delivery",{"text":1972,"config":1973},"Value stream management",{"href":1512,"dataGaName":1974,"dataGaLocation":1834},"value stream management",{"text":1918,"config":1976},{"href":1977,"dataGaName":1921,"dataGaLocation":1834},"/solutions/gitops/",{"text":1522,"config":1979},{"href":1525,"dataGaName":1526,"dataGaLocation":1834},{"text":1981,"config":1982},"Small business",{"href":1531,"dataGaName":1532,"dataGaLocation":1834},{"text":1984,"config":1985},"Public sector",{"href":1537,"dataGaName":1538,"dataGaLocation":1834},{"text":1987,"config":1988},"Education",{"href":1989,"dataGaName":1990,"dataGaLocation":1834},"/solutions/education/","education",{"text":1992,"config":1993},"Financial services",{"href":1994,"dataGaName":1995,"dataGaLocation":1834},"/solutions/finance/","financial services",{"title":1545,"links":1997},[1998,2000,2002,2004,2007,2009,2012,2014,2016,2018,2020,2022,2024,2026],{"text":1558,"config":1999},{"href":1560,"dataGaName":1561,"dataGaLocation":1834},{"text":1563,"config":2001},{"href":1565,"dataGaName":1566,"dataGaLocation":1834},{"text":1568,"config":2003},{"href":1570,"dataGaName":1571,"dataGaLocation":1834},{"text":1573,"config":2005},{"href":1575,"dataGaName":2006,"dataGaLocation":1834},"docs",{"text":1596,"config":2008},{"href":1598,"dataGaName":1599,"dataGaLocation":1834},{"text":2010,"config":2011},"What's new",{"href":1656,"dataGaName":1657,"dataGaLocation":1834},{"text":1591,"config":2013},{"href":1593,"dataGaName":1594,"dataGaLocation":1834},{"text":1610,"config":2015},{"href":1612,"dataGaName":1613,"dataGaLocation":1834},{"text":1618,"config":2017},{"href":1620,"dataGaName":1621,"dataGaLocation":1834},{"text":1623,"config":2019},{"href":1625,"dataGaName":1626,"dataGaLocation":1834},{"text":1628,"config":2021},{"href":1630,"dataGaName":1631,"dataGaLocation":1834},{"text":1633,"config":2023},{"href":1635,"dataGaName":1636,"dataGaLocation":1834},{"text":1638,"config":2025},{"href":1640,"dataGaName":1641,"dataGaLocation":1834},{"text":1643,"config":2027},{"href":1645,"dataGaName":1646,"dataGaLocation":1834},{"title":1659,"links":2029},[2030,2032,2034,2036,2038,2040,2044,2049,2051,2053,2055],{"text":1667,"config":2031},{"href":1669,"dataGaName":1661,"dataGaLocation":1834},{"text":1672,"config":2033},{"href":1674,"dataGaName":1675,"dataGaLocation":1834},{"text":1680,"config":2035},{"href":1682,"dataGaName":1683,"dataGaLocation":1834},{"text":1685,"config":2037},{"href":1687,"dataGaName":1688,"dataGaLocation":1834},{"text":1690,"config":2039},{"href":1692,"dataGaName":1693,"dataGaLocation":1834},{"text":2041,"config":2042},"Sustainability",{"href":2043,"dataGaName":2041,"dataGaLocation":1834},"/sustainability/",{"text":2045,"config":2046},"Diversity, inclusion and belonging (DIB)",{"href":2047,"dataGaName":2048,"dataGaLocation":1834},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":1695,"config":2050},{"href":1697,"dataGaName":1698,"dataGaLocation":1834},{"text":1705,"config":2052},{"href":1707,"dataGaName":1708,"dataGaLocation":1834},{"text":1710,"config":2054},{"href":1712,"dataGaName":1713,"dataGaLocation":1834},{"text":2056,"config":2057},"Modern Slavery Transparency Statement",{"href":2058,"dataGaName":2059,"dataGaLocation":1834},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":2061},[2062,2065,2068],{"text":2063,"config":2064},"Terms",{"href":1886,"dataGaName":1887,"dataGaLocation":1834},{"text":2066,"config":2067},"Cookies",{"dataGaName":1896,"dataGaLocation":1834,"id":1897,"isOneTrustButton":327},{"text":2069,"config":2070},"Privacy",{"href":1891,"dataGaName":1892,"dataGaLocation":1834},[2072],{"id":2073,"title":7,"body":1372,"config":2074,"content":2076,"description":1372,"extension":2079,"meta":2080,"navigation":327,"path":2081,"seo":2082,"stem":2083,"__hash__":2084},"blogAuthors/en-us/blog/authors/eric-eastwood.yml",{"template":2075},"BlogAuthor",{"name":7,"config":2077},{"headshot":232,"ctfId":2078},"MadLittleMods","yml",{},"/en-us/blog/authors/eric-eastwood",{},"en-us/blog/authors/eric-eastwood","3GY5NqxW7tQ8UAQZ6F5lUcRr_K1vdp-6xP1nVi6jI4I",[2086,2095,2103],{"title":2087,"description":2088,"heroImage":2089,"category":1368,"date":2090,"authors":2091,"slug":2094,"externalUrl":1372},"Confidential AI for GitLab Self-Hosted","Give developers AI coding agents in GitLab Duo without source code leaving a hardware-encrypted boundary — no GPUs needed.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1773866173/vte9qh8rriznvyclhkes.png","2026-08-06",[2092,2093],"Mathias Ewald","Martin Paloncy, Edgeless Systems","confidential-ai-for-gitlab-self-hosted",{"title":2096,"description":2097,"heroImage":2098,"category":1368,"date":2099,"authors":2100,"slug":2102,"externalUrl":1372},"Green DevOps: Why carbon measurement belongs in your CI/CD pipeline","CI/CD pipelines have a hidden carbon cost. Here's why measuring it matters, and how you can get started with Eco CI and Carmen in GitLab.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1765809212/noh0mdfn9o94ry9ykura.png","2026-07-09",[2101],"Lysanne Pinto","green-devops-carbon-measurement-cicd-pipeline",{"title":2104,"description":2105,"heroImage":2106,"category":1368,"date":2107,"authors":2108,"slug":2110,"externalUrl":1372},"How to build CI/CD observability at scale","This practical guide to GitLab pipeline analytics helps self-managed users gain operational insights using Prometheus and Grafana.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1774465167/n5hlvrsrheadeccyr1oz.png","2026-04-28",[2109],"Paul Meresanu","how-to-build-ci-cd-observability-at-scale",{"promotions":2112},[2113,2127,2139,2151],{"id":2114,"categories":2115,"header":2117,"text":2118,"button":2119,"image":2124},"ai-modernization",[2116],"ai","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":2120,"config":2121},"Get your AI maturity score",{"href":2122,"dataGaName":2123,"dataGaLocation":1599},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":2125},{"src":2126},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":2128,"categories":2129,"header":2131,"text":2118,"button":2132,"image":2136},"devops-modernization",[2130,1936],"product","Are you just managing tools or shipping innovation?",{"text":2133,"config":2134},"Get your DevOps maturity score",{"href":2135,"dataGaName":2123,"dataGaLocation":1599},"/assessments/devops-modernization-assessment/",{"config":2137},{"src":2138},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":2140,"categories":2141,"header":2143,"text":2118,"button":2144,"image":2148},"security-modernization",[2142],"security","Are you trading speed for security?",{"text":2145,"config":2146},"Get your security maturity score",{"href":2147,"dataGaName":2123,"dataGaLocation":1599},"/assessments/security-modernization-assessment/",{"config":2149},{"src":2150},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":2152,"paths":2153,"header":2156,"text":2157,"button":2158,"image":2163},"github-azure-migration",[2154,2155],"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":2159,"config":2160},"See how GitLab compares to GitHub",{"href":2161,"dataGaName":2162,"dataGaLocation":1599},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":2164},{"src":2138},{"header":2166,"blurb":2167,"button":2168,"secondaryButton":2173},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":2169,"config":2170},"Get your free trial",{"href":2171,"dataGaName":1399,"dataGaLocation":2172},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":1872,"config":2174},{"href":1724,"dataGaName":1404,"dataGaLocation":2172},1786803764322]