[{"data":1,"prerenderedAt":1583},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Fci-cd":342,"\u002Fdeployment\u002Fci-cd-surround":1580},[4,14,36,69,140,325],{"title":5,"path":6,"stem":7,"children":8},"Introduction","\u002Fgetting-started","1.getting-started\u002F1.index",[9,10],{"title":5,"path":6,"stem":7},{"title":11,"path":12,"stem":13},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation",{"title":15,"path":16,"stem":17,"children":18,"page":35},"Guides","\u002Fguides","2.guides",[19,23,27,31],{"title":20,"path":21,"stem":22},"Your First Layout","\u002Fguides\u002Fyour-first-layout","2.guides\u002F1.your-first-layout",{"title":24,"path":25,"stem":26},"Your First Page Template","\u002Fguides\u002Fyour-first-page-template","2.guides\u002F2.your-first-page-template",{"title":28,"path":29,"stem":30},"Your First Component","\u002Fguides\u002Fyour-first-component","2.guides\u002F3.your-first-component",{"title":32,"path":33,"stem":34},"Alternative UI Variants","\u002Fguides\u002Falternative-ui-variants","2.guides\u002F4.alternative-ui-variants",false,{"title":37,"path":38,"stem":39,"children":40,"page":35},"Core Concepts","\u002Fcore-concepts","3.core-concepts",[41,45,49,53,57,61,65],{"title":42,"path":43,"stem":44},"How It All Works","\u002Fcore-concepts\u002Farchitecture","3.core-concepts\u002F1.architecture",{"title":46,"path":47,"stem":48},"The Data Model","\u002Fcore-concepts\u002Fthe-data-model","3.core-concepts\u002F2.the-data-model",{"title":50,"path":51,"stem":52},"Layouts & Pages","\u002Fcore-concepts\u002Flayouts-and-pages","3.core-concepts\u002F3.layouts-and-pages",{"title":54,"path":55,"stem":56},"Dynamic Pages","\u002Fcore-concepts\u002Fdynamic-pages","3.core-concepts\u002F4.dynamic-pages",{"title":58,"path":59,"stem":60},"Components","\u002Fcore-concepts\u002Fcomponents","3.core-concepts\u002F5.components",{"title":62,"path":63,"stem":64},"Draft & Publish Workflow","\u002Fcore-concepts\u002Fdraft-and-publish","3.core-concepts\u002F6.draft-and-publish",{"title":66,"path":67,"stem":68},"The Admin Panel","\u002Fcore-concepts\u002Fadmin-panel","3.core-concepts\u002F7.admin-panel",{"title":70,"path":71,"stem":72,"children":73,"page":35},"Api","\u002Fapi","4.api",[74,78,116,120,124,128,132,136],{"title":75,"path":76,"stem":77},"Bundle Setup","\u002Fapi\u002Fbundle-setup","4.api\u002F1.bundle-setup",{"title":58,"path":79,"stem":80,"children":81,"page":35},"\u002Fapi\u002Fcomponents","4.api\u002F2.components",[82,86,103],{"title":83,"path":84,"stem":85},"Creating Components","\u002Fapi\u002Fcomponents\u002Fcreating-components","4.api\u002F2.components\u002F1.creating-components",{"title":87,"path":88,"stem":89,"children":90,"page":35},"Annotations","\u002Fapi\u002Fcomponents\u002Fannotations","4.api\u002F2.components\u002F2.annotations",[91,95,99],{"title":92,"path":93,"stem":94},"Publishable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fpublishable","4.api\u002F2.components\u002F2.annotations\u002F1.publishable",{"title":96,"path":97,"stem":98},"Uploadable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fuploadable","4.api\u002F2.components\u002F2.annotations\u002F2.uploadable",{"title":100,"path":101,"stem":102},"Timestamped","\u002Fapi\u002Fcomponents\u002Fannotations\u002Ftimestamped","4.api\u002F2.components\u002F2.annotations\u002F3.timestamped",{"title":104,"path":105,"stem":106,"children":107,"page":35},"Built Ins","\u002Fapi\u002Fcomponents\u002Fbuilt-ins","4.api\u002F2.components\u002F3.built-ins",[108,112],{"title":109,"path":110,"stem":111},"Collection Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fcollection-component","4.api\u002F2.components\u002F3.built-ins\u002F1.collection-component",{"title":113,"path":114,"stem":115},"Form Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fform-component","4.api\u002F2.components\u002F3.built-ins\u002F2.form-component",{"title":117,"path":118,"stem":119},"Dynamic & Nested Pages","\u002Fapi\u002Fdynamic-pages","4.api\u002F3.dynamic-pages",{"title":121,"path":122,"stem":123},"Users & Security","\u002Fapi\u002Fusers-and-security","4.api\u002F4.users-and-security",{"title":125,"path":126,"stem":127},"Data Fixtures","\u002Fapi\u002Fdata-fixtures","4.api\u002F5.data-fixtures",{"title":129,"path":130,"stem":131},"Configuration Reference","\u002Fapi\u002Fconfiguration","4.api\u002F6.configuration",{"title":133,"path":134,"stem":135},"Console Commands","\u002Fapi\u002Fconsole-commands","4.api\u002F7.console-commands",{"title":137,"path":138,"stem":139},"Debugging & Profiler","\u002Fapi\u002Fdebugging","4.api\u002F8.debugging",{"title":141,"path":142,"stem":143,"children":144,"page":35},"Nuxt Module","\u002Fnuxt-module","5.nuxt-module",[145,149,162,178,203,207,279,304,308],{"title":146,"path":147,"stem":148},"Module Setup","\u002Fnuxt-module\u002Fmodule-setup","5.nuxt-module\u002F1.module-setup",{"title":150,"path":151,"stem":152,"children":153,"page":35},"Configuration","\u002Fnuxt-module\u002Fconfiguration","5.nuxt-module\u002F2.configuration",[154,158],{"title":155,"path":156,"stem":157},"Nuxt Config","\u002Fnuxt-module\u002Fconfiguration\u002Fnuxt-config","5.nuxt-module\u002F2.configuration\u002F1.nuxt-config",{"title":159,"path":160,"stem":161},"Site Config & SEO","\u002Fnuxt-module\u002Fconfiguration\u002Fsite-config-and-seo","5.nuxt-module\u002F2.configuration\u002F2.site-config-and-seo",{"title":163,"path":164,"stem":165,"children":166,"page":35},"Building Your Ui","\u002Fnuxt-module\u002Fbuilding-your-ui","5.nuxt-module\u002F3.building-your-ui",[167,171,175],{"title":168,"path":169,"stem":170},"Layouts","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-layouts","5.nuxt-module\u002F3.building-your-ui\u002F1.creating-layouts",{"title":172,"path":173,"stem":174},"Page Templates","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-page-templates","5.nuxt-module\u002F3.building-your-ui\u002F2.creating-page-templates",{"title":83,"path":176,"stem":177},"\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-components","5.nuxt-module\u002F3.building-your-ui\u002F3.creating-components",{"title":179,"path":180,"stem":181,"children":182,"page":35},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F4.cwa-components",[183,187,191,195,199],{"title":184,"path":185,"stem":186},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F4.cwa-components\u002F1.cwa-component-group",{"title":188,"path":189,"stem":190},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F4.cwa-components\u002F2.cwa-page",{"title":192,"path":193,"stem":194},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F4.cwa-components\u002F3.cwa-link",{"title":196,"path":197,"stem":198},"\u003CCwaImage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-image","5.nuxt-module\u002F4.cwa-components\u002F4.cwa-image",{"title":200,"path":201,"stem":202},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F4.cwa-components\u002F5.cwa-default-layout",{"title":204,"path":205,"stem":206},"The useCwa() API","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F5.cwa-api",{"title":208,"path":209,"stem":210,"children":211,"page":35},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F6.composables",[212,245,262],{"title":213,"path":214,"stem":215,"children":216,"page":35},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F6.composables\u002F1.component",[217,221,225,229,233,237,241],{"title":218,"path":219,"stem":220},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F1.use-cwa-resource",{"title":222,"path":223,"stem":224},"Collection Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F2.use-cwa-collection-resource",{"title":226,"path":227,"stem":228},"Image Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-image-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F3.use-cwa-image-resource",{"title":230,"path":231,"stem":232},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form","5.nuxt-module\u002F6.composables\u002F1.component\u002F4.use-cwa-form",{"title":234,"path":235,"stem":236},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-input","5.nuxt-module\u002F6.composables\u002F1.component\u002F5.use-cwa-form-input",{"title":238,"path":239,"stem":240},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F6.composables\u002F1.component\u002F6.use-cwa-form-repeated",{"title":242,"path":243,"stem":244},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-collection","5.nuxt-module\u002F6.composables\u002F1.component\u002F7.use-cwa-form-collection",{"title":246,"path":247,"stem":248,"children":249,"page":35},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F6.composables\u002F2.admin-manager",[250,254,258],{"title":251,"path":252,"stem":253},"Manager Tab","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-manager-tab","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F1.use-cwa-resource-manager-tab",{"title":255,"path":256,"stem":257},"Resource Model","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-model","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F2.use-cwa-resource-model",{"title":259,"path":260,"stem":261},"Resource Upload","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-upload","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F3.use-cwa-resource-upload",{"title":263,"path":264,"stem":265,"children":266,"page":35},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F6.composables\u002F3.utilities",[267,271,275],{"title":268,"path":269,"stem":270},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F1.use-cwa-resource-endpoint",{"title":272,"path":273,"stem":274},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F2.use-query-bound-model",{"title":276,"path":277,"stem":278},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F3.use-cwa-resource-route",{"title":280,"path":281,"stem":282,"children":283,"page":35},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F7.component-helpers",[284,288,292,296,300],{"title":285,"path":286,"stem":287},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F7.component-helpers\u002F1.images-and-uploads",{"title":289,"path":290,"stem":291},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F7.component-helpers\u002F2.collections-and-pagination",{"title":293,"path":294,"stem":295},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F7.component-helpers\u002F3.html-content",{"title":297,"path":298,"stem":299},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F7.component-helpers\u002F4.real-time-updates",{"title":301,"path":302,"stem":303},"Forms","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F7.component-helpers\u002F5.forms",{"title":305,"path":306,"stem":307},"Authentication","\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F8.authentication",{"title":309,"path":310,"stem":311,"children":312,"page":35},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[313,317,321],{"title":314,"path":315,"stem":316},"Overview","\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":318,"path":319,"stem":320},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":322,"path":323,"stem":324},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":326,"path":327,"stem":328,"children":329,"page":35},"Deployment","\u002Fdeployment","6.deployment",[330,334,338],{"title":331,"path":332,"stem":333},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":335,"path":336,"stem":337},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":339,"path":340,"stem":341},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"id":343,"title":339,"badge":344,"body":345,"description":1575,"extension":1576,"links":344,"meta":1577,"navigation":1493,"path":340,"seo":1578,"stem":341,"__hash__":1579},"docs\u002F6.deployment\u002F3.ci-cd.md",null,{"type":346,"value":347,"toc":1550},"minimark",[348,361,374,379,382,413,417,421,444,454,475,478,481,524,527,531,540,554,561,564,586,589,599,602,616,620,634,637,641,644,650,653,701,706,709,853,855,859,870,874,992,996,1046,1053,1057,1171,1175,1256,1260,1341,1345,1445,1447,1451,1543,1546],[349,350,351,352,356,357,360],"p",{},"The template ships a complete ",[353,354,355],"code",{},".gitlab-ci.yml"," with eight stages. Every step delegates to shell functions in ",[353,358,359],{},"bin\u002Fdevops\u002F"," — readable, modifiable scripts you own. There is no GitLab Auto DevOps magic here.",[362,363,364],"blockquote",{},[349,365,366,370,371,373],{},[367,368,369],"strong",{},"GitLab only."," CI\u002FCD support is currently GitLab CI only. GitHub Actions support is planned but not yet implemented. If you need GitHub Actions, the shell functions in ",[353,372,359],{}," are reusable — you would need to write the workflow YAML that calls them.",[375,376,378],"h2",{"id":377},"pipeline-overview","Pipeline Overview",[380,381],"diagram-ci-pipeline",{},[349,383,384,385,388,389,392,393,396,397,400,401,404,405,408,409,412],{},"On every branch: ",[367,386,387],{},"build"," then ",[367,390,391],{},"test"," then spin up a ",[367,394,395],{},"review app"," on Kubernetes. When a branch merges to ",[353,398,399],{},"main",": build and test run again, then the pipeline auto-deploys to ",[367,402,403],{},"staging",", from where you manually promote through ",[367,406,407],{},"canary"," to ",[367,410,411],{},"production",".",[375,414,416],{"id":415},"stages","Stages",[418,419,420],"h3",{"id":387},"Build",[349,422,423,424,427,428,431,432,435,436,439,440,443],{},"Two parallel jobs — ",[353,425,426],{},"build api"," and ",[353,429,430],{},"build app"," — each run ",[353,433,434],{},"docker buildx"," and push to the GitLab Container Registry. Layer caching is handled with ",[353,437,438],{},"--cache-to"," \u002F ",[353,441,442],{},"--cache-from"," against a dedicated cache image tag, so incremental builds are fast.",[445,446,451],"pre",{"className":447,"code":449,"language":450},[448],"language-text","$CI_REGISTRY_IMAGE\u002Fphp:$CI_COMMIT_REF_SLUG    ← FrankenPHP image (--target frankenphp_prod)\n$CI_REGISTRY_IMAGE\u002Fapp:$CI_COMMIT_REF_SLUG    ← Nuxt image       (--target prod)\n","text",[353,452,449],{"__ignoreMap":453},"",[349,455,456,457,460,461,464,465,427,468,471,472,412],{},"The PHP image build uses ",[353,458,459],{},"api\u002FDockerfile","; the Nuxt image uses ",[353,462,463],{},"app\u002FDockerfile",". Build logic lives in ",[353,466,467],{},"build_api()",[353,469,470],{},"build_app()"," in ",[353,473,474],{},"bin\u002Fdevops\u002Fk8s.sh",[418,476,477],{"id":391},"Test",[349,479,480],{},"Two parallel jobs against the image built in the previous stage:",[482,483,484,497],"table",{},[485,486,487],"thead",{},[488,489,490,494],"tr",{},[491,492,493],"th",{},"Job",[491,495,496],{},"What runs",[498,499,500,514],"tbody",{},[488,501,502,508],{},[503,504,505],"td",{},[353,506,507],{},"unit tests",[503,509,510,513],{},[353,511,512],{},"simple-phpunit tests\u002FUnit"," — fast, no database",[488,515,516,521],{},[503,517,518],{},[353,519,520],{},"behat tests",[503,522,523],{},"Behat feature suite with a live PostgreSQL service; migrations run before the suite",[349,525,526],{},"Both export JUnit XML for GitLab's test report UI.",[418,528,530],{"id":529},"review-apps","Review Apps",[349,532,533,534,536,537,412],{},"Every branch (except ",[353,535,399],{},") gets its own Kubernetes namespace and Helm release. The URL follows the pattern ",[353,538,539],{},"https:\u002F\u002F$CI_COMMIT_REF_SLUG-review.$KUBE_INGRESS_BASE_DOMAIN",[541,542,544],"callout",{"icon":543},"i-heroicons-information-circle",[349,545,546,547,550,551,412],{},"Namespaces must be ",[367,548,549],{},"pre-created"," with the appropriate RBAC — the pipeline's runner permissions don't include namespace creation. See the comment in ",[353,552,553],{},"ensure_namespace()",[349,555,556,557,560],{},"Review apps are torn down manually via the ",[367,558,559],{},"stop review"," job (triggered from the GitLab environment list), or automatically when the branch is deleted.",[418,562,563],{"id":403},"Staging",[349,565,566,567,569,570,573,574,577,578,581,582,585],{},"Runs automatically on every merge to ",[353,568,399],{}," (controlled by ",[353,571,572],{},"$STAGING_ENABLED",", default ",[353,575,576],{},"\"true\"","). Calls ",[353,579,580],{},"deploy staging"," which runs ",[353,583,584],{},"helm upgrade --install"," in the staging namespace.",[418,587,588],{"id":407},"Canary",[349,590,591,592,595,596,412],{},"A manual job that runs ",[353,593,594],{},"deploy canary",". Useful for routing a portion of production traffic to the new image before full rollout. Enable\u002Fdisable via ",[353,597,598],{},"$CANARY_ENABLED",[418,600,601],{"id":411},"Production",[349,603,604,605,608,609,427,612,615],{},"A manual job requiring explicit trigger from GitLab. Calls ",[353,606,607],{},"deploy"," (stable track), then ",[353,610,611],{},"delete canary",[353,613,614],{},"delete staging"," to clean up the intermediate environments.",[418,617,619],{"id":618},"performance-optional","Performance (optional)",[349,621,591,622,629,630,633],{},[623,624,628],"a",{"href":625,"rel":626},"https:\u002F\u002Fwww.sitespeed.io\u002F",[627],"nofollow","sitespeed.io"," against the production environment URL. Reads URLs from ",[353,631,632],{},".gitlab-urls.txt"," if present, otherwise tests the environment root. Results upload as GitLab artifacts.",[635,636],"hr",{},[375,638,640],{"id":639},"the-devops-scripts","The Devops Scripts",[349,642,643],{},"All CI logic lives in two scripts sourced at the top of every job:",[418,645,647],{"id":646},"bindevopssetupsh",[353,648,649],{},"bin\u002Fdevops\u002Fsetup.sh",[349,651,652],{},"Sets shared environment variables:",[654,655,656,666,675,684,693],"ul",{},[657,658,659,439,662,665],"li",{},[353,660,661],{},"CI_APPLICATION_REPOSITORY",[353,663,664],{},"CI_APPLICATION_TAG"," — image repo and SHA tag",[657,667,668,439,671,674],{},[353,669,670],{},"PHP_REPOSITORY",[353,672,673],{},"APP_REPOSITORY"," — full image paths",[657,676,677,439,680,683],{},[353,678,679],{},"PHP_REPOSITORY_CACHE",[353,681,682],{},"APP_REPOSITORY_CACHE"," — layer cache image paths",[657,685,686,689,690],{},[353,687,688],{},"DOMAIN"," — derived from ",[353,691,692],{},"CI_ENVIRONMENT_URL",[657,694,695,698,699],{},[353,696,697],{},"DEPLOYMENT_BRANCH"," — defaults to ",[353,700,399],{},[418,702,704],{"id":703},"bindevopsk8ssh",[353,705,474],{},[349,707,708],{},"Contains all the functions the pipeline calls:",[482,710,711,721],{},[485,712,713],{},[488,714,715,718],{},[491,716,717],{},"Function",[491,719,720],{},"What it does",[498,722,723,733,743,753,769,779,789,799,809,823,839],{},[488,724,725,730],{},[503,726,727],{},[353,728,729],{},"install_dependencies",[503,731,732],{},"Installs Helm, kubectl, curl, and other tools on the Alpine CI runner",[488,734,735,740],{},[503,736,737],{},[353,738,739],{},"generate_jwt_keys",[503,741,742],{},"Generates RSA key pair and Mercure JWT secret if not set as CI variables",[488,744,745,750],{},[503,746,747],{},[353,748,749],{},"setup_docker_environment",[503,751,752],{},"Handles Docker-in-Docker host config for Kubernetes runners",[488,754,755,763],{},[503,756,757,439,760],{},[353,758,759],{},"build_api",[353,761,762],{},"build_app",[503,764,765,768],{},[353,766,767],{},"docker buildx build --push"," with registry layer caching",[488,770,771,776],{},[503,772,773],{},[353,774,775],{},"run_test_phpunit",[503,777,778],{},"Runs PHPUnit unit tests; outputs JUnit XML",[488,780,781,786],{},[503,782,783],{},[353,784,785],{},"run_test_behat",[503,787,788],{},"Configures test DB, runs Behat; outputs JUnit XML",[488,790,791,796],{},[503,792,793],{},[353,794,795],{},"helm_init",[503,797,798],{},"Updates and builds Helm chart dependencies",[488,800,801,806],{},[503,802,803],{},[353,804,805],{},"ensure_namespace",[503,807,808],{},"Verifies the K8s namespace exists (fails fast if not)",[488,810,811,816],{},[503,812,813],{},[353,814,815],{},"create_docker_pull_secret",[503,817,818,819,822],{},"Creates an ",[353,820,821],{},"imagePullSecret"," for the GitLab registry",[488,824,825,830],{},[503,826,827],{},[353,828,829],{},"deploy [track]",[503,831,832,833,836,837],{},"Generates ",[353,834,835],{},"values.tmp.yaml"," from CI variables and runs ",[353,838,584],{},[488,840,841,846],{},[503,842,843],{},[353,844,845],{},"delete [track]",[503,847,848,849,852],{},"Runs ",[353,850,851],{},"helm uninstall"," for review\u002Fcanary\u002Fstaging cleanup",[635,854],{},[375,856,858],{"id":857},"required-cicd-variables","Required CI\u002FCD Variables",[349,860,861,862,865,866,869],{},"Set these in ",[367,863,864],{},"Settings → CI\u002FCD → Variables"," in GitLab. Variables marked auto-generated are created by ",[353,867,868],{},"generate_jwt_keys()"," at pipeline start if not set — useful for review apps, but for production you should pin them as CI secrets so they don't rotate on every deploy.",[418,871,873],{"id":872},"kubernetes","Kubernetes",[482,875,876,889],{},[485,877,878],{},[488,879,880,883,886],{},[491,881,882],{},"Variable",[491,884,885],{},"Required",[491,887,888],{},"Notes",[498,890,891,907,919,934,945,962,974],{},[488,892,893,898,901],{},[503,894,895],{},[353,896,897],{},"KUBE_CONTEXT",[503,899,900],{},"Yes",[503,902,903,904],{},"GitLab agent context, e.g. ",[353,905,906],{},"my-group\u002Fmy-project:my-agent",[488,908,909,914,916],{},[503,910,911],{},[353,912,913],{},"KUBE_NAMESPACE",[503,915,900],{},[503,917,918],{},"Pre-created namespace for this environment",[488,920,921,926,928],{},[503,922,923],{},[353,924,925],{},"KUBE_INGRESS_BASE_DOMAIN",[503,927,900],{},[503,929,930,931],{},"Base domain for ingress URLs, e.g. ",[353,932,933],{},"k8s.example.com",[488,935,936,940,942],{},[503,937,938],{},[353,939,692],{},[503,941,900],{},[503,943,944],{},"Full URL of this environment (GitLab sets this for named environments)",[488,946,947,952,955],{},[503,948,949],{},[353,950,951],{},"CLUSTER_ISSUER",[503,953,954],{},"No",[503,956,957,958,961],{},"cert-manager ClusterIssuer name (default: ",[353,959,960],{},"letsencrypt-staging",")",[488,963,964,969,971],{},[503,965,966],{},[353,967,968],{},"KUBE_INGRESS_ALIAS_DOMAINS",[503,970,954],{},[503,972,973],{},"Comma-separated extra domains to alias in ingress",[488,975,976,981,983],{},[503,977,978],{},[353,979,980],{},"INGRESS_ENABLED",[503,982,954],{},[503,984,985,986,988,989,961],{},"Set ",[353,987,576],{}," to enable the ingress resource (default: ",[353,990,991],{},"\"false\"",[418,993,995],{"id":994},"jwt-mercure","JWT & Mercure",[482,997,998,1006],{},[485,999,1000],{},[488,1001,1002,1004],{},[491,1003,882],{},[491,1005,888],{},[498,1007,1008,1018,1027,1037],{},[488,1009,1010,1015],{},[503,1011,1012],{},[353,1013,1014],{},"JWT_PASSPHRASE",[503,1016,1017],{},"Auto-generated if not set",[488,1019,1020,1025],{},[503,1021,1022],{},[353,1023,1024],{},"JWT_SECRET_KEY",[503,1026,1017],{},[488,1028,1029,1034],{},[503,1030,1031],{},[353,1032,1033],{},"JWT_PUBLIC_KEY",[503,1035,1036],{},"Derived from secret key if not set",[488,1038,1039,1044],{},[503,1040,1041],{},[353,1042,1043],{},"MERCURE_JWT_SECRET",[503,1045,1017],{},[349,1047,1048,1049,1052],{},"For production, generate these once and save them as ",[367,1050,1051],{},"protected, masked"," CI variables. Otherwise they regenerate on every deploy, invalidating all active user sessions.",[418,1054,1056],{"id":1055},"application","Application",[482,1058,1059,1070],{},[485,1060,1061],{},[488,1062,1063,1065,1068],{},[491,1064,882],{},[491,1066,1067],{},"Default",[491,1069,888],{},[498,1071,1072,1088,1100,1115,1132,1147,1159],{},[488,1073,1074,1079,1082],{},[503,1075,1076],{},[353,1077,1078],{},"CORS_ALLOW_ORIGIN",[503,1080,1081],{},"—",[503,1083,1084,1085],{},"Regex, e.g. ",[353,1086,1087],{},"^https?:\u002F\u002F(.*\\.)?example\\.com",[488,1089,1090,1095,1097],{},[503,1091,1092],{},[353,1093,1094],{},"TRUSTED_HOSTS",[503,1096,1081],{},[503,1098,1099],{},"Symfony trusted hosts regex",[488,1101,1102,1107,1112],{},[503,1103,1104],{},[353,1105,1106],{},"ADMIN_USERNAME",[503,1108,1109],{},[353,1110,1111],{},"admin",[503,1113,1114],{},"Initial admin account username",[488,1116,1117,1122,1126],{},[503,1118,1119],{},[353,1120,1121],{},"ADMIN_PASSWORD",[503,1123,1124],{},[353,1125,1111],{},[503,1127,1128,1129],{},"Initial admin account password — ",[367,1130,1131],{},"change this",[488,1133,1134,1139,1144],{},[503,1135,1136],{},[353,1137,1138],{},"ADMIN_EMAIL",[503,1140,1141],{},[353,1142,1143],{},"hello@cwa.rocks",[503,1145,1146],{},"Initial admin account email",[488,1148,1149,1154,1156],{},[503,1150,1151],{},[353,1152,1153],{},"MAILER_DSN",[503,1155,1081],{},[503,1157,1158],{},"SMTP or SES DSN for transactional email",[488,1160,1161,1166,1168],{},[503,1162,1163],{},[353,1164,1165],{},"MAILER_EMAIL",[503,1167,1081],{},[503,1169,1170],{},"From address for outgoing email",[418,1172,1174],{"id":1173},"database","Database",[482,1176,1177,1187],{},[485,1178,1179],{},[488,1180,1181,1183,1185],{},[491,1182,882],{},[491,1184,1067],{},[491,1186,888],{},[498,1188,1189,1206,1218,1244],{},[488,1190,1191,1196,1201],{},[503,1192,1193],{},[353,1194,1195],{},"POSTGRESQL_ENABLED",[503,1197,1198],{},[353,1199,1200],{},"true",[503,1202,985,1203,1205],{},[353,1204,991],{}," to use an external DB (disables the bundled Postgres pod)",[488,1207,1208,1213,1215],{},[503,1209,1210],{},[353,1211,1212],{},"DATABASE_URL",[503,1214,1081],{},[503,1216,1217],{},"Connection string when using external Postgres",[488,1219,1220,1225,1230],{},[503,1221,1222],{},[353,1223,1224],{},"DATABASE_SSL_MODE",[503,1226,1227],{},[353,1228,1229],{},"prefer",[503,1231,1232,1235,1236,1235,1238,1235,1241],{},[353,1233,1234],{},"disable",", ",[353,1237,1229],{},[353,1239,1240],{},"require",[353,1242,1243],{},"verify-full",[488,1245,1246,1251,1253],{},[503,1247,1248],{},[353,1249,1250],{},"DATABASE_CA_CERT",[503,1252,1081],{},[503,1254,1255],{},"CA cert PEM for SSL verification",[418,1257,1259],{"id":1258},"autoscaling","Autoscaling",[482,1261,1262,1270],{},[485,1263,1264],{},[488,1265,1266,1268],{},[491,1267,882],{},[491,1269,1067],{},[498,1271,1272,1284,1295,1306,1318,1330],{},[488,1273,1274,1279],{},[503,1275,1276],{},[353,1277,1278],{},"REPLICA_COUNT",[503,1280,1281],{},[353,1282,1283],{},"1",[488,1285,1286,1291],{},[503,1287,1288],{},[353,1289,1290],{},"AUTOSCALE",[503,1292,1293],{},[353,1294,1200],{},[488,1296,1297,1302],{},[503,1298,1299],{},[353,1300,1301],{},"AUTOSCALE_MIN",[503,1303,1304],{},[353,1305,1283],{},[488,1307,1308,1313],{},[503,1309,1310],{},[353,1311,1312],{},"AUTOSCALE_MAX",[503,1314,1315],{},[353,1316,1317],{},"3",[488,1319,1320,1325],{},[503,1321,1322],{},[353,1323,1324],{},"AUTOSCALE_CPU_PERCENT",[503,1326,1327],{},[353,1328,1329],{},"90",[488,1331,1332,1337],{},[503,1333,1334],{},[353,1335,1336],{},"AUTOSCALE_MEMORY_PERCENT",[503,1338,1339],{},[353,1340,1329],{},[418,1342,1344],{"id":1343},"pipeline-flags","Pipeline Flags",[482,1346,1347,1358],{},[485,1348,1349],{},[488,1350,1351,1353,1355],{},[491,1352,882],{},[491,1354,1067],{},[491,1356,1357],{},"Effect",[498,1359,1360,1375,1389,1403,1417,1431],{},[488,1361,1362,1367,1372],{},[503,1363,1364],{},[353,1365,1366],{},"BUILD_DISABLED",[503,1368,1369],{},[353,1370,1371],{},"false",[503,1373,1374],{},"Skip the build stage entirely",[488,1376,1377,1382,1386],{},[503,1378,1379],{},[353,1380,1381],{},"TEST_DISABLED",[503,1383,1384],{},[353,1385,1371],{},[503,1387,1388],{},"Skip the test stage",[488,1390,1391,1396,1400],{},[503,1392,1393],{},[353,1394,1395],{},"STAGING_ENABLED",[503,1397,1398],{},[353,1399,1200],{},[503,1401,1402],{},"Auto-deploy staging on merge to main",[488,1404,1405,1410,1414],{},[503,1406,1407],{},[353,1408,1409],{},"CANARY_ENABLED",[503,1411,1412],{},[353,1413,1200],{},[503,1415,1416],{},"Show the canary job",[488,1418,1419,1424,1426],{},[503,1420,1421],{},[353,1422,1423],{},"REVIEW_DISABLED",[503,1425,1081],{},[503,1427,985,1428,1430],{},[353,1429,576],{}," to disable review apps on branches",[488,1432,1433,1438,1442],{},[503,1434,1435],{},[353,1436,1437],{},"PERFORMANCE_DISABLED",[503,1439,1440],{},[353,1441,1371],{},[503,1443,1444],{},"Skip the sitespeed.io performance job",[635,1446],{},[375,1448,1450],{"id":1449},"rollback","Rollback",[445,1452,1456],{"className":1453,"code":1454,"language":1455,"meta":453,"style":453},"language-bash shiki shiki-themes github-light github-dark material-theme-palenight","# List Helm history\nhelm history cwa --namespace production\n\n# Roll back to a specific revision\nhelm rollback cwa 3 --namespace production\n\n# Roll back to the previous release\nhelm rollback cwa --namespace production\n","bash",[353,1457,1458,1467,1488,1495,1501,1519,1524,1530],{"__ignoreMap":453},[1459,1460,1463],"span",{"class":1461,"line":1462},"line",1,[1459,1464,1466],{"class":1465},"sTBSN","# List Helm history\n",[1459,1468,1470,1474,1478,1481,1485],{"class":1461,"line":1469},2,[1459,1471,1473],{"class":1472},"sRCss","helm",[1459,1475,1477],{"class":1476},"sLL54"," history",[1459,1479,1480],{"class":1476}," cwa",[1459,1482,1484],{"class":1483},"szhYu"," --namespace",[1459,1486,1487],{"class":1476}," production\n",[1459,1489,1491],{"class":1461,"line":1490},3,[1459,1492,1494],{"emptyLinePlaceholder":1493},true,"\n",[1459,1496,1498],{"class":1461,"line":1497},4,[1459,1499,1500],{"class":1465},"# Roll back to a specific revision\n",[1459,1502,1504,1506,1509,1511,1515,1517],{"class":1461,"line":1503},5,[1459,1505,1473],{"class":1472},[1459,1507,1508],{"class":1476}," rollback",[1459,1510,1480],{"class":1476},[1459,1512,1514],{"class":1513},"scSvc"," 3",[1459,1516,1484],{"class":1483},[1459,1518,1487],{"class":1476},[1459,1520,1522],{"class":1461,"line":1521},6,[1459,1523,1494],{"emptyLinePlaceholder":1493},[1459,1525,1527],{"class":1461,"line":1526},7,[1459,1528,1529],{"class":1465},"# Roll back to the previous release\n",[1459,1531,1533,1535,1537,1539,1541],{"class":1461,"line":1532},8,[1459,1534,1473],{"class":1472},[1459,1536,1508],{"class":1476},[1459,1538,1480],{"class":1476},[1459,1540,1484],{"class":1483},[1459,1542,1487],{"class":1476},[349,1544,1545],{},"The previous image tag (the git SHA) is baked into the Helm release history, so Helm's rollback restores both the configuration and the image version in one command.",[1547,1548,1549],"style",{},"html pre.shiki code .sTBSN, html code.shiki .sTBSN{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sRCss, html code.shiki .sRCss{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#FFCB6B}html pre.shiki code .sLL54, html code.shiki .sLL54{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#C3E88D}html pre.shiki code .szhYu, html code.shiki .szhYu{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#C3E88D}html pre.shiki code .scSvc, html code.shiki .scSvc{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":453,"searchDepth":1469,"depth":1469,"links":1551},[1552,1553,1562,1566,1574],{"id":377,"depth":1469,"text":378},{"id":415,"depth":1469,"text":416,"children":1554},[1555,1556,1557,1558,1559,1560,1561],{"id":387,"depth":1490,"text":420},{"id":391,"depth":1490,"text":477},{"id":529,"depth":1490,"text":530},{"id":403,"depth":1490,"text":563},{"id":407,"depth":1490,"text":588},{"id":411,"depth":1490,"text":601},{"id":618,"depth":1490,"text":619},{"id":639,"depth":1469,"text":640,"children":1563},[1564,1565],{"id":646,"depth":1490,"text":649},{"id":703,"depth":1490,"text":474},{"id":857,"depth":1469,"text":858,"children":1567},[1568,1569,1570,1571,1572,1573],{"id":872,"depth":1490,"text":873},{"id":994,"depth":1490,"text":995},{"id":1055,"depth":1490,"text":1056},{"id":1173,"depth":1490,"text":1174},{"id":1258,"depth":1490,"text":1259},{"id":1343,"depth":1490,"text":1344},{"id":1449,"depth":1469,"text":1450},"The template's GitLab CI pipeline — Docker Buildx builds, PHPUnit + Behat tests, per-branch review apps, and staged Kubernetes deployments via Helm.","md",{},{"title":339,"description":1575},"9NhhgpCDNIpAeY4-LXK06B2xgyXSpBNuZs3O9sn-pik",[1581,344],{"title":335,"path":336,"stem":337,"description":1582,"children":-1},"Deploying the CWA stack to Kubernetes using Helm — values configuration, secrets management, migration Jobs, and rolling updates.",1782241287019]