diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e9d6e90..985006d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,23 +1,76 @@
-name: release
+name: Release
on:
- push:
- tags:
- - "v*.*.*"
- - "v*.*.*-pre"
workflow_dispatch:
+ inputs:
+ package:
+ description: "The published name of a single package to release"
+ type: choice
+ required: false
+ options:
+ - "all"
+ - "@nanoforge-dev/actions"
+ - "@nanoforge-dev/asset-manager"
+ - "@nanoforge-dev/common"
+ - "@nanoforge-dev/config"
+ - "@nanoforge-dev/core"
+ - "@nanoforge-dev/docgen"
+ - "@nanoforge-dev/ecs-client"
+ - "@nanoforge-dev/ecs-lib"
+ - "@nanoforge-dev/ecs-server"
+ - "@nanoforge-dev/graphics-2d"
+ - "@nanoforge-dev/input"
+ - "@nanoforge-dev/music"
+ - "@nanoforge-dev/network-client"
+ - "@nanoforge-dev/network-server"
+ - "@nanoforge-dev/sound"
+ - "@nanoforge-dev/utils-eslint-config"
+ - "@nanoforge-dev/utils-prettier-config"
+ exclude:
+ description: "Comma separated list of packages to exclude from release (if not depended upon)"
+ required: false
+ type: string
+ dry_run:
+ description: Perform a dry run?
+ type: boolean
+ default: false
+
+env:
+ EM_CACHE_FOLDER: "emsdk-cache"
jobs:
- release:
+ npm-publish:
+ name: npm publish
runs-on: ubuntu-latest
- name: Building stack
+ if: github.repository_owner == 'NanoForge-dev'
steps:
- - name: Checkout
- uses: actions/checkout@v5
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Install Node.js v24
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ package-manager-cache: false
+ registry-url: https://registry.npmjs.org/
+
+ - name: Install dependencies
+ uses: ./utils/actions/src/pnpm-install
+
+ - name: "Install emscipten"
+ uses: "mymindstorm/setup-emsdk@v14"
+ with:
+ actions-cache-folder: ${{env.EM_CACHE_FOLDER}}
+
+ - name: Build dependencies
+ run: pnpm run build
- - name: Release
- uses: softprops/action-gh-release@v2
- if: startsWith(github.ref, 'refs/tags/')
+ - name: Release packages
+ uses: ./utils/actions/src/release-packages
with:
- files: |
- CHANGELOG.md
+ package: ${{ inputs.package }}
+ exclude: ${{ inputs.exclude }}
+ dry: ${{ inputs.dry_run }}
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.idea/betterCommentsSettings.xml b/.idea/betterCommentsSettings.xml
new file mode 100644
index 0000000..4f152ed
--- /dev/null
+++ b/.idea/betterCommentsSettings.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b0546b8..1988bfa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,7 +32,7 @@ importers:
version: 24.10.2
'@vitest/coverage-v8':
specifier: ^4.0.15
- version: 4.0.15(vitest@4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2))
+ version: 4.0.15(vitest@4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@2.6.1)
@@ -56,7 +56,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.15
- version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
packages/asset-manager:
dependencies:
@@ -90,7 +90,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.15
- version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
packages/common:
devDependencies:
@@ -240,7 +240,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.15
- version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
packages/ecs-lib:
dependencies:
@@ -277,7 +277,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.15
- version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
packages/ecs-server:
dependencies:
@@ -323,7 +323,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.15
- version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
packages/graphics-2d:
dependencies:
@@ -360,7 +360,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.15
- version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ version: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
packages/input:
dependencies:
@@ -535,6 +535,52 @@ importers:
specifier: ^5.9.3
version: 5.9.3
+ utils/actions:
+ dependencies:
+ '@actions/core':
+ specifier: ^2.0.1
+ version: 2.0.1
+ '@actions/github':
+ specifier: ^6.0.1
+ version: 6.0.1
+ commander:
+ specifier: ^14.0.2
+ version: 14.0.2
+ devDependencies:
+ '@favware/cliff-jumper':
+ specifier: ^6.0.0
+ version: 6.0.0
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: workspace:^
+ version: link:../eslint-config
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: workspace:^
+ version: link:../prettier-config
+ '@npm/types':
+ specifier: ^2.1.0
+ version: 2.1.0
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: ^6.0.0
+ version: 6.0.0(prettier@3.7.4)
+ '@types/bun':
+ specifier: ^1.3.5
+ version: 1.3.5
+ eslint:
+ specifier: ^9.39.1
+ version: 9.39.1(jiti@2.6.1)
+ prettier:
+ specifier: ^3.6.2
+ version: 3.7.4
+ terser:
+ specifier: ^5.44.1
+ version: 5.44.1
+ tsup:
+ specifier: ^8.5.1
+ version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2)
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
utils/eslint-config:
dependencies:
'@eslint/js':
@@ -589,6 +635,24 @@ importers:
packages:
+ '@actions/core@2.0.1':
+ resolution: {integrity: sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==}
+
+ '@actions/exec@2.0.0':
+ resolution: {integrity: sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==}
+
+ '@actions/github@6.0.1':
+ resolution: {integrity: sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==}
+
+ '@actions/http-client@2.2.3':
+ resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
+
+ '@actions/http-client@3.0.1':
+ resolution: {integrity: sha512-SbGS8c/vySbNO3kjFgSW77n83C4MQx/Yoe+b1hAdpuvfHxnkHzDq2pWljUpAA56Si1Gae/7zjeZsV0CYjmLo/w==}
+
+ '@actions/io@2.0.0':
+ resolution: {integrity: sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==}
+
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -1070,6 +1134,10 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@fastify/busboy@2.1.1':
+ resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
+ engines: {node: '>=14'}
+
'@favware/cliff-jumper@6.0.0':
resolution: {integrity: sha512-9uXg/fGHFLh4AnG3HCtlrrrmDvUnmr5vrbs7H9pet3WlUCsGGGqeNT0bFb8LG0M0GatYUi9RM/F60p1yn2ndEA==}
engines: {node: '>=v18'}
@@ -1106,6 +1174,9 @@ packages:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
+ '@jridgewell/source-map@0.3.11':
+ resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
+
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -1117,10 +1188,22 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ '@npm/types@2.1.0':
+ resolution: {integrity: sha512-humQVe2BrWR7Yum5hGDYBnIPnnZJvKSOH/I4QN1ZL2bdb4c4zQHaHupEJ3cOkSJ07G3YfN793ptbNh196BWLgA==}
+ engines: {node: '>=18.6.0'}
+
+ '@octokit/auth-token@4.0.0':
+ resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
+ engines: {node: '>= 18'}
+
'@octokit/auth-token@5.1.2':
resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==}
engines: {node: '>= 18'}
+ '@octokit/core@5.2.2':
+ resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==}
+ engines: {node: '>= 18'}
+
'@octokit/core@6.1.6':
resolution: {integrity: sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==}
engines: {node: '>= 18'}
@@ -1129,27 +1212,67 @@ packages:
resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==}
engines: {node: '>= 18'}
+ '@octokit/endpoint@9.0.6':
+ resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==}
+ engines: {node: '>= 18'}
+
+ '@octokit/graphql@7.1.1':
+ resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==}
+ engines: {node: '>= 18'}
+
'@octokit/graphql@8.2.2':
resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==}
engines: {node: '>= 18'}
+ '@octokit/openapi-types@20.0.0':
+ resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==}
+
+ '@octokit/openapi-types@24.2.0':
+ resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==}
+
'@octokit/openapi-types@25.1.0':
resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==}
+ '@octokit/plugin-paginate-rest@9.2.2':
+ resolution: {integrity: sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ '@octokit/core': '5'
+
+ '@octokit/plugin-rest-endpoint-methods@10.4.1':
+ resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ '@octokit/core': '5'
+
'@octokit/plugin-retry@7.2.1':
resolution: {integrity: sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '>=6'
+ '@octokit/request-error@5.1.1':
+ resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==}
+ engines: {node: '>= 18'}
+
'@octokit/request-error@6.1.8':
resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==}
engines: {node: '>= 18'}
+ '@octokit/request@8.4.1':
+ resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==}
+ engines: {node: '>= 18'}
+
'@octokit/request@9.2.4':
resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==}
engines: {node: '>= 18'}
+ '@octokit/types@12.6.0':
+ resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==}
+
+ '@octokit/types@13.10.0':
+ resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
+
'@octokit/types@14.1.0':
resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==}
@@ -1304,6 +1427,9 @@ packages:
svelte:
optional: true
+ '@types/bun@1.3.5':
+ resolution: {integrity: sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==}
+
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -1498,6 +1624,9 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ before-after-hook@2.2.3:
+ resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
+
before-after-hook@3.0.2:
resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==}
@@ -1514,6 +1643,12 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+ bun-types@1.3.5:
+ resolution: {integrity: sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==}
+
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1584,6 +1719,9 @@ packages:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
+ commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -1669,6 +1807,9 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deprecation@2.3.1:
+ resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -2272,6 +2413,9 @@ packages:
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
@@ -2467,6 +2611,13 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
@@ -2538,6 +2689,11 @@ packages:
resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==}
engines: {node: '>=18'}
+ terser@5.44.1:
+ resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==}
+ engines: {node: '>=10'}
+ hasBin: true
+
text-extensions@2.4.0:
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
engines: {node: '>=8'}
@@ -2612,6 +2768,10 @@ packages:
typescript:
optional: true
+ tunnel@0.0.6:
+ resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
+ engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
+
turbo-darwin-64@2.6.3:
resolution: {integrity: sha512-BlJJDc1CQ7SK5Y5qnl7AzpkvKSnpkfPmnA+HeU/sgny3oHZckPV2776ebO2M33CYDSor7+8HQwaodY++IINhYg==}
cpu: [x64]
@@ -2668,6 +2828,10 @@ packages:
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+ undici@5.29.0:
+ resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
+ engines: {node: '>=14.0'}
+
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@@ -2676,6 +2840,9 @@ packages:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
+ universal-user-agent@6.0.1:
+ resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
+
universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
@@ -2791,6 +2958,9 @@ packages:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
wrtc@0.4.7:
resolution: {integrity: sha512-P6Hn7VT4lfSH49HxLHcHhDq+aFf/jd9dPY7lDHeFhZ22N3858EKuwm2jmnlPzpsRGEPaoF6XwkcxY5SYnt4f/g==}
engines: {node: ^8.11.2 || >=10.0.0}
@@ -2844,6 +3014,37 @@ packages:
snapshots:
+ '@actions/core@2.0.1':
+ dependencies:
+ '@actions/exec': 2.0.0
+ '@actions/http-client': 3.0.1
+
+ '@actions/exec@2.0.0':
+ dependencies:
+ '@actions/io': 2.0.0
+
+ '@actions/github@6.0.1':
+ dependencies:
+ '@actions/http-client': 2.2.3
+ '@octokit/core': 5.2.2
+ '@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.2)
+ '@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.2)
+ '@octokit/request': 8.4.1
+ '@octokit/request-error': 5.1.1
+ undici: 5.29.0
+
+ '@actions/http-client@2.2.3':
+ dependencies:
+ tunnel: 0.0.6
+ undici: 5.29.0
+
+ '@actions/http-client@3.0.1':
+ dependencies:
+ tunnel: 0.0.6
+ undici: 5.29.0
+
+ '@actions/io@2.0.0': {}
+
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
@@ -3219,6 +3420,8 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
+ '@fastify/busboy@2.1.1': {}
+
'@favware/cliff-jumper@6.0.0':
dependencies:
'@favware/colorette-spinner': 1.0.1
@@ -3262,6 +3465,11 @@ snapshots:
'@jridgewell/resolve-uri@3.1.2': {}
+ '@jridgewell/source-map@0.3.11':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
@@ -3282,8 +3490,22 @@ snapshots:
- encoding
- supports-color
+ '@npm/types@2.1.0': {}
+
+ '@octokit/auth-token@4.0.0': {}
+
'@octokit/auth-token@5.1.2': {}
+ '@octokit/core@5.2.2':
+ dependencies:
+ '@octokit/auth-token': 4.0.0
+ '@octokit/graphql': 7.1.1
+ '@octokit/request': 8.4.1
+ '@octokit/request-error': 5.1.1
+ '@octokit/types': 13.10.0
+ before-after-hook: 2.2.3
+ universal-user-agent: 6.0.1
+
'@octokit/core@6.1.6':
dependencies:
'@octokit/auth-token': 5.1.2
@@ -3299,14 +3521,39 @@ snapshots:
'@octokit/types': 14.1.0
universal-user-agent: 7.0.3
+ '@octokit/endpoint@9.0.6':
+ dependencies:
+ '@octokit/types': 13.10.0
+ universal-user-agent: 6.0.1
+
+ '@octokit/graphql@7.1.1':
+ dependencies:
+ '@octokit/request': 8.4.1
+ '@octokit/types': 13.10.0
+ universal-user-agent: 6.0.1
+
'@octokit/graphql@8.2.2':
dependencies:
'@octokit/request': 9.2.4
'@octokit/types': 14.1.0
universal-user-agent: 7.0.3
+ '@octokit/openapi-types@20.0.0': {}
+
+ '@octokit/openapi-types@24.2.0': {}
+
'@octokit/openapi-types@25.1.0': {}
+ '@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2)':
+ dependencies:
+ '@octokit/core': 5.2.2
+ '@octokit/types': 12.6.0
+
+ '@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.2)':
+ dependencies:
+ '@octokit/core': 5.2.2
+ '@octokit/types': 12.6.0
+
'@octokit/plugin-retry@7.2.1(@octokit/core@6.1.6)':
dependencies:
'@octokit/core': 6.1.6
@@ -3314,10 +3561,23 @@ snapshots:
'@octokit/types': 14.1.0
bottleneck: 2.19.5
+ '@octokit/request-error@5.1.1':
+ dependencies:
+ '@octokit/types': 13.10.0
+ deprecation: 2.3.1
+ once: 1.4.0
+
'@octokit/request-error@6.1.8':
dependencies:
'@octokit/types': 14.1.0
+ '@octokit/request@8.4.1':
+ dependencies:
+ '@octokit/endpoint': 9.0.6
+ '@octokit/request-error': 5.1.1
+ '@octokit/types': 13.10.0
+ universal-user-agent: 6.0.1
+
'@octokit/request@9.2.4':
dependencies:
'@octokit/endpoint': 10.1.4
@@ -3326,6 +3586,14 @@ snapshots:
fast-content-type-parse: 2.0.1
universal-user-agent: 7.0.3
+ '@octokit/types@12.6.0':
+ dependencies:
+ '@octokit/openapi-types': 20.0.0
+
+ '@octokit/types@13.10.0':
+ dependencies:
+ '@octokit/openapi-types': 24.2.0
+
'@octokit/types@14.1.0':
dependencies:
'@octokit/openapi-types': 25.1.0
@@ -3422,6 +3690,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@types/bun@1.3.5':
+ dependencies:
+ bun-types: 1.3.5
+
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -3545,7 +3817,7 @@ snapshots:
'@typescript-eslint/types': 8.49.0
eslint-visitor-keys: 4.2.1
- '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2))':
+ '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.15
@@ -3558,7 +3830,7 @@ snapshots:
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
- vitest: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ vitest: 4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -3571,13 +3843,13 @@ snapshots:
chai: 6.2.1
tinyrainbow: 3.0.3
- '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2))':
+ '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.15
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
'@vitest/pretty-format@4.0.15':
dependencies:
@@ -3660,6 +3932,8 @@ snapshots:
balanced-match@1.0.2: {}
+ before-after-hook@2.2.3: {}
+
before-after-hook@3.0.2: {}
bottleneck@2.19.5: {}
@@ -3677,6 +3951,12 @@ snapshots:
dependencies:
fill-range: 7.1.1
+ buffer-from@1.1.2: {}
+
+ bun-types@1.3.5:
+ dependencies:
+ '@types/node': 24.10.2
+
bundle-require@5.1.0(esbuild@0.27.1):
dependencies:
esbuild: 0.27.1
@@ -3736,6 +4016,8 @@ snapshots:
commander@14.0.2: {}
+ commander@2.20.3: {}
+
commander@4.1.1: {}
compare-func@2.0.0:
@@ -3810,6 +4092,8 @@ snapshots:
deep-is@0.1.4: {}
+ deprecation@2.3.1: {}
+
detect-libc@2.1.2: {}
domexception@1.0.1:
@@ -4415,6 +4699,10 @@ snapshots:
obug@2.1.1: {}
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
@@ -4587,6 +4875,13 @@ snapshots:
source-map-js@1.2.1: {}
+ source-map-support@0.5.21:
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+
+ source-map@0.6.1: {}
+
source-map@0.7.6: {}
split2@4.2.0: {}
@@ -4659,6 +4954,13 @@ snapshots:
minizlib: 3.1.0
yallist: 5.0.0
+ terser@5.44.1:
+ dependencies:
+ '@jridgewell/source-map': 0.3.11
+ acorn: 8.15.0
+ commander: 2.20.3
+ source-map-support: 0.5.21
+
text-extensions@2.4.0: {}
thenify-all@1.6.0:
@@ -4728,6 +5030,8 @@ snapshots:
- tsx
- yaml
+ tunnel@0.0.6: {}
+
turbo-darwin-64@2.6.3:
optional: true
@@ -4776,10 +5080,16 @@ snapshots:
undici-types@7.16.0: {}
+ undici@5.29.0:
+ dependencies:
+ '@fastify/busboy': 2.1.1
+
unicorn-magic@0.1.0: {}
unicorn-magic@0.3.0: {}
+ universal-user-agent@6.0.1: {}
+
universal-user-agent@7.0.3: {}
uri-js@4.4.1:
@@ -4788,7 +5098,7 @@ snapshots:
validator@13.15.23: {}
- vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2):
+ vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -4800,12 +5110,13 @@ snapshots:
'@types/node': 24.10.2
fsevents: 2.3.3
jiti: 2.6.1
+ terser: 5.44.1
yaml: 2.8.2
- vitest@4.0.15(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2):
+ vitest@4.0.15(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.15
- '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2))
+ '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.15
'@vitest/runner': 4.0.15
'@vitest/snapshot': 4.0.15
@@ -4822,7 +5133,7 @@ snapshots:
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
- vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(yaml@2.8.2)
+ vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.2
@@ -4872,6 +5183,8 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.1.2
+ wrappy@1.0.2: {}
+
wrtc@0.4.7:
optionalDependencies:
domexception: 1.0.1
diff --git a/utils/actions/.cliff-jumperrc.json b/utils/actions/.cliff-jumperrc.json
new file mode 100644
index 0000000..b3bb770
--- /dev/null
+++ b/utils/actions/.cliff-jumperrc.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
+ "name": "actions",
+ "org": "nanoforge-dev",
+ "packagePath": "packages/actions",
+ "identifierBase": false
+}
diff --git a/utils/actions/.gitignore b/utils/actions/.gitignore
new file mode 100644
index 0000000..f7652f8
--- /dev/null
+++ b/utils/actions/.gitignore
@@ -0,0 +1,231 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Turbo
+.turbo/
+
+# Compiled files
+src/**/*.js
+src/**/*.d.ts
diff --git a/utils/actions/.nvmrc b/utils/actions/.nvmrc
new file mode 100644
index 0000000..c519bf5
--- /dev/null
+++ b/utils/actions/.nvmrc
@@ -0,0 +1 @@
+v24.11.0
diff --git a/utils/actions/.prettierignore b/utils/actions/.prettierignore
new file mode 100644
index 0000000..64b127c
--- /dev/null
+++ b/utils/actions/.prettierignore
@@ -0,0 +1,11 @@
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+bun.lock
+
+.turbo/
+node_modules/
+dist/
+coverage/
+CHANGELOG.md
diff --git a/utils/actions/LICENSE b/utils/actions/LICENSE
new file mode 100644
index 0000000..4672486
--- /dev/null
+++ b/utils/actions/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 NanoForge
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/utils/actions/README.md b/utils/actions/README.md
new file mode 100644
index 0000000..975bcd7
--- /dev/null
+++ b/utils/actions/README.md
@@ -0,0 +1,52 @@
+
+
+## About
+
+`@nanoforge-dev/actions` is library that contains all utils for nanoforge actions.
+
+Most of the sources of this package come from [discord.js][discordjs-source]. Spetial thanks to them !
+
+## Installation
+
+**Node.js 24.11.0 or newer is required.**
+
+```sh
+npm install @nanoforge-dev/actions
+yarn add @nanoforge-dev/actions
+pnpm add @nanoforge-dev/actions
+bun add @nanoforge-dev/actions
+```
+
+## Links
+
+- [GitHub][source]
+- [npm][npm]
+
+## Contributing
+
+Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
+[documentation][documentation].
+See [the contribution guide][contributing] if you'd like to submit a PR.
+
+## Help
+
+If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to ask questions in [discussions][discussions].
+
+[documentation]: https://github.com/NanoForge-dev/Engine
+[discussions]: https://github.com/NanoForge-dev/Engine/discussions
+[source]: https://github.com/NanoForge-dev/Engine/tree/main/packages/actions
+[npm]: https://www.npmjs.com/package/@nanoforge-dev/actions
+[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
+[discordjs-source]: https://github.com/discordjs/discord.js
diff --git a/utils/actions/cliff.toml b/utils/actions/cliff.toml
new file mode 100644
index 0000000..21fe47a
--- /dev/null
+++ b/utils/actions/cliff.toml
@@ -0,0 +1,79 @@
+[changelog]
+header = """
+# Changelog
+
+All notable changes to this project will be documented in this file.\n
+"""
+body = """
+{%- macro remote_url() -%}
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
+{%- endmacro -%}
+{% if version %}\
+ # [{{ version | trim_start_matches(pat="v") }}]\
+ {% if previous %}\
+ {% if previous.version %}\
+ ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
+ {% else %}\
+ ({{ self::remote_url() }}/tree/{{ version }})\
+ {% endif %}\
+ {% endif %} \
+ - ({{ timestamp | date(format="%Y-%m-%d") }})
+{% else %}\
+ # [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ## {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.scope %}\
+ **{{commit.scope}}:** \
+ {% endif %}\
+ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
+ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
+ {% if commit.breaking %}\
+ {% for footer in commit.footers %}\
+ {% if footer.breaking %}\
+ \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
+ {% endif %}\
+ {% endfor %}\
+ {% endif %}\
+ {% endfor %}
+{% endfor %}\
+{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
+ \n### New Contributors\n
+ {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
+ - @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
+ {% endfor %}\
+{% endif %}\n
+"""
+trim = true
+footer = ""
+
+[git]
+conventional_commits = true
+filter_unconventional = true
+commit_parsers = [
+ { message = "^feat", group = "Features"},
+ { message = "^fix", group = "Bug Fixes"},
+ { message = "^docs", group = "Documentation"},
+ { message = "^perf", group = "Performance"},
+ { message = "^refactor", group = "Refactor"},
+ { message = "^types", group = "Typings"},
+ { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"},
+ { message = "^revert", skip = true},
+ { message = "^style", group = "Styling"},
+ { message = "^test", group = "Testing"},
+ { message = "^chore", skip = true},
+ { message = "^ci", skip = true},
+ { message = "^build", skip = true},
+ { body = ".*security", group = "Security"},
+]
+filter_commits = true
+protect_breaking_commits = true
+tag_pattern = "@nanoforge-dev/actions@[0-9]*"
+ignore_tags = ""
+topo_order = false
+sort_commits = "newest"
+
+[remote.github]
+owner = "NanoForge-dev"
+repo = "Engine"
diff --git a/utils/actions/eslint.config.js b/utils/actions/eslint.config.js
new file mode 100644
index 0000000..62ec06d
--- /dev/null
+++ b/utils/actions/eslint.config.js
@@ -0,0 +1,3 @@
+import config from "@nanoforge-dev/utils-eslint-config";
+
+export default config;
diff --git a/utils/actions/package.json b/utils/actions/package.json
new file mode 100644
index 0000000..08883dd
--- /dev/null
+++ b/utils/actions/package.json
@@ -0,0 +1,77 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@nanoforge-dev/actions",
+ "version": "1.0.0",
+ "description": "NanoForge Engine - Actions",
+ "keywords": [
+ "nanoforge",
+ "game",
+ "engine"
+ ],
+ "homepage": "https://github.com/NanoForge-dev/Engine#readme",
+ "bugs": "https://github.com/NanoForge-dev/Engine/issues",
+ "license": "MIT",
+ "contributors": [
+ "Bill ",
+ "Exelo ",
+ "Fexkoser ",
+ "Tchips "
+ ],
+ "files": [
+ "dist"
+ ],
+ "exports": {
+ "./package.json": "./package.json"
+ },
+ "type": "module",
+ "directories": {
+ "lib": "src"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NanoForge-dev/Engine.git",
+ "directory": "packages/actions"
+ },
+ "funding": "https://github.com/NanoForge-dev/Engine?sponsor",
+ "scripts": {
+ "build": "tsc --noEmit && tsup",
+ "lint": "prettier --check . && eslint --format=pretty src",
+ "format": "prettier --write . && eslint --fix --format=pretty src",
+ "prepack": "pnpm run build && pnpm run lint",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/actions/*'",
+ "release": "cliff-jumper"
+ },
+ "dependencies": {
+ "@actions/core": "^2.0.1",
+ "@actions/github": "^6.0.1",
+ "commander": "^14.0.2"
+ },
+ "devDependencies": {
+ "@favware/cliff-jumper": "^6.0.0",
+ "@nanoforge-dev/utils-eslint-config": "workspace:^",
+ "@nanoforge-dev/utils-prettier-config": "workspace:^",
+ "@npm/types": "^2.1.0",
+ "@trivago/prettier-plugin-sort-imports": "^6.0.0",
+ "@types/bun": "^1.3.5",
+ "eslint": "^9.39.1",
+ "prettier": "^3.6.2",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "typescript": "^5.9.3"
+ },
+ "packageManager": "pnpm@10.22.0",
+ "engines": {
+ "node": "24.11.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "lint-staged": {
+ "**/*.ts": [
+ "prettier --write"
+ ],
+ "src/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/utils/actions/prettier.config.js b/utils/actions/prettier.config.js
new file mode 100644
index 0000000..27d0e26
--- /dev/null
+++ b/utils/actions/prettier.config.js
@@ -0,0 +1,3 @@
+import config from "@nanoforge-dev/utils-prettier-config";
+
+export default config;
diff --git a/utils/actions/src/pnpm-install/action.yml b/utils/actions/src/pnpm-install/action.yml
new file mode 100644
index 0000000..17996f5
--- /dev/null
+++ b/utils/actions/src/pnpm-install/action.yml
@@ -0,0 +1,42 @@
+name: "pnpm install"
+description: "Run pnpm install with cache enabled"
+runs:
+ using: "composite"
+ steps:
+ - name: Set up swap space
+ if: runner.os == 'Linux'
+ uses: pierotofy/set-swap-space@v1.0
+ with:
+ swap-size-gb: 10
+
+ - uses: pnpm/action-setup@v4.1.0
+ name: Install pnpm
+ with:
+ run_install: false
+
+ - name: Expose pnpm config(s) through "$GITHUB_OUTPUT"
+ id: pnpm-config
+ shell: bash
+ run: |
+ echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
+
+ - name: Cache rotation keys
+ id: cache-rotation
+ shell: bash
+ run: |
+ echo "YEAR_MONTH=$(/bin/date -u "+%Y%m")" >> $GITHUB_OUTPUT
+
+ - uses: actions/cache@v4
+ name: Setup pnpm cache
+ with:
+ path: ${{ steps.pnpm-config.outputs.STORE_PATH }}
+ key: ${{ runner.os }}-pnpm-store-cache-${{ steps.cache-rotation.outputs.YEAR_MONTH }}-${{ hashFiles('**/pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-pnpm-store-cache-${{ steps.cache-rotation.outputs.YEAR_MONTH }}-
+
+ - name: Install dependencies
+ shell: bash
+ run: |
+ pnpm install --frozen-lockfile --prefer-offline --loglevel error
+ env:
+ HUSKY: "0"
diff --git a/utils/actions/src/release-packages/action.yml b/utils/actions/src/release-packages/action.yml
new file mode 100644
index 0000000..be41749
--- /dev/null
+++ b/utils/actions/src/release-packages/action.yml
@@ -0,0 +1,27 @@
+name: "Release Packages"
+description: "Tags and releases any unreleased packages"
+inputs:
+ dev:
+ description: "Releases development versions of packages (skips tagging and github releases)"
+ default: "false"
+ dry:
+ description: "Perform a dry run that skips publishing and outputs logs indicating what would have happened"
+ default: "false"
+ package:
+ description: "The published name of a single package to release"
+ exclude:
+ description: "Comma separated list of packages to exclude from release (if not depended upon)"
+ tag:
+ description: "The tag to use, generally a feature name"
+runs:
+ using: composite
+ steps:
+ - uses: oven-sh/setup-bun@v2
+ - run: bun $GITHUB_ACTION_PATH/index.ts
+ shell: bash
+ env:
+ INPUT_DEV: ${{ inputs.dev }}
+ INPUT_DRY: ${{ inputs.dry }}
+ INPUT_PACKAGE: ${{ inputs.package }}
+ INPUT_EXCLUDE: ${{ inputs.exclude }}
+ INPUT_TAG: ${{ inputs.tag }}
diff --git a/utils/actions/src/release-packages/generate-release-tree.ts b/utils/actions/src/release-packages/generate-release-tree.ts
new file mode 100644
index 0000000..c799911
--- /dev/null
+++ b/utils/actions/src/release-packages/generate-release-tree.ts
@@ -0,0 +1,242 @@
+import { info, warning } from "@actions/core";
+import type { PackageJSON, PackumentVersion } from "@npm/types";
+import { $, file, write } from "bun";
+
+interface pnpmTreeDependency {
+ from: string;
+ path: string;
+ version: string;
+}
+
+interface pnpmTree {
+ dependencies?: Record;
+ name?: string;
+ path: string;
+ private?: boolean;
+ unsavedDependencies?: Record;
+ version?: string;
+}
+
+export interface ReleaseEntry {
+ changelog?: string;
+ dependsOn?: string[];
+ name: string;
+ version: string;
+}
+
+async function fetchDevVersion(pkg: string, tag: string) {
+ try {
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/${tag}`);
+ if (!res.ok) return null;
+ const packument = (await res.json()) as PackumentVersion;
+ return packument.version;
+ } catch {
+ return null;
+ }
+}
+
+async function getReleaseEntries(dry: boolean, devTag?: string) {
+ const releaseEntries: ReleaseEntry[] = [];
+ const packageList: pnpmTree[] =
+ await $`pnpm list --recursive --only-projects --filter {packages/\*} --filter {utils/\*} --prod --json`.json();
+
+ const commitHash = (await $`git rev-parse --short HEAD`.text()).trim();
+ const timestamp = Math.round(Date.now() / 1_000);
+
+ for (const pkg of packageList) {
+ // Don't release private packages ever (npm will error anyways)
+ if (pkg.private) continue;
+ // Just in case
+ if (!pkg.version || !pkg.name) continue;
+
+ const release: ReleaseEntry = {
+ name: pkg.name,
+ version: pkg.version,
+ };
+
+ if (devTag) {
+ // Replace workspace dependencies with * to pin to associated dev versions
+ if (!dry) {
+ const pkgJsonString = await file(`${pkg.path}/package.json`).text();
+ await write(
+ `${pkg.path}/package.json`,
+ pkgJsonString.replaceAll(/workspace:[\^~]/g, "workspace:*"),
+ );
+ }
+
+ const devVersion = await fetchDevVersion(pkg.name, devTag);
+ if (devVersion?.endsWith(commitHash)) {
+ // Write the currently released dev version so when pnpm publish runs on dependents they depend on the dev versions
+ if (dry) {
+ info(`[DRY] ${pkg.name}@${devVersion} already released. Editing package.json version.`);
+ } else {
+ const pkgJson = (await file(`${pkg.path}/package.json`).json()) as PackageJSON;
+ pkgJson.version = devVersion;
+ await write(`${pkg.path}/package.json`, JSON.stringify(pkgJson, null, "\t"));
+ }
+
+ release.version = devVersion;
+ } else if (dry) {
+ info(`[DRY] Bumping ${pkg.name} via git-cliff.`);
+ release.version = `${pkg.version}.DRY-${devTag}.${timestamp}-${commitHash}`;
+ } else {
+ await $`pnpm --filter=${pkg.name} run release --preid "${devTag}.${timestamp}-${commitHash}" --skip-changelog`;
+ // Read again instead of parsing the output to be sure we're matching when checking against npm
+ const pkgJson = (await file(`${pkg.path}/package.json`).json()) as PackageJSON;
+ release.version = pkgJson.version;
+ }
+ }
+ // Only need changelog for releases published to github
+ else {
+ try {
+ // Find and parse changelog to post in github release
+ const changelogFile = await file(`${pkg.path}/CHANGELOG.md`).text();
+
+ let changelogLines: string[] = [];
+ let foundChangelog = false;
+
+ for (const line of changelogFile.split("\n")) {
+ if (line.startsWith("# [")) {
+ if (foundChangelog) {
+ if (changelogLines.at(-1) === "") {
+ changelogLines = changelogLines.slice(2, -1);
+ }
+
+ break;
+ }
+
+ // Check changelog release version and assume no changelog if version does not match
+ if (!line.startsWith(`# [${release.name}@${release.version}]`)) {
+ break;
+ }
+
+ foundChangelog = true;
+ }
+
+ if (foundChangelog) {
+ changelogLines.push(line);
+ }
+ }
+
+ release.changelog = changelogLines.join("\n");
+ } catch (error) {
+ // Probably just no changelog file but log just in case
+ warning(`Error parsing changelog for ${pkg.name}, will use auto generated: ${error}`);
+ }
+ }
+
+ if (pkg.dependencies) {
+ release.dependsOn = Object.keys(pkg.dependencies);
+ }
+
+ releaseEntries.push(release);
+ }
+
+ return releaseEntries;
+}
+
+export async function generateReleaseTree(
+ dry: boolean,
+ devTag?: string,
+ packageName?: string,
+ exclude?: string[],
+) {
+ let releaseEntries = await getReleaseEntries(dry, devTag);
+ // Try to early return if the package doesn't have deps
+ if (packageName && packageName !== "all") {
+ const releaseEntry = releaseEntries.find((entry) => entry.name === packageName);
+ if (!releaseEntry) {
+ throw new Error(`Package ${packageName} not releaseable`);
+ }
+
+ if (!releaseEntry.dependsOn) {
+ return [[releaseEntry]];
+ }
+ }
+
+ // Generate the whole tree first, then prune if specified
+ const releaseTree: ReleaseEntry[][] = [];
+ const didRelease = new Set();
+
+ while (releaseEntries.length) {
+ const nextBranch: ReleaseEntry[] = [];
+ const unreleased: ReleaseEntry[] = [];
+ for (const entry of releaseEntries) {
+ if (!entry.dependsOn) {
+ nextBranch.push(entry);
+ continue;
+ }
+
+ const allDepsReleased = entry.dependsOn.every((dep) => didRelease.has(dep));
+ if (allDepsReleased) {
+ nextBranch.push(entry);
+ } else {
+ unreleased.push(entry);
+ }
+ }
+
+ // Update didRelease in a second loop to avoid loop order issues
+ for (const release of nextBranch) {
+ didRelease.add(release.name);
+ }
+
+ if (releaseEntries.length === unreleased.length) {
+ throw new Error(
+ `One or more packages have dependents that can't be released: ${unreleased.map((entry) => entry.name).join(",")}`,
+ );
+ }
+
+ releaseTree.push(nextBranch);
+ releaseEntries = unreleased;
+ }
+
+ // Prune exclusions
+ if ((!packageName || packageName === "all") && Array.isArray(exclude) && exclude.length) {
+ const neededPackages = new Set();
+ const excludedReleaseTree: ReleaseEntry[][] = [];
+
+ for (const releaseBranch of releaseTree.reverse()) {
+ const newThisBranch: ReleaseEntry[] = [];
+
+ for (const entry of releaseBranch) {
+ if (exclude.includes(entry.name) && !neededPackages.has(entry.name)) {
+ continue;
+ }
+
+ newThisBranch.push(entry);
+ for (const dep of entry.dependsOn ?? []) {
+ neededPackages.add(dep);
+ }
+ }
+
+ if (newThisBranch.length) excludedReleaseTree.unshift(newThisBranch);
+ }
+
+ return excludedReleaseTree;
+ }
+
+ if (!packageName || packageName === "all") {
+ return releaseTree;
+ }
+
+ // Prune the tree for the specified package
+ const neededPackages = new Set([packageName]);
+ const packageReleaseTree: ReleaseEntry[][] = [];
+
+ for (const releaseBranch of releaseTree.reverse()) {
+ const newThisBranch: ReleaseEntry[] = [];
+
+ for (const entry of releaseBranch) {
+ if (neededPackages.has(entry.name)) {
+ newThisBranch.push(entry);
+ for (const dep of entry.dependsOn ?? []) {
+ neededPackages.add(dep);
+ }
+ }
+ }
+
+ if (newThisBranch.length) packageReleaseTree.unshift(newThisBranch);
+ }
+
+ return packageReleaseTree;
+}
diff --git a/utils/actions/src/release-packages/index.ts b/utils/actions/src/release-packages/index.ts
new file mode 100644
index 0000000..0703feb
--- /dev/null
+++ b/utils/actions/src/release-packages/index.ts
@@ -0,0 +1,118 @@
+import { endGroup, getBooleanInput, getInput, startGroup, summary } from "@actions/core";
+import { program } from "commander";
+
+import { generateReleaseTree } from "./generate-release-tree";
+import { releasePackage } from "./release-package";
+
+function npmPackageLink(packageName: string) {
+ return `https://npmjs.com/package/${packageName}` as const;
+}
+
+const excludeInput = getInput("exclude");
+let dryInput = false;
+let devInput = false;
+
+try {
+ devInput = getBooleanInput("dev");
+} catch {
+ // We're not running in actions
+}
+
+try {
+ dryInput = getBooleanInput("dry");
+} catch {
+ // We're not running in actions or the input isn't set (cron)
+}
+
+program
+ .name("release packages")
+ .description("releases monorepo packages with proper sequencing")
+ .argument("[package]", "release a specific package (and it's dependencies)", getInput("package"))
+ .option(
+ "-e, --exclude ",
+ "exclude specific packages from releasing (will still release if necessary for another package)",
+ excludeInput ? excludeInput.split(",") : [],
+ )
+ .option("--dry", "skips actual publishing and outputs logs instead", dryInput)
+ .option("--dev", "publishes development versions and skips tagging / github releases", devInput)
+ .option("--tag ", 'tag to use for dev releases (defaults to "dev")', getInput("tag"))
+ .parse();
+
+const {
+ exclude,
+ dry,
+ dev,
+ tag: inputTag,
+} = program.opts<{ dev: boolean; dry: boolean; exclude: string[]; tag: string }>();
+
+// All this because getInput('tag') will return empty string when not set :P
+if (!dev && inputTag.length) {
+ throw new Error("The --tag option can only be used with --dev");
+}
+
+const tag = inputTag.length ? inputTag : dev ? "dev" : undefined;
+const [packageName] = program.processedArgs as [string];
+const tree = await generateReleaseTree(dry, tag, packageName, exclude);
+
+interface ReleaseResult {
+ identifier: string;
+ url: string;
+}
+
+const publishedPackages: ReleaseResult[] = [];
+const skippedPackages: ReleaseResult[] = [];
+
+for (const branch of tree) {
+ startGroup(`Releasing ${branch.map((entry) => `${entry.name}@${entry.version}`).join(", ")}`);
+
+ await Promise.all(
+ branch.map(async (release) => {
+ const published = await releasePackage(release, dry, tag);
+ const identifier = `${release.name}@${release.version}`;
+
+ if (published) {
+ publishedPackages.push({ identifier, url: npmPackageLink(release.name) });
+ } else {
+ skippedPackages.push({ identifier, url: npmPackageLink(release.name) });
+ }
+ }),
+ );
+
+ endGroup();
+}
+
+const result = summary.addHeading("Release summary");
+
+if (dry) {
+ result.addRaw("\n\n> [!NOTE]\n> This is a dry run.\n\n");
+}
+
+result.addHeading("Released", 2);
+
+if (publishedPackages.length === 0) {
+ result.addRaw("\n_None_\n\n");
+} else {
+ result.addRaw("\n");
+
+ for (const { identifier, url } of publishedPackages) {
+ result.addRaw(`- [${identifier}](${url})\n`);
+ }
+
+ result.addRaw(`\n`);
+}
+
+result.addHeading("Skipped", 2);
+
+if (skippedPackages.length === 0) {
+ result.addRaw("\n_None_\n\n");
+} else {
+ result.addRaw("\n");
+
+ for (const { identifier, url } of skippedPackages) {
+ result.addRaw(`- [${identifier}](${url})\n`);
+ }
+
+ result.addRaw(`\n`);
+}
+
+await result.write();
diff --git a/utils/actions/src/release-packages/release-package.ts b/utils/actions/src/release-packages/release-package.ts
new file mode 100644
index 0000000..4f161ab
--- /dev/null
+++ b/utils/actions/src/release-packages/release-package.ts
@@ -0,0 +1,86 @@
+import { info, warning } from "@actions/core";
+import { context, getOctokit } from "@actions/github";
+import { $ } from "bun";
+import process from "node:process";
+
+import type { ReleaseEntry } from "./generate-release-tree";
+
+let octokit: ReturnType | undefined;
+
+if (process.env.GITHUB_TOKEN) {
+ octokit = getOctokit(process.env.GITHUB_TOKEN);
+}
+
+async function checkRegistry(release: ReleaseEntry) {
+ const res = await fetch(`https://registry.npmjs.org/${release.name}/${release.version}`);
+ return res.ok;
+}
+
+async function gitTagAndRelease(release: ReleaseEntry, dry: boolean) {
+ const tagName = `${release.name}@${release.version}`;
+
+ if (dry) {
+ info(`[DRY] Release would be "${tagName}", skipping release creation.`);
+ return;
+ }
+
+ const commitHash = (await $`git rev-parse HEAD`.text()).trim();
+
+ try {
+ await octokit?.rest.repos.createRelease({
+ ...context.repo,
+ tag_name: tagName,
+ target_commitish: commitHash,
+ name: tagName,
+ body: release.changelog ?? "",
+ generate_release_notes: release.changelog === undefined,
+ make_latest: "true",
+ });
+ } catch (error) {
+ warning(`Failed to create github release: ${error}`);
+ }
+}
+
+export async function releasePackage(
+ release: ReleaseEntry,
+ dry: boolean,
+ devTag?: string,
+ doGitRelease = !devTag,
+) {
+ // Sanity check against the registry first
+ if (await checkRegistry(release)) {
+ info(`${release.name}@${release.version} already published, skipping.`);
+ return false;
+ }
+
+ if (dry) {
+ info(`[DRY] Releasing ${release.name}@${release.version}`);
+ } else {
+ await $`pnpm --filter=${release.name} publish --provenance --no-git-checks ${devTag ? `--tag=${devTag}` : ""}`;
+ }
+
+ // && !devTag just to be sure
+ if (doGitRelease && !devTag) await gitTagAndRelease(release, dry);
+
+ if (dry) return true;
+
+ const before = performance.now();
+
+ // Poll registry to ensure next publishes won't fail
+ await new Promise((resolve, reject) => {
+ const interval = setInterval(async () => {
+ if (await checkRegistry(release)) {
+ clearInterval(interval);
+ resolve();
+ return;
+ }
+
+ if (performance.now() > before + 5 * 60 * 1_000) {
+ clearInterval(interval);
+ reject(new Error(`Release for ${release.name} failed.`));
+ }
+ }, 15_000);
+ });
+
+ return true;
+}
diff --git a/utils/actions/tsconfig.json b/utils/actions/tsconfig.json
new file mode 100644
index 0000000..b8e2058
--- /dev/null
+++ b/utils/actions/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node"],
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/utils/actions/tsup.config.ts b/utils/actions/tsup.config.ts
new file mode 100644
index 0000000..e2e7148
--- /dev/null
+++ b/utils/actions/tsup.config.ts
@@ -0,0 +1,11 @@
+import { createTsupConfig } from "../../tsup.config.js";
+
+export default [
+ createTsupConfig({
+ entry: ["src/release-packages/index.ts"],
+ dts: false,
+ format: "esm",
+ minify: "terser",
+ target: "esnext",
+ }),
+];