Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/actions/node-pnpm-ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion packages/actions/node-pnpm-ci/action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id: node-pnpm-ci
name: Node pnpm CI
version: 1.0.0
version: 1.1.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Breaking removal versioned as a minor bump

Removing the pnpmVersion input is a backward-incompatible change: any consumer that currently passes pnpmVersion in their action invocation will have that input silently dropped (or error, depending on the action runner). Under semver this warrants a major version bump (2.0.0), not 1.1.0. The same applies to node-pnpm-test. Existing users who discover the upgrade path from a changelog or registry entry will assume 1.x → 1.1.0 is safe to adopt.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

category: ci
description: Least-privilege Node/TypeScript CI workflow using pnpm install, typecheck, and test.
provider: github
Expand Down
5 changes: 0 additions & 5 deletions packages/actions/node-pnpm-ci/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions packages/actions/node-pnpm-ci/sh1pt.actionpack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/actions/node-pnpm-ci/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions packages/actions/node-pnpm-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion packages/actions/node-pnpm-test/action.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 0 additions & 5 deletions packages/actions/node-pnpm-test/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions packages/actions/node-pnpm-test/sh1pt.actionpack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/actions/node-pnpm-test/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions packages/actions/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
});
Expand Down
135 changes: 135 additions & 0 deletions packages/bots/irc/deploy/SERVER.md
Original file line number Diff line number Diff line change
@@ -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 -> <droplet-ipv4>
AAAA irc.profullstack.com -> <droplet-ipv6> # 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
Comment on lines +52 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security No binary integrity check on the Ergo download

The curl | tar install pipeline fetches the release binary over HTTPS but does not verify its SHA-256 checksum against the .sha256sum file that Ergo publishes alongside each release. A compromised CDN, redirected request, or supply-chain incident could deliver a tampered binary that tar would unpack silently. Adding a sha256sum -c step after the download (using the published checksum file from the same release) would close this gap.

Fix in Codex Fix in Claude Code

```

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 2>/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"],
});
```
11 changes: 11 additions & 0 deletions packages/bots/irc/deploy/certbot-deploy-hook.sh
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions packages/bots/irc/deploy/ergo.service
Original file line number Diff line number Diff line change
@@ -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
Loading