[{"data":1,"prerenderedAt":2202},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Fmigrations-and-backups":573,"\u002Fdeployment\u002Fmigrations-and-backups-surround":2197},[4,22,64,89,223,511],{"title":5,"path":6,"stem":7,"children":8},"Introduction","\u002Fgetting-started","1.getting-started\u002F1.index",[9,10,14,18],{"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},"Key Ideas","\u002Fgetting-started\u002Fkey-ideas","1.getting-started\u002F3.key-ideas",{"title":19,"path":20,"stem":21},"Releases","\u002Fgetting-started\u002Freleases","1.getting-started\u002F4.releases",{"title":23,"path":24,"stem":25,"children":26},"Overview","\u002Fcore-concepts","2.core-concepts\u002F0.index",[27,28,32,36,40,44,48,52,56,60],{"title":23,"path":24,"stem":25},{"title":29,"path":30,"stem":31},"How It All Works","\u002Fcore-concepts\u002Farchitecture","2.core-concepts\u002F1.architecture",{"title":33,"path":34,"stem":35},"The Data Model","\u002Fcore-concepts\u002Fthe-data-model","2.core-concepts\u002F2.the-data-model",{"title":37,"path":38,"stem":39},"Layouts & Pages","\u002Fcore-concepts\u002Flayouts-and-pages","2.core-concepts\u002F3.layouts-and-pages",{"title":41,"path":42,"stem":43},"Dynamic Pages","\u002Fcore-concepts\u002Fdynamic-pages","2.core-concepts\u002F4.dynamic-pages",{"title":45,"path":46,"stem":47},"Components","\u002Fcore-concepts\u002Fcomponents","2.core-concepts\u002F5.components",{"title":49,"path":50,"stem":51},"Draft & Publish Workflow","\u002Fcore-concepts\u002Fdraft-and-publish","2.core-concepts\u002F6.draft-and-publish",{"title":53,"path":54,"stem":55},"The Admin Panel","\u002Fcore-concepts\u002Fadmin-panel","2.core-concepts\u002F7.admin-panel",{"title":57,"path":58,"stem":59},"Caching","\u002Fcore-concepts\u002Fcaching","2.core-concepts\u002F8.caching",{"title":61,"path":62,"stem":63},"Glossary","\u002Fcore-concepts\u002Fglossary","2.core-concepts\u002F9.glossary",{"title":23,"path":65,"stem":66,"children":67},"\u002Fguides","3.guides\u002F0.index",[68,69,73,77,81,85],{"title":23,"path":65,"stem":66},{"title":70,"path":71,"stem":72},"Your First Layout","\u002Fguides\u002Fyour-first-layout","3.guides\u002F1.your-first-layout",{"title":74,"path":75,"stem":76},"Your First Page Template","\u002Fguides\u002Fyour-first-page-template","3.guides\u002F2.your-first-page-template",{"title":78,"path":79,"stem":80},"Your First Component","\u002Fguides\u002Fyour-first-component","3.guides\u002F3.your-first-component",{"title":82,"path":83,"stem":84},"Alternative UI Variants","\u002Fguides\u002Falternative-ui-variants","3.guides\u002F4.alternative-ui-variants",{"title":86,"path":87,"stem":88},"Your First Dynamic Page","\u002Fguides\u002Fyour-first-dynamic-page","3.guides\u002F5.your-first-dynamic-page",{"title":23,"path":90,"stem":91,"children":92},"\u002Fapi","4.api\u002F0.index",[93,94,98,102,120,166,170,174,203,207,211,215,219],{"title":23,"path":90,"stem":91},{"title":95,"path":96,"stem":97},"Bundle Setup","\u002Fapi\u002Fbundle-setup","4.api\u002F02.bundle-setup",{"title":99,"path":100,"stem":101},"Configuration Reference","\u002Fapi\u002Fconfiguration","4.api\u002F03.configuration",{"title":103,"path":104,"stem":105,"children":106,"page":119},"Users","\u002Fapi\u002Fusers","4.api\u002F04.users",[107,111,115],{"title":108,"path":109,"stem":110},"Authentication","\u002Fapi\u002Fusers\u002Fauthentication","4.api\u002F04.users\u002F1.authentication",{"title":112,"path":113,"stem":114},"User Emails","\u002Fapi\u002Fusers\u002Fuser-emails","4.api\u002F04.users\u002F2.user-emails",{"title":116,"path":117,"stem":118},"Access Control","\u002Fapi\u002Fusers\u002Faccess-control","4.api\u002F04.users\u002F3.access-control",false,{"title":45,"path":121,"stem":122,"children":123,"page":119},"\u002Fapi\u002Fcomponents","4.api\u002F05.components",[124,128,149,162],{"title":125,"path":126,"stem":127},"Creating Components","\u002Fapi\u002Fcomponents\u002Fcreating-components","4.api\u002F05.components\u002F1.creating-components",{"title":129,"path":130,"stem":131,"children":132,"page":119},"Annotations","\u002Fapi\u002Fcomponents\u002Fannotations","4.api\u002F05.components\u002F2.annotations",[133,137,141,145],{"title":134,"path":135,"stem":136},"Publishable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fpublishable","4.api\u002F05.components\u002F2.annotations\u002F1.publishable",{"title":138,"path":139,"stem":140},"Uploadable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fuploadable","4.api\u002F05.components\u002F2.annotations\u002F2.uploadable",{"title":142,"path":143,"stem":144},"Timestamped","\u002Fapi\u002Fcomponents\u002Fannotations\u002Ftimestamped","4.api\u002F05.components\u002F2.annotations\u002F3.timestamped",{"title":146,"path":147,"stem":148},"Explicit Allow Only","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fexplicit-allow-only","4.api\u002F05.components\u002F2.annotations\u002F4.explicit-allow-only",{"title":150,"path":151,"stem":152,"children":153,"page":119},"Built Ins","\u002Fapi\u002Fcomponents\u002Fbuilt-ins","4.api\u002F05.components\u002F3.built-ins",[154,158],{"title":155,"path":156,"stem":157},"Collection Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fcollection-component","4.api\u002F05.components\u002F3.built-ins\u002F1.collection-component",{"title":159,"path":160,"stem":161},"Form Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fform-component","4.api\u002F05.components\u002F3.built-ins\u002F2.form-component",{"title":163,"path":164,"stem":165},"Groups & Positions","\u002Fapi\u002Fcomponents\u002Fcomponent-groups","4.api\u002F05.components\u002F4.component-groups",{"title":167,"path":168,"stem":169},"Page Data & Nested Pages","\u002Fapi\u002Fdynamic-pages","4.api\u002F06.dynamic-pages",{"title":171,"path":172,"stem":173},"Routes","\u002Fapi\u002Froutes","4.api\u002F07.routes",{"title":175,"path":176,"stem":177,"children":178,"page":119},"Data Fixtures","\u002Fapi\u002Fdata-fixtures","4.api\u002F08.data-fixtures",[179,183,187,191,195,199],{"title":180,"path":181,"stem":182},"Getting Started with Fixtures","\u002Fapi\u002Fdata-fixtures\u002Fgetting-started","4.api\u002F08.data-fixtures\u002F1.getting-started",{"title":184,"path":185,"stem":186},"Writing a Scaffold","\u002Fapi\u002Fdata-fixtures\u002Fwriting-a-scaffold","4.api\u002F08.data-fixtures\u002F2.writing-a-scaffold",{"title":188,"path":189,"stem":190},"Builder Reference","\u002Fapi\u002Fdata-fixtures\u002Fbuilder-reference","4.api\u002F08.data-fixtures\u002F3.builder-reference",{"title":192,"path":193,"stem":194},"Seeding Content","\u002Fapi\u002Fdata-fixtures\u002Fseeding-content","4.api\u002F08.data-fixtures\u002F4.seeding-content",{"title":196,"path":197,"stem":198},"Appending & Multiple Scaffolds","\u002Fapi\u002Fdata-fixtures\u002Fappending","4.api\u002F08.data-fixtures\u002F5.appending",{"title":200,"path":201,"stem":202},"Generating Fixtures","\u002Fapi\u002Fdata-fixtures\u002Fgenerating-fixtures","4.api\u002F08.data-fixtures\u002F6.generating-fixtures",{"title":204,"path":205,"stem":206},"Real-Time (Mercure)","\u002Fapi\u002Freal-time","4.api\u002F09.real-time",{"title":208,"path":209,"stem":210},"HTTP Caching","\u002Fapi\u002Fhttp-caching","4.api\u002F10.http-caching",{"title":212,"path":213,"stem":214},"Orphaned Data","\u002Fapi\u002Forphaned-data","4.api\u002F11.orphaned-data",{"title":216,"path":217,"stem":218},"Console Commands","\u002Fapi\u002Fconsole-commands","4.api\u002F12.console-commands",{"title":220,"path":221,"stem":222},"Debugging & Errors","\u002Fapi\u002Fdebugging","4.api\u002F13.debugging",{"title":23,"path":224,"stem":225,"children":226},"\u002Fnuxt-module","5.nuxt-module\u002F0.index",[227,228,232,245,265,294,315,438,472,475,503,507],{"title":23,"path":224,"stem":225},{"title":229,"path":230,"stem":231},"Module Setup","\u002Fnuxt-module\u002Fmodule-setup","5.nuxt-module\u002F01.module-setup",{"title":233,"path":234,"stem":235,"children":236,"page":119},"Configuration","\u002Fnuxt-module\u002Fconfiguration","5.nuxt-module\u002F02.configuration",[237,241],{"title":238,"path":239,"stem":240},"Nuxt Config","\u002Fnuxt-module\u002Fconfiguration\u002Fnuxt-config","5.nuxt-module\u002F02.configuration\u002F1.nuxt-config",{"title":242,"path":243,"stem":244},"Site Config & SEO","\u002Fnuxt-module\u002Fconfiguration\u002Fsite-config-and-seo","5.nuxt-module\u002F02.configuration\u002F2.site-config-and-seo",{"title":246,"path":247,"stem":248,"children":249,"page":119},"Building Your Ui","\u002Fnuxt-module\u002Fbuilding-your-ui","5.nuxt-module\u002F03.building-your-ui",[250,254,258,261],{"title":251,"path":252,"stem":253},"Layouts","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-layouts","5.nuxt-module\u002F03.building-your-ui\u002F1.creating-layouts",{"title":255,"path":256,"stem":257},"Page Templates","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-page-templates","5.nuxt-module\u002F03.building-your-ui\u002F2.creating-page-templates",{"title":45,"path":259,"stem":260},"\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-components","5.nuxt-module\u002F03.building-your-ui\u002F3.creating-components",{"title":262,"path":263,"stem":264},"CLI Generator","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcwa-cli","5.nuxt-module\u002F03.building-your-ui\u002F4.cwa-cli",{"title":266,"path":267,"stem":268,"children":269,"page":119},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F04.component-helpers",[270,274,278,282,286,290],{"title":271,"path":272,"stem":273},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F04.component-helpers\u002F1.images-and-uploads",{"title":275,"path":276,"stem":277},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F04.component-helpers\u002F2.collections-and-pagination",{"title":279,"path":280,"stem":281},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F04.component-helpers\u002F3.html-content",{"title":283,"path":284,"stem":285},"Rich-Text Editor","\u002Fnuxt-module\u002Fcomponent-helpers\u002Frich-text-editor","5.nuxt-module\u002F04.component-helpers\u002F4.rich-text-editor",{"title":287,"path":288,"stem":289},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F04.component-helpers\u002F5.real-time-updates",{"title":291,"path":292,"stem":293},"Forms","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F04.component-helpers\u002F6.forms",{"title":295,"path":296,"stem":297,"children":298,"page":119},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F05.cwa-components",[299,303,307,311],{"title":300,"path":301,"stem":302},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F05.cwa-components\u002F1.cwa-component-group",{"title":304,"path":305,"stem":306},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F05.cwa-components\u002F2.cwa-page",{"title":308,"path":309,"stem":310},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F05.cwa-components\u002F3.cwa-link",{"title":312,"path":313,"stem":314},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F05.cwa-components\u002F5.cwa-default-layout",{"title":316,"path":317,"stem":318,"children":319,"page":119},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F06.composables",[320,323,331,356,376,393,413],{"title":23,"path":321,"stem":322},"\u002Fnuxt-module\u002Fcomposables\u002Foverview","5.nuxt-module\u002F06.composables\u002F0.overview",{"title":324,"path":325,"stem":326,"children":327,"page":119},"Layout","\u002Fnuxt-module\u002Fcomposables\u002Flayout","5.nuxt-module\u002F06.composables\u002F1.layout",[328],{"title":324,"path":329,"stem":330},"\u002Fnuxt-module\u002Fcomposables\u002Flayout\u002Fuse-cwa-layout","5.nuxt-module\u002F06.composables\u002F1.layout\u002F1.use-cwa-layout",{"title":332,"path":333,"stem":334,"children":335,"page":119},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F06.composables\u002F2.component",[336,340,344,348,352],{"title":337,"path":338,"stem":339},"Component (recommended)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-component","5.nuxt-module\u002F06.composables\u002F2.component\u002F1.use-cwa-component",{"title":341,"path":342,"stem":343},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F06.composables\u002F2.component\u002F2.use-cwa-resource",{"title":345,"path":346,"stem":347},"File Field","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-file-field","5.nuxt-module\u002F06.composables\u002F2.component\u002F3.use-cwa-file-field",{"title":349,"path":350,"stem":351},"Collection Pagination","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-pagination","5.nuxt-module\u002F06.composables\u002F2.component\u002F4.use-cwa-collection-pagination",{"title":353,"path":354,"stem":355},"Collection Resource (deprecated)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F06.composables\u002F2.component\u002F9.use-cwa-collection-resource",{"title":357,"path":358,"stem":359,"children":360,"page":119},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fform","5.nuxt-module\u002F06.composables\u002F3.form",[361,364,368,372],{"title":357,"path":362,"stem":363},"\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form","5.nuxt-module\u002F06.composables\u002F3.form\u002F1.use-cwa-form",{"title":365,"path":366,"stem":367},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-input","5.nuxt-module\u002F06.composables\u002F3.form\u002F2.use-cwa-form-input",{"title":369,"path":370,"stem":371},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F06.composables\u002F3.form\u002F3.use-cwa-form-repeated",{"title":373,"path":374,"stem":375},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-collection","5.nuxt-module\u002F06.composables\u002F3.form\u002F4.use-cwa-form-collection",{"title":377,"path":378,"stem":379,"children":380,"page":119},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F06.composables\u002F4.admin-manager",[381,385,389],{"title":382,"path":383,"stem":384},"Manager Tab","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-manager-tab","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F1.use-cwa-resource-manager-tab",{"title":386,"path":387,"stem":388},"Resource Model","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-model","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F2.use-cwa-resource-model",{"title":390,"path":391,"stem":392},"Resource Upload","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-upload","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F3.use-cwa-resource-upload",{"title":394,"path":395,"stem":396,"children":397,"page":119},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F06.composables\u002F5.utilities",[398,402,406,410],{"title":399,"path":400,"stem":401},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F1.use-cwa-resource-endpoint",{"title":403,"path":404,"stem":405},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F2.use-query-bound-model",{"title":407,"path":408,"stem":409},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F3.use-cwa-resource-route",{"title":279,"path":411,"stem":412},"\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-html-content","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F4.use-html-content",{"title":414,"path":415,"stem":416,"children":417,"page":119},"Auth","\u002Fnuxt-module\u002Fcomposables\u002Fauth","5.nuxt-module\u002F06.composables\u002F6.auth",[418,422,426,430,434],{"title":419,"path":420,"stem":421},"Login","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-login","5.nuxt-module\u002F06.composables\u002F6.auth\u002F1.use-login",{"title":423,"path":424,"stem":425},"Forgot Password","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-forgot-password","5.nuxt-module\u002F06.composables\u002F6.auth\u002F2.use-forgot-password",{"title":427,"path":428,"stem":429},"Reset Password","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-reset-password","5.nuxt-module\u002F06.composables\u002F6.auth\u002F3.use-reset-password",{"title":431,"path":432,"stem":433},"Verify Email","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-verify-email","5.nuxt-module\u002F06.composables\u002F6.auth\u002F4.use-verify-email",{"title":435,"path":436,"stem":437},"Resend Verify Email","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-resend-verify-email","5.nuxt-module\u002F06.composables\u002F6.auth\u002F5.use-resend-verify-email",{"title":439,"path":440,"stem":441,"children":442,"page":119},"Cwa Api","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F07.cwa-api",[443,446,450,454,457,460,464,468],{"title":23,"path":444,"stem":445},"\u002Fnuxt-module\u002Fcwa-api\u002Foverview","5.nuxt-module\u002F07.cwa-api\u002F1.overview",{"title":447,"path":448,"stem":449},"Resources","\u002Fnuxt-module\u002Fcwa-api\u002Fresources","5.nuxt-module\u002F07.cwa-api\u002F2.resources",{"title":451,"path":452,"stem":453},"Resources Manager","\u002Fnuxt-module\u002Fcwa-api\u002Fresources-manager","5.nuxt-module\u002F07.cwa-api\u002F3.resources-manager",{"title":414,"path":455,"stem":456},"\u002Fnuxt-module\u002Fcwa-api\u002Fauth","5.nuxt-module\u002F07.cwa-api\u002F4.auth",{"title":291,"path":458,"stem":459},"\u002Fnuxt-module\u002Fcwa-api\u002Fforms","5.nuxt-module\u002F07.cwa-api\u002F5.forms",{"title":461,"path":462,"stem":463},"Admin","\u002Fnuxt-module\u002Fcwa-api\u002Fadmin","5.nuxt-module\u002F07.cwa-api\u002F6.admin",{"title":465,"path":466,"stem":467},"Site Config","\u002Fnuxt-module\u002Fcwa-api\u002Fsite-config","5.nuxt-module\u002F07.cwa-api\u002F7.site-config",{"title":469,"path":470,"stem":471},"Orphaned Resources","\u002Fnuxt-module\u002Fcwa-api\u002Forphaned-resources","5.nuxt-module\u002F07.cwa-api\u002F8.orphaned-resources",{"title":108,"path":473,"stem":474},"\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F08.authentication",{"title":476,"path":477,"stem":478,"children":479,"page":119},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[480,483,487,491,495,499],{"title":23,"path":481,"stem":482},"\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":484,"path":485,"stem":486},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":488,"path":489,"stem":490},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":492,"path":493,"stem":494},"Styling","\u002Fnuxt-module\u002Fcwa-layer\u002Fstyling","5.nuxt-module\u002F9.cwa-layer\u002F4.styling",{"title":496,"path":497,"stem":498},"Date Pickers","\u002Fnuxt-module\u002Fcwa-layer\u002Fdate-pickers","5.nuxt-module\u002F9.cwa-layer\u002F5.date-pickers",{"title":500,"path":501,"stem":502},"Maintenance","\u002Fnuxt-module\u002Fcwa-layer\u002Fmaintenance","5.nuxt-module\u002F9.cwa-layer\u002F6.maintenance",{"title":504,"path":505,"stem":506},"PWA & Offline","\u002Fnuxt-module\u002Fpwa","5.nuxt-module\u002F91.pwa",{"title":508,"path":509,"stem":510},"Page Caching","\u002Fnuxt-module\u002Fpage-caching","5.nuxt-module\u002F92.page-caching",{"title":512,"path":513,"stem":514,"children":515},"Deploying a CWA Site","\u002Fdeployment","6.deployment\u002F0.index",[516,517,521,525,529,533,537,541,545,549,553,557,561,565,569],{"title":512,"path":513,"stem":514},{"title":518,"path":519,"stem":520},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":522,"path":523,"stem":524},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":526,"path":527,"stem":528},"Scaled Mode","\u002Fdeployment\u002Fscaled-mode","6.deployment\u002F2.scaled-mode",{"title":530,"path":531,"stem":532},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"title":534,"path":535,"stem":536},"Migrations and Backups","\u002Fdeployment\u002Fmigrations-and-backups","6.deployment\u002F3.migrations-and-backups",{"title":538,"path":539,"stem":540},"CI\u002FCD Variables","\u002Fdeployment\u002Fvariables","6.deployment\u002F4.variables",{"title":542,"path":543,"stem":544},"Cloudflare","\u002Fdeployment\u002Fcloudflare","6.deployment\u002F5.cloudflare",{"title":546,"path":547,"stem":548},"Origin Protection","\u002Fdeployment\u002Forigin-protection","6.deployment\u002F5.origin-protection",{"title":550,"path":551,"stem":552},"Load Testing","\u002Fdeployment\u002Fload-testing","6.deployment\u002F6.load-testing",{"title":554,"path":555,"stem":556},"Performance Audit","\u002Fdeployment\u002Fperformance-audit","6.deployment\u002F6.performance-audit",{"title":558,"path":559,"stem":560},"Reading Lighthouse Scores","\u002Fdeployment\u002Freading-lighthouse-scores","6.deployment\u002F6.reading-lighthouse-scores",{"title":562,"path":563,"stem":564},"Browser Tests","\u002Fdeployment\u002Fbrowser-tests","6.deployment\u002F7.browser-tests",{"title":566,"path":567,"stem":568},"Faster Functional Tests","\u002Fdeployment\u002Ffaster-functional-tests","6.deployment\u002F7.faster-functional-tests",{"title":570,"path":571,"stem":572},"Troubleshooting","\u002Fdeployment\u002Ftroubleshooting","6.deployment\u002F7.troubleshooting",{"id":574,"title":534,"badge":575,"body":576,"description":2192,"extension":2193,"links":575,"meta":2194,"navigation":1300,"path":535,"seo":2195,"stem":536,"__hash__":2196},"docs\u002F6.deployment\u002F3.migrations-and-backups.md",null,{"type":577,"value":578,"toc":2176},"minimark",[579,583,634,720,728,760,782,787,798,818,826,835,838,841,880,897,912,922,925,980,989,992,998,1025,1031,1073,1083,1092,1116,1120,1123,1137,1165,1195,1214,1222,1229,1244,1390,1431,1446,1467,1476,1480,1487,1493,1542,1549,1618,1635,1645,1676,1683,1686,1689,1715,1736,1739,1745,1751,1754,1765,1773,1776,1783,1808,1816,1819,1822,1866,1869,1889,1900,1903,1917,1923,1934,1962,1999,2013,2020,2027,2031,2041,2045,2064,2088,2102,2108,2112,2127,2133,2169,2172],[580,581,582],"p",{},"A deploy normally migrates the database when the new php pod starts. That's fine for most releases, but for a migration that rewrites a large table, or one the running release can't work with, you want to see the SQL first, keep a backup, and maybe close the site while it runs.",[580,584,585,586,593,594,593,602,593,609,593,616,623,624],{},"The template can do each of these before the rollout. They're all off by default except the read-only plan, and they come from the template's deploy scripts, not the bundle (",[587,588,592],"a",{"href":589,"rel":590},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F180",[591],"nofollow","#180",", ",[587,595,598],{"href":596,"rel":597},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Febbb6ed5cc9a1fe3d1194725a75663d61501c2b4",[591],[599,600,601],"code",{},"ebbb6ed",[587,603,606],{"href":604,"rel":605},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F03cc5f580fa79121fc25b3acdf206b8e3520e110",[591],[599,607,608],{},"03cc5f5",[587,610,613],{"href":611,"rel":612},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fd8a2384f5f4b2b883bdeb42b980f8573e8db1367",[591],[599,614,615],{},"d8a2384",[587,617,620],{"href":618,"rel":619},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F6bc3ac7a4810764154b74e4a24e380abb3c461cb",[591],[599,621,622],{},"6bc3ac7","). ",[625,626,627,628,633],"em",{},"Since ",[587,629,632],{"href":630,"rel":631},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Freleases\u002Ftag\u002Fv2.0.0-alpha.43",[591],"template 2.0.0-alpha.43",".",[635,636,637,653],"table",{},[638,639,640],"thead",{},[641,642,643,647,650],"tr",{},[644,645,646],"th",{},"Step",[644,648,649],{},"Switch",[644,651,652],{},"Default",[654,655,656,673,689,704],"tbody",{},[641,657,658,665,670],{},[659,660,661],"td",{},[587,662,664],{"href":663},"#the-migrations-plan","The migrations plan",[659,666,667],{},[599,668,669],{},"CWA_CI_MIGRATIONS_PLAN_PRODUCTION",[659,671,672],{},"On, production only",[641,674,675,681,686],{},[659,676,677],{},[587,678,680],{"href":679},"#a-backup-before-migrating","A backup before migrating",[659,682,683],{},[599,684,685],{},"CWA_CI_DATABASE_BACKUP_\u003CTRACK>",[659,687,688],{},"Off",[641,690,691,697,702],{},[659,692,693],{},[587,694,696],{"href":695},"#migrating-in-a-job-before-the-rollout","Migrating in a Job before the rollout",[659,698,699],{},[599,700,701],{},"CWA_CI_MIGRATIONS_JOB_\u003CTRACK>",[659,703,688],{},[641,705,706,712,717],{},[659,707,708],{},[587,709,711],{"href":710},"#the-maintenance-window","The maintenance window",[659,713,714],{},[599,715,716],{},"CWA_CI_MAINTENANCE_DURING_MIGRATIONS",[659,718,719],{},"Off, and set for one pipeline only",[580,721,722,723,727],{},"You can also back the database up without a deploy (see ",[587,724,726],{"href":725},"#a-backup-on-demand","A backup on demand",").",[580,729,730,733,734,593,737,593,740,743,744,747,748,751,752,755,756,633],{},[599,731,732],{},"\u003CTRACK>"," is ",[599,735,736],{},"PRODUCTION",[599,738,739],{},"STAGING",[599,741,742],{},"CANARY"," or ",[599,745,746],{},"REVIEW",". The switches are named per track because on GitLab staging runs under the ",[599,749,750],{},"production"," environment, so an environment scope can't tell the two apart. Each one is on only when it's exactly ",[599,753,754],{},"\"true\"",". All the variables are listed in ",[587,757,759],{"href":758},"\u002Fdeployment\u002Fvariables#migrations-and-backups","CI\u002FCD Variables › Migrations and backups",[761,762,764],"callout",{"icon":763},"i-heroicons-information-circle",[580,765,766,770,771,774,775,778,779],{},[767,768,769],"strong",{},"These steps need PostgreSQL."," The plan makes its database session read-only with PostgreSQL's own commands, and the backup runs ",[599,772,773],{},"pg_dump",", which refuses any other ",[599,776,777],{},"DATABASE_URL"," with ",[599,780,781],{},"DATABASE_URL is not a PostgreSQL URL, so there is no backup.",[783,784,786],"h2",{"id":785},"how-the-steps-run","How the steps run",[580,788,789,790,793,794,797],{},"Every step runs in a one-off Kubernetes Job made from the chart's ",[599,791,792],{},"templates\u002Fdatabase-job.yaml",". Helm never installs that template. The deploy renders it on its own from the live release's values, with the ",[767,795,796],{},"new"," php image, so the Job connects to the release's database with the release's settings but runs the code you're about to deploy.",[580,799,800,801,804,805,808,809,813,814,817],{},"The Jobs set ",[599,802,803],{},"SKIP_MIGRATIONS=true",", so the php entrypoint doesn't migrate on start the way a pod does. The plan and the backup also run in read-only database sessions, so neither can change the database. ",[599,806,807],{},"SKIP_MIGRATIONS"," is a reserved name (see ",[587,810,812],{"href":811},"\u002Fdeployment\u002Fvariables#reserved-names","CI\u002FCD Variables › Reserved names","): ",[599,815,816],{},"CWA_API_SKIP_MIGRATIONS"," can't turn off the API's own migrations.",[580,819,820,821,825],{},"Inside a deploy, the database step runs after the checks that stop a bad deploy cold, and before Helm runs. So a deploy it stops leaves the live site as it was. The track's image tag only moves once the migrations and both rollouts have succeeded, so a failed deploy never moves it (see ",[587,822,824],{"href":823},"\u002Fdeployment\u002Fci-cd#which-build-a-deploy-runs","CI\u002FCD › Which build a deploy runs","). It does nothing unless a switch is on, and it stops after the plan when nothing is pending.",[580,827,828,831,832,633],{},[599,829,830],{},"CWA_CI_DATABASE_JOB_TIMEOUT"," (1800 seconds) is each Job's deadline. A Job that's never scheduled, or runs past it, stops the deploy with ",[599,833,834],{},"Job \u003Cname> did not finish (not scheduled, or past its deadline): kubectl describe job \u003Cname> -n \u003Cnamespace>",[783,836,664],{"id":837},"the-migrations-plan",[580,839,840],{},"The plan lists production's pending migrations and the SQL they'll run, without changing anything. Read it before you start the production deploy: starting the deploy after reading it is the approval.",[842,843,844,867],"ul",{},[845,846,847,850,851,854,855,858,859,862,863,866],"li",{},[767,848,849],{},"GitLab:"," the ",[767,852,853],{},"migrations plan production"," job, in the production stage of every ",[599,856,857],{},"main"," pipeline, after ",[599,860,861],{},"build api"," and staging. It runs by itself and may fail without blocking the deploy. ",[599,864,865],{},"CWA_CI_MIGRATIONS_PLAN_PRODUCTION=\"false\""," removes it.",[845,868,869,872,873,876,877,633],{},[767,870,871],{},"GitHub:"," run the ",[767,874,875],{},"Production"," workflow with the action ",[767,878,879],{},"migrations-plan",[580,881,882,883,886,887,890,891,893,894,896],{},"It uses the php image this pipeline built. On GitHub, where ",[599,884,885],{},"production.yml"," builds nothing, it's whatever ",[599,888,889],{},":main"," holds when it runs, as for the deploy (see ",[587,892,824],{"href":823},"), so run it again if ",[599,895,857],{}," moves on before you deploy.",[580,898,899,900,903,904,907,908,911],{},"The Job runs ",[599,901,902],{},"app:database:migrations-plan",", the template's own command. It lists the migrations with ",[599,905,906],{},"doctrine:migrations:up-to-date --list-migrations",", then prints their SQL from a dry run of ",[599,909,910],{},"doctrine:migrations:migrate --write-sql",". Doctrine prints \"Successfully migrated\" for the dry run too, but nothing ran. The job's log ends with one of:",[913,914,919],"pre",{"className":915,"code":917,"language":918},[916],"language-text","Migrations plan: the database of '\u003Crelease>' is up to date; this deploy migrates nothing.\nMigrations plan: 2 migration(s) pending for '\u003Crelease>' (PostgreSQL 16.4); their SQL is above.\nMigrations plan: release '\u003Crelease>' isn't deployed yet, so its first deploy creates the schema.\n","text",[599,920,917],{"__ignoreMap":921},"",[580,923,924],{},"A database with no migrations table yet counts every migration as pending and shows no SQL.",[761,926,929,971],{"icon":927,"color":928},"i-heroicons-exclamation-triangle","warning",[580,930,931,940,941,944,945,948,949,952,953,956,957],{},[767,932,933,934,743,937,633],{},"The plan can't show the SQL of a migration that writes data from ",[599,935,936],{},"up()",[599,938,939],{},"postUp()"," The session is read-only, so a write made through the connection, rather than with ",[599,942,943],{},"addSql()",", can't run there. The plan still lists the pending migrations, says ",[599,946,947],{},"Nothing was changed. The SQL is not shown; read the pending migrations above before deploying.",", and the deploy warns ",[599,950,951],{},"WARNING: a pending migration writes from up() or postUp(), so the plan shows no SQL: read the migrations listed above before deploying."," (on GitHub, a ",[767,954,955],{},"Migrations plan incomplete"," warning on the run). The backup, the migrate Job and the window still run. Read those migrations' code before you deploy. ",[625,958,627,959,962,963,970],{},[587,960,632],{"href":630,"rel":961},[591]," (",[587,964,967],{"href":965,"rel":966},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fe8952d5acaa65799482945ce26295987bc756eea",[591],[599,968,969],{},"e8952d5","); before, such a migration failed the plan and stopped every deploy with a switch on.",[580,972,973,974,778,977],{},"Any other error in the dry run still fails the plan. While any of the switches below is on, the deploy runs the same plan first, and ",[767,975,976],{},"a failed plan stops the deploy",[599,978,979],{},"MIGRATIONS PLAN FAILED (exit \u003Ccode>): see the log above. Nothing was changed.",[761,981,983],{"icon":982},"i-heroicons-photo",[580,984,985,988],{},[767,986,987],{},"Image placeholder:"," the migrations plan production job's log, with the pending migrations, their SQL and the closing \"migration(s) pending\" line.",[783,990,680],{"id":991},"a-backup-before-migrating",[580,993,994,997],{},[599,995,996],{},"CWA_CI_DATABASE_BACKUP_PRODUCTION=\"true\""," dumps production's database to Cloud Storage before a deploy that has migrations pending. A deploy with nothing pending takes no backup, so this is a backup before each migration, not a schedule.",[580,999,1000,1001,1004,1005,1008,1009,1012,1013,1016,1017,1020,1021,1024],{},"The backup Job runs ",[599,1002,1003],{},"pg_dump --format=custom"," in a ",[599,1006,1007],{},"postgres:\u003Cmajor>-alpine"," container, using the server's major version from the plan (16 if the plan didn't report one), and checks the dump with ",[599,1010,1011],{},"pg_restore --list",". It honours ",[599,1014,1015],{},"sslmode"," and the ",[599,1018,1019],{},"DATABASE_*_CERT"," settings. The php container then uploads it with the template's ",[599,1022,1023],{},"app:database:backup-upload"," command to:",[913,1026,1029],{"className":1027,"code":1028,"language":918},[916],"gs:\u002F\u002F\u003CCWA_CI_DATABASE_BACKUP_BUCKET>\u002F\u003Cproject folder>\u002F\u003Ctrack>\u002F\u003Crelease>\u002F\u003CYYYYMMDD-HHMMSS>Z-\u003Ccommit>.dump\n",[599,1030,1028],{"__ignoreMap":921},[580,1032,1033,1036,1037,1041,1042,733,1045,593,1047,593,1050,743,1053,1056,1057,1060,1061],{},[599,1034,1035],{},"\u003Cproject folder>"," is the project's own folder in the bucket (see ",[587,1038,1040],{"href":1039},"#the-backup-bucket","The backup bucket","), and ",[599,1043,1044],{},"\u003Ctrack>",[599,1046,750],{},[599,1048,1049],{},"staging",[599,1051,1052],{},"canary",[599,1054,1055],{},"review",". ",[599,1058,1059],{},"GCLOUD_PATH_PREFIX"," plays no part: it's for the media bucket. ",[625,1062,627,1063,962,1066,727],{},[587,1064,632],{"href":630,"rel":1065},[591],[587,1067,1070],{"href":1068,"rel":1069},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F724f51de3c16a079d4b548887699a8cf9fe1455f",[591],[599,1071,1072],{},"724f51d",[580,1074,1075,1076,1079,1080,633],{},"It keeps the newest ",[599,1077,1078],{},"CWA_CI_DATABASE_BACKUP_KEEP"," (5) in that directory and deletes older ones. Only names in exactly that form are ever deleted, so nothing else in the bucket is touched. The log prints ",[599,1081,1082],{},"Database backup: gs:\u002F\u002F…",[580,1084,1085,1088,1089],{},[767,1086,1087],{},"A failed backup stops the deploy"," before anything migrates: ",[599,1090,1091],{},"DATABASE BACKUP FAILED (exit \u003Ccode>): the deploy stopped before migrating; the live site is unchanged.",[580,1093,1094,1101,1102,1105,1106],{},[767,1095,1096,1097,1100],{},"The dump has ",[599,1098,1099],{},"CWA_CI_DATABASE_BACKUP_STORAGE"," of space in the Job, 5Gi by default."," It's the limit on the Job's scratch volume, so a dump bigger than that gets the Job evicted, and the backup fails, instead of filling the node's disk under other pods. The failure message then adds ",[599,1103,1104],{},"A dump bigger than CWA_CI_DATABASE_BACKUP_STORAGE (5Gi) gets the Job evicted: kubectl get events -n \u003Cnamespace>."," Raise it for a large database. ",[625,1107,627,1108,962,1111,727],{},[587,1109,632],{"href":630,"rel":1110},[591],[587,1112,1114],{"href":965,"rel":1113},[591],[599,1115,969],{},[1117,1118,1040],"h3",{"id":1119},"the-backup-bucket",[580,1121,1122],{},"A backup needs two variables. Without either, a deploy with a backup switch on fails before it starts any Job.",[842,1124,1125,1131],{},[845,1126,1127,1130],{},[599,1128,1129],{},"CWA_CI_DATABASE_BACKUP_BUCKET",": a private bucket of its own. Several projects can share it, each in its own folder, so set it once on a GitLab group or as a GitHub organisation variable.",[845,1132,1133,1136],{},[599,1134,1135],{},"CWA_CI_DATABASE_BACKUP_GCLOUD_JSON",": the key of a service account that can only reach this project's folder. Set it on the project, masked (on GitHub, a repository secret). It can be the key JSON base64-encoded on one line, which GitLab can mask, or the JSON itself.",[761,1138,1139],{"icon":927,"color":928},[580,1140,1141,1148,1149,1153,1154,1156,1157,1160,1161,1164],{},[767,1142,1143,1144,1147],{},"Never use the media bucket (",[599,1145,1146],{},"GCLOUD_BUCKET","), and keep the backup bucket private."," A dump holds every user's email address and password hash. The media bucket is public, and the ",[587,1150,1152],{"href":1151},"\u002Fdeployment\u002Fkubernetes#daily-orphan-scan","Kubernetes & Helm › Daily Orphan Scan"," lists everything under the media storage's root, so deleting all orphaned files would delete the dumps too. The deploy refuses ",[599,1155,1146],{},". After each upload, the command also asks Cloud Storage for the dump ",[767,1158,1159],{},"without credentials",": if that works, it deletes the dump and fails with ",[599,1162,1163],{},"Anyone can read objects in the bucket \u003Cbucket>, so the backup was deleted again. Use a private bucket (no allUsers or allAuthenticatedUsers access)."," If it gets no clear answer, it only warns.",[580,1166,1167,1173,1174,1177,1178,1188,1189,1191,1192,633],{},[767,1168,1169,1170],{},"Backups never use ",[599,1171,1172],{},"GCLOUD_JSON",", the key the API uses for media. The deploy refuses a backup key that belongs to the same service account, and the backup Job gets its key in a Secret of its own, deleted as soon as the Job finishes. The API pods never have it. The service account needs ",[767,1175,1176],{},"create, get, list and delete"," on objects in its folder, and nothing else. ",[625,1179,627,1180,962,1183,727],{},[587,1181,632],{"href":630,"rel":1182},[591],[587,1184,1186],{"href":1068,"rel":1185},[591],[599,1187,1072],{}," Before, the upload used the release's ",[599,1190,1172],{}," and wrote under ",[599,1193,1194],{},"\u003CGCLOUD_PATH_PREFIX>database\u002F\u003Crelease>\u002F",[761,1196,1197],{"icon":927,"color":928},[580,1198,1199,1202,1203,962,1206,1213],{},[767,1200,1201],{},"On GitLab, don't make the key variable Protected unless the branch that deploys is protected."," A Protected variable only reaches pipelines on protected branches, so on any other branch the job never gets it and fails with ",[599,1204,1205],{},"A database backup needs CWA_CI_DATABASE_BACKUP_GCLOUD_JSON, … A Protected variable only reaches protected branches.",[587,1207,1210],{"href":1208,"rel":1209},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F0921e60972524b5127437e38cf6019995925825a",[591],[599,1211,1212],{},"0921e60",")",[1215,1216,1218,1219],"h4",{"id":1217},"setting-up-the-bucket-with-backup-bucketsh","Setting up the bucket with ",[599,1220,1221],{},"backup-bucket.sh",[580,1223,1224,1225,1228],{},"The template's ",[599,1226,1227],{},"bin\u002Fdevops\u002Fbackup-bucket.sh"," sets up a shared bucket on Google Cloud, with one folder and one service account for each project. Run it yourself, with the Google Cloud CLI signed in as someone who can manage the Google Cloud project, never from CI. Every command can be run again safely.",[580,1230,1231,1232,1235,1236,1239,1240,1243],{},"Set ",[599,1233,1234],{},"CWA_BACKUP_GCP_PROJECT"," and ",[599,1237,1238],{},"CWA_BACKUP_BUCKET"," to your Google Cloud project and bucket name first, and ",[599,1241,1242],{},"CWA_BACKUP_LOCATION"," for the bucket's region. The script's built-in defaults aren't yours.",[913,1245,1249],{"className":1246,"code":1247,"language":1248,"meta":921,"style":921},"language-bash shiki shiki-themes github-light github-dark material-theme-palenight","export CWA_BACKUP_GCP_PROJECT=\u003Cgcp project> CWA_BACKUP_BUCKET=\u003Cbackup bucket> CWA_BACKUP_LOCATION=\u003Cregion>\n\n# Once: the bucket, and a custom role that can only create, get, list and delete objects\nbin\u002Fdevops\u002Fbackup-bucket.sh init\n\n# Once per project: its folder, its service account and a new key, printed as one base64 line\nbin\u002Fdevops\u002Fbackup-bucket.sh project \u003Cproject slug> --output backup-key.txt\n\n# Check the key works in its folder, and only there\nbin\u002Fdevops\u002Fbackup-bucket.sh verify \u003Cproject slug> backup-key.txt\n","bash",[599,1250,1251,1295,1302,1309,1319,1324,1330,1359,1364,1370],{"__ignoreMap":921},[1252,1253,1256,1260,1264,1268,1271,1274,1277,1279,1282,1284,1287,1289,1292],"span",{"class":1254,"line":1255},"line",1,[1252,1257,1259],{"class":1258},"swB56","export",[1252,1261,1263],{"class":1262},"sPB8G"," CWA_BACKUP_GCP_PROJECT",[1252,1265,1267],{"class":1266},"sVlFx","=\u003C",[1252,1269,1270],{"class":1262},"gcp project",[1252,1272,1273],{"class":1266},">",[1252,1275,1276],{"class":1262}," CWA_BACKUP_BUCKET",[1252,1278,1267],{"class":1266},[1252,1280,1281],{"class":1262},"backup bucket",[1252,1283,1273],{"class":1266},[1252,1285,1286],{"class":1262}," CWA_BACKUP_LOCATION",[1252,1288,1267],{"class":1266},[1252,1290,1291],{"class":1262},"region",[1252,1293,1294],{"class":1266},">\n",[1252,1296,1298],{"class":1254,"line":1297},2,[1252,1299,1301],{"emptyLinePlaceholder":1300},true,"\n",[1252,1303,1305],{"class":1254,"line":1304},3,[1252,1306,1308],{"class":1307},"sTBSN","# Once: the bucket, and a custom role that can only create, get, list and delete objects\n",[1252,1310,1312,1315],{"class":1254,"line":1311},4,[1252,1313,1227],{"class":1314},"sRCss",[1252,1316,1318],{"class":1317},"sLL54"," init\n",[1252,1320,1322],{"class":1254,"line":1321},5,[1252,1323,1301],{"emptyLinePlaceholder":1300},[1252,1325,1327],{"class":1254,"line":1326},6,[1252,1328,1329],{"class":1307},"# Once per project: its folder, its service account and a new key, printed as one base64 line\n",[1252,1331,1333,1335,1338,1341,1344,1347,1350,1352,1356],{"class":1254,"line":1332},7,[1252,1334,1227],{"class":1314},[1252,1336,1337],{"class":1317}," project",[1252,1339,1340],{"class":1266}," \u003C",[1252,1342,1343],{"class":1317},"project",[1252,1345,1346],{"class":1317}," slu",[1252,1348,1349],{"class":1262},"g",[1252,1351,1273],{"class":1266},[1252,1353,1355],{"class":1354},"szhYu"," --output",[1252,1357,1358],{"class":1317}," backup-key.txt\n",[1252,1360,1362],{"class":1254,"line":1361},8,[1252,1363,1301],{"emptyLinePlaceholder":1300},[1252,1365,1367],{"class":1254,"line":1366},9,[1252,1368,1369],{"class":1307},"# Check the key works in its folder, and only there\n",[1252,1371,1373,1375,1378,1380,1382,1384,1386,1388],{"class":1254,"line":1372},10,[1252,1374,1227],{"class":1314},[1252,1376,1377],{"class":1317}," verify",[1252,1379,1340],{"class":1266},[1252,1381,1343],{"class":1317},[1252,1383,1346],{"class":1317},[1252,1385,1349],{"class":1262},[1252,1387,1273],{"class":1266},[1252,1389,1358],{"class":1317},[842,1391,1392,1404,1423],{},[845,1393,1394,1399,1400,1403],{},[767,1395,1396],{},[599,1397,1398],{},"init"," creates the bucket with uniform bucket-level access and public access prevention enforced, sets a lifecycle rule that ",[767,1401,1402],{},"deletes objects after 90 days",", and creates the role. Run again, it puts those settings back.",[845,1405,1406,1411,1412,1415,1416,1418,1419,1422],{},[767,1407,1408],{},[599,1409,1410],{},"project \u003Cproject slug>"," creates a managed folder named after the slug, a service account with the role on that folder only, and a new key. It prints the key as one base64 line, or writes it to the file ",[599,1413,1414],{},"--output"," names: that's the project's ",[599,1417,1135],{},". Delete the file once the key is in CI. ",[599,1420,1421],{},"--no-key"," skips the key. If the service account already has a key, it says so: delete the old one once CI uses the new one.",[845,1424,1425,1430],{},[767,1426,1427],{},[599,1428,1429],{},"verify \u003Cproject slug> \u003Ckey file>"," uses the key to write, list and delete a test object in the folder, and checks that it can't reach the rest of the bucket and that an anonymous read is refused.",[580,1432,1433,1434,1437,1438,1441,1442,1445],{},"The slug is the folder the deploy writes to: GitLab's ",[599,1435,1436],{},"CI_PROJECT_PATH_SLUG",", or on GitHub ",[599,1439,1440],{},"owner\u002Frepo"," lower-cased with every other character a dash. If you named the folder something else, set ",[599,1443,1444],{},"CWA_CI_DATABASE_BACKUP_FOLDER"," to it.",[761,1447,1448],{"icon":763},[580,1449,1450,1461,1462,1464,1465,727],{},[767,1451,1452,1453,1455,1456,727],{},"The 90-day rule applies on top of ",[599,1454,1078],{},", and stays on purpose (",[587,1457,1460],{"href":1458,"rel":1459},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F198",[591],"components-web-app #198"," Deploy backups are meant to be few and short-lived: a deploy keeps the newest ",[599,1463,1078],{}," (5) and the bucket deletes anything older than 90 days. So a production site that hasn't migrated for 90 days has no backup left from a deploy. Before risky work, take one by hand (see ",[587,1466,726],{"href":725},[580,1468,1469,1472,1473,1475],{},[599,1470,1471],{},"CWA_CI_DATABASE_BACKUP_IMAGE"," replaces the ",[599,1474,773],{}," image, for example to use a mirror.",[1117,1477,1479],{"id":1478},"restoring-a-backup","Restoring a backup",[580,1481,1482,1483,1486],{},"A failed migration rarely needs one: the migrations run with ",[599,1484,1485],{},"--all-or-nothing",", in one transaction, so PostgreSQL rolls the whole set back. Restore when a migration succeeded but did the wrong thing.",[580,1488,1489,1492],{},[767,1490,1491],{},"Restoring replaces the database",", so it loses anything written since the backup. Download the dump first. You need the Google Cloud CLI signed in with access to the backup bucket:",[913,1494,1496],{"className":1246,"code":1495,"language":1248,"meta":921,"style":921},"gcloud storage cp gs:\u002F\u002F\u003Cbucket>\u002F\u003Cpath>.dump database.dump\n",[599,1497,1498],{"__ignoreMap":921},[1252,1499,1500,1503,1506,1509,1512,1515,1518,1521,1523,1526,1528,1531,1534,1536,1539],{"class":1254,"line":1255},[1252,1501,1502],{"class":1314},"gcloud",[1252,1504,1505],{"class":1317}," storage",[1252,1507,1508],{"class":1317}," cp",[1252,1510,1511],{"class":1317}," gs:\u002F\u002F",[1252,1513,1514],{"class":1266},"\u003C",[1252,1516,1517],{"class":1317},"bucke",[1252,1519,1520],{"class":1262},"t",[1252,1522,1273],{"class":1266},[1252,1524,1525],{"class":1317},"\u002F",[1252,1527,1514],{"class":1266},[1252,1529,1530],{"class":1317},"pat",[1252,1532,1533],{"class":1262},"h",[1252,1535,1273],{"class":1266},[1252,1537,1538],{"class":1317},".dump",[1252,1540,1541],{"class":1317}," database.dump\n",[580,1543,1544,1545,1548],{},"With the ",[767,1546,1547],{},"in-cluster PostgreSQL",", restore it inside the PostgreSQL pod:",[913,1550,1552],{"className":1246,"code":1551,"language":1248,"meta":921,"style":921},"kubectl exec -i -n \u003Cnamespace> \u003Crelease>-postgresql-0 -c postgresql -- sh -c 'PGPASSWORD=\"$POSTGRES_PASSWORD\" pg_restore --clean --if-exists --no-owner -h 127.0.0.1 -U \"${POSTGRES_USER:-postgres}\" -d \"${POSTGRES_DATABASE:-postgres}\"' \u003C database.dump\n",[599,1553,1554],{"__ignoreMap":921},[1252,1555,1556,1559,1562,1565,1568,1570,1573,1576,1578,1580,1583,1585,1587,1590,1593,1596,1599,1602,1604,1608,1611,1614,1616],{"class":1254,"line":1255},[1252,1557,1558],{"class":1314},"kubectl",[1252,1560,1561],{"class":1317}," exec",[1252,1563,1564],{"class":1354}," -i",[1252,1566,1567],{"class":1354}," -n",[1252,1569,1340],{"class":1266},[1252,1571,1572],{"class":1317},"namespac",[1252,1574,1575],{"class":1262},"e",[1252,1577,1273],{"class":1266},[1252,1579,1340],{"class":1266},[1252,1581,1582],{"class":1317},"releas",[1252,1584,1575],{"class":1262},[1252,1586,1273],{"class":1266},[1252,1588,1589],{"class":1317},"-postgresql-0",[1252,1591,1592],{"class":1354}," -c",[1252,1594,1595],{"class":1317}," postgresql",[1252,1597,1598],{"class":1354}," --",[1252,1600,1601],{"class":1317}," sh",[1252,1603,1592],{"class":1354},[1252,1605,1607],{"class":1606},"seSrl"," '",[1252,1609,1610],{"class":1317},"PGPASSWORD=\"$POSTGRES_PASSWORD\" pg_restore --clean --if-exists --no-owner -h 127.0.0.1 -U \"${POSTGRES_USER:-postgres}\" -d \"${POSTGRES_DATABASE:-postgres}\"",[1252,1612,1613],{"class":1606},"'",[1252,1615,1340],{"class":1266},[1252,1617,1541],{"class":1317},[580,1619,1620,1621,1056,1624],{},"It reads the user, password and database from the PostgreSQL container's own environment, as the chart's PostgreSQL sets them; without a user or database it uses ",[599,1622,1623],{},"postgres",[625,1625,627,1626,962,1629,1634],{},[587,1627,632],{"href":630,"rel":1628},[591],[587,1630,1632],{"href":965,"rel":1631},[591],[599,1633,969],{},"), which checks those names against the chart.",[580,1636,1637,1638,962,1641,1644],{},"With an ",[767,1639,1640],{},"external database",[599,1642,1643],{},"CWA_CI_POSTGRES_ENABLED=\"false\"","), restore it from anywhere that can reach it:",[913,1646,1648],{"className":1246,"code":1647,"language":1248,"meta":921,"style":921},"pg_restore --clean --if-exists --no-owner --dbname='postgresql:\u002F\u002F\u003Cuser>:\u003Cpassword>@\u003Chost>\u002F\u003Cdatabase>' database.dump\n",[599,1649,1650],{"__ignoreMap":921},[1252,1651,1652,1655,1658,1661,1664,1667,1669,1672,1674],{"class":1254,"line":1255},[1252,1653,1654],{"class":1314},"pg_restore",[1252,1656,1657],{"class":1354}," --clean",[1252,1659,1660],{"class":1354}," --if-exists",[1252,1662,1663],{"class":1354}," --no-owner",[1252,1665,1666],{"class":1354}," --dbname=",[1252,1668,1613],{"class":1606},[1252,1670,1671],{"class":1317},"postgresql:\u002F\u002F\u003Cuser>:\u003Cpassword>@\u003Chost>\u002F\u003Cdatabase>",[1252,1673,1613],{"class":1606},[1252,1675,1541],{"class":1317},[580,1677,1678,1679,727],{},"A failed maintenance window prints these commands with the namespace, release and backup path filled in (see ",[587,1680,1682],{"href":1681},"#when-the-window-fails","When the window fails",[1117,1684,726],{"id":1685},"a-backup-on-demand",[580,1687,1688],{},"To back production up without a deploy, for example before risky manual work, run:",[842,1690,1691,1706],{},[845,1692,1693,1695,1696,1699,1700,1702,1703,1705],{},[767,1694,849],{}," the manual ",[767,1697,1698],{},"database backup production"," job, in the production stage of ",[599,1701,857],{}," pipelines. It's there only while ",[599,1704,1129],{}," is set, and may fail without blocking anything.",[845,1707,1708,850,1710,876,1712,633],{},[767,1709,871],{},[767,1711,875],{},[767,1713,1714],{},"database-backup",[580,1716,1717,1718,1720,1721,1056,1724],{},"It runs the read-only plan first, to learn the server's PostgreSQL version, then the same backup Job as a deploy, to the same directory, with the same bucket, key and ",[599,1719,1078],{},". It backs up whether or not migrations are pending, and doesn't need ",[599,1722,1723],{},"CWA_CI_DATABASE_BACKUP_PRODUCTION",[625,1725,627,1726,962,1729,727],{},[587,1727,632],{"href":630,"rel":1728},[591],[587,1730,1733],{"href":1731,"rel":1732},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fc0a05455556e170236dcb8a529c8a24cf0546e15",[591],[599,1734,1735],{},"c0a0545",[783,1737,696],{"id":1738},"migrating-in-a-job-before-the-rollout",[580,1740,1741,1744],{},[599,1742,1743],{},"CWA_CI_MIGRATIONS_JOB_PRODUCTION=\"true\""," runs the pending migrations in a Job before Helm, instead of in the new pod's entrypoint. If they fail, the deploy stops before the rollout and the old pods keep serving:",[913,1746,1749],{"className":1747,"code":1748,"language":918},[916],"!!!! MIGRATIONS FAILED (exit \u003Ccode>): the deploy stopped before the rollout; the old pods still serve. !!!!\n",[599,1750,1748],{"__ignoreMap":921},[580,1752,1753],{},"Without it, a failed migration fails the new pod's start, and the rollout waits on a pod that never becomes ready.",[580,1755,899,1756,1759,1760,1764],{},[599,1757,1758],{},"app:database:migrate",", the same command the entrypoint runs (see ",[587,1761,1763],{"href":1762},"\u002Fdeployment\u002Fkubernetes#running-migrations","Kubernetes & Helm › Running Migrations","). The new pods still run it when they start, and find nothing left to do.",[761,1766,1767],{"icon":763},[580,1768,1769,1772],{},[767,1770,1771],{},"The old release still serves while the migration runs, and until the rollout finishes."," For an additive migration (a new table or column) that's fine. For one the running release can't work with, such as a renamed or dropped column, use the maintenance window.",[783,1774,711],{"id":1775},"the-maintenance-window",[580,1777,1778,1779,1782],{},"For a large or breaking migration, run that one production pipeline with ",[599,1780,1781],{},"CWA_CI_MAINTENANCE_DURING_MIGRATIONS=\"true\"",". Visitors get the maintenance page from before the migration until the new release is warmed. Admins bypass maintenance mode, so you can check the release before it ends.",[842,1784,1785,1796],{},[845,1786,1787,1789,1790,1792,1793,1795],{},[767,1788,849],{}," set it as a variable when you run a new pipeline on ",[599,1791,857],{},", or on the manual ",[767,1794,750],{}," job when you start it.",[845,1797,1798,1800,1801,1804,1805,1807],{},[767,1799,871],{}," tick ",[767,1802,1803],{},"maintenance_during_migrations"," when you run the ",[767,1806,875],{}," workflow. The job sets the variable from the input itself, so a repository variable of that name is never used.",[761,1809,1810],{"icon":927,"color":928},[580,1811,1812,1815],{},[767,1813,1814],{},"Never set it as a project variable."," Every production deploy with a pending migration would close the site. GitLab can't stop you, so it's up to you to set it for one pipeline only.",[580,1817,1818],{},"It applies to production only: staging, canary and review deploys print that it's ignored. It only opens a window when the plan finds migrations pending; with nothing pending, the deploy runs as usual.",[580,1820,1821],{},"With migrations pending, the production deploy:",[1823,1824,1825,1837,1840,1847,1854,1857,1863],"ol",{},[845,1826,1827,1828,1831,1832,1836],{},"Checks the live API can run ",[599,1829,1830],{},"silverback:api-components:maintenance"," (see ",[587,1833,1835],{"href":1834},"\u002Fapi\u002Fconsole-commands#maintenance","Console Commands › maintenance","), and stops if it can't.",[845,1838,1839],{},"Turns maintenance on.",[845,1841,1842,1843,733,1845,633],{},"Backs the database up, if ",[599,1844,1723],{},[599,1846,754],{},[845,1848,1849,1850,1853],{},"Migrates in a Job. A window always does, whatever ",[599,1851,1852],{},"CWA_CI_MIGRATIONS_JOB_PRODUCTION"," says.",[845,1855,1856],{},"Rolls out the new release with Helm, and clears the cached pages.",[845,1858,1859,1860,633],{},"Flushes the whole HTTP cache and warms ",[599,1861,1862],{},"\u002F_api",[845,1864,1865],{},"Turns maintenance off.",[580,1867,1868],{},"The commands run in a ready API pod, never one that's shutting down.",[761,1870,1871,1883],{"icon":927,"color":928},[580,1872,1873,1876,1877,1882],{},[767,1874,1875],{},"The live site's API image needs the maintenance command",", from api-components-bundle 2.0.0-alpha.33 (",[587,1878,1881],{"href":1879,"rel":1880},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fapi-components-bundle\u002Fissues\u002F549",[591],"api-components-bundle#549","). The check runs in the pod that's serving now, not the new image, so the first deploy that brings the bundle in can't use the window. Otherwise it stops before changing anything:",[913,1884,1887],{"className":1885,"code":1886,"language":918},[916],"❌ The live API can't run silverback:api-components:maintenance, so the window can't start: \u003Cerror>\n   Deploy once without CWA_CI_MAINTENANCE_DURING_MIGRATIONS (or with an API image that has the command).\n",[599,1888,1886],{"__ignoreMap":921},[580,1890,1891,1892,1895,1896,1899],{},"A window without a backup still runs, but warns: ",[599,1893,1894],{},"WARNING: this maintenance window takes no database backup (CWA_CI_DATABASE_BACKUP_PRODUCTION isn't \"true\"): a failed migration can only be fixed forward."," On GitHub it's also a ",[767,1897,1898],{},"No backup"," warning on the run.",[1117,1901,1682],{"id":1902},"when-the-window-fails",[580,1904,1905,1908,1909,1912,1913,1916],{},[767,1906,1907],{},"Anything that fails after maintenance is on leaves it on",", so visitors never see a half-migrated site. The job's log then ends with what to do: GitLab's ",[599,1910,1911],{},"after_script"," prints it, and on GitHub the step ",[767,1914,1915],{},"Maintenance mode is still on"," does, with an error on the run.",[913,1918,1921],{"className":1919,"code":1920,"language":918},[916],"!!!! MAINTENANCE MODE IS ON for '\u003Crelease>' from a maintenance window: visitors see the maintenance page. !!!!\nThe backup taken before migrating: gs:\u002F\u002F\u003Cbucket>\u002F\u003Cpath>.dump\nA failed migration is rolled back (one transaction), so restoring is rarely needed. To restore it (replaces the database):\n  gcloud storage cp gs:\u002F\u002F\u003Cbucket>\u002F\u003Cpath>.dump database.dump\n  kubectl exec -i -n \u003Cnamespace> \u003Crelease>-postgresql-0 -c postgresql -- sh -c '…pg_restore …' \u003C database.dump\nFix forward and deploy again: any production deploy ends the window, with or without CWA_CI_MAINTENANCE_DURING_MIGRATIONS.\nOr turn it off by hand in a Ready API pod (not one that is Terminating), then remove the window's marker:\n  kubectl get pods -n \u003Cnamespace> -l app.kubernetes.io\u002Fname=cwa,app.kubernetes.io\u002Finstance=\u003Crelease>\n  kubectl exec -n \u003Cnamespace> \u003Cready pod> -- php bin\u002Fconsole silverback:api-components:maintenance off\n  kubectl delete configmap -n \u003Cnamespace> \u003Cfullname>-maintenance-window\n",[599,1922,1920],{"__ignoreMap":921},[580,1924,1925,1926,1929,1930,1933],{},"The backup and restore lines appear only when a backup was taken, and an external database gets the ",[599,1927,1928],{},"pg_restore --dbname=…"," line from ",[587,1931,1479],{"href":1932},"#restoring-a-backup"," instead.",[580,1935,1936,1939,1940,1943,1944,1947,1948,1951,1952,962,1955,1958,1959,1961],{},[767,1937,1938],{},"The window marks itself."," Before it turns maintenance on, the deploy creates a ConfigMap, ",[599,1941,1942],{},"\u003Cfullname>-maintenance-window",", where ",[599,1945,1946],{},"\u003Cfullname>"," is the API's name: the release name plus ",[599,1949,1950],{},"-cwa",", or the release name alone if it already contains ",[599,1953,1954],{},"cwa",[599,1956,1957],{},"production-cwa-maintenance-window"," for a release named ",[599,1960,750],{},"). The deploy deletes it once maintenance is off. Helm doesn't own it, so it survives a deploy. If the deploy can't create it, the window doesn't start: the deploy fails with maintenance still off.",[580,1963,1964,1965,1970,1971,952,1974,1977,1978,1981,1982,1994,1995,1998],{},"To finish the window, fix forward and ",[767,1966,1967,1968],{},"run any production deploy, with or without ",[599,1969,716],{},". Every production deploy looks for the marker, and when it's there says ",[599,1972,1973],{},"!!!! Maintenance mode is still on from an earlier deploy's failed window: this deploy turns it off after the rollout. !!!!",[767,1975,1976],{},"Maintenance window left on"," warning), then rolls out, flushes, warms and turns maintenance off. ",[767,1979,1980],{},"Maintenance an admin turned on has no marker",", so a deploy leaves it alone. ",[625,1983,627,1984,962,1987,727],{},[587,1985,632],{"href":630,"rel":1986},[591],[587,1988,1991],{"href":1989,"rel":1990},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fabcc4a116c518c493b192c70294e21ab558be7f9",[591],[599,1992,1993],{},"abcc4a1"," Before, only a deploy with the variable still set ended the window, and the by-hand command used ",[599,1996,1997],{},"deploy\u002F\u003Cdeployment>",", which straight after a rollout can reach a pod that's stopping.",[761,2000,2001],{"icon":927,"color":928},[580,2002,2003,2009,2010,2012],{},[767,2004,2005,2006,2008],{},"A window left on by a deploy before ",[599,2007,1993],{}," has no marker,"," so a later deploy without the variable doesn't end it. Deploy with ",[599,2011,716],{}," set, which finds maintenance on with nothing pending and ends it, or turn it off by hand.",[580,2014,2015,2016,727],{},"If you turn maintenance off by hand, delete the marker too, as the notice says, or the next production deploy flushes, warms and turns maintenance off again. GitLab doesn't retry a failed deploy by itself: the production job is retried only when CI itself failed, such as a lost runner or the job's timeout (see ",[587,2017,2019],{"href":2018},"\u002Fdeployment\u002Fci-cd#production","CI\u002FCD › Production",[761,2021,2022],{"icon":982},[580,2023,2024,2026],{},[767,2025,987],{}," a production job log from a maintenance window: maintenance on, the backup path, the migrations, the rollout, the flush and warm, and \"Maintenance mode is off.\"",[783,2028,2030],{"id":2029},"staging-and-other-tracks","Staging and other tracks",[580,2032,2033,2034,593,2037,2040],{},"The backup and migrate switches work on every track (",[599,2035,2036],{},"CWA_CI_DATABASE_BACKUP_STAGING",[599,2038,2039],{},"CWA_CI_MIGRATIONS_JOB_REVIEW"," and so on), each with its own backup directory named after the track and release. The plan job and the maintenance window are production only.",[1117,2042,2044],{"id":2043},"a-track-on-productions-database","A track on production's database",[580,2046,2047,2048,962,2050,2052,2053,623,2057,2060,2061,2063],{},"Staging and canary deploy into production's namespace, and on GitLab they get production's variables, so with an external ",[599,2049,777],{},[599,2051,1643],{},") they can share production's database (see ",[587,2054,2056],{"href":2055},"\u002Fdeployment\u002Fci-cd#staging","CI\u002FCD › Staging",[767,2058,2059],{},"A staging or canary deploy never migrates production's database."," The deploy compares its ",[599,2062,777],{}," with the one in production's live release: when the host, port and database name match, whatever the credentials, then:",[842,2065,2066,2072,2075],{},[845,2067,2068,2069,2071],{},"Its pods and CronJobs get ",[599,2070,803],{},", so none of them migrates.",[845,2073,2074],{},"The deploy runs production's read-only migrations plan with the new image, instead of the track's own backup and migrate steps.",[845,2076,2077,2078,2081,2082,778,2085],{},"With migrations pending, ",[767,2079,2080],{},"staging isn't deployed",": the job succeeds with a warning, the old staging keeps running, and its cached pages aren't purged. A ",[767,2083,2084],{},"canary fails",[599,2086,2087],{},"CANARY NOT DEPLOYED: \u003CN> migration(s) pending on production's database, which a canary must not run. Deploy production instead.",[580,2089,2090,2091,2101],{},"So the production deploy runs the migrations, with its own plan, backup and window. Deploy staging again after it. ",[625,2092,627,2093,962,2096,727],{},[587,2094,632],{"href":630,"rel":2095},[591],[587,2097,2099],{"href":1068,"rel":2098},[591],[599,2100,1072],{}," Before, staging's deploy migrated production's database first, without production's backup or window.",[580,2103,2104,2107],{},[767,2105,2106],{},"The check reads production's release in the deploy's own namespace",", which is production's for staging and canary.",[1215,2109,2111],{"id":2110},"review-apps","Review apps",[580,2113,2114,2117,2118,962,2120,2122,2123,2126],{},[767,2115,2116],{},"A review app must have its own database."," A review app has its own release and, usually, its own namespace, so it can't wait for production's plan the way staging does: on production's database it would migrate it. So a review deploy with an external ",[599,2119,777],{},[599,2121,1643],{},") reads production's database URL and, when the host, port and database name match, ",[767,2124,2125],{},"is refused"," before Helm runs:",[913,2128,2131],{"className":2129,"code":2130,"language":918},[916],"!!!! REVIEW APP NOT DEPLOYED: its DATABASE_URL is production's database, which a review app would migrate. Give review apps their own database (a review-scoped DATABASE_URL, or the in-cluster PostgreSQL with CWA_CI_POSTGRES_ENABLED unset). !!!!\n",[599,2132,2130],{"__ignoreMap":921},[580,2134,2135,2136,2139,2140,2142,2143,2139,2146,2149,2150,2152,2153,2156,2157],{},"It looks production up as the release ",[599,2137,2138],{},"RELEASE_PRODUCTION"," (default ",[599,2141,750],{},") in the namespace ",[599,2144,2145],{},"KUBE_NAMESPACE_PRODUCTION",[599,2147,2148],{},"\u003Cproject name>-production",", the namespace GitLab's production job gets by default). Set either if yours differ (see ",[587,2151,759],{"href":758},"). If it can't read production's database URL there, for example because the review job has no access to that namespace, the deploy only warns, ",[599,2154,2155],{},"WARNING: could not read production's database URL (release '\u003Crelease>' in namespace '\u003Cnamespace>'; …), so this review app's external database is unchecked.",", and goes on. A review app on the in-cluster PostgreSQL, the default, isn't checked: it has its own. ",[625,2158,627,2159,962,2162,727],{},[587,2160,632],{"href":630,"rel":2161},[591],[587,2163,2166],{"href":2164,"rel":2165},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F95ce541d61e89815bcf4ea5a10fc20f6535fce34",[591],[599,2167,2168],{},"95ce541",[580,2170,2171],{},"This is GitLab's case: GitHub's review job always uses the in-cluster PostgreSQL.",[2173,2174,2175],"style",{},"html pre.shiki code .swB56, html code.shiki .swB56{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#C792EA}html pre.shiki code .sPB8G, html code.shiki .sPB8G{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#BABED8}html pre.shiki code .sVlFx, html code.shiki .sVlFx{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#89DDFF}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 .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);}html pre.shiki code .seSrl, html code.shiki .seSrl{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#89DDFF}",{"title":921,"searchDepth":1297,"depth":1297,"links":2177},[2178,2179,2180,2185,2186,2189],{"id":785,"depth":1297,"text":786},{"id":837,"depth":1297,"text":664},{"id":991,"depth":1297,"text":680,"children":2181},[2182,2183,2184],{"id":1119,"depth":1304,"text":1040},{"id":1478,"depth":1304,"text":1479},{"id":1685,"depth":1304,"text":726},{"id":1738,"depth":1297,"text":696},{"id":1775,"depth":1297,"text":711,"children":2187},[2188],{"id":1902,"depth":1304,"text":1682},{"id":2029,"depth":1297,"text":2030,"children":2190},[2191],{"id":2043,"depth":1304,"text":2044},"The template's optional database step before a deploy rolls out - a read-only plan of production's pending migrations, a backup to a private bucket, migrating in a Job before the new pods start, and a maintenance window for breaking changes.","md",{},{"title":534,"description":2192},"osYMVrxlsnkbcuDbYmS0E1e3O8B8-o74dhF4bjZd-9w",[2198,2200],{"title":530,"path":531,"stem":532,"description":2199,"children":-1},"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.",{"title":538,"path":539,"stem":540,"description":2201,"children":-1},"Every CI\u002FCD variable the template's pipeline reads, grouped by concern, with its default and the Helm value it sets. Starts with the few a new site must set.",1792522056760]