Lab 11 — ChatOps EmojiChef
Duration: 60 minutes
Operators do not type commands in plaintext. They post what looks like a food appreciation thread in a GitHub issue, and the Worker decodes it into an authenticated command that gets dispatched to the correct device. This is EmojiChef: a steganographic encoding scheme that maps ASCII text to Cloudflare’s food emoji range (U+1F345 through U+1F37F) using a 6-bit-per-emoji quasi-base64 alphabet.
In this lab you configure a GitHub repository webhook that fires at your Worker,
verify the HMAC-SHA256 signature, decode the emoji payload, and wire the
decoded command into the KV job queue from Lab 10. When it works, posting
@alpha 🥘🥫🥩🌯🥙🥘 as a comment on issue #1 silently enqueues a status
job and the Worker replies in the same thread.
Learning objectives
- Understand the EmojiChef encoding scheme: base codepoint, 6-bit windows, byte assembly.
- Verify GitHub webhook signatures with HMAC-SHA256 and the
X-Hub-Signature-256header. - Parse the GitHub issue comment event envelope, extract the command payload, and guard against reply loops.
- Wire the decoded command into the Lab 10 job queue (
enqueueJob/RATE_LIMITSKV). - Configure a GitHub repository webhook and a fine-grained PAT scoped to a single repo.
Pre-state
Before starting, confirm:
# Lab 10 validation passes (KV + R2 working)
bash courses/engagement-platform-labs/labs/lab10-kv-r2-storage/validate.sh
# DOMAIN is exported
echo "${DOMAIN}"
# wrangler is authenticated
npx wrangler whoami
You also need a GitHub account and a repository you control (your fork of
epl-student-starter works; a fresh empty repo is fine too).
Codespace users: wrangler is pre-installed in the operator console. Run
npx wrangler whoamito confirm your session is active. If it is not, runnpx wrangler loginagain.
Where each step runs
This is a pure cloud lab. No bench hardware is involved.
| Step | Operator console |
|---|---|
| 1-9. All steps | yes |
| Validation | yes |
Walkthrough
Open labs/lab07-first-worker/worker/src/index.js and read
handleGithubChatops(). Key behaviors:
- Signature verification: GitHub signs every request with HMAC-SHA256
using your webhook secret. The Worker reads
X-Hub-Signature-256and recomputes the digest over the raw request body. Requests with missing or mismatched signatures are rejected with HTTP 401. - Loop protection: The Worker drops any comment whose body starts with
[eplabs:result](its own replies) or that was authored by a bot user (sender.type === "Bot"). DuplicateX-GitHub-DeliveryIDs within 600 seconds are also rejected. - Prefix gate: The comment body must start with
@${STUDENT_SLOT}(the student’s slot, e.g.@alpha). The Worker strips the prefix before passing the remainder to EmojiChef. - Decode and dispatch:
EmojiChefQuick.decode()converts the emoji string to ASCII. The first whitespace-delimited token is the command name; remaining tokens are args. - Command vocabulary:
status,reboot,capture,list,ping,exec,fetch, andHSCare valid commands. Unknown commands return 422 with a list of known commands. - Result post: Uses the GitHub Issues API to post a reply on the same
issue. The reply body begins with
[eplabs:result] @${STUDENT_SLOT}.
Also open labs/lab07-first-worker/worker/src/emojichef.js and read the
EmojiChefQuick class. The encoder and decoder are symmetric: each ASCII byte
becomes 8 bits, the bitstream is cut into 6-bit windows, and each window value
n maps to codepoint 0x1F345 + n.
Use the live widget below to encode and decode strings. Enter status, HSC,
or reboot and copy the emoji output.
Verify the known test vectors with Node if preferred:
cd courses/engagement-platform-labs/labs/lab07-first-worker/worker
node -e "
const BASE = 0x1F345;
const encode = t => [...t].reduce((b, c) => b + c.charCodeAt(0).toString(2).padStart(8,'0'), '')
.match(/.{6}/g).map(s => String.fromCodePoint(BASE + parseInt(s,2))).join('');
const decode = e => [...e].map(c => (c.codePointAt(0) - BASE).toString(2).padStart(6,'0')).join('')
.match(/.{8}/g).map(s => String.fromCharCode(parseInt(s,2))).join('');
['HSC','status','reboot'].forEach(t => console.log(t, '->', encode(t), '-> decoded:', decode(encode(t))));
"
Expected output:
HSC -> 🍗🍊🍒🍈 -> decoded: HSC
status -> 🥘🥫🥩🌯🥙🥘 -> decoded: status
reboot -> 🍱🥤🥩🥓🥨🥯🌯 -> decoded: reboot
The full set of test vectors is in test-vectors.txt in this lab directory.
The Worker reads GITHUB_OWNER, GITHUB_REPO, and GITHUB_ISSUE_NUMBER from
its [vars] to know where to post replies. Issue #1 in your repo is the
canonical command queue for this lab.
-
Go to your workshop GitHub repo (fork of
epl-student-starteror a fresh repo). If you do not have one, create a new empty public repo now. -
Open the Issues tab and click New issue.
-
Title it
EPL Command Queue(or anything recognizable). Click Submit new issue. -
Note the issue number in the URL. It should be
#1if this is the first issue in the repo.
Note your values for the next step:
GITHUB_OWNER=<your-github-username>
GITHUB_REPO=<your-repo-name>
GITHUB_ISSUE_NUMBER=1
The Worker needs a token to post replies. Generate a fine-grained token scoped
to Issues on this one repo only. See
github-issue-chatops-setup.md in this lab directory for the full click-by-click
walkthrough; the abbreviated path is:
- GitHub Settings (avatar menu, top right).
- Developer settings (bottom of the left sidebar).
- Personal access tokens > Fine-grained tokens > Generate new token.
- Token name:
eplabs-chatops-<your-slot>. - Expiration: 30 days (or workshop duration).
- Repository access: “Only select repositories” > choose your workshop repo.
- Permissions > Repository permissions > Issues: set to Read and write.
- Leave all other permissions at their defaults (no access).
- Click Generate token and copy the token immediately (you will not see it again).
Keep the token value; you will need it in the next step.
Generate a random webhook secret:
openssl rand -hex 32
# or:
python3 -c 'import secrets; print(secrets.token_hex(32))'
Copy the output. This is your GITHUB_WEBHOOK_SECRET. Store it somewhere safe
for this lab session. You will paste it into both the Worker and the GitHub
webhook configuration.
Set the secrets:
cd courses/engagement-platform-labs/labs/lab07-first-worker/worker
npx wrangler secret put GITHUB_WEBHOOK_SECRET
# paste the 64-char hex string at the prompt
npx wrangler secret put GITHUB_TOKEN
# paste the fine-grained PAT from step 4
Open worker/wrangler.toml and add the [vars] entries for your repo:
[vars]
# ... existing vars ...
GITHUB_OWNER = "your-github-username"
GITHUB_REPO = "your-repo-name"
GITHUB_ISSUE_NUMBER = "1"
STUDENT_SLOT = "alpha"
Replace alpha with your assigned slot (e.g. bravo, charlie).
Redeploy the Worker so the new vars and secrets are active:
npx wrangler deploy
In your GitHub repository:
- Go to Settings > Webhooks > Add webhook.
- Payload URL:
https://api.${DOMAIN}/v1/chatops/github(substitute your actual domain). - Content type:
application/json. - Secret: paste the hex string you generated in step 5.
- Under Which events would you like to trigger this webhook? select “Let me select individual events”. Uncheck Pushes, then check Issue comments only.
- Make sure Active is checked.
- Click Add webhook.
GitHub immediately sends a ping event. Open Recent Deliveries on the
webhook settings page. You should see the ping with HTTP 204 (the Worker
ignores pings) or HTTP 401 (bad secret: recheck the hex you pasted).
See github-issue-chatops-setup.md in this lab directory for the full
walkthrough including screenshots.
Start tailing Worker logs in a terminal:
cd courses/engagement-platform-labs/labs/lab07-first-worker/worker
npx wrangler tail --format pretty
In a second terminal (or a browser), post a comment on your issue #1:
@alpha 🥘🥫🥩🌯🥙🥘
Replace @alpha with your actual STUDENT_SLOT value.
In the wrangler tail output you should see a log entry showing:
decoded: "status" job_id: <uuid> slot: alpha
The comment body 🥘🥫🥩🌯🥙🥘 decodes to the ASCII string status.
You can also post the comment via the GitHub API:
curl -s -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues/${GITHUB_ISSUE_NUMBER}/comments" \
-d '{"body":"@alpha 🥘🥫🥩🌯🥙🥘"}'
(Replace the emoji unicode escapes with the actual emoji string 🥘🥫🥩🌯🥙🥘
when typing in a shell that supports it.)
Reload your GitHub issue page. The Worker should have posted a reply comment whose body starts with:
[eplabs:result] @alpha status: queued, job_id: <uuid>
Confirm the job is also in KV:
JOB_ID="paste-job-id-here"
curl -s "https://api.${DOMAIN}/v1/jobs/${JOB_ID}" | jq .
Expected:
{
"job_id": "...",
"device_id": "broadcast",
"command": "status",
"params": { "args": [], "raw": "status" },
"status": "queued",
"created_at": "...",
"timeout": 60,
"source": "github_chatops",
"author": "<your-github-username>"
}
Try all three known test vectors from issue #1:
| Comment body | Decoded command |
|---|---|
@alpha 🍗🍊🍒🍈 | HSC |
@alpha 🥘🥫🥩🌯🥙🥘 | status |
@alpha 🍱🥤🥩🥓🥨🥯🌯 | reboot |
Each should produce a separate [eplabs:result] reply and a KV job entry.
chmod +x courses/engagement-platform-labs/labs/lab11-chatops-emojichef/validate.sh
export DOMAIN="<your-domain>"
# GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO, GITHUB_ISSUE_NUMBER, STUDENT_SLOT
# are read from the environment or from wrangler.toml [vars]
courses/engagement-platform-labs/labs/lab11-chatops-emojichef/validate.sh
The script posts a fresh @<slot> 🥘🥫🥩🌯🥙🥘 comment using your
GITHUB_TOKEN, then polls the issue for the bot’s reply for up to 30 seconds.
It asserts that the reply body starts with [eplabs:result] @<slot> and that
the decoded command is status.
Post-state
When this lab is complete:
-
GITHUB_WEBHOOK_SECRETandGITHUB_TOKENare set as Worker secrets. -
wrangler.toml[vars]containsGITHUB_OWNER,GITHUB_REPO,GITHUB_ISSUE_NUMBER, andSTUDENT_SLOT. - The GitHub webhook fires on issue comment events and shows HTTP 204 or 200 in GitHub’s Recent Deliveries panel.
- Posting
@<slot> 🥘🥫🥩🌯🥙🥘on issue #1 produces a[eplabs:result]reply and a KV job withcommand: "status". -
validate.shexits 0.
Validation
Three paths. All call the same validate.sh; only the delivery channel
differs.
Path 1: epl CLI (recommended from the operator console).
epl validate lab11-chatops-emojichef
Path 2: direct script.
bash courses/engagement-platform-labs/labs/lab11-chatops-emojichef/validate.sh
Path 3: paste output into the widget.
Run validate.sh anywhere, copy the full terminal output, and paste it below.
Troubleshooting
GitHub webhook shows HTTP 401 “Unauthorized” in Recent Deliveries
The HMAC-SHA256 signature did not match. The most common cause is a mismatch
between the secret you typed into the GitHub webhook configuration and the
GITHUB_WEBHOOK_SECRET value in the Worker.
Steps to fix:
- Generate a fresh secret with
openssl rand -hex 32. - Run
npx wrangler secret put GITHUB_WEBHOOK_SECRETand paste the new value. - Run
npx wrangler deployto push the updated secret. - Edit the GitHub webhook (Settings > Webhooks > your webhook > Edit) and paste the same new value into the Secret field.
- Click Update webhook. GitHub sends a new ping; check Recent Deliveries.
GitHub webhook shows HTTP 204 for the ping but no response for issue comments
The Worker returns 204 for ping events (the action field is absent on ping
payloads). If issue comment events are also returning 204, the event filter
may not be set correctly.
- Re-open the webhook settings and confirm Issue comments is the only checked event.
- Check that the comment body starts with
@<STUDENT_SLOT>(with a trailing space). The prefix gate rejects comments that do not start with the correct slot marker.
Worker posts a reply but then immediately processes it again (loop)
The loop-protection sentinel [eplabs:result] must appear at the very start of
the Worker’s reply body. Check your Worker source for the handleGithubChatops
reply format. The reply body must begin with [eplabs:result]; any whitespace
before it breaks the guard.
validate.sh times out waiting for the bot reply
The Worker may be slow on the first invocation after a cold start. Run the script again; subsequent runs should be faster.
If the timeout repeats:
- Confirm
GITHUB_TOKENin the Worker secrets has Issues write permission on the correct repo. - Check
wrangler tailfor errors in the reply POST. - Confirm
GITHUB_OWNER,GITHUB_REPO, andGITHUB_ISSUE_NUMBERinwrangler.toml[vars]match the repo where you are posting comments.
POST /v1/chatops/github returns 422 “Unknown command”
The emoji string decoded to a command name not in the vocabulary. Use the
EmojiChef widget above to verify your encoding before posting. The vocabulary
is: status, reboot, capture, list, ping, exec, fetch, HSC.
Primitives and drop-in substitutes
The observable behavior of this lab is an asynchronous out-of-band command queue with built-in audit trail: an operator posts a message to an external service; the Worker receives it, authenticates the source, decodes the payload, enqueues a job, and posts a result back to the same thread.
The transport layer is the only thing that changes between systems. The EmojiChef encoder, KV job queue, Tailnet dispatch, and R2 signed URL mechanisms are identical regardless of which transport you use.
| Transport | Authentication | Protocol | Invocation form |
|---|---|---|---|
| GitHub Issues (this lab) | HMAC-SHA256 webhook secret | HTTPS POST | Issue comment starting with @<slot> |
| Discord (take-home) | Ed25519 application public key | HTTPS POST | Slash command or message interaction |
| Slack | Signing secret + timestamp | HTTPS POST | Slash command or app mention |
| Mattermost | Token in header or signing | HTTPS POST | Slash command |
| Matrix | Access token | HTTPS PUT | Room message event |
| Telegram | Bot API token in URL path | HTTPS POST | Webhook update |
| MQTT broker | TLS client cert or username/password | MQTT 3.1.1 / 5.0 | Topic publish |
Switching transports requires changing only the webhook handler and the secrets. No Worker route logic, no job-queue shape, no audit-log schema changes.
Further reading
- Cloudflare Workers: Handling webhooks
- GitHub: Webhooks documentation
- GitHub: Fine-grained PAT scopes
- GitHub: Securing your webhooks
- GitHub: Issue comment event payload
Take-home extension
The Discord variant of this lab uses the same EmojiChef encoder and KV job queue. Only the transport changes: Ed25519 application key verification instead of HMAC-SHA256, and Discord slash command interaction bodies instead of GitHub issue comment events.
See take-home/discord/README.md for the full
framing and setup guide.
ValidateOutputPaster lab="lab11")