A complete operational guide for sr.ht: repositories, builds, mailing lists, issue tracking, pages, and the GraphQL API.
# Arch Linux
paru -S hut
# From source
git clone https://git.sr.ht/~emersion/hut
cd hut && go build . && mv hut ~/.local/bin/
hut init
This creates ~/.config/hut/config interactively. Alternatively, write it manually.
access-token-cmd cat ~/.config/srht/token
instance "git.sr.ht" {
access-token-cmd cat ~/.config/srht/token
}
instance "builds.sr.ht" {
access-token-cmd cat ~/.config/srht/token
}
instance "todo.sr.ht" {
access-token-cmd cat ~/.config/srht/token
}
instance "lists.sr.ht" {
access-token-cmd cat ~/.config/srht/token
}
instance "meta.sr.ht" {
access-token-cmd cat ~/.config/srht/token
}
instance "pages.sr.ht" {
access-token-cmd cat ~/.config/srht/token
}
The access-token-cmd can be any shell command that prints the token to stdout. Common patterns:
# From a file
access-token-cmd cat ~/.config/srht/token
# From pass
access-token-cmd pass show sourcehut/token
# From a secret manager
access-token-cmd secret-tool lookup service sourcehut key token
Place .hut.scfg in a repo root to override the global config for that repository:
# .hut.scfg
git-repo "~username/repo-name"
build-instance "builds.sr.ht"
lists-repo "~username/my-list"
hut git create my-repo
hut git create my-repo --description "A short description"
hut git create my-repo --visibility private # public | private | unlisted
hut git list
hut git list --user ~otheruser
hut git update my-repo --description "New description"
hut git update my-repo --visibility unlisted
hut git update my-repo --readme README.md # set rendered README
hut git delete my-repo
git clone https://git.sr.ht/~username/repo-name
git clone git@git.sr.ht:~username/repo-name # SSH
After creating a repo on sr.ht:
git remote add origin git@git.sr.ht:~username/repo-name
git push -u origin main
# Grant read access to another user
hut git acl grant my-repo ~collaborator read
# Grant write access
hut git acl grant my-repo ~collaborator write
# Revoke access
hut git acl revoke my-repo ~collaborator
# List ACL
hut git acl list my-repo
Attach binary artifacts to a tag or commit:
# Upload artifact to a release tag
hut git artifact upload my-repo v1.0.0 dist/my-binary.tar.gz
# Upload multiple artifacts
hut git artifact upload my-repo v1.0.0 dist/*.tar.gz
# List artifacts
hut git artifact list my-repo v1.0.0
# Delete artifact
hut git artifact delete my-repo v1.0.0 my-binary.tar.gz
# Submit the .build.yml in the current directory
hut builds submit .build.yml
# Submit with a specific note
hut builds submit .build.yml --note "Testing auth fix"
# Submit targeting a specific tag
hut builds submit .build.yml --tags "my-tag,another-tag"
hut builds list
hut builds list --count 20
hut builds list --status failed
hut builds show 12345
hut builds show 12345 -f
The -f flag tails the log output in real time, similar to tail -f.
hut builds cancel 12345
hut builds resubmit 12345
After a build fails, it stays alive for a short window so you can SSH in and debug:
hut builds ssh 12345
This connects you to the build runner VM at the point of failure. Useful for interactive debugging of flaky build environments.
Full key reference:
# Image: distribution to run the build on
# Format: <distro>/<version>
image: archlinux
# Architecture: x86_64 (default), aarch64
arch: x86_64
# Packages to install before running tasks
packages:
- git
- go
- make
# Repositories/sources to clone into the build environment
# Format: <url> or <url>#<branch>
sources:
- https://git.sr.ht/~username/repo-name
- https://git.sr.ht/~username/other-repo#feature-branch
# Artifacts: files to extract from the build and attach to the job
artifacts:
- repo-name/dist/binary
- repo-name/dist/archive.tar.gz
# Environment variables (plain values only; use secrets for sensitive data)
environment:
GO111MODULE: "on"
GOPROXY: "https://proxy.golang.org"
BUILD_VERSION: "1.0.0"
# Secrets: UUIDs from builds.sr.ht/secrets
# Placed at ~/.secrets/<uuid> inside the build VM
secrets:
- 00000000-0000-0000-0000-000000000000
# OAuth: grant the build job an API token with these grants
# Allows the build to interact with sr.ht services
oauth:
- pages.sr.ht/PAGES:RW
# Shell: drop into a shell after the last task (for debugging)
# Only useful when submitting manually; auto-triggered builds ignore this
shell: false
# Triggers: actions to run after the build completes
triggers:
- action: email
condition: failure
to: you@example.com
- action: webhook
condition: always
url: https://example.com/hook
- action: email
condition: always
cc: team@example.com
# Tasks: ordered list of named shell scripts
# Each task runs in a fresh shell; environment is shared
tasks:
- setup: |
cd repo-name
go mod download
- test: |
cd repo-name
go test ./...
- build: |
cd repo-name
make release
- upload: |
cd repo-name
hut pages publish -d my-site dist/
Common images (check builds.sr.ht for the full list):
| Image | Notes |
|---|---|
archlinux |
Rolling release |
alpine/edge |
Minimal, fast |
alpine/3.19 |
Pinned Alpine |
debian/bookworm |
Debian stable |
ubuntu/jammy |
Ubuntu 22.04 |
fedora/39 |
Fedora |
freebsd/14.0 |
FreeBSD |
openbsd/7.4 |
OpenBSD |
netbsd/10.0 |
NetBSD |
Place .build.yml at the repository root. Every push to any ref triggers a build automatically. No webhook configuration required.
repo/
.build.yml <- triggers on every push
src/
Place multiple manifests in .builds/:
repo/
.builds/
linux-amd64.yml
linux-arm64.yml
test.yml
src/
All files in .builds/ are submitted on every push. The .build.yml at the root is also submitted if present.
Append [skip ci] or [ci skip] to the commit message subject:
git commit -m "Fix typo in README [skip ci]"
Push with options to select a specific manifest:
# Submit a specific manifest file on push
git push -o submit=.builds/release.yml
# Submit multiple manifests
git push -o submit=.builds/test.yml -o submit=.builds/release.yml
# Skip all CI
git push -o skip-ci
Use the triggers key with conditions inside the manifest, or name manifests descriptively and use push options to control which runs.
Visit https://builds.sr.ht/secrets and create a secret file, environment variable, or SSH key. You get a UUID.
secrets:
- a1b2c3d4-e5f6-7890-abcd-ef1234567890
Secret files land at ~/.secrets/<uuid> inside the build VM.
Always disable command tracing before reading a secret:
tasks:
- deploy: |
set +x # disable xtrace
source ~/.secrets/a1b2c3d4-e5f6-7890-abcd-ef1234567890
set -x # re-enable xtrace
deploy --token "$SECRET_TOKEN"
The set +x pattern prevents the secret value from appearing in the build log. Without it, every executed command including variable assignments prints to the log.
When the secret is an environment variable file (key=value format), source it:
tasks:
- auth: |
set +x
source ~/.secrets/<uuid>
set -x
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com
Install msmtp and configure it at ~/.config/msmtp/config:
defaults
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.config/msmtp/msmtp.log
account sourcehut
host mail.sourcehut.org
port 587
auth on
user your@email.com
passwordeval cat ~/.config/msmtp/password
from your@email.com
account default: sourcehut
Then configure git to use msmtp:
git config --global sendemail.smtpserver /usr/bin/msmtp
git config --global sendemail.smtpserveroption "-a sourcehut"
git config --global sendemail.confirm always
git config --global sendemail.suppresscc self
git config --global format.signOff true
Configure the target mailing list per repository:
cd my-repo
git config sendemail.to "~username/list-name@lists.sr.ht"
Now git send-email targets the correct list by default for that repo.
# Create a patch from the last commit
git format-patch -1 HEAD
# Send it
git send-email 0001-my-fix.patch
# Create patches for the last 3 commits
git format-patch -3 HEAD
# Send with a cover letter
git send-email --cover-letter --annotate 0001-*.patch 0002-*.patch 0003-*.patch
The --annotate flag opens each patch in your editor before sending.
# Mark as revision 2
git send-email -v2 --cover-letter 0001-*.patch
This prepends [PATCH v2] to the subject, linking reviewers to the previous series.
To thread a revision under the original thread:
# Get the Message-ID of the original cover letter from the list archive
git send-email -v2 --in-reply-to="<message-id@lists.sr.ht>" 0001-*.patch
hut lists create my-list --description "Patches and discussion"
hut lists subscribe ~username/my-list your@email.com
hut lists archive ~username/my-list
hut lists patchset list ~username/my-list
hut lists patchset show ~username/my-list 42
hut lists patchset update-status ~username/my-list 42 APPLIED
hut lists patchset update-status ~username/my-list 42 NEEDS_REVISION
hut lists patchset update-status ~username/my-list 42 REJECTED
Valid statuses: PROPOSED, NEEDS_REVISION, SUPERSEDED, APPROVED, REJECTED, APPLIED
hut todo create my-tracker --description "Bug reports and features"
hut todo ticket create ~username/my-tracker --title "Something is broken"
# With a body from stdin
echo "Details about the bug." | hut todo ticket create ~username/my-tracker --title "Bug report"
# With body from a file
hut todo ticket create ~username/my-tracker --title "Feature request" --body feature.md
hut todo ticket comment ~username/my-tracker 7 --comment "I can reproduce this."
hut todo ticket update-status ~username/my-tracker 7 RESOLVED
hut todo ticket update-status ~username/my-tracker 7 CLOSED
hut todo ticket update-status ~username/my-tracker 7 IN_PROGRESS
Valid statuses: REPORTED, CONFIRMED, IN_PROGRESS, PENDING, RESOLVED, CLOSED
hut todo ticket label ~username/my-tracker 7 bug
hut todo ticket label ~username/my-tracker 7 enhancement
hut todo ticket assign ~username/my-tracker 7 ~collaborator
# Publish the current directory
hut pages publish
# Publish a specific directory
hut pages publish -d my-domain.tld dist/
# Publish and set the domain
hut pages publish -d my-domain.tld --domain my-domain.tld dist/
Use the oauth key to grant the build job pages write access, then call hut pages publish from within a task:
image: archlinux
packages:
- hut
- nodejs
- npm
oauth:
- pages.sr.ht/PAGES:RW
sources:
- https://git.sr.ht/~username/my-site
tasks:
- build: |
cd my-site
npm ci
npm run build
- deploy: |
cd my-site
hut pages publish -d my-domain.tld out/
The oauth grant creates a short-lived token inside the build environment that hut picks up automatically.
hut paste create myfile.txt
# With a custom filename shown in the UI
hut paste create myfile.txt --filename "config.fish"
# Set visibility
hut paste create myfile.txt --visibility unlisted
# Pipe directly
cat somefile.txt | hut paste create
# Here string
hut paste create <<'EOF'
This is my paste content.
EOF
# Command output
git diff HEAD | hut paste create --filename "my.diff"
All sr.ht services expose a GraphQL API. The hut graphql command sends raw queries.
# Query against a specific service
hut graphql git.sr.ht < query.graphql
# Inline query
echo '{ me { username email } }' | hut graphql meta.sr.ht
# With variables (JSON)
hut graphql git.sr.ht --variables '{"name": "my-repo"}' < query.graphql
All services accept Authorization: Bearer <token> with a JSON body containing query and optionally variables:
TOKEN=$(cat ~/.config/srht/token)
# meta.sr.ht: get current user
curl -s https://meta.sr.ht/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ me { username email } }"}' | jq .
# git.sr.ht: list repos
curl -s https://git.sr.ht/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ repositories { results { name visibility } } }"}' | jq .
# builds.sr.ht: get a build
curl -s https://builds.sr.ht/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ job(id: 12345) { status log } }"}' | jq .
hut git webhook create my-repo \
--url https://example.com/hook \
--events repo:post-update
hut git webhook list my-repo
hut git webhook delete my-repo <webhook-id>
sr.ht signs webhook payloads with Ed25519. The public key is published at https://meta.sr.ht/.well-known/sourcehut/ed25519.
Example verification in Python:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import load_pem_public_key
import base64, hashlib
def verify_webhook(public_key_pem: bytes, payload: bytes, signature_b64: str) -> bool:
pub = load_pem_public_key(public_key_pem)
sig = base64.b64decode(signature_b64)
digest = hashlib.sha256(payload).digest()
try:
pub.verify(sig, digest)
return True
except Exception:
return False
The X-Sourcehut-Signature header contains the base64-encoded signature.
Webhooks send JSON payloads. To respond by querying the API:
#!/usr/bin/env bash
# Called by webhook, reads JSON from stdin
PAYLOAD=$(cat)
REPO=$(echo "$PAYLOAD" | jq -r '.repo.name')
TOKEN=$(cat ~/.config/srht/token)
curl -s https://git.sr.ht/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"query\": \"{ repository(name: \\\"$REPO\\\") { HEAD { shorthand } } }\"}" | jq .
aerc integrates cleanly with mailing lists and patch workflows on sr.ht.
[sourcehut]
source = imaps://your@email.com@imap.fastmail.com:993
outgoing = msmtp -a sourcehut
default = INBOX
smtp-starttls = yes
from = Your Name <your@email.com>
copy-to = Sent
In ~/.config/aerc/binds.conf:
[messages]
# Apply a patch from a message
gp = :pipe -m git am -3<Enter>
# Reply with patch review
gr = :reply<Enter>
# Mark patchset as applied (send X-Sourcehut-Patchset-Update)
ga = :reply -a<Enter>:header X-Sourcehut-Patchset-Update APPLIED<Enter>
gn = :reply -a<Enter>:header X-Sourcehut-Patchset-Update NEEDS_REVISION<Enter>
[compose]
# Toggle quoted reply
<C-q> = :toggle-headers<Enter>
Create ~/.config/aerc/templates/patch-applied:
X-Sourcehut-Patchset-Update: APPLIED
Thanks, applied to main.
Use in aerc:
:reply -T patch-applied
When lists.sr.ht sees X-Sourcehut-Patchset-Update in a reply to a patchset thread, it updates the patchset status automatically.
b4 is a tool for working with patches sent to mailing lists. It can pull patchsets from lists.sr.ht archives.
paru -S b4
b4 am <message-id>
This downloads all patches in the series and prepares an mbox file ready for git am.
b4 am <message-id> -o patches/
git am patches/*.mbox
In ~/.config/b4/config:
[b4]
midmask = https://lists.sr.ht/%s
linkmask = https://lists.sr.ht/%s
# Gets the latest revision of a patch thread
b4 am --use-version 2 <original-message-id>
When you reply to a patchset thread on a lists.sr.ht mailing list and include this header, lists.sr.ht automatically updates the patchset status.
| Value | Meaning |
|---|---|
PROPOSED |
Patch submitted for review (default state) |
NEEDS_REVISION |
Reviewer requests changes |
SUPERSEDED |
This version replaced by a new revision |
APPROVED |
Reviewer approves, ready to merge |
REJECTED |
Patch will not be merged |
APPLIED |
Maintainer has applied the patch |
When composing a review reply in any mail client:
X-Sourcehut-Patchset-Update: NEEDS_REVISION
The approach looks good but the error handling needs work.
See inline comments.
Only maintainers and the original submitter can change status. lists.sr.ht validates permissions before updating.
sr.ht's developer community follows strict plain-text email conventions. Violating these makes patches harder to review and apply.
Plain text only. No HTML. No rich text. Set your mail client to always send plain text.
No top-posting. Quote the relevant context, then reply below it. When reviewing a patch, quote the relevant diff hunk and comment inline beneath it.
72-column line wrap. Wrap prose at 72 characters. Patch content (diff lines) can be longer, but your commentary should be wrapped. Most email clients and git format-patch handle this automatically.
Trim quoted context. Quote only what you are directly responding to. Do not quote entire threads.
Inline review comments. For patches, quote the specific hunk you are commenting on and place your comment directly below it. Use > for quoted lines.
Subject lines. Keep them clear and concise. For patch revisions, include [PATCH v2] etc.
Signature. Keep it short (four lines maximum, prefixed with -- ).
In ~/.config/aerc/aerc.conf:
[compose]
editor = nvim
aerc defaults to plain text composition. Avoid HTML or rich-text plugins.
| Service | Endpoint |
|---|---|
| meta.sr.ht | https://meta.sr.ht/query |
| git.sr.ht | https://git.sr.ht/query |
| builds.sr.ht | https://builds.sr.ht/query |
| todo.sr.ht | https://todo.sr.ht/query |
| lists.sr.ht | https://lists.sr.ht/query |
| pages.sr.ht | https://pages.sr.ht/query |
| paste.sr.ht | https://paste.sr.ht/query |
All endpoints accept POST with Content-Type: application/json and Authorization: Bearer <token>.
Interactive GraphQL explorer (GraphiQL) is available by visiting each endpoint in a browser with a valid token.
{
repositories {
results {
name
description
visibility
updated
}
}
}
mutation {
createRepository(name: "new-repo", visibility: PUBLIC, description: "My repo") {
id
name
}
}
{
job(id: 12345) {
status
note
tags
tasks {
name
status
}
}
}
{
jobs(count: 10) {
results {
id
status
note
created
}
}
}
mutation {
submitTicket(
tracker: "~username/my-tracker"
input: {
subject: "Bug: something is wrong"
body: "Steps to reproduce..."
}
) {
id
ref
}
}
{
me {
sshKeys {
id
fingerprint
comment
created
}
}
}
mutation {
createSSHKey(key: "ssh-ed25519 AAAA... comment") {
id
fingerprint
}
}
{
me {
username
email
url
created
location
bio
}
}
Add these to ~/.config/fish/config.fish or a file in ~/.config/fish/functions/:
# sr.ht token shortcut
function srht_token
cat ~/.config/srht/token
end
# GraphQL query helper
function srht_gql
set -l service $argv[1]
set -l query $argv[2]
set -l token (srht_token)
curl -s "https://$service/query" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d (echo "{\"query\": $query}" | jq -c .) | jq .
end
# Quick repo create + remote setup
function srht_new
set -l name $argv[1]
set -l desc $argv[2]
hut git create $name --description $desc
git remote add origin "git@git.sr.ht:~"(hut graphql meta.sr.ht -e '{ me { username } }' | jq -r .data.me.username)"/$name"
echo "Remote added. Push with: git push -u origin main"
end
# Clone a sr.ht repo by shorthand (~user/repo)
function srht_clone
set -l parts (string split / $argv[1])
set -l user $parts[1] # e.g. ~username
set -l repo $parts[2]
git clone "https://git.sr.ht/$user/$repo"
end
# Submit a build and follow the log
function srht_build
set -l manifest $argv[1]
if test -z "$manifest"
set manifest .build.yml
end
set -l id (hut builds submit $manifest | grep -oP 'job #\K[0-9]+')
echo "Build #$id submitted"
hut builds show $id -f
end
# Watch latest build
function srht_watch
set -l id (hut builds list --count 1 | awk 'NR==2 {print $1}')
hut builds show $id -f
end
# Send a patch series to the configured list
function srht_send
set -l count $argv[1]
if test -z "$count"
set count 1
end
git format-patch -$count HEAD
git send-email --annotate (ls -t *.patch | head -$count | sort)
end
# Paste a file and print the URL
function srht_paste
hut paste create $argv[1] | grep -oP 'https://paste\.sr\.ht/\S+'
end
# Paste stdin
function srht_pastein
cat | hut paste create | grep -oP 'https://paste\.sr\.ht/\S+'
end
# Open a repo in the browser
function srht_open
set -l name $argv[1]
set -l user (hut graphql meta.sr.ht -e '{ me { username } }' 2>/dev/null | jq -r .data.me.username 2>/dev/null)
if test -z "$user"
set user (cat ~/.config/srht/username 2>/dev/null)
end
xdg-open "https://git.sr.ht/~$user/$name"
end
# List all repos with visibility
function srht_repos
echo '{ repositories { results { name visibility description } } }' \
| hut graphql git.sr.ht \
| jq -r '.data.repositories.results[] | "\(.visibility)\t\(.name)\t\(.description // "")"' \
| column -t -s (printf '\t')
end
# Check build status for a repo's latest push
function srht_status
hut builds list --count 5 | column -t
end
# Update patchset status from the command line
function srht_patch_status
set -l tracker $argv[1]
set -l id $argv[2]
set -l status $argv[3]
hut lists patchset update-status $tracker $id $status
end
# Create a new repo
srht_new my-project "A cool project"
# Clone someone's repo
srht_clone ~sircmpwn/aerc
# Submit build and watch
srht_build .build.yml
# Send last 3 commits as a patch series
srht_send 3
# Paste a diff
git diff | srht_pastein
# List all your repos
srht_repos
# Mark patchset 42 as applied
srht_patch_status ~username/my-list 42 APPLIED
hut git create <name> create repo
hut git list list repos
hut git update <name> --description X update metadata
hut git acl grant <name> ~user write share access
hut git artifact upload <name> <tag> attach release artifact
hut builds submit .build.yml submit build
hut builds show <id> -f follow build log
hut builds ssh <id> debug failed build
hut todo ticket create ~u/t --title X new ticket
hut todo ticket update-status ~u/t N S change ticket status
hut lists patchset list ~u/l list patchsets
hut lists patchset update-status ~u/l N APPLIED
hut pages publish -d domain.tld dist/ publish site
hut paste create file.txt create paste
echo "..." | hut paste create paste from stdin
git push -o skip-ci skip build trigger
git push -o submit=.builds/release.yml trigger specific manifest
git format-patch -3 HEAD prepare 3 patches
git send-email --cover-letter *.patch send patch series