[{"data":1,"prerenderedAt":1876},["ShallowReactive",2],{"/blog/upgrading-bootstrap-vue":3,"navigation-en-us":1101,"banner-en-us":1533,"footer-en-us":1543,"blog-post-authors-en-us-Enrique Alcántara|Paul Gascou-Vaillancourt":1804,"blog-related-posts-en-us-upgrading-bootstrap-vue":1833,"blog-share-wrapper-en-us":1856,"next-steps-en-us":1866},{"id":4,"title":5,"authors":6,"body":9,"category":1084,"date":1085,"description":1086,"extension":1087,"externalUrl":1088,"faq":1088,"featured":1089,"heroImage":1090,"meta":1091,"navigation":560,"path":1092,"seo":1093,"slug":1097,"stem":1098,"tags":1088,"template":1099,"updatedDate":1088,"__hash__":1100},"blogPosts/en-us/blog/upgrading-bootstrap-vue.md","Upgrading bootstrap-vue in gitlab-ui",[7,8],"Enrique Alcántara","Paul Gascou-Vaillancourt",{"type":10,"value":11,"toc":1067},"minimark",[12,21,38,43,46,53,56,61,78,82,91,97,107,111,126,307,327,362,369,391,398,408,411,415,419,422,447,456,475,479,484,499,526,529,615,624,628,638,712,716,719,724,727,731,740,751,760,763,785,795,802,811,816,825,948,952,955,985,1002,1010,1014,1018,1021,1030,1034,1037,1052,1056,1063],[13,14,15,16,20],"p",{},"Three months ago, we started a process to upgrade bootstrap-vue in gitlab-ui from ",[17,18,19],"code",{},"2.0.0-rc.27"," to the\nlatest stable version. You may wonder: Why has it taken so long? We found several impediments along the\nway caused by backward compatibility issues widespread across GitLab’s codebase. In this blog post\nwe’ll cover the following topics:",[22,23,24,32],"ol",{},[25,26,27],"li",{},[28,29,31],"a",{"href":30},"#1-how-gitlab-ci-helped-us-during-the-migration","How GitLab CI helped us during the migration",[25,33,34],{},[28,35,37],{"href":36},"#2-bootstrap-vue-breaking-changes","Bootstrap-vue breaking changes",[39,40,42],"h2",{"id":41},"_1-how-gitlab-ci-helped-us-during-the-migration","1. How GitLab CI helped us during the migration",[13,44,45],{},"When dealing with a situation like this, where one of your libraries introduces a lot of breaking\nchanges that need to be iteratively fixed in the products that depend on them, having access to\ngreat tools like GitLab CI can be a huge time-saver, especially in a distributed company like GitLab.",[13,47,48,49,52],{},"It goes without saying that GitLab itself is heavily tested, with tens or even hundreds of CI jobs\nrun against every commit. While this is extremely useful, GitLab depends on an official\nrelease of GitLab UI which is pinned in its ",[17,50,51],{},"package.json",". This isn't helpful in our case\nbecause we don't want to release a new version of GitLab UI with BootstrapVue 2.0.0 stable unless we're\nabsolutely certain that it won’t cause adverse effects in GitLab.",[13,54,55],{},"While there are several ways to test an unreleased version of an NPM package in a project, we would\nlike to briefly explain how we did it thanks to a few GitLab CI jobs that demonstrate how you can\ntake advantage of this powerful feature for things that go beyond running test suites in your projects.",[57,58,60],"h3",{"id":59},"npm-publish","npm publish",[13,62,63,64,70,71,74,75,77],{},"Let's talk briefly about the ",[28,65,68],{"href":66,"rel":67},"https://docs.npmjs.com/cli/publish",[],[17,69,60],{}," command: it is the\ncommand that you would run to publish an NPM package to NPM's registry. When running the command, a\ntarball of your package is created and uploaded to the registry where it is tagged with the ",[17,72,73],{},"version","\nthat's defined in your ",[17,76,51],{},". This is great, but once you run the publish command, a new\nversion of your package becomes publicly available. Of course you could release beta versions of your\npackage but then again, you would most likely bloat the registry with versions of your package that\nyou know won't work. So, is there a way to publish a test npm package outside the npm registry?",[57,79,81],{"id":80},"yarn-pack","yarn pack",[13,83,84,85],{},"What now? Another CLI command? Okay so what does this one do? According to the doc,\n",[28,86,89],{"href":87,"rel":88},"https://yarnpkg.com/en/docs/cli/pack",[],[17,90,81],{},[92,93,94],"blockquote",{},[13,95,96],{},"Creates a compressed gzip archive of package dependencies.",[13,98,99,100,102,103,106],{},"So this means that it archives your package, like ",[17,101,60],{}," does, except that the archive will\nstay on your computer and won't be uploaded or published to any registry. And you can even give your\narchive a name using the ",[17,104,105],{},"--filename"," flag.",[57,108,110],{"id":109},"okay-so-how-does-that-help-us","Okay, so how does that help us?",[13,112,113,114,117,118,125],{},"All right, let's see how this is useful in GitLab UI's CI setup. If you look at the ",[17,115,116],{},".gitlab-ci.yml","\nfile in GitLab UI, you'll see an ",[28,119,122],{"href":120,"rel":121},"https://gitlab.com/gitlab-org/gitlab-ui/blob/3e8fd5e7f54542b7534203d65097039a3e731fd7/.gitlab-ci.yml#L183-201",[],[17,123,124],{},"upload_artifacts","\njob.",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-yaml shiki shiki-themes github-light","upload_artifacts:\n  extends: .node\n  stage: deploy\n  variables:\n    TAR_ARCHIVE_NAME: gitlab-ui.$CI_COMMIT_REF_SLUG.tgz\n  needs:\n    - build\n  script:\n    - yarn pack --filename $TAR_ARCHIVE_NAME\n    - DEPENDENCY_URL=\"$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/raw/$TAR_ARCHIVE_NAME\"\n    - echo \"The package.json dependency URL is $DEPENDENCY_URL\"\n    - echo $DEPENDENCY_URL > .dependency_url\n  artifacts:\n    when: always\n    paths:\n      - $TAR_ARCHIVE_NAME\n      - .dependency_url\n  only:\n    - /.+/@gitlab-org/gitlab-ui\n\n","yaml","",[17,134,135,147,160,171,179,190,198,207,215,223,231,239,247,255,266,274,283,291,299],{"__ignoreMap":132},[136,137,140,143],"span",{"class":138,"line":139},"line",1,[136,141,124],{"class":142},"shJU0",[136,144,146],{"class":145},"sgsFI",":\n",[136,148,150,153,156],{"class":138,"line":149},2,[136,151,152],{"class":142},"  extends",[136,154,155],{"class":145},": ",[136,157,159],{"class":158},"sYBdl",".node\n",[136,161,163,166,168],{"class":138,"line":162},3,[136,164,165],{"class":142},"  stage",[136,167,155],{"class":145},[136,169,170],{"class":158},"deploy\n",[136,172,174,177],{"class":138,"line":173},4,[136,175,176],{"class":142},"  variables",[136,178,146],{"class":145},[136,180,182,185,187],{"class":138,"line":181},5,[136,183,184],{"class":142},"    TAR_ARCHIVE_NAME",[136,186,155],{"class":145},[136,188,189],{"class":158},"gitlab-ui.$CI_COMMIT_REF_SLUG.tgz\n",[136,191,193,196],{"class":138,"line":192},6,[136,194,195],{"class":142},"  needs",[136,197,146],{"class":145},[136,199,201,204],{"class":138,"line":200},7,[136,202,203],{"class":145},"    - ",[136,205,206],{"class":158},"build\n",[136,208,210,213],{"class":138,"line":209},8,[136,211,212],{"class":142},"  script",[136,214,146],{"class":145},[136,216,218,220],{"class":138,"line":217},9,[136,219,203],{"class":145},[136,221,222],{"class":158},"yarn pack --filename $TAR_ARCHIVE_NAME\n",[136,224,226,228],{"class":138,"line":225},10,[136,227,203],{"class":145},[136,229,230],{"class":158},"DEPENDENCY_URL=\"$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/raw/$TAR_ARCHIVE_NAME\"\n",[136,232,234,236],{"class":138,"line":233},11,[136,235,203],{"class":145},[136,237,238],{"class":158},"echo \"The package.json dependency URL is $DEPENDENCY_URL\"\n",[136,240,242,244],{"class":138,"line":241},12,[136,243,203],{"class":145},[136,245,246],{"class":158},"echo $DEPENDENCY_URL > .dependency_url\n",[136,248,250,253],{"class":138,"line":249},13,[136,251,252],{"class":142},"  artifacts",[136,254,146],{"class":145},[136,256,258,261,263],{"class":138,"line":257},14,[136,259,260],{"class":142},"    when",[136,262,155],{"class":145},[136,264,265],{"class":158},"always\n",[136,267,269,272],{"class":138,"line":268},15,[136,270,271],{"class":142},"    paths",[136,273,146],{"class":145},[136,275,277,280],{"class":138,"line":276},16,[136,278,279],{"class":145},"      - ",[136,281,282],{"class":158},"$TAR_ARCHIVE_NAME\n",[136,284,286,288],{"class":138,"line":285},17,[136,287,279],{"class":145},[136,289,290],{"class":158},".dependency_url\n",[136,292,294,297],{"class":138,"line":293},18,[136,295,296],{"class":142},"  only",[136,298,146],{"class":145},[136,300,302,304],{"class":138,"line":301},19,[136,303,203],{"class":145},[136,305,306],{"class":158},"/.+/@gitlab-org/gitlab-ui\n",[13,308,309,310,313,314,317,318,320,321,323,324,326],{},"Notice how this job depends on the ",[17,311,312],{},"build"," job via the ",[17,315,316],{},"needs"," option, this means that it will\nonly run after ",[17,319,312],{}," has completed, and it will have access to ",[17,322,312],{},"'s artifacts, which, among\nother things, contain the production-ready compiled version of GitLab UI. Now ",[17,325,124],{},"\ndoes 3 important things:",[328,329,330,345,355],"ul",{},[25,331,332,333,336,337,340,341,344],{},"It declares a ",[17,334,335],{},"TAR_ARCHIVE_NAME"," envrionment variable which is a combination of ",[17,338,339],{},"gitlab-ui"," and the\ncurrent branch's name, followed by the ",[17,342,343],{},".tgz"," extension.",[25,346,347,348,351,352,354],{},"It calls ",[17,349,350],{},"yarn pack --filename $TAR_ARCHIVE_NAME"," which, as we've seen above, will create an\narchive of the package's dependencies, including the production-ready bundle from the ",[17,353,312],{}," job.",[25,356,357,358,361],{},"And finally, it lists ",[17,359,360],{},"$TAR_ARCHIVE_NAME"," as one the job's artifacts paths.",[13,363,364,365,368],{},"The last point is where all the magic happens, because the package's archive now becomes a\ndownloadable artifact, which means that we have a kind of simple and private registry for all of our\ndevelopment branches from which we can install GitLab UI's development builds to test them out in\nGitLab. Notice how the job also prints a URL which is constructed this way:\n",[17,366,367],{},"$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/raw/$TAR_ARCHIVE_NAME",". This gives us a direct download\nlink to the archived package that we can use to install the build in GitLab:",[127,370,374],{"className":371,"code":372,"language":373,"meta":132,"style":132},"language-sh shiki shiki-themes github-light","yarn add @gitlab/ui@$DEPENDENCY_URL\n","sh",[17,375,376],{"__ignoreMap":132},[136,377,378,382,385,388],{"class":138,"line":139},[136,379,381],{"class":380},"s7eDp","yarn",[136,383,384],{"class":158}," add",[136,386,387],{"class":158}," @gitlab/ui@",[136,389,390],{"class":145},"$DEPENDENCY_URL\n",[13,392,393,394,397],{},"Where ",[17,395,396],{},"$DEPENDENCY_URL"," is the artifact's URL.",[13,399,400,401,403,404,407],{},"This has helped us a lot during this big migration because we were able to open a merge request in\nGitLab where the ",[17,402,51],{}," would point to our GitLab UI development build. This allowed us to\nbenefit from the huge CI pipeline could benefit from the huge CI pipeline in GitLab to run every test\nsuite, thus giving us a nice overview of what needed to be fixed. It was also very useful in terms of\ncollaboration, because we were able to involve more engineers in the migration process without\nrequiring them to setup their local environment in any particular way. They would simply checkout the\ndevelopment branch, run ",[17,405,406],{},"yarn install",", and they would be good to go.",[13,409,410],{},"This is only one of the endless possibilities that GitLab CI offers. Hopefully it gave you some\nideas for your own special CI job!",[39,412,414],{"id":413},"_2-bootstrap-vue-breaking-changes","2. Bootstrap-vue breaking changes",[57,416,418],{"id":417},"different-import-statements","Different import statements",[13,420,421],{},"Importing bootstrap-vue components requires a different syntax. In bootstrap-vue 2.0.0-rc.27 (previous version in production), we imported components directly from the source path:",[127,423,427],{"className":424,"code":425,"language":426,"meta":132,"style":132},"language-javascript shiki shiki-themes github-light","import BDropdown from 'bootstrap-vue/src/components/dropdown/dropdown';\n","javascript",[17,428,429],{"__ignoreMap":132},[136,430,431,435,438,441,444],{"class":138,"line":139},[136,432,434],{"class":433},"sD7c4","import",[136,436,437],{"class":145}," BDropdown ",[136,439,440],{"class":433},"from",[136,442,443],{"class":158}," 'bootstrap-vue/src/components/dropdown/dropdown'",[136,445,446],{"class":145},";\n",[13,448,449,450,455],{},"In bootstrap-vue 2.0, the component source file does not have a default\n",[28,451,454],{"href":452,"rel":453},"https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/dropdown/dropdown.js#L85",[],"export statement anymore",".\nTo circumvent this issue, we changed all import statements to reference bootstrap-vue main import file.",[127,457,459],{"className":424,"code":458,"language":426,"meta":132,"style":132},"import { BDropdown } from 'bootstrap-vue';\n",[17,460,461],{"__ignoreMap":132},[136,462,463,465,468,470,473],{"class":138,"line":139},[136,464,434],{"class":433},[136,466,467],{"class":145}," { BDropdown } ",[136,469,440],{"class":433},[136,471,472],{"class":158}," 'bootstrap-vue'",[136,474,446],{"class":145},[57,476,478],{"id":477},"a-new-slot-syntax-for-bvtable-and-bvtab-components","A new slot syntax for BVTable and BVTab components",[480,481,483],"h4",{"id":482},"tables","Tables",[13,485,486,487,492,493,498],{},"Bootstrap-vue BTable dynamically generates Vue template slots based on the table’s\n",[28,488,491],{"href":489,"rel":490},"https://bootstrap-vue.js.org/docs/components/table",[],"column definitions","\nto customize the presentation of\n",[28,494,497],{"href":495,"rel":496},"https://bootstrap-vue.js.org/docs/components/table#custom-data-rendering",[],"content",".\nIn bootstrap-vue 2.0, the naming syntax for these slots changed:",[127,500,504],{"className":501,"code":502,"language":503,"meta":132,"style":132},"language-html shiki shiki-themes github-light","\u003Ctemplate #cell(project)=\"data\">\n","html",[17,505,506],{"__ignoreMap":132},[136,507,508,511,514,517,520,523],{"class":138,"line":139},[136,509,510],{"class":145},"\u003C",[136,512,513],{"class":142},"template",[136,515,516],{"class":380}," #cell(project)",[136,518,519],{"class":145},"=",[136,521,522],{"class":158},"\"data\"",[136,524,525],{"class":145},">\n",[13,527,528],{},"GitLab uses BTable widely, and we found several approaches to declare these slots across the codebase:",[127,530,532],{"className":501,"code":531,"language":503,"meta":132,"style":132},"\u003C!-- Version 1 -->\n\u003Ctemplate #HEAD_changeInPercent=\"{ label }\">\n\n\u003C!-- Version 2 -->\n\u003Ctemplate #name=\"items\">\n\n\u003C!-- Version 3 -->\n\u003Ctemplate slot=\"project\" slot-scope=\"data\">\n",[17,533,534,540,556,562,567,583,587,592],{"__ignoreMap":132},[136,535,536],{"class":138,"line":139},[136,537,539],{"class":538},"sAwPA","\u003C!-- Version 1 -->\n",[136,541,542,544,546,549,551,554],{"class":138,"line":149},[136,543,510],{"class":145},[136,545,513],{"class":142},[136,547,548],{"class":380}," #HEAD_changeInPercent",[136,550,519],{"class":145},[136,552,553],{"class":158},"\"{ label }\"",[136,555,525],{"class":145},[136,557,558],{"class":138,"line":162},[136,559,561],{"emptyLinePlaceholder":560},true,"\n",[136,563,564],{"class":138,"line":173},[136,565,566],{"class":538},"\u003C!-- Version 2 -->\n",[136,568,569,571,573,576,578,581],{"class":138,"line":181},[136,570,510],{"class":145},[136,572,513],{"class":142},[136,574,575],{"class":380}," #name",[136,577,519],{"class":145},[136,579,580],{"class":158},"\"items\"",[136,582,525],{"class":145},[136,584,585],{"class":138,"line":192},[136,586,561],{"emptyLinePlaceholder":560},[136,588,589],{"class":138,"line":200},[136,590,591],{"class":538},"\u003C!-- Version 3 -->\n",[136,593,594,596,598,601,603,606,609,611,613],{"class":138,"line":209},[136,595,510],{"class":145},[136,597,513],{"class":142},[136,599,600],{"class":380}," slot",[136,602,519],{"class":145},[136,604,605],{"class":158},"\"project\"",[136,607,608],{"class":380}," slot-scope",[136,610,519],{"class":145},[136,612,522],{"class":158},[136,614,525],{"class":145},[13,616,617,618,623],{},"GitLab test suite detected the components broken by this change when running the tests using the artifact generated by gitlab-ui.\nWe fixed these failures in the ",[28,619,622],{"href":620,"rel":621},"https://gitlab.com/gitlab-org/gitlab/merge_requests/18913",[],"upgrade bootstrap-vue merge request",".",[480,625,627],{"id":626},"tabs","Tabs",[13,629,630,631,633,634,637],{},"The BTabs component does not have the ",[17,632,626],{}," slot anymore (BV deprecated this slot in previous versions). You should use ",[17,635,636],{},"tabs-end"," instead.",[127,639,641],{"className":501,"code":640,"language":503,"meta":132,"style":132},"\u003Ctemplate #tabs> \u003C!-- Deprecated version -->\n\u003Ctemplate #tabs-end> \u003C!-- Use tabs-end instead -->\n \u003Cli class=\"nav-item align-self-center\">\n Contentless tab\n \u003C/li>\n\u003C/template>\n",[17,642,643,658,672,689,694,703],{"__ignoreMap":132},[136,644,645,647,649,652,655],{"class":138,"line":139},[136,646,510],{"class":145},[136,648,513],{"class":142},[136,650,651],{"class":380}," #tabs",[136,653,654],{"class":145},"> ",[136,656,657],{"class":538},"\u003C!-- Deprecated version -->\n",[136,659,660,662,664,667,669],{"class":138,"line":149},[136,661,510],{"class":145},[136,663,513],{"class":142},[136,665,666],{"class":380}," #tabs-end",[136,668,654],{"class":145},[136,670,671],{"class":538},"\u003C!-- Use tabs-end instead -->\n",[136,673,674,677,679,682,684,687],{"class":138,"line":162},[136,675,676],{"class":145}," \u003C",[136,678,25],{"class":142},[136,680,681],{"class":380}," class",[136,683,519],{"class":145},[136,685,686],{"class":158},"\"nav-item align-self-center\"",[136,688,525],{"class":145},[136,690,691],{"class":138,"line":173},[136,692,693],{"class":145}," Contentless tab\n",[136,695,696,699,701],{"class":138,"line":181},[136,697,698],{"class":145}," \u003C/",[136,700,25],{"class":142},[136,702,525],{"class":145},[136,704,705,708,710],{"class":138,"line":192},[136,706,707],{"class":145},"\u003C/",[136,709,513],{"class":142},[136,711,525],{"class":145},[57,713,715],{"id":714},"heavily-refactored-tooltip-and-popover-components","Heavily refactored tooltip and popover components",[13,717,718],{},"The changes introduced in the tooltip component generated the most significant number of side-effects in gitlab-ui and GitLab.\nFrom the bootstrap-vue changelog entry for 2.0.0:",[92,720,721],{},[13,722,723],{},"Tooltips and popovers have been completely re-written for better reactivity and stability. The directive versions are now reactive to trigger element title attribute changes and configuration changes.",[13,725,726],{},"Since the API for these components didn’t change, our codebase shouldn’t have been affected by their\nrefactoring. That wasn’t the case. When we ran the GitLab test suite, all test specs for components\nthat have the tooltip as a dependency failed. As you may realize, those are many Karma, Jest,\nand unit tests. You’ll wonder... what happened?",[480,728,730],{"id":729},"problem-1-the-tooltip-directive-expects-to-be-attached-to-the-dom-document","Problem 1: The tooltip directive expects to be attached to the DOM document",[13,732,733,734,739],{},"One of the behaviors introduced by the tooltip component refactoring is that we can’t initialize the\ncomponent unless it is attached to the\n",[28,735,738],{"href":736,"rel":737},"https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/tooltip/helpers/bv-tooltip.js#L218",[],"document object",". If that condition is not satisfied, bootstrap-vue logs the following\nwarning message, and won’t initialize the component:",[127,741,745],{"className":742,"code":743,"language":744,"meta":132,"style":132},"language-log shiki shiki-themes github-light","[BootstrapVue warn]: tooltip unable to find target element in document\"\n","log",[17,746,747],{"__ignoreMap":132},[136,748,749],{"class":138,"line":139},[136,750,743],{"class":145},[13,752,753,754,759],{},"None of our unit tests attach the component under test to the ",[28,755,758],{"href":756,"rel":757},"https://developer.mozilla.org/en-US/docs/Web/API/Window/document",[],"document"," object. This action could cause memory leaks in Karma unit tests\nbecause test suite environments are not isolated.",[13,761,762],{},"How did we solve this problem?",[13,764,765,766,771,772,777,778,106],{},"First, we migrated all the failing Karma tests to Jest in separate merge requests (see ",[28,767,770],{"href":768,"rel":769},"https://gitlab.com/gitlab-org/gitlab/merge_requests/18913?diff_id=70349853#1-karma",[],"group 1"," and ",[28,773,776],{"href":774,"rel":775},"https://gitlab.com/gitlab-org/gitlab/merge_requests/18913?diff_id=70349853#note_249314702",[],"group 2",").\nWe also rewrote those tests to use vue-test-utils instead of a legacy vue test utility we wrote.\nThis step was essential because Karma doesn’t have dependency mocking capabilities.\nAlso, when mounting the component under test using vue-test-utils, we can easily attach it to the document using the ",[28,779,782],{"href":780,"rel":781},"https://vue-test-utils.vuejs.org/api/options.html#attachtodocument",[],[17,783,784],{},"attachToDocument",[13,786,787,788,790,791,794],{},"The next steps were a mix of migrating Jest specs to vue-test-utils if needed and setting the ",[17,789,784],{}," flag to ",[17,792,793],{},"true",".\nThe work ahead was staggering: We had to fix 74 test suites before calling a victory. Many of those specs required significant changes. To avoid increasing the size of the upgrade MR, we opened separate\nMRs for each spec file. Another advantage of using different MRs is that it enabled us to distribute\nthe migration effort among several hands.",[13,796,797],{},[798,799],"img",{"alt":800,"src":801},"upgrading bootstrap-vue","https://res.cloudinary.com/about-gitlab-com/image/upload/v1782404919/site/Content%20Images/upgrading-bootstrap-vue/failing-spec-list.png",[13,803,804,805,810],{},"Over time, this strategy became an uphill battle as more failing specs popped-up. We were pursuing a moving target, so we decided to\nfind an alternative. The reason behind adapting the failing tests to work with the rewritten tooltip is because tests were coupled to the\ntooltip implementation. Instead of honoring that coupling, we mocked the tooltip dependency to eliminate it. As explained\nby ",[28,806,809],{"href":807,"rel":808},"https://vue-test-utils.vuejs.org/guides/#shallow-rendering",[],"vue test utils",":",[92,812,813],{},[13,814,815],{},"In unit tests, we typically want to focus on the component being tested as an isolated unit and avoid indirectly asserting the behavior of its child components.",[13,817,818,819,824],{},"We used Jest ",[28,820,823],{"href":821,"rel":822},"https://jestjs.io/docs/en/manual-mocks#mocking-node-modules",[],"manual mock"," feature to replace the tooltip directive\nwith a shallow version that only provides essential functionality to avoid breaking tests.",[127,826,828],{"className":424,"code":827,"language":426,"meta":132,"style":132},"export * from '@gitlab/ui';\n\nexport const GlTooltipDirective = {\n bind() {},\n};\n\nexport const GlTooltip = {\n render(h) {\n return h('div', this.$attrs, this.$slots.default);\n },\n};\n",[17,829,830,847,851,867,875,880,884,897,912,939,944],{"__ignoreMap":132},[136,831,832,835,839,842,845],{"class":138,"line":139},[136,833,834],{"class":433},"export",[136,836,838],{"class":837},"sYu0t"," *",[136,840,841],{"class":433}," from",[136,843,844],{"class":158}," '@gitlab/ui'",[136,846,446],{"class":145},[136,848,849],{"class":138,"line":149},[136,850,561],{"emptyLinePlaceholder":560},[136,852,853,855,858,861,864],{"class":138,"line":162},[136,854,834],{"class":433},[136,856,857],{"class":433}," const",[136,859,860],{"class":837}," GlTooltipDirective",[136,862,863],{"class":433}," =",[136,865,866],{"class":145}," {\n",[136,868,869,872],{"class":138,"line":173},[136,870,871],{"class":380}," bind",[136,873,874],{"class":145},"() {},\n",[136,876,877],{"class":138,"line":181},[136,878,879],{"class":145},"};\n",[136,881,882],{"class":138,"line":192},[136,883,561],{"emptyLinePlaceholder":560},[136,885,886,888,890,893,895],{"class":138,"line":200},[136,887,834],{"class":433},[136,889,857],{"class":433},[136,891,892],{"class":837}," GlTooltip",[136,894,863],{"class":433},[136,896,866],{"class":145},[136,898,899,902,905,909],{"class":138,"line":209},[136,900,901],{"class":380}," render",[136,903,904],{"class":145},"(",[136,906,908],{"class":907},"sqxcx","h",[136,910,911],{"class":145},") {\n",[136,913,914,917,920,922,925,928,931,934,936],{"class":138,"line":217},[136,915,916],{"class":433}," return",[136,918,919],{"class":380}," h",[136,921,904],{"class":145},[136,923,924],{"class":158},"'div'",[136,926,927],{"class":145},", ",[136,929,930],{"class":837},"this",[136,932,933],{"class":145},".$attrs, ",[136,935,930],{"class":837},[136,937,938],{"class":145},".$slots.default);\n",[136,940,941],{"class":138,"line":225},[136,942,943],{"class":145}," },\n",[136,945,946],{"class":138,"line":233},[136,947,879],{"class":145},[480,949,951],{"id":950},"problem-2-asserting-tooltip-directives-internals","Problem 2: Asserting tooltip directive’s internals",[13,953,954],{},"We found many unit tests that contained the following assertion pattern:",[127,956,958],{"className":424,"code":957,"language":426,"meta":132,"style":132},"expect(wrapper.attributes('data-original-title')).toContain(statusMessage);\n",[17,959,960],{"__ignoreMap":132},[136,961,962,965,968,971,973,976,979,982],{"class":138,"line":139},[136,963,964],{"class":380},"expect",[136,966,967],{"class":145},"(wrapper.",[136,969,970],{"class":380},"attributes",[136,972,904],{"class":145},[136,974,975],{"class":158},"'data-original-title'",[136,977,978],{"class":145},")).",[136,980,981],{"class":380},"toContain",[136,983,984],{"class":145},"(statusMessage);\n",[13,986,987,988,991,992,995,996,998,999,1001],{},"The purpose of this assertion is verifying that the content of a tooltip attached to a component\nis correct. The tooltip directive expects to find its content in the ",[17,989,990],{},"title"," attribute of an element.\nWhy were we verifying the ",[17,993,994],{},"data-original-title"," attribute then? Before bootstrap-vue 2.0, the tooltip\ndirective unset the ",[17,997,990],{}," attribute and set ",[17,1000,994],{}," with the former’s value. All\nthe specs that asserted the tooltip’s content were coupled to this internal behavior. Once we\nupgraded to a version that didn’t follow this behavior, these specs failed.",[13,1003,1004,1005,1007,1008,623],{},"The solution to this problem is very similar to problem 1’s. First, mocking the tooltip directive\nallowed us to decouple unit tests from BV implementation. Afterward, We just needed to replace all\n",[17,1006,994],{}," references with the ",[17,1009,990],{},[39,1011,1013],{"id":1012},"lessons-learned","Lessons learned",[57,1015,1017],{"id":1016},"_1-technical-debt-impacts-significantly-our-ability-to-upgrade-dependencies","1. Technical debt impacts significantly our ability to upgrade dependencies",[13,1019,1020],{},"This upgrade was costly because it required fixing hundreds of unit and feature tests. The costs\ncould’ve been much lower if we hadn’t had to migrate as many tests to use jest and vue-test-utils. This\nis a palpable reason to schedule time in each milestone to migrate specs written with legacy\npractices to modern ones.",[13,1022,1023,1024,1029],{},"The FE department has organized an effort to migrate legacy specs to use jest and vue-test-utils in this\n",[28,1025,1028],{"href":1026,"rel":1027},"https://gitlab.com/groups/gitlab-org/-/epics/895",[],"epic",". The epic also contains guidelines about how to do it.\nAll contributions are appreciated.",[57,1031,1033],{"id":1032},"_2-avoid-testing-components-dependencies-internals","2. Avoid testing component’s dependencies internals",[13,1035,1036],{},"Unit tests should focus as much as possible in the component under test and refrain from making\nassumptions about how the component’s dependencies work. The other factor that increased costs\ndrastically was finding specs that made assumptions about how the tooltip directive worked under\nthe hood. We should strive to mock component’s dependencies to keep unit tests focused.",[13,1038,1039,1040,1045,1046,1051],{},"The FE department has opened a ",[28,1041,1044],{"href":1042,"rel":1043},"https://gitlab.com/gitlab-org/gitlab/merge_requests/18296",[],"merge request","\nthat proposes using higher-level selectors in unit tests. There are scenarios where an integration\ntest is more suited, though. We are also ",[28,1047,1050],{"href":1048,"rel":1049},"https://gitlab.com/gitlab-org/gitlab/issues/26982",[],"discussing approaches","\nto have a better distinction between unit and integration tests in the frontend.",[39,1053,1055],{"id":1054},"useful-resources","Useful resources",[13,1057,1058],{},[28,1059,1062],{"href":1060,"rel":1061},"https://bootstrap-vue.js.org/docs/misc/changelog",[],"Bootstrap-vue 2.0 migration guide and changelogs",[1064,1065,1066],"style",{},"html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}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 .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":132,"searchDepth":149,"depth":149,"links":1068},[1069,1074,1079,1083],{"id":41,"depth":149,"text":42,"children":1070},[1071,1072,1073],{"id":59,"depth":162,"text":60},{"id":80,"depth":162,"text":81},{"id":109,"depth":162,"text":110},{"id":413,"depth":149,"text":414,"children":1075},[1076,1077,1078],{"id":417,"depth":162,"text":418},{"id":477,"depth":162,"text":478},{"id":714,"depth":162,"text":715},{"id":1012,"depth":149,"text":1013,"children":1080},[1081,1082],{"id":1016,"depth":162,"text":1017},{"id":1032,"depth":162,"text":1033},{"id":1054,"depth":149,"text":1055},"unfiltered","2020-01-24","How we upgraded BootstrapVue to v2 stable in GitLab UI, our UI library, and what challenges we encountered in the process","md",null,false,"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749664102/Blog/Hero%20Images/gitlab-values-cover.png",{},"/en-us/blog/upgrading-bootstrap-vue",{"title":5,"description":1086,"ogTitle":5,"ogDescription":1086,"noIndex":1089,"ogImage":1090,"ogUrl":1094,"ogSiteName":1095,"ogType":1096,"canonicalUrls":1094},"https://about.gitlab.com/blog/upgrading-bootstrap-vue","https://about.gitlab.com","article","upgrading-bootstrap-vue","en-us/blog/upgrading-bootstrap-vue","BlogPost","eiH1HRvFCvgeJOTCUF8Tj5rhjrgxbOOyjyOYjBoXZIQ",{"logo":1102,"freeTrial":1107,"sales":1112,"login":1117,"items":1122,"search":1453,"minimal":1484,"duo":1503,"switchNav":1512,"pricingDeployment":1523},{"config":1103},{"href":1104,"dataGaName":1105,"dataGaLocation":1106},"/","gitlab logo","header",{"text":1108,"config":1109},"Get free trial",{"href":1110,"dataGaName":1111,"dataGaLocation":1106},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":1113,"config":1114},"Request a demo",{"href":1115,"dataGaName":1116,"dataGaLocation":1106},"/request-a-demo/","sales",{"text":1118,"config":1119},"Sign in",{"href":1120,"dataGaName":1121,"dataGaLocation":1106},"https://gitlab.com/users/sign_in/","sign in",[1123,1209,1297,1302,1377,1431],{"text":1124,"left":560,"config":1125,"menu":1127},"Platform",{"dataNavLevelOne":1126},"platform",{"type":1128,"link":1129,"columns":1133,"feature":1198},"lists",{"text":1130,"config":1131},"Explore the Platform",{"href":1132,"dataGaName":1126,"dataGaLocation":1106},"/platform/",[1134,1157,1180],{"title":1135,"items":1136},"Execution & Workflows",[1137,1142,1147,1152],{"text":1138,"config":1139},"CI/CD",{"href":1140,"dataGaLocation":1106,"dataGaName":1141},"/solutions/continuous-integration/","ci/cd",{"text":1143,"config":1144},"Source Code Management",{"href":1145,"dataGaLocation":1106,"dataGaName":1146},"/solutions/source-code-management/","source code management",{"text":1148,"config":1149},"Agile Delivery",{"href":1150,"dataGaLocation":1106,"dataGaName":1151},"/solutions/agile-delivery/","agile delivery",{"text":1153,"config":1154},"Artifact Management",{"href":1155,"dataGaLocation":1106,"dataGaName":1156},"/artifact-central/","artifact management",{"title":1158,"items":1159},"Security & Governance",[1160,1165,1170,1175],{"text":1161,"config":1162},"Application Security Testing",{"href":1163,"dataGaLocation":1106,"dataGaName":1164},"/solutions/application-security-testing/","application security testing",{"text":1166,"config":1167},"Governance & Compliance",{"href":1168,"dataGaLocation":1106,"dataGaName":1169},"/solutions/software-compliance/","governance and compliance",{"text":1171,"config":1172},"Secrets Management",{"href":1173,"dataGaLocation":1106,"dataGaName":1174},"/gitlab-secrets-manager/","secrets management",{"text":1176,"config":1177},"Supply Chain Security",{"href":1178,"dataGaLocation":1106,"dataGaName":1179},"/solutions/supply-chain/","supply chain security",{"title":1181,"items":1182},"Context & AI",[1183,1188,1193],{"text":1184,"config":1185},"Agentic Orchestration",{"href":1186,"dataGaLocation":1106,"dataGaName":1187},"/gitlab-duo-agent-platform/","agentic orchestration",{"text":1189,"config":1190},"Context Graph",{"href":1191,"dataGaLocation":1106,"dataGaName":1192},"/gitlab-orbit/","context graph",{"text":1194,"config":1195},"Visibility & Measurement",{"href":1196,"dataGaLocation":1106,"dataGaName":1197},"/solutions/visibility-measurement/","visibility and measurement",{"config":1199,"title":1202,"text":1203,"link":1204},{"background":1200,"textColor":1201},"url('https://res.cloudinary.com/about-gitlab-com/image/upload/v1790185102/o7dn32anqcb6z8dgd3as.jpg')","#000","Why GitLab","One platform for speed with control across your software lifecycle.",{"text":1205,"config":1206},"Learn more",{"href":1207,"dataGaName":1208,"dataGaLocation":1106},"/why-gitlab/","why gitlab",{"text":1210,"left":560,"config":1211,"menu":1213},"Solutions",{"dataNavLevelOne":1212},"solutions",{"type":1128,"link":1214,"columns":1218,"feature":1288},{"text":1215,"config":1216},"View all Solutions",{"href":1217,"dataGaName":1212,"dataGaLocation":1106},"/solutions/",[1219,1237,1255],{"title":1220,"items":1221},"Outcomes",[1222,1227,1232],{"text":1223,"config":1224},"DevOps Modernization",{"href":1225,"dataGaLocation":1106,"dataGaName":1226},"/assessments/devops-modernization-assessment/","devops modernization",{"text":1228,"config":1229},"Security Modernization",{"href":1230,"dataGaLocation":1106,"dataGaName":1231},"/assessments/security-modernization-assessment/","security modernization",{"text":1233,"config":1234},"AI Modernization",{"href":1235,"dataGaLocation":1106,"dataGaName":1236},"/assessments/ai-modernization-assessment/","ai modernization",{"title":1238,"items":1239},"By size",[1240,1245,1250],{"text":1241,"config":1242},"Enterprise",{"href":1243,"dataGaLocation":1106,"dataGaName":1244},"/enterprise/","enterprise",{"text":1246,"config":1247},"Small Business",{"href":1248,"dataGaLocation":1106,"dataGaName":1249},"/small-business/","small business",{"text":1251,"config":1252},"Startups",{"href":1253,"dataGaLocation":1106,"dataGaName":1254},"/solutions/startups/","startups",{"title":1256,"items":1257},"Industries",[1258,1263,1268,1273,1278,1283],{"text":1259,"config":1260},"Financial Services",{"href":1261,"dataGaLocation":1106,"dataGaName":1262},"/solutions/finance/","financial services",{"text":1264,"config":1265},"Public Sector",{"href":1266,"dataGaLocation":1106,"dataGaName":1267},"/solutions/public-sector/","public sector",{"text":1269,"config":1270},"Telecommunications",{"href":1271,"dataGaLocation":1106,"dataGaName":1272},"/solutions/telecommunications/","telecommunications",{"text":1274,"config":1275},"Automotive",{"href":1276,"dataGaLocation":1106,"dataGaName":1277},"/solutions/automotive/","automotive",{"text":1279,"config":1280},"Education",{"href":1281,"dataGaLocation":1106,"dataGaName":1282},"/solutions/education/","education",{"text":1284,"config":1285},"Aerospace",{"href":1286,"dataGaLocation":1106,"dataGaName":1287},"/solutions/aerospace/","aerospace",{"config":1289,"title":1290,"text":1291,"link":1292},{"background":1200,"textColor":1201},"GitLab Security Standard","Learn how to harden your software factory and let AI agents earn autonomy, without trading away security.",{"text":1293,"config":1294},"Explore the standard",{"href":1295,"dataGaName":1296,"dataGaLocation":1106},"/gitlab-security-standard/","gitlab security standard",{"text":1298,"config":1299},"Pricing",{"href":1300,"dataGaName":1301,"dataGaLocation":1106,"dataNavLevelOne":1301},"/pricing/","pricing",{"text":1303,"config":1304,"menu":1306},"Resources",{"dataNavLevelOne":1305},"resources",{"type":1128,"link":1307,"columns":1311,"feature":1368},{"text":1308,"config":1309},"View all resources",{"href":1310,"dataGaName":1305,"dataGaLocation":1106},"/resources/",[1312,1340],{"title":1313,"items":1314},"Discover",[1315,1320,1325,1330,1335],{"text":1316,"config":1317},"Docs",{"href":1318,"dataGaName":1319,"dataGaLocation":1106},"https://docs.gitlab.com/","docs",{"text":1321,"config":1322},"University",{"href":1323,"dataGaName":1324,"dataGaLocation":1106},"https://university.gitlab.com/","university",{"text":1326,"config":1327},"Demo Series",{"href":1328,"dataGaName":1329,"dataGaLocation":1106},"/gitlab-technical-demo-series/","demo series",{"text":1331,"config":1332},"Demo Hub",{"href":1333,"dataGaName":1334,"dataGaLocation":1106},"/demo-hub/","demo hub",{"text":1336,"config":1337},"Services",{"href":1338,"dataGaName":1339,"dataGaLocation":1106},"/services/","services",{"title":1341,"items":1342},"Connect",[1343,1348,1353,1358,1363],{"text":1344,"config":1345},"Blog",{"href":1346,"dataGaName":1347,"dataGaLocation":1106},"/blog/","blog",{"text":1349,"config":1350},"Community",{"href":1351,"dataGaName":1352,"dataGaLocation":1106},"/community/","community",{"text":1354,"config":1355},"Customers",{"href":1356,"dataGaName":1357,"dataGaLocation":1106},"/customers/","customers",{"text":1359,"config":1360},"Partners",{"href":1361,"dataGaName":1362,"dataGaLocation":1106},"/partners/","partners",{"text":1364,"config":1365},"Events",{"href":1366,"dataGaName":1367,"dataGaLocation":1106},"/events/","events",{"config":1369,"title":1370,"text":1371,"link":1372},{"background":1200,"textColor":1201},"What’s new in GitLab","Stay updated with our latest features and improvements.",{"text":1373,"config":1374},"Read the latest",{"href":1375,"dataGaName":1376,"dataGaLocation":1106},"/whats-new/","whats new",{"text":1378,"config":1379,"menu":1381},"Company",{"dataNavLevelOne":1380},"company",{"type":1128,"columns":1382},[1383],{"items":1384},[1385,1390,1396,1401,1406,1411,1416,1421,1426],{"text":1386,"config":1387},"About",{"href":1388,"dataGaName":1389,"dataGaLocation":1106},"/company/","about",{"text":1391,"config":1392,"footerGa":1395},"Jobs",{"href":1393,"dataGaName":1394,"dataGaLocation":1106},"/jobs/","jobs",{"dataGaName":1394},{"text":1397,"config":1398},"Press",{"href":1399,"dataGaName":1400,"dataGaLocation":1106},"/press/","press",{"text":1402,"config":1403},"Handbook",{"href":1404,"dataGaName":1405,"dataGaLocation":1106},"https://handbook.gitlab.com/","handbook",{"text":1407,"config":1408},"Leadership",{"href":1409,"dataGaName":1410,"dataGaLocation":1106},"/company/team/e-group/","leadership",{"text":1412,"config":1413},"Investor relations",{"href":1414,"dataGaName":1415,"dataGaLocation":1106},"https://ir.gitlab.com/overview/default.aspx","investor relations",{"text":1417,"config":1418},"Trust Center",{"href":1419,"dataGaName":1420,"dataGaLocation":1106},"/security/","trust center",{"text":1422,"config":1423},"AI Transparency Center",{"href":1424,"dataGaName":1425,"dataGaLocation":1106},"/ai-transparency-center/","ai transparency center",{"text":1427,"config":1428},"Newsletter",{"href":1429,"dataGaName":1430,"dataGaLocation":1106},"/company/contact/#contact-forms","newsletter",{"text":1432,"config":1433,"menu":1434},"Contact us",{"dataNavLevelOne":1380},{"type":1128,"columns":1435},[1436],{"items":1437},[1438,1443,1448],{"text":1439,"config":1440},"Talk to sales",{"href":1441,"dataGaName":1442,"dataGaLocation":1106},"/sales/","talk to sales",{"text":1444,"config":1445},"Support portal",{"href":1446,"dataGaName":1447,"dataGaLocation":1106},"https://support.gitlab.com/hc/en-us","support portal",{"text":1449,"config":1450},"Customer portal",{"href":1451,"dataGaName":1452,"dataGaLocation":1106},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":1454,"login":1455,"suggestions":1462},"Close",{"text":1456,"link":1457},"To search repositories and projects, login to",{"text":1458,"config":1459},"gitlab.com",{"href":1120,"dataGaName":1460,"dataGaLocation":1461},"search login","search",{"text":1463,"default":1464},"Suggestions",[1465,1468,1471,1473,1477,1481],{"text":1466,"config":1467},"GitLab Duo Agent Platform",{"href":1186,"dataGaName":1466,"dataGaLocation":1461},{"text":1469,"config":1470},"Code Suggestions (AI)",{"href":1186,"dataGaName":1469,"dataGaLocation":1461},{"text":1138,"config":1472},{"href":1140,"dataGaName":1138,"dataGaLocation":1461},{"text":1474,"config":1475},"GitLab on AWS",{"href":1476,"dataGaName":1474,"dataGaLocation":1461},"/partners/technology-partners/aws/",{"text":1478,"config":1479},"GitLab on Google Cloud",{"href":1480,"dataGaName":1478,"dataGaLocation":1461},"/partners/technology-partners/google-cloud-platform/",{"text":1482,"config":1483},"Why GitLab?",{"href":1207,"dataGaName":1482,"dataGaLocation":1461},{"freeTrial":1485,"mobileIcon":1490,"desktopIcon":1495,"secondaryButton":1498},{"text":1486,"config":1487},"Start free trial",{"href":1488,"dataGaName":1111,"dataGaLocation":1489},"https://gitlab.com/-/trials/new/","nav",{"altText":1491,"config":1492},"Gitlab Icon",{"src":1493,"dataGaName":1494,"dataGaLocation":1489},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":1491,"config":1496},{"src":1497,"dataGaName":1494,"dataGaLocation":1489},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":1499,"config":1500},"Get Started",{"href":1501,"dataGaName":1502,"dataGaLocation":1489},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":1504,"mobileIcon":1508,"desktopIcon":1510},{"text":1505,"config":1506},"Learn more about GitLab Duo",{"href":1186,"dataGaName":1507,"dataGaLocation":1489},"gitlab duo",{"altText":1491,"config":1509},{"src":1493,"dataGaName":1494,"dataGaLocation":1489},{"altText":1491,"config":1511},{"src":1497,"dataGaName":1494,"dataGaLocation":1489},{"button":1513,"mobileIcon":1518,"desktopIcon":1520},{"text":1514,"config":1515},"/switch",{"href":1516,"dataGaName":1517,"dataGaLocation":1489},"#contact","switch",{"altText":1491,"config":1519},{"src":1493,"dataGaName":1494,"dataGaLocation":1489},{"altText":1491,"config":1521},{"src":1522,"dataGaName":1494,"dataGaLocation":1489},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":1524,"mobileIcon":1529,"desktopIcon":1531},{"text":1525,"config":1526},"Back to pricing",{"href":1300,"dataGaName":1527,"dataGaLocation":1489,"icon":1528},"back to pricing","GoBack",{"altText":1491,"config":1530},{"src":1493,"dataGaName":1494,"dataGaLocation":1489},{"altText":1491,"config":1532},{"src":1497,"dataGaName":1494,"dataGaLocation":1489},{"title":1534,"button":1535,"config":1541},"Ship at agent speed. Prove every step. Transcend is here.",{"text":1536,"config":1537},"Watch now",{"href":1538,"dataGaName":1539,"dataGaLocation":1540},"/events/transcend/virtual/#watch-gitlab-transcend","transcend-october-2026-banner","global-banner",{"layout":1542,"disabled":1089},"release",{"data":1544},{"text":1545,"source":1546,"edit":1552,"contribute":1557,"config":1562,"items":1567,"minimal":1793},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":1547,"config":1548},"View page source",{"href":1549,"dataGaName":1550,"dataGaLocation":1551},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":1553,"config":1554},"Edit this page",{"href":1555,"dataGaName":1556,"dataGaLocation":1551},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":1558,"config":1559},"Please contribute",{"href":1560,"dataGaName":1561,"dataGaLocation":1551},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":1563,"facebook":1564,"youtube":1565,"linkedin":1566},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[1568,1615,1673,1720,1761],{"title":1298,"links":1569,"subMenu":1584},[1570,1574,1579],{"text":1571,"config":1572},"View plans",{"href":1300,"dataGaName":1573,"dataGaLocation":1551},"view plans",{"text":1575,"config":1576},"Why Premium?",{"href":1577,"dataGaName":1578,"dataGaLocation":1551},"/pricing/premium/","why premium",{"text":1580,"config":1581},"Why Ultimate?",{"href":1582,"dataGaName":1583,"dataGaLocation":1551},"/pricing/ultimate/","why ultimate",[1585],{"title":1586,"links":1587},"Contact Us",[1588,1591,1593,1595,1600,1605,1610],{"text":1589,"config":1590},"Contact sales",{"href":1441,"dataGaName":1116,"dataGaLocation":1551},{"text":1444,"config":1592},{"href":1446,"dataGaName":1447,"dataGaLocation":1551},{"text":1449,"config":1594},{"href":1451,"dataGaName":1452,"dataGaLocation":1551},{"text":1596,"config":1597},"Status",{"href":1598,"dataGaName":1599,"dataGaLocation":1551},"https://status.gitlab.com/","status",{"text":1601,"config":1602},"Terms of use",{"href":1603,"dataGaName":1604,"dataGaLocation":1551},"/terms/","terms of use",{"text":1606,"config":1607},"Privacy statement",{"href":1608,"dataGaName":1609,"dataGaLocation":1551},"/privacy/","privacy statement",{"text":1611,"config":1612},"Cookie preferences",{"dataGaName":1613,"dataGaLocation":1551,"id":1614,"isOneTrustButton":560},"cookie preferences","ot-sdk-btn",{"title":1124,"links":1616,"subMenu":1630},[1617,1620,1622,1624,1626,1628],{"text":1618,"config":1619},"Explore the platform",{"href":1132,"dataGaName":1126,"dataGaLocation":1551},{"text":1184,"config":1621},{"href":1186,"dataGaName":1187,"dataGaLocation":1551},{"text":1138,"config":1623},{"href":1140,"dataGaName":1141,"dataGaLocation":1551},{"text":1189,"config":1625},{"href":1191,"dataGaName":1192,"dataGaLocation":1551},{"text":1171,"config":1627},{"href":1173,"dataGaName":1174,"dataGaLocation":1551},{"text":1202,"config":1629},{"href":1207,"dataGaName":1208,"dataGaLocation":1551},[1631],{"title":1632,"links":1633},"Topics",[1634,1638,1643,1648,1653,1658,1663,1668],{"text":1138,"config":1635},{"href":1636,"dataGaName":1637,"dataGaLocation":1551},"/topics/ci-cd/","cicd",{"text":1639,"config":1640},"GitOps",{"href":1641,"dataGaName":1642,"dataGaLocation":1551},"/topics/gitops/","gitops",{"text":1644,"config":1645},"DevOps",{"href":1646,"dataGaName":1647,"dataGaLocation":1551},"/topics/devops/","devops",{"text":1649,"config":1650},"Version Control",{"href":1651,"dataGaName":1652,"dataGaLocation":1551},"/topics/version-control/","version control",{"text":1654,"config":1655},"DevSecOps",{"href":1656,"dataGaName":1657,"dataGaLocation":1551},"/topics/devsecops/","devsecops",{"text":1659,"config":1660},"Cloud Native",{"href":1661,"dataGaName":1662,"dataGaLocation":1551},"/topics/cloud-native/","cloud native",{"text":1664,"config":1665},"AI for Coding",{"href":1666,"dataGaName":1667,"dataGaLocation":1551},"/topics/devops/ai-for-coding/","ai for coding",{"text":1669,"config":1670},"Agentic AI",{"href":1671,"dataGaName":1672,"dataGaLocation":1551},"/topics/agentic-ai/","agentic ai",{"title":1210,"links":1674},[1675,1678,1681,1684,1687,1690,1692,1694,1696,1698,1700,1703,1705,1708,1712,1716],{"text":1676,"config":1677},"DevOps modernization",{"href":1225,"dataGaName":1226,"dataGaLocation":1551},{"text":1679,"config":1680},"Security modernization",{"href":1230,"dataGaName":1231,"dataGaLocation":1551},{"text":1682,"config":1683},"AI modernization",{"href":1235,"dataGaName":1236,"dataGaLocation":1551},{"text":1685,"config":1686},"Financial services",{"href":1261,"dataGaName":1262,"dataGaLocation":1551},{"text":1688,"config":1689},"Public sector",{"href":1266,"dataGaName":1267,"dataGaLocation":1551},{"text":1269,"config":1691},{"href":1271,"dataGaName":1272,"dataGaLocation":1551},{"text":1274,"config":1693},{"href":1276,"dataGaName":1277,"dataGaLocation":1551},{"text":1279,"config":1695},{"href":1281,"dataGaName":1282,"dataGaLocation":1551},{"text":1284,"config":1697},{"href":1286,"dataGaName":1287,"dataGaLocation":1551},{"text":1241,"config":1699},{"href":1243,"dataGaName":1244,"dataGaLocation":1551},{"text":1701,"config":1702},"Small business",{"href":1248,"dataGaName":1249,"dataGaLocation":1551},{"text":1251,"config":1704},{"href":1253,"dataGaName":1254,"dataGaLocation":1551},{"text":1639,"config":1706},{"href":1707,"dataGaName":1642,"dataGaLocation":1551},"/solutions/gitops/",{"text":1709,"config":1710},"Software composition analysis",{"href":1178,"dataGaName":1711,"dataGaLocation":1551},"software composition analysis",{"text":1713,"config":1714},"Value stream management",{"href":1196,"dataGaName":1715,"dataGaLocation":1551},"value stream management",{"text":1717,"config":1718},"All solutions",{"href":1217,"dataGaName":1719,"dataGaLocation":1551},"all solutions",{"title":1303,"links":1721},[1722,1727,1732,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752,1754,1757],{"text":1723,"config":1724},"Install",{"href":1725,"dataGaName":1726,"dataGaLocation":1551},"/install/","install",{"text":1728,"config":1729},"Quick start guides",{"href":1730,"dataGaName":1731,"dataGaLocation":1551},"/get-started/","quick setup checklists",{"text":1316,"config":1733},{"href":1318,"dataGaName":1319,"dataGaLocation":1551},{"text":1321,"config":1735},{"href":1323,"dataGaName":1324,"dataGaLocation":1551},{"text":1326,"config":1737},{"href":1328,"dataGaName":1329,"dataGaLocation":1551},{"text":1331,"config":1739},{"href":1333,"dataGaName":1334,"dataGaLocation":1551},{"text":1336,"config":1741},{"href":1338,"dataGaName":1339,"dataGaLocation":1551},{"text":1344,"config":1743},{"href":1346,"dataGaName":1347,"dataGaLocation":1551},{"text":1349,"config":1745},{"href":1351,"dataGaName":1352,"dataGaLocation":1551},{"text":1354,"config":1747},{"href":1356,"dataGaName":1357,"dataGaLocation":1551},{"text":1359,"config":1749},{"href":1361,"dataGaName":1362,"dataGaLocation":1551},{"text":1364,"config":1751},{"href":1366,"dataGaName":1367,"dataGaLocation":1551},{"text":1427,"config":1753},{"href":1429,"dataGaName":1430,"dataGaLocation":1551},{"text":1755,"config":1756},"What's new",{"href":1375,"dataGaName":1376,"dataGaLocation":1551},{"text":1758,"config":1759},"All resources",{"href":1310,"dataGaName":1760,"dataGaLocation":1551},"all resources",{"title":1378,"links":1762},[1763,1765,1767,1769,1771,1773,1775,1777,1779,1783,1788],{"text":1386,"config":1764},{"href":1388,"dataGaName":1380,"dataGaLocation":1551},{"text":1391,"config":1766},{"href":1393,"dataGaName":1394,"dataGaLocation":1551},{"text":1397,"config":1768},{"href":1399,"dataGaName":1400,"dataGaLocation":1551},{"text":1402,"config":1770},{"href":1404,"dataGaName":1405,"dataGaLocation":1551},{"text":1407,"config":1772},{"href":1409,"dataGaName":1410,"dataGaLocation":1551},{"text":1412,"config":1774},{"href":1414,"dataGaName":1415,"dataGaLocation":1551},{"text":1417,"config":1776},{"href":1419,"dataGaName":1420,"dataGaLocation":1551},{"text":1422,"config":1778},{"href":1424,"dataGaName":1425,"dataGaLocation":1551},{"text":1780,"config":1781},"Sustainability",{"href":1782,"dataGaName":1780,"dataGaLocation":1551},"/sustainability/",{"text":1784,"config":1785},"Diversity, inclusion and belonging (DIB)",{"href":1786,"dataGaName":1787,"dataGaLocation":1551},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":1789,"config":1790},"Modern Slavery Transparency Statement",{"href":1791,"dataGaName":1792,"dataGaLocation":1551},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":1794},[1795,1798,1801],{"text":1796,"config":1797},"Terms",{"href":1603,"dataGaName":1604,"dataGaLocation":1551},{"text":1799,"config":1800},"Cookies",{"dataGaName":1613,"dataGaLocation":1551,"id":1614,"isOneTrustButton":560},{"text":1802,"config":1803},"Privacy",{"href":1608,"dataGaName":1609,"dataGaLocation":1551},[1805,1820],{"id":1806,"title":1807,"body":1088,"config":1808,"content":1810,"description":1088,"extension":1814,"meta":1815,"navigation":560,"path":1816,"seo":1817,"stem":1818,"__hash__":1819},"blogAuthors/en-us/blog/authors/enrique-alcntara.yml","Enrique Alcntara",{"template":1809},"BlogAuthor",{"name":7,"config":1811},{"headshot":1812,"ctfId":1813},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749669746/Blog/Author%20Headshots/ealcantara-headshot.jpg","3E3c30ZWRUTq6rlFiYrjtq","yml",{},"/en-us/blog/authors/enrique-alcntara",{},"en-us/blog/authors/enrique-alcntara","yzkAxnHZOC8tnAT8M2Uerpw4Gu5nQu2NxgUjUhbbnQU",{"id":1821,"title":1822,"body":1088,"config":1823,"content":1824,"description":1088,"extension":1814,"meta":1828,"navigation":560,"path":1829,"seo":1830,"stem":1831,"__hash__":1832},"blogAuthors/en-us/blog/authors/paul-gascou-vaillancourt.yml","Paul Gascou Vaillancourt",{"template":1809},{"name":8,"config":1825},{"headshot":1826,"ctfId":1827},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659488/Blog/Author%20Headshots/gitlab-logo-extra-whitespace.png","6Yg7HWPoy2E5vwudH0EZja",{},"/en-us/blog/authors/paul-gascou-vaillancourt",{},"en-us/blog/authors/paul-gascou-vaillancourt","6ElCPM5snHNtavdT1R6QSv4iN6QODsnI8TOvIy6EVlM",[1834,1841,1849],{"title":1835,"description":1836,"heroImage":1090,"category":1084,"date":1837,"authors":1838,"slug":1840,"externalUrl":1088},"CEO Shadow Takeaways from Jacie","Recap of my experience in the CEO Shadow Program.","2021-05-18",[1839],"Jacie Bandur","ceo-shadow-recap",{"title":1842,"description":1843,"heroImage":1844,"category":1084,"date":1845,"authors":1846,"slug":1848,"externalUrl":1088},"Why I love contributing to GitLab","Making small meaningful changes is what it's all about.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749679501/Blog/Hero%20Images/new-feature.png","2021-05-11",[1847],"Austin Regnery","why-i-love-contributing-to-gitlab",{"title":1850,"description":1851,"heroImage":1852,"category":1084,"date":1845,"authors":1853,"slug":1855,"externalUrl":1088},"Placebo Lines on the Pipeline Graph","Have you noticed the connecting lines missing on your pipelines lately? Here's why","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749679507/Blog/Hero%20Images/ci-cd.png",[1854],"Sam Beckham","placebo-lines-on-the-pipeline-graph",{"header":1857,"text":1858,"button":1862,"disclaimer":1865},"Start your free\n\n30-day GitLab trial\n",[1859,1860,1861],"GitLab Duo Agent Platform access","Try our most advanced features","Automate complex tasks with AI",{"text":1108,"config":1863},{"href":1864,"dataGaName":1111,"dataGaLocation":1347},"https://gitlab.com/-/trials/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/blog/","No credit card required.",{"header":1867,"blurb":1868,"button":1869,"secondaryButton":1874},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":1870,"config":1871},"Get your free trial",{"href":1872,"dataGaName":1111,"dataGaLocation":1873},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":1589,"config":1875},{"href":1441,"dataGaName":1116,"dataGaLocation":1873},1792002696770]