assay · getting started · identities
GitHub created the App. Now take the key.
GitHub sent you here with a one-time code in the address bar. This page reads that code only to assemble the command you paste — it never sends it anywhere; the exchange that turns the code into the App's private key runs entirely in your own shell. The code expires in one hour and the key is issued exactly once.
Exchange the code
The address bar reads https://assay.guide/apps-created.html?code=…. The exchange is two moves: save one small script, then run it with the role you just created and that one-time code. It needs no authentication — the code is the authentication — and it returns the App ID, the slug, and the private key in one response. If you opened each App in its own tab, every tab collects its own role code pair here (in your browser only) so you can create all eight and then run one command. This page reads the code only to fill it into the command below; it never sends the code anywhere. The exchange runs entirely in your own shell.
create-app.sh · once
Paste this whole block into your terminal once. It writes create-app.sh into the current directory and makes it executable. The script takes one or more role code pairs as arguments and has nothing baked in, so you reuse the same file for all eight Apps — one at a time, or all eight in a single run. It needs curl and jq.
cat > create-app.sh <<'EOF'
#!/usr/bin/env bash
# create-app.sh — exchange one-time GitHub App manifest codes for Assay App
# private keys. One OR MORE (role, code) pairs, all in a single run:
#
# ./create-app.sh <role> <code> [<role> <code> ...]
#
# Each <code> is the value after code= in the address bar GitHub sent you to
# (https://assay.guide/apps-created.html?code=...). A code is single-use and
# expires about an hour after GitHub issued it; if a pair fails with no key,
# delete that half-made App on GitHub and create it again.
#
# For each pair this posts the code to GitHub's manifest-conversion endpoint
# from YOUR machine and nowhere else, writes the returned private key to
# ~/.config/assay/<role>-app.pem at mode 0600, records the App id in
# ~/.config/assay/apps.env, and deletes the raw response (which also carries a
# client secret you never use). One generic script, reused for all eight roles.
set -euo pipefail
umask 077
usage() {
cat >&2 <<'USAGE'
usage: ./create-app.sh <role> <code> [<role> <code> ...]
<role> one of: reviewer worker verifier desk issue-loop intake-loop board-writer promote
<code> the one-time code from the address bar after GitHub created that App
Pass as many role/code pairs as you like — all eight in one run.
USAGE
}
valid_role() {
case "$1" in
reviewer|worker|verifier|desk|issue-loop|intake-loop|board-writer|promote) return 0 ;;
*) return 1 ;;
esac
}
args=("$@")
n=${#args[@]}
if [ "$n" -eq 0 ] || [ $((n % 2)) -ne 0 ]; then
echo "create-app.sh: expected role/code pairs (an even number of arguments), got $n." >&2
usage
exit 2
fi
# Pass 1 — validate every pair BEFORE any network call, so a typo in the last
# pair does not leave you half-done.
i=0
while [ "$i" -lt "$n" ]; do
role=${args[$i]}
code=${args[$((i + 1))]}
if ! valid_role "$role"; then
echo "create-app.sh: unknown role: $role" >&2
usage
exit 2
fi
if [ -z "$code" ]; then
echo "create-app.sh: the code for role $role is empty." >&2
usage
exit 2
fi
i=$((i + 2))
done
conf=$HOME/.config/assay
mkdir -p "$conf"
tmpfiles=()
cleanup() {
if [ "${#tmpfiles[@]}" -gt 0 ]; then rm -f "${tmpfiles[@]}"; fi
}
trap cleanup EXIT
exchange_one() {
role=$1
code=$2
env_key=$(printf '%s' "$role" | tr 'a-z-' 'A-Z_')_APP_ID
json=$(mktemp)
tmpfiles+=("$json")
if ! curl -sS -X POST -H 'Accept: application/vnd.github+json' \
"https://api.github.com/app-manifests/$code/conversions" > "$json"; then
echo "create-app.sh: network error exchanging the code for role $role." >&2
rm -f "$json"
return 1
fi
if ! jq -e .pem "$json" >/dev/null 2>&1; then
echo "create-app.sh: no key for role $role — its code was already used or has expired." >&2
echo "Delete that half-made App on GitHub and create it again." >&2
rm -f "$json"
return 1
fi
pem=$conf/$role-app.pem
jq -r .pem "$json" > "$pem"
chmod 600 "$pem"
app_id=$(jq -r .id "$json")
slug=$(jq -r .slug "$json")
rm -f "$json" # the response also holds a client secret
printf '%s=%s\n' "$env_key" "$app_id" >> "$conf/apps.env"
echo "Saved $pem (mode 0600) - $slug app id $app_id"
echo " install: https://github.com/apps/$slug/installations/new"
return 0
}
# Pass 2 — exchange each pair. A failed pair is reported and the run continues
# to the next, so one spent code does not abandon the others.
failed=0
i=0
while [ "$i" -lt "$n" ]; do
if exchange_one "${args[$i]}" "${args[$((i + 1))]}"; then :; else failed=1; fi
i=$((i + 2))
done
if [ "$failed" -ne 0 ]; then
echo "create-app.sh: one or more exchanges failed (see above)." >&2
exit 1
fi
echo "Done. Re-resolve your desk tools (for example /plugin) so the new Apps are picked up."
EOF
chmod +x create-app.sh
Pick the role of the App GitHub just made. This page will not guess it for you — a wrong role would write the wrong filename and the wrong apps.env key — so the command in step 3 stays incomplete until you choose. Your pick also adds this App’s role code pair to the batch collected in step 4. If you are unsure, the App’s name on GitHub ends in its role (for example …-assay-worker).
Your one-time code is read from the address bar and filled in here; the role fills in when you pick it above. Run this in the same directory where you saved create-app.sh. Nothing is sent from this page — the code leaves your machine only when your own create-app.sh posts it, from your shell.
No JavaScript, or the fields did not fill in? Take the code from your address bar (the value after code=) and run ./create-app.sh <role> <code> yourself with the role you picked.
./create-app.sh <role> <code>
Each App tab you picked a role in has added its role code pair to this list, kept in your browser only (localStorage) and never sent anywhere. When you have created all the Apps you want, copy the one command below and run it once — it exchanges every collected code and writes every key in a single pass. The codes sit in localStorage only until you use them; each is single-use and expires about an hour after GitHub issued it, so run this while they are fresh, then clear the list.
# pick a role in each App tab; the collected command appears here
The key files land at ~/.config/assay/<role>-app.pem, mode 0600 — umask 077 and an explicit chmod 600 both guarantee it, and the desk tools refuse anything else. Each apps.env line names the App by role: REVIEWER_APP_ID=1234567, ISSUE_LOOP_APP_ID=…. If the exchange reports no key for a role, that code has been used or has expired; delete the half-made App on GitHub and create it again.
Give the App a face
Download the icon for the App you just created and upload it as its avatar (App settings → Display information) so a review or commit is visually attributable. One mark per role, drawn in the site's ledger-green line; swap them for your own whenever you like — the point is only that each identity looks like itself in a PR's reviewer list.
| Mark | Role | File | Download |
|---|---|---|---|
| reviewer | app-icon-reviewer.png |
Download | |
| worker | app-icon-worker.png |
Download | |
| verifier | app-icon-verifier.png |
Download | |
| desk | app-icon-desk.png |
Download | |
| issue-loop | app-icon-issue-loop.png |
Download | |
| intake-loop | app-icon-intake-loop.png |
Download | |
| board-writer | app-icon-board-writer.png |
Download | |
| promote | app-icon-promote.png |
Download |
Then install it, and do the next one
Open https://github.com/apps/<slug>/installations/new — the slug is what the exchange printed — choose the account and Only select repositories, and record the installation ID from the resulting address bar as <ROLE>_INSTALL_ID_<OWNER> in apps.env. Full detail under Then install each App.
Then go back to the table for the next role. Eight Apps, eight codes, eight keys, one file of IDs.
# ~/.config/assay/apps.env — when all eight are done
REVIEWER_APP_ID=1234567
REVIEWER_INSTALL_ID_MY_ORG=87654321
WORKER_APP_ID=1234568
WORKER_INSTALL_ID_MY_ORG=87654322
VERIFIER_APP_ID=…
DESK_APP_ID=…
ISSUE_LOOP_APP_ID=…
INTAKE_LOOP_APP_ID=…
BOARD_WRITER_APP_ID=…