[{"data":1,"prerenderedAt":759},["ShallowReactive",2],{"/en-us/blog/open-sourcing-the-gitter-mobile-apps":3,"navigation-en-us":37,"banner-en-us":464,"footer-en-us":481,"Eric Eastwood":726,"next-steps-en-us":738,"footer-source-/en-us/blog/open-sourcing-the-gitter-mobile-apps/":753},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":27,"_id":30,"_type":31,"title":32,"_source":33,"_file":34,"_stem":35,"_extension":36},"/en-us/blog/open-sourcing-the-gitter-mobile-apps","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"Open-sourcing the Gitter mobile apps","Learn how we open sourced the Android and iOS Gitter apps.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749666717/Blog/Hero%20Images/cover-image.jpg","https://about.gitlab.com/blog/open-sourcing-the-gitter-mobile-apps","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"Open-sourcing the Gitter mobile apps\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Eric Eastwood\"}],\n        \"datePublished\": \"2019-11-22\",\n      }",{"title":9,"description":10,"authors":17,"heroImage":11,"date":19,"body":20,"category":21,"tags":22},[18],"Eric Eastwood","2019-11-22","Before we acquired Gitter most every part of Gitter was\nprivate/closed-source. The main\n[webapp](https://gitlab.com/gitlab-org/gitter/webapp) was open-sourced in\nJune 2017 and got both mobile\n[Android](https://gitlab.com/gitlab-org/gitter/gitter-android-app)/[iOS](https://gitlab.com/gitlab-org/gitter/gitter-ios-app)\napps open sourced in September 2018. If you would like to come help out,\nfeel free to send us a merge request! This blog post will go over some the\ntechnical details of making the projects available for anyone to contribute.\n\n\nHere is the basic overview:\n\n\n1.  Find secrets in the current state of the project (don't worry about the\ncommit history) and move to some config that isn't tracked in the repo.\n\n1.  Find/remove secrets throughout the whole repo commit history.\n\n1.  Make the project public 🎉\n\n1.  Caveats:\n    - 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.\n\nQuick navigation:\n\n\n- [Jump to open sourcing Android](#android)\n\n- [Jump to open sourcing iOS](#ios)\n\n\n## Android\n\n\nIf you want to check out the full project and final result, you can check\nout the [project on\nGitLab](https://gitlab.com/gitlab-org/gitter/gitter-android-app)\n([open-sourced\n2018-8-8](https://twitter.com/gitchat/status/1027293167471812611)).\n\n\nTo start out, we used the [GitHub to GitLab project\nimport](https://docs.gitlab.com/ee/user/project/import/github.html) to move\nthe private GitHub project over to GitLab. We named it `gitter-android-app2`\nso that later on we could create the actual clean public project without any\nof the orphaned git references that may potentially leak.\n\n\n### Finding secrets\n\n\n[`truffleHog`](https://github.com/dxa4481/truffleHog) will search for high\nentropy strings (like tokens/passwords) through the entire git repo history.\nIt's also useful to find all the potential areas where secrets may still\nexist in the current state of the project. Some sticky points we encountered\nwhile using include:\n\n\n- \"I wish we could just search the current state of the project instead of\nall git history (the `--max_depth=2` argument will just make it search the\ndiff of the latest commit)\"\n[dxa4481/truffleHog#92](https://github.com/dxa4481/truffleHog/issues/92).\n\n- \"The output will show the entire diff for the triggered commit which is a\nbit burdensome to see exactly what is wrong. The JSON output `--json` is\nsometimes easier to understand\"\n[https://github.com/dxa4481/truffleHog/issues/58](https://github.com/dxa4481/truffleHog/issues/58)\nor\n[dxa4481/truffleHog#102](https://github.com/dxa4481/truffleHog/issues/102).\n\n\n### Moving secrets to untracked config\n\n\nOnce we figure out where all of the secrets are we need a config/variable\nsolution that isn't tracked by git but still lets them be available when\nbuilding. We also wanted the solution to work in GitLab CI for some sanity\nbuilds/testing. There are lots of good articles on this topic:\n\n\n- [Remove private signing information from your\nproject](https://developer.android.com/studio/build/gradle-tips#remove-private-signing-information-from-your-project)\n\n- [Keeping Your Android Project’s Secrets\nSecret](https://medium.com/@geocohn/keeping-your-android-projects-secrets-secret-393b8855765d)\n\n- [Hiding Secrets in Android\nApps](https://rammic.github.io/2015/07/28/hiding-secrets-in-android-apps/)\n\n- [Keeping secrets in an Android\nApplication](https://joshmcarthur.com/2014/02/16/keeping-secrets-in-an-android-application.html)\n\n- [Android: Loading API Keys and other secrets from properties file using\ngradle](https://gist.github.com/curioustechizen/9f7d745f9f5f51355bd6)\n\n- [How can I keep API keys out of source\ncontrol?](https://arstechnica.com/information-technology/2013/12/how-can-i-keep-api-keys-out-of-source-control/)\n\n\nOur solution is completely based on the information in these articles. We\nchose to go the route of defining things in a `secrets.properties` file\nwhich can easily be read in the Gradle build script which handles the build\neven when using Android Studio. If the `secrets.properties` file doesn't\nexist (like in CI), it will try to read the secrets from [environment\nvariables which can easily be supplied in the project\nsettings](https://docs.gitlab.com/ee/ci/variables/).\n\n\n`secerts.properties`\n\n\n```properties\n\n# Visit https://developer.gitter.im/apps (sign in) and create a new app\n\n# Name: my-gitter-android-app (can be anything)\n\n# Redirect URL: https://gitter.im/login/oauth/callback\n\noauth_client_id=\"...\"\n\noauth_client_secret=\"...\"\n\noauth_redirect_uri=\"https://gitter.im/login/oauth/callback\"\n\n```\n\n\n`build.gradle`\n\n\n```gradle\n\napply plugin: 'com.android.application'\n\n\n// Try reading secrets from file\n\ndef secretsPropertiesFile = rootProject.file(\"secrets.properties\")\n\ndef secretProperties = new Properties()\n\nif (secretsPropertiesFile.exists()) {\n    secretProperties.load(new FileInputStream(secretsPropertiesFile))\n}\n\n// Otherwise read from environment variables, this happens in CI\n\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\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\n```\n\n\nUse the config variables in the Java app:\n\n\n```java\n\nimport im.gitter.gitter.BuildConfig;\n\n\nBuildConfig.oauth_client_id;\n\nBuildConfig.oauth_client_secret;\n\nBuildConfig.oauth_redirect_uri;\n\n```\n\n\n#### Removing compiled assets\n\n\nWe use a `WebView` to display the HTML markdown messages in the chat room.\nThis view uses assets built from the main [`webapp`\nproject](https://gitlab.com/gitlab-org/gitter/webapp). Because these assets\nhad some inlined production\n[`webapp`](https://gitlab.com/gitlab-org/gitter/webapp) secrets that whole\ndirectory needed to be removed.\n\n\nInitially, we opted to have the developer build these assets with their own\nsecrets and symlink the build output directory. The [community made this\neven\nsimpler](https://gitlab.com/gitlab-org/gitter/gitter-android-app/merge_requests/113),\nso now there is just a Gradle task to run which fetches the latest build we\nhave available from the `webapp` GitLab CI.\n\n\n### Removing secrets from the repo history\n\n\nFrom your `truffleHog` results earlier, you should know where secrets were\nstored throughout the history. We can use [BFG\nRepo-Cleaner](https://rtyley.github.io/bfg-repo-cleaner/) to remove and\nrewrite the repo history quickly.\n\n\nWhen using BFG, I wanted just to rewrite all of the sensitive values in\n`app/src/main/res/values/settings.xml` instead of completely removing them,\nbut rewriting isn't an option with BFG so I went ahead with deleting it and\nrecreated it in a commit afterwards. 🤷\n\n\nFor the Android app, here are the BFG commands I used,\n\n\n- Remove `app/src/main/assets/www/`\n  - `java -jar \"bfg.jar\" --delete-folders www`\n- Remove `app/src/main/res/values/settings.xml`\n  - `java -jar \"bfg.jar\" --delete-files settings.xml`\n- Remove sensitive strings where we can't just remove the whole file\n(collected from `truffleHog` results)\n  - `java -jar \"bfg.jar\" --replace-text \"gitter-android-bad-words.txt\"`\n\nAfter you think you removed all the secrets, it's best to run `truffleHog`\nagain just to make sure no secrets are leftover. 😉\n\n\n### Make it public\n\n\nNow it's time to update your `readme` with some setup instruction so the\ncommunity knows how to contribute.\n\n\nThis is the scary part 😅. Go to **Project settings** > **General** >\n**Permissions** > set **Project visibility** as **Public**. You can [read\nmore about project access\nhere](https://docs.gitlab.com/ee/public_access/public_access.html).\n\n\nCurious about how to setup builds in GitLab CI? [Learn more from this blog\npost](/blog/setting-up-gitlab-ci-for-android-projects/), which was what we\nused to set it up for our projects.\n\n\nYou can even learn how we [automated the release process so we can publish\nstraight to the Google Play Store from GitLab CI via fastlane\n🚀](/blog/android-publishing-with-gitlab-and-fastlane/).\n\n\n## iOS\n\n\nIf you want to see the full project and final result, you can check out the\n[project on GitLab](https://gitlab.com/gitlab-org/gitter/gitter-ios-app)\n([open-sourced\n2018-9-18](https://twitter.com/gitchat/status/1041795909103898625)).\n\n\nThe same concepts apply from the Android section. We create a separate\nprivate project, `gitter-ios-app2`, where we can work and later on, we can\ncreate the actual clean public project(`gitter-ios-app`) without any of the\norphaned git references that could leak.\n\n\n### Finding secrets\n\n\n`truffleHog` didn't work well in the iOS project because there was a bunch\nof generated XCode files that had file hashes (high entropy strings which\ntruffleHog looks for) – which meant every commit was listed. 🤦‍ Instead of\ntrying to find something to filter the results down or get another tool, I\ndecided just search manually. Here is the list of things we looked for:\n\n\n- `token`\n\n- `secret`\n\n- `key`\n\n- `cert`\n\n- `api`\n\n- `pw`\n\n- `password`\n\n\nI used this directory filter when `Ctrl + f` those strings above to avoid\nfinding things outside of the repo itself (copy-paste for Atom editor):\n`!Common/,!Libraries,!Gitter/www,!Pods/,!xctool`\n\n\n### Moving secrets to untracked config\n\n\nThe iOS app uses a few git sub-modules which we also had to check for\nsecrets before making them public. It turned out only one of the sub-modules\n–\n[`troupeobjccommon`](https://gitlab.com/gitlab-org/gitter/troupeobjccommon)\n– had secrets of it's own so I ran through the same secret removal process.\n\n\nWe had the same OAuth secrets in the main part of the iOS app, but since\n`troupeobjccommon` was also trying to handle OAuth secret settings, we opted\nfor putting the new logic in `troupeobjccommon` to avoid having to refactor\nwhatever other downstream code that uses the same submodule (like the macOS\ndesktop app).\n\n\nHere are some articles around handling secrets in an iOS project,\n\n\n- [Secret variables in Xcode AND your CI for fun and profit\n💌](https://medium.com/flawless-app-stories/secret-variables-in-xcode-and-your-ci-for-fun-and-profit-d387a50475d7)\n\n- [Secrets Management in iOS\nApplications](https://medium.com/@jules2689/secrets-management-in-ios-applications-52795c254ec1)\n\n\nSince iOS apps can only be built on macOS and we don't have any macOS GitLab\nCI runners, our solution doesn't have to be CI compatible. You can track\n[this issue for shared macOS GitLab CI\nrunners](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5720).\n\n\n`Gitter/GitterSecrets-Dev.plist`\n\n\n```xml\n\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\u003C!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\n\u003Cplist version=\"1.0\">\n\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\n\u003C/plist>\n\n```\n\n\n[`troupeobjccommon`](https://gitlab.com/gitlab-org/gitter/troupeobjccommon)\nis in Objective-C\n\n\n`TRAppSettings.h`\n\n\n```h\n\n#import \u003CFoundation/Foundation.h>\n\n\n@interface TRAppSettings : NSObject\n\n\n+ (TRAppSettings *) sharedInstance;\n\n\n- (NSString *) clientID;\n\n\n- (NSString *) clientSecret;\n\n\n- (NSString *) oauthScope;\n\n\n@end\n\n```\n\n\n`TRAppSettings.m`\n\n\n```objc\n\n@interface TRAppSettings ()\n\n\n@property (strong, nonatomic) NSUserDefaults *secrets;\n\n\n@end\n\n\nstatic TRAppSettings *sharedAppSettingsSingleton;\n\n\n@implementation TRAppSettings {\n    int firstRunPostUpdate;\n}\n\n\n+ (void)initialize\n\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\n+ (TRAppSettings *) sharedInstance\n\n{\n    return sharedAppSettingsSingleton;\n}\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\n- (NSString *) clientID {\n    return [self.secrets stringForKey:@\"OAuthClientId\"];\n}\n\n\n- (NSString *) clientSecret {\n    return [self.secrets stringForKey:@\"OAuthClientSecret\"];\n}\n\n\n- (NSString *)oauthScope {\n    return [self.secrets stringForKey:@\"OAuthCallback\"];\n}\n\n```\n\n\nUsage in the Swift app:\n\n\n```swift\n\nprivate let appSettings = TRAppSettings.sharedInstance()\n\n\nappSettings!.clientID()\n\nappSettings!.clientSecret()\n\nappSettings!.oauthScope()\n\n```\n\n\n### Adding in GitLab CI\n\n\nIf you're interested in setting up automated builds and publish releases to\nthe Apple App Store from GitLab CI, you can learn how [blog post about using\nfastlane](/blog/ios-publishing-with-gitlab-and-fastlane/).\n\n\n### Removing secrets from the repo history\n\n\nWe didn't have a complete picture of what to remove because `truffleHog`\ndidn't work well, so we didn't use BFG Repo-Cleaner. To remove secrets from\nthe git repo history, we just squashed all of the history into a single\ncommit.\n\n\n## Life after open sourcing apps\n\n\nWe have some [thoughts of deprecating the Android/iOS\napps](https://gitlab.com/gitlab-org/gitter/webapp/issues/2281) but the\ncommunity has been great to keep the apps alive so far. We released a couple\nversions of each app including [dark\ntheme](https://gitlab.com/gitlab-org/gitter/gitter-android-app/merge_requests/2)\nand [GitLab\nsign-in](https://gitlab.com/gitlab-org/gitter/gitter-android-app/merge_requests/112)\nfor Android and a bunch of technical debt and fixes for iOS, including\nremoving the deprecated\n[`SlackTextViewController`](https://gitlab.com/gitlab-org/gitter/gitter-ios-app/merge_requests/8)\n(and we are intensely working on incorporating the new\n[`SlackWysiwygInputController`](https://goo.gl/7NDM3x) 😜).\n\n\nThe\n[Android](https://gitlab.com/gitlab-org/gitter/gitter-android-app)/[iOS](https://gitlab.com/gitlab-org/gitter/gitter-ios-app)\napps could benefit from a lot of polish and fixes, so if you see anything\nparticularly annoying, we would love to review and merge your updates!\n\n\nCover image by [Nate Johnston](https://unsplash.com/@natejohnston) on\n[Unsplash](https://unsplash.com/photos/DkCydKeaLV8).\n\n{: .note}\n","engineering",[23,24,25,26],"open source","collaboration","contributors","CI/CD",{"slug":28,"featured":6,"template":29},"open-sourcing-the-gitter-mobile-apps","BlogPost","content:en-us:blog:open-sourcing-the-gitter-mobile-apps.yml","yaml","Open Sourcing The Gitter Mobile Apps","content","en-us/blog/open-sourcing-the-gitter-mobile-apps.yml","en-us/blog/open-sourcing-the-gitter-mobile-apps","yml",{"_path":38,"_dir":39,"_draft":6,"_partial":6,"_locale":7,"data":40,"_id":460,"_type":31,"title":461,"_source":33,"_file":462,"_stem":463,"_extension":36},"/shared/en-us/main-navigation","en-us",{"logo":41,"freeTrial":46,"sales":51,"login":56,"items":61,"search":391,"minimal":422,"duo":441,"pricingDeployment":450},{"config":42},{"href":43,"dataGaName":44,"dataGaLocation":45},"/","gitlab logo","header",{"text":47,"config":48},"Get free trial",{"href":49,"dataGaName":50,"dataGaLocation":45},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":52,"config":53},"Talk to sales",{"href":54,"dataGaName":55,"dataGaLocation":45},"/sales/","sales",{"text":57,"config":58},"Sign in",{"href":59,"dataGaName":60,"dataGaLocation":45},"https://gitlab.com/users/sign_in/","sign in",[62,106,202,207,312,372],{"text":63,"config":64,"cards":66,"footer":89},"Platform",{"dataNavLevelOne":65},"platform",[67,73,81],{"title":63,"description":68,"link":69},"The most comprehensive AI-powered DevSecOps Platform",{"text":70,"config":71},"Explore our Platform",{"href":72,"dataGaName":65,"dataGaLocation":45},"/platform/",{"title":74,"description":75,"link":76},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":77,"config":78},"Meet GitLab Duo",{"href":79,"dataGaName":80,"dataGaLocation":45},"/gitlab-duo/","gitlab duo ai",{"title":82,"description":83,"link":84},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":85,"config":86},"Learn more",{"href":87,"dataGaName":88,"dataGaLocation":45},"/why-gitlab/","why gitlab",{"title":90,"items":91},"Get started with",[92,97,102],{"text":93,"config":94},"Platform Engineering",{"href":95,"dataGaName":96,"dataGaLocation":45},"/solutions/platform-engineering/","platform engineering",{"text":98,"config":99},"Developer Experience",{"href":100,"dataGaName":101,"dataGaLocation":45},"/developer-experience/","Developer experience",{"text":103,"config":104},"MLOps",{"href":105,"dataGaName":103,"dataGaLocation":45},"/topics/devops/the-role-of-ai-in-devops/",{"text":107,"left":108,"config":109,"link":111,"lists":115,"footer":184},"Product",true,{"dataNavLevelOne":110},"solutions",{"text":112,"config":113},"View all Solutions",{"href":114,"dataGaName":110,"dataGaLocation":45},"/solutions/",[116,140,163],{"title":117,"description":118,"link":119,"items":124},"Automation","CI/CD and automation to accelerate deployment",{"config":120},{"icon":121,"href":122,"dataGaName":123,"dataGaLocation":45},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[125,128,132,136],{"text":26,"config":126},{"href":127,"dataGaLocation":45,"dataGaName":26},"/solutions/continuous-integration/",{"text":129,"config":130},"AI-Assisted Development",{"href":79,"dataGaLocation":45,"dataGaName":131},"AI assisted development",{"text":133,"config":134},"Source Code Management",{"href":135,"dataGaLocation":45,"dataGaName":133},"/solutions/source-code-management/",{"text":137,"config":138},"Automated Software Delivery",{"href":122,"dataGaLocation":45,"dataGaName":139},"Automated software delivery",{"title":141,"description":142,"link":143,"items":148},"Security","Deliver code faster without compromising security",{"config":144},{"href":145,"dataGaName":146,"dataGaLocation":45,"icon":147},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[149,153,158],{"text":150,"config":151},"Application Security Testing",{"href":145,"dataGaName":152,"dataGaLocation":45},"Application security testing",{"text":154,"config":155},"Software Supply Chain Security",{"href":156,"dataGaLocation":45,"dataGaName":157},"/solutions/supply-chain/","Software supply chain security",{"text":159,"config":160},"Software Compliance",{"href":161,"dataGaName":162,"dataGaLocation":45},"/solutions/software-compliance/","software compliance",{"title":164,"link":165,"items":170},"Measurement",{"config":166},{"icon":167,"href":168,"dataGaName":169,"dataGaLocation":45},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[171,175,179],{"text":172,"config":173},"Visibility & Measurement",{"href":168,"dataGaLocation":45,"dataGaName":174},"Visibility and Measurement",{"text":176,"config":177},"Value Stream Management",{"href":178,"dataGaLocation":45,"dataGaName":176},"/solutions/value-stream-management/",{"text":180,"config":181},"Analytics & Insights",{"href":182,"dataGaLocation":45,"dataGaName":183},"/solutions/analytics-and-insights/","Analytics and insights",{"title":185,"items":186},"GitLab for",[187,192,197],{"text":188,"config":189},"Enterprise",{"href":190,"dataGaLocation":45,"dataGaName":191},"/enterprise/","enterprise",{"text":193,"config":194},"Small Business",{"href":195,"dataGaLocation":45,"dataGaName":196},"/small-business/","small business",{"text":198,"config":199},"Public Sector",{"href":200,"dataGaLocation":45,"dataGaName":201},"/solutions/public-sector/","public sector",{"text":203,"config":204},"Pricing",{"href":205,"dataGaName":206,"dataGaLocation":45,"dataNavLevelOne":206},"/pricing/","pricing",{"text":208,"config":209,"link":211,"lists":215,"feature":299},"Resources",{"dataNavLevelOne":210},"resources",{"text":212,"config":213},"View all resources",{"href":214,"dataGaName":210,"dataGaLocation":45},"/resources/",[216,249,271],{"title":217,"items":218},"Getting started",[219,224,229,234,239,244],{"text":220,"config":221},"Install",{"href":222,"dataGaName":223,"dataGaLocation":45},"/install/","install",{"text":225,"config":226},"Quick start guides",{"href":227,"dataGaName":228,"dataGaLocation":45},"/get-started/","quick setup checklists",{"text":230,"config":231},"Learn",{"href":232,"dataGaLocation":45,"dataGaName":233},"https://university.gitlab.com/","learn",{"text":235,"config":236},"Product documentation",{"href":237,"dataGaName":238,"dataGaLocation":45},"https://docs.gitlab.com/","product documentation",{"text":240,"config":241},"Best practice videos",{"href":242,"dataGaName":243,"dataGaLocation":45},"/getting-started-videos/","best practice videos",{"text":245,"config":246},"Integrations",{"href":247,"dataGaName":248,"dataGaLocation":45},"/integrations/","integrations",{"title":250,"items":251},"Discover",[252,257,261,266],{"text":253,"config":254},"Customer success stories",{"href":255,"dataGaName":256,"dataGaLocation":45},"/customers/","customer success stories",{"text":258,"config":259},"Blog",{"href":260,"dataGaName":5,"dataGaLocation":45},"/blog/",{"text":262,"config":263},"Remote",{"href":264,"dataGaName":265,"dataGaLocation":45},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":267,"config":268},"TeamOps",{"href":269,"dataGaName":270,"dataGaLocation":45},"/teamops/","teamops",{"title":272,"items":273},"Connect",[274,279,284,289,294],{"text":275,"config":276},"GitLab Services",{"href":277,"dataGaName":278,"dataGaLocation":45},"/services/","services",{"text":280,"config":281},"Community",{"href":282,"dataGaName":283,"dataGaLocation":45},"/community/","community",{"text":285,"config":286},"Forum",{"href":287,"dataGaName":288,"dataGaLocation":45},"https://forum.gitlab.com/","forum",{"text":290,"config":291},"Events",{"href":292,"dataGaName":293,"dataGaLocation":45},"/events/","events",{"text":295,"config":296},"Partners",{"href":297,"dataGaName":298,"dataGaLocation":45},"/partners/","partners",{"backgroundColor":300,"textColor":301,"text":302,"image":303,"link":307},"#2f2a6b","#fff","Insights for the future of software development",{"altText":304,"config":305},"the source promo card",{"src":306},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758208064/dzl0dbift9xdizyelkk4.svg",{"text":308,"config":309},"Read the latest",{"href":310,"dataGaName":311,"dataGaLocation":45},"/the-source/","the source",{"text":313,"config":314,"lists":316},"Company",{"dataNavLevelOne":315},"company",[317],{"items":318},[319,324,330,332,337,342,347,352,357,362,367],{"text":320,"config":321},"About",{"href":322,"dataGaName":323,"dataGaLocation":45},"/company/","about",{"text":325,"config":326,"footerGa":329},"Jobs",{"href":327,"dataGaName":328,"dataGaLocation":45},"/jobs/","jobs",{"dataGaName":328},{"text":290,"config":331},{"href":292,"dataGaName":293,"dataGaLocation":45},{"text":333,"config":334},"Leadership",{"href":335,"dataGaName":336,"dataGaLocation":45},"/company/team/e-group/","leadership",{"text":338,"config":339},"Team",{"href":340,"dataGaName":341,"dataGaLocation":45},"/company/team/","team",{"text":343,"config":344},"Handbook",{"href":345,"dataGaName":346,"dataGaLocation":45},"https://handbook.gitlab.com/","handbook",{"text":348,"config":349},"Investor relations",{"href":350,"dataGaName":351,"dataGaLocation":45},"https://ir.gitlab.com/","investor relations",{"text":353,"config":354},"Trust Center",{"href":355,"dataGaName":356,"dataGaLocation":45},"/security/","trust center",{"text":358,"config":359},"AI Transparency Center",{"href":360,"dataGaName":361,"dataGaLocation":45},"/ai-transparency-center/","ai transparency center",{"text":363,"config":364},"Newsletter",{"href":365,"dataGaName":366,"dataGaLocation":45},"/company/contact/","newsletter",{"text":368,"config":369},"Press",{"href":370,"dataGaName":371,"dataGaLocation":45},"/press/","press",{"text":373,"config":374,"lists":375},"Contact us",{"dataNavLevelOne":315},[376],{"items":377},[378,381,386],{"text":52,"config":379},{"href":54,"dataGaName":380,"dataGaLocation":45},"talk to sales",{"text":382,"config":383},"Support portal",{"href":384,"dataGaName":385,"dataGaLocation":45},"https://support.gitlab.com","support portal",{"text":387,"config":388},"Customer portal",{"href":389,"dataGaName":390,"dataGaLocation":45},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":392,"login":393,"suggestions":400},"Close",{"text":394,"link":395},"To search repositories and projects, login to",{"text":396,"config":397},"gitlab.com",{"href":59,"dataGaName":398,"dataGaLocation":399},"search login","search",{"text":401,"default":402},"Suggestions",[403,405,409,411,415,419],{"text":74,"config":404},{"href":79,"dataGaName":74,"dataGaLocation":399},{"text":406,"config":407},"Code Suggestions (AI)",{"href":408,"dataGaName":406,"dataGaLocation":399},"/solutions/code-suggestions/",{"text":26,"config":410},{"href":127,"dataGaName":26,"dataGaLocation":399},{"text":412,"config":413},"GitLab on AWS",{"href":414,"dataGaName":412,"dataGaLocation":399},"/partners/technology-partners/aws/",{"text":416,"config":417},"GitLab on Google Cloud",{"href":418,"dataGaName":416,"dataGaLocation":399},"/partners/technology-partners/google-cloud-platform/",{"text":420,"config":421},"Why GitLab?",{"href":87,"dataGaName":420,"dataGaLocation":399},{"freeTrial":423,"mobileIcon":428,"desktopIcon":433,"secondaryButton":436},{"text":424,"config":425},"Start free trial",{"href":426,"dataGaName":50,"dataGaLocation":427},"https://gitlab.com/-/trials/new/","nav",{"altText":429,"config":430},"Gitlab Icon",{"src":431,"dataGaName":432,"dataGaLocation":427},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":429,"config":434},{"src":435,"dataGaName":432,"dataGaLocation":427},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":437,"config":438},"Get Started",{"href":439,"dataGaName":440,"dataGaLocation":427},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":442,"mobileIcon":446,"desktopIcon":448},{"text":443,"config":444},"Learn more about GitLab Duo",{"href":79,"dataGaName":445,"dataGaLocation":427},"gitlab duo",{"altText":429,"config":447},{"src":431,"dataGaName":432,"dataGaLocation":427},{"altText":429,"config":449},{"src":435,"dataGaName":432,"dataGaLocation":427},{"freeTrial":451,"mobileIcon":456,"desktopIcon":458},{"text":452,"config":453},"Back to pricing",{"href":205,"dataGaName":454,"dataGaLocation":427,"icon":455},"back to pricing","GoBack",{"altText":429,"config":457},{"src":431,"dataGaName":432,"dataGaLocation":427},{"altText":429,"config":459},{"src":435,"dataGaName":432,"dataGaLocation":427},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":465,"_dir":39,"_draft":6,"_partial":6,"_locale":7,"title":466,"button":467,"image":472,"config":476,"_id":478,"_type":31,"_source":33,"_file":479,"_stem":480,"_extension":36},"/shared/en-us/banner","is now in public beta!",{"text":468,"config":469},"Try the Beta",{"href":470,"dataGaName":471,"dataGaLocation":45},"/gitlab-duo/agent-platform/","duo banner",{"altText":473,"config":474},"GitLab Duo Agent Platform",{"src":475},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1753720689/somrf9zaunk0xlt7ne4x.svg",{"layout":477},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":482,"_dir":39,"_draft":6,"_partial":6,"_locale":7,"data":483,"_id":722,"_type":31,"title":723,"_source":33,"_file":724,"_stem":725,"_extension":36},"/shared/en-us/main-footer",{"text":484,"source":485,"edit":491,"contribute":496,"config":501,"items":506,"minimal":714},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":486,"config":487},"View page source",{"href":488,"dataGaName":489,"dataGaLocation":490},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":492,"config":493},"Edit this page",{"href":494,"dataGaName":495,"dataGaLocation":490},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":497,"config":498},"Please contribute",{"href":499,"dataGaName":500,"dataGaLocation":490},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":502,"facebook":503,"youtube":504,"linkedin":505},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[507,554,607,651,680],{"title":203,"links":508,"subMenu":523},[509,513,518],{"text":510,"config":511},"View plans",{"href":205,"dataGaName":512,"dataGaLocation":490},"view plans",{"text":514,"config":515},"Why Premium?",{"href":516,"dataGaName":517,"dataGaLocation":490},"/pricing/premium/","why premium",{"text":519,"config":520},"Why Ultimate?",{"href":521,"dataGaName":522,"dataGaLocation":490},"/pricing/ultimate/","why ultimate",[524],{"title":525,"links":526},"Contact Us",[527,530,532,534,539,544,549],{"text":528,"config":529},"Contact sales",{"href":54,"dataGaName":55,"dataGaLocation":490},{"text":382,"config":531},{"href":384,"dataGaName":385,"dataGaLocation":490},{"text":387,"config":533},{"href":389,"dataGaName":390,"dataGaLocation":490},{"text":535,"config":536},"Status",{"href":537,"dataGaName":538,"dataGaLocation":490},"https://status.gitlab.com/","status",{"text":540,"config":541},"Terms of use",{"href":542,"dataGaName":543,"dataGaLocation":490},"/terms/","terms of use",{"text":545,"config":546},"Privacy statement",{"href":547,"dataGaName":548,"dataGaLocation":490},"/privacy/","privacy statement",{"text":550,"config":551},"Cookie preferences",{"dataGaName":552,"dataGaLocation":490,"id":553,"isOneTrustButton":108},"cookie preferences","ot-sdk-btn",{"title":107,"links":555,"subMenu":563},[556,560],{"text":557,"config":558},"DevSecOps platform",{"href":72,"dataGaName":559,"dataGaLocation":490},"devsecops platform",{"text":129,"config":561},{"href":79,"dataGaName":562,"dataGaLocation":490},"ai-assisted development",[564],{"title":565,"links":566},"Topics",[567,572,577,582,587,592,597,602],{"text":568,"config":569},"CICD",{"href":570,"dataGaName":571,"dataGaLocation":490},"/topics/ci-cd/","cicd",{"text":573,"config":574},"GitOps",{"href":575,"dataGaName":576,"dataGaLocation":490},"/topics/gitops/","gitops",{"text":578,"config":579},"DevOps",{"href":580,"dataGaName":581,"dataGaLocation":490},"/topics/devops/","devops",{"text":583,"config":584},"Version Control",{"href":585,"dataGaName":586,"dataGaLocation":490},"/topics/version-control/","version control",{"text":588,"config":589},"DevSecOps",{"href":590,"dataGaName":591,"dataGaLocation":490},"/topics/devsecops/","devsecops",{"text":593,"config":594},"Cloud Native",{"href":595,"dataGaName":596,"dataGaLocation":490},"/topics/cloud-native/","cloud native",{"text":598,"config":599},"AI for Coding",{"href":600,"dataGaName":601,"dataGaLocation":490},"/topics/devops/ai-for-coding/","ai for coding",{"text":603,"config":604},"Agentic AI",{"href":605,"dataGaName":606,"dataGaLocation":490},"/topics/agentic-ai/","agentic ai",{"title":608,"links":609},"Solutions",[610,612,614,619,623,626,630,633,635,638,641,646],{"text":150,"config":611},{"href":145,"dataGaName":150,"dataGaLocation":490},{"text":139,"config":613},{"href":122,"dataGaName":123,"dataGaLocation":490},{"text":615,"config":616},"Agile development",{"href":617,"dataGaName":618,"dataGaLocation":490},"/solutions/agile-delivery/","agile delivery",{"text":620,"config":621},"SCM",{"href":135,"dataGaName":622,"dataGaLocation":490},"source code management",{"text":568,"config":624},{"href":127,"dataGaName":625,"dataGaLocation":490},"continuous integration & delivery",{"text":627,"config":628},"Value stream management",{"href":178,"dataGaName":629,"dataGaLocation":490},"value stream management",{"text":573,"config":631},{"href":632,"dataGaName":576,"dataGaLocation":490},"/solutions/gitops/",{"text":188,"config":634},{"href":190,"dataGaName":191,"dataGaLocation":490},{"text":636,"config":637},"Small business",{"href":195,"dataGaName":196,"dataGaLocation":490},{"text":639,"config":640},"Public sector",{"href":200,"dataGaName":201,"dataGaLocation":490},{"text":642,"config":643},"Education",{"href":644,"dataGaName":645,"dataGaLocation":490},"/solutions/education/","education",{"text":647,"config":648},"Financial services",{"href":649,"dataGaName":650,"dataGaLocation":490},"/solutions/finance/","financial services",{"title":208,"links":652},[653,655,657,659,662,664,666,668,670,672,674,676,678],{"text":220,"config":654},{"href":222,"dataGaName":223,"dataGaLocation":490},{"text":225,"config":656},{"href":227,"dataGaName":228,"dataGaLocation":490},{"text":230,"config":658},{"href":232,"dataGaName":233,"dataGaLocation":490},{"text":235,"config":660},{"href":237,"dataGaName":661,"dataGaLocation":490},"docs",{"text":258,"config":663},{"href":260,"dataGaName":5,"dataGaLocation":490},{"text":253,"config":665},{"href":255,"dataGaName":256,"dataGaLocation":490},{"text":262,"config":667},{"href":264,"dataGaName":265,"dataGaLocation":490},{"text":275,"config":669},{"href":277,"dataGaName":278,"dataGaLocation":490},{"text":267,"config":671},{"href":269,"dataGaName":270,"dataGaLocation":490},{"text":280,"config":673},{"href":282,"dataGaName":283,"dataGaLocation":490},{"text":285,"config":675},{"href":287,"dataGaName":288,"dataGaLocation":490},{"text":290,"config":677},{"href":292,"dataGaName":293,"dataGaLocation":490},{"text":295,"config":679},{"href":297,"dataGaName":298,"dataGaLocation":490},{"title":313,"links":681},[682,684,686,688,690,692,694,698,703,705,707,709],{"text":320,"config":683},{"href":322,"dataGaName":315,"dataGaLocation":490},{"text":325,"config":685},{"href":327,"dataGaName":328,"dataGaLocation":490},{"text":333,"config":687},{"href":335,"dataGaName":336,"dataGaLocation":490},{"text":338,"config":689},{"href":340,"dataGaName":341,"dataGaLocation":490},{"text":343,"config":691},{"href":345,"dataGaName":346,"dataGaLocation":490},{"text":348,"config":693},{"href":350,"dataGaName":351,"dataGaLocation":490},{"text":695,"config":696},"Sustainability",{"href":697,"dataGaName":695,"dataGaLocation":490},"/sustainability/",{"text":699,"config":700},"Diversity, inclusion and belonging (DIB)",{"href":701,"dataGaName":702,"dataGaLocation":490},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":353,"config":704},{"href":355,"dataGaName":356,"dataGaLocation":490},{"text":363,"config":706},{"href":365,"dataGaName":366,"dataGaLocation":490},{"text":368,"config":708},{"href":370,"dataGaName":371,"dataGaLocation":490},{"text":710,"config":711},"Modern Slavery Transparency Statement",{"href":712,"dataGaName":713,"dataGaLocation":490},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":715},[716,718,720],{"text":540,"config":717},{"href":542,"dataGaName":543,"dataGaLocation":490},{"text":545,"config":719},{"href":547,"dataGaName":548,"dataGaLocation":490},{"text":550,"config":721},{"dataGaName":552,"dataGaLocation":490,"id":553,"isOneTrustButton":108},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[727],{"_path":728,"_dir":729,"_draft":6,"_partial":6,"_locale":7,"content":730,"config":733,"_id":735,"_type":31,"title":18,"_source":33,"_file":736,"_stem":737,"_extension":36},"/en-us/blog/authors/eric-eastwood","authors",{"name":18,"config":731},{"headshot":7,"ctfId":732},"MadLittleMods",{"template":734},"BlogAuthor","content:en-us:blog:authors:eric-eastwood.yml","en-us/blog/authors/eric-eastwood.yml","en-us/blog/authors/eric-eastwood",{"_path":739,"_dir":39,"_draft":6,"_partial":6,"_locale":7,"header":740,"eyebrow":741,"blurb":742,"button":743,"secondaryButton":747,"_id":749,"_type":31,"title":750,"_source":33,"_file":751,"_stem":752,"_extension":36},"/shared/en-us/next-steps","Start shipping better software faster","50%+ of the Fortune 100 trust GitLab","See what your team can do with the intelligent\n\n\nDevSecOps platform.\n",{"text":47,"config":744},{"href":745,"dataGaName":50,"dataGaLocation":746},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":52,"config":748},{"href":54,"dataGaName":55,"dataGaLocation":746},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":754,"content":755,"config":758,"_id":30,"_type":31,"title":32,"_source":33,"_file":34,"_stem":35,"_extension":36},{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},{"title":9,"description":10,"authors":756,"heroImage":11,"date":19,"body":20,"category":21,"tags":757},[18],[23,24,25,26],{"slug":28,"featured":6,"template":29},1761814412654]