Skip to content

Commit 2be0046

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat: add hyperpolymath utility scripts
- bootstrap-workers-rescript.sh: ReScript worker bootstrapping - check-changes.sh: Change detection utility - create-welcome-discussions.sh: GitHub discussions automation - enhance-kinoite.sh: Fedora Kinoite enhancements - init-wiki.sh: Wiki initialization - migrate-to-eclipse.sh: Eclipse migration tooling - nvidia-auto-setup.sh: NVIDIA driver auto-setup - restructure-repos.jl: Repository restructuring (Julia) - scheduled-optimize.sh: Scheduled system optimization - set-mirror-secrets.sh: Mirror secrets configuration - setup-kinoite-dev.sh: Kinoite dev environment setup - standardize-repos.jl: Repository standardization (Julia) - sync-github-repos.sh: GitHub repo sync - sync-repos-parallel.sh: Parallel repo sync - sync-repos.sh: Repository sync - system-optimize.sh: System optimization - github-analysis/: GitHub workflow analysis scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9473a35 commit 2be0046

21 files changed

Lines changed: 4191 additions & 0 deletions

bootstrap-workers-rescript.sh

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
say() { printf "\n==> %s\n" "$*"; }
5+
6+
need() {
7+
local var="$1" prompt="$2" secret="${3:-false}"
8+
if [[ "${secret}" == "true" ]]; then
9+
read -r -s -p "${prompt}: " "${var}"; echo
10+
else
11+
read -r -p "${prompt}: " "${var}"
12+
fi
13+
if [[ -z "${!var}" ]]; then
14+
echo "Missing: ${var}" >&2
15+
exit 1
16+
fi
17+
}
18+
19+
say "Cloudflare Workers + ReScript bootstrap"
20+
21+
need APP_SLUG "Worker name (e.g. git-eco-bot-hook)"
22+
need CF_ACCOUNT_ID "Cloudflare Account ID"
23+
need WEBHOOK_SECRET "GitHub Webhook Secret (you already generated one)" true
24+
25+
# optional bits
26+
read -r -p "Create GitHub Actions deploy workflow too? (y/N): " WITH_GHA
27+
WITH_GHA="${WITH_GHA:-N}"
28+
29+
say "Creating project: ${APP_SLUG}"
30+
mkdir -p "${APP_SLUG}"
31+
cd "${APP_SLUG}"
32+
33+
# Basic files
34+
mkdir -p src docs
35+
cat > docs/README.adoc <<'ADOC'
36+
= Git Eco Bot — Webhook Receiver (Workers)
37+
:toc:
38+
39+
== What this is
40+
A Cloudflare Worker webhook receiver for a GitHub App (Installation/Webhook mode).
41+
42+
== Environment
43+
* `GITHUB_WEBHOOK_SECRET` — webhook HMAC secret
44+
* Cloudflare bindings via `wrangler.jsonc`
45+
46+
== Local dev
47+
. Install deps: `npm i` (wrangler) and `npm i -D rescript`
48+
. Build: `npx rescript build -w`
49+
. Run worker dev: `npx wrangler dev`
50+
51+
== Deploy
52+
`npx wrangler deploy`
53+
54+
== GitHub App setup (manual)
55+
. Create GitHub App
56+
. Set webhook URL to `https://<your-worker>.<your-subdomain>.workers.dev/github/webhook`
57+
. Set webhook secret to match `GITHUB_WEBHOOK_SECRET`
58+
. Subscribe to `Issues` / `Pull request` events (or what you need)
59+
ADOC
60+
61+
# ReScript config (minimal)
62+
cat > bsconfig.json <<'JSON'
63+
{
64+
"name": "git-eco-bot-worker",
65+
"sources": [{ "dir": "src", "subdirs": true }],
66+
"package-specs": { "module": "esmodule", "in-source": false },
67+
"suffix": ".mjs",
68+
"bs-dependencies": [],
69+
"warnings": {
70+
"error": "+101"
71+
}
72+
}
73+
JSON
74+
75+
# Worker entrypoint (tiny JS shim importing ReScript output)
76+
mkdir -p dist
77+
cat > dist/worker.mjs <<'JS'
78+
import { handleFetch } from "../lib/es6/src/Webhook.mjs";
79+
80+
export default {
81+
async fetch(request, env, ctx) {
82+
return handleFetch(request, env, ctx);
83+
}
84+
};
85+
JS
86+
87+
# ReScript webhook handler (WebCrypto HMAC verify + routing)
88+
cat > src/Webhook.res <<'RES'
89+
let textEncoder = TextEncoder.make()
90+
91+
let toHex = (buf: Js.TypedArray2.ArrayBuffer.t): string => {
92+
let u8 = Js.TypedArray2.Uint8Array.fromBuffer(buf)
93+
let len = Js.TypedArray2.Uint8Array.length(u8)
94+
let parts = Belt.Array.make(len, "")
95+
for i in 0 to len - 1 {
96+
let b = Js.TypedArray2.Uint8Array.unsafe_get(u8, i)
97+
let h = Js.Int.toStringAs(~base=16, b)->Js.String2.padStart(2, "0")
98+
parts[i] = h
99+
}
100+
parts->Belt.Array.joinWith("")
101+
}
102+
103+
let timingSafeEq = (a: string, b: string): bool => {
104+
if a->Js.String2.length != b->Js.String2.length { false } else {
105+
// constant-ish time compare in JS string space
106+
let mutable diff = 0
107+
for i in 0 to a->Js.String2.length - 1 {
108+
diff = diff lor ((a->Js.String2.charCodeAt(i)) lxor (b->Js.String2.charCodeAt(i)))
109+
}
110+
diff == 0
111+
}
112+
}
113+
114+
let verifySignature = async (~secret: string, ~body: Js.TypedArray2.ArrayBuffer.t, ~sigHeader: option<string>) => {
115+
switch sigHeader {
116+
| None => false
117+
| Some(h) =>
118+
// Expect "sha256=..."
119+
if !Js.String2.startsWith(h, "sha256=") { false } else {
120+
let expected = h->Js.String2.sliceToEnd(7)
121+
let keyData = textEncoder->TextEncoder.encode(secret)
122+
let algo: Js.Json.t = %raw(`({ name: "HMAC", hash: "SHA-256" })`)
123+
let key =
124+
await Webcrypto.Subtle.importKey(
125+
"raw",
126+
keyData->Js.TypedArray2.Uint8Array.buffer,
127+
algo,
128+
false,
129+
["sign"],
130+
)
131+
let sig = await Webcrypto.Subtle.sign("HMAC", key, body)
132+
let actual = toHex(sig)
133+
timingSafeEq(actual, expected)
134+
}
135+
}
136+
}
137+
138+
let bad = (msg: string) =>
139+
Response.makeWithInit(
140+
msg,
141+
{
142+
"status": 401,
143+
"headers": HeadersInit.makeWithArray([("content-type", "text/plain")]),
144+
},
145+
)
146+
147+
let ok = (msg: string) =>
148+
Response.makeWithInit(
149+
msg,
150+
{
151+
"status": 200,
152+
"headers": HeadersInit.makeWithArray([("content-type", "text/plain")]),
153+
},
154+
)
155+
156+
let notFound = () =>
157+
Response.makeWithInit(
158+
"not found",
159+
{ "status": 404, "headers": HeadersInit.makeWithArray([("content-type", "text/plain")]) },
160+
)
161+
162+
@val external consoleLog: 'a => unit = "console.log"
163+
164+
let handleFetch = async (request: Request.t, env: 'env, _ctx: 'ctx): Promise.t<Response.t> => {
165+
let url = URL.make(request["url"])
166+
167+
if url["pathname"] != "/github/webhook" {
168+
Promise.resolve(notFound())
169+
} else {
170+
let secret: option<string> = %raw(`env && env.GITHUB_WEBHOOK_SECRET ? env.GITHUB_WEBHOOK_SECRET : null`)
171+
switch secret {
172+
| None => Promise.resolve(bad("missing env.GITHUB_WEBHOOK_SECRET"))
173+
| Some(secret) =>
174+
let sigHeader = request["headers"]->Headers.get("X-Hub-Signature-256")
175+
let eventName = request["headers"]->Headers.get("X-GitHub-Event")->Belt.Option.getWithDefault("unknown")
176+
let delivery = request["headers"]->Headers.get("X-GitHub-Delivery")->Belt.Option.getWithDefault("")
177+
178+
// IMPORTANT: read raw bytes
179+
let! bodyBuf = request->Request.arrayBuffer
180+
let! verified = verifySignature(~secret, ~body=bodyBuf, ~sigHeader)
181+
182+
if !verified {
183+
Promise.resolve(bad("bad signature"))
184+
} else {
185+
// Minimal routing: just log + acknowledge
186+
consoleLog({ "event": eventName, "delivery": delivery })
187+
Promise.resolve(ok("ok"))
188+
}
189+
}
190+
}
191+
}
192+
RES
193+
194+
# Wrangler config (Workers)
195+
cat > wrangler.jsonc <<JSONC
196+
{
197+
"name": "${APP_SLUG}",
198+
"main": "dist/worker.mjs",
199+
"compatibility_date": "2025-12-25",
200+
"account_id": "${CF_ACCOUNT_ID}"
201+
}
202+
JSONC
203+
204+
# package.json (kept minimal; uses npm only for wrangler/rescript tooling)
205+
cat > package.json <<'JSON'
206+
{
207+
"name": "git-eco-bot-worker",
208+
"private": true,
209+
"type": "module",
210+
"scripts": {
211+
"build": "rescript build",
212+
"dev": "rescript build -w",
213+
"wrangler:dev": "wrangler dev",
214+
"deploy": "rescript build && wrangler deploy"
215+
},
216+
"devDependencies": {
217+
"rescript": "^11.1.0",
218+
"wrangler": "^3.0.0"
219+
}
220+
}
221+
JSON
222+
223+
# Optional GitHub Actions workflow
224+
if [[ "${WITH_GHA}" == "y" || "${WITH_GHA}" == "Y" ]]; then
225+
mkdir -p .github/workflows
226+
cat > .github/workflows/deploy-workers.yml <<'YML'
227+
name: deploy-workers
228+
on:
229+
push:
230+
branches: [ "main" ]
231+
232+
jobs:
233+
deploy:
234+
runs-on: ubuntu-latest
235+
steps:
236+
- uses: actions/checkout@v4
237+
238+
- uses: actions/setup-node@v4
239+
with:
240+
node-version: "20"
241+
242+
- run: npm ci || npm install
243+
- run: npm run deploy
244+
env:
245+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
246+
# wrangler uses account_id from wrangler.jsonc; token must have Workers deploy permissions
247+
YML
248+
249+
say "Added GitHub Actions workflow."
250+
say "You'll need to set repo secret: CLOUDFLARE_API_TOKEN"
251+
fi
252+
253+
say "Installing deps…"
254+
npm install
255+
256+
say "Setting Workers secret (wrangler will prompt)…"
257+
npx wrangler secret put GITHUB_WEBHOOK_SECRET <<EOF
258+
${WEBHOOK_SECRET}
259+
EOF
260+
261+
say "Build + Deploy…"
262+
npm run deploy
263+
264+
say "Done."
265+
say "Next: set GitHub App Webhook URL to: https://${APP_SLUG}.<your-subdomain>.workers.dev/github/webhook"

