[{"data":1,"prerenderedAt":2167},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository":3},{"id":4,"title":5,"body":6,"description":2160,"extension":2161,"meta":2162,"navigation":301,"path":2163,"seo":2164,"stem":2165,"__hash__":2166},"docs\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Findex.md","Publishing to the QGIS Plugin Repository",{"type":7,"value":8,"toc":2143},"minimark",[9,13,32,39,44,84,88,91,234,238,260,318,362,377,413,436,442,509,532,536,542,549,557,618,640,644,647,673,676,978,989,993,1020,1134,1164,1168,1185,1205,1561,1574,1580,1584,1592,1681,1696,1709,1713,1728,1746,1754,1758,1786,1796,1799,1846,1852,1856,1872,1875,1906,1910,1992,1998,2002,2054,2058,2081,2090,2096,2102,2111,2115,2139],[10,11,5],"h1",{"id":12},"publishing-to-the-qgis-plugin-repository",[14,15,16,17,21,22,27,28,31],"p",{},"Writing a working plugin is only half the job. To reach the hundreds of thousands of QGIS users who install extensions through ",[18,19,20],"code",{},"Plugins → Manage and Install Plugins",", you have to package your code into a compliant ZIP archive, declare accurate metadata, and pass the automated and human review on the official repository at plugins.qgis.org. This guide sits within ",[23,24,26],"a",{"href":25},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development"," and walks through the entire release pipeline: building a clean archive, filling out ",[18,29,30],{},"metadata.txt",", choosing version numbers, declaring QGIS compatibility, creating a publisher account, and shipping updates without breaking existing installs.",[14,33,34,35,38],{},"Publishing is a one-way door in the sense that a released version is downloaded and cached by users immediately. A malformed archive or a mistaken ",[18,36,37],{},"qgisMinimumVersion"," reaches real installations within minutes of approval, so the discipline you apply here directly determines how much support churn you face later.",[40,41,43],"h2",{"id":42},"prerequisites","Prerequisites",[45,46,47,56,63,66,69],"ul",{},[48,49,50,51,55],"li",{},"A functional plugin that loads cleanly in a fresh QGIS profile (see ",[23,52,54],{"href":53},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate & Structure",").",[48,57,58,62],{},[59,60,61],"strong",{},"QGIS 3.34 LTR"," or newer installed for validation testing (bundles Python 3.12).",[48,64,65],{},"A GPL-compatible license file in the plugin root (GPLv2+ or GPLv3).",[48,67,68],{},"An OSGeo userid (account on plugins.qgis.org) for uploading.",[48,70,71,72,75,76,79,80,83],{},"Command-line access to ",[18,73,74],{},"zip",", plus optionally ",[18,77,78],{},"pb_tool"," or ",[18,81,82],{},"paver"," for repeatable builds.",[40,85,87],{"id":86},"the-release-pipeline","The Release Pipeline",[14,89,90],{},"Every successful release follows the same five stages. Treat each stage as a gate: do not advance until the previous one is clean.",[92,93,98,99,98,103,98,107,98,114,98,186,98,207,98,213],"svg",{"viewBox":94,"role":95,"ariaLabel":96,"xmlns":97},"0 0 760 200","img","QGIS plugin release pipeline from build to live","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[100,101,102],"title",{},"QGIS plugin release pipeline",[104,105,106],"desc",{},"Five stages flow left to right: build the source tree, package into a ZIP, upload to plugins.qgis.org, automated and human review, then live in the plugin manager. A dashed arrow loops from review back to package when validation fails.",[108,109],"rect",{"x":110,"y":110,"width":111,"height":112,"fill":113},"0","760","200","#f6f3ea",[115,116,119,120,119,130,119,138,119,143,119,146,119,150,119,153,119,157,119,161,119,164,119,168,119,172,119,175,119,179,119,183,98],"g",{"fill":117,"style":118},"#2f3b35","font-family:sans-serif;font-size:13px","\n    ",[108,121],{"x":122,"y":123,"width":124,"height":125,"rx":126,"fill":127,"stroke":128,"style":129},"14","60","124","64","8","#fffdf7","#0f766e","stroke-width:2.5",[131,132,137],"text",{"x":133,"y":134,"fill":135,"style":136},"76","88","#17211d","text-anchor:middle;font-weight:bold","Build",[131,139,142],{"x":133,"y":140,"style":141},"108","text-anchor:middle","compile UI\u002Fqrc",[108,144],{"x":145,"y":123,"width":124,"height":125,"rx":126,"fill":127,"stroke":128,"style":129},"166",[131,147,149],{"x":148,"y":134,"fill":135,"style":136},"228","Package",[131,151,152],{"x":148,"y":140,"style":141},"clean ZIP",[108,154],{"x":155,"y":123,"width":124,"height":125,"rx":126,"fill":127,"stroke":156,"style":129},"318","#2563eb",[131,158,160],{"x":159,"y":134,"fill":135,"style":136},"380","Upload",[131,162,163],{"x":159,"y":140,"style":141},"plugins.qgis.org",[108,165],{"x":166,"y":123,"width":124,"height":125,"rx":126,"fill":127,"stroke":167,"style":129},"470","#b45309",[131,169,171],{"x":170,"y":134,"fill":135,"style":136},"532","Review",[131,173,174],{"x":170,"y":140,"style":141},"auto + human",[108,176],{"x":177,"y":123,"width":124,"height":125,"rx":126,"fill":127,"stroke":178,"style":129},"622","#15803d",[131,180,182],{"x":181,"y":134,"fill":135,"style":136},"684","Live",[131,184,185],{"x":181,"y":140,"style":141},"in manager",[115,187,119,189,119,194,119,197,119,200,119,203,98],{"stroke":117,"fill":188,"style":129},"none",[190,191],"path",{"d":192,"style":193},"M138 92 H166","marker-end:url(#ah)",[190,195],{"d":196,"style":193},"M290 92 H318",[190,198],{"d":199,"style":193},"M442 92 H470",[190,201],{"d":202,"style":193},"M594 92 H622",[190,204],{"d":205,"stroke":167,"style":206},"M532 124 V160 H228 V124","stroke-dasharray:6 5;marker-end:url(#ahr)",[131,208,212],{"x":209,"y":210,"fill":167,"style":211},"360","178","text-anchor:middle;font-family:sans-serif;font-size:12px","validation failed → fix & repackage",[214,215,119,216,119,229,98],"defs",{},[217,218,225,226,119],"marker",{"id":219,"viewBox":220,"refX":221,"refY":222,"markerWidth":223,"markerHeight":223,"orient":224},"ah","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[190,227],{"d":228,"fill":117},"M0 0 L10 5 L0 10 z",[217,230,225,232,119],{"id":231,"viewBox":220,"refX":221,"refY":222,"markerWidth":223,"markerHeight":223,"orient":224},"ahr",[190,233],{"d":228,"fill":167},[40,235,237],{"id":236},"building-a-clean-source-tree","Building a Clean Source Tree",[14,239,240,241,244,245,248,249,252,253,255,256,259],{},"Before you can package anything, the source tree has to contain only what QGIS will run. Two artifacts commonly need compilation: Qt resource files (",[18,242,243],{},".qrc"," → ",[18,246,247],{},"resources_rc.py",") and, if you use static UI compilation, ",[18,250,251],{},".ui"," files. Most modern plugins skip static UI compilation and load ",[18,254,251],{}," files dynamically with ",[18,257,258],{},"uic.loadUiType()",", so resources are usually the only build step.",[261,262,267],"pre",{"className":263,"code":264,"language":265,"meta":266,"style":266},"language-bash shiki shiki-themes github-dark","# From the plugin root, compile the Qt resource file\npyrcc5 resources.qrc -o resources_rc.py\n\n# Optional: compile a translation if you ship .ts files\nlrelease i18n\u002Fmyplugin_de.ts\n","bash","",[18,268,269,278,296,303,309],{"__ignoreMap":266},[270,271,274],"span",{"class":272,"line":273},"line",1,[270,275,277],{"class":276},"sAwPA","# From the plugin root, compile the Qt resource file\n",[270,279,281,285,289,293],{"class":272,"line":280},2,[270,282,284],{"class":283},"svObZ","pyrcc5",[270,286,288],{"class":287},"sU2Wk"," resources.qrc",[270,290,292],{"class":291},"sDLfK"," -o",[270,294,295],{"class":287}," resources_rc.py\n",[270,297,299],{"class":272,"line":298},3,[270,300,302],{"emptyLinePlaceholder":301},true,"\n",[270,304,306],{"class":272,"line":305},4,[270,307,308],{"class":276},"# Optional: compile a translation if you ship .ts files\n",[270,310,312,315],{"class":272,"line":311},5,[270,313,314],{"class":283},"lrelease",[270,316,317],{"class":287}," i18n\u002Fmyplugin_de.ts\n",[14,319,320,323,324,326,327,330,331,333,334,337,338,341,342,344,345,347,348,351,352,355,356,358,359,361],{},[59,321,322],{},"Breakdown:"," ",[18,325,284],{}," bundles icons referenced in ",[18,328,329],{},"resources.qrc"," into an importable Python module. If your ",[18,332,30],{}," points ",[18,335,336],{},"icon="," at a real PNG on disk instead of a ",[18,339,340],{},":\u002F"," resource path, you can skip ",[18,343,243],{}," entirely. ",[18,346,314],{}," turns human-edited ",[18,349,350],{},".ts"," translation sources into binary ",[18,353,354],{},".qm"," files that QGIS loads at runtime; never ship ",[18,357,350],{}," without the compiled ",[18,360,354],{},".",[14,363,364,365,367,368,371,372,376],{},"A repeatable build is worth automating early. The ",[18,366,78],{}," utility reads a ",[18,369,370],{},"pb_tool.cfg"," manifest and handles compilation, deployment, and zipping in one command. This is the same toolchain discussed in ",[23,373,375],{"href":374},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002F","Testing & CI for QGIS Plugins",", where the build step also feeds your continuous integration pipeline.",[261,378,380],{"className":263,"code":379,"language":265,"meta":266,"style":266},"pip install pb_tool\npb_tool compile   # builds resources and UI\npb_tool zip       # produces a repository-ready archive\n",[18,381,382,393,403],{"__ignoreMap":266},[270,383,384,387,390],{"class":272,"line":273},[270,385,386],{"class":283},"pip",[270,388,389],{"class":287}," install",[270,391,392],{"class":287}," pb_tool\n",[270,394,395,397,400],{"class":272,"line":280},[270,396,78],{"class":283},[270,398,399],{"class":287}," compile",[270,401,402],{"class":276},"   # builds resources and UI\n",[270,404,405,407,410],{"class":272,"line":298},[270,406,78],{"class":283},[270,408,409],{"class":287}," zip",[270,411,412],{"class":276},"       # produces a repository-ready archive\n",[14,414,415,323,417,420,421,424,425,427,428,431,432,435],{},[59,416,322],{},[18,418,419],{},"pb_tool zip"," reads the ",[18,422,423],{},"[files]"," section of ",[18,426,370],{}," to decide exactly which files enter the archive, which is far safer than zipping a directory by hand and accidentally including ",[18,429,430],{},"__pycache__",", ",[18,433,434],{},".git",", or local test data.",[14,437,438,439,441],{},"A typical ",[18,440,370],{}," declares the plugin name, the Python sources, the extra assets, and the compiled artifacts explicitly:",[261,443,447],{"className":444,"code":445,"language":446,"meta":266,"style":266},"language-ini shiki shiki-themes github-dark","[plugin]\nname: field_tools\npackage_name: field_tools\n\n[files]\npython_files: __init__.py field_tools.py logic.py\nmain_dialog: ui\u002Fmain_dialog.ui\ncompiled_ui_files:\nresource_files: resources.qrc\nextras: metadata.txt icons LICENSE\nlocales:\n","ini",[18,448,449,454,459,464,468,473,479,485,491,497,503],{"__ignoreMap":266},[270,450,451],{"class":272,"line":273},[270,452,453],{},"[plugin]\n",[270,455,456],{"class":272,"line":280},[270,457,458],{},"name: field_tools\n",[270,460,461],{"class":272,"line":298},[270,462,463],{},"package_name: field_tools\n",[270,465,466],{"class":272,"line":305},[270,467,302],{"emptyLinePlaceholder":301},[270,469,470],{"class":272,"line":311},[270,471,472],{},"[files]\n",[270,474,476],{"class":272,"line":475},6,[270,477,478],{},"python_files: __init__.py field_tools.py logic.py\n",[270,480,482],{"class":272,"line":481},7,[270,483,484],{},"main_dialog: ui\u002Fmain_dialog.ui\n",[270,486,488],{"class":272,"line":487},8,[270,489,490],{},"compiled_ui_files:\n",[270,492,494],{"class":272,"line":493},9,[270,495,496],{},"resource_files: resources.qrc\n",[270,498,500],{"class":272,"line":499},10,[270,501,502],{},"extras: metadata.txt icons LICENSE\n",[270,504,506],{"class":272,"line":505},11,[270,507,508],{},"locales:\n",[14,510,511,513,514,517,518,520,521,524,525,527,528,531],{},[59,512,322],{}," Listing sources by name means the archive is a deliberate manifest rather than an accident of whatever sits in your working directory. The ",[18,515,516],{},"extras"," line pulls in ",[18,519,30],{},", the icon folder, and the license; ",[18,522,523],{},"resource_files"," triggers ",[18,526,284],{}," during ",[18,529,530],{},"pb_tool compile",". Keeping this file under version control gives every release an identical, reproducible build regardless of which machine runs it.",[40,533,535],{"id":534},"packaging-the-zip-correctly","Packaging the ZIP Correctly",[14,537,538,539,541],{},"The repository imposes one strict structural rule: the ZIP must contain a single top-level folder whose name matches the plugin's package name, and ",[18,540,30],{}," must live directly inside that folder. QGIS uses the folder name as the Python package name when it imports your plugin, so it must be a valid identifier (lowercase, underscores, no spaces or hyphens).",[261,543,547],{"className":544,"code":546,"language":131,"meta":266},[545],"language-text","field_tools.zip\n└── field_tools\u002F\n    ├── __init__.py\n    ├── metadata.txt\n    ├── field_tools.py\n    ├── resources_rc.py\n    ├── ui\u002F\n    │   └── main_dialog.ui\n    ├── icons\u002F\n    │   └── icon.png\n    └── LICENSE\n",[18,548,546],{"__ignoreMap":266},[14,550,551,552,556],{},"If you zip from the command line, archive the ",[553,554,555],"em",{},"parent"," of the plugin folder so the folder itself becomes the top-level entry:",[261,558,560],{"className":263,"code":559,"language":265,"meta":266,"style":266},"# Standing one level above the plugin folder\nzip -r field_tools.zip field_tools \\\n    -x \"field_tools\u002F__pycache__\u002F*\" \\\n    -x \"field_tools\u002F.git\u002F*\" \\\n    -x \"field_tools\u002Ftests\u002F*\" \\\n    -x \"*.pyc\"\n",[18,561,562,567,583,593,602,611],{"__ignoreMap":266},[270,563,564],{"class":272,"line":273},[270,565,566],{"class":276},"# Standing one level above the plugin folder\n",[270,568,569,571,574,577,580],{"class":272,"line":280},[270,570,74],{"class":283},[270,572,573],{"class":291}," -r",[270,575,576],{"class":287}," field_tools.zip",[270,578,579],{"class":287}," field_tools",[270,581,582],{"class":291}," \\\n",[270,584,585,588,591],{"class":272,"line":298},[270,586,587],{"class":291},"    -x",[270,589,590],{"class":287}," \"field_tools\u002F__pycache__\u002F*\"",[270,592,582],{"class":291},[270,594,595,597,600],{"class":272,"line":305},[270,596,587],{"class":291},[270,598,599],{"class":287}," \"field_tools\u002F.git\u002F*\"",[270,601,582],{"class":291},[270,603,604,606,609],{"class":272,"line":311},[270,605,587],{"class":291},[270,607,608],{"class":287}," \"field_tools\u002Ftests\u002F*\"",[270,610,582],{"class":291},[270,612,613,615],{"class":272,"line":475},[270,614,587],{"class":291},[270,616,617],{"class":287}," \"*.pyc\"\n",[14,619,620,622,623,626,627,431,629,632,633,636,637,639],{},[59,621,322],{}," The ",[18,624,625],{},"-x"," exclusions strip development artifacts. Bytecode caches (",[18,628,430],{},[18,630,631],{},"*.pyc",") bloat the archive and can ship stale code; ",[18,634,635],{},"tests\u002F"," is useful in your repo but should not run in users' QGIS installs. The repository rejects archives where ",[18,638,30],{}," is at the root rather than inside a single folder, and it rejects folder names containing dashes.",[40,641,643],{"id":642},"verifying-the-archive-before-upload","Verifying the Archive Before Upload",[14,645,646],{},"The cheapest bug to fix is the one you catch before uploading. Install your freshly built ZIP into a clean QGIS profile exactly as a user would, and confirm a full load\u002Funload cycle leaves no traceback. A throwaway profile guarantees you are not relying on something already present in your working setup.",[261,648,650],{"className":263,"code":649,"language":265,"meta":266,"style":266},"# Launch QGIS with a disposable profile so nothing is pre-installed\nqgis --profile release_test\n# In the GUI: Plugins → Manage and Install Plugins → Install from ZIP\n",[18,651,652,657,668],{"__ignoreMap":266},[270,653,654],{"class":272,"line":273},[270,655,656],{"class":276},"# Launch QGIS with a disposable profile so nothing is pre-installed\n",[270,658,659,662,665],{"class":272,"line":280},[270,660,661],{"class":283},"qgis",[270,663,664],{"class":291}," --profile",[270,666,667],{"class":287}," release_test\n",[270,669,670],{"class":272,"line":298},[270,671,672],{"class":276},"# In the GUI: Plugins → Manage and Install Plugins → Install from ZIP\n",[14,674,675],{},"You can also script a sanity check on the archive contents so packaging mistakes surface in CI rather than in front of a user:",[261,677,681],{"className":678,"code":679,"language":680,"meta":266,"style":266},"language-python shiki shiki-themes github-dark","import zipfile\n\nARCHIVE = \"field_tools.zip\"\nwith zipfile.ZipFile(ARCHIVE) as zf:\n    names = zf.namelist()\n\n# Exactly one top-level folder, metadata.txt directly inside it\ntops = {n.split(\"\u002F\")[0] for n in names}\nassert len(tops) == 1, f\"archive must have one top folder, found {tops}\"\nroot = tops.pop()\nassert f\"{root}\u002Fmetadata.txt\" in names, \"metadata.txt not at folder root\"\n\n# No development junk leaked in\nleaked = [n for n in names if \"__pycache__\" in n or n.endswith(\".pyc\")\n          or \"\u002F.git\u002F\" in n]\nassert not leaked, f\"remove dev artifacts: {leaked}\"\nprint(f\"{ARCHIVE} structure OK ({root})\")\n","python",[18,682,683,693,697,708,727,738,742,747,780,817,827,856,861,867,909,923,948],{"__ignoreMap":266},[270,684,685,689],{"class":272,"line":273},[270,686,688],{"class":687},"snl16","import",[270,690,692],{"class":691},"s95oV"," zipfile\n",[270,694,695],{"class":272,"line":280},[270,696,302],{"emptyLinePlaceholder":301},[270,698,699,702,705],{"class":272,"line":298},[270,700,701],{"class":291},"ARCHIVE",[270,703,704],{"class":687}," =",[270,706,707],{"class":287}," \"field_tools.zip\"\n",[270,709,710,713,716,718,721,724],{"class":272,"line":305},[270,711,712],{"class":687},"with",[270,714,715],{"class":691}," zipfile.ZipFile(",[270,717,701],{"class":291},[270,719,720],{"class":691},") ",[270,722,723],{"class":687},"as",[270,725,726],{"class":691}," zf:\n",[270,728,729,732,735],{"class":272,"line":311},[270,730,731],{"class":691},"    names ",[270,733,734],{"class":687},"=",[270,736,737],{"class":691}," zf.namelist()\n",[270,739,740],{"class":272,"line":475},[270,741,302],{"emptyLinePlaceholder":301},[270,743,744],{"class":272,"line":481},[270,745,746],{"class":276},"# Exactly one top-level folder, metadata.txt directly inside it\n",[270,748,749,752,754,757,760,763,765,768,771,774,777],{"class":272,"line":487},[270,750,751],{"class":691},"tops ",[270,753,734],{"class":687},[270,755,756],{"class":691}," {n.split(",[270,758,759],{"class":287},"\"\u002F\"",[270,761,762],{"class":691},")[",[270,764,110],{"class":291},[270,766,767],{"class":691},"] ",[270,769,770],{"class":687},"for",[270,772,773],{"class":691}," n ",[270,775,776],{"class":687},"in",[270,778,779],{"class":691}," names}\n",[270,781,782,785,788,791,794,797,799,802,805,808,811,814],{"class":272,"line":493},[270,783,784],{"class":687},"assert",[270,786,787],{"class":291}," len",[270,789,790],{"class":691},"(tops) ",[270,792,793],{"class":687},"==",[270,795,796],{"class":291}," 1",[270,798,431],{"class":691},[270,800,801],{"class":687},"f",[270,803,804],{"class":287},"\"archive must have one top folder, found ",[270,806,807],{"class":291},"{",[270,809,810],{"class":691},"tops",[270,812,813],{"class":291},"}",[270,815,816],{"class":287},"\"\n",[270,818,819,822,824],{"class":272,"line":499},[270,820,821],{"class":691},"root ",[270,823,734],{"class":687},[270,825,826],{"class":691}," tops.pop()\n",[270,828,829,831,834,837,839,842,844,847,850,853],{"class":272,"line":505},[270,830,784],{"class":687},[270,832,833],{"class":687}," f",[270,835,836],{"class":287},"\"",[270,838,807],{"class":291},[270,840,841],{"class":691},"root",[270,843,813],{"class":291},[270,845,846],{"class":287},"\u002Fmetadata.txt\"",[270,848,849],{"class":687}," in",[270,851,852],{"class":691}," names, ",[270,854,855],{"class":287},"\"metadata.txt not at folder root\"\n",[270,857,859],{"class":272,"line":858},12,[270,860,302],{"emptyLinePlaceholder":301},[270,862,864],{"class":272,"line":863},13,[270,865,866],{"class":276},"# No development junk leaked in\n",[270,868,870,873,875,878,880,882,884,887,890,893,895,897,900,903,906],{"class":272,"line":869},14,[270,871,872],{"class":691},"leaked ",[270,874,734],{"class":687},[270,876,877],{"class":691}," [n ",[270,879,770],{"class":687},[270,881,773],{"class":691},[270,883,776],{"class":687},[270,885,886],{"class":691}," names ",[270,888,889],{"class":687},"if",[270,891,892],{"class":287}," \"__pycache__\"",[270,894,849],{"class":687},[270,896,773],{"class":691},[270,898,899],{"class":687},"or",[270,901,902],{"class":691}," n.endswith(",[270,904,905],{"class":287},"\".pyc\"",[270,907,908],{"class":691},")\n",[270,910,912,915,918,920],{"class":272,"line":911},15,[270,913,914],{"class":687},"          or",[270,916,917],{"class":287}," \"\u002F.git\u002F\"",[270,919,849],{"class":687},[270,921,922],{"class":691}," n]\n",[270,924,926,928,931,934,936,939,941,944,946],{"class":272,"line":925},16,[270,927,784],{"class":687},[270,929,930],{"class":687}," not",[270,932,933],{"class":691}," leaked, ",[270,935,801],{"class":687},[270,937,938],{"class":287},"\"remove dev artifacts: ",[270,940,807],{"class":291},[270,942,943],{"class":691},"leaked",[270,945,813],{"class":291},[270,947,816],{"class":287},[270,949,951,954,957,959,961,964,967,969,971,973,976],{"class":272,"line":950},17,[270,952,953],{"class":291},"print",[270,955,956],{"class":691},"(",[270,958,801],{"class":687},[270,960,836],{"class":287},[270,962,963],{"class":291},"{ARCHIVE}",[270,965,966],{"class":287}," structure OK (",[270,968,807],{"class":291},[270,970,841],{"class":691},[270,972,813],{"class":291},[270,974,975],{"class":287},")\"",[270,977,908],{"class":691},[14,979,980,982,983,985,986,988],{},[59,981,322],{}," The script enforces the repository's two structural rules — a single top-level folder and ",[18,984,30],{}," directly inside it — plus the cleanliness rules, by inspecting the ZIP's name list without unpacking it. Running this as a CI step means a malformed archive fails the build rather than the upload, mirroring the philosophy of the ",[23,987,375],{"href":374}," pipeline.",[40,990,992],{"id":991},"filling-out-metadatatxt","Filling Out metadata.txt",[14,994,995,996,998,999,431,1002,431,1004,431,1007,431,1010,431,1013,1016,1017,361],{},"The repository parses ",[18,997,30],{}," to populate the plugin listing, enforce version compatibility, and render the changelog. A handful of fields are mandatory; the rest improve discoverability and trust. The minimum required keys are ",[18,1000,1001],{},"name",[18,1003,37],{},[18,1005,1006],{},"description",[18,1008,1009],{},"version",[18,1011,1012],{},"author",[18,1014,1015],{},"email",", and ",[18,1018,1019],{},"about",[261,1021,1023],{"className":444,"code":1022,"language":446,"meta":266,"style":266},"[general]\nname=Field Tools\nqgisMinimumVersion=3.34\nqgisMaximumVersion=3.99\ndescription=Streamlines attribute editing for field survey workflows.\nversion=1.2.0\nauthor=Jane Cartographer\nemail=jane@example.com\nabout=Field Tools adds bulk attribute editing, quick CRS checks, and\n    survey export presets to speed up post-fieldwork data cleaning.\ntracker=https:\u002F\u002Fgithub.com\u002Fjane\u002Ffield-tools\u002Fissues\nrepository=https:\u002F\u002Fgithub.com\u002Fjane\u002Ffield-tools\nhomepage=https:\u002F\u002Fgithub.com\u002Fjane\u002Ffield-tools\ncategory=Vector\ntags=attributes,editing,field,survey\nicon=icons\u002Ficon.png\nexperimental=False\ndeprecated=False\nchangelog=1.2.0 - Add CRS quick-check panel\n    1.1.0 - Bulk attribute editor\n    1.0.0 - Initial release\n",[18,1024,1025,1030,1035,1040,1045,1050,1055,1060,1065,1070,1075,1080,1085,1090,1095,1100,1105,1110,1116,1122,1128],{"__ignoreMap":266},[270,1026,1027],{"class":272,"line":273},[270,1028,1029],{},"[general]\n",[270,1031,1032],{"class":272,"line":280},[270,1033,1034],{},"name=Field Tools\n",[270,1036,1037],{"class":272,"line":298},[270,1038,1039],{},"qgisMinimumVersion=3.34\n",[270,1041,1042],{"class":272,"line":305},[270,1043,1044],{},"qgisMaximumVersion=3.99\n",[270,1046,1047],{"class":272,"line":311},[270,1048,1049],{},"description=Streamlines attribute editing for field survey workflows.\n",[270,1051,1052],{"class":272,"line":475},[270,1053,1054],{},"version=1.2.0\n",[270,1056,1057],{"class":272,"line":481},[270,1058,1059],{},"author=Jane Cartographer\n",[270,1061,1062],{"class":272,"line":487},[270,1063,1064],{},"email=jane@example.com\n",[270,1066,1067],{"class":272,"line":493},[270,1068,1069],{},"about=Field Tools adds bulk attribute editing, quick CRS checks, and\n",[270,1071,1072],{"class":272,"line":499},[270,1073,1074],{},"    survey export presets to speed up post-fieldwork data cleaning.\n",[270,1076,1077],{"class":272,"line":505},[270,1078,1079],{},"tracker=https:\u002F\u002Fgithub.com\u002Fjane\u002Ffield-tools\u002Fissues\n",[270,1081,1082],{"class":272,"line":858},[270,1083,1084],{},"repository=https:\u002F\u002Fgithub.com\u002Fjane\u002Ffield-tools\n",[270,1086,1087],{"class":272,"line":863},[270,1088,1089],{},"homepage=https:\u002F\u002Fgithub.com\u002Fjane\u002Ffield-tools\n",[270,1091,1092],{"class":272,"line":869},[270,1093,1094],{},"category=Vector\n",[270,1096,1097],{"class":272,"line":911},[270,1098,1099],{},"tags=attributes,editing,field,survey\n",[270,1101,1102],{"class":272,"line":925},[270,1103,1104],{},"icon=icons\u002Ficon.png\n",[270,1106,1107],{"class":272,"line":950},[270,1108,1109],{},"experimental=False\n",[270,1111,1113],{"class":272,"line":1112},18,[270,1114,1115],{},"deprecated=False\n",[270,1117,1119],{"class":272,"line":1118},19,[270,1120,1121],{},"changelog=1.2.0 - Add CRS quick-check panel\n",[270,1123,1125],{"class":272,"line":1124},20,[270,1126,1127],{},"    1.1.0 - Bulk attribute editor\n",[270,1129,1131],{"class":272,"line":1130},21,[270,1132,1133],{},"    1.0.0 - Initial release\n",[14,1135,1136,1138,1139,431,1141,1144,1145,1148,1149,1152,1153,1155,1156,1159,1160,361],{},[59,1137,322],{}," Multi-line values (",[18,1140,1019],{},[18,1142,1143],{},"changelog",") continue onto indented lines. ",[18,1146,1147],{},"qgisMaximumVersion"," is optional but recommended to set generously (e.g. ",[18,1150,1151],{},"3.99",") so the manager does not hide your plugin from users on newer releases. The ",[18,1154,336],{}," path is relative to the plugin folder; if you compiled resources you can instead use a Qt resource path such as ",[18,1157,1158],{},"icon=:\u002Fplugins\u002Ffield_tools\u002Ficon.png",". For an exhaustive, annotated reference to every key and its validation rules, see ",[23,1161,1163],{"href":1162},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin\u002F","Write metadata.txt for a QGIS Plugin",[40,1165,1167],{"id":1166},"semantic-versioning-and-the-changelog","Semantic Versioning and the Changelog",[14,1169,1170,1171,1173,1174,1180,1181,1184],{},"The repository treats ",[18,1172,1009],{}," as a sortable string and uses it to decide whether a user's installed copy is outdated. Adopt ",[23,1175,1179],{"href":1176,"rel":1177},"https:\u002F\u002Fsemver.org\u002F",[1178],"nofollow","semantic versioning"," — ",[18,1182,1183],{},"MAJOR.MINOR.PATCH"," — so the meaning of each release is unambiguous:",[45,1186,1187,1193,1199],{},[48,1188,1189,1192],{},[59,1190,1191],{},"MAJOR"," — incompatible changes: removed features, changed settings keys, dropped support for an older QGIS.",[48,1194,1195,1198],{},[59,1196,1197],{},"MINOR"," — new, backward-compatible functionality.",[48,1200,1201,1204],{},[59,1202,1203],{},"PATCH"," — backward-compatible bug fixes only.",[261,1206,1208],{"className":678,"code":1207,"language":680,"meta":266,"style":266},"# A tiny helper to keep metadata.txt and your code in sync at release time\nimport re\nfrom pathlib import Path\n\ndef bump_patch(metadata_path):\n    \"\"\"Increment the PATCH component in metadata.txt and return the new version.\"\"\"\n    text = Path(metadata_path).read_text(encoding=\"utf-8\")\n    match = re.search(r\"^version=(\\d+)\\.(\\d+)\\.(\\d+)$\", text, re.MULTILINE)\n    if not match:\n        raise ValueError(\"version= must be MAJOR.MINOR.PATCH\")\n    major, minor, patch = (int(g) for g in match.groups())\n    new_version = f\"{major}.{minor}.{patch + 1}\"\n    text = re.sub(r\"^version=.*$\", f\"version={new_version}\",\n                  text, count=1, flags=re.MULTILINE)\n    Path(metadata_path).write_text(text, encoding=\"utf-8\")\n    return new_version\n\nif __name__ == \"__main__\":\n    print(\"Released\", bump_patch(\"field_tools\u002Fmetadata.txt\"))\n",[18,1209,1210,1215,1222,1235,1239,1250,1255,1276,1336,1346,1361,1387,1428,1474,1501,1514,1522,1526,1542],{"__ignoreMap":266},[270,1211,1212],{"class":272,"line":273},[270,1213,1214],{"class":276},"# A tiny helper to keep metadata.txt and your code in sync at release time\n",[270,1216,1217,1219],{"class":272,"line":280},[270,1218,688],{"class":687},[270,1220,1221],{"class":691}," re\n",[270,1223,1224,1227,1230,1232],{"class":272,"line":298},[270,1225,1226],{"class":687},"from",[270,1228,1229],{"class":691}," pathlib ",[270,1231,688],{"class":687},[270,1233,1234],{"class":691}," Path\n",[270,1236,1237],{"class":272,"line":305},[270,1238,302],{"emptyLinePlaceholder":301},[270,1240,1241,1244,1247],{"class":272,"line":311},[270,1242,1243],{"class":687},"def",[270,1245,1246],{"class":283}," bump_patch",[270,1248,1249],{"class":691},"(metadata_path):\n",[270,1251,1252],{"class":272,"line":475},[270,1253,1254],{"class":287},"    \"\"\"Increment the PATCH component in metadata.txt and return the new version.\"\"\"\n",[270,1256,1257,1260,1262,1265,1269,1271,1274],{"class":272,"line":481},[270,1258,1259],{"class":691},"    text ",[270,1261,734],{"class":687},[270,1263,1264],{"class":691}," Path(metadata_path).read_text(",[270,1266,1268],{"class":1267},"s9osk","encoding",[270,1270,734],{"class":687},[270,1272,1273],{"class":287},"\"utf-8\"",[270,1275,908],{"class":691},[270,1277,1278,1281,1283,1286,1289,1291,1294,1298,1301,1304,1307,1311,1313,1315,1317,1319,1321,1323,1326,1328,1331,1334],{"class":272,"line":487},[270,1279,1280],{"class":691},"    match ",[270,1282,734],{"class":687},[270,1284,1285],{"class":691}," re.search(",[270,1287,1288],{"class":687},"r",[270,1290,836],{"class":287},[270,1292,1293],{"class":291},"^",[270,1295,1297],{"class":1296},"sns5M","version=",[270,1299,1300],{"class":291},"(\\d",[270,1302,1303],{"class":687},"+",[270,1305,1306],{"class":291},")",[270,1308,1310],{"class":1309},"sRjNt","\\.",[270,1312,1300],{"class":291},[270,1314,1303],{"class":687},[270,1316,1306],{"class":291},[270,1318,1310],{"class":1309},[270,1320,1300],{"class":291},[270,1322,1303],{"class":687},[270,1324,1325],{"class":291},")$",[270,1327,836],{"class":287},[270,1329,1330],{"class":691},", text, re.",[270,1332,1333],{"class":291},"MULTILINE",[270,1335,908],{"class":691},[270,1337,1338,1341,1343],{"class":272,"line":493},[270,1339,1340],{"class":687},"    if",[270,1342,930],{"class":687},[270,1344,1345],{"class":691}," match:\n",[270,1347,1348,1351,1354,1356,1359],{"class":272,"line":499},[270,1349,1350],{"class":687},"        raise",[270,1352,1353],{"class":291}," ValueError",[270,1355,956],{"class":691},[270,1357,1358],{"class":287},"\"version= must be MAJOR.MINOR.PATCH\"",[270,1360,908],{"class":691},[270,1362,1363,1366,1368,1371,1374,1377,1379,1382,1384],{"class":272,"line":505},[270,1364,1365],{"class":691},"    major, minor, patch ",[270,1367,734],{"class":687},[270,1369,1370],{"class":691}," (",[270,1372,1373],{"class":291},"int",[270,1375,1376],{"class":691},"(g) ",[270,1378,770],{"class":687},[270,1380,1381],{"class":691}," g ",[270,1383,776],{"class":687},[270,1385,1386],{"class":691}," match.groups())\n",[270,1388,1389,1392,1394,1396,1398,1400,1403,1405,1407,1409,1412,1414,1416,1418,1421,1423,1426],{"class":272,"line":858},[270,1390,1391],{"class":691},"    new_version ",[270,1393,734],{"class":687},[270,1395,833],{"class":687},[270,1397,836],{"class":287},[270,1399,807],{"class":291},[270,1401,1402],{"class":691},"major",[270,1404,813],{"class":291},[270,1406,361],{"class":287},[270,1408,807],{"class":291},[270,1410,1411],{"class":691},"minor",[270,1413,813],{"class":291},[270,1415,361],{"class":287},[270,1417,807],{"class":291},[270,1419,1420],{"class":691},"patch ",[270,1422,1303],{"class":687},[270,1424,1425],{"class":291}," 1}",[270,1427,816],{"class":287},[270,1429,1430,1432,1434,1437,1439,1441,1443,1445,1447,1450,1453,1455,1457,1459,1462,1464,1467,1469,1471],{"class":272,"line":863},[270,1431,1259],{"class":691},[270,1433,734],{"class":687},[270,1435,1436],{"class":691}," re.sub(",[270,1438,1288],{"class":687},[270,1440,836],{"class":287},[270,1442,1293],{"class":291},[270,1444,1297],{"class":1296},[270,1446,361],{"class":291},[270,1448,1449],{"class":687},"*",[270,1451,1452],{"class":291},"$",[270,1454,836],{"class":287},[270,1456,431],{"class":691},[270,1458,801],{"class":687},[270,1460,1461],{"class":287},"\"version=",[270,1463,807],{"class":291},[270,1465,1466],{"class":691},"new_version",[270,1468,813],{"class":291},[270,1470,836],{"class":287},[270,1472,1473],{"class":691},",\n",[270,1475,1476,1479,1482,1484,1487,1489,1492,1494,1497,1499],{"class":272,"line":869},[270,1477,1478],{"class":691},"                  text, ",[270,1480,1481],{"class":1267},"count",[270,1483,734],{"class":687},[270,1485,1486],{"class":291},"1",[270,1488,431],{"class":691},[270,1490,1491],{"class":1267},"flags",[270,1493,734],{"class":687},[270,1495,1496],{"class":691},"re.",[270,1498,1333],{"class":291},[270,1500,908],{"class":691},[270,1502,1503,1506,1508,1510,1512],{"class":272,"line":911},[270,1504,1505],{"class":691},"    Path(metadata_path).write_text(text, ",[270,1507,1268],{"class":1267},[270,1509,734],{"class":687},[270,1511,1273],{"class":287},[270,1513,908],{"class":691},[270,1515,1516,1519],{"class":272,"line":925},[270,1517,1518],{"class":687},"    return",[270,1520,1521],{"class":691}," new_version\n",[270,1523,1524],{"class":272,"line":950},[270,1525,302],{"emptyLinePlaceholder":301},[270,1527,1528,1530,1533,1536,1539],{"class":272,"line":1112},[270,1529,889],{"class":687},[270,1531,1532],{"class":291}," __name__",[270,1534,1535],{"class":687}," ==",[270,1537,1538],{"class":287}," \"__main__\"",[270,1540,1541],{"class":691},":\n",[270,1543,1544,1547,1549,1552,1555,1558],{"class":272,"line":1118},[270,1545,1546],{"class":291},"    print",[270,1548,956],{"class":691},[270,1550,1551],{"class":287},"\"Released\"",[270,1553,1554],{"class":691},", bump_patch(",[270,1556,1557],{"class":287},"\"field_tools\u002Fmetadata.txt\"",[270,1559,1560],{"class":691},"))\n",[14,1562,1563,1565,1566,1568,1569,1571,1572,361],{},[59,1564,322],{}," This script parses the ",[18,1567,1297],{}," line, increments the patch number, and writes it back. Automating the bump prevents the classic mistake of uploading a ZIP whose ",[18,1570,1009],{}," matches an already-published release, which the repository rejects. Wire a longer version of this into the release job described in ",[23,1573,375],{"href":374},[14,1575,1576,1577,1579],{},"The ",[18,1578,1143],{}," field is rendered verbatim on your plugin page and shown to users when an update is available. Keep the newest version at the top, one entry per line, and describe user-visible changes rather than internal refactors.",[40,1581,1583],{"id":1582},"qgis-compatibility-flags","QGIS Compatibility Flags",[14,1585,1586,1588,1589,1591],{},[18,1587,37],{}," and ",[18,1590,1147],{}," are the contract between your plugin and the user's QGIS. The plugin manager hides any plugin whose required range does not include the running version. Set the minimum to the oldest QGIS you actually test against, not the oldest you hope works.",[261,1593,1595],{"className":678,"code":1594,"language":680,"meta":266,"style":266},"# Guard against API differences at runtime when you support a version range\nfrom qgis.core import Qgis\n\nif Qgis.versionInt() >= 33400:        # 3.34.x and newer\n    from qgis.core import QgsVectorFileWriter\n    save_options = QgsVectorFileWriter.SaveVectorOptions()\n    # use writeAsVectorFormatV3 on modern builds\nelse:\n    # fall back to the older signature on 3.28 LTR\n    pass\n",[18,1596,1597,1602,1614,1618,1637,1649,1659,1664,1671,1676],{"__ignoreMap":266},[270,1598,1599],{"class":272,"line":273},[270,1600,1601],{"class":276},"# Guard against API differences at runtime when you support a version range\n",[270,1603,1604,1606,1609,1611],{"class":272,"line":280},[270,1605,1226],{"class":687},[270,1607,1608],{"class":691}," qgis.core ",[270,1610,688],{"class":687},[270,1612,1613],{"class":691}," Qgis\n",[270,1615,1616],{"class":272,"line":298},[270,1617,302],{"emptyLinePlaceholder":301},[270,1619,1620,1622,1625,1628,1631,1634],{"class":272,"line":305},[270,1621,889],{"class":687},[270,1623,1624],{"class":691}," Qgis.versionInt() ",[270,1626,1627],{"class":687},">=",[270,1629,1630],{"class":291}," 33400",[270,1632,1633],{"class":691},":        ",[270,1635,1636],{"class":276},"# 3.34.x and newer\n",[270,1638,1639,1642,1644,1646],{"class":272,"line":311},[270,1640,1641],{"class":687},"    from",[270,1643,1608],{"class":691},[270,1645,688],{"class":687},[270,1647,1648],{"class":691}," QgsVectorFileWriter\n",[270,1650,1651,1654,1656],{"class":272,"line":475},[270,1652,1653],{"class":691},"    save_options ",[270,1655,734],{"class":687},[270,1657,1658],{"class":691}," QgsVectorFileWriter.SaveVectorOptions()\n",[270,1660,1661],{"class":272,"line":481},[270,1662,1663],{"class":276},"    # use writeAsVectorFormatV3 on modern builds\n",[270,1665,1666,1669],{"class":272,"line":487},[270,1667,1668],{"class":687},"else",[270,1670,1541],{"class":691},[270,1672,1673],{"class":272,"line":493},[270,1674,1675],{"class":276},"    # fall back to the older signature on 3.28 LTR\n",[270,1677,1678],{"class":272,"line":499},[270,1679,1680],{"class":687},"    pass\n",[14,1682,1683,323,1685,1688,1689,1692,1693,1695],{},[59,1684,322],{},[18,1686,1687],{},"Qgis.versionInt()"," returns an integer like ",[18,1690,1691],{},"33400"," for 3.34.0, which is easy to compare. Use it to branch on API changes when your declared range spans both the 3.28 LTR (Python 3.9) and 3.34 LTR (Python 3.12) lines. If you cannot or do not want to support older builds, simply raise ",[18,1694,37],{}," instead of writing compatibility shims.",[14,1697,1698,1699,1702,1703,1705,1706,1708],{},"There is a real cost to setting the minimum too low. Every version you declare support for is a version someone will install you on and file bugs against. Declaring ",[18,1700,1701],{},"qgisMinimumVersion=3.16"," when you have only ever run the plugin on 3.34 is a promise you cannot keep. The honest figure is the oldest QGIS in your CI matrix — the testing workflow in ",[23,1704,375],{"href":374}," exists precisely so this number reflects reality rather than hope. When you do drop an old version, raise the minimum and add a ",[18,1707,1143],{}," line announcing it, because users on that build will silently stop receiving updates.",[40,1710,1712],{"id":1711},"the-experimental-and-deprecated-flags","The Experimental and Deprecated Flags",[14,1714,1715,1716,1719,1720,1723,1724,1727],{},"Two boolean flags change how the repository surfaces your plugin. ",[18,1717,1718],{},"experimental=True"," hides the version from users unless they tick ",[59,1721,1722],{},"Show also experimental plugins"," in the manager settings — use it for alpha-quality releases or risky new versions of an established plugin. ",[18,1725,1726],{},"deprecated=True"," marks the entire plugin as unmaintained and discourages new installs.",[14,1729,1730,1731,1733,1734,1737,1738,1741,1742,1745],{},"A practical pattern is to publish a new major rewrite as ",[18,1732,1718],{}," first, gather feedback, then flip it to ",[18,1735,1736],{},"False"," once stable. The flag lives per-version, so you can have a stable ",[18,1739,1740],{},"1.x"," line and an experimental ",[18,1743,1744],{},"2.0.0"," rewrite available simultaneously — cautious users stay on stable while early adopters opt into the new build.",[14,1747,1748,1750,1751,1753],{},[18,1749,1726],{}," is the graceful way to retire a plugin you no longer maintain. It does not delete anything; existing installs keep working, but the manager discourages new ones and your plugin page shows a deprecation banner. If a replacement exists, name it in the ",[18,1752,1019],{}," text so users know where to migrate. Reserve deprecation for a genuine end-of-life rather than a temporary pause in development.",[40,1755,1757],{"id":1756},"creating-an-account-and-uploading","Creating an Account and Uploading",[1759,1760,1761,1764,1770,1776,1783],"ol",{},[48,1762,1763],{},"Register an OSGeo userid at plugins.qgis.org (the same credentials work across OSGeo services).",[48,1765,1766,1767,361],{},"Sign in and choose ",[59,1768,1769],{},"Plugins → Upload a plugin",[48,1771,1772,1773,1775],{},"Select your ZIP. The site parses ",[18,1774,30],{}," immediately and reports any structural or field errors before accepting the file.",[48,1777,1778,1779,1782],{},"On first upload, the plugin enters the queue as ",[59,1780,1781],{},"unapproved",". A repository maintainer reviews it for licensing, security (no obfuscated code, no bundled binaries phoning home), and basic functionality.",[48,1784,1785],{},"Once approved, you become the plugin owner and can grant additional maintainers, manage version visibility, and upload future releases without re-review (subsequent versions of an already-approved plugin go live automatically unless flagged).",[14,1787,1788,1789,1791,1792,1795],{},"The first review is the slowest gate. You can shorten it by shipping clean, readable code, a clear ",[18,1790,1019],{},", a working ",[18,1793,1794],{},"tracker"," URL, and a recognized open-source license file.",[14,1797,1798],{},"What reviewers actually look for is predictable, and pre-empting it saves a round trip:",[45,1800,1801,1807,1821,1827,1837],{},[48,1802,1803,1806],{},[59,1804,1805],{},"No obfuscated or minified Python."," The repository hosts source, not compiled blobs; code reviewers must be able to read what they approve.",[48,1808,1809,1812,1813,1816,1817,1820],{},[59,1810,1811],{},"No bundled binaries that execute or download code at runtime."," Pure-Python dependencies vendored into the plugin folder are acceptable; shipping a compiled ",[18,1814,1815],{},".so","\u002F",[18,1818,1819],{},".dll"," or fetching executables on first run is not.",[48,1822,1823,1826],{},[59,1824,1825],{},"A working tracker and repository URL."," Dead links signal an unmaintained submission and stall approval.",[48,1828,1829,1832,1833,1836],{},[59,1830,1831],{},"A license file present in the root",", matching what you declare. A GPL header in source files alongside a ",[18,1834,1835],{},"LICENSE"," file is the conventional, fast-to-approve combination.",[48,1838,1839,1842,1843,1845],{},[59,1840,1841],{},"Sensible network behavior."," If the plugin contacts a remote service, the ",[18,1844,1019],{}," text should say so plainly.",[14,1847,1848,1849,1851],{},"If a reviewer requests changes, you fix them, bump the ",[18,1850,1009],{},", and re-upload to the same plugin page rather than creating a second listing.",[40,1853,1855],{"id":1854},"releasing-updates","Releasing Updates",[14,1857,1858,1859,1861,1862,1864,1865,1867,1868,1871],{},"For an established plugin, releasing is mechanical: bump ",[18,1860,1009],{},", prepend a ",[18,1863,1143],{}," entry, rebuild the ZIP, and upload it to the same plugin page. The repository validates that the new ",[18,1866,1009],{}," is strictly greater than the latest published one. Users see an ",[59,1869,1870],{},"Upgradeable"," badge in their plugin manager on next refresh.",[14,1873,1874],{},"A safe update checklist:",[45,1876,1877,1883,1889,1895,1900],{},[48,1878,1879,1880,1882],{},"Bump ",[18,1881,1009],{}," per semantic versioning and confirm it is higher than the live release.",[48,1884,1885,1886,1888],{},"Add a ",[18,1887,1143],{}," line describing the change.",[48,1890,1891,1892,1894],{},"If you changed the minimum QGIS, update ",[18,1893,37],{}," and announce it — raising it strands users on older builds.",[48,1896,1897,1898,361],{},"Reinstall the freshly built ZIP into a clean profile and confirm a full load\u002Funload cycle, exactly as you would when validating ",[23,1899,54],{"href":53},[48,1901,1902,1903,1905],{},"If the update carries risk, publish it as ",[18,1904,1718],{}," first.",[40,1907,1909],{"id":1908},"compatibility-notes","Compatibility Notes",[1911,1912,1913,1934],"table",{},[1914,1915,1916],"thead",{},[1917,1918,1919,1923,1926,1931],"tr",{},[1920,1921,1922],"th",{},"QGIS line",[1920,1924,1925],{},"Bundled Python",[1920,1927,1928,1929],{},"Recommended ",[18,1930,37],{},[1920,1932,1933],{},"Notes",[1935,1936,1937,1958,1974],"tbody",{},[1917,1938,1939,1943,1946,1951],{},[1940,1941,1942],"td",{},"3.28 LTR",[1940,1944,1945],{},"3.9",[1940,1947,1948],{},[18,1949,1950],{},"3.28",[1940,1952,1953,1954,1957],{},"Older ",[18,1955,1956],{},"QgsVectorFileWriter"," signatures; avoid f-string-heavy syntax only if you also target 3.6",[1917,1959,1960,1963,1966,1971],{},[1940,1961,1962],{},"3.34 LTR",[1940,1964,1965],{},"3.12",[1940,1967,1968],{},[18,1969,1970],{},"3.34",[1940,1972,1973],{},"Baseline for this guide; modern Processing and writer APIs available",[1917,1975,1976,1979,1981,1989],{},[1940,1977,1978],{},"3.40 \u002F 3.44",[1940,1980,1965],{},[1940,1982,1983,1985,1986],{},[18,1984,1970],{}," with ",[18,1987,1988],{},"qgisMaximumVersion=3.99",[1940,1990,1991],{},"Set a high maximum so the manager keeps showing your plugin",[14,1993,1994,1995,1997],{},"Pin examples and your CI matrix to ",[59,1996,1962],{},", and only declare support for an older or newer line after you have actually loaded the plugin there.",[40,1999,2001],{"id":2000},"key-takeaways","Key Takeaways",[45,2003,2004,2010,2027,2033,2042,2051],{},[48,2005,2006,2007,2009],{},"The ZIP must contain exactly one top-level folder, named as a valid Python package, with ",[18,2008,30],{}," inside it.",[48,2011,2012,2013,431,2015,431,2017,431,2019,431,2021,431,2023,1016,2025,361],{},"Required metadata keys are ",[18,2014,1001],{},[18,2016,37],{},[18,2018,1006],{},[18,2020,1009],{},[18,2022,1012],{},[18,2024,1015],{},[18,2026,1019],{},[48,2028,2029,2030,2032],{},"Use semantic versioning; the repository rejects a ",[18,2031,1009],{}," that is not strictly greater than the live one.",[48,2034,2035,2036,2038,2039,2041],{},"Set ",[18,2037,37],{}," to what you test and ",[18,2040,1147],{}," generously so newer QGIS users still see your plugin.",[48,2043,2044,1588,2047,2050],{},[18,2045,2046],{},"experimental",[18,2048,2049],{},"deprecated"," are per-version flags that control visibility and discourage installs respectively.",[48,2052,2053],{},"First upload requires human review; later versions of an approved plugin publish automatically.",[40,2055,2057],{"id":2056},"frequently-asked-questions","Frequently Asked Questions",[14,2059,2060,2063,2064,2067,2068,2070,2071,2074,2075,2077,2078,2080],{},[59,2061,2062],{},"Do I need to compile resources before publishing?","\nOnly if your plugin references icons or assets through Qt resource paths (",[18,2065,2066],{},":\u002F...","). If ",[18,2069,30],{}," and your code point at real files on disk such as ",[18,2072,2073],{},"icons\u002Ficon.png",", you can skip ",[18,2076,284],{}," entirely. Always ship compiled ",[18,2079,354],{}," translations if you ship any translations.",[14,2082,2083,2086,2087,2089],{},[59,2084,2085],{},"Why was my upload rejected with \"metadata.txt not found\"?","\nThe archive almost certainly has ",[18,2088,30],{}," at the ZIP root or inside a wrongly named or nested folder. The file must sit directly inside a single top-level folder whose name is a valid Python package identifier. Re-zip from the parent directory of the plugin folder.",[14,2091,2092,2095],{},[59,2093,2094],{},"Can I delete a published version?","\nYou can hide or set a version as experimental, but you generally cannot scrub a release that users have already downloaded. The correct fix for a bad release is to publish a higher patch version immediately rather than trying to delete the broken one.",[14,2097,2098,2101],{},[59,2099,2100],{},"How long does first approval take?","\nIt depends on maintainer availability and the cleanliness of your submission, ranging from a day to a couple of weeks. Clean code, a clear license, and a working tracker URL speed it up considerably.",[14,2103,2104,2107,2108,2110],{},[59,2105,2106],{},"What license must my plugin use?","\nThe repository requires a GPL-compatible license because QGIS itself is GPL and plugins link against its API. GPLv2+ or GPLv3 are the typical choices. Include the full license text as a ",[18,2109,1835],{}," file in the plugin root.",[40,2112,2114],{"id":2113},"related","Related",[45,2116,2117,2121,2125,2131,2135],{},[48,2118,2119],{},[23,2120,26],{"href":25},[48,2122,2123],{},[23,2124,54],{"href":53},[48,2126,2127],{},[23,2128,2130],{"href":2129},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002F","Processing Provider Plugins",[48,2132,2133],{},[23,2134,375],{"href":374},[48,2136,2137],{},[23,2138,1163],{"href":1162},[2140,2141,2142],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}",{"title":266,"searchDepth":280,"depth":280,"links":2144},[2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159],{"id":42,"depth":280,"text":43},{"id":86,"depth":280,"text":87},{"id":236,"depth":280,"text":237},{"id":534,"depth":280,"text":535},{"id":642,"depth":280,"text":643},{"id":991,"depth":280,"text":992},{"id":1166,"depth":280,"text":1167},{"id":1582,"depth":280,"text":1583},{"id":1711,"depth":280,"text":1712},{"id":1756,"depth":280,"text":1757},{"id":1854,"depth":280,"text":1855},{"id":1908,"depth":280,"text":1909},{"id":2000,"depth":280,"text":2001},{"id":2056,"depth":280,"text":2057},{"id":2113,"depth":280,"text":2114},"Package a QGIS plugin as a ZIP, write metadata.txt, apply semantic versioning, and navigate the plugins.qgis.org upload, review, and release-update flow.","md",{},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository",{"title":5,"description":2160},"qgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Findex","XjYQUcUyKapg9TNokrZ_S6dxVUR85t6Vp7uvl0We38o",1781792483475]