{"version":"https:\/\/jsonfeed.org\/version\/1","title":"GitHub Actions on Just a Theory","description":"An ongoing list of Just a Theory posts about GitHub Actions","home_page_url":"https:\/\/justatheory.com\/tags\/github-actions\/","feed_url":"https:\/\/justatheory.com\/tags\/github-actions\/feed.json","icon":"https:\/\/justatheory.com\/icon-512x512.png","favicon":"https:\/\/justatheory.com\/icon-64x64.png","items":[{"id":"https:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/","url":"https:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/","title":"Auto-Release PostgreSQL Extensions on PGXN","summary":"Step-by-step instructions to publish PostgreSQL extensions and utilities on\nthe PostgreSQL Extension Network (PGXN).\n","date_published":"2025-05-20T15:49:30Z","authors":[{"name":"David E. Wheeler","url":"https:\/\/justatheory.com\/"}],"tags":["Postgres","PGXN","Extension","GitHub","GitHub Actions","Automation","CI\/CD"],"content_html":"<article class=\"post\">\n        <div class=\"text\">\n<p>I last wrote about auto-releasing PostgreSQL extensions on PGXN <a href=\"https:\/\/justatheory.com\/2020\/10\/release-postgres-extensions-with-github-actions\/\" title=\"Automate Postgres Extension Releases on GitHub and PGXN\">back in\n2020<\/a>, but I thought it worthwhile, following my <a href=\"https:\/\/pgext.day\" title=\"Postgres Extensions Day Montr\u00e9al 2025\">Postgres\nExtensions Day<\/a> talk last week, to return again to the basics. With the\ngoal to get as many extensions distributed on <a href=\"https:\/\/pgxn.org\" title=\"PostgreSQL Extension Network\">PGXN<\/a> as possible, this post\nprovides step-by-step instructions to help the author of any extension or\nPostgres utility to quickly and easily publish every release.<\/p>\n<h2 id=\"tl-dr\">TL;DR<\/h2>\n<ol>\n<li>Create a <a href=\"https:\/\/manager.pgxn.org\/account\/register\" title=\"Request a PGXN Account\">PGXN Manager<\/a> account<\/li>\n<li>Add a <a href=\"https:\/\/rfcs.pgxn.org\/0001-meta-spec-v1.html\"><code>META.json<\/code><\/a> file to your project<\/li>\n<li>Add a <a href=\"https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\">pgxn-tools<\/a> powered CI\/CD pipeline to publish on tag push<\/li>\n<li><a href=\"#write-killer-docs\">Fully-document<\/a> your extensions<\/li>\n<\/ol>\n<h2 id=\"release-your-extensions-on-pgxn\">Release your extensions on PGXN<\/h2>\n<p><a href=\"https:\/\/pgxn.org\" title=\"PostgreSQL Extension Network\">PGXN<\/a> aims to become the defacto source for all open-source PostgreSQL\nextensions and tools, in order to help users quickly find and learn how to use\nextensions to meet their needs. Currently, PGXN distributes source releases\nfor around 400 extensions (stats on the <a href=\"https:\/\/pgxn.org\/about\/\" title=\"About PGXN\">about page<\/a>), a fraction of the ca.\n<a href=\"https:\/\/gist.github.com\/joelonsql\/e5aa27f8cc9bd22b8999b7de8aee9d47\" title=\"\ud83d\uddfa\ud83d\udc18 1000+ PostgreSQL EXTENSIONs\">1200 known extensions<\/a>. Anyone looking for an extension might exist to solve\nsome problem must rely on search engines to find potential solutions between\nPGXN, GitHub, GitLab, blogs, social media posts, and more. Without a single\ntrusted source for extensions, and with the proliferation of <a href=\"https:\/\/en.wikipedia.org\/wiki\/AI_slop\" title=\"Wikipedia: AI Slop\">AI Slop<\/a> in\nsearch engine results, finding extensions aside from a few well-known\nsolutions proves a challenge.<\/p>\n<p>By publishing releases and full documentation &mdash; all fully indexed by its\nsearch index &mdash; PGXN aims to be that trusted source. Extension authors\nprovide all the documentation, which PGXN formats for legibility and linking.\nSee, for example, the <a href=\"https:\/\/pgxn.org\/dist\/vector\/README.html\">pgvector docs<\/a>.<\/p>\n<p>If you want to make it easier for users to find your extensions, to read your\ndocumentation &mdash; not to mention provide sources for binary packaging systems\n&mdash; publish every release on PGXN.<\/p>\n<p>Here&rsquo;s how.<\/p>\n<h3 id=\"create-an-account\">Create an Account<\/h3>\n<p>Step one: create a <a href=\"https:\/\/manager.pgxn.org\/account\/register\" title=\"Request a PGXN Account\">PGXN Manager<\/a> account. The <em>Email<\/em>, <em>Nickname<\/em>, and <em>Why<\/em>\nfields are required. The form asks &ldquo;why&rdquo; as a simple filter for bad actors.\nWrite a sentence describing what you&rsquo;d like to release &mdash; ideally with a link\nto the source repository &mdash; and submit. We&rsquo;ll get the account approved\nforthwith, which will send a confirmation email to your address. Follow the\nlink in the email and you&rsquo;ll be good to go.<\/p>\n<h3 id=\"anatomy-of-a-distribution\">Anatomy of a Distribution<\/h3>\n<p>A PostgreSQL extension source tree generally looks something like this (taken\nfrom the <a href=\"https:\/\/github.com\/theory\/kv-pair\/\">pair repository<\/a>):<\/p>\n<pre tabindex=\"0\"><code class=\"language-tree\" data-lang=\"tree\">pair\n\u251c\u2500\u2500 Changes\n\u251c\u2500\u2500 doc\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 pair.md\n\u251c\u2500\u2500 Makefile\n\u251c\u2500\u2500 META.json\n\u251c\u2500\u2500 pair.control\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 sql\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 pair--unpackaged--0.1.2.sql\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 pair.sql\n\u2514\u2500\u2500 test\n    \u251c\u2500\u2500 expected\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 base.out\n    \u2514\u2500\u2500 sql\n        \u2514\u2500\u2500 base.sql\n<\/code><\/pre><p>Extension authors will recognize the standard <a href=\"https:\/\/www.postgresql.org\/docs\/current\/extend-pgxs.html\" title=\"PostgreSQL Docs: Extension Building Infrastructure\">PGXS<\/a> (or <a href=\"https:\/\/github.com\/pgcentralfoundation\/pgrx\" title=\"pgrx: Build Postgres Extensions with Rust!\">pgrx<\/a>) source\ndistribution files; only <code>META.json<\/code> file needs explaining. The <code>META.json<\/code>\nfile is, frankly, the only file that PGXN requires in a release. It contains\nthe metadata to describe the release, following the <a href=\"https:\/\/rfcs.pgxn.org\/0001-meta-spec-v1.html\">PGXN Meta Spec<\/a>.\nThis example contains only the required fields:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-json\" data-lang=\"json\"><span class=\"line\"><span class=\"cl\"><span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;name&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;pair&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;0.1.0&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;abstract&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;A key\/value pair data type&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;maintainer&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;David E. Wheeler &lt;david@justatheory.com&gt;&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;license&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;postgresql&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;provides&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">    <span class=\"nt\">&#34;pair&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;file&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;sql\/pair.sql&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;0.1.0&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">    <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"nt\">&#34;meta-spec&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">    <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;1.0.0&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">  <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/div><p>Presumably these fields contain no surprises, but a couple of details:<\/p>\n<ul>\n<li>It starts with the name of the distribution, <code>pair<\/code>, and the release\nversion, <code>0.1.0<\/code>.<\/li>\n<li>The <code>abstract<\/code> provides a brief description of the extension, while the\n<code>maintainer<\/code> contains contact information.<\/li>\n<li>The <code>license<\/code> stipulates the distribution license, of course, usually one\nof a few known, but may be <a href=\"https:\/\/rfcs.pgxn.org\/0001-meta-spec-v1.html#license\">customized<\/a>.<\/li>\n<li>The <code>provides<\/code> object lists the extensions or tools provided, each named\nby an object key that points to details about the extension, including\nmain file, version, and potentially an abstract and documentation file.<\/li>\n<li>The <code>meta-spec<\/code> object identifies the meta spec version used for the\n<code>META.json<\/code> itself.<\/li>\n<\/ul>\n<h3 id=\"release-it\">Release It!<\/h3>\n<p>This file with these fields is all you need to make a release. Assuming Git,\npackage up the extension source files like so (replacing your extension name\nand version as appropriate).<\/p>\n<pre tabindex=\"0\"><code>git archive --format zip --prefix=pair-0.1.0 -o pair-0.1.0.zip HEAD\n<\/code><\/pre><p>Then navigate to the <a href=\"https:\/\/manager.pgxn.org\/upload\">release page<\/a>, authenticate, and upload the resulting\n<code>.zip<\/code> file.<\/p>\n\n\n\t<figure class=\"frame\" title=\"Screenshot with a box labeled \u201cUpload a Distribution Archive\u201d. It contains an \u201cArchive\u201d label in front of a button labeled \u201cChoose File\u201d. Next to it is a zip file icon and  the text \u201cpair-0.1.0.zip\u201d. Below the box is another button labeled \u201cRelease It!\u201d\">\n\t\t\t<img src=\"https:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/upload-to-pgxn.png\" alt=\"Screenshot with a box labeled \u201cUpload a Distribution Archive\u201d. It contains an \u201cArchive\u201d label in front of a button labeled \u201cChoose File\u201d. Next to it is a zip file icon and  the text \u201cpair-0.1.0.zip\u201d. Below the box is another button labeled \u201cRelease It!\u201d\" \/>\n\t<\/figure>\n\n<p>And that&rsquo;s it! Your release will appear on <a href=\"https:\/\/pgxn.org\" title=\"PostgreSQL Extension Network\">pgxn.org<\/a> and on <a href=\"https:\/\/mastodon.social\/@pgxn\">Mastodon<\/a>\nwithin five minutes.<\/p>\n<h2 id=\"let-s-automate-it\">Let&rsquo;s Automate it!<\/h2>\n<p>All those steps would be a pain in the ass to follow for every release. Let&rsquo;s\nautomate it using <a href=\"https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\">pgxn-tools<\/a>! This OCI image contains the tools necessary to\npackage and upload an extension release to PGXN. Ideally, use a <a href=\"https:\/\/en.wikipedia.org\/wiki\/CI\/CD\" title=\"Wikipedia: CI\/CD\">CI\/CD<\/a>\npipeline like a <a href=\"https:\/\/docs.github.com\/en\/actions\/writing-workflows\">GitHub Workflow<\/a> to publish a release on every version tag.<\/p>\n<h3 id=\"set-up-secrets\">Set up Secrets<\/h3>\n<p><a href=\"https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\">pgxn-tools<\/a> uses your PGXN credentials to publish releases. To keep them\nsafe, use the secrets feature of your preferred CI\/CD tool. This figure shows\nthe &ldquo;Secrets and variables&rdquo; configuration for a GitHub repository, with two\nrepository secrets: <code>PGXN_USERNAME<\/code> and <code>PGXN_PASSWORD<\/code>:<\/p>\n\n\n\t<figure class=\"frame\" title=\"Screenshot of GitHub Secrets configuration featuring two repository secrets: PGXN_USERNAME and PGXN_PASSWORD.\">\n\t\t\t<img src=\"https:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/github-secrets.png\" alt=\"Screenshot of GitHub Secrets configuration featuring two repository secrets: PGXN_USERNAME and PGXN_PASSWORD.\" \/>\n\t<\/figure>\n\n<h3 id=\"create-a-pipeline\">Create a Pipeline<\/h3>\n<p>Use those secrets and <a href=\"https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\">pgxn-tools<\/a> in CI\/CD pipeline. Here, for example, is a\nminimal GitHub workflow to publish a release for every <a href=\"https:\/\/semver.org\" title=\"Semantic Versioning 2.0.0\">SemVer<\/a> tag:<\/p>\n<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"lnt\"> 1\n<\/span><span class=\"lnt\"> 2\n<\/span><span class=\"lnt\"> 3\n<\/span><span class=\"lnt\"> 4\n<\/span><span class=\"lnt\"> 5\n<\/span><span class=\"lnt\"> 6\n<\/span><span class=\"lnt\"> 7\n<\/span><span class=\"lnt\"> 8\n<\/span><span class=\"lnt\"> 9\n<\/span><span class=\"lnt\">10\n<\/span><span class=\"lnt\">11\n<\/span><span class=\"lnt\">12\n<\/span><span class=\"lnt\">13\n<\/span><span class=\"lnt\">14\n<\/span><span class=\"lnt\">15\n<\/span><span class=\"lnt\">16\n<\/span><span class=\"lnt\">17\n<\/span><span class=\"lnt\">18\n<\/span><\/code><\/pre><\/td>\n<td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code class=\"language-yaml\" data-lang=\"yaml\"><span class=\"line\"><span class=\"cl\"><span class=\"nt\">on<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">push<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">tags<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"s1\">&#39;v[0-9]+.[0-9]+.[0-9]+&#39;<\/span><span class=\"p\">]<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">jobs<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">release<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release on PGXN<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">runs-on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">ubuntu-latest<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">container<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn\/pgxn-tools<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">env<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">PGXN_USERNAME<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.PGXN_USERNAME }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">PGXN_PASSWORD<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.PGXN_PASSWORD }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">steps<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Check out the repo<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">actions\/checkout@v4<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Bundle the Release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn-bundle<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release on PGXN<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn-release<\/span><span class=\"w\">\n<\/span><\/span><\/span><\/code><\/pre><\/td><\/tr><\/table>\n<\/div>\n<\/div><p>Details:<\/p>\n<ul>\n<li>Line 3 configures the workflow to run on a <a href=\"https:\/\/semver.org\" title=\"Semantic Versioning 2.0.0\">SemVer<\/a> tag push, typically\nused to denote a release.<\/li>\n<li>Line 8 configures the workflow job to run inside a <a href=\"https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\">pgxn-tools<\/a> container.<\/li>\n<li>Lines 10-11 set environment variables with the credentials from the\nsecrets.<\/li>\n<li>Line 16 bundles the release using either <code>git archive<\/code> or <code>zip<\/code>.<\/li>\n<li>Line 18 publishes the release on PGXN.<\/li>\n<\/ul>\n<p>Now publishing a new release is as simple as pushing a <a href=\"https:\/\/semver.org\" title=\"Semantic Versioning 2.0.0\">SemVer<\/a> tag, like so:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sh\" data-lang=\"sh\"><span class=\"line\"><span class=\"cl\">git tag v0.1.0 -sm <span class=\"s1\">&#39;Tag v0.1.0&#39;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">git push --follow-tags\n<\/span><\/span><\/code><\/pre><\/div><p>That&rsquo;s it! The workflow will automatically publish the extension for every\nrelease, ensuring the latest and greatest always make it to PGXN where users\nand packagers will find them.<\/p>\n<p>The <a href=\"https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\">pgxn-tools<\/a> image also provides tools to easily test a <a href=\"https:\/\/www.postgresql.org\/docs\/current\/extend-pgxs.html\" title=\"PostgreSQL Docs: Extension Building Infrastructure\">PGXS<\/a> or <a href=\"https:\/\/github.com\/pgcentralfoundation\/pgrx\" title=\"pgrx: Build Postgres Extensions with Rust!\">pgrx<\/a>\nextension on supported PostgreSQL versions (going back as far as 8.2), also\nsuper useful in a CI\/CD pipeline. See <a href=\"https:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/\">Test Postgres Extensions With GitHub\nActions<\/a> for instructions. Depending on your CI\/CD tool of choice, you might\ntake additional steps, such as publishing a release on GitHub, as <a href=\"https:\/\/justatheory.com\/2020\/10\/release-postgres-extensions-with-github-actions\/\" title=\"Automate Postgres Extension Releases on GitHub and PGXN\">previously\ndescribed<\/a>.<\/p>\n<h2 id=\"optimizing-for-pgxn\">Optimizing for PGXN<\/h2>\n<p>But let&rsquo;s dig deeper into how to optimize extensions for maximum\ndiscoverability and user visibility on <a href=\"https:\/\/pgxn.org\" title=\"PostgreSQL Extension Network\">PGXN<\/a>.<\/p>\n<h3 id=\"add-more-metadata\">Add More Metadata<\/h3>\n<p>The <code>META.json<\/code> file supports many more fields that PGXN indexes and\nreferences. These improve the chances users will find what they&rsquo;re looking\nfor. This detailed example demonstrates how a <a href=\"https:\/\/postgis.net\" title=\"PostGIS\">PostGIS<\/a> <code>META.json<\/code> file might\nstart to provide additional metadata:<\/p>\n<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"lnt\"> 1\n<\/span><span class=\"lnt\"> 2\n<\/span><span class=\"lnt\"> 3\n<\/span><span class=\"lnt\"> 4\n<\/span><span class=\"lnt\"> 5\n<\/span><span class=\"lnt\"> 6\n<\/span><span class=\"lnt\"> 7\n<\/span><span class=\"lnt\"> 8\n<\/span><span class=\"lnt\"> 9\n<\/span><span class=\"lnt\">10\n<\/span><span class=\"lnt\">11\n<\/span><span class=\"lnt\">12\n<\/span><span class=\"lnt\">13\n<\/span><span class=\"lnt\">14\n<\/span><span class=\"lnt\">15\n<\/span><span class=\"lnt\">16\n<\/span><span class=\"lnt\">17\n<\/span><span class=\"lnt\">18\n<\/span><span class=\"lnt\">19\n<\/span><span class=\"lnt\">20\n<\/span><span class=\"lnt\">21\n<\/span><span class=\"lnt\">22\n<\/span><span class=\"lnt\">23\n<\/span><span class=\"lnt\">24\n<\/span><span class=\"lnt\">25\n<\/span><span class=\"lnt\">26\n<\/span><span class=\"lnt\">27\n<\/span><span class=\"lnt\">28\n<\/span><span class=\"lnt\">29\n<\/span><span class=\"lnt\">30\n<\/span><span class=\"lnt\">31\n<\/span><span class=\"lnt\">32\n<\/span><span class=\"lnt\">33\n<\/span><span class=\"lnt\">34\n<\/span><span class=\"lnt\">35\n<\/span><span class=\"lnt\">36\n<\/span><span class=\"lnt\">37\n<\/span><span class=\"lnt\">38\n<\/span><span class=\"lnt\">39\n<\/span><span class=\"lnt\">40\n<\/span><span class=\"lnt\">41\n<\/span><span class=\"lnt\">42\n<\/span><span class=\"lnt\">43\n<\/span><span class=\"lnt\">44\n<\/span><span class=\"lnt\">45\n<\/span><span class=\"lnt\">46\n<\/span><span class=\"lnt\">47\n<\/span><span class=\"lnt\">48\n<\/span><span class=\"lnt\">49\n<\/span><span class=\"lnt\">50\n<\/span><span class=\"lnt\">51\n<\/span><span class=\"lnt\">52\n<\/span><span class=\"lnt\">53\n<\/span><span class=\"lnt\">54\n<\/span><span class=\"lnt\">55\n<\/span><span class=\"lnt\">56\n<\/span><span class=\"lnt\">57\n<\/span><span class=\"lnt\">58\n<\/span><span class=\"lnt\">59\n<\/span><span class=\"lnt\">60\n<\/span><span class=\"lnt\">61\n<\/span><\/code><\/pre><\/td>\n<td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code class=\"language-json\" data-lang=\"json\"><span class=\"line\"><span class=\"cl\"><span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;name&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;postgis&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;abstract&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;Geographic Information Systems Extensions to PostgreSQL&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;description&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;This distribution contains a module which implements GIS simple features, ties the features to R-tree indexing, and provides many spatial functions for accessing and analyzing geographic data.&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;3.5.0&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;maintainer&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">[<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;Paul Ramsey &lt;pramsey@example.com&gt;&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;Sandro Santilli &lt;sandro@examle.net&gt;&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"p\">],<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;license&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">[<\/span> <span class=\"s2\">&#34;gpl_2&#34;<\/span><span class=\"p\">,<\/span> <span class=\"s2\">&#34;gpl_3&#34;<\/span> <span class=\"p\">],<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;provides&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;postgis&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;abstract&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;PostGIS geography spatial types and functions&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;file&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;extensions\/postgis\/postgis.control&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;docfile&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;extensions\/postgis\/doc\/postgis.md&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;3.5.0&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;address_standardizer&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;abstract&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;Used to parse an address into constituent elements. Generally used to support geocoding address normalization step.&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;file&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;extensions\/address_standardizer\/address_standardizer.control&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;docfile&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;extensions\/address_standardizer\/README.address_standardizer&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;3.5.0&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;prereqs&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;runtime&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;requires&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">            <span class=\"nt\">&#34;PostgreSQL&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;12.0.0&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">            <span class=\"nt\">&#34;plpgsql&#34;<\/span><span class=\"p\">:<\/span> <span class=\"mi\">0<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;test&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;recommends&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">            <span class=\"nt\">&#34;pgTAP&#34;<\/span><span class=\"p\">:<\/span> <span class=\"mi\">0<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;resources&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;bugtracker&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;web&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;https:\/\/trac.osgeo.org\/postgis\/&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;repository&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;url&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;https:\/\/git.osgeo.org\/gitea\/postgis\/postgis.git&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;web&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;https:\/\/git.osgeo.org\/gitea\/postgis\/postgis&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">         <span class=\"nt\">&#34;type&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;git&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"p\">}<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;generated_by&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;David E. Wheeler&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;meta-spec&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;version&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;1.0.0&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"nt\">&#34;url&#34;<\/span><span class=\"p\">:<\/span> <span class=\"s2\">&#34;https:\/\/pgxn.org\/meta\/spec.txt&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"p\">},<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"nt\">&#34;tags&#34;<\/span><span class=\"p\">:<\/span> <span class=\"p\">[<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;gis&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;spatial&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;geometry&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;raster&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;geography&#34;<\/span><span class=\"p\">,<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;location&#34;<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\">   <span class=\"p\">]<\/span>\n<\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}<\/span>\n<\/span><\/span><\/code><\/pre><\/td><\/tr><\/table>\n<\/div>\n<\/div><ul>\n<li>Line 4 contains a longer description of the distribution.<\/li>\n<li>Lines 6-9 show how to list multiple maintainers as an array.<\/li>\n<li>Line 10 demonstrates support for an array of licenses.<\/li>\n<li>Lines 11-24 list multiple extensions included in the distribution, with\nabstracts and documentation files for each.<\/li>\n<li>Lines 25-37 identify dependencies for various phases of the distribution\nlifecycle, including configure, build, test, runtime, and develop. Each\ncontains an object identifying PostgreSQL or extension dependencies.<\/li>\n<li>Lines 38-47 lists resources for the distribution, including issue\ntracking and source code repository.<\/li>\n<li>Lines 53-60 contains an array of tags, an arbitrary list of keywords for a\ndistribution used both in the search index and the <a href=\"https:\/\/pgxn.org\/tags\" title=\"PGXN Release Tags\">PGXN tag cloud<\/a>.<\/li>\n<\/ul>\n<p>Admittedly the <a href=\"https:\/\/rfcs.pgxn.org\/0001-meta-spec-v1.html\">PGXN Meta Spec<\/a> provides a great deal of information.\nPerhaps the simplest way to manage it is to copy an existing <code>META.json<\/code> from\nanother project (or above) and edit it. In general, only the <code>version<\/code> fields\nrequire updating for each release.<\/p>\n<h3 id=\"write-killer-docs\">Write Killer Docs<\/h3>\n<p>The most successful extensions provide ample descriptive and reference\ndocumentation, as well as examples. Most extensions feature a README, of\ncourse, which contains basic information, build and install instructions, and\ncontact info. But as the <a href=\"#anatomy-of-a-distribution\">pair tree<\/a>, illustrates,\nPGXN also supports extension-specific documentation in a variety of formats,\nincluding:<\/p>\n<ul>\n<li><a href=\"https:\/\/asciidoc.org\">Asciidoc<\/a><\/li>\n<li><a href=\"https:\/\/www.bbcode.org\">BBcode<\/a><\/li>\n<li><a href=\"https:\/\/www.wikicreole.org\">Creole<\/a><\/li>\n<li><a href=\"https:\/\/whatwg.org\/html\">HTML<\/a><\/li>\n<li><a href=\"https:\/\/daringfireball.net\/projects\/markdown\/\">Markdown<\/a><\/li>\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Help:Contents\/Editing_Wikipedia\">MediaWiki<\/a><\/li>\n<li><a href=\"https:\/\/fletcherpenney.net\/multimarkdown\/\">MultiMarkdown<\/a><\/li>\n<li><a href=\"https:\/\/metacpan.org\/dist\/perl\/view\/pod\/perlpodspec.pod\">Pod<\/a><\/li>\n<li><a href=\"https:\/\/docutils.sourceforge.io\/rst.html\">reStructuredText<\/a><\/li>\n<li><a href=\"https:\/\/textile-lang.com\">Textile<\/a><\/li>\n<li><a href=\"https:\/\/trac.edgewall.org\/wiki\/WikiFormatting\">Trac<\/a><\/li>\n<\/ul>\n<p>Some examples:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/theory\/pg-jsonschema-boon\/blob\/main\/doc\/jsonschema.md\">jsonschema<\/a> (Markdown)<\/li>\n<li><a href=\"https:\/\/github.com\/theory\/pg-semver\/blob\/main\/doc\/semver.mmd\">semver<\/a>\n(MultiMarkdown)<\/li>\n<\/ul>\n<p>PGXN will also index and format additional documentation files in any of the\nabove formats. See, for example, all the files formatted for <a href=\"https:\/\/pgxn.org\/dist\/orafce\/\">orafce<\/a>.<\/p>\n<h3 id=\"exclude-files-from-release\">Exclude Files from Release<\/h3>\n<p>Use <a href=\"https:\/\/git-scm.com\/docs\/gitattributes\">gitattributes<\/a> to exclude files from the release. For example,\ndistributions don&rsquo;t generally include <code>.gitignore<\/code> or the contents of the\n<code>.github<\/code> directory. Exclude them from the archive created by <code>git archive<\/code> by\nassigning <code>export-ignore<\/code> to each path to exclude in the <code>.gitattributes<\/code>\nfile, like so:<\/p>\n<pre tabindex=\"0\"><code>.gitignore export-ignore\n.gitattributes export-ignore\n.github export-ignore\n<\/code><\/pre><h2 id=\"what-s-it-all-for\">What&rsquo;s It All For?<\/h2>\n<p><a href=\"https:\/\/pgxn.org\" title=\"PostgreSQL Extension Network\">PGXN<\/a> aims to be the trusted system of record for open-source PostgreSQL\nextensions. Of course that requires that it contain all (or nearly all) of\nsaid extensions. Hence this post.<\/p>\n<p>Please help make it so by adding your extensions, both to help users find the\nextensions they need, and to improve the discoverability of your extensions.\nOver time, we aim to feed downstream extension distribution systems, such as\n<a href=\"https:\/\/yum.postgresql.org\" title=\"PostgreSQL Yum Repository\">Yum<\/a>, <a href=\"https:\/\/wiki.postgresql.org\/wiki\/Apt\" title=\"The PostgreSQL Wiki: \u201cApt\u201d\">APT<\/a>, <a href=\"https:\/\/cloudnative-pg.io\" title=\"Run PostgreSQL. The Kubernetes way.\">CloudNativePG<\/a>, <a href=\"https:\/\/justatheory.com\/2024\/06\/trunk-oci-poc\/\" title=\"POC: Distributing Trunk Binaries via OCI\">OCI<\/a>, and more.<\/p>\n<p>Let&rsquo;s make extensions available everywhere to everyone.<\/p>\n\n        <\/div>\n\n        <footer class=\"tags\">\n            <h5>More about\u2026<\/h5>\n            <ul>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/postgres\/\">Postgres<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/pgxn\/\">PGXN<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/extension\/\">Extension<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github\/\">GitHub<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github-actions\/\">GitHub Actions<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/automation\/\">Automation<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/ci\/cd\/\">CI\/CD<\/a><\/li>\n            <\/ul>\n        <\/footer>\n    <\/article>","content_text":"I last wrote about auto-releasing PostgreSQL extensions on PGXN [back in\n2020][release-extensions], but I thought it worthwhile, following my [Postgres\nExtensions Day][pgext] talk last week, to return again to the basics. With the\ngoal to get as many extensions distributed on [PGXN] as possible, this post\nprovides step-by-step instructions to help the author of any extension or\nPostgres utility to quickly and easily publish every release.\n\n## TL;DR\n\n1.  Create a [PGXN Manager] account\n2.  Add a [`META.json`][spec] file to your project\n3.  Add a [pgxn-tools] powered CI\/CD pipeline to publish on tag push\n4.  [Fully-document](#write-killer-docs) your extensions\n\n## Release your extensions on PGXN\n\n[PGXN] aims to become the defacto source for all open-source PostgreSQL\nextensions and tools, in order to help users quickly find and learn how to use\nextensions to meet their needs. Currently, PGXN distributes source releases\nfor around 400 extensions (stats on the [about page]), a fraction of the ca.\n[1200 known extensions]. Anyone looking for an extension might exist to solve\nsome problem must rely on search engines to find potential solutions between\nPGXN, GitHub, GitLab, blogs, social media posts, and more. Without a single\ntrusted source for extensions, and with the proliferation of [AI Slop] in\nsearch engine results, finding extensions aside from a few well-known\nsolutions proves a challenge.\n\nBy publishing releases and full documentation --- all fully indexed by its\nsearch index --- PGXN aims to be that trusted source. Extension authors\nprovide all the documentation, which PGXN formats for legibility and linking.\nSee, for example, the [pgvector docs].\n\nIf you want to make it easier for users to find your extensions, to read your\ndocumentation --- not to mention provide sources for binary packaging systems\n--- publish every release on PGXN.\n\nHere's how.\n\n### Create an Account\n\nStep one: create a [PGXN Manager] account. The *Email*, *Nickname*, and *Why*\nfields are required. The form asks \"why\" as a simple filter for bad actors.\nWrite a sentence describing what you'd like to release --- ideally with a link\nto the source repository --- and submit. We'll get the account approved\nforthwith, which will send a confirmation email to your address. Follow the\nlink in the email and you'll be good to go.\n\n### Anatomy of a Distribution\n\nA PostgreSQL extension source tree generally looks something like this (taken\nfrom the [pair repository]):\n\n```tree\npair\n\u251c\u2500\u2500 Changes\n\u251c\u2500\u2500 doc\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 pair.md\n\u251c\u2500\u2500 Makefile\n\u251c\u2500\u2500 META.json\n\u251c\u2500\u2500 pair.control\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 sql\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 pair--unpackaged--0.1.2.sql\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 pair.sql\n\u2514\u2500\u2500 test\n    \u251c\u2500\u2500 expected\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 base.out\n    \u2514\u2500\u2500 sql\n        \u2514\u2500\u2500 base.sql\n```\n\nExtension authors will recognize the standard [PGXS] (or [pgrx]) source\ndistribution files; only `META.json` file needs explaining. The `META.json`\nfile is, frankly, the only file that PGXN requires in a release. It contains\nthe metadata to describe the release, following the [PGXN Meta Spec][spec].\nThis example contains only the required fields:\n\n```json\n{\n  \"name\": \"pair\",\n  \"version\": \"0.1.0\",\n  \"abstract\": \"A key\/value pair data type\",\n  \"maintainer\": \"David E. Wheeler <david@justatheory.com>\",\n  \"license\": \"postgresql\",\n  \"provides\": {\n    \"pair\": {\n      \"file\": \"sql\/pair.sql\",\n      \"version\": \"0.1.0\"\n    }\n  },\n  \"meta-spec\": {\n    \"version\": \"1.0.0\"\n  }\n}\n```\n\nPresumably these fields contain no surprises, but a couple of details:\n\n*   It starts with the name of the distribution, `pair`, and the release\n    version, `0.1.0`.\n*   The `abstract` provides a brief description of the extension, while the\n    `maintainer` contains contact information.\n*   The `license` stipulates the distribution license, of course, usually one\n    of a few known, but may be [customized][license].\n*   The `provides` object lists the extensions or tools provided, each named\n    by an object key that points to details about the extension, including\n    main file, version, and potentially an abstract and documentation file.\n*   The `meta-spec` object identifies the meta spec version used for the\n    `META.json` itself.\n\n### Release It!\n\nThis file with these fields is all you need to make a release. Assuming Git,\npackage up the extension source files like so (replacing your extension name\nand version as appropriate).\n\n```\ngit archive --format zip --prefix=pair-0.1.0 -o pair-0.1.0.zip HEAD\n```\n\nThen navigate to the [release page], authenticate, and upload the resulting\n`.zip` file.\n\n\n\n\t<figure class=\"frame\" title=\"Screenshot with a box labeled \u201cUpload a Distribution Archive\u201d. It contains an \u201cArchive\u201d label in front of a button labeled \u201cChoose File\u201d. Next to it is a zip file icon and  the text \u201cpair-0.1.0.zip\u201d. Below the box is another button labeled \u201cRelease It!\u201d\">\n\t\t\t<img src=\"https:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/upload-to-pgxn.png\" alt=\"Screenshot with a box labeled \u201cUpload a Distribution Archive\u201d. It contains an \u201cArchive\u201d label in front of a button labeled \u201cChoose File\u201d. Next to it is a zip file icon and  the text \u201cpair-0.1.0.zip\u201d. Below the box is another button labeled \u201cRelease It!\u201d\" \/>\n\t<\/figure>\n\n\nAnd that's it! Your release will appear on [pgxn.org][PGXN] and on [Mastodon]\nwithin five minutes.\n\n## Let's Automate it!\n\nAll those steps would be a pain in the ass to follow for every release. Let's\nautomate it using [pgxn-tools]! This OCI image contains the tools necessary to\npackage and upload an extension release to PGXN. Ideally, use a [CI\/CD]\npipeline like a [GitHub Workflow] to publish a release on every version tag.\n\n### Set up Secrets\n\n[pgxn-tools] uses your PGXN credentials to publish releases. To keep them\nsafe, use the secrets feature of your preferred CI\/CD tool. This figure shows\nthe \"Secrets and variables\" configuration for a GitHub repository, with two\nrepository secrets: `PGXN_USERNAME` and `PGXN_PASSWORD`:\n\n\n\n\t<figure class=\"frame\" title=\"Screenshot of GitHub Secrets configuration featuring two repository secrets: PGXN_USERNAME and PGXN_PASSWORD.\">\n\t\t\t<img src=\"https:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/github-secrets.png\" alt=\"Screenshot of GitHub Secrets configuration featuring two repository secrets: PGXN_USERNAME and PGXN_PASSWORD.\" \/>\n\t<\/figure>\n\n\n### Create a Pipeline\n\nUse those secrets and [pgxn-tools] in CI\/CD pipeline. Here, for example, is a\nminimal GitHub workflow to publish a release for every [SemVer] tag:\n\n```yaml {linenos=table}\non:\n  push:\n    tags: ['v[0-9]+.[0-9]+.[0-9]+']\njobs:\n  release:\n    name: Release on PGXN\n    runs-on: ubuntu-latest\n    container: pgxn\/pgxn-tools\n    env:\n      PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}\n      PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}\n    steps:\n    - name: Check out the repo\n      uses: actions\/checkout@v4\n    - name: Bundle the Release\n      run: pgxn-bundle\n    - name: Release on PGXN\n      run: pgxn-release\n```\n\nDetails:\n\n*   Line 3 configures the workflow to run on a [SemVer] tag push, typically\n    used to denote a release.\n*   Line 8 configures the workflow job to run inside a [pgxn-tools] container.\n*   Lines 10-11 set environment variables with the credentials from the\n    secrets.\n*   Line 16 bundles the release using either `git archive` or `zip`.\n*   Line 18 publishes the release on PGXN.\n\nNow publishing a new release is as simple as pushing a [SemVer] tag, like so:\n\n```sh\ngit tag v0.1.0 -sm 'Tag v0.1.0'\ngit push --follow-tags\n```\n\nThat's it! The workflow will automatically publish the extension for every\nrelease, ensuring the latest and greatest always make it to PGXN where users\nand packagers will find them.\n\nThe [pgxn-tools] image also provides tools to easily test a [PGXS] or [pgrx]\nextension on supported PostgreSQL versions (going back as far as 8.2), also\nsuper useful in a CI\/CD pipeline. See [Test Postgres Extensions With GitHub\nActions] for instructions. Depending on your CI\/CD tool of choice, you might\ntake additional steps, such as publishing a release on GitHub, as [previously\ndescribed][release-extensions].\n\n## Optimizing for PGXN\n\nBut let's dig deeper into how to optimize extensions for maximum\ndiscoverability and user visibility on [PGXN].\n\n### Add More Metadata\n\nThe `META.json` file supports many more fields that PGXN indexes and\nreferences. These improve the chances users will find what they're looking\nfor. This detailed example demonstrates how a [PostGIS] `META.json` file might\nstart to provide additional metadata:\n\n```json {linenos=table}\n{\n   \"name\": \"postgis\",\n   \"abstract\": \"Geographic Information Systems Extensions to PostgreSQL\",\n   \"description\": \"This distribution contains a module which implements GIS simple features, ties the features to R-tree indexing, and provides many spatial functions for accessing and analyzing geographic data.\",\n   \"version\": \"3.5.0\",\n   \"maintainer\": [\n      \"Paul Ramsey <pramsey@example.com>\",\n      \"Sandro Santilli <sandro@examle.net>\"\n   ],\n   \"license\": [ \"gpl_2\", \"gpl_3\" ],\n   \"provides\": {\n      \"postgis\": {\n         \"abstract\": \"PostGIS geography spatial types and functions\",\n         \"file\": \"extensions\/postgis\/postgis.control\",\n         \"docfile\": \"extensions\/postgis\/doc\/postgis.md\",\n         \"version\": \"3.5.0\"\n      },\n      \"address_standardizer\": {\n         \"abstract\": \"Used to parse an address into constituent elements. Generally used to support geocoding address normalization step.\",\n         \"file\": \"extensions\/address_standardizer\/address_standardizer.control\",\n         \"docfile\": \"extensions\/address_standardizer\/README.address_standardizer\",\n         \"version\": \"3.5.0\"\n      }\n   },\n   \"prereqs\": {\n      \"runtime\": {\n         \"requires\": {\n            \"PostgreSQL\": \"12.0.0\",\n            \"plpgsql\": 0\n         }\n      },\n      \"test\": {\n         \"recommends\": {\n            \"pgTAP\": 0\n         }\n      }\n   },\n   \"resources\": {\n      \"bugtracker\": {\n         \"web\": \"https:\/\/trac.osgeo.org\/postgis\/\"\n      },\n      \"repository\": {\n         \"url\": \"https:\/\/git.osgeo.org\/gitea\/postgis\/postgis.git\",\n         \"web\": \"https:\/\/git.osgeo.org\/gitea\/postgis\/postgis\",\n         \"type\": \"git\"\n      }\n   },\n   \"generated_by\": \"David E. Wheeler\",\n   \"meta-spec\": {\n      \"version\": \"1.0.0\",\n      \"url\": \"https:\/\/pgxn.org\/meta\/spec.txt\"\n   },\n   \"tags\": [\n      \"gis\",\n      \"spatial\",\n      \"geometry\",\n      \"raster\",\n      \"geography\",\n      \"location\"\n   ]\n}\n```\n\n*   Line 4 contains a longer description of the distribution.\n*   Lines 6-9 show how to list multiple maintainers as an array.\n*   Line 10 demonstrates support for an array of licenses.\n*   Lines 11-24 list multiple extensions included in the distribution, with\n    abstracts and documentation files for each.\n*   Lines 25-37 identify dependencies for various phases of the distribution\n    lifecycle, including configure, build, test, runtime, and develop. Each\n    contains an object identifying PostgreSQL or extension dependencies.\n*   Lines 38-47 lists resources for the distribution, including issue\n    tracking and source code repository.\n*   Lines 53-60 contains an array of tags, an arbitrary list of keywords for a\n    distribution used both in the search index and the [PGXN tag cloud].\n\nAdmittedly the [PGXN Meta Spec][spec] provides a great deal of information.\nPerhaps the simplest way to manage it is to copy an existing `META.json` from\nanother project (or above) and edit it. In general, only the `version` fields\nrequire updating for each release.\n\n### Write Killer Docs\n\nThe most successful extensions provide ample descriptive and reference\ndocumentation, as well as examples. Most extensions feature a README, of\ncourse, which contains basic information, build and install instructions, and\ncontact info. But as the [pair tree](#anatomy-of-a-distribution), illustrates,\nPGXN also supports extension-specific documentation in a variety of formats,\nincluding:\n\n*   [Asciidoc](https:\/\/asciidoc.org)\n*   [BBcode](https:\/\/www.bbcode.org)\n*   [Creole](https:\/\/www.wikicreole.org)\n*   [HTML](https:\/\/whatwg.org\/html)\n*   [Markdown](https:\/\/daringfireball.net\/projects\/markdown\/)\n*   [MediaWiki](https:\/\/en.wikipedia.org\/wiki\/Help:Contents\/Editing_Wikipedia)\n*   [MultiMarkdown](https:\/\/fletcherpenney.net\/multimarkdown\/)\n*   [Pod](https:\/\/metacpan.org\/dist\/perl\/view\/pod\/perlpodspec.pod)\n*   [reStructuredText](https:\/\/docutils.sourceforge.io\/rst.html)\n*   [Textile](https:\/\/textile-lang.com)\n*   [Trac](https:\/\/trac.edgewall.org\/wiki\/WikiFormatting)\n\nSome examples:\n\n*   [jsonschema](https:\/\/github.com\/theory\/pg-jsonschema-boon\/blob\/main\/doc\/jsonschema.md) (Markdown)\n*   [semver](https:\/\/github.com\/theory\/pg-semver\/blob\/main\/doc\/semver.mmd)\n    (MultiMarkdown)\n\nPGXN will also index and format additional documentation files in any of the\nabove formats. See, for example, all the files formatted for [orafce].\n\n### Exclude Files from Release\n\nUse [gitattributes] to exclude files from the release. For example,\ndistributions don't generally include `.gitignore` or the contents of the\n`.github` directory. Exclude them from the archive created by `git archive` by\nassigning `export-ignore` to each path to exclude in the `.gitattributes`\nfile, like so:\n\n```\n.gitignore export-ignore\n.gitattributes export-ignore\n.github export-ignore\n```\n\n## What's It All For?\n\n[PGXN] aims to be the trusted system of record for open-source PostgreSQL\nextensions. Of course that requires that it contain all (or nearly all) of\nsaid extensions. Hence this post.\n\nPlease help make it so by adding your extensions, both to help users find the\nextensions they need, and to improve the discoverability of your extensions.\nOver time, we aim to feed downstream extension distribution systems, such as\n[Yum], [APT], [CloudNativePG], [OCI], and more.\n\nLet's make extensions available everywhere to everyone.\n\n  [release-extensions]: https:\/\/justatheory.com\/2020\/10\/release-postgres-extensions-with-github-actions\/\n    \"Automate Postgres Extension Releases on GitHub and PGXN\"\n  [pgext]: https:\/\/pgext.day \"Postgres Extensions Day Montr\u00e9al 2025\"\n  [PGXN]: https:\/\/pgxn.org \"PostgreSQL Extension Network\"\n  [about page]: https:\/\/pgxn.org\/about\/ \"About PGXN\"\n  [1200 known extensions]: https:\/\/gist.github.com\/joelonsql\/e5aa27f8cc9bd22b8999b7de8aee9d47\n    \"\ud83d\uddfa\ud83d\udc18 1000+ PostgreSQL EXTENSIONs\"\n  [AI Slop]: https:\/\/en.wikipedia.org\/wiki\/AI_slop \"Wikipedia: AI Slop\"\n  [pgvector docs]: https:\/\/pgxn.org\/dist\/vector\/README.html\n  [PGXN Manager]: https:\/\/manager.pgxn.org\/account\/register\n    \"Request a PGXN Account\"\n  [pair repository]: https:\/\/github.com\/theory\/kv-pair\/\n  [PGXS]: https:\/\/www.postgresql.org\/docs\/current\/extend-pgxs.html\n    \"PostgreSQL Docs: Extension Building Infrastructure\"\n  [pgrx]: https:\/\/github.com\/pgcentralfoundation\/pgrx\n    \"pgrx: Build Postgres Extensions with Rust!\"\n  [spec]: https:\/\/rfcs.pgxn.org\/0001-meta-spec-v1.html\n  [license]: https:\/\/rfcs.pgxn.org\/0001-meta-spec-v1.html#license\n  [Mastodon]: https:\/\/mastodon.social\/@pgxn\n  [release page]: https:\/\/manager.pgxn.org\/upload\n  [pgxn-tools]: https:\/\/hub.docker.com\/r\/pgxn\/pgxn-tools\n  [CI\/CD]: https:\/\/en.wikipedia.org\/wiki\/CI\/CD \"Wikipedia: CI\/CD\"\n  [GitHub Workflow]: https:\/\/docs.github.com\/en\/actions\/writing-workflows\n  [SemVer]: https:\/\/semver.org \"Semantic Versioning 2.0.0\"\n  [Test Postgres Extensions With GitHub Actions]: https:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/\n  [gitattributes]: https:\/\/git-scm.com\/docs\/gitattributes\n  [PostGIS]: https:\/\/postgis.net \"PostGIS\"\n  [PGXN tag cloud]: https:\/\/pgxn.org\/tags \"PGXN Release Tags\"\n  [orafce]: https:\/\/pgxn.org\/dist\/orafce\/\n  [Yum]: https:\/\/yum.postgresql.org \"PostgreSQL Yum Repository\"\n  [APT]: https:\/\/wiki.postgresql.org\/wiki\/Apt \"The PostgreSQL Wiki: \u201cApt\u201d\"\n  [CloudNativePG]: https:\/\/cloudnative-pg.io \"Run PostgreSQL. The Kubernetes way.\"\n  [OCI]: https:\/\/justatheory.com\/2024\/06\/trunk-oci-poc\/ \"POC: Distributing Trunk Binaries via OCI\"\n\n-------------------------------------------------------------------------------\n\n\u2756 Tuesday, 20 May 2025\nBy David E. Wheeler\nhttps:\/\/justatheory.com\/2025\/05\/release-on-pgxn\/\n\nMore about...\n\n* Postgres:       https:\/\/justatheory.com\/tags\/postgres\/\n* PGXN:           https:\/\/justatheory.com\/tags\/pgxn\/\n* Extension:      https:\/\/justatheory.com\/tags\/extension\/\n* GitHub:         https:\/\/justatheory.com\/tags\/github\/\n* GitHub Actions: https:\/\/justatheory.com\/tags\/github-actions\/\n* Automation:     https:\/\/justatheory.com\/tags\/automation\/\n* CI\/CD:          https:\/\/justatheory.com\/tags\/ci\/cd\/"},{"id":"https:\/\/justatheory.com\/2021\/11\/cache-perl-github-workflows\/","url":"https:\/\/justatheory.com\/2021\/11\/cache-perl-github-workflows\/","title":"Accelerate Perl Github Workflows with Caching","summary":"A quick tip for speeding up Perl builds in GitHub workflows by caching dependencies.","date_published":"2021-11-28T16:43:20Z","date_modified":"2023-02-20T23:55:19Z","authors":[{"name":"David E. Wheeler","url":"https:\/\/justatheory.com\/"}],"tags":["Perl","GitHub","GitHub Actions","GitHub Workflows","Caching"],"content_html":"<article class=\"post\">\n        <div class=\"text\">\n<p>I&rsquo;ve spent quite a few hours evenings and weekends recently building out a\ncomprehensive suite of <a href=\"https:\/\/github.com\/sqitchers\/sqitch\/actions\">GitHub Actions for Sqitch<\/a>. They cover a dozen versions\nof Perl, nearly 70 database versions amongst nine database engines, plus a\ncoverage test and a release workflow. A pull request can expect over 100 actions\nto run. Each build requires over 100 direct dependencies, plus all <em>their<\/em>\ndependencies. Installing them for every build would make any given run\nuntenable.<\/p>\n<p>Happily, GitHub Actions include a <a href=\"https:\/\/docs.github.com\/en\/actions\/advanced-guides\/caching-dependencies-to-speed-up-workflows\" title=\"GitHub Actions: \u201cCaching dependencies to speed up workflows\u201d\">caching feature<\/a>, and thanks to a\n<a href=\"https:\/\/github.com\/shogo82148\/actions-setup-perl\/pull\/892\">recent improvement to shogo82148\/actions-setup-perl<\/a>,\nit&rsquo;s quite easy to use in a version-independent way. Here&rsquo;s an example:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-yaml\" data-lang=\"yaml\"><span class=\"line\"><span class=\"cl\"><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Test<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"l\">push, pull_request]<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">jobs<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">OS<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">strategy<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">matrix<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">os<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"w\"> <\/span><span class=\"l\">ubuntu, macos, windows ]<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">perl<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;latest&#39;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;5.34&#39;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;5.32&#39;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;5.30&#39;<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"s1\">&#39;5.28&#39;<\/span><span class=\"w\"> <\/span><span class=\"p\">]<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Perl ${{ matrix.perl }} on ${{ matrix.os }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">runs-on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ matrix.os }}-latest<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">steps<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Checkout Source<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">actions\/checkout@v3<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Setup Perl<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">id<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">perl<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">shogo82148\/actions-setup-perl@v1<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">with<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span>{<span class=\"w\"> <\/span><span class=\"nt\">perl-version<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&#34;${{ matrix.perl }}&#34;<\/span><span class=\"w\"> <\/span>}<span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Cache CPAN Modules<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">actions\/cache@v3<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">with<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">          <\/span><span class=\"nt\">path<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">local<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">          <\/span><span class=\"nt\">key<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">perl-${{ steps.perl.outputs.perl-hash }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Install Dependencies<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">cpm install --verbose --show-build-log-on-failure --no-test --cpanfile cpanfile<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Run Tests<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">env<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span>{<span class=\"w\"> <\/span><span class=\"nt\">PERL5LIB<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"s2\">&#34;${{ github.workspace }}\/local\/lib\/perl5&#34;<\/span><span class=\"w\"> <\/span>}<span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">prove -lrj4<\/span><span class=\"w\">\n<\/span><\/span><\/span><\/code><\/pre><\/div><p>This workflow tests every permutation of OS and Perl version specified in\n<code>jobs.OS.strategy.matrix<\/code>, resulting in 15 jobs. The <code>runs-on<\/code> value determines\nthe OS, while the <code>steps<\/code> section defines steps for each permutation. Let&rsquo;s take\neach step in turn:<\/p>\n<ul>\n<li>&ldquo;Checkout Source&rdquo; checks the project out of GitHub. Pretty much required for\nany project.<\/li>\n<li>&ldquo;Setup Perl&rdquo; sets up the version of Perl using the value from the matrix.\nNote the <code>id<\/code> key set to <code>perl<\/code>, used in the next step.<\/li>\n<li>&ldquo;Cache CPAN Modules&rdquo; uses the <a href=\"https:\/\/github.com\/actions\/cache\">cache action<\/a> to cache the directory named\n<code>local<\/code> with the key <code>perl-${{ steps.perl.outputs.perl-hash }}<\/code>. The key\nlets us keep different versions of the <code>local<\/code> directory based on a unique\nkey. Here we&rsquo;ve used the <code>perl-hash<\/code> output from the <code>perl<\/code> step defined\nabove. The <code>actions-setup-perl<\/code> action outputs this value, which contains a\nhash of the output of <code>perl -V<\/code>, so we&rsquo;re tying the cache to a very specific\nversion and build of Perl. This is important since compiled modules are not\ncompatible across major versions of Perl.<\/li>\n<li>&ldquo;Install Dependencies&rdquo; uses <a href=\"https:\/\/metacpan.org\/dist\/App-cpm\/view\/script\/cpm\" title=\"cpm - a fast CPAN module installer\"><code>cpm<\/code><\/a> to quickly install Perl dependencies. By\ndefault, it puts them into the <code>local<\/code> subdirectory of the current directory\n&mdash; just where we configured the cache. On the first run for a given OS and\nPerl version, it will install all the dependencies. But on subsequent runs\nit will find the dependencies already present, thank to the cache, and\nquickly exit, reporting &ldquo;All requirements are satisfied.&rdquo; In <a href=\"https:\/\/github.com\/sqitchers\/sqitch\/runs\/4275487924?check_suite_focus=true\">this Sqitch\njob<\/a>, it takes less than a second.<\/li>\n<li>&ldquo;Run Tests&rdquo; runs the tests that require the dependencies. It requires the\n<code>PERL5LIB<\/code> environment variable to point to the location of our cached\ndependencies.<\/li>\n<\/ul>\n<p>That&rsquo;s the whole deal. The first run will be the slowest, depending on the\nnumber of dependencies, but subsequent runs will be much faster, up to the\nseven-day caching period. For a complex project like Sqitch, which uses the same\nOS and Perl version for most of its actions, this results in a tremendous build\ntime savings. CI configurations we&rsquo;ve used in the past often took an hour or\nmore to run. Today, most builds take only a few minutes to test, with longer\ntimes determined not by dependency installation but by container and database\nlatency.<\/p>\n\n        <\/div>\n\n        <footer class=\"tags\">\n            <h5>More about\u2026<\/h5>\n            <ul>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/perl\/\">Perl<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github\/\">GitHub<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github-actions\/\">GitHub Actions<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github-workflows\/\">GitHub Workflows<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/caching\/\">Caching<\/a><\/li>\n            <\/ul>\n        <\/footer>\n    <\/article>","content_text":"I've spent quite a few hours evenings and weekends recently building out a\ncomprehensive suite of [GitHub Actions for Sqitch]. They cover a dozen versions\nof Perl, nearly 70 database versions amongst nine database engines, plus a\ncoverage test and a release workflow. A pull request can expect over 100 actions\nto run. Each build requires over 100 direct dependencies, plus all *their*\ndependencies. Installing them for every build would make any given run\nuntenable.\n\nHappily, GitHub Actions include a [caching feature], and thanks to a\n[recent improvement to shogo82148\/actions-setup-perl][perl-version],\nit's quite easy to use in a version-independent way. Here's an example:\n\n``` yaml\nname: Test\non: [push, pull_request]\njobs:\n  OS:\n    strategy:\n      matrix:\n        os: [ ubuntu, macos, windows ]\n        perl: [ 'latest', '5.34', '5.32', '5.30', '5.28' ]\n    name: Perl ${{ matrix.perl }} on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}-latest\n    steps:\n      - name: Checkout Source\n        uses: actions\/checkout@v3\n      - name: Setup Perl\n        id: perl\n        uses: shogo82148\/actions-setup-perl@v1\n        with: { perl-version: \"${{ matrix.perl }}\" }\n      - name: Cache CPAN Modules\n        uses: actions\/cache@v3\n        with:\n          path: local\n          key: perl-${{ steps.perl.outputs.perl-hash }}\n      - name: Install Dependencies\n        run: cpm install --verbose --show-build-log-on-failure --no-test --cpanfile cpanfile\n      - name: Run Tests\n        env: { PERL5LIB: \"${{ github.workspace }}\/local\/lib\/perl5\" }\n        run: prove -lrj4\n```\n\nThis workflow tests every permutation of OS and Perl version specified in\n`jobs.OS.strategy.matrix`, resulting in 15 jobs. The `runs-on` value determines\nthe OS, while the `steps` section defines steps for each permutation. Let's take\neach step in turn:\n\n*   \"Checkout Source\" checks the project out of GitHub. Pretty much required for\n    any project.\n*   \"Setup Perl\" sets up the version of Perl using the value from the matrix.\n    Note the `id` key set to `perl`, used in the next step.\n*   \"Cache CPAN Modules\" uses the [cache action] to cache the directory named\n    `local` with the key `perl-${{ steps.perl.outputs.perl-hash }}`. The key\n    lets us keep different versions of the `local` directory based on a unique\n    key. Here we've used the `perl-hash` output from the `perl` step defined\n    above. The `actions-setup-perl` action outputs this value, which contains a\n    hash of the output of `perl -V`, so we're tying the cache to a very specific\n    version and build of Perl. This is important since compiled modules are not\n    compatible across major versions of Perl.\n*   \"Install Dependencies\" uses [`cpm`] to quickly install Perl dependencies. By\n    default, it puts them into the `local` subdirectory of the current directory\n    --- just where we configured the cache. On the first run for a given OS and\n    Perl version, it will install all the dependencies. But on subsequent runs\n    it will find the dependencies already present, thank to the cache, and\n    quickly exit, reporting \"All requirements are satisfied.\" In [this Sqitch\n    job], it takes less than a second.\n*   \"Run Tests\" runs the tests that require the dependencies. It requires the\n    `PERL5LIB` environment variable to point to the location of our cached\n    dependencies.\n\nThat's the whole deal. The first run will be the slowest, depending on the\nnumber of dependencies, but subsequent runs will be much faster, up to the\nseven-day caching period. For a complex project like Sqitch, which uses the same\nOS and Perl version for most of its actions, this results in a tremendous build\ntime savings. CI configurations we've used in the past often took an hour or\nmore to run. Today, most builds take only a few minutes to test, with longer\ntimes determined not by dependency installation but by container and database\nlatency.\n\n  [GitHub Actions for Sqitch]: https:\/\/github.com\/sqitchers\/sqitch\/actions\n  [caching feature]:\n     https:\/\/docs.github.com\/en\/actions\/advanced-guides\/caching-dependencies-to-speed-up-workflows\n     \"GitHub Actions: \u201cCaching dependencies to speed up workflows\u201d\"\n  [perl-version]: https:\/\/github.com\/shogo82148\/actions-setup-perl\/pull\/892\n  [cache action]: https:\/\/github.com\/actions\/cache\n  [`cpm`]: https:\/\/metacpan.org\/dist\/App-cpm\/view\/script\/cpm\n    \"cpm - a fast CPAN module installer\"\n  [this Sqitch job]:\n    https:\/\/github.com\/sqitchers\/sqitch\/runs\/4275487924?check_suite_focus=true\n\n-------------------------------------------------------------------------------\n\n\u2756 Sunday, 28 Nov 2021\nBy David E. Wheeler\nhttps:\/\/justatheory.com\/2021\/11\/cache-perl-github-workflows\/\n\nMore about...\n\n* Perl:             https:\/\/justatheory.com\/tags\/perl\/\n* GitHub:           https:\/\/justatheory.com\/tags\/github\/\n* GitHub Actions:   https:\/\/justatheory.com\/tags\/github-actions\/\n* GitHub Workflows: https:\/\/justatheory.com\/tags\/github-workflows\/\n* Caching:          https:\/\/justatheory.com\/tags\/caching\/"},{"id":"https:\/\/justatheory.com\/2020\/10\/release-postgres-extensions-with-github-actions\/","url":"https:\/\/justatheory.com\/2020\/10\/release-postgres-extensions-with-github-actions\/","title":"Automate Postgres Extension Releases on GitHub and PGXN\"","summary":"Go beyond testing and fully automate the release of Postgres extensions on both GitHub and PGXN using GitHub actions.","date_published":"2020-10-25T23:48:36Z","date_modified":"2025-02-25T20:22:08Z","authors":[{"name":"David E. Wheeler","url":"https:\/\/justatheory.com\/"}],"tags":["Postgres","PGXN","GitHub","GitHub Actions","Automation","CI\/CD"],"content_html":"<article class=\"post\">\n        <div class=\"text\">\n<p>Back in June, I wrote about <a href=\"https:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/\" title=\"Test Postgres Extensions With GitHub Actions\">testing Postgres extensions<\/a> on\nmultiple versions of Postgres using <a href=\"https:\/\/github.com\/features\/actions\">GitHub Actions<\/a>. The pattern relies on\nDocker image, <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">pgxn\/pgxn-tools<\/a>, which contains scripts to build and run any\nversion of PostgreSQL, install additional dependencies, build, test, bundle, and\nrelease an extension. I&rsquo;ve since updated it to support testing on the the latest\ndevelopment release of Postgres, meaning one can test on any major version from\n8.4 to (currently) 14. I&rsquo;ve also created GitHub workflows for all of my PGXN\nextensions (except for <a href=\"https:\/\/pgtap.org\">pgTAP<\/a>, which is complicated). I&rsquo;m quite happy with it.<\/p>\n<p>But I was never quite satisfied with the release process. Quite a number of\nPostgres extensions also release on GitHub; indeed, <a href=\"http:\/\/blog.cleverelephant.ca\">Paul Ramsey<\/a> told me\nstraight up that he did not want to manually upload extensions like <a href=\"https:\/\/github.com\/pramsey\/pgsql-http\">pgsql-http<\/a>\nand <a href=\"http:\/\/postgis.net\">PostGIS<\/a> to PGXN, but for PGXN to automatically pull them in when they were\npublished on GitHub. It&rsquo;s pretty cool that newer packaging systems like\n<a href=\"https:\/\/pkg.go.dev\">pkg.go.dev<\/a> auto-index any packages on GibHub. Adding such a feature to PGXN\nwould be an interesting exercise.<\/p>\n<p>But since I&rsquo;m low on TUITs for such a significant undertaking, I decided instead\nto work out how to automatically publish a release on GitHub <em>and<\/em> PGXN via\n<a href=\"https:\/\/github.com\/features\/actions\">GitHub Actions<\/a>. After experimenting for a few months, I&rsquo;ve worked out a\nstraightforward method that should meet the needs of most projects. I&rsquo;ve proven\nthe pattern via the <a href=\"https:\/\/github.com\/theory\/kv-pair\/\">pair extension<\/a>&rsquo;s <a href=\"https:\/\/github.com\/theory\/kv-pair\/blob\/main\/.github\/workflows\/release.yml\"><code>release.yml<\/code><\/a>, which successfully\npublished the v0.1.7 release today on both <a href=\"https:\/\/github.com\/theory\/kv-pair\/releases\/tag\/v0.1.7\">GitHub<\/a> and\n<a href=\"https:\/\/pgxn.org\/dist\/pair\/0.1.7\/\">PGXN<\/a>. With that success, I updated the <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">pgxn\/pgxn-tools<\/a>\ndocumentation with a starter example. It looks like this:<\/p>\n<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"lnt\"> 1\n<\/span><span class=\"lnt\"> 2\n<\/span><span class=\"lnt\"> 3\n<\/span><span class=\"lnt\"> 4\n<\/span><span class=\"lnt\"> 5\n<\/span><span class=\"lnt\"> 6\n<\/span><span class=\"lnt\"> 7\n<\/span><span class=\"lnt\"> 8\n<\/span><span class=\"lnt\"> 9\n<\/span><span class=\"lnt\">10\n<\/span><span class=\"lnt\">11\n<\/span><span class=\"lnt\">12\n<\/span><span class=\"lnt\">13\n<\/span><span class=\"lnt\">14\n<\/span><span class=\"lnt\">15\n<\/span><span class=\"lnt\">16\n<\/span><span class=\"lnt\">17\n<\/span><span class=\"lnt\">18\n<\/span><span class=\"lnt\">19\n<\/span><span class=\"lnt\">20\n<\/span><span class=\"lnt\">21\n<\/span><span class=\"lnt\">22\n<\/span><span class=\"lnt\">23\n<\/span><span class=\"lnt\">24\n<\/span><span class=\"lnt\">25\n<\/span><span class=\"lnt\">26\n<\/span><span class=\"lnt\">27\n<\/span><span class=\"lnt\">28\n<\/span><span class=\"lnt\">29\n<\/span><span class=\"lnt\">30\n<\/span><span class=\"lnt\">31\n<\/span><span class=\"lnt\">32\n<\/span><span class=\"lnt\">33\n<\/span><span class=\"lnt\">34\n<\/span><span class=\"lnt\">35\n<\/span><span class=\"lnt\">36\n<\/span><\/code><\/pre><\/td>\n<td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code class=\"language-yaml\" data-lang=\"yaml\"><span class=\"line\"><span class=\"cl\"><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">on<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">push<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">tags<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"s1\">&#39;v*&#39;<\/span><span class=\"w\"> <\/span><span class=\"c\"># Push events matching v1.0, v20.15.10, etc.<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">jobs<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">release<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release on GitHub and PGXN<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">runs-on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">ubuntu-latest<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">container<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn\/pgxn-tools<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">env<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"c\"># Required to create GitHub release and upload the bundle.<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">GITHUB_TOKEN<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.GITHUB_TOKEN }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">steps<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Check out the repo<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">actions\/checkout@v4<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Bundle the Release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">id<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">bundle<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn-bundle<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release on PGXN<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">env<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"c\"># Required to release on PGXN.<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">PGXN_USERNAME<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.PGXN_USERNAME }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">PGXN_USERNAME<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.PGXN_PASSWORD }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn-release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Create GitHub Release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">id<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">softprops\/action-gh-release@v2<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">with<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">tag_name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ github.ref }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release ${{ github.ref }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">body<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">|<\/span><span class=\"sd\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">          Changes in this Release\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">          - First Change\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">          - Second Change<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">files<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ steps.bundle.outputs.bundle }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><\/code><\/pre><\/td><\/tr><\/table>\n<\/div>\n<\/div>  \n    <blockquote class=\"alert alert-note\">\n      <p class=\"alert-title\">Update 2025-02-25<\/p>\n      <p>Updated the example and description to use <code>softprops\/action-gh-release<\/code> to\ncreate a GitHub release and upload files, replacing two steps that used the\nnow-deprecated <code>actions\/create-release<\/code> and <code>actions\/upload-release-asset<\/code>\nactions.<\/p>\n    <\/blockquote>\n  \n<p>Here&rsquo;s how it works:<\/p>\n<ul>\n<li>\n<p>Lines 4-5 trigger the workflow only when a tag starting with the letter v is\npushed to the repository. This follows the common convention of tagging\nreleases with version numbers, such as <code>v0.1.7<\/code> or <code>v4.6.0-dev<\/code>. This\nassumes that the tag represents the commit for the release.<\/p>\n<\/li>\n<li>\n<p>Line 10 specifies that the job run in the <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">pgxn\/pgxn-tools<\/a> container, where\nwe have our tools for building and releasing extensions.<\/p>\n<\/li>\n<li>\n<p>Line 13 passes the <code>GITHUB_TOKEN<\/code> variable into the container. This is the\nGitHub <a href=\"https:\/\/github.com\/settings\/tokens\/new\">personal access token<\/a> that&rsquo;s automatically set for every build. It\nlets us call the <a href=\"https:\/\/docs.github.com\/\">GitHub API<\/a> via actions later in the workflow.<\/p>\n<\/li>\n<li>\n<p>Step &ldquo;Bundle the Release&rdquo;, on Lines 17-19, validates the extension\n<code>META.json<\/code> file and creates the release zip file. It does so by simply\nreading the distribution name and version from the <code>META.json<\/code> file and\narchiving the Git repo into a zip file. If your process for creating a\nrelease file is more complicated, you can do it yourself here; just be sure\nto include an <code>id<\/code> for the step, and emit a line of text so that later\nactions know what file to release. The output should be appended to the\n<code>$GITHUB_OUTPUT<\/code> file like this, with <code>$filename<\/code> representing the name of\nthe release file, usually <code>$extension-$version.zip<\/code>:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sh\" data-lang=\"sh\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo<\/span> <span class=\"nv\">bundle<\/span><span class=\"o\">=<\/span><span class=\"nv\">$filename<\/span> &gt;&gt; <span class=\"nv\">$GITHUB_OUTPUT<\/span>\n<\/span><\/span><\/code><\/pre><\/div><\/li>\n<li>\n<p>Step &ldquo;Release on PGXN&rdquo;, on lines 20-25, releases the extension on PGXN. We\ntake this step first because it&rsquo;s the strictest, and therefore the most\nlikely to fail. If it fails, we don&rsquo;t end up with an orphan GitHub release\nto clean up once we&rsquo;ve fixed things for PGXN.<\/p>\n<\/li>\n<li>\n<p>With the success of a PGXN release, step &ldquo;Create GitHub Release&rdquo;, on lines\n26-36, uses the GitHub <a href=\"https:\/\/github.com\/softprops\/action-gh-release\">softprops\/action-gh-release<\/a> action to create a\nrelease corresponding to the tag. You&rsquo;ll want to customize the body of the\nrelease; for the <a href=\"https:\/\/github.com\/theory\/kv-pair\/\">pair extension<\/a>, I added a simple <a href=\"https:\/\/github.com\/theory\/kv-pair\/blob\/798cd00e76b5b029967262101b9bb2c4add0e9d2\/Makefile#L28-L29\">make target<\/a> to\ngenerate a file, then pass it via the <code>body_path<\/code> config:<\/p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-yaml\" data-lang=\"yaml\"><span class=\"line\"><span class=\"cl\">- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Generate Release Changes<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">make latest-changes.md<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\">- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Create GitHub Release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">softprops\/action-gh-release@v2<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">with<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">tag_name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ github.ref }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release ${{ github.ref }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">body_path<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">latest-changes.md<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">files<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ steps.bundle.outputs.bundle }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><\/code><\/pre><\/div><\/li>\n<\/ul>\n<p>Lotta steps, but works nicely. I only wish I could require that the testing\nworkflow finish before doing a release, but I generally tag a release once it\nhas been thoroughly tested in previous commits, so I think it&rsquo;s acceptable.<\/p>\n<p>Now if you&rsquo;ll excuse me, I&rsquo;m off to add this workflow to my other PGXN\nextensions.<\/p>\n\n        <\/div>\n\n        <footer class=\"tags\">\n            <h5>More about\u2026<\/h5>\n            <ul>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/postgres\/\">Postgres<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/pgxn\/\">PGXN<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github\/\">GitHub<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github-actions\/\">GitHub Actions<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/automation\/\">Automation<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/ci\/cd\/\">CI\/CD<\/a><\/li>\n            <\/ul>\n        <\/footer>\n    <\/article>","content_text":"Back in June, I wrote about [testing Postgres extensions][pgxn-tools] on\nmultiple versions of Postgres using [GitHub Actions]. The pattern relies on\nDocker image, [pgxn\/pgxn-tools], which contains scripts to build and run any\nversion of PostgreSQL, install additional dependencies, build, test, bundle, and\nrelease an extension. I've since updated it to support testing on the the latest\ndevelopment release of Postgres, meaning one can test on any major version from\n8.4 to (currently) 14. I've also created GitHub workflows for all of my PGXN\nextensions (except for [pgTAP], which is complicated). I'm quite happy with it.\n\nBut I was never quite satisfied with the release process. Quite a number of\nPostgres extensions also release on GitHub; indeed, [Paul Ramsey] told me\nstraight up that he did not want to manually upload extensions like [pgsql-http]\nand [PostGIS] to PGXN, but for PGXN to automatically pull them in when they were\npublished on GitHub. It's pretty cool that newer packaging systems like\n[pkg.go.dev] auto-index any packages on GibHub. Adding such a feature to PGXN\nwould be an interesting exercise.\n\nBut since I'm low on TUITs for such a significant undertaking, I decided instead\nto work out how to automatically publish a release on GitHub *and* PGXN via\n[GitHub Actions]. After experimenting for a few months, I've worked out a\nstraightforward method that should meet the needs of most projects. I've proven\nthe pattern via the [pair extension]'s [`release.yml`], which successfully\npublished the v0.1.7 release today on both [GitHub][gh-release] and\n[PGXN][pgxn-release]. With that success, I updated the [pgxn\/pgxn-tools]\ndocumentation with a starter example. It looks like this:\n\n```yaml {linenos=table}\nname: Release\non:\n  push:\n    tags:\n      - 'v*' # Push events matching v1.0, v20.15.10, etc.\njobs:\n  release:\n    name: Release on GitHub and PGXN\n    runs-on: ubuntu-latest\n    container: pgxn\/pgxn-tools\n    env:\n      # Required to create GitHub release and upload the bundle.\n      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    steps:\n    - name: Check out the repo\n      uses: actions\/checkout@v4\n    - name: Bundle the Release\n      id: bundle\n      run: pgxn-bundle\n    - name: Release on PGXN\n      env:\n        # Required to release on PGXN.\n        PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}\n        PGXN_USERNAME: ${{ secrets.PGXN_PASSWORD }}\n      run: pgxn-release\n    - name: Create GitHub Release\n      id: release\n      uses: softprops\/action-gh-release@v2\n      with:\n        tag_name: ${{ github.ref }}\n        name: Release ${{ github.ref }}\n        body: |\n          Changes in this Release\n          - First Change\n          - Second Change\n        files: ${{ steps.bundle.outputs.bundle }}\n```\n\n> [!NOTE] Update 2025-02-25\n>\n> Updated the example and description to use `softprops\/action-gh-release` to\n> create a GitHub release and upload files, replacing two steps that used the\n> now-deprecated `actions\/create-release` and `actions\/upload-release-asset`\n> actions.\n\nHere's how it works:\n\n*   Lines 4-5 trigger the workflow only when a tag starting with the letter v is\n    pushed to the repository. This follows the common convention of tagging\n    releases with version numbers, such as `v0.1.7` or `v4.6.0-dev`. This\n    assumes that the tag represents the commit for the release.\n\n*   Line 10 specifies that the job run in the [pgxn\/pgxn-tools] container, where\n    we have our tools for building and releasing extensions.\n\n*   Line 13 passes the `GITHUB_TOKEN` variable into the container. This is the\n    GitHub [personal access token] that's automatically set for every build. It\n    lets us call the [GitHub API] via actions later in the workflow.\n\n*   Step \"Bundle the Release\", on Lines 17-19, validates the extension\n    `META.json` file and creates the release zip file. It does so by simply\n    reading the distribution name and version from the `META.json` file and\n    archiving the Git repo into a zip file. If your process for creating a\n    release file is more complicated, you can do it yourself here; just be sure\n    to include an `id` for the step, and emit a line of text so that later\n    actions know what file to release. The output should be appended to the\n    `$GITHUB_OUTPUT` file like this, with `$filename` representing the name of\n    the release file, usually `$extension-$version.zip`:\n\n    ``` sh\n    echo bundle=$filename >> $GITHUB_OUTPUT\n    ```\n\n*   Step \"Release on PGXN\", on lines 20-25, releases the extension on PGXN. We\n    take this step first because it's the strictest, and therefore the most\n    likely to fail. If it fails, we don't end up with an orphan GitHub release\n    to clean up once we've fixed things for PGXN.\n\n*   With the success of a PGXN release, step \"Create GitHub Release\", on lines\n    26-36, uses the GitHub [softprops\/action-gh-release] action to create a\n    release corresponding to the tag. You'll want to customize the body of the\n    release; for the [pair extension], I added a simple [make target] to\n    generate a file, then pass it via the `body_path` config:\n\n    ``` yaml\n    - name: Generate Release Changes\n      run: make latest-changes.md\n    - name: Create GitHub Release\n      uses: softprops\/action-gh-release@v2\n      with:\n        tag_name: ${{ github.ref }}\n        name: Release ${{ github.ref }}\n        body_path: latest-changes.md\n        files: ${{ steps.bundle.outputs.bundle }}\n    ```\n\nLotta steps, but works nicely. I only wish I could require that the testing\nworkflow finish before doing a release, but I generally tag a release once it\nhas been thoroughly tested in previous commits, so I think it's acceptable.\n\nNow if you'll excuse me, I'm off to add this workflow to my other PGXN\nextensions.\n\n  [pgxn-tools]: https:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/\n    \"Test Postgres Extensions With GitHub Actions\"\n  [GitHub Actions]: https:\/\/github.com\/features\/actions\n  [pgxn\/pgxn-tools]: https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\n  [pgTAP]: https:\/\/pgtap.org\n  [Paul Ramsey]: http:\/\/blog.cleverelephant.ca\n  [pgsql-http]: https:\/\/github.com\/pramsey\/pgsql-http\n  [PostGIS]: http:\/\/postgis.net\n  [pkg.go.dev]: https:\/\/pkg.go.dev\n  [pair extension]: https:\/\/github.com\/theory\/kv-pair\/\n  [`release.yml`]: https:\/\/github.com\/theory\/kv-pair\/blob\/main\/.github\/workflows\/release.yml\n  [gh-release]: https:\/\/github.com\/theory\/kv-pair\/releases\/tag\/v0.1.7\n  [pgxn-release]: https:\/\/pgxn.org\/dist\/pair\/0.1.7\/\n  [personal access token]: https:\/\/github.com\/settings\/tokens\/new\n  [GitHub API]: https:\/\/docs.github.com\/\n  [softprops\/action-gh-release]: https:\/\/github.com\/softprops\/action-gh-release\n  [make target]: https:\/\/github.com\/theory\/kv-pair\/blob\/798cd00e76b5b029967262101b9bb2c4add0e9d2\/Makefile#L28-L29\n\n-------------------------------------------------------------------------------\n\n\u2756 Sunday, 25 Oct 2020\nBy David E. Wheeler\nhttps:\/\/justatheory.com\/2020\/10\/release-postgres-extensions-with-github-actions\/\n\nMore about...\n\n* Postgres:       https:\/\/justatheory.com\/tags\/postgres\/\n* PGXN:           https:\/\/justatheory.com\/tags\/pgxn\/\n* GitHub:         https:\/\/justatheory.com\/tags\/github\/\n* GitHub Actions: https:\/\/justatheory.com\/tags\/github-actions\/\n* Automation:     https:\/\/justatheory.com\/tags\/automation\/\n* CI\/CD:          https:\/\/justatheory.com\/tags\/ci\/cd\/"},{"id":"https:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/","url":"https:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/","title":"Test Postgres Extensions With GitHub Actions","summary":"I finally made the jump from Travis CI to GitHub Actions for my Postgres extensions. Here's how you can, too.","date_published":"2020-06-28T17:52:14Z","date_modified":"2023-02-20T23:55:18Z","authors":[{"name":"David E. Wheeler","url":"https:\/\/justatheory.com\/"}],"tags":["PGXN","GitHub Actions","Automation","CI\/CD"],"content_html":"<article class=\"post\">\n        <div class=\"text\">\n<p>I first heard about <a href=\"https:\/\/github.com\/features\/actions\">GitHub Actions<\/a> a couple years ago, but fully embraced them\nonly in the last few weeks. Part of the challenge has been the paucity of simple\nbut realistic examples, and quite a lot of complicated-looking JavaScript-based\nactions that seem like overkill. But through trial-and-error, I figured out\nenough to update my Postgres extensions projects to automatically test on\nmultiple versions of Postgres, as well as to bundle and release them on <a href=\"https:\/\/pgxn.org\/\" title=\"PGXN: The PostgreSQL Extension Network\">PGXN<\/a>.\nThe first draft of that effort is <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">pgxn\/pgxn-tools<\/a><sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" rel=\"footnote\" role=\"doc-noteref\">1<\/a><\/sup>, a Docker image\nwith scripts to build and run any version of PostgreSQL between 8.4 and 12,\ninstall additional dependencies, build, test, bundle, and release an extension.<\/p>\n<p>Here&rsquo;s how I&rsquo;ve put it to use in a <a href=\"https:\/\/github.com\/theory\/pg-semver\/blob\/c56d76dcbe85e0348b44c6c098560a0df7ab25a5\/.github\/workflows\/ci.yml\">GitHub workflow for semver<\/a>, the\n<a href=\"https:\/\/semver.org\">Semantic Version<\/a> data type:<\/p>\n<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"lnt\"> 1\n<\/span><span class=\"lnt\"> 2\n<\/span><span class=\"lnt\"> 3\n<\/span><span class=\"lnt\"> 4\n<\/span><span class=\"lnt\"> 5\n<\/span><span class=\"lnt\"> 6\n<\/span><span class=\"hl\"><span class=\"lnt\"> 7\n<\/span><\/span><span class=\"lnt\"> 8\n<\/span><span class=\"lnt\"> 9\n<\/span><span class=\"hl\"><span class=\"lnt\">10\n<\/span><\/span><span class=\"lnt\">11\n<\/span><span class=\"hl\"><span class=\"lnt\">12\n<\/span><\/span><span class=\"hl\"><span class=\"lnt\">13\n<\/span><\/span><span class=\"hl\"><span class=\"lnt\">14\n<\/span><\/span><\/code><\/pre><\/td>\n<td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code class=\"language-yaml\" data-lang=\"yaml\"><span class=\"line\"><span class=\"cl\"><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">CI<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"l\">push, pull_request]<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">jobs<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">test<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">strategy<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">matrix<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line hl\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">pg<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"p\">[<\/span><span class=\"m\">12<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">11<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">10<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">9.6<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">9.5<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">9.4<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">9.3<\/span><span class=\"p\">,<\/span><span class=\"w\"> <\/span><span class=\"m\">9.2<\/span><span class=\"p\">]<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">\ud83d\udc18 PostgreSQL ${{ matrix.pg }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">runs-on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">ubuntu-latest<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line hl\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">container<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn\/pgxn-tools<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">steps<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line hl\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pg-start ${{ matrix.pg }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line hl\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">actions\/checkout@v3<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line hl\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pg-build-test<\/span><span class=\"w\">\n<\/span><\/span><\/span><\/code><\/pre><\/td><\/tr><\/table>\n<\/div>\n<\/div><p>The important bits are in the <code>jobs.test<\/code> object. Under <code>strategy.matrix<\/code>, which\ndefines the build matrix, the <code>pg<\/code> array defines each version to be tested. The\njob will run once for each version, and can be referenced via <code>${{ matrix.pg }}<\/code>\nelsewhere in the job. Line 10 has the job a <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">pgxn\/pgxn-tools<\/a> container, where\nthe <code>steps<\/code> run. The are are:<\/p>\n<ul>\n<li>Line 12: Install and start the specified version of PostgreSQL<\/li>\n<li>Line 13: Clone the <a href=\"https:\/\/github.com\/theory\/pg-semver\">semver repository<\/a><\/li>\n<li>Line 14: Build and test the extension<\/li>\n<\/ul>\n<p>The intent here is to cover the vast majority of cases for testing Postgres\nextensions, where a project uses <a href=\"https:\/\/www.postgresql.org\/docs\/current\/extend-pgxs.html\" title=\"PostgreSQL Extension Building Infrastructure\">PGXS<\/a> <code>Makefile<\/code>. The <code>pg-build-test<\/code> script\ndoes just that.<\/p>\n<p>A few notes on the scripts included in <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">pgxn\/pgxn-tools<\/a>:<\/p>\n<ul>\n<li>\n<p><code>pg-start<\/code> installs, initializes, and starts the specified version of Postgres.\nIf you need other dependencies, simply list their <a href=\"https:\/\/www.debian.org\/distrib\/packages#search_packages\" title=\"Search Debian Packages\">Debian package names<\/a>\nafter the Postgres version.<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/pgxn\/pgxnclient\"><code>pgxn<\/code><\/a> is a client for PGXN itself. You can use it to install other\ndependencies required to test your extension.<\/p>\n<\/li>\n<li>\n<p><code>pg-build-test<\/code> simply builds, installs, and tests a PostgreSQL extension or\nother code in the current directory. Effectively the equivalent of\n<code>make &amp;&amp; make install &amp;&amp; make installcheck<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>pgxn-bundle<\/code> validates the <a href=\"http:\/\/manager.pgxn.org\/howto\" title=\"PGXN How To\">PGXN <code>META.json<\/code><\/a> file, reads the distribution\nname and version, and bundles up the project into a zip file for release to\nPGXN.<\/p>\n<\/li>\n<li>\n<p><code>pgxn-release<\/code> uploads a release zip file to <a href=\"https:\/\/pgxn.org\/\" title=\"PGXN: The PostgreSQL Extension Network\">PGXN<\/a>.<\/p>\n<\/li>\n<\/ul>\n<p>In short, use the first three utilities to handle dependencies and test your\nextension, and the last two to release it on PGXN. Simply set <a href=\"https:\/\/help.github.com\/en\/actions\/configuring-and-managing-workflows\/creating-and-storing-encrypted-secrets\">GitHub secrets<\/a>\nwith your PGXN credentials, pass them in environment variables named\n<code>PGXN_USERNAME<\/code> and <code>PGXN_PASSWORD<\/code>, and the script will handle the rest. Here&rsquo;s\nhow a release job might look:<\/p>\n<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"lnt\">15\n<\/span><span class=\"lnt\">16\n<\/span><span class=\"lnt\">17\n<\/span><span class=\"lnt\">18\n<\/span><span class=\"lnt\">19\n<\/span><span class=\"lnt\">20\n<\/span><span class=\"lnt\">21\n<\/span><span class=\"lnt\">22\n<\/span><span class=\"lnt\">23\n<\/span><span class=\"lnt\">24\n<\/span><span class=\"lnt\">25\n<\/span><span class=\"lnt\">26\n<\/span><span class=\"lnt\">27\n<\/span><span class=\"lnt\">28\n<\/span><span class=\"lnt\">29\n<\/span><span class=\"lnt\">30\n<\/span><span class=\"lnt\">31\n<\/span><span class=\"lnt\">32\n<\/span><\/code><\/pre><\/td>\n<td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code class=\"language-yaml\" data-lang=\"yaml\"><span class=\"line\"><span class=\"cl\"><span class=\"w\">  <\/span><span class=\"nt\">release<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release on PGXN<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"c\"># Release pon push to main when the test job succeeds.<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">needs<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">test<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">if<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">github.ref == &#39;refs\/heads\/main&#39; &amp;&amp; github.event_name == &#39;push&#39; &amp;&amp; needs.test.result == &#39;success&#39;<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">runs-on<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">ubuntu-latest<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">container<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">image<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn\/pgxn-tools<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span><span class=\"nt\">env<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">PGXN_USERNAME<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.PGXN_USERNAME }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">PGXN_PASSWORD<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">${{ secrets.PGXN_PASSWORD }}<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    <\/span><span class=\"nt\">steps<\/span><span class=\"p\">:<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Check out the repo<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">uses<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">actions\/checkout@v3<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Bundle the Release<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn-bundle<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      <\/span>- <span class=\"nt\">name<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">Release on PGXN<\/span><span class=\"w\">\n<\/span><\/span><\/span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        <\/span><span class=\"nt\">run<\/span><span class=\"p\">:<\/span><span class=\"w\"> <\/span><span class=\"l\">pgxn-release<\/span><span class=\"w\">\n<\/span><\/span><\/span><\/code><\/pre><\/td><\/tr><\/table>\n<\/div>\n<\/div><p>Note that lines 18-19 require that the <code>test<\/code> job defined above pass, and ensure\nthe job runs only on a push event to the <a href=\"https:\/\/www.hanselman.com\/blog\/EasilyRenameYourGitDefaultBranchFromMasterToMain.aspx\" title=\"Easily rename your Git default branch from master to main\">main branch<\/a>, where  we push final\nreleases. We set <code>PGXN_USERNAME<\/code> and <code>PGXN_PASSWORD<\/code> from the secrets of the\nsame name, and then, in lines 27-32, check out the project, bundle it into a zip\nfile, and release it on PGXN.<\/p>\n<p>There are a few more features of the image, so <a href=\"https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\">read the docs<\/a>\nfor the details. As a first cut at PGXN <a href=\"https:\/\/en.wikipedia.org\/wiki\/CI\/CD\" title=\"Wikipedia: \u201cCI\/CD\u201d\">CI\/CD<\/a> tools, I think it&rsquo;s fairly\nrobust. Still, as I gain experience and build and release more extensions in the\ncoming year, I expect to work out integration with publishing <a href=\"https:\/\/help.github.com\/en\/github\/administering-a-repository\/managing-releases-in-a-repository\">GitHub releases<\/a>,\nand perhaps build and publish relevant actions on the <a href=\"https:\/\/github.com\/marketplace\">GitHub Marketplace<\/a>.<\/p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\">\n<p>Not a great name, I know, will probably change as I learn more.&#160;<a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;<\/a><\/p>\n<\/li>\n<\/ol>\n<\/div>\n\n        <\/div>\n\n        <footer class=\"tags\">\n            <h5>More about\u2026<\/h5>\n            <ul>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/pgxn\/\">PGXN<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/github-actions\/\">GitHub Actions<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/automation\/\">Automation<\/a><\/li>\n                <li><a href=\"https:\/\/justatheory.com\/tags\/ci\/cd\/\">CI\/CD<\/a><\/li>\n            <\/ul>\n        <\/footer>\n    <\/article>","content_text":"I first heard about [GitHub Actions] a couple years ago, but fully embraced them\nonly in the last few weeks. Part of the challenge has been the paucity of simple\nbut realistic examples, and quite a lot of complicated-looking JavaScript-based\nactions that seem like overkill. But through trial-and-error, I figured out\nenough to update my Postgres extensions projects to automatically test on\nmultiple versions of Postgres, as well as to bundle and release them on [PGXN].\nThe first draft of that effort is [pgxn\/pgxn-tools][][^may-rename], a Docker image\nwith scripts to build and run any version of PostgreSQL between 8.4 and 12,\ninstall additional dependencies, build, test, bundle, and release an extension.\n\nHere's how I've put it to use in a [GitHub workflow for semver], the\n[Semantic Version] data type:\n\n```yaml {linenos=table,hl_lines=[7 10 \"12-14\"]}\nname: CI\non: [push, pull_request]\njobs:\n  test:\n    strategy:\n      matrix:\n        pg: [12, 11, 10, 9.6, 9.5, 9.4, 9.3, 9.2]\n    name: \ud83d\udc18 PostgreSQL ${{ matrix.pg }}\n    runs-on: ubuntu-latest\n    container: pgxn\/pgxn-tools\n    steps:\n      - run: pg-start ${{ matrix.pg }}\n      - uses: actions\/checkout@v3\n      - run: pg-build-test\n```\n\nThe important bits are in the `jobs.test` object. Under `strategy.matrix`, which\ndefines the build matrix, the `pg` array defines each version to be tested. The\njob will run once for each version, and can be referenced via `${{ matrix.pg }}`\nelsewhere in the job. Line 10 has the job a [pgxn\/pgxn-tools] container, where\nthe `steps` run. The are are:\n\n*   Line 12: Install and start the specified version of PostgreSQL\n*   Line 13: Clone the [semver repository]\n*   Line 14: Build and test the extension\n\nThe intent here is to cover the vast majority of cases for testing Postgres\nextensions, where a project uses [PGXS] `Makefile`. The `pg-build-test` script\ndoes just that.\n\nA few notes on the scripts included in [pgxn\/pgxn-tools]:\n\n*   `pg-start` installs, initializes, and starts the specified version of Postgres.\n    If you need other dependencies, simply list their [Debian package names]\n    after the Postgres version.\n\n*   [`pgxn`] is a client for PGXN itself. You can use it to install other\n    dependencies required to test your extension.\n\n*   `pg-build-test` simply builds, installs, and tests a PostgreSQL extension or\n    other code in the current directory. Effectively the equivalent of\n    `make && make install && make installcheck`.\n\n*   `pgxn-bundle` validates the [PGXN `META.json`] file, reads the distribution\n    name and version, and bundles up the project into a zip file for release to\n    PGXN.\n\n*   `pgxn-release` uploads a release zip file to [PGXN].\n\nIn short, use the first three utilities to handle dependencies and test your\nextension, and the last two to release it on PGXN. Simply set [GitHub secrets]\nwith your PGXN credentials, pass them in environment variables named\n`PGXN_USERNAME` and `PGXN_PASSWORD`, and the script will handle the rest. Here's\nhow a release job might look:\n\n```yaml {linenos=table,linenostart=15}\n  release:\n    name: Release on PGXN\n    # Release pon push to main when the test job succeeds.\n    needs: test\n    if: github.ref == 'refs\/heads\/main' && github.event_name == 'push' && needs.test.result == 'success'\n    runs-on: ubuntu-latest\n    container:\n      image: pgxn\/pgxn-tools\n      env:\n        PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}\n        PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}\n    steps:\n      - name: Check out the repo\n        uses: actions\/checkout@v3\n      - name: Bundle the Release\n        run: pgxn-bundle\n      - name: Release on PGXN\n        run: pgxn-release\n```\n\nNote that lines 18-19 require that the `test` job defined above pass, and ensure\nthe job runs only on a push event to the [main branch], where  we push final\nreleases. We set `PGXN_USERNAME` and `PGXN_PASSWORD` from the secrets of the\nsame name, and then, in lines 27-32, check out the project, bundle it into a zip\nfile, and release it on PGXN.\n\nThere are a few more features of the image, so [read the docs][pgxn\/pgxn-tools]\nfor the details. As a first cut at PGXN [CI\/CD] tools, I think it's fairly\nrobust. Still, as I gain experience and build and release more extensions in the\ncoming year, I expect to work out integration with publishing [GitHub releases],\nand perhaps build and publish relevant actions on the [GitHub Marketplace].\n\n  [GitHub Actions]: https:\/\/github.com\/features\/actions\n  [PGXN]: https:\/\/pgxn.org\/ \"PGXN: The PostgreSQL Extension Network\"\n  [pgxn\/pgxn-tools]: https:\/\/hub.docker.com\/repository\/docker\/pgxn\/pgxn-tools\n  [GitHub workflow for semver]:\n    https:\/\/github.com\/theory\/pg-semver\/blob\/c56d76dcbe85e0348b44c6c098560a0df7ab25a5\/.github\/workflows\/ci.yml\n  [Semantic Version]: https:\/\/semver.org\n  [semver repository]: https:\/\/github.com\/theory\/pg-semver\n  [Debian package names]: https:\/\/www.debian.org\/distrib\/packages#search_packages\n    \"Search Debian Packages\"\n  [PGXS]: https:\/\/www.postgresql.org\/docs\/current\/extend-pgxs.html\n    \"PostgreSQL Extension Building Infrastructure\"\n  [`pgxn`]: https:\/\/github.com\/pgxn\/pgxnclient\n  [main branch]: https:\/\/www.hanselman.com\/blog\/EasilyRenameYourGitDefaultBranchFromMasterToMain.aspx\n    \"Easily rename your Git default branch from master to main\"\n  [CI\/CD]: https:\/\/en.wikipedia.org\/wiki\/CI\/CD \"Wikipedia: \u201cCI\/CD\u201d\"\n  [GitHub releases]:\n    https:\/\/help.github.com\/en\/github\/administering-a-repository\/managing-releases-in-a-repository\n  [GitHub Marketplace]: https:\/\/github.com\/marketplace\n  [GitHub secrets]:\n    https:\/\/help.github.com\/en\/actions\/configuring-and-managing-workflows\/creating-and-storing-encrypted-secrets\n  [PGXN `META.json`]: http:\/\/manager.pgxn.org\/howto \"PGXN How To\"\n\n  [^may-rename]: Not a great name, I know, will probably change as I learn more.\n\n-------------------------------------------------------------------------------\n\n\u2756 Sunday, 28 Jun 2020\nBy David E. Wheeler\nhttps:\/\/justatheory.com\/2020\/06\/test-extensions-with-github-actions\/\n\nMore about...\n\n* PGXN:           https:\/\/justatheory.com\/tags\/pgxn\/\n* GitHub Actions: https:\/\/justatheory.com\/tags\/github-actions\/\n* Automation:     https:\/\/justatheory.com\/tags\/automation\/\n* CI\/CD:          https:\/\/justatheory.com\/tags\/ci\/cd\/"}]}