check-changes.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
# Check repos with local changes
4+
5+
repos=(
6+
asdfghj bebop-v-ffi disinfo-nesy-detector disinfo-nsai-detector
7+
doit-ssg drift-ssg flathub flight-ssg forth-estate-ssg
8+
git-eco-bot idaptik-dlc-reversible idaptiky january-ssg kith
9+
macports-ports nano-aida nano-ruber parallel-press-ssg poly-db-mcp
10+
poly-iac-mcp poly-observability-mcp poly-secret-mcp project-wharf
11+
rhodium-standard-repositories-fix robot-repo-bot shift-ssg
12+
sinople-wharf svalinn-ecosystem synapse-release template-repo
13+
union-policy-parsers webforge-ssg winget-pkgs wordpress-wharf
14+
wp-audit-toolkit yocaml-ssg zotero-nsai
15+
)
16+
17+
for repo in "${repos[@]}"; do
18+
if [ -d "$HOME/repos/$repo/.git" ]; then
19+
echo "=== $repo ==="
20+
cd "$HOME/repos/$repo"
21+
git status --short 2>/dev/null | head -8
22+
echo ""
23+
fi
24+
done

create-welcome-discussions.sh

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
# Create welcome discussions for repos that don't have one
4+
5+
create_welcome_discussion() {
6+
local repo="$1"
7+
8+
echo -n "Creating welcome discussion for $repo: "
9+
10+
# Get repo ID
11+
local repo_id
12+
repo_id=$(gh api graphql -f query='query { repository(owner: "hyperpolymath", name: "'"$repo"'") { id } }' --jq '.data.repository.id' 2>/dev/null)
13+
14+
if [[ -z "$repo_id" || "$repo_id" == "null" ]]; then
15+
echo "SKIP (could not get repo ID)"
16+
return 1
17+
fi
18+
19+
# Get Announcements category ID
20+
local cat_id
21+
cat_id=$(gh api graphql -f query='query { repository(owner: "hyperpolymath", name: "'"$repo"'") { discussionCategories(first: 10) { nodes { id name } } } }' --jq '.data.repository.discussionCategories.nodes[] | select(.name == "Announcements") | .id' 2>/dev/null)
22+
23+
if [[ -z "$cat_id" || "$cat_id" == "null" ]]; then
24+
echo "SKIP (could not get category ID)"
25+
return 1
26+
fi
27+
28+
# Create discussion body as JSON
29+
local title="Welcome to $repo Discussions!"
30+
local body="Welcome! This is the official discussion space for **$repo**.
31+
32+
## How to Use
33+
34+
- **Announcements**: Project updates from maintainers
35+
- **Q&A**: Ask questions and get help
36+
- **Ideas**: Suggest new features
37+
- **Show and tell**: Share what you've built
38+
39+
Please be respectful and follow our community guidelines."
40+
41+
# Create the discussion
42+
local result
43+
result=$(gh api graphql \
44+
-F repositoryId="$repo_id" \
45+
-F categoryId="$cat_id" \
46+
-F title="$title" \
47+
-F body="$body" \
48+
-f query='
49+
mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
50+
createDiscussion(input: {
51+
repositoryId: $repositoryId
52+
categoryId: $categoryId
53+
title: $title
54+
body: $body
55+
}) {
56+
discussion {
57+
id
58+
title
59+
}
60+
}
61+
}' 2>&1)
62+
63+
if echo "$result" | grep -q '"title"'; then
64+
echo "OK"
65+
return 0
66+
else
67+
local err
68+
err=$(echo "$result" | jq -r '.errors[0].message // "unknown error"' 2>/dev/null || echo "$result")
69+
echo "FAILED: $err"
70+
return 1
71+
fi
72+
}
73+
74+
# List of repos needing welcome discussions
75+
repos=(
76+
"mustfile" "network-dashboard" "bgp-backbone-lab"
77+
"flatracoon-os" "hesiod-dns-map" "ipv6-site-enforcer" "ipfs-overlay"
78+
"zerotier-k8s-link" "twingate-helm-deploy" "explicit-trust-plane" "theoneshow"
79+
"wp-resurrect" "docudactyl" "rhodibot" "anchor.scm" "funfriendly-git"
80+
"git-dispatcher" "total-upgrade" "dnfinition" "nickel-augmented" "seambot"
81+
"bebop-v-ffi" "claude-gecko-browser-extension" "amethe" "total-recall"
82+
"git-secure" "avatar-fabrication-facility" "snapcreate" "dei-ssg"
83+
"poly-proof-mcp" "tyrano-ssg" "vladik-ssg" "ultimatum-ssg" "repo-customiser"
84+
"ephapax-playground" "reliquary-ssg" "tiamat-ssg" "tripos-ssg"
85+
"developer-ecosystem" "neural-foundations" "cccp" "reasonably-good-token-vault"
86+
"neurosym-scm"
87+
)
88+
89+
echo "Creating welcome discussions for ${#repos[@]} repos..."
90+
echo ""
91+
92+
success=0
93+
failed=0
94+
95+
for repo in "${repos[@]}"; do
96+
if create_welcome_discussion "$repo"; then
97+
((success++))
98+
else
99+
((failed++))
100+
fi
101+
done
102+
103+
echo ""
104+
echo "Done. Success: $success, Failed: $failed"

0 commit comments

Comments
 (0)