[{"data":1,"prerenderedAt":1741},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Fci-cd":358,"\u002Fdeployment\u002Fci-cd-surround":1738},[4,14,36,69,140,341],{"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,182,207,211,295,320,324],{"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,178],{"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},"CLI Generator","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcwa-cli","5.nuxt-module\u002F3.building-your-ui\u002F4.cwa-cli",{"title":183,"path":184,"stem":185,"children":186,"page":35},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F4.cwa-components",[187,191,195,199,203],{"title":188,"path":189,"stem":190},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F4.cwa-components\u002F1.cwa-component-group",{"title":192,"path":193,"stem":194},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F4.cwa-components\u002F2.cwa-page",{"title":196,"path":197,"stem":198},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F4.cwa-components\u002F3.cwa-link",{"title":200,"path":201,"stem":202},"\u003CCwaImage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-image","5.nuxt-module\u002F4.cwa-components\u002F4.cwa-image",{"title":204,"path":205,"stem":206},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F4.cwa-components\u002F5.cwa-default-layout",{"title":208,"path":209,"stem":210},"The useCwa() API","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F5.cwa-api",{"title":212,"path":213,"stem":214,"children":215,"page":35},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F6.composables",[216,224,261,278],{"title":217,"path":218,"stem":219,"children":220,"page":35},"Layout","\u002Fnuxt-module\u002Fcomposables\u002Flayout","5.nuxt-module\u002F6.composables\u002F0.layout",[221],{"title":217,"path":222,"stem":223},"\u002Fnuxt-module\u002Fcomposables\u002Flayout\u002Fuse-cwa-layout","5.nuxt-module\u002F6.composables\u002F0.layout\u002F1.use-cwa-layout",{"title":225,"path":226,"stem":227,"children":228,"page":35},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F6.composables\u002F1.component",[229,233,237,241,245,249,253,257],{"title":230,"path":231,"stem":232},"Component (recommended)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-component","5.nuxt-module\u002F6.composables\u002F1.component\u002F0.use-cwa-component",{"title":234,"path":235,"stem":236},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F1.use-cwa-resource",{"title":238,"path":239,"stem":240},"Collection Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F2.use-cwa-collection-resource",{"title":242,"path":243,"stem":244},"Image Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-image-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F3.use-cwa-image-resource",{"title":246,"path":247,"stem":248},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form","5.nuxt-module\u002F6.composables\u002F1.component\u002F4.use-cwa-form",{"title":250,"path":251,"stem":252},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-input","5.nuxt-module\u002F6.composables\u002F1.component\u002F5.use-cwa-form-input",{"title":254,"path":255,"stem":256},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F6.composables\u002F1.component\u002F6.use-cwa-form-repeated",{"title":258,"path":259,"stem":260},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-collection","5.nuxt-module\u002F6.composables\u002F1.component\u002F7.use-cwa-form-collection",{"title":262,"path":263,"stem":264,"children":265,"page":35},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F6.composables\u002F2.admin-manager",[266,270,274],{"title":267,"path":268,"stem":269},"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":271,"path":272,"stem":273},"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":275,"path":276,"stem":277},"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":279,"path":280,"stem":281,"children":282,"page":35},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F6.composables\u002F3.utilities",[283,287,291],{"title":284,"path":285,"stem":286},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F1.use-cwa-resource-endpoint",{"title":288,"path":289,"stem":290},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F2.use-query-bound-model",{"title":292,"path":293,"stem":294},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F3.use-cwa-resource-route",{"title":296,"path":297,"stem":298,"children":299,"page":35},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F7.component-helpers",[300,304,308,312,316],{"title":301,"path":302,"stem":303},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F7.component-helpers\u002F1.images-and-uploads",{"title":305,"path":306,"stem":307},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F7.component-helpers\u002F2.collections-and-pagination",{"title":309,"path":310,"stem":311},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F7.component-helpers\u002F3.html-content",{"title":313,"path":314,"stem":315},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F7.component-helpers\u002F4.real-time-updates",{"title":317,"path":318,"stem":319},"Forms","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F7.component-helpers\u002F5.forms",{"title":321,"path":322,"stem":323},"Authentication","\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F8.authentication",{"title":325,"path":326,"stem":327,"children":328,"page":35},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[329,333,337],{"title":330,"path":331,"stem":332},"Overview","\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":334,"path":335,"stem":336},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":338,"path":339,"stem":340},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":342,"path":343,"stem":344,"children":345,"page":35},"Deployment","\u002Fdeployment","6.deployment",[346,350,354],{"title":347,"path":348,"stem":349},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":351,"path":352,"stem":353},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":355,"path":356,"stem":357},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"id":359,"title":355,"badge":360,"body":361,"description":1733,"extension":1734,"links":360,"meta":1735,"navigation":1649,"path":356,"seo":1736,"stem":357,"__hash__":1737},"docs\u002F6.deployment\u002F3.ci-cd.md",null,{"type":362,"value":363,"toc":1706},"minimark",[364,378,385,390,397,471,485,500,504,511,515,518,549,553,557,580,590,611,614,617,654,657,661,670,682,689,692,714,717,727,730,744,748,762,765,769,772,778,781,829,834,837,980,982,986,997,1001,1119,1123,1173,1180,1184,1298,1302,1383,1387,1468,1472,1588,1601,1603,1607,1699,1702],[365,366,367,368,372,373,377],"p",{},"The template ships complete CI\u002FCD for ",[369,370,371],"strong",{},"both GitLab CI and GitHub Actions",". Every step delegates to shell functions in ",[374,375,376],"code",{},"bin\u002Fdevops\u002F"," — readable, modifiable scripts you own.",[365,379,380,381,384],{},"Choose your CI\u002FCD provider when you run ",[374,382,383],{},"pnpm create cwa"," — the CLI wires up the correct pipeline files automatically. You can switch later by copying the relevant files from the template.",[386,387,389],"h2",{"id":388},"github-actions","GitHub Actions",[365,391,392,393,396],{},"Four workflows live in ",[374,394,395],{},".github\u002Fworkflows\u002F",":",[398,399,400,416],"table",{},[401,402,403],"thead",{},[404,405,406,410,413],"tr",{},[407,408,409],"th",{},"Workflow",[407,411,412],{},"Trigger",[407,414,415],{},"What it does",[417,418,419,433,446,459],"tbody",{},[404,420,421,427,430],{},[422,423,424],"td",{},[374,425,426],{},"ci.yml",[422,428,429],{},"Every push",[422,431,432],{},"Build + test; deploy review environment on PRs",[404,434,435,440,443],{},[422,436,437],{},[374,438,439],{},"production.yml",[422,441,442],{},"Manual",[422,444,445],{},"Deploy canary or full production",[404,447,448,453,456],{},[422,449,450],{},[374,451,452],{},"cleanup.yml",[422,454,455],{},"PR closed",[422,457,458],{},"Tear down the review environment",[404,460,461,466,468],{},[422,462,463],{},[374,464,465],{},"performance.yml",[422,467,442],{},[422,469,470],{},"Run sitespeed.io tests",[365,472,473,474,477,478,480,481,484],{},"Images push to GHCR (",[374,475,476],{},"ghcr.io\u002F${{ github.repository }}","). The workflows call the same ",[374,479,376],{}," shell functions as GitLab CI. Required secrets and variables are listed in the template repo's ",[374,482,483],{},"CLAUDE.md"," and in the workflow files themselves.",[486,487,489],"callout",{"icon":488},"i-heroicons-information-circle",[365,490,491,492,495,496,499],{},"The workflows run on every push by default. Set the GitHub Actions ",[369,493,494],{},"variable"," ",[374,497,498],{},"CI_DISABLED=true"," (under Settings → Secrets and variables → Actions → Variables) to suppress them. This is used on the template repo itself, which deploys via GitLab CI and only mirrors to GitHub.",[386,501,503],{"id":502},"gitlab-ci","GitLab CI",[365,505,506,507,510],{},"The ",[374,508,509],{},".gitlab-ci.yml"," has eight stages. There is no GitLab Auto DevOps magic here.",[386,512,514],{"id":513},"pipeline-overview","Pipeline Overview",[516,517],"diagram-ci-pipeline",{},[365,519,520,521,524,525,528,529,532,533,536,537,540,541,544,545,548],{},"On every branch: ",[369,522,523],{},"build"," then ",[369,526,527],{},"test"," then spin up a ",[369,530,531],{},"review app"," on Kubernetes. When a branch merges to ",[374,534,535],{},"main",": build and test run again, then the pipeline auto-deploys to ",[369,538,539],{},"staging",", from where you manually promote through ",[369,542,543],{},"canary"," to ",[369,546,547],{},"production",".",[386,550,552],{"id":551},"stages","Stages",[554,555,556],"h3",{"id":523},"Build",[365,558,559,560,563,564,567,568,571,572,575,576,579],{},"Two parallel jobs — ",[374,561,562],{},"build api"," and ",[374,565,566],{},"build app"," — each run ",[374,569,570],{},"docker buildx"," and push to the GitLab Container Registry. Layer caching is handled with ",[374,573,574],{},"--cache-to"," \u002F ",[374,577,578],{},"--cache-from"," against a dedicated cache image tag, so incremental builds are fast.",[581,582,587],"pre",{"className":583,"code":585,"language":586},[584],"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",[374,588,585],{"__ignoreMap":589},"",[365,591,592,593,596,597,600,601,563,604,607,608,548],{},"The PHP image build uses ",[374,594,595],{},"api\u002FDockerfile","; the Nuxt image uses ",[374,598,599],{},"app\u002FDockerfile",". Build logic lives in ",[374,602,603],{},"build_api()",[374,605,606],{},"build_app()"," in ",[374,609,610],{},"bin\u002Fdevops\u002Fk8s.sh",[554,612,613],{"id":527},"Test",[365,615,616],{},"Two parallel jobs against the image built in the previous stage:",[398,618,619,629],{},[401,620,621],{},[404,622,623,626],{},[407,624,625],{},"Job",[407,627,628],{},"What runs",[417,630,631,644],{},[404,632,633,638],{},[422,634,635],{},[374,636,637],{},"unit tests",[422,639,640,643],{},[374,641,642],{},"simple-phpunit tests\u002FUnit"," — fast, no database",[404,645,646,651],{},[422,647,648],{},[374,649,650],{},"behat tests",[422,652,653],{},"Behat feature suite with a live PostgreSQL service; migrations run before the suite",[365,655,656],{},"Both export JUnit XML for GitLab's test report UI.",[554,658,660],{"id":659},"review-apps","Review Apps",[365,662,663,664,666,667,548],{},"Every branch (except ",[374,665,535],{},") gets its own Kubernetes namespace and Helm release. The URL follows the pattern ",[374,668,669],{},"https:\u002F\u002F$CI_COMMIT_REF_SLUG-review.$KUBE_INGRESS_BASE_DOMAIN",[486,671,672],{"icon":488},[365,673,674,675,678,679,548],{},"Namespaces must be ",[369,676,677],{},"pre-created"," with the appropriate RBAC — the pipeline's runner permissions don't include namespace creation. See the comment in ",[374,680,681],{},"ensure_namespace()",[365,683,684,685,688],{},"Review apps are torn down manually via the ",[369,686,687],{},"stop review"," job (triggered from the GitLab environment list), or automatically when the branch is deleted.",[554,690,691],{"id":539},"Staging",[365,693,694,695,697,698,701,702,705,706,709,710,713],{},"Runs automatically on every merge to ",[374,696,535],{}," (controlled by ",[374,699,700],{},"$STAGING_ENABLED",", default ",[374,703,704],{},"\"true\"","). Calls ",[374,707,708],{},"deploy staging"," which runs ",[374,711,712],{},"helm upgrade --install"," in the staging namespace.",[554,715,716],{"id":543},"Canary",[365,718,719,720,723,724,548],{},"A manual job that runs ",[374,721,722],{},"deploy canary",". Useful for routing a portion of production traffic to the new image before full rollout. Enable\u002Fdisable via ",[374,725,726],{},"$CANARY_ENABLED",[554,728,729],{"id":547},"Production",[365,731,732,733,736,737,563,740,743],{},"A manual job requiring explicit trigger from GitLab. Calls ",[374,734,735],{},"deploy"," (stable track), then ",[374,738,739],{},"delete canary",[374,741,742],{},"delete staging"," to clean up the intermediate environments.",[554,745,747],{"id":746},"performance-optional","Performance (optional)",[365,749,719,750,757,758,761],{},[751,752,756],"a",{"href":753,"rel":754},"https:\u002F\u002Fwww.sitespeed.io\u002F",[755],"nofollow","sitespeed.io"," against the production environment URL. Reads URLs from ",[374,759,760],{},".gitlab-urls.txt"," if present, otherwise tests the environment root. Results upload as GitLab artifacts.",[763,764],"hr",{},[386,766,768],{"id":767},"the-devops-scripts","The Devops Scripts",[365,770,771],{},"All CI logic lives in two scripts sourced at the top of every job:",[554,773,775],{"id":774},"bindevopssetupsh",[374,776,777],{},"bin\u002Fdevops\u002Fsetup.sh",[365,779,780],{},"Sets shared environment variables:",[782,783,784,794,803,812,821],"ul",{},[785,786,787,575,790,793],"li",{},[374,788,789],{},"CI_APPLICATION_REPOSITORY",[374,791,792],{},"CI_APPLICATION_TAG"," — image repo and SHA tag",[785,795,796,575,799,802],{},[374,797,798],{},"PHP_REPOSITORY",[374,800,801],{},"APP_REPOSITORY"," — full image paths",[785,804,805,575,808,811],{},[374,806,807],{},"PHP_REPOSITORY_CACHE",[374,809,810],{},"APP_REPOSITORY_CACHE"," — layer cache image paths",[785,813,814,817,818],{},[374,815,816],{},"DOMAIN"," — derived from ",[374,819,820],{},"CI_ENVIRONMENT_URL",[785,822,823,826,827],{},[374,824,825],{},"DEPLOYMENT_BRANCH"," — defaults to ",[374,828,535],{},[554,830,832],{"id":831},"bindevopsk8ssh",[374,833,610],{},[365,835,836],{},"Contains all the functions the pipeline calls:",[398,838,839,848],{},[401,840,841],{},[404,842,843,846],{},[407,844,845],{},"Function",[407,847,415],{},[417,849,850,860,870,880,896,906,916,926,936,950,966],{},[404,851,852,857],{},[422,853,854],{},[374,855,856],{},"install_dependencies",[422,858,859],{},"Installs Helm, kubectl, curl, and other tools on the Alpine CI runner",[404,861,862,867],{},[422,863,864],{},[374,865,866],{},"generate_jwt_keys",[422,868,869],{},"Generates RSA key pair and Mercure JWT secret if not set as CI variables",[404,871,872,877],{},[422,873,874],{},[374,875,876],{},"setup_docker_environment",[422,878,879],{},"Handles Docker-in-Docker host config for Kubernetes runners",[404,881,882,890],{},[422,883,884,575,887],{},[374,885,886],{},"build_api",[374,888,889],{},"build_app",[422,891,892,895],{},[374,893,894],{},"docker buildx build --push"," with registry layer caching",[404,897,898,903],{},[422,899,900],{},[374,901,902],{},"run_test_phpunit",[422,904,905],{},"Runs PHPUnit unit tests; outputs JUnit XML",[404,907,908,913],{},[422,909,910],{},[374,911,912],{},"run_test_behat",[422,914,915],{},"Configures test DB, runs Behat; outputs JUnit XML",[404,917,918,923],{},[422,919,920],{},[374,921,922],{},"helm_init",[422,924,925],{},"Updates and builds Helm chart dependencies",[404,927,928,933],{},[422,929,930],{},[374,931,932],{},"ensure_namespace",[422,934,935],{},"Verifies the K8s namespace exists (fails fast if not)",[404,937,938,943],{},[422,939,940],{},[374,941,942],{},"create_docker_pull_secret",[422,944,945,946,949],{},"Creates an ",[374,947,948],{},"imagePullSecret"," for the GitLab registry",[404,951,952,957],{},[422,953,954],{},[374,955,956],{},"deploy [track]",[422,958,959,960,963,964],{},"Generates ",[374,961,962],{},"values.tmp.yaml"," from CI variables and runs ",[374,965,712],{},[404,967,968,973],{},[422,969,970],{},[374,971,972],{},"delete [track]",[422,974,975,976,979],{},"Runs ",[374,977,978],{},"helm uninstall"," for review\u002Fcanary\u002Fstaging cleanup",[763,981],{},[386,983,985],{"id":984},"required-cicd-variables","Required CI\u002FCD Variables",[365,987,988,989,992,993,996],{},"Set these in ",[369,990,991],{},"Settings → CI\u002FCD → Variables"," in GitLab. Variables marked auto-generated are created by ",[374,994,995],{},"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.",[554,998,1000],{"id":999},"kubernetes","Kubernetes",[398,1002,1003,1016],{},[401,1004,1005],{},[404,1006,1007,1010,1013],{},[407,1008,1009],{},"Variable",[407,1011,1012],{},"Required",[407,1014,1015],{},"Notes",[417,1017,1018,1034,1046,1061,1072,1089,1101],{},[404,1019,1020,1025,1028],{},[422,1021,1022],{},[374,1023,1024],{},"KUBE_CONTEXT",[422,1026,1027],{},"Yes",[422,1029,1030,1031],{},"GitLab agent context, e.g. ",[374,1032,1033],{},"my-group\u002Fmy-project:my-agent",[404,1035,1036,1041,1043],{},[422,1037,1038],{},[374,1039,1040],{},"KUBE_NAMESPACE",[422,1042,1027],{},[422,1044,1045],{},"Pre-created namespace for this environment",[404,1047,1048,1053,1055],{},[422,1049,1050],{},[374,1051,1052],{},"KUBE_INGRESS_BASE_DOMAIN",[422,1054,1027],{},[422,1056,1057,1058],{},"Base domain for ingress URLs, e.g. ",[374,1059,1060],{},"k8s.example.com",[404,1062,1063,1067,1069],{},[422,1064,1065],{},[374,1066,820],{},[422,1068,1027],{},[422,1070,1071],{},"Full URL of this environment (GitLab sets this for named environments)",[404,1073,1074,1079,1082],{},[422,1075,1076],{},[374,1077,1078],{},"CLUSTER_ISSUER",[422,1080,1081],{},"No",[422,1083,1084,1085,1088],{},"cert-manager ClusterIssuer name (default: ",[374,1086,1087],{},"letsencrypt-staging",")",[404,1090,1091,1096,1098],{},[422,1092,1093],{},[374,1094,1095],{},"KUBE_INGRESS_ALIAS_DOMAINS",[422,1097,1081],{},[422,1099,1100],{},"Comma-separated extra domains to alias in ingress",[404,1102,1103,1108,1110],{},[422,1104,1105],{},[374,1106,1107],{},"INGRESS_ENABLED",[422,1109,1081],{},[422,1111,1112,1113,1115,1116,1088],{},"Set ",[374,1114,704],{}," to enable the ingress resource (default: ",[374,1117,1118],{},"\"false\"",[554,1120,1122],{"id":1121},"jwt-mercure","JWT & Mercure",[398,1124,1125,1133],{},[401,1126,1127],{},[404,1128,1129,1131],{},[407,1130,1009],{},[407,1132,1015],{},[417,1134,1135,1145,1154,1164],{},[404,1136,1137,1142],{},[422,1138,1139],{},[374,1140,1141],{},"JWT_PASSPHRASE",[422,1143,1144],{},"Auto-generated if not set",[404,1146,1147,1152],{},[422,1148,1149],{},[374,1150,1151],{},"JWT_SECRET_KEY",[422,1153,1144],{},[404,1155,1156,1161],{},[422,1157,1158],{},[374,1159,1160],{},"JWT_PUBLIC_KEY",[422,1162,1163],{},"Derived from secret key if not set",[404,1165,1166,1171],{},[422,1167,1168],{},[374,1169,1170],{},"MERCURE_JWT_SECRET",[422,1172,1144],{},[365,1174,1175,1176,1179],{},"For production, generate these once and save them as ",[369,1177,1178],{},"protected, masked"," CI variables. Otherwise they regenerate on every deploy, invalidating all active user sessions.",[554,1181,1183],{"id":1182},"application","Application",[398,1185,1186,1197],{},[401,1187,1188],{},[404,1189,1190,1192,1195],{},[407,1191,1009],{},[407,1193,1194],{},"Default",[407,1196,1015],{},[417,1198,1199,1215,1227,1242,1259,1274,1286],{},[404,1200,1201,1206,1209],{},[422,1202,1203],{},[374,1204,1205],{},"CORS_ALLOW_ORIGIN",[422,1207,1208],{},"—",[422,1210,1211,1212],{},"Regex, e.g. ",[374,1213,1214],{},"^https?:\u002F\u002F(.*\\.)?example\\.com",[404,1216,1217,1222,1224],{},[422,1218,1219],{},[374,1220,1221],{},"TRUSTED_HOSTS",[422,1223,1208],{},[422,1225,1226],{},"Symfony trusted hosts regex",[404,1228,1229,1234,1239],{},[422,1230,1231],{},[374,1232,1233],{},"ADMIN_USERNAME",[422,1235,1236],{},[374,1237,1238],{},"admin",[422,1240,1241],{},"Initial admin account username",[404,1243,1244,1249,1253],{},[422,1245,1246],{},[374,1247,1248],{},"ADMIN_PASSWORD",[422,1250,1251],{},[374,1252,1238],{},[422,1254,1255,1256],{},"Initial admin account password — ",[369,1257,1258],{},"change this",[404,1260,1261,1266,1271],{},[422,1262,1263],{},[374,1264,1265],{},"ADMIN_EMAIL",[422,1267,1268],{},[374,1269,1270],{},"hello@cwa.rocks",[422,1272,1273],{},"Initial admin account email",[404,1275,1276,1281,1283],{},[422,1277,1278],{},[374,1279,1280],{},"MAILER_DSN",[422,1282,1208],{},[422,1284,1285],{},"SMTP or SES DSN for transactional email",[404,1287,1288,1293,1295],{},[422,1289,1290],{},[374,1291,1292],{},"MAILER_EMAIL",[422,1294,1208],{},[422,1296,1297],{},"From address for outgoing email",[554,1299,1301],{"id":1300},"database","Database",[398,1303,1304,1314],{},[401,1305,1306],{},[404,1307,1308,1310,1312],{},[407,1309,1009],{},[407,1311,1194],{},[407,1313,1015],{},[417,1315,1316,1333,1345,1371],{},[404,1317,1318,1323,1328],{},[422,1319,1320],{},[374,1321,1322],{},"POSTGRESQL_ENABLED",[422,1324,1325],{},[374,1326,1327],{},"true",[422,1329,1112,1330,1332],{},[374,1331,1118],{}," to use an external DB (disables the bundled Postgres pod)",[404,1334,1335,1340,1342],{},[422,1336,1337],{},[374,1338,1339],{},"DATABASE_URL",[422,1341,1208],{},[422,1343,1344],{},"Connection string when using external Postgres",[404,1346,1347,1352,1357],{},[422,1348,1349],{},[374,1350,1351],{},"DATABASE_SSL_MODE",[422,1353,1354],{},[374,1355,1356],{},"prefer",[422,1358,1359,1362,1363,1362,1365,1362,1368],{},[374,1360,1361],{},"disable",", ",[374,1364,1356],{},[374,1366,1367],{},"require",[374,1369,1370],{},"verify-full",[404,1372,1373,1378,1380],{},[422,1374,1375],{},[374,1376,1377],{},"DATABASE_CA_CERT",[422,1379,1208],{},[422,1381,1382],{},"CA cert PEM for SSL verification",[554,1384,1386],{"id":1385},"autoscaling","Autoscaling",[398,1388,1389,1397],{},[401,1390,1391],{},[404,1392,1393,1395],{},[407,1394,1009],{},[407,1396,1194],{},[417,1398,1399,1411,1422,1433,1445,1457],{},[404,1400,1401,1406],{},[422,1402,1403],{},[374,1404,1405],{},"REPLICA_COUNT",[422,1407,1408],{},[374,1409,1410],{},"1",[404,1412,1413,1418],{},[422,1414,1415],{},[374,1416,1417],{},"AUTOSCALE",[422,1419,1420],{},[374,1421,1327],{},[404,1423,1424,1429],{},[422,1425,1426],{},[374,1427,1428],{},"AUTOSCALE_MIN",[422,1430,1431],{},[374,1432,1410],{},[404,1434,1435,1440],{},[422,1436,1437],{},[374,1438,1439],{},"AUTOSCALE_MAX",[422,1441,1442],{},[374,1443,1444],{},"3",[404,1446,1447,1452],{},[422,1448,1449],{},[374,1450,1451],{},"AUTOSCALE_CPU_PERCENT",[422,1453,1454],{},[374,1455,1456],{},"90",[404,1458,1459,1464],{},[422,1460,1461],{},[374,1462,1463],{},"AUTOSCALE_MEMORY_PERCENT",[422,1465,1466],{},[374,1467,1456],{},[554,1469,1471],{"id":1470},"pipeline-flags","Pipeline Flags",[398,1473,1474,1485],{},[401,1475,1476],{},[404,1477,1478,1480,1482],{},[407,1479,1009],{},[407,1481,1194],{},[407,1483,1484],{},"Effect",[417,1486,1487,1502,1516,1530,1544,1558,1572],{},[404,1488,1489,1494,1499],{},[422,1490,1491],{},[374,1492,1493],{},"BUILD_DISABLED",[422,1495,1496],{},[374,1497,1498],{},"false",[422,1500,1501],{},"Skip the build stage entirely",[404,1503,1504,1509,1513],{},[422,1505,1506],{},[374,1507,1508],{},"TEST_DISABLED",[422,1510,1511],{},[374,1512,1498],{},[422,1514,1515],{},"Skip the test stage",[404,1517,1518,1523,1527],{},[422,1519,1520],{},[374,1521,1522],{},"STAGING_ENABLED",[422,1524,1525],{},[374,1526,1327],{},[422,1528,1529],{},"Auto-deploy staging on merge to main",[404,1531,1532,1537,1541],{},[422,1533,1534],{},[374,1535,1536],{},"CANARY_ENABLED",[422,1538,1539],{},[374,1540,1327],{},[422,1542,1543],{},"Show the canary job",[404,1545,1546,1551,1553],{},[422,1547,1548],{},[374,1549,1550],{},"REVIEW_DISABLED",[422,1552,1208],{},[422,1554,1112,1555,1557],{},[374,1556,704],{}," to disable review apps on branches",[404,1559,1560,1565,1569],{},[422,1561,1562],{},[374,1563,1564],{},"PERFORMANCE_DISABLED",[422,1566,1567],{},[374,1568,1498],{},[422,1570,1571],{},"Skip the sitespeed.io performance job",[404,1573,1574,1579,1583],{},[422,1575,1576],{},[374,1577,1578],{},"ENABLE_DATABASE_FIXTURES",[422,1580,1581],{},[374,1582,1498],{},[422,1584,1112,1585,1587],{},[374,1586,704],{}," to show manual fixture-load jobs in the pipeline",[365,1589,1590,1591,1593,1594,1596,1597,1600],{},"When ",[374,1592,1578],{}," is ",[374,1595,704],{}," (set as a GitLab CI\u002FCD variable or a GitHub Actions variable), a ",[369,1598,1599],{},"load fixtures"," job appears for each environment (review, staging, production) after the deploy job completes. It does not run automatically — trigger it manually from the pipeline UI when you want to seed the database. Fixtures are never loaded automatically during deployment.",[763,1602],{},[386,1604,1606],{"id":1605},"rollback","Rollback",[581,1608,1612],{"className":1609,"code":1610,"language":1611,"meta":589,"style":589},"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",[374,1613,1614,1623,1644,1651,1657,1675,1680,1686],{"__ignoreMap":589},[1615,1616,1619],"span",{"class":1617,"line":1618},"line",1,[1615,1620,1622],{"class":1621},"sTBSN","# List Helm history\n",[1615,1624,1626,1630,1634,1637,1641],{"class":1617,"line":1625},2,[1615,1627,1629],{"class":1628},"sRCss","helm",[1615,1631,1633],{"class":1632},"sLL54"," history",[1615,1635,1636],{"class":1632}," cwa",[1615,1638,1640],{"class":1639},"szhYu"," --namespace",[1615,1642,1643],{"class":1632}," production\n",[1615,1645,1647],{"class":1617,"line":1646},3,[1615,1648,1650],{"emptyLinePlaceholder":1649},true,"\n",[1615,1652,1654],{"class":1617,"line":1653},4,[1615,1655,1656],{"class":1621},"# Roll back to a specific revision\n",[1615,1658,1660,1662,1665,1667,1671,1673],{"class":1617,"line":1659},5,[1615,1661,1629],{"class":1628},[1615,1663,1664],{"class":1632}," rollback",[1615,1666,1636],{"class":1632},[1615,1668,1670],{"class":1669},"scSvc"," 3",[1615,1672,1640],{"class":1639},[1615,1674,1643],{"class":1632},[1615,1676,1678],{"class":1617,"line":1677},6,[1615,1679,1650],{"emptyLinePlaceholder":1649},[1615,1681,1683],{"class":1617,"line":1682},7,[1615,1684,1685],{"class":1621},"# Roll back to the previous release\n",[1615,1687,1689,1691,1693,1695,1697],{"class":1617,"line":1688},8,[1615,1690,1629],{"class":1628},[1615,1692,1664],{"class":1632},[1615,1694,1636],{"class":1632},[1615,1696,1640],{"class":1639},[1615,1698,1643],{"class":1632},[365,1700,1701],{},"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.",[1703,1704,1705],"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":589,"searchDepth":1625,"depth":1625,"links":1707},[1708,1709,1710,1711,1720,1724,1732],{"id":388,"depth":1625,"text":389},{"id":502,"depth":1625,"text":503},{"id":513,"depth":1625,"text":514},{"id":551,"depth":1625,"text":552,"children":1712},[1713,1714,1715,1716,1717,1718,1719],{"id":523,"depth":1646,"text":556},{"id":527,"depth":1646,"text":613},{"id":659,"depth":1646,"text":660},{"id":539,"depth":1646,"text":691},{"id":543,"depth":1646,"text":716},{"id":547,"depth":1646,"text":729},{"id":746,"depth":1646,"text":747},{"id":767,"depth":1625,"text":768,"children":1721},[1722,1723],{"id":774,"depth":1646,"text":777},{"id":831,"depth":1646,"text":610},{"id":984,"depth":1625,"text":985,"children":1725},[1726,1727,1728,1729,1730,1731],{"id":999,"depth":1646,"text":1000},{"id":1121,"depth":1646,"text":1122},{"id":1182,"depth":1646,"text":1183},{"id":1300,"depth":1646,"text":1301},{"id":1385,"depth":1646,"text":1386},{"id":1470,"depth":1646,"text":1471},{"id":1605,"depth":1625,"text":1606},"The template ships full CI\u002FCD for both GitLab CI and GitHub Actions — Docker Buildx builds, tests, per-branch review apps, and staged Kubernetes deployments via Helm.","md",{},{"title":355,"description":1733},"XzhaA21w1BZGRmJDjv1ZqHyJHY9KV6Y9HLMPJPKoa9U",[1739,360],{"title":351,"path":352,"stem":353,"description":1740,"children":-1},"Deploying the CWA stack to Kubernetes using Helm — values configuration, secrets management, migration Jobs, and rolling updates.",1782512903667]