[{"data":1,"prerenderedAt":2789},["ShallowReactive",2],{"/blog/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss":3,"navigation-en-us":2005,"banner-en-us":2429,"footer-en-us":2439,"blog-post-authors-en-us-Julien Andrieux":2684,"blog-related-posts-en-us-go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss":2699,"blog-promotions-en-us":2725,"next-steps-en-us":2779},{"id":4,"title":5,"authors":6,"body":8,"category":1973,"date":1974,"description":1975,"extension":1976,"externalUrl":1977,"faq":1977,"featured":1978,"heroImage":1979,"meta":1980,"navigation":879,"path":1992,"seo":1993,"slug":1997,"stem":1998,"tags":1999,"template":2003,"updatedDate":1977,"__hash__":2004},"blogPosts/en-us/blog/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss.md","Go tools and GitLab: How to do continuous integration like a boss",[7],"Julien Andrieux",{"type":9,"value":10,"toc":1965},"minimark",[11,32,62,80,130,135,148,153,181,198,211,239,242,245,258,265,302,319,323,333,368,372,398,434,438,457,493,497,512,515,657,660,684,688,691,712,716,727,750,758,771,779,782,805,811,814,820,824,833,845,856,1286,1289,1361,1373,1386,1447,1465,1478,1549,1562,1565,1575,1587,1596,1610,1616,1628,1640,1643,1690,1693,1705,1711,1714,1720,1729,1735,1738,1745,1749,1758,1763,1777,1788,1791,1799,1805,1810,1816,1821,1827,1832,1838,1855,1870,1876,1887,1891,1913,1925,1930,1938,1948,1961],[12,13,14,15,21,22,26,27,31],"p",{},"At ",[16,17,20],"a",{"href":18,"rel":19},"https://pantomath.io/",[],"Pantomath",", we use ",[16,23,25],{"href":24},"/","GitLab"," for all our development work. The purpose of this paper is not to present GitLab and all ",[16,28,30],{"href":29},"/pricing/feature-comparison/","its features",", but to introduce how we use these tools to ease our lives. So what is it all about? To automate everything that is related to your development project, and let you focus on your code.",[12,33,34,35,40,41,40,46,40,51,40,56,61],{},"We’ll cover the ",[16,36,39],{"href":37,"rel":38},"https://en.wikipedia.org/wiki/Lint_(software)",[],"lint",", ",[16,42,45],{"href":43,"rel":44},"https://en.wikipedia.org/wiki/Unit_testing",[],"unit tests",[16,47,50],{"href":48,"rel":49},"https://en.wikipedia.org/wiki/Race_condition",[],"data race",[16,52,55],{"href":53,"rel":54},"https://clang.llvm.org/docs/MemorySanitizer.html",[],"memory sanitizer",[16,57,60],{"href":58,"rel":59},"https://en.wikipedia.org/wiki/Code_coverage",[],"code coverage",", and build.",[12,63,64,65,70,71,75,76,79],{},"All the source code shown in this post is available at ",[16,66,69],{"href":67,"rel":68},"https://gitlab.com/pantomath-io/demo-tools",[],"gitlab.com/pantomath-io/demo-tools",". So feel free to get the repository, and use the tags to navigate in it. The repository should be placed in the ",[72,73,74],"code",{},"src"," folder of your ",[72,77,78],{},"$GOPATH",":",[81,82,87],"pre",{"className":83,"code":84,"language":85,"meta":86,"style":86},"language-bash shiki shiki-themes github-light","$ go get -v -d gitlab.com/pantomath-io/demo-tools\n$ cd $GOPATH/src/gitlab.com/pantomath-io/demo-tools\n","bash","",[72,88,89,115],{"__ignoreMap":86},[90,91,94,98,102,105,109,112],"span",{"class":92,"line":93},"line",1,[90,95,97],{"class":96},"s7eDp","$",[90,99,101],{"class":100},"sYBdl"," go",[90,103,104],{"class":100}," get",[90,106,108],{"class":107},"sYu0t"," -v",[90,110,111],{"class":107}," -d",[90,113,114],{"class":100}," gitlab.com/pantomath-io/demo-tools\n",[90,116,118,120,123,127],{"class":92,"line":117},2,[90,119,97],{"class":96},[90,121,122],{"class":100}," cd",[90,124,126],{"class":125},"sgsFI"," $GOPATH",[90,128,129],{"class":100},"/src/gitlab.com/pantomath-io/demo-tools\n",[131,132,134],"h3",{"id":133},"go-tools","Go tools",[12,136,137,138,141,142,147],{},"Luckily, ",[72,139,140],{},"Go"," — the open source programming language also known as golang — comes with a ",[16,143,146],{"href":144,"rel":145},"https://golang.org/cmd/go/",[],"lot of useful tools",", to build, test, and check your code. In fact, it’s all there. We’ll just add extra tools to glue them together. But before we go there, we need to take them one by one, and see what they do.",[149,150,152],"h4",{"id":151},"package-list","Package list",[12,154,155,156,161,162,164,165,168,169,174,175,180],{},"Your Go project is a collection of packages, as described in the ",[16,157,160],{"href":158,"rel":159},"https://golang.org/doc/code.html",[],"official doc",". Most of the following tools will be fed with these packages, and thus the first command we need is a way to list the packages. Hopefully, the ",[72,163,140],{}," language covers our back with the ",[72,166,167],{},"list"," subcommand (",[16,170,173],{"href":171,"rel":172},"https://golang.org/cmd/go/#hdr-List_packages",[],"read the fine manual"," and this ",[16,176,179],{"href":177,"rel":178},"https://dave.cheney.net/2014/09/14/go-list-your-swiss-army-knife",[],"excellent post from Dave Cheney","):",[81,182,184],{"className":83,"code":183,"language":85,"meta":86,"style":86},"$ go list ./...\n",[72,185,186],{"__ignoreMap":86},[90,187,188,190,192,195],{"class":92,"line":93},[90,189,97],{"class":96},[90,191,101],{"class":100},[90,193,194],{"class":100}," list",[90,196,197],{"class":100}," ./...\n",[12,199,200,201,205,206,79],{},"Note that we want to avoid applying our tools on external packages or resources, and restrict it to ",[202,203,204],"strong",{},"our"," code. So we need to get rid of the ",[16,207,210],{"href":208,"rel":209},"https://golang.org/cmd/go/#hdr-Vendor_Directories",[],"vendor directories",[81,212,214],{"className":83,"code":213,"language":85,"meta":86,"style":86},"$ go list ./... | grep -v /vendor/\n",[72,215,216],{"__ignoreMap":86},[90,217,218,220,222,224,227,231,234,236],{"class":92,"line":93},[90,219,97],{"class":96},[90,221,101],{"class":100},[90,223,194],{"class":100},[90,225,226],{"class":100}," ./...",[90,228,230],{"class":229},"sD7c4"," |",[90,232,233],{"class":96}," grep",[90,235,108],{"class":107},[90,237,238],{"class":100}," /vendor/\n",[149,240,241],{"id":39},"Lint",[12,243,244],{},"This is the very first tool we use on the code: the linter. Its role is to make sure that the code respects the code style. This may sounds like an optional tool, or at least a “nice-to-have” but it really helps to keep consistent style over your project.",[12,246,247,248,252,253,257],{},"This linter is not part of Go ",[249,250,251],"em",{},"per se",", so you need to grab it and install it by hand (see ",[16,254,160],{"href":255,"rel":256},"https://github.com/golang/lint",[],").",[12,259,260,261,264],{},"The usage is fairly simple: you just run it on the packages of your code (you can also point the ",[72,262,263],{},".go"," files):",[81,266,268],{"className":83,"code":267,"language":85,"meta":86,"style":86},"$ golint -set_exit_status $(go list ./... | grep -v /vendor/)\n",[72,269,270],{"__ignoreMap":86},[90,271,272,274,277,280,283,286,288,290,292,294,296,299],{"class":92,"line":93},[90,273,97],{"class":96},[90,275,276],{"class":100}," golint",[90,278,279],{"class":107}," -set_exit_status",[90,281,282],{"class":125}," $(",[90,284,285],{"class":96},"go",[90,287,194],{"class":100},[90,289,226],{"class":100},[90,291,230],{"class":229},[90,293,233],{"class":96},[90,295,108],{"class":107},[90,297,298],{"class":100}," /vendor/",[90,300,301],{"class":125},")\n",[12,303,304,305,308,309,312,313,315,316,318],{},"Note the ",[72,306,307],{},"-set_exit_status"," option. By default, ",[72,310,311],{},"golint"," only prints the style issues, and returns (with a 0 return code), so the CI never considers something went wrong. If you specify the ",[72,314,307],{},", the return code from ",[72,317,311],{}," will be different from 0 if any style issue is encountered.",[149,320,322],{"id":321},"unit-test","Unit test",[12,324,325,326,328,329,332],{},"These are the most common tests you can run on your code. For each ",[72,327,263],{}," file, we need to have an associated ",[72,330,331],{},"_test.go"," file holding the unit tests. You can run the tests for all the packages with the following command:",[81,334,336],{"className":83,"code":335,"language":85,"meta":86,"style":86},"$ go test -short $(go list ./... | grep -v /vendor/)\n",[72,337,338],{"__ignoreMap":86},[90,339,340,342,344,347,350,352,354,356,358,360,362,364,366],{"class":92,"line":93},[90,341,97],{"class":96},[90,343,101],{"class":100},[90,345,346],{"class":100}," test",[90,348,349],{"class":107}," -short",[90,351,282],{"class":125},[90,353,285],{"class":96},[90,355,194],{"class":100},[90,357,226],{"class":100},[90,359,230],{"class":229},[90,361,233],{"class":96},[90,363,108],{"class":107},[90,365,298],{"class":100},[90,367,301],{"class":125},[149,369,371],{"id":370},"data-race","Data race",[12,373,374,375,377,378,40,381,40,384,387,388,391,392,397],{},"This is usually a hard subject to cover, but the ",[72,376,140],{}," tool has it by default (but only available on ",[72,379,380],{},"linux/amd64",[72,382,383],{},"freebsd/amd64",[72,385,386],{},"darwin/amd64"," and ",[72,389,390],{},"windows/amd64","). For more information about data race, see ",[16,393,396],{"href":394,"rel":395},"https://golang.org/doc/articles/race_detector.html",[],"this article",". Meanwhile, here is how to run it:",[81,399,401],{"className":83,"code":400,"language":85,"meta":86,"style":86},"$ go test -race -short $(go list ./... | grep -v /vendor/)\n",[72,402,403],{"__ignoreMap":86},[90,404,405,407,409,411,414,416,418,420,422,424,426,428,430,432],{"class":92,"line":93},[90,406,97],{"class":96},[90,408,101],{"class":100},[90,410,346],{"class":100},[90,412,413],{"class":107}," -race",[90,415,349],{"class":107},[90,417,282],{"class":125},[90,419,285],{"class":96},[90,421,194],{"class":100},[90,423,226],{"class":100},[90,425,230],{"class":229},[90,427,233],{"class":96},[90,429,108],{"class":107},[90,431,298],{"class":100},[90,433,301],{"class":125},[149,435,437],{"id":436},"memory-sanitizer","Memory sanitizer",[12,439,440,441,445,446,449,450,452,453,456],{},"Clang has a nice detector for uninitialized reads called ",[16,442,444],{"href":53,"rel":443},[],"MemorySanitizer",". The ",[72,447,448],{},"go test"," tool is kind enough to interact with this Clang module (as soon as you are on ",[72,451,380],{}," host and using a recent version of Clang/LLVM (",[72,454,455],{},">=3.8.0","). This command is how to run it:",[81,458,460],{"className":83,"code":459,"language":85,"meta":86,"style":86},"$ go test -msan -short $(go list ./... | grep -v /vendor/)\n",[72,461,462],{"__ignoreMap":86},[90,463,464,466,468,470,473,475,477,479,481,483,485,487,489,491],{"class":92,"line":93},[90,465,97],{"class":96},[90,467,101],{"class":100},[90,469,346],{"class":100},[90,471,472],{"class":107}," -msan",[90,474,349],{"class":107},[90,476,282],{"class":125},[90,478,285],{"class":96},[90,480,194],{"class":100},[90,482,226],{"class":100},[90,484,230],{"class":229},[90,486,233],{"class":96},[90,488,108],{"class":107},[90,490,298],{"class":100},[90,492,301],{"class":125},[149,494,496],{"id":495},"code-coverage","Code coverage",[12,498,499,500,505,506,511],{},"This is also a must have to evaluate the health of your code, and see what the part of code is under unit tests and what part is not. ",[16,501,504],{"href":502,"rel":503},"https://twitter.com/rob_pike",[],"Rob Pike"," wrote a ",[16,507,510],{"href":508,"rel":509},"https://blog.golang.org/cover",[],"full post on that very subject",".",[12,513,514],{},"To calculate the code coverage ratio, we need to run the following script:",[81,516,518],{"className":83,"code":517,"language":85,"meta":86,"style":86},"$ PKG_LIST=$(go list ./... | grep -v /vendor/)\n$ for package in ${PKG_LIST}; do\n    go test -covermode=count -coverprofile \"cover/${package##*/}.cov\" \"$package\" ;\ndone\n$ tail -q -n +2 cover/*.cov >> cover/coverage.cov\n$ go tool cover -func=cover/coverage.cov\n",[72,519,520,546,565,603,609,641],{"__ignoreMap":86},[90,521,522,524,527,530,532,534,536,538,540,542,544],{"class":92,"line":93},[90,523,97],{"class":96},[90,525,526],{"class":100}," PKG_LIST=",[90,528,529],{"class":125},"$(",[90,531,285],{"class":96},[90,533,194],{"class":100},[90,535,226],{"class":100},[90,537,230],{"class":229},[90,539,233],{"class":96},[90,541,108],{"class":107},[90,543,298],{"class":100},[90,545,301],{"class":125},[90,547,548,550,553,556,559,562],{"class":92,"line":117},[90,549,97],{"class":96},[90,551,552],{"class":100}," for",[90,554,555],{"class":100}," package",[90,557,558],{"class":100}," in",[90,560,561],{"class":125}," ${PKG_LIST}; ",[90,563,564],{"class":229},"do\n",[90,566,568,571,573,576,579,582,585,588,591,594,597,600],{"class":92,"line":567},3,[90,569,570],{"class":96},"    go",[90,572,346],{"class":100},[90,574,575],{"class":107}," -covermode=count",[90,577,578],{"class":107}," -coverprofile",[90,580,581],{"class":100}," \"cover/${",[90,583,584],{"class":125},"package",[90,586,587],{"class":229},"##*/",[90,589,590],{"class":100},"}.cov\"",[90,592,593],{"class":100}," \"",[90,595,596],{"class":125},"$package",[90,598,599],{"class":100},"\"",[90,601,602],{"class":125}," ;\n",[90,604,606],{"class":92,"line":605},4,[90,607,608],{"class":229},"done\n",[90,610,612,614,617,620,623,626,629,632,635,638],{"class":92,"line":611},5,[90,613,97],{"class":96},[90,615,616],{"class":100}," tail",[90,618,619],{"class":107}," -q",[90,621,622],{"class":107}," -n",[90,624,625],{"class":100}," +2",[90,627,628],{"class":100}," cover/",[90,630,631],{"class":107},"*",[90,633,634],{"class":100},".cov",[90,636,637],{"class":229}," >>",[90,639,640],{"class":100}," cover/coverage.cov\n",[90,642,644,646,648,651,654],{"class":92,"line":643},6,[90,645,97],{"class":96},[90,647,101],{"class":100},[90,649,650],{"class":100}," tool",[90,652,653],{"class":100}," cover",[90,655,656],{"class":107}," -func=cover/coverage.cov\n",[12,658,659],{},"If we want to get the coverage report in HTML format, we need to add the following command:",[81,661,663],{"className":83,"code":662,"language":85,"meta":86,"style":86},"$ go tool cover -html=cover/coverage.cov -o coverage.html\n",[72,664,665],{"__ignoreMap":86},[90,666,667,669,671,673,675,678,681],{"class":92,"line":93},[90,668,97],{"class":96},[90,670,101],{"class":100},[90,672,650],{"class":100},[90,674,653],{"class":100},[90,676,677],{"class":107}," -html=cover/coverage.cov",[90,679,680],{"class":107}," -o",[90,682,683],{"class":100}," coverage.html\n",[149,685,687],{"id":686},"build","Build",[12,689,690],{},"Last but not least, once the code has been fully tested, we might want to compile it to make sure we can build a working binary.",[81,692,694],{"className":83,"code":693,"language":85,"meta":86,"style":86},"$ go build -i -v gitlab.com/pantomath-io/demo-tools\n",[72,695,696],{"__ignoreMap":86},[90,697,698,700,702,705,708,710],{"class":92,"line":93},[90,699,97],{"class":96},[90,701,101],{"class":100},[90,703,704],{"class":100}," build",[90,706,707],{"class":107}," -i",[90,709,108],{"class":107},[90,711,114],{"class":100},[131,713,715],{"id":714},"makefile","Makefile",[12,717,718,721,722],{},[249,719,720],{},"git tag:"," ",[16,723,726],{"href":724,"rel":725},"https://gitlab.com/pantomath-io/demo-tools/tags/init-makefile",[],"init-makefile",[12,728,729,733],{},[730,731],"img",{"alt":86,"src":732},"https://cdn-images-1.medium.com/max/1600/1*Ip_q_6I-kNpUjuPMOutuTA.jpeg",[249,734,735],{},[736,737,738,739,744,745],"small",{},"Photo by ",[16,740,743],{"href":741,"rel":742},"https://unsplash.com/photos/qJE5Svhs2ek?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText",[],"Matt Artz"," on ",[16,746,749],{"href":747,"rel":748},"https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText",[],"Unsplash",[12,751,752,753,757],{},"Now we have all the tools that we may use in the context of continuous integration, we can wrap them all in a ",[16,754,715],{"href":755,"rel":756},"https://gitlab.com/pantomath-io/demo-tools/blob/init-makefile/Makefile",[],", and have a consistent way to call them.",[12,759,760,761,764,765,770],{},"The purpose of this doc is not to present ",[72,762,763],{},"make",", but you can refer to ",[16,766,769],{"href":767,"rel":768},"https://www.gnu.org/software/make/manual/make.html",[],"official documentation"," to learn more about it.",[81,772,777],{"className":773,"code":775,"language":776},[774],"language-text","PROJECT_NAME := \"demo-tools\"\nPKG := \"gitlab.com/pantomath-io/$(PROJECT_NAME)\"\nPKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/)\nGO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)\n\n.PHONY: all dep build clean test coverage coverhtml lint\n\nall: build\n\nlint: ## Lint the files\n  @golint -set_exit_status ${PKG_LIST}\n\ntest: ## Run unittests\n  @go test -short ${PKG_LIST}\n\nrace: dep ## Run data race detector\n  @go test -race -short ${PKG_LIST}\n\nmsan: dep ## Run memory sanitizer\n  @go test -msan -short ${PKG_LIST}\n\ncoverage: ## Generate global code coverage report\n  ./tools/coverage.sh;\n\ncoverhtml: ## Generate global code coverage report in HTML\n  ./tools/coverage.sh html;\n\ndep: ## Get the dependencies\n  @go get -v -d ./...\n\nbuild: dep ## Build the binary file\n  @go build -i -v $(PKG)\n\nclean: ## Remove previous build\n  @rm -f $(PROJECT_NAME)\n\nhelp: ## Display this help screen\n  @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n","text",[72,778,775],{"__ignoreMap":86},[12,780,781],{},"What do we have now? One target for any tool previously presented, and three more targets for:",[783,784,785,793,799],"ul",{},[786,787,788,789,792],"li",{},"installation of dependencies (",[72,790,791],{},"dep",");",[786,794,795,796,792],{},"housekeeping of the project (",[72,797,798],{},"clean",[786,800,801,802,257],{},"some nice and shiny help (",[72,803,804],{},"help",[12,806,807,808,810],{},"Note that we also had to create a script for the code coverage work. This is because implementing loops over files in a Makefile is a pain. So the work is done in a ",[72,809,85],{}," script, and the Makefile only triggers this script.",[12,812,813],{},"You can try the Makefile with the following commands:",[81,815,818],{"className":816,"code":817,"language":776},[774],"$ make help\n$ make lint\n$ make coverage\n",[72,819,817],{"__ignoreMap":86},[131,821,823],{"id":822},"continuous-integration","Continuous integration",[12,825,826,721,828],{},[249,827,720],{},[16,829,832],{"href":830,"rel":831},"https://gitlab.com/pantomath-io/demo-tools/tags/init-ci",[],"init-ci",[12,834,835,836,840,841,844],{},"Now the tools are in place, and we can run various tests on our code, we’d like to automate these, on your repository. Luckily, GitLab offers ",[16,837,839],{"href":838},"/solutions/continuous-integration/","CI pipelines"," just for this. And the setup for this is pretty straightforward: all you create is a ",[72,842,843],{},".gitlab-ci.yml"," file at the root of the repository.",[12,846,847,848,853,854,79],{},"The ",[16,849,852],{"href":850,"rel":851},"https://docs.gitlab.com/ci/yaml/",[],"full documentation"," on this Yaml file presents all the options, but you can start with this ",[72,855,843],{},[81,857,861],{"className":858,"code":859,"language":860,"meta":86,"style":86},"language-yaml shiki shiki-themes github-light","image: golang:1.9\n\ncache:\n  paths:\n    - /apt-cache\n    - /go/src/github.com\n    - /go/src/golang.org\n    - /go/src/google.golang.org\n    - /go/src/gopkg.in\n\nstages:\n  - test\n  - build\n\nbefore_script:\n  - mkdir -p /go/src/gitlab.com/pantomath-io /go/src/_/builds\n  - cp -r $CI_PROJECT_DIR /go/src/gitlab.com/pantomath-io/pantomath\n  - ln -s /go/src/gitlab.com/pantomath-io /go/src/_/builds/pantomath-io\n  - make dep\n\nunit_tests:\n  stage: test\n  script:\n    - make test\n\nrace_detector:\n  stage: test\n  script:\n    - make race\n\nmemory_sanitizer:\n  stage: test\n  script:\n    - make msan\n\ncode_coverage:\n  stage: test\n  script:\n    - make coverage\n\ncode_coverage_report:\n  stage: test\n  script:\n    - make coverhtml\n  only:\n  - master\n\nlint_code:\n  stage: test\n  script:\n    - make lint\n\nbuild:\n  stage: build\n  script:\n    - make\n\n","yaml",[72,862,863,875,881,889,896,904,911,919,927,935,940,948,957,965,970,978,986,994,1002,1010,1015,1023,1033,1041,1049,1054,1062,1071,1078,1086,1091,1099,1108,1115,1123,1128,1136,1145,1152,1160,1165,1173,1182,1189,1197,1205,1213,1218,1226,1235,1242,1250,1255,1262,1271,1278],{"__ignoreMap":86},[90,864,865,869,872],{"class":92,"line":93},[90,866,868],{"class":867},"shJU0","image",[90,870,871],{"class":125},": ",[90,873,874],{"class":100},"golang:1.9\n",[90,876,877],{"class":92,"line":117},[90,878,880],{"emptyLinePlaceholder":879},true,"\n",[90,882,883,886],{"class":92,"line":567},[90,884,885],{"class":867},"cache",[90,887,888],{"class":125},":\n",[90,890,891,894],{"class":92,"line":605},[90,892,893],{"class":867},"  paths",[90,895,888],{"class":125},[90,897,898,901],{"class":92,"line":611},[90,899,900],{"class":125},"    - ",[90,902,903],{"class":100},"/apt-cache\n",[90,905,906,908],{"class":92,"line":643},[90,907,900],{"class":125},[90,909,910],{"class":100},"/go/src/github.com\n",[90,912,914,916],{"class":92,"line":913},7,[90,915,900],{"class":125},[90,917,918],{"class":100},"/go/src/golang.org\n",[90,920,922,924],{"class":92,"line":921},8,[90,923,900],{"class":125},[90,925,926],{"class":100},"/go/src/google.golang.org\n",[90,928,930,932],{"class":92,"line":929},9,[90,931,900],{"class":125},[90,933,934],{"class":100},"/go/src/gopkg.in\n",[90,936,938],{"class":92,"line":937},10,[90,939,880],{"emptyLinePlaceholder":879},[90,941,943,946],{"class":92,"line":942},11,[90,944,945],{"class":867},"stages",[90,947,888],{"class":125},[90,949,951,954],{"class":92,"line":950},12,[90,952,953],{"class":125},"  - ",[90,955,956],{"class":100},"test\n",[90,958,960,962],{"class":92,"line":959},13,[90,961,953],{"class":125},[90,963,964],{"class":100},"build\n",[90,966,968],{"class":92,"line":967},14,[90,969,880],{"emptyLinePlaceholder":879},[90,971,973,976],{"class":92,"line":972},15,[90,974,975],{"class":867},"before_script",[90,977,888],{"class":125},[90,979,981,983],{"class":92,"line":980},16,[90,982,953],{"class":125},[90,984,985],{"class":100},"mkdir -p /go/src/gitlab.com/pantomath-io /go/src/_/builds\n",[90,987,989,991],{"class":92,"line":988},17,[90,990,953],{"class":125},[90,992,993],{"class":100},"cp -r $CI_PROJECT_DIR /go/src/gitlab.com/pantomath-io/pantomath\n",[90,995,997,999],{"class":92,"line":996},18,[90,998,953],{"class":125},[90,1000,1001],{"class":100},"ln -s /go/src/gitlab.com/pantomath-io /go/src/_/builds/pantomath-io\n",[90,1003,1005,1007],{"class":92,"line":1004},19,[90,1006,953],{"class":125},[90,1008,1009],{"class":100},"make dep\n",[90,1011,1013],{"class":92,"line":1012},20,[90,1014,880],{"emptyLinePlaceholder":879},[90,1016,1018,1021],{"class":92,"line":1017},21,[90,1019,1020],{"class":867},"unit_tests",[90,1022,888],{"class":125},[90,1024,1026,1029,1031],{"class":92,"line":1025},22,[90,1027,1028],{"class":867},"  stage",[90,1030,871],{"class":125},[90,1032,956],{"class":100},[90,1034,1036,1039],{"class":92,"line":1035},23,[90,1037,1038],{"class":867},"  script",[90,1040,888],{"class":125},[90,1042,1044,1046],{"class":92,"line":1043},24,[90,1045,900],{"class":125},[90,1047,1048],{"class":100},"make test\n",[90,1050,1052],{"class":92,"line":1051},25,[90,1053,880],{"emptyLinePlaceholder":879},[90,1055,1057,1060],{"class":92,"line":1056},26,[90,1058,1059],{"class":867},"race_detector",[90,1061,888],{"class":125},[90,1063,1065,1067,1069],{"class":92,"line":1064},27,[90,1066,1028],{"class":867},[90,1068,871],{"class":125},[90,1070,956],{"class":100},[90,1072,1074,1076],{"class":92,"line":1073},28,[90,1075,1038],{"class":867},[90,1077,888],{"class":125},[90,1079,1081,1083],{"class":92,"line":1080},29,[90,1082,900],{"class":125},[90,1084,1085],{"class":100},"make race\n",[90,1087,1089],{"class":92,"line":1088},30,[90,1090,880],{"emptyLinePlaceholder":879},[90,1092,1094,1097],{"class":92,"line":1093},31,[90,1095,1096],{"class":867},"memory_sanitizer",[90,1098,888],{"class":125},[90,1100,1102,1104,1106],{"class":92,"line":1101},32,[90,1103,1028],{"class":867},[90,1105,871],{"class":125},[90,1107,956],{"class":100},[90,1109,1111,1113],{"class":92,"line":1110},33,[90,1112,1038],{"class":867},[90,1114,888],{"class":125},[90,1116,1118,1120],{"class":92,"line":1117},34,[90,1119,900],{"class":125},[90,1121,1122],{"class":100},"make msan\n",[90,1124,1126],{"class":92,"line":1125},35,[90,1127,880],{"emptyLinePlaceholder":879},[90,1129,1131,1134],{"class":92,"line":1130},36,[90,1132,1133],{"class":867},"code_coverage",[90,1135,888],{"class":125},[90,1137,1139,1141,1143],{"class":92,"line":1138},37,[90,1140,1028],{"class":867},[90,1142,871],{"class":125},[90,1144,956],{"class":100},[90,1146,1148,1150],{"class":92,"line":1147},38,[90,1149,1038],{"class":867},[90,1151,888],{"class":125},[90,1153,1155,1157],{"class":92,"line":1154},39,[90,1156,900],{"class":125},[90,1158,1159],{"class":100},"make coverage\n",[90,1161,1163],{"class":92,"line":1162},40,[90,1164,880],{"emptyLinePlaceholder":879},[90,1166,1168,1171],{"class":92,"line":1167},41,[90,1169,1170],{"class":867},"code_coverage_report",[90,1172,888],{"class":125},[90,1174,1176,1178,1180],{"class":92,"line":1175},42,[90,1177,1028],{"class":867},[90,1179,871],{"class":125},[90,1181,956],{"class":100},[90,1183,1185,1187],{"class":92,"line":1184},43,[90,1186,1038],{"class":867},[90,1188,888],{"class":125},[90,1190,1192,1194],{"class":92,"line":1191},44,[90,1193,900],{"class":125},[90,1195,1196],{"class":100},"make coverhtml\n",[90,1198,1200,1203],{"class":92,"line":1199},45,[90,1201,1202],{"class":867},"  only",[90,1204,888],{"class":125},[90,1206,1208,1210],{"class":92,"line":1207},46,[90,1209,953],{"class":125},[90,1211,1212],{"class":100},"master\n",[90,1214,1216],{"class":92,"line":1215},47,[90,1217,880],{"emptyLinePlaceholder":879},[90,1219,1221,1224],{"class":92,"line":1220},48,[90,1222,1223],{"class":867},"lint_code",[90,1225,888],{"class":125},[90,1227,1229,1231,1233],{"class":92,"line":1228},49,[90,1230,1028],{"class":867},[90,1232,871],{"class":125},[90,1234,956],{"class":100},[90,1236,1238,1240],{"class":92,"line":1237},50,[90,1239,1038],{"class":867},[90,1241,888],{"class":125},[90,1243,1245,1247],{"class":92,"line":1244},51,[90,1246,900],{"class":125},[90,1248,1249],{"class":100},"make lint\n",[90,1251,1253],{"class":92,"line":1252},52,[90,1254,880],{"emptyLinePlaceholder":879},[90,1256,1258,1260],{"class":92,"line":1257},53,[90,1259,686],{"class":867},[90,1261,888],{"class":125},[90,1263,1265,1267,1269],{"class":92,"line":1264},54,[90,1266,1028],{"class":867},[90,1268,871],{"class":125},[90,1270,964],{"class":100},[90,1272,1274,1276],{"class":92,"line":1273},55,[90,1275,1038],{"class":867},[90,1277,888],{"class":125},[90,1279,1281,1283],{"class":92,"line":1280},56,[90,1282,900],{"class":125},[90,1284,1285],{"class":100},"make\n",[12,1287,1288],{},"If you break down the file, here are some explanations on its content:",[783,1290,1291,1300,1309,1329,1342],{},[786,1292,1293,1294,1299],{},"The first thing is to choose what Docker image will be used to run the CI. Head to the ",[16,1295,1298],{"href":1296,"rel":1297},"https://hub.docker.com/",[],"Docker Hub"," to choose the right image for your project.",[786,1301,1302,1303,1308],{},"Then, you specify some folders of this image ",[16,1304,1307],{"href":1305,"rel":1306},"https://docs.gitlab.com/ci/yaml/#cache",[],"to be cached",". The goal here is to avoid downloading the same content several times. Once a job is completed, the listed paths will be archived, and next job will use the same archive.",[786,1310,1311,1312,1314,1315,1319,1320,387,1323,1325,1326,511],{},"You define the different ",[72,1313,945],{}," that will group your jobs. In our case, we have two ",[16,1316,945],{"href":1317,"rel":1318},"https://docs.gitlab.com/ci/yaml/#stages",[]," (to be processed in that order): ",[72,1321,1322],{},"test",[72,1324,686],{},". We could have other stages, such as ",[72,1327,1328],{},"deploy",[786,1330,847,1331,721,1333,1338,1339,1341],{},[72,1332,975],{},[16,1334,1337],{"href":1335,"rel":1336},"https://docs.gitlab.com/ci/yaml/#before_script",[],"section"," defines the commands to run in the Docker container right before the job is actually done. In our context, the commands just copy or link the repository deployed in the ",[72,1340,78],{},", and install dependencies.",[786,1343,1344,1345,1350,1351,1353,1354,1356,1357,1360],{},"Then come the actual ",[16,1346,1349],{"href":1347,"rel":1348},"https://docs.gitlab.com/ci/jobs/",[],"jobs",", using the ",[72,1352,715],{}," targets. Note the special case for ",[72,1355,1170],{}," where execution is restricted to the ",[72,1358,1359],{},"master"," branch (we don’t want to update the code coverage report from feature branches for instance).",[12,1362,1363,1364,1366,1367,1372],{},"As we commit/push the ",[72,1365,843],{}," file in the repository, the CI is ",[16,1368,1371],{"href":1369,"rel":1370},"https://gitlab.com/pantomath-io/demo-tools/pipelines/13481935",[],"automatically triggered",". And the pipeline fails. Howcome?",[12,1374,847,1375,721,1377,1382,1383,1385],{},[72,1376,1223],{},[16,1378,1381],{"href":1379,"rel":1380},"https://gitlab.com/pantomath-io/demo-tools/-/jobs/38690212",[],"job"," fails because it can’t find the ",[72,1384,311],{}," binary:",[81,1387,1389],{"className":83,"code":1388,"language":85,"meta":86,"style":86},"$ make lint\nmake: golint: Command not found\nMakefile:11: recipe for target 'lint' failed\nmake: *** [lint] Error 127\n",[72,1390,1391,1401,1418,1437],{"__ignoreMap":86},[90,1392,1393,1395,1398],{"class":92,"line":93},[90,1394,97],{"class":96},[90,1396,1397],{"class":100}," make",[90,1399,1400],{"class":100}," lint\n",[90,1402,1403,1406,1409,1412,1415],{"class":92,"line":117},[90,1404,1405],{"class":96},"make:",[90,1407,1408],{"class":100}," golint:",[90,1410,1411],{"class":100}," Command",[90,1413,1414],{"class":100}," not",[90,1416,1417],{"class":100}," found\n",[90,1419,1420,1423,1426,1428,1431,1434],{"class":92,"line":567},[90,1421,1422],{"class":96},"Makefile:11:",[90,1424,1425],{"class":100}," recipe",[90,1427,552],{"class":100},[90,1429,1430],{"class":100}," target",[90,1432,1433],{"class":100}," 'lint'",[90,1435,1436],{"class":100}," failed\n",[90,1438,1439,1441,1444],{"class":92,"line":605},[90,1440,1405],{"class":96},[90,1442,1443],{"class":107}," ***",[90,1445,1446],{"class":125}," [lint] Error 127\n",[12,1448,1449,1450,1455,1456,1458,1459,1461,1462,1464],{},"So, ",[16,1451,1454],{"href":1452,"rel":1453},"https://gitlab.com/pantomath-io/demo-toolscommit/17a0206eb626504e559f56773e2d81c7b5808dbe",[],"update"," your ",[72,1457,715],{}," to install ",[72,1460,311],{}," as part of the ",[72,1463,791],{}," target.",[12,1466,847,1467,721,1469,1473,1474,1477],{},[72,1468,1096],{},[16,1470,1381],{"href":1471,"rel":1472},"https://gitlab.com/pantomath-io/demo-tools/-/jobs/38690209",[]," fails because ",[72,1475,1476],{},"gcc"," complains:",[81,1479,1481],{"className":83,"code":1480,"language":85,"meta":86,"style":86},"$ make msan\n# runtime/cgo\ngcc: error: unrecognized argument to -fsanitize= option: 'memory'\nMakefile:20: recipe for target 'msan' failed\nmake: *** [msan] Error 2\n",[72,1482,1483,1492,1498,1524,1540],{"__ignoreMap":86},[90,1484,1485,1487,1489],{"class":92,"line":93},[90,1486,97],{"class":96},[90,1488,1397],{"class":100},[90,1490,1491],{"class":100}," msan\n",[90,1493,1494],{"class":92,"line":117},[90,1495,1497],{"class":1496},"sAwPA","# runtime/cgo\n",[90,1499,1500,1503,1506,1509,1512,1515,1518,1521],{"class":92,"line":567},[90,1501,1502],{"class":96},"gcc:",[90,1504,1505],{"class":100}," error:",[90,1507,1508],{"class":100}," unrecognized",[90,1510,1511],{"class":100}," argument",[90,1513,1514],{"class":100}," to",[90,1516,1517],{"class":107}," -fsanitize=",[90,1519,1520],{"class":100}," option:",[90,1522,1523],{"class":100}," 'memory'\n",[90,1525,1526,1529,1531,1533,1535,1538],{"class":92,"line":605},[90,1527,1528],{"class":96},"Makefile:20:",[90,1530,1425],{"class":100},[90,1532,552],{"class":100},[90,1534,1430],{"class":100},[90,1536,1537],{"class":100}," 'msan'",[90,1539,1436],{"class":100},[90,1541,1542,1544,1546],{"class":92,"line":611},[90,1543,1405],{"class":96},[90,1545,1443],{"class":107},[90,1547,1548],{"class":125}," [msan] Error 2\n",[12,1550,1551,1552,1554,1555,1558,1559,1561],{},"But remember we need to use Clang/LLVM ",[72,1553,455],{}," to enjoy the ",[72,1556,1557],{},"-msan"," option in ",[72,1560,448],{}," command.",[12,1563,1564],{},"We have two options here:",[783,1566,1567,1572],{},[786,1568,1569,1570,792],{},"either we set up Clang in the job (using ",[72,1571,975],{},[786,1573,1574],{},"or we use a Docker image with Clang installed by default.",[12,1576,1577,1578,1581,1582,511],{},"The first option is nice, but that implies to have this setup done ",[202,1579,1580],{},"for every single job",". This is going to be so long, we should do it once and for all. So we prefer the second option, which is a good way to play with ",[16,1583,1586],{"href":1584,"rel":1585},"https://docs.gitlab.com/user/packages/container_registry/",[],"GitLab Registry",[12,1588,1589,721,1591],{},[249,1590,720],{},[16,1592,1595],{"href":1593,"rel":1594},"https://gitlab.com/pantomath-io/demo-tools/tags/use-own-docker",[],"use-own-docker",[12,1597,1598,1599,1604,1605,1609],{},"We need to create a ",[16,1600,1603],{"href":1601,"rel":1602},"https://gitlab.com/pantomath-io/demo-tools/blob/use-own-docker/Dockerfile",[],"Dockerfile"," for the container (as usual: read the ",[16,1606,769],{"href":1607,"rel":1608},"https://docs.docker.com/engine/reference/builder",[]," for more options about it):",[81,1611,1614],{"className":1612,"code":1613,"language":776},[774],"# Base image:\nFROM golang:1.9\nMAINTAINER Julien Andrieux \u003Cjulien@pantomath.io>\n\n# Install golint\nENV GOPATH /go\nENV PATH ${GOPATH}/bin:$PATH\nRUN go get -u github.com/golang/lint/golint\n\n# Add apt key for LLVM repository\nRUN wget -O -\n | apt-key add -\n\n# Add LLVM apt repository\nRUN echo \"deb\n llvm-toolchain-stretch-5.0 main\" | tee -a /etc/apt/sources.list\n\n# Install clang from LLVM repository\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    clang-5.0 \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\n\n# Set Clang as default CC\nENV set_clang /etc/profile.d/set-clang-cc.sh\nRUN echo \"export CC=clang-5.0\" | tee -a ${set_clang} && chmod a+x ${set_clang}\n",[72,1615,1613],{"__ignoreMap":86},[12,1617,1618,1619,1624,1625,1627],{},"The container built out of this Dockerfile will be based on ",[16,1620,1623],{"href":1621,"rel":1622},"https://hub.docker.com/_/golang/",[],"golang:1.9"," image (the one referenced in the ",[72,1626,843],{}," file).",[12,1629,1630,1631,1633,1634,1639],{},"While we’re at it, we install ",[72,1632,311],{}," in the container, so we have it available. Then we follow ",[16,1635,1638],{"href":1636,"rel":1637},"http://apt.llvm.org/",[],"official way"," of installing Clang 5.0 from LLVM repository.",[12,1641,1642],{},"Now we have the Dockerfile in place, we need to build the container image and make it available for GitLab:",[81,1644,1646],{"className":83,"code":1645,"language":85,"meta":86,"style":86},"$ docker login registry.gitlab.com\n$ docker build -t registry.gitlab.com/pantomath-io/demo-tools .\n$ docker push registry.gitlab.com/pantomath-io/demo-tools\n",[72,1647,1648,1661,1678],{"__ignoreMap":86},[90,1649,1650,1652,1655,1658],{"class":92,"line":93},[90,1651,97],{"class":96},[90,1653,1654],{"class":100}," docker",[90,1656,1657],{"class":100}," login",[90,1659,1660],{"class":100}," registry.gitlab.com\n",[90,1662,1663,1665,1667,1669,1672,1675],{"class":92,"line":117},[90,1664,97],{"class":96},[90,1666,1654],{"class":100},[90,1668,704],{"class":100},[90,1670,1671],{"class":107}," -t",[90,1673,1674],{"class":100}," registry.gitlab.com/pantomath-io/demo-tools",[90,1676,1677],{"class":100}," .\n",[90,1679,1680,1682,1684,1687],{"class":92,"line":567},[90,1681,97],{"class":96},[90,1683,1654],{"class":100},[90,1685,1686],{"class":100}," push",[90,1688,1689],{"class":100}," registry.gitlab.com/pantomath-io/demo-tools\n",[12,1691,1692],{},"The first command connects you to the GitLab Registry. Then you build the container image described in the Dockerfile. And finally, you push it to the GitLab Registry.",[12,1694,1695,1696,1701,1702,1704],{},"Take a look at the ",[16,1697,1700],{"href":1698,"rel":1699},"https://gitlab.com/pantomath-io/demo-tools/container_registry",[],"Registry for your repository",", you’ll see your image, ready to be used. And to have the CI using your image, you just need to update the ",[72,1703,843],{}," file:",[81,1706,1709],{"className":1707,"code":1708,"language":776},[774],"image: golang:1.9\n",[72,1710,1708],{"__ignoreMap":86},[12,1712,1713],{},"becomes",[81,1715,1718],{"className":1716,"code":1717,"language":776},[774],"image: registry.gitlab.com/pantomath-io/demo-tools:latest\n",[72,1719,1717],{"__ignoreMap":86},[12,1721,1722,1723,1726,1727,1704],{},"One last detail: you need to tell the CI to use the proper compiler (i.e. the ",[72,1724,1725],{},"CC"," environment variable), so we add the variable initialization in the ",[72,1728,843],{},[81,1730,1733],{"className":1731,"code":1732,"language":776},[774],"export CC=clang-5.0\n",[72,1734,1732],{"__ignoreMap":86},[12,1736,1737],{},"Once the modification are done, next commit will trigger the pipeline, which now works:",[12,1739,1740],{},[16,1741,1744],{"href":1742,"rel":1743},"https://gitlab.com/pantomath-io/demo-tools/pipelines/13497136",[],"gitlab.com/pantomath-io/demo-tools/pipelines/13497136",[131,1746,1748],{"id":1747},"badges","Badges",[12,1750,1751,721,1753],{},[249,1752,720],{},[16,1754,1757],{"href":1755,"rel":1756},"https://gitlab.com/pantomath-io/demo-tools/tags/init-badges",[],"init-badges",[12,1759,1760],{},[730,1761],{"alt":86,"src":1762},"https://cdn-images-1.medium.com/max/1600/1*0pY_6oCiHZ_eLh0vfg5rDA.jpeg",[12,1764,1765],{},[249,1766,1767],{},[736,1768,738,1769,744,1774],{},[16,1770,1773],{"href":1771,"rel":1772},"https://unsplash.com/photos/ZBadHaTUkP0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText",[],"Jakob Owens",[16,1775,749],{"href":747,"rel":1776},[],[12,1778,1779,1780,721,1783,511],{},"Now the tools are in place, every commit will launch a test suite, and you probably want to show it, and that’s legitimate :) The best way to do so is to use badges, and the best place for it is the ",[72,1781,1782],{},"README",[16,1784,1787],{"href":1785,"rel":1786},"https://gitlab.com/pantomath-io/demo-tools/blob/init-badges/README.md",[],"file",[12,1789,1790],{},"Edit it and add the four following badges:",[783,1792,1793],{},[786,1794,1795,1796,1798],{},"Build Status: the status of the last pipeline on the ",[72,1797,1359],{}," branch:",[81,1800,1803],{"className":1801,"code":1802,"language":776,"meta":86},[774],"[![Build Status](https://gitlab.com/pantomath-io/demo-tools/badges/master/build.svg)](https://gitlab.com/pantomath-io/demo-tools/commits/master)\n",[72,1804,1802],{"__ignoreMap":86},[783,1806,1807],{},[786,1808,1809],{},"Coverage Report: the percentage of source code covered by tests",[81,1811,1814],{"className":1812,"code":1813,"language":776,"meta":86},[774],"[![Coverage Report](https://gitlab.com/pantomath-io/demo-tools/badges/master/coverage.svg)](https://gitlab.com/pantomath-io/demo-tools/commits/master)\n",[72,1815,1813],{"__ignoreMap":86},[783,1817,1818],{},[786,1819,1820],{},"Go Report Card:",[81,1822,1825],{"className":1823,"code":1824,"language":776,"meta":86},[774],"[![Go Report Card](https://goreportcard.com/badge/gitlab.com/pantomath-io/demo-tools)](https://goreportcard.com/report/gitlab.com/pantomath-io/demo-tools)\n",[72,1826,1824],{"__ignoreMap":86},[783,1828,1829],{},[786,1830,1831],{},"License:",[81,1833,1836],{"className":1834,"code":1835,"language":776,"meta":86},[774],"[![License MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://img.shields.io/badge/License-MIT-brightgreen.svg)\n",[72,1837,1835],{"__ignoreMap":86},[12,1839,1840,1841,1844,1845,1848,1849,1854],{},"The coverage report needs a special configuration. You need to tell GitLab how to get that information, considering that there is a job in the CI that ",[249,1842,1843],{},"displays"," it when it runs.",[1846,1847],"br",{}," There is a ",[16,1850,1853],{"href":1851,"rel":1852},"https://gitlab.com/help/user/project/pipelines/settings#test-coverage-parsing",[],"configuration"," to provide GitLab with a regexp, used in any job’ output. If the regexp matches, GitLab consider the match to be the code coverage result.",[12,1856,1857,1858,1861,1862,1865,1866,1869],{},"So head to ",[72,1859,1860],{},"Settings > CI/CD"," in your repository, scroll down to the ",[72,1863,1864],{},"Test coverage parsing"," setting in the ",[72,1867,1868],{},"General pipelines settings"," section, and use the following regexp:",[81,1871,1874],{"className":1872,"code":1873,"language":776},[774],"total:\\s+\\(statements\\)\\s+(\\d+.\\d+\\%)\n",[72,1875,1873],{"__ignoreMap":86},[12,1877,1878,1879,1884,1885,79],{},"You’re all set! Head to the ",[16,1880,1883],{"href":1881,"rel":1882},"https://gitlab.com/pantomath-io/demo-tools/tree/init-badges",[],"overview of your repository",", and look at your ",[72,1886,1782],{},[131,1888,1890],{"id":1889},"conclusion","Conclusion",[12,1892,1893,1894,1899,1900,1905,1906,1908,1909,1912],{},"What’s next? Probably more tests in your CI. You can also look at the CD (",[16,1895,1898],{"href":1896,"rel":1897},"https://docs.gitlab.com/ci/environments/",[],"Continuous Deployment",") to automate the deployment of your builds. The documentation can be done using ",[16,1901,1904],{"href":1902,"rel":1903},"https://godoc.org/-/about",[],"GoDoc",". Note that you generate a coverage report with the ",[72,1907,1170],{},", but don’t use it in the CI. You can make the job copy the HTML file to a web server, using ",[72,1910,1911],{},"scp"," (see this documentation on how to use SSH keys).",[12,1914,1915,1916,1921,1922,511],{},"Many thanks to ",[16,1917,1920],{"href":1918,"rel":1919},"https://dev.to/loderunner",[],"Charles Francoise"," who co-wrote this paper and ",[16,1923,69],{"href":67,"rel":1924},[],[1926,1927,1929],"h2",{"id":1928},"about-the-guest-author","About the Guest Author",[12,1931,1932,1933,511],{},"Julien Andrieux is currently working on Pantomath. Pantomath is a modern, open source monitoring solution, built for performance, that bridges the gaps across all levels of your company. The wellbeing of your infrastructure is everyone’s business. ",[16,1934,1937],{"href":1935,"rel":1936},"http://goo.gl/tcxtXq",[],"Keep up with the project",[12,1939,1940],{},[249,1941,1942,1947],{},[16,1943,1946],{"href":1944,"rel":1945},"https://medium.com/pantomath/go-tools-gitlab-how-to-do-continuous-integration-like-a-boss-941a3a9ad0b6",[],"Go tools & GitLab - how to do Continuous Integration like a boss"," was originally published on Medium.",[12,1949,1950],{},[249,1951,1952,1953,744,1958],{},"Cover photo by ",[16,1954,1957],{"href":1955,"rel":1956},"https://unsplash.com/photos/IClZBVw5W5A?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText",[],"Todd Quackenbush",[16,1959,749],{"href":747,"rel":1960},[],[1962,1963,1964],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html .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 .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":86,"searchDepth":117,"depth":117,"links":1966},[1967,1968,1969,1970,1971,1972],{"id":133,"depth":567,"text":134},{"id":714,"depth":567,"text":715},{"id":822,"depth":567,"text":823},{"id":1747,"depth":567,"text":1748},{"id":1889,"depth":567,"text":1890},{"id":1928,"depth":117,"text":1929},"engineering","2017-11-27","How the team at Pantomath makes their lives easier with GitLab CI.","md",null,false,"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749667978/Blog/Hero%20Images/go-tools-and-gitlab.jpg",{"excerpt":1981},{"type":9,"value":1982},[1983],[12,1984,14,1985,21,1988,26,1990,31],{},[16,1986,20],{"href":18,"rel":1987},[],[16,1989,25],{"href":24},[16,1991,30],{"href":29},"/en-us/blog/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss",{"title":5,"description":1975,"ogTitle":5,"ogDescription":1975,"noIndex":1978,"ogImage":1979,"ogUrl":1994,"ogSiteName":1995,"ogType":1996,"canonicalUrls":1994},"https://about.gitlab.com/blog/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss","https://about.gitlab.com","article","go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss","en-us/blog/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss",[2000,2001,2002],"CI/CD","user stories","tutorial","BlogPost","DNpMW12sgp82A71rh74U2LxQfEv37JIMZ-PhP8BZElw",{"logo":2006,"freeTrial":2010,"sales":2015,"login":2020,"items":2025,"search":2349,"minimal":2380,"duo":2399,"switchNav":2408,"pricingDeployment":2419},{"config":2007},{"href":24,"dataGaName":2008,"dataGaLocation":2009},"gitlab logo","header",{"text":2011,"config":2012},"Get free trial",{"href":2013,"dataGaName":2014,"dataGaLocation":2009},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":2016,"config":2017},"Request a demo",{"href":2018,"dataGaName":2019,"dataGaLocation":2009},"/sales/?contact-topic=request-demo","sales",{"text":2021,"config":2022},"Sign in",{"href":2023,"dataGaName":2024,"dataGaLocation":2009},"https://gitlab.com/users/sign_in/","sign in",[2026,2055,2153,2158,2272,2327],{"text":2027,"config":2028,"menu":2030},"Platform",{"dataNavLevelOne":2029},"platform",{"type":2031,"columns":2032},"cards",[2033,2039,2047],{"title":2027,"description":2034,"link":2035},"The intelligent orchestration platform for DevSecOps",{"text":2036,"config":2037},"Explore our Platform",{"href":2038,"dataGaName":2029,"dataGaLocation":2009},"/platform/",{"title":2040,"description":2041,"link":2042},"GitLab Duo Agent Platform","Agentic AI for the entire software lifecycle",{"text":2043,"config":2044},"Meet GitLab Duo",{"href":2045,"dataGaName":2046,"dataGaLocation":2009},"/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":2048,"description":2049,"link":2050},"Why GitLab","See the top reasons enterprises choose GitLab",{"text":2051,"config":2052},"Learn more",{"href":2053,"dataGaName":2054,"dataGaLocation":2009},"/why-gitlab/","why gitlab",{"text":2056,"left":879,"config":2057,"menu":2059},"Product",{"dataNavLevelOne":2058},"solutions",{"type":2060,"link":2061,"columns":2065,"feature":2132},"lists",{"text":2062,"config":2063},"View all Solutions",{"href":2064,"dataGaName":2058,"dataGaLocation":2009},"/solutions/",[2066,2088,2111],{"title":2067,"description":2068,"link":2069,"items":2074},"Automation","CI/CD and automation to accelerate deployment",{"config":2070},{"icon":2071,"href":2072,"dataGaName":2073,"dataGaLocation":2009},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[2075,2077,2080,2084],{"text":2000,"config":2076},{"href":838,"dataGaLocation":2009,"dataGaName":2000},{"text":2040,"config":2078},{"href":2045,"dataGaLocation":2009,"dataGaName":2079},"gitlab duo agent platform - product menu",{"text":2081,"config":2082},"Source Code Management",{"href":2083,"dataGaLocation":2009,"dataGaName":2081},"/solutions/source-code-management/",{"text":2085,"config":2086},"Automated Software Delivery",{"href":2072,"dataGaLocation":2009,"dataGaName":2087},"Automated software delivery",{"title":2089,"description":2090,"link":2091,"items":2096},"Security","Deliver code faster without compromising security",{"config":2092},{"href":2093,"dataGaName":2094,"dataGaLocation":2009,"icon":2095},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[2097,2101,2106],{"text":2098,"config":2099},"Application Security Testing",{"href":2093,"dataGaName":2100,"dataGaLocation":2009},"Application security testing",{"text":2102,"config":2103},"Software Supply Chain Security",{"href":2104,"dataGaLocation":2009,"dataGaName":2105},"/solutions/supply-chain/","Software supply chain security",{"text":2107,"config":2108},"Software Compliance",{"href":2109,"dataGaName":2110,"dataGaLocation":2009},"/solutions/software-compliance/","software compliance",{"title":2112,"link":2113,"items":2118},"Measurement",{"config":2114},{"icon":2115,"href":2116,"dataGaName":2117,"dataGaLocation":2009},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[2119,2123,2127],{"text":2120,"config":2121},"Visibility & Measurement",{"href":2116,"dataGaLocation":2009,"dataGaName":2122},"Visibility and Measurement",{"text":2124,"config":2125},"Value Stream Management",{"href":2126,"dataGaLocation":2009,"dataGaName":2124},"/solutions/value-stream-management/",{"text":2128,"config":2129},"Analytics & Insights",{"href":2130,"dataGaLocation":2009,"dataGaName":2131},"/solutions/analytics-and-insights/","Analytics and insights",{"title":2133,"type":2060,"items":2134},"GitLab for",[2135,2141,2147],{"text":2136,"config":2137},"Enterprise",{"icon":2138,"href":2139,"dataGaLocation":2009,"dataGaName":2140},"Building","/enterprise/","enterprise",{"text":2142,"config":2143},"Small Business",{"icon":2144,"href":2145,"dataGaLocation":2009,"dataGaName":2146},"Work","/small-business/","small business",{"text":2148,"config":2149},"Public Sector",{"icon":2150,"href":2151,"dataGaLocation":2009,"dataGaName":2152},"Organization","/solutions/public-sector/","public sector",{"text":2154,"config":2155},"Pricing",{"href":2156,"dataGaName":2157,"dataGaLocation":2009,"dataNavLevelOne":2157},"/pricing/","pricing",{"text":2159,"config":2160,"menu":2162},"Resources",{"dataNavLevelOne":2161},"resources",{"type":2060,"link":2163,"columns":2167,"feature":2261},{"text":2164,"config":2165},"View all resources",{"href":2166,"dataGaName":2161,"dataGaLocation":2009},"/resources/",[2168,2201,2228],{"title":2169,"items":2170},"Getting started",[2171,2176,2181,2186,2191,2196],{"text":2172,"config":2173},"Install",{"href":2174,"dataGaName":2175,"dataGaLocation":2009},"/install/","install",{"text":2177,"config":2178},"Quick start guides",{"href":2179,"dataGaName":2180,"dataGaLocation":2009},"/get-started/","quick setup checklists",{"text":2182,"config":2183},"Learn",{"href":2184,"dataGaLocation":2009,"dataGaName":2185},"https://university.gitlab.com/","learn",{"text":2187,"config":2188},"Product documentation",{"href":2189,"dataGaName":2190,"dataGaLocation":2009},"https://docs.gitlab.com/","product documentation",{"text":2192,"config":2193},"Best practice videos",{"href":2194,"dataGaName":2195,"dataGaLocation":2009},"/getting-started-videos/","best practice videos",{"text":2197,"config":2198},"Integrations",{"href":2199,"dataGaName":2200,"dataGaLocation":2009},"/integrations/","integrations",{"title":2202,"items":2203},"Discover",[2204,2209,2214,2219,2223],{"text":2205,"config":2206},"Customer success stories",{"href":2207,"dataGaName":2208,"dataGaLocation":2009},"/customers/","customer success stories",{"text":2210,"config":2211},"Blog",{"href":2212,"dataGaName":2213,"dataGaLocation":2009},"/blog/","blog",{"text":2215,"config":2216},"Demo Hub",{"href":2217,"dataGaName":2218,"dataGaLocation":2009},"/demo-hub/","demo hub",{"text":2220,"config":2221},"The Source",{"href":2222,"dataGaName":2213,"dataGaLocation":2009},"/the-source/",{"text":2224,"config":2225},"Remote",{"href":2226,"dataGaName":2227,"dataGaLocation":2009},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":2229,"items":2230},"Connect",[2231,2236,2241,2246,2251,2256],{"text":2232,"config":2233},"GitLab Services",{"href":2234,"dataGaName":2235,"dataGaLocation":2009},"/services/","services",{"text":2237,"config":2238},"Contribute",{"href":2239,"dataGaName":2240,"dataGaLocation":2009},"https://contributors.gitlab.com","contribute",{"text":2242,"config":2243},"Community",{"href":2244,"dataGaName":2245,"dataGaLocation":2009},"/community/","community",{"text":2247,"config":2248},"Forum",{"href":2249,"dataGaName":2250,"dataGaLocation":2009},"https://forum.gitlab.com/","forum",{"text":2252,"config":2253},"Events",{"href":2254,"dataGaName":2255,"dataGaLocation":2009},"/events/","events",{"text":2257,"config":2258},"Partners",{"href":2259,"dataGaName":2260,"dataGaLocation":2009},"/partners/","partners",{"config":2262,"title":2265,"text":2266,"link":2267},{"background":2263,"textColor":2264},"url('https://res.cloudinary.com/about-gitlab-com/image/upload/v1777322348/qpq8yrgn8knii57omj0c.png')","#000","What’s new in GitLab","Stay updated with our latest features and improvements.",{"text":2268,"config":2269},"Read the latest",{"href":2270,"dataGaName":2271,"dataGaLocation":2009},"/whats-new/","whats new",{"text":2273,"config":2274,"menu":2276},"Company",{"dataNavLevelOne":2275},"company",{"type":2060,"columns":2277},[2278],{"items":2279},[2280,2285,2290,2292,2297,2302,2307,2312,2317,2322],{"text":2281,"config":2282},"About",{"href":2283,"dataGaName":2284,"dataGaLocation":2009},"/company/","about",{"text":2286,"config":2287,"footerGa":2289},"Jobs",{"href":2288,"dataGaName":1349,"dataGaLocation":2009},"/jobs/",{"dataGaName":1349},{"text":2252,"config":2291},{"href":2254,"dataGaName":2255,"dataGaLocation":2009},{"text":2293,"config":2294},"Leadership",{"href":2295,"dataGaName":2296,"dataGaLocation":2009},"/company/team/e-group/","leadership",{"text":2298,"config":2299},"Handbook",{"href":2300,"dataGaName":2301,"dataGaLocation":2009},"https://handbook.gitlab.com/","handbook",{"text":2303,"config":2304},"Investor relations",{"href":2305,"dataGaName":2306,"dataGaLocation":2009},"https://ir.gitlab.com/overview/default.aspx","investor relations",{"text":2308,"config":2309},"Trust Center",{"href":2310,"dataGaName":2311,"dataGaLocation":2009},"/security/","trust center",{"text":2313,"config":2314},"AI Transparency Center",{"href":2315,"dataGaName":2316,"dataGaLocation":2009},"/ai-transparency-center/","ai transparency center",{"text":2318,"config":2319},"Newsletter",{"href":2320,"dataGaName":2321,"dataGaLocation":2009},"/company/contact/#contact-forms","newsletter",{"text":2323,"config":2324},"Press",{"href":2325,"dataGaName":2326,"dataGaLocation":2009},"/press/","press",{"text":2328,"config":2329,"menu":2330},"Contact us",{"dataNavLevelOne":2275},{"type":2060,"columns":2331},[2332],{"items":2333},[2334,2339,2344],{"text":2335,"config":2336},"Talk to sales",{"href":2337,"dataGaName":2338,"dataGaLocation":2009},"/sales/","talk to sales",{"text":2340,"config":2341},"Support portal",{"href":2342,"dataGaName":2343,"dataGaLocation":2009},"https://support.gitlab.com/hc/en-us","support portal",{"text":2345,"config":2346},"Customer portal",{"href":2347,"dataGaName":2348,"dataGaLocation":2009},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":2350,"login":2351,"suggestions":2358},"Close",{"text":2352,"link":2353},"To search repositories and projects, login to",{"text":2354,"config":2355},"gitlab.com",{"href":2023,"dataGaName":2356,"dataGaLocation":2357},"search login","search",{"text":2359,"default":2360},"Suggestions",[2361,2363,2367,2369,2373,2377],{"text":2040,"config":2362},{"href":2045,"dataGaName":2040,"dataGaLocation":2357},{"text":2364,"config":2365},"Code Suggestions (AI)",{"href":2366,"dataGaName":2364,"dataGaLocation":2357},"/solutions/code-suggestions/",{"text":2000,"config":2368},{"href":838,"dataGaName":2000,"dataGaLocation":2357},{"text":2370,"config":2371},"GitLab on AWS",{"href":2372,"dataGaName":2370,"dataGaLocation":2357},"/partners/technology-partners/aws/",{"text":2374,"config":2375},"GitLab on Google Cloud",{"href":2376,"dataGaName":2374,"dataGaLocation":2357},"/partners/technology-partners/google-cloud-platform/",{"text":2378,"config":2379},"Why GitLab?",{"href":2053,"dataGaName":2378,"dataGaLocation":2357},{"freeTrial":2381,"mobileIcon":2386,"desktopIcon":2391,"secondaryButton":2394},{"text":2382,"config":2383},"Start free trial",{"href":2384,"dataGaName":2014,"dataGaLocation":2385},"https://gitlab.com/-/trials/new/","nav",{"altText":2387,"config":2388},"Gitlab Icon",{"src":2389,"dataGaName":2390,"dataGaLocation":2385},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":2387,"config":2392},{"src":2393,"dataGaName":2390,"dataGaLocation":2385},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":2395,"config":2396},"Get Started",{"href":2397,"dataGaName":2398,"dataGaLocation":2385},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":2400,"mobileIcon":2404,"desktopIcon":2406},{"text":2401,"config":2402},"Learn more about GitLab Duo",{"href":2045,"dataGaName":2403,"dataGaLocation":2385},"gitlab duo",{"altText":2387,"config":2405},{"src":2389,"dataGaName":2390,"dataGaLocation":2385},{"altText":2387,"config":2407},{"src":2393,"dataGaName":2390,"dataGaLocation":2385},{"button":2409,"mobileIcon":2414,"desktopIcon":2416},{"text":2410,"config":2411},"/switch",{"href":2412,"dataGaName":2413,"dataGaLocation":2385},"#contact","switch",{"altText":2387,"config":2415},{"src":2389,"dataGaName":2390,"dataGaLocation":2385},{"altText":2387,"config":2417},{"src":2418,"dataGaName":2390,"dataGaLocation":2385},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":2420,"mobileIcon":2425,"desktopIcon":2427},{"text":2421,"config":2422},"Back to pricing",{"href":2156,"dataGaName":2423,"dataGaLocation":2385,"icon":2424},"back to pricing","GoBack",{"altText":2387,"config":2426},{"src":2389,"dataGaName":2390,"dataGaLocation":2385},{"altText":2387,"config":2428},{"src":2393,"dataGaName":2390,"dataGaLocation":2385},{"title":2430,"titleMobile":2431,"button":2432,"config":2437},"Duo Agent Platform delivers 400% ROI, per new Forrester Consulting study.","400% ROI: Forrester TEI for GitLab Duo",{"text":2051,"config":2433},{"href":2434,"dataGaName":2435,"dataGaLocation":2436},"https://about.gitlab.com/blog/gitlab-duo-agent-platform-delivers-400-percent-roi/","forrester-tei-dap-banner","global-banner",{"layout":2438,"disabled":1978},"release",{"data":2440},{"text":2441,"source":2442,"edit":2448,"contribute":2453,"config":2458,"items":2463,"minimal":2673},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":2443,"config":2444},"View page source",{"href":2445,"dataGaName":2446,"dataGaLocation":2447},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":2449,"config":2450},"Edit this page",{"href":2451,"dataGaName":2452,"dataGaLocation":2447},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":2454,"config":2455},"Please contribute",{"href":2456,"dataGaName":2457,"dataGaLocation":2447},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":2459,"facebook":2460,"youtube":2461,"linkedin":2462},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[2464,2511,2565,2609,2641],{"title":2154,"links":2465,"subMenu":2480},[2466,2470,2475],{"text":2467,"config":2468},"View plans",{"href":2156,"dataGaName":2469,"dataGaLocation":2447},"view plans",{"text":2471,"config":2472},"Why Premium?",{"href":2473,"dataGaName":2474,"dataGaLocation":2447},"/pricing/premium/","why premium",{"text":2476,"config":2477},"Why Ultimate?",{"href":2478,"dataGaName":2479,"dataGaLocation":2447},"/pricing/ultimate/","why ultimate",[2481],{"title":2482,"links":2483},"Contact Us",[2484,2487,2489,2491,2496,2501,2506],{"text":2485,"config":2486},"Contact sales",{"href":2337,"dataGaName":2019,"dataGaLocation":2447},{"text":2340,"config":2488},{"href":2342,"dataGaName":2343,"dataGaLocation":2447},{"text":2345,"config":2490},{"href":2347,"dataGaName":2348,"dataGaLocation":2447},{"text":2492,"config":2493},"Status",{"href":2494,"dataGaName":2495,"dataGaLocation":2447},"https://status.gitlab.com/","status",{"text":2497,"config":2498},"Terms of use",{"href":2499,"dataGaName":2500,"dataGaLocation":2447},"/terms/","terms of use",{"text":2502,"config":2503},"Privacy statement",{"href":2504,"dataGaName":2505,"dataGaLocation":2447},"/privacy/","privacy statement",{"text":2507,"config":2508},"Cookie preferences",{"dataGaName":2509,"dataGaLocation":2447,"id":2510,"isOneTrustButton":879},"cookie preferences","ot-sdk-btn",{"title":2056,"links":2512,"subMenu":2521},[2513,2517],{"text":2514,"config":2515},"DevSecOps platform",{"href":2038,"dataGaName":2516,"dataGaLocation":2447},"devsecops platform",{"text":2518,"config":2519},"AI-Assisted Development",{"href":2045,"dataGaName":2520,"dataGaLocation":2447},"ai-assisted development",[2522],{"title":2523,"links":2524},"Topics",[2525,2530,2535,2540,2545,2550,2555,2560],{"text":2526,"config":2527},"CICD",{"href":2528,"dataGaName":2529,"dataGaLocation":2447},"/topics/ci-cd/","cicd",{"text":2531,"config":2532},"GitOps",{"href":2533,"dataGaName":2534,"dataGaLocation":2447},"/topics/gitops/","gitops",{"text":2536,"config":2537},"DevOps",{"href":2538,"dataGaName":2539,"dataGaLocation":2447},"/topics/devops/","devops",{"text":2541,"config":2542},"Version Control",{"href":2543,"dataGaName":2544,"dataGaLocation":2447},"/topics/version-control/","version control",{"text":2546,"config":2547},"DevSecOps",{"href":2548,"dataGaName":2549,"dataGaLocation":2447},"/topics/devsecops/","devsecops",{"text":2551,"config":2552},"Cloud Native",{"href":2553,"dataGaName":2554,"dataGaLocation":2447},"/topics/cloud-native/","cloud native",{"text":2556,"config":2557},"AI for Coding",{"href":2558,"dataGaName":2559,"dataGaLocation":2447},"/topics/devops/ai-for-coding/","ai for coding",{"text":2561,"config":2562},"Agentic AI",{"href":2563,"dataGaName":2564,"dataGaLocation":2447},"/topics/agentic-ai/","agentic ai",{"title":2566,"links":2567},"Solutions",[2568,2570,2572,2577,2581,2584,2588,2591,2593,2596,2599,2604],{"text":2098,"config":2569},{"href":2093,"dataGaName":2098,"dataGaLocation":2447},{"text":2087,"config":2571},{"href":2072,"dataGaName":2073,"dataGaLocation":2447},{"text":2573,"config":2574},"Agile development",{"href":2575,"dataGaName":2576,"dataGaLocation":2447},"/solutions/agile-delivery/","agile delivery",{"text":2578,"config":2579},"SCM",{"href":2083,"dataGaName":2580,"dataGaLocation":2447},"source code management",{"text":2526,"config":2582},{"href":838,"dataGaName":2583,"dataGaLocation":2447},"continuous integration & delivery",{"text":2585,"config":2586},"Value stream management",{"href":2126,"dataGaName":2587,"dataGaLocation":2447},"value stream management",{"text":2531,"config":2589},{"href":2590,"dataGaName":2534,"dataGaLocation":2447},"/solutions/gitops/",{"text":2136,"config":2592},{"href":2139,"dataGaName":2140,"dataGaLocation":2447},{"text":2594,"config":2595},"Small business",{"href":2145,"dataGaName":2146,"dataGaLocation":2447},{"text":2597,"config":2598},"Public sector",{"href":2151,"dataGaName":2152,"dataGaLocation":2447},{"text":2600,"config":2601},"Education",{"href":2602,"dataGaName":2603,"dataGaLocation":2447},"/solutions/education/","education",{"text":2605,"config":2606},"Financial services",{"href":2607,"dataGaName":2608,"dataGaLocation":2447},"/solutions/finance/","financial services",{"title":2159,"links":2610},[2611,2613,2615,2617,2620,2622,2625,2627,2629,2631,2633,2635,2637,2639],{"text":2172,"config":2612},{"href":2174,"dataGaName":2175,"dataGaLocation":2447},{"text":2177,"config":2614},{"href":2179,"dataGaName":2180,"dataGaLocation":2447},{"text":2182,"config":2616},{"href":2184,"dataGaName":2185,"dataGaLocation":2447},{"text":2187,"config":2618},{"href":2189,"dataGaName":2619,"dataGaLocation":2447},"docs",{"text":2210,"config":2621},{"href":2212,"dataGaName":2213,"dataGaLocation":2447},{"text":2623,"config":2624},"What's new",{"href":2270,"dataGaName":2271,"dataGaLocation":2447},{"text":2205,"config":2626},{"href":2207,"dataGaName":2208,"dataGaLocation":2447},{"text":2224,"config":2628},{"href":2226,"dataGaName":2227,"dataGaLocation":2447},{"text":2232,"config":2630},{"href":2234,"dataGaName":2235,"dataGaLocation":2447},{"text":2237,"config":2632},{"href":2239,"dataGaName":2240,"dataGaLocation":2447},{"text":2242,"config":2634},{"href":2244,"dataGaName":2245,"dataGaLocation":2447},{"text":2247,"config":2636},{"href":2249,"dataGaName":2250,"dataGaLocation":2447},{"text":2252,"config":2638},{"href":2254,"dataGaName":2255,"dataGaLocation":2447},{"text":2257,"config":2640},{"href":2259,"dataGaName":2260,"dataGaLocation":2447},{"title":2273,"links":2642},[2643,2645,2647,2649,2651,2653,2657,2662,2664,2666,2668],{"text":2281,"config":2644},{"href":2283,"dataGaName":2275,"dataGaLocation":2447},{"text":2286,"config":2646},{"href":2288,"dataGaName":1349,"dataGaLocation":2447},{"text":2293,"config":2648},{"href":2295,"dataGaName":2296,"dataGaLocation":2447},{"text":2298,"config":2650},{"href":2300,"dataGaName":2301,"dataGaLocation":2447},{"text":2303,"config":2652},{"href":2305,"dataGaName":2306,"dataGaLocation":2447},{"text":2654,"config":2655},"Sustainability",{"href":2656,"dataGaName":2654,"dataGaLocation":2447},"/sustainability/",{"text":2658,"config":2659},"Diversity, inclusion and belonging (DIB)",{"href":2660,"dataGaName":2661,"dataGaLocation":2447},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":2308,"config":2663},{"href":2310,"dataGaName":2311,"dataGaLocation":2447},{"text":2318,"config":2665},{"href":2320,"dataGaName":2321,"dataGaLocation":2447},{"text":2323,"config":2667},{"href":2325,"dataGaName":2326,"dataGaLocation":2447},{"text":2669,"config":2670},"Modern Slavery Transparency Statement",{"href":2671,"dataGaName":2672,"dataGaLocation":2447},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":2674},[2675,2678,2681],{"text":2676,"config":2677},"Terms",{"href":2499,"dataGaName":2500,"dataGaLocation":2447},{"text":2679,"config":2680},"Cookies",{"dataGaName":2509,"dataGaLocation":2447,"id":2510,"isOneTrustButton":879},{"text":2682,"config":2683},"Privacy",{"href":2504,"dataGaName":2505,"dataGaLocation":2447},[2685],{"id":2686,"title":7,"body":1977,"config":2687,"content":2689,"description":1977,"extension":2693,"meta":2694,"navigation":879,"path":2695,"seo":2696,"stem":2697,"__hash__":2698},"blogAuthors/en-us/blog/authors/julien-andrieux.yml",{"template":2688},"BlogAuthor",{"name":7,"config":2690},{"headshot":2691,"ctfId":2692},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659488/Blog/Author%20Headshots/gitlab-logo-extra-whitespace.png","Julien-Andrieux","yml",{},"/en-us/blog/authors/julien-andrieux",{},"en-us/blog/authors/julien-andrieux","j9ErAlshG5c4T_rCh_0Ima3UIl_H0fl_WpXY3eRZOAY",[2700,2709,2717],{"title":2701,"description":2702,"heroImage":2703,"category":1973,"date":2704,"authors":2705,"slug":2708,"externalUrl":1977},"Confidential AI for GitLab Self-Hosted","Give developers AI coding agents in GitLab Duo without source code leaving a hardware-encrypted boundary — no GPUs needed.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1773866173/vte9qh8rriznvyclhkes.png","2026-08-06",[2706,2707],"Mathias Ewald","Martin Paloncy, Edgeless Systems","confidential-ai-for-gitlab-self-hosted",{"title":2710,"description":2711,"heroImage":2712,"category":1973,"date":2713,"authors":2714,"slug":2716,"externalUrl":1977},"Green DevOps: Why carbon measurement belongs in your CI/CD pipeline","CI/CD pipelines have a hidden carbon cost. Here's why measuring it matters, and how you can get started with Eco CI and Carmen in GitLab.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1765809212/noh0mdfn9o94ry9ykura.png","2026-07-09",[2715],"Lysanne Pinto","green-devops-carbon-measurement-cicd-pipeline",{"title":2718,"description":2719,"heroImage":2720,"category":1973,"date":2721,"authors":2722,"slug":2724,"externalUrl":1977},"How to build CI/CD observability at scale","This practical guide to GitLab pipeline analytics helps self-managed users gain operational insights using Prometheus and Grafana.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1774465167/n5hlvrsrheadeccyr1oz.png","2026-04-28",[2723],"Paul Meresanu","how-to-build-ci-cd-observability-at-scale",{"promotions":2726},[2727,2741,2753,2765],{"id":2728,"categories":2729,"header":2731,"text":2732,"button":2733,"image":2738},"ai-modernization",[2730],"ai","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":2734,"config":2735},"Get your AI maturity score",{"href":2736,"dataGaName":2737,"dataGaLocation":2213},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":2739},{"src":2740},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":2742,"categories":2743,"header":2745,"text":2732,"button":2746,"image":2750},"devops-modernization",[2744,2549],"product","Are you just managing tools or shipping innovation?",{"text":2747,"config":2748},"Get your DevOps maturity score",{"href":2749,"dataGaName":2737,"dataGaLocation":2213},"/assessments/devops-modernization-assessment/",{"config":2751},{"src":2752},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":2754,"categories":2755,"header":2757,"text":2732,"button":2758,"image":2762},"security-modernization",[2756],"security","Are you trading speed for security?",{"text":2759,"config":2760},"Get your security maturity score",{"href":2761,"dataGaName":2737,"dataGaLocation":2213},"/assessments/security-modernization-assessment/",{"config":2763},{"src":2764},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":2766,"paths":2767,"header":2770,"text":2771,"button":2772,"image":2777},"github-azure-migration",[2768,2769],"migration-from-azure-devops-to-gitlab","integrating-azure-devops-scm-and-gitlab","Is your team ready for GitHub's Azure move?","GitHub is already rebuilding around Azure. Find out what it means for you.",{"text":2773,"config":2774},"See how GitLab compares to GitHub",{"href":2775,"dataGaName":2776,"dataGaLocation":2213},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":2778},{"src":2752},{"header":2780,"blurb":2781,"button":2782,"secondaryButton":2787},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":2783,"config":2784},"Get your free trial",{"href":2785,"dataGaName":2014,"dataGaLocation":2786},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":2485,"config":2788},{"href":2337,"dataGaName":2019,"dataGaLocation":2786},1786803770817]