{"id":5221,"date":"2026-08-30T11:00:40","date_gmt":"2026-08-30T02:00:40","guid":{"rendered":"https:\/\/donguri3.net\/server-tech\/papermc-hangar-github-actions-quickshare-2\/"},"modified":"2026-08-30T11:00:41","modified_gmt":"2026-08-30T02:00:41","slug":"papermc-hangar-github-actions-quickshare","status":"publish","type":"post","link":"https:\/\/donguri3.net\/en\/server-tech\/minecraft\/papermc-hangar-github-actions-quickshare\/","title":{"rendered":"Automating PaperMC Plugin Releases with GitHub Actions"},"content":{"rendered":"<p>I set up an environment at home to practice running the PDCA cycle through Minecraft, and as a supportive tool for that, I created a whiteboard plugin.<\/p>\n<ul>\n<li><a href=\"https:\/\/donguri3.net\/server-tech\/minecraft-pdca-education\/\">Learning the PDCA Cycle in Minecraft: Fostering Critical Thinking While Playing<\/a><\/li>\n<li><a href=\"https:\/\/donguri3.net\/server-tech\/minecraft-paper-server-setup\/\">How to Set Up a Minecraft Server: A Guide to Setting Up a Paper Server for Learning with Kids<\/a><\/li>\n<\/ul>\n<p>Honestly, if you&#8217;re just running the PDCA cycle for yourself, publishing to a distribution site isn&#8217;t strictly necessary. However, with the mindset of &#8220;since I went through the trouble of making it, let&#8217;s publish it so others can use it too,&#8221; I set up a distribution pipeline. Specifically, I made it possible to publish directly to Hangar (PaperMC&#8217;s official distribution site) using just GitHub Actions, creating a workflow that allows me to focus on improving the README and taking screenshots from now on.<\/p>\n<h2>Tools and Versions<\/h2>\n<table style=\"width: 738px; height: 552px;\">\n<thead>\n<tr style=\"height: 74px;\">\n<th style=\"width: 137.906px; height: 74px;\">Element<\/th>\n<th style=\"width: 80.9375px; height: 74px;\">Version<\/th>\n<th style=\"width: 518.156px; height: 74px;\">Role<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr style=\"height: 45px;\">\n<td style=\"width: 137.906px; height: 45px;\">Java (JDK)<\/td>\n<td style=\"width: 80.9375px; height: 45px;\">21<\/td>\n<td style=\"width: 518.156px; height: 45px;\">Mandatory requirement for Paper 1.20.5 and later. Unified to JDK 21 for both local and CI environments.<\/td>\n<\/tr>\n<tr style=\"height: 75px;\">\n<td style=\"width: 137.906px; height: 75px;\">Gradle (Wrapper)<\/td>\n<td style=\"width: 80.9375px; height: 75px;\"><em>9.1.0<\/em><\/td>\n<td style=\"width: 518.156px; height: 75px;\">Java build automation tool. Automates building and publishing. Make sure to commit <code>gradle\/wrapper\/gradle-wrapper.jar<\/code>.<\/td>\n<\/tr>\n<tr style=\"height: 45px;\">\n<td style=\"width: 137.906px; height: 45px;\">Paper<\/td>\n<td style=\"width: 80.9375px; height: 45px;\">1.21.9<\/td>\n<td style=\"width: 518.156px; height: 45px;\">Minecraft server<\/td>\n<\/tr>\n<tr style=\"height: 45px;\">\n<td style=\"width: 137.906px; height: 45px;\">GitHub<\/td>\n<td style=\"width: 80.9375px; height: 45px;\">\u2014<\/td>\n<td style=\"width: 518.156px; height: 45px;\">Java source repository, Issue management, Release management.<\/td>\n<\/tr>\n<tr style=\"height: 75px;\">\n<td style=\"width: 137.906px; height: 75px;\">GitHub Actions<\/td>\n<td style=\"width: 80.9375px; height: 75px;\">\u2014<\/td>\n<td style=\"width: 518.156px; height: 75px;\">Automated build &amp; publishing (push \u2192 Snapshot, tag \u2192 Release). Stores <code>HANGAR_API_TOKEN<\/code> in Secrets.<\/td>\n<\/tr>\n<tr style=\"height: 74px;\">\n<td style=\"width: 137.906px; height: 74px;\">Hangar<\/td>\n<td style=\"width: 80.9375px; height: 74px;\">\u2014<\/td>\n<td style=\"width: 518.156px; height: 74px;\">PaperMC&#8217;s official plugin distribution site. Used for Project creation, Channel setup (color required for Snapshot), and API Key generation.<\/td>\n<\/tr>\n<tr style=\"height: 74px;\">\n<td style=\"width: 137.906px; height: 74px;\">Hangar API key<\/td>\n<td style=\"width: 80.9375px; height: 74px;\">\u2014<\/td>\n<td style=\"width: 518.156px; height: 74px;\">Used by GitHub to upload plugin files to Hangar.<br \/>Registered as HANGAR_API_TOKEN in GitHub repository Secrets.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>What You Will Gain From This Article (Overview)<\/h2>\n<ul>\n<li>Creating a project on Hangar and publishing the plugin<\/li>\n<li>Generating an API Key on Hangar and saving it to GitHub Repository Secrets<\/li>\n<li>Placing necessary files in the GitHub repository (correct location for plugin.yml, committing the Gradle Wrapper JAR)<\/li>\n<li>Using GitHub Actions for:\n<ul>\n<li>Pushing to <code>main<\/code> \u2192 Automatic Snapshot publication<\/li>\n<li>Pushing a <code>v*<\/code> tag \u2192 Automatic Release publication<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><a href=\"https:\/\/hangar.papermc.io\/nando256\/whiteboard\" data-blogcard=\"1\">Whiteboard (Paper plugin) <\/a><\/p>\n<p><a href=\"https:\/\/github.com\/nando256\/whiteboard\" data-blogcard=\"1\">Github nando256 whiteboard<\/a><\/p>\n<h2>Step 1: Initial Setup on Hangar<\/h2>\n<ol>\n<li>Create Project<br \/>\nDetermine the name and slug to create it (will be used later in Gradle&#8217;s <code>id.set(&#8220;&#8221;)<\/code>).<\/li>\n<li>Add Channel (Snapshot) *The Release channel exists by default<br \/>\nProjects \u2192 Channels \u2192 New<br \/>\nName: Snapshot, make sure to select a Color (without a color, it will fail via API with &#8230;noColor).<br \/>\n<a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de.png\"><img decoding=\"async\" class=\"aligncenter size-large wp-image-2254\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-1024x455.png\" alt=\"\" width=\"1024\" height=\"455\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-1024x455.png 1024w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-300x133.png 300w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-768x341.png 768w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-530x235.png 530w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-565x251.png 565w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-710x315.png 710w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de-725x322.png 725w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/a9fcd98f3e43fc8b23169cfd5721d9de.png 1274w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/li>\n<li>Generate API Key<br \/>\nAccount Settings \u2192 API Keys \u2192 Create<br \/>\nSave it later in GitHub Secrets (do not hardcode it in the repository).<br \/>\n<a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/11090290e4f733525aa36dc7a7b898fa.png\"><img decoding=\"async\" class=\"aligncenter size-full wp-image-2256\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/11090290e4f733525aa36dc7a7b898fa.png\" alt=\"\" width=\"377\" height=\"236\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/11090290e4f733525aa36dc7a7b898fa.png 377w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/11090290e4f733525aa36dc7a7b898fa-300x188.png 300w\" sizes=\"(max-width: 377px) 100vw, 377px\" \/><\/a> <a href=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4.png\"><img decoding=\"async\" class=\"aligncenter size-large wp-image-2257\" src=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-1024x424.png\" alt=\"\" width=\"1024\" height=\"424\" srcset=\"https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-1024x424.png 1024w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-300x124.png 300w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-768x318.png 768w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-530x220.png 530w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-565x234.png 565w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-710x294.png 710w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4-725x301.png 725w, https:\/\/donguri3.net\/wp-content\/uploads\/2025\/10\/65e364e39255037d47258962fd39dba4.png 1269w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/li>\n<\/ol>\n<h2>Step 2: Minimum GitHub Repository Structure<\/h2>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">whiteboard\/\n\u251c\u2500 build.gradle.kts\n\u251c\u2500 settings.gradle.kts\n\u251c\u2500 gradle\/wrapper\/gradle-wrapper.jar   \u2190 \u2605 Make sure to commit the JAR as well\n\u251c\u2500 gradle\/wrapper\/gradle-wrapper,properties\n\u251c\u2500 gradlew \/ gradlew.bat                             \u2190 gradlew requires execute permission (+x)\n\u251c\u2500 src\/main\/java\/net\/nando256\/whiteboard\/WhiteboardPlugin.java\n\u2514\u2500 src\/main\/resources\/plugin.yml                     \u2190 \u2605 Place here (do not put directly under root)<\/pre>\n<h3>plugin.yml<\/h3>\n<pre class=\"brush: yaml; title: ; notranslate\" title=\"\">\/\/ build.gradle.kts (Excerpt)\nimport io.papermc.hangarpublishplugin.model.Platforms\n\nplugins {\n    java\n    id(&quot;io.papermc.hangar-publish-plugin&quot;) version &quot;0.1.3&quot;\n}\n\ngroup = &quot;net.nando256&quot;\n\/\/ Use version.override if supplied from CI, otherwise use local default\nversion = (findProperty(&quot;version.override&quot;) as String?) ?: &quot;0.0.0-local&quot;\n\njava { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) }\n\nrepositories {\n    mavenCentral()\n    maven(&quot;https:\/\/repo.papermc.io\/repository\/maven-public\/&quot;)\n}\n\ndependencies {\n    compileOnly(&quot;io.papermc.paper:paper-api:1.21.9-R0.1-SNAPSHOT&quot;)\n}\n\n\/\/ Expand ${version} in plugin.yml using Gradle's version\ntasks.processResources {\n    filesMatching(&quot;plugin.yml&quot;) { expand(&quot;version&quot; to project.version) }\n}\n\nhangarPublish {\n    publications.register(&quot;plugin&quot;) {\n        id.set(&quot;whiteboard&quot;)                         \/\/ \u2190 Hangar project slug\n        version.set(project.version.toString())\n        channel.set(providers.gradleProperty(&quot;hangar.channel&quot;).orElse(&quot;Snapshot&quot;))\n        apiKey.set(System.getenv(&quot;HANGAR_API_TOKEN&quot;))\n        platforms {\n            register(Platforms.PAPER) {\n                jar.set(tasks.jar.flatMap { it.archiveFile })\n                platformVersions.set(listOf(&quot;1.21.9&quot;)) \/\/ Multiple versions can be specified\n            }\n        }\n    }\n}\n<\/pre>\n<h2>Step 4: GitHub Actions (push = Snapshot \/ tag = Release)<\/h2>\n<p>.github\/workflows\/publish.yml<\/p>\n<pre class=\"brush: yaml; title: ; notranslate\" title=\"\">name: Publish to Hangar\n\non:\n  push:\n    branches: &#x5B; &quot;main&quot; ]\n    tags: &#x5B; &quot;v*&quot; ]\n\njobs:\n  build-and-publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: JDK 21\n        uses: actions\/setup-java@v4\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Gradle\n        uses: gradle\/actions\/setup-gradle@v4\n\n      - name: Determine channel &amp; version\n        id: meta\n        run: |\n          if &#x5B;&#x5B; &quot;${GITHUB_REF}&quot; == refs\/tags\/* ]]; then\n            echo &quot;channel=Release&quot; &gt;&gt; $GITHUB_OUTPUT\n            echo &quot;version=${GITHUB_REF#refs\/tags\/v}&quot; &gt;&gt; $GITHUB_OUTPUT\n          else\n            echo &quot;channel=Snapshot&quot; &gt;&gt; $GITHUB_OUTPUT\n            echo &quot;version=0.0.0-${GITHUB_SHA::7}-SNAPSHOT&quot; &gt;&gt; $GITHUB_OUTPUT\n          fi\n\n      - name: Build\n        run: .\/gradlew -q clean build\n\n      - name: Publish to Hangar\n        env:\n          HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }}\n        run: |\n          .\/gradlew \\\n            -Phangar.channel=${{ steps.meta.outputs.channel }} \\\n            -Pversion.override=${{ steps.meta.outputs.version }} \\\n            publishPluginPublicationToHangar<\/pre>\n<h4>Registering Secrets<\/h4>\n<p>GitHub \u2192 Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret<\/p>\n<ul>\n<li>Name: HANGAR_API_TOKEN<\/li>\n<li>Value: (API Key generated on Hangar)<\/li>\n<\/ul>\n<h2>Operational Rules (Just Remember These)<\/h2>\n<ul>\n<li>During Development: Push to <code>main<\/code> \u2192 Automatically published as a Snapshot (<code>0.0.0--SNAPSHOT<\/code>)<\/li>\n<li>For Release: <code>git tag v0.1.0 &amp;&amp; git push --tags<\/code> \u2192 Automatically published as a Release<\/li>\n<li>Version Name Conflicts: Hangar does not allow duplicate names even across different channels. If a conflict occurs, bump the tag or delete the older version.<\/li>\n<\/ul>\n<h2>Common Issues and Quick Fixes<\/h2>\n<ul>\n<li>version.new.error.channel.noColor<br \/>\n\u2192 Create the Snapshot channel via the UI and &#8220;assign a color&#8221;<\/li>\n<li>version.new.error.duplicateNameAndPlatform<br \/>\n\u2192 A version with the same name already exists. Avoid collisions by including the SHA for Snapshots and using tags for Releases.<\/li>\n<li>Unable to access \u2026 gradle-wrapper.jar<br \/>\n\u2192 Wrapper JAR is not committed. Review <code>.gitignore<\/code> and run <code>git add -f gradle\/wrapper\/gradle-wrapper.jar<\/code><\/li>\n<li>Build\/Startup Failure<br \/>\n\u2192 Assumes Java 21 (fix both local and CI to 21), ensure <code>plugin.yml<\/code> is under <code>src\/main\/resources\/<\/code><\/li>\n<\/ul>\n<h2>Conclusion (Leave Publishing to Automation, Focus Entirely on Creation)<\/h2>\n<p>Publishing isn&#8217;t strictly required to run a PDCA cycle. However, putting what you&#8217;ve made out into the world might help others learn and play.<br \/>\nFollowing the steps in this article, distribution is reduced to just pushing or tagging, leaving you with more time to spend on READMEs, screenshots, and feature improvements. It takes very little effort while delivering huge benefits\u2014that&#8217;s what building this distribution pipeline is all about.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I set up an environment at home to practice running the PDCA cycle through Minecraft, and as a supportive tool [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":2266,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_locale":"en_US","_original_post":"https:\/\/donguri3.net\/?p=2210","footnotes":""},"categories":[1168],"tags":[51,52,388,836,876,881,291,10,179],"class_list":["post-5221","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-minecraft","tag-git","tag-https","tag-java-21","tag-minecraft","tag-papermc","tag-pdca","tag-yaml","tag-server","tag-179","en-US"],"_links":{"self":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5221","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/comments?post=5221"}],"version-history":[{"count":1,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5221\/revisions"}],"predecessor-version":[{"id":5224,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/posts\/5221\/revisions\/5224"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/media\/2266"}],"wp:attachment":[{"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/media?parent=5221"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/categories?post=5221"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/donguri3.net\/wp-json\/wp\/v2\/tags?post=5221"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}