~kris/dots

srice

ref: 9f828eb14bdd54d2c4fd8a3b2c90253021df3152 srice/doc/sourcehut.md -rw-r--r-- 26.1 KiB
9f828eb1 — Kris Yotam mksh: backslash-escape commands in history hook to bypass module aliases (wc=tokei, tr=transmission-remote, cat=bat) 2 months ago

#SourceHut (sr.ht) Workflow Reference

A complete operational guide for sr.ht: repositories, builds, mailing lists, issue tracking, pages, and the GraphQL API.


#1. hut CLI Setup

#Install

# Arch Linux
paru -S hut

# From source
git clone https://git.sr.ht/~emersion/hut
cd hut && go build . && mv hut ~/.local/bin/

#Initialize

hut init

This creates ~/.config/hut/config interactively. Alternatively, write it manually.

#~/.config/hut/config

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

#Per-repo .hut.scfg

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"

#2. Repository Management

#Create a Repository

hut git create my-repo
hut git create my-repo --description "A short description"
hut git create my-repo --visibility private   # public | private | unlisted

#List Repositories

hut git list
hut git list --user ~otheruser

#Update Repository Metadata

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

#Delete a Repository

hut git delete my-repo

#Clone

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

#Access Control (ACL)

# 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

#Artifact Upload

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

#3. Build System

#Submit a Build

# 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"

#List Builds

hut builds list
hut builds list --count 20
hut builds list --status failed

#Show Build Details

hut builds show 12345

#Follow Build Output (Live)

hut builds show 12345 -f

The -f flag tails the log output in real time, similar to tail -f.

#Cancel a Build

hut builds cancel 12345

#Resubmit a Build

hut builds resubmit 12345

#SSH into a Failed Build

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.


#4. .build.yml Manifest Reference

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/

#Available Images

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

#5. Auto-trigger Patterns

#Trigger on Push

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/

#Multiple Build Manifests

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.

#Skip CI

Append [skip ci] or [ci skip] to the commit message subject:

git commit -m "Fix typo in README [skip ci]"

#Custom Build on Push

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

#Branch-Specific Triggers

Use the triggers key with conditions inside the manifest, or name manifests descriptively and use push options to control which runs.


#6. Secret Management

#Create a Secret on builds.sr.ht

Visit https://builds.sr.ht/secrets and create a secret file, environment variable, or SSH key. You get a UUID.

#Reference in .build.yml

secrets:
  - a1b2c3d4-e5f6-7890-abcd-ef1234567890

Secret files land at ~/.secrets/<uuid> inside the build VM.

#Use Secrets Safely

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.

#Environment Variable Secrets

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

#7. Patch Workflow

#Configure git send-email with msmtp

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

#Per-repo sendemail.to

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.

#Send a Single Patch

# Create a patch from the last commit
git format-patch -1 HEAD

# Send it
git send-email 0001-my-fix.patch

#Send Multiple Patches as a Series

# 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.

#Send a Revision of a Previous Series

# 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.

#In-Reply-To (Threading)

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

#8. Mailing Lists

#Create a List

hut lists create my-list --description "Patches and discussion"

#Subscribe to a List

hut lists subscribe ~username/my-list your@email.com

#Archive a List

hut lists archive ~username/my-list

#List Patchsets

hut lists patchset list ~username/my-list

#Show a Patchset

hut lists patchset show ~username/my-list 42

#Update Patchset Status

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


#9. Issue Tracking (todo.sr.ht)

#Create a Tracker

hut todo create my-tracker --description "Bug reports and features"

#Create a Ticket

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

#Comment on a Ticket

hut todo ticket comment ~username/my-tracker 7 --comment "I can reproduce this."

#Update Ticket Status

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

#Add a Label

hut todo ticket label ~username/my-tracker 7 bug
hut todo ticket label ~username/my-tracker 7 enhancement

#Assign a Ticket

hut todo ticket assign ~username/my-tracker 7 ~collaborator

#10. Pages Deployment

#Publish a Directory

# 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/

#In-build Deployment

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.


#11. Paste

#Create a Paste from a File

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

#Create a Paste from stdin

# 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"

#12. GraphQL API

All sr.ht services expose a GraphQL API. The hut graphql command sends raw queries.

#Send a Query

# 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

#curl Patterns

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 .

#13. Webhook Configuration

#Create a Git Webhook

hut git webhook create my-repo \
  --url https://example.com/hook \
  --events repo:post-update

#List Webhooks

hut git webhook list my-repo

#Delete a Webhook

hut git webhook delete my-repo <webhook-id>

#Verify Webhook Signatures

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.

#GraphQL Query in a Webhook Handler

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 .

#14. aerc Email Client

aerc integrates cleanly with mailing lists and patch workflows on sr.ht.

#accounts.conf

[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

#Keybindings for Patch Workflows

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>

#Reply Templates with X-Sourcehut-Patchset-Update

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.


#15. b4 Integration with sr.ht Lists

b4 is a tool for working with patches sent to mailing lists. It can pull patchsets from lists.sr.ht archives.

#Install

paru -S b4

#Fetch a Patchset by Message-ID

b4 am <message-id>

This downloads all patches in the series and prepares an mbox file ready for git am.

#Apply the Patchset

b4 am <message-id> -o patches/
git am patches/*.mbox

#Configure b4 for sr.ht

In ~/.config/b4/config:

[b4]
midmask = https://lists.sr.ht/%s
linkmask = https://lists.sr.ht/%s

#Fetch Latest Revision

# Gets the latest revision of a patch thread
b4 am --use-version 2 <original-message-id>

#16. X-Sourcehut-Patchset-Update Header

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

#Usage in a Reply

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.


#17. Email Etiquette

sr.ht's developer community follows strict plain-text email conventions. Violating these makes patches harder to review and apply.

#Rules

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 -- ).

#aerc Plain Text Config

In ~/.config/aerc/aerc.conf:

[compose]
editor = nvim

aerc defaults to plain text composition. Avoid HTML or rich-text plugins.


#18. GraphQL Endpoints Table

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.


#19. Useful GraphQL Queries

#List Repositories

{
  repositories {
    results {
      name
      description
      visibility
      updated
    }
  }
}

#Create a Repository

mutation {
  createRepository(name: "new-repo", visibility: PUBLIC, description: "My repo") {
    id
    name
  }
}

#Check Build Status

{
  job(id: 12345) {
    status
    note
    tags
    tasks {
      name
      status
    }
  }
}

#List Recent Builds

{
  jobs(count: 10) {
    results {
      id
      status
      note
      created
    }
  }
}

#Submit a Ticket

mutation {
  submitTicket(
    tracker: "~username/my-tracker"
    input: {
      subject: "Bug: something is wrong"
      body: "Steps to reproduce..."
    }
  ) {
    id
    ref
  }
}

#List SSH Keys

{
  me {
    sshKeys {
      id
      fingerprint
      comment
      created
    }
  }
}

#Add an SSH Key

mutation {
  createSSHKey(key: "ssh-ed25519 AAAA... comment") {
    id
    fingerprint
  }
}

#Get Current User Info

{
  me {
    username
    email
    url
    created
    location
    bio
  }
}

#20. Fish Shell Functions for sr.ht Operations

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

#Usage Examples

# 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

#Quick Reference Card

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