1"use strict";(self.webpackChunkphpvms_docs=self.webpackChunkphpvms_docs||[]).push([[5957],{64023(e,s,n){n.r(s),n.d(s,{assets:()=>l,contentTitle:()=>t,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>a});const r=JSON.parse('{"id":"developers/addons/publishing","title":"Publishing to the Addon Registry","description":"The phpvms addon registry is the","source":"@site/docs/developers/addons/publishing.md","sourceDirName":"developers/addons","slug":"/developers/addons/publishing","permalink":"/8.x/developers/addons/publishing","draft":false,"unlisted":false,"editUrl":"https://github.com/phpvms/docs/tree/master/docs/developers/addons/publishing.md","tags":[],"version":"current","frontMatter":{"id":"publishing","title":"Publishing to the Addon Registry"},"sidebar":"docs","previous":{"title":"Overview","permalink":"/8.x/developers/addons/overview"},"next":{"title":"Routing & Controllers","permalink":"/8.x/developers/addons/routing-controllers"}}');var i=n(74848),d=n(28453);const o={id:"publishing",title:"Publishing to the Addon Registry"},t=void 0,l={},a=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Adding registry_id to module.json",id:"adding-registry_id-to-modulejson",level:3},{value:"What you submit",id:"what-you-submit",level:2},{value:"Naming rules",id:"naming-rules",level:3},{value:"Minimal package YAML",id:"minimal-package-yaml",level:3},{value:"Allowed category values",id:"allowed-category-values",level:3},{value:"meta.yml (first-time author)",id:"metayml-first-time-author",level:3},{value:"What CI checks at PR time",id:"what-ci-checks-at-pr-time",level:2},{value:"What happens after merge",id:"what-happens-after-merge",level:2},{value:"Updating your addon",id:"updating-your-addon",level:2},{value:"Marking an addon revoked or archived",id:"marking-an-addon-revoked-or-archived",level:2}];function c(e){const s={a:"a",code:"code",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,d.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.a,{href:"https://github.com/phpvms/addon-registry",children:"phpvms addon registry"})," is the\ncentral index of community addons. Once your addon is registered, operators can\ndiscover and install it directly from their phpvms admin UI \u2014 they never need to\nclone repos or run composer commands."]}),"\n",(0,i.jsxs)(s.p,{children:["This guide is the short reference. The\n",(0,i.jsx)(s.a,{href:"https://github.com/phpvms/addon-registry/blob/main/docs/plugin-authors.md",children:"full plugin authors guide"}),"\nin the registry repo is the authoritative source."]}),"\n",(0,i.jsx)(s.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,i.jsx)(s.p,{children:"Before submitting:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["A ",(0,i.jsx)(s.strong,{children:"public GitHub repository"})," for your addon."]}),"\n",(0,i.jsxs)(s.li,{children:["At least one ",(0,i.jsx)(s.strong,{children:"published GitHub release"})," with a zip asset attached."]}),"\n",(0,i.jsxs)(s.li,{children:["The zip contains ",(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"module.json"})," at its root"]})," (not inside a subdirectory)."]}),"\n",(0,i.jsxs)(s.li,{children:["The zip's ",(0,i.jsx)(s.code,{children:"module.json"})," declares a ",(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"registry_id"})})," equal to your registry\nname (for example, ",(0,i.jsx)(s.code,{children:"acme/reports"}),"). See\n",(0,i.jsxs)(s.a,{href:"#adding-registry_id-to-modulejson",children:["Adding ",(0,i.jsx)(s.code,{children:"registry_id"})," to module.json"]}),"\nbelow."]}),"\n",(0,i.jsxs)(s.li,{children:["All migrations under ",(0,i.jsx)(s.code,{children:"database/migrations/"})," follow the\n",(0,i.jsx)(s.a,{href:"/8.x/developers/addons/database#migration-rules",children:"migration rules"}),"."]}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"adding-registry_id-to-modulejson",children:"Adding registry_id to module.json"}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"alias"})," field in ",(0,i.jsx)(s.code,{children:"module.json"})," is restricted to lowercase identifiers\n(letters, digits, underscores) because Laravel uses it as the namespace for\nviews, configs, and translations \u2014 for example ",(0,i.jsx)(s.code,{children:"view('reports::index')"}
1),".\nRegistry names use the ",(0,i.jsx)(s.code,{children:"{author}/{package}"})," form, which is not a legal alias."]}),"\n",(0,i.jsxs)(s.p,{children:["Add a separate ",(0,i.jsx)(s.code,{children:"registry_id"})," field to ",(0,i.jsx)(s.code,{children:"module.json"})," so the registry can match\nyour addon to its YAML entry:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",children:'{\n "name": "Reports",\n "alias": "reports",\n // highlight-next-line\n "registry_id": "acme/reports",\n "description": "KPIs, route performance, scheduled exports.",\n "keywords": ["reports", "analytics"],\n "priority": 0,\n "providers": [\n "Modules\\\\Reports\\\\Providers\\\\ReportsServiceProvider"\n ],\n "files": []\n}\n'})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"name"})," \u2014 display name. Free-form."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"alias"})," \u2014 Laravel namespace. Lowercase, no slashes."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"registry_id"})," \u2014 ",(0,i.jsx)(s.strong,{children:"must"})," equal the ",(0,i.jsx)(s.code,{children:"name:"}
1)," value in your\n",(0,i.jsx)(s.code,{children:"packages/{author}/{name}.yml"}),"."]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"what-you-submit",children:"What you submit"}),"\n",(0,i.jsxs)(s.p,{children:["A ",(0,i.jsx)(s.strong,{children:"single YAML file"})," at ",(0,i.jsx)(s.code,{children:"packages/{author}/{name}.yml"})," describing your addon.\nYou do ",(0,i.jsx)(s.strong,{children:"not"})," submit a ",(0,i.jsx)(s.code,{children:"release:"})," block \u2014 the registry's bot queries your\nGitHub releases and adds it after merge."]}),"\n",(0,i.jsxs)(s.p,{children:["If your namespace doesn't exist yet, also submit ",(0,i.jsx)(s.code,{children:"packages/{author}/meta.yml"})," in\nthe same PR."]}),"\n",(0,i.jsx)(s.h3,{id:"naming-rules",children:"Naming rules"}),"\n",(0,i.jsxs)(s.p,{children:["Registry names use the form ",(0,i.jsx)(s.code,{children:"{author}/{package}"}),":"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"Lowercase letters, digits, and hyphens only."}),"\n",(0,i.jsx)(s.li,{children:"Each segment is at least two characters."}),"\n",(0,i.jsx)(s.li,{children:"No underscores, no uppercase, no periods."}),"\n",(0,i.jsxs)(s.li,{children:["The slug ",(0,i.jsx)(s.code,{children:"meta"})," is reserved (it denotes namespace metadata)."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Conventions"})," (recommended, not enforced):"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.code,{children:"{author}"})," segment should match your ",(0,i.jsx)(s.strong,{children:"GitHub username or organisation"}),".\nThis keeps ownership unambiguous and makes the source repo easy to find."]}),"\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.code,{children:"{package}"})," segment should match the ",(0,i.jsx)(s.strong,{children:"GitHub repository name"})," of the\naddon source. So an addon hosted at ",(0,i.jsx)(s.code,{children:"https://github.com/acme/reports-addon"})," is\nbest registered as ",(0,i.jsx)(s.code,{children:"acme/reports-addon"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["Examples that follow the convention: ",(0,i.jsx)(s.code,{children:"acme/reports"}),", ",(0,i.jsx)(s.code,{children:"phpvms/core-tools"}),",\n",(0,i.jsx)(s.code,{children:"crew-tools/dispatch"}),"."]}),"\n",(0,i.jsx)(s.h3,{id:"minimal-package-yaml",children:"Minimal package YAML"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",children:"# packages/acme/reports.yml\nname: acme/reports\ndescription:\n Reports addon for phpvms \u2014 KPIs, route performance, scheduled exports.\ncategory: reporting\nlicense: MIT\nkeywords:\n - reports\n - analytics\n - dashboard\nsource:\n type: github-release\n repository: acme/reports-addon\nrequirements:\n php: '>=8.3'\n phpvms: '>=7.0.0'\n"})}),"\n",(0,i.jsxs)(s.p,{children:["That's the entire submission. The bot resolves the ",(0,i.jsx)(s.code,{children:"release:"})," block after merge."]}),"\n",(0,i.jsx)(s.h3,{id:"allowed-category-values",children:"Allowed category values"}),"\n",(0,i.jsxs)(s.p,{children:["Pick exactly one. The current list lives in\n",(0,i.jsx)(s.a,{href:"https://github.com/phpvms/addon-registry/blob/main/schema/categories.yml",children:(0,i.jsx)(s.code,{children:"schema/categories.yml"})}),":"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"accounting"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"communications"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"crew"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"dev-tools"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"integration"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"operations"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"pireps"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"reporting"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"scheduling"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"templates"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"ui"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"widget"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"other"})}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["To request a new category, open a separate PR adding it to that file ",(0,i.jsx)(s.strong,{children:"before"}),"\nsubmitting your package YAML."]}),"\n",(0,i.jsx)(s.h3,{id:"metayml-first-time-author",children:"meta.yml (first-time author)"}),"\n",(0,i.jsxs)(s.p,{children:["If your namespace is new (no other addons under ",(0,i.jsx)(s.code,{children:"packages/{author}/"}),"), include\n",(0,i.jsx)(s.code,{children:"packages/{author}/meta.yml"})," in the same PR:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",children:"# packages/acme/meta.yml\nname: Acme Corp\nurl: https://acme.example.com\nmaintainers:\n - acme-dev\n - jdoe\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"maintainers"})," is a list of GitHub usernames. The first listed is treated as the\nprimary contact."]}),"\n",(0,i.jsx)(s.h2,{id:"what-ci-checks-at-pr-time",children:"What CI checks at PR time"}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Schema"})," \u2014 required fields, valid ",(0,i.jsx)(s.code,{children:"name"})," regex, allowed category,\nrequirements present."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Filename matches name"})," \u2014 ",(0,i.jsx)(s.code,{children:"acme/reports"})," lives at\n",(0,i.jsx)(s.code,{children:"packages/acme/reports.yml"}),", no exceptions."]}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.strong,{children:"Source repo exists and is public."})}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Latest release"})," \u2014 at least one published release with a zip asset."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Zip integrity"})," \u2014 downloadable, contains ",(0,i.jsx)(s.code,{children:"module.json"})," at the root, no\nforbidden paths (",(0,i.jsx)(s.code,{children:".git/"}),", ",(0,i.jsx)(s.code,{children:".github/"}),", ",(0,i.jsx)(s.code,{children:"tests/"}),", ",(0,i.jsx)(s.code,{children:"node_modules/"}),", ",(0,i.jsx)(s.code,{children:".idea/"}),",\n",(0,i.jsx)(s.code,{children:".vscode/"}),", ",(0,i.jsx)(s.code,{children:".DS_Store"}),", ",(0,i.jsx)(s.code,{children:"Tests/"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"module.json"})," \u2014 schema-valid; ",(0,i.jsx)(s.code,{children:"registry_id"}
1)," equals the registry name."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Migration lint"})," \u2014 see ",(0,i.jsx)(s.a,{href:"/8.x/developers/addons/database#migration-rules",children:"Migration rules"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["The validator posts a single comment summarising results. If everything passes,\nthe comment includes the proposed ",(0,i.jsx)(s.code,{children:"release:"})," block."]}),"\n",(0,i.jsx)(s.h2,{id:"what-happens-after-merge",children:"What happens after merge"}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.code,{children:"release-block"})," workflow opens an auto-merging bot PR appending the\nresolved ",(0,i.jsx)(s.code,{children:"release:"})," block to your YAML."]}),"\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.code,{children:"publish"})," workflow builds ",(0,i.jsx)(s.code,{children:"raw/packages.json"})," and ",(0,i.jsx)(s.code,{children:"raw/keywords.json"}),",\nuploads them to R2, and refreshes the worker's edge cache."]}),"\n",(0,i.jsx)(s.li,{children:"Hosts polling the read API see your addon within a few minutes."}),"\n",(0,i.jsx)(s.li,{children:"Subsequent releases on your repo are picked up by the discovery sweep (cron\nevery 6h, plus push triggers from the worker)."}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"updating-your-addon",children:"Updating your addon"}),"\n",(0,i.jsxs)(s.p,{children:["Tag a new release on your GitHub repo. The discovery sweep opens an auto-merging\n",(0,i.jsx)(s.code,{children:"bot/bump-{author}-{name}-{version}"})," PR within hours."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"You do not interact with the registry repository for routine updates."})}),"\n",(0,i.jsx)(s.h2,{id:"marking-an-addon-revoked-or-archived",children:"Marking an addon revoked or archived"}),"\n",(0,i.jsxs)(s.p,{children:["Revocation and archival are maintainer actions, not author actions. See\n",(0,i.jsx)(s.a,{href:"https://github.com/phpvms/addon-registry/blob/main/docs/revocation.md",children:(0,i.jsx)(s.code,{children:"revocation.md"})}),"\nin the registry repo for the process."]})]})}function h(e={}){const{wrapper:s}={...(0,d.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},28453(e,s,n){n.d(s,{R:()=>o,x:()=>t});var r=n(96540);const i={},d=r.createContext(i);function o(e){const s=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function t(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(d.Provider,{value:s},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.