diff --git a/packages/actions/node-pnpm-ci/README.md b/packages/actions/node-pnpm-ci/README.md index 72757696..c4e7e6ec 100644 --- a/packages/actions/node-pnpm-ci/README.md +++ b/packages/actions/node-pnpm-ci/README.md @@ -4,6 +4,10 @@ Installs a GitHub Actions workflow for Node and TypeScript projects that use pnp The workflow installs dependencies, runs typecheck, and runs tests with explicit read-only repository permissions. +## Requirements + +The repository's `package.json` must declare a `packageManager` field (e.g. `"packageManager": "pnpm@10.0.0"`). `pnpm/action-setup@v4` reads the pnpm version from there. The workflow does **not** pin a pnpm `version` itself — pinning one that disagrees with `packageManager` fails the run with `ERR_PNPM_BAD_PM_VERSION`. + ## Output `node-pnpm-ci` writes `.github/workflows/ci.yml` through a pull request. diff --git a/packages/actions/node-pnpm-ci/action.yml b/packages/actions/node-pnpm-ci/action.yml index 0e5e6684..3d384f65 100644 --- a/packages/actions/node-pnpm-ci/action.yml +++ b/packages/actions/node-pnpm-ci/action.yml @@ -1,6 +1,6 @@ id: node-pnpm-ci name: Node pnpm CI -version: 1.0.0 +version: 1.1.0 category: ci description: Least-privilege Node/TypeScript CI workflow using pnpm install, typecheck, and test. provider: github diff --git a/packages/actions/node-pnpm-ci/schema.json b/packages/actions/node-pnpm-ci/schema.json index 431ca038..a3e61f63 100644 --- a/packages/actions/node-pnpm-ci/schema.json +++ b/packages/actions/node-pnpm-ci/schema.json @@ -9,11 +9,6 @@ "default": "22", "description": "Node.js version to install." }, - "pnpmVersion": { - "type": "string", - "default": "9", - "description": "pnpm major version to install." - }, "installCommand": { "type": "string", "default": "pnpm install --frozen-lockfile", diff --git a/packages/actions/node-pnpm-ci/sh1pt.actionpack.yaml b/packages/actions/node-pnpm-ci/sh1pt.actionpack.yaml index f740944d..4780d34c 100644 --- a/packages/actions/node-pnpm-ci/sh1pt.actionpack.yaml +++ b/packages/actions/node-pnpm-ci/sh1pt.actionpack.yaml @@ -2,7 +2,7 @@ schemaVersion: 1 id: node-pnpm-ci name: Node pnpm CI description: Least-privilege Node/TypeScript CI workflow using pnpm — install, typecheck, test. -version: 1.0.0 +version: 1.1.0 publisher: profullstack visibility: public license: MIT @@ -29,10 +29,6 @@ inputs: type: string default: '22' description: Node.js version to install. - pnpmVersion: - type: string - default: '9' - description: pnpm major version to install. installCommand: type: string default: pnpm install --frozen-lockfile diff --git a/packages/actions/node-pnpm-ci/workflow.yml b/packages/actions/node-pnpm-ci/workflow.yml index b422b3a1..6adf7afb 100644 --- a/packages/actions/node-pnpm-ci/workflow.yml +++ b/packages/actions/node-pnpm-ci/workflow.yml @@ -19,9 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 + # pnpm version is read from the "packageManager" field in package.json. + # Do not pin a version here — it conflicts with packageManager and fails + # with ERR_PNPM_BAD_PM_VERSION. - uses: pnpm/action-setup@v4 - with: - version: {{pnpmVersion}} - uses: actions/setup-node@v4 with: diff --git a/packages/actions/node-pnpm-test/README.md b/packages/actions/node-pnpm-test/README.md index 7965bf08..0ffeaf56 100644 --- a/packages/actions/node-pnpm-test/README.md +++ b/packages/actions/node-pnpm-test/README.md @@ -4,6 +4,10 @@ Installs the lightweight test workflow used by the sh1pt repository. The workflow runs on pull requests and pushes to the configured branch, installs with pnpm, and runs the configured test command with `CI=true`. +## Requirements + +The repository's `package.json` must declare a `packageManager` field (e.g. `"packageManager": "pnpm@10.0.0"`). `pnpm/action-setup@v4` reads the pnpm version from there. The workflow does **not** pin a pnpm `version` itself — pinning one that disagrees with `packageManager` fails the run with `ERR_PNPM_BAD_PM_VERSION`. + ## Output `node-pnpm-test` writes `.github/workflows/test.yml` through a pull request. diff --git a/packages/actions/node-pnpm-test/action.yml b/packages/actions/node-pnpm-test/action.yml index a9521b7b..646a663d 100644 --- a/packages/actions/node-pnpm-test/action.yml +++ b/packages/actions/node-pnpm-test/action.yml @@ -1,6 +1,6 @@ id: node-pnpm-test name: Node pnpm Test -version: 1.0.0 +version: 1.1.0 category: test description: Node/pnpm test workflow based on sh1pt's own repository test workflow. provider: github diff --git a/packages/actions/node-pnpm-test/schema.json b/packages/actions/node-pnpm-test/schema.json index e624694a..8e618cc6 100644 --- a/packages/actions/node-pnpm-test/schema.json +++ b/packages/actions/node-pnpm-test/schema.json @@ -9,11 +9,6 @@ "default": "22", "description": "Node.js version to install." }, - "pnpmVersion": { - "type": "string", - "default": "9.12.0", - "description": "pnpm version to install." - }, "pushBranch": { "type": "string", "default": "master", diff --git a/packages/actions/node-pnpm-test/sh1pt.actionpack.yaml b/packages/actions/node-pnpm-test/sh1pt.actionpack.yaml index 295b5889..e5b1f4c4 100644 --- a/packages/actions/node-pnpm-test/sh1pt.actionpack.yaml +++ b/packages/actions/node-pnpm-test/sh1pt.actionpack.yaml @@ -2,7 +2,7 @@ schemaVersion: 1 id: node-pnpm-test name: Node pnpm Test description: Node/pnpm test workflow based on sh1pt's own repository test workflow. -version: 1.0.0 +version: 1.1.0 publisher: profullstack visibility: public license: MIT @@ -26,10 +26,6 @@ inputs: type: string default: '22' description: Node.js version to install. - pnpmVersion: - type: string - default: 9.12.0 - description: pnpm version to install. pushBranch: type: string default: master diff --git a/packages/actions/node-pnpm-test/workflow.yml b/packages/actions/node-pnpm-test/workflow.yml index 49c05df0..2ae6b6da 100644 --- a/packages/actions/node-pnpm-test/workflow.yml +++ b/packages/actions/node-pnpm-test/workflow.yml @@ -15,9 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 + # pnpm version is read from the "packageManager" field in package.json. + # Do not pin a version here — it conflicts with packageManager and fails + # with ERR_PNPM_BAD_PM_VERSION. - uses: pnpm/action-setup@v4 - with: - version: {{pnpmVersion}} - uses: actions/setup-node@v4 with: diff --git a/packages/actions/src/index.test.ts b/packages/actions/src/index.test.ts index d2394d4b..ccd69331 100644 --- a/packages/actions/src/index.test.ts +++ b/packages/actions/src/index.test.ts @@ -40,7 +40,9 @@ describe('built-in packs', () => { const file = result.files[0]; expect(file?.destination).toBe('.github/workflows/ci.yml'); expect(file?.content).toContain("node-version: '22'"); - expect(file?.content).toContain('version: 9'); + expect(file?.content).toContain('pnpm/action-setup@v4'); + // pnpm version comes from package.json's packageManager field, not a pinned input. + expect(file?.content).not.toContain('version: 9'); expect(file?.content).toContain('pnpm install --frozen-lockfile'); expect(file?.content).toContain('${{ github.workflow }}'); expect(file?.content).toContain('# Managed by sh1pt Actions Fleet'); @@ -73,7 +75,9 @@ describe('built-in packs', () => { expect(file?.destination).toBe('.github/workflows/test.yml'); expect(file?.content).toContain('branches: [master]'); expect(file?.content).toContain('node-version: 22'); - expect(file?.content).toContain('version: 9.12.0'); + expect(file?.content).toContain('pnpm/action-setup@v4'); + // pnpm version comes from package.json's packageManager field, not a pinned input. + expect(file?.content).not.toContain('version: 9.12.0'); expect(file?.content).toContain('pnpm test'); expect(file?.content).toContain('# Managed by sh1pt Actions Fleet'); }); diff --git a/packages/bots/irc/deploy/SERVER.md b/packages/bots/irc/deploy/SERVER.md new file mode 100644 index 00000000..dbd080f7 --- /dev/null +++ b/packages/bots/irc/deploy/SERVER.md @@ -0,0 +1,135 @@ +# irc.profullstack.com — TLS-only ircd deployment + +Runbook for hosting the Profullstack IRC endpoint on a DigitalOcean droplet +using [Ergo](https://github.com/ergochat/ergo) (single-binary Go ircd). + +**Design goal: accept only SSL/TLS connections.** This is enforced *not* by DNS +(a hostname carries no port) but by binding a single TLS listener on `6697` and +never opening the cleartext `6667` port — at the firewall and in the ircd config. + +Pinned: Ergo **v2.18.0**. + +--- + +## 1. DNS + +``` +A irc.profullstack.com -> +AAAA irc.profullstack.com -> # optional +``` + +## 2. Firewall (this is what makes it SSL-only at the network edge) + +```bash +ufw allow 22/tcp +ufw allow 6697/tcp # IRC over TLS +ufw allow 80/tcp # ONLY needed during cert issuance/renewal (http-01) +ufw enable +# 6667 is never opened -> plaintext IRC is unreachable from the internet +``` + +## 3. TLS certificate (Let's Encrypt) + +```bash +apt-get update && apt-get install -y certbot +certbot certonly --standalone -d irc.profullstack.com +# -> /etc/letsencrypt/live/irc.profullstack.com/{fullchain,privkey}.pem +``` + +Install the renewal hook so Ergo reloads its cert after each renewal: + +```bash +install -m 0755 certbot-deploy-hook.sh \ + /etc/letsencrypt/renewal-hooks/deploy/reload-ergo.sh +``` + +## 4. Install Ergo + +```bash +useradd -r -s /usr/sbin/nologin ergo || true +mkdir -p /opt/ergo +cd /opt/ergo +VER=v2.18.0 +curl -fsSL "https://github.com/ergochat/ergo/releases/download/${VER}/ergo-${VER}-linux-x86_64.tar.gz" \ + | tar xz --strip-components=1 +cp default.yaml ircd.yaml # start from the shipped default, then apply §5 +``` + +Give the `ergo` user read access to the certs: + +```bash +# certs are root-only by default; a group read grant is the least-privilege option +groupadd -f tls-cert +usermod -aG tls-cert ergo +setfacl -R -m g:tls-cert:rX /etc/letsencrypt/live /etc/letsencrypt/archive +``` + +## 5. ircd.yaml overlay (the SSL-only bit) + +Edit `/opt/ergo/ircd.yaml`. The only changes that matter for this goal are the +server name and the **listeners** block — define one TLS listener on `6697` and +remove the default plaintext `:6667` listener entirely: + +```yaml +server: + name: irc.profullstack.com + + listeners: + # NO ":6667" entry. The plaintext socket simply does not exist. + ":6697": + tls: + cert: /etc/letsencrypt/live/irc.profullstack.com/fullchain.pem + key: /etc/letsencrypt/live/irc.profullstack.com/privkey.pem + # optional hardening: + # min-tls-version: 1.2 + + # If you ever bind a localhost-only plaintext listener for an internal bot, + # restrict it to loopback so it is never exposed: + # "127.0.0.1:6667": {} +``` + +Do **not** use STARTTLS on a cleartext port — it is vulnerable to TLS-stripping. +Implicit TLS on 6697 with no 6667 is strictly simpler and safer. + +Smoke-test the config in the foreground before wiring up systemd (Ergo +auto-creates its datastore on first run — no `initdb` step needed): + +```bash +sudo -u ergo /opt/ergo/ergo run --conf /opt/ergo/ircd.yaml +# watch for "listening on" :6697 and no TLS errors, then Ctrl-C +``` + +## 6. systemd + +```bash +cp ergo.service /etc/systemd/system/ergo.service +systemctl daemon-reload +systemctl enable --now ergo +systemctl status ergo +``` + +## 7. Verify it is TLS-only + +```bash +# TLS handshake succeeds: +openssl s_client -connect irc.profullstack.com:6697 -servername irc.profullstack.com /dev/null | head + +# plaintext is refused / times out (no listener, firewall closed): +nc -vz -w5 irc.profullstack.com 6667 # expect: connection refused / timed out +``` + +## 8. Connect sh1pt's IRC bot + +`@profullstack/sh1pt-bot-irc` already supports implicit TLS — set `tls: true` +and the port defaults to 6697 (see `../src/index.ts`): + +```ts +import { IrcBot } from "@profullstack/sh1pt-bot-irc"; + +const bot = new IrcBot({ + server: "irc.profullstack.com", + tls: true, // port omitted -> 6697 + nick: "sh1pt", + channels: ["#sh1pt"], +}); +``` diff --git a/packages/bots/irc/deploy/certbot-deploy-hook.sh b/packages/bots/irc/deploy/certbot-deploy-hook.sh new file mode 100755 index 00000000..2d65f79f --- /dev/null +++ b/packages/bots/irc/deploy/certbot-deploy-hook.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Installed to /etc/letsencrypt/renewal-hooks/deploy/reload-ergo.sh +# certbot runs every deploy hook after a successful renewal. Reload Ergo so it +# picks up the new cert without dropping client connections (SIGHUP = rehash). +set -euo pipefail + +# Only act when the irc.profullstack.com cert was (re)issued. +case "${RENEWED_LINEAGE:-}" in + */irc.profullstack.com) systemctl reload ergo ;; + *) : ;; # some other cert renewed; nothing to do +esac diff --git a/packages/bots/irc/deploy/ergo.service b/packages/bots/irc/deploy/ergo.service new file mode 100644 index 00000000..54612e68 --- /dev/null +++ b/packages/bots/irc/deploy/ergo.service @@ -0,0 +1,27 @@ +[Unit] +Description=Ergo IRC server (irc.profullstack.com) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=ergo +Group=ergo +WorkingDirectory=/opt/ergo +ExecStart=/opt/ergo/ergo run --conf /opt/ergo/ircd.yaml +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=2s +LimitNOFILE=1048576 + +# Hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +ReadWritePaths=/opt/ergo +# allow reading the Let's Encrypt cert tree (symlinks live -> archive) +ReadOnlyPaths=/etc/letsencrypt + +[Install] +WantedBy=multi-user.target