Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
599dbba
feat(market-making): add docker image, compose, and docker hub publish
julien-devatom Aug 4, 2026
8bde1da
refactor(market-making): publish docker image from ci instead of script
julien-devatom Aug 4, 2026
cc75810
feat(market-making): publish docker image on github release
julien-devatom Aug 4, 2026
2734841
ci(checks): port morpho-apps tag-releases and claude release notes
julien-devatom Aug 4, 2026
4256ee4
fix(market-making): persist /state volume and pin release target sha
julien-devatom Aug 4, 2026
3028dbc
fix(market-making): address codex review round on release + docker flow
julien-devatom Aug 4, 2026
ece5579
fix(market-making): scope bump releases to market-making, ignore *.env
julien-devatom Aug 5, 2026
28accb7
Merge origin/main: reconcile Railway deploy with docker hub distribution
julien-devatom Aug 6, 2026
87fff59
fix(market-making): close release-origin race and version-sync gaps
julien-devatom Aug 7, 2026
fe7fda1
chore(market-making): align compose before main sync
prd-carapulse[bot] Aug 10, 2026
84bfe29
Merge branch 'main' into market-bot-docker-deploy-ddc5d2
prd-carapulse[bot] Aug 10, 2026
cc55033
fix(market-making): resolve main compose conflict
prd-carapulse[bot] Aug 10, 2026
c7ce000
Merge origin/main: adapt docker distribution to the pnpm migration
julien-devatom Aug 10, 2026
6dd52c6
Merge remote branch: unify parallel main-sync resolutions
julien-devatom Aug 10, 2026
cccb69d
fix(repo): restore pnpm-era root manifest clobbered by bun tooling
julien-devatom Aug 10, 2026
af2f630
fix(repo): publicly hoist all dependencies for bun test resolution
julien-devatom Aug 10, 2026
66ef2de
fix(repo): drop bun-test manifest clobber from previous commit
julien-devatom Aug 10, 2026
da9e62b
fix(market-making): align container release operations
prd-carapulse[bot] Aug 10, 2026
5d6f176
ci(checks): add temporary hoist-layout diagnostics
julien-devatom Aug 10, 2026
79c2a92
Merge remote-tracking branch 'origin/market-bot-docker-deploy-ddc5d2'…
julien-devatom Aug 10, 2026
7d4aeae
fix(repo): stop bun test from mutating the pnpm dependency tree
julien-devatom Aug 10, 2026
49b7ee4
fix(market-making): gate railway deploy on release preflight
julien-devatom Aug 10, 2026
8f8426d
fix(repo): route workspace task filtering through pnpm
julien-devatom Aug 10, 2026
4fa977e
test(market-making): align pages-workflow contract with pnpm filtering
julien-devatom Aug 10, 2026
3dc5c87
fix(market-making): harden release tagging and pages workflow toolchain
julien-devatom Aug 10, 2026
cad3bfd
fix(market-making): resolve main integration conflicts
prd-carapulse[bot] Aug 11, 2026
fd2d8dd
chore(repo): sync current main (1/3)
prd-carapulse[bot] Aug 11, 2026
1a616c8
chore(repo): sync current main (2/3)
prd-carapulse[bot] Aug 11, 2026
0582ddb
chore(repo): sync current main (3/3)
prd-carapulse[bot] Aug 11, 2026
6fe720b
fix(market-making): build runnable release image
prd-carapulse[bot] Aug 11, 2026
4c5201d
fix(ci): address release review feedback
prd-carapulse[bot] Aug 11, 2026
13e950c
chore: align Dockerfile before base merge
prd-carapulse[bot] Aug 11, 2026
06377c5
Merge branch 'main' into market-bot-docker-deploy-ddc5d2
prd-carapulse[bot] Aug 11, 2026
55ad6e9
fix(docker): disable Husky in market-making image
prd-carapulse[bot] Aug 11, 2026
d1b043f
fix(market-making): address release review feedback
prd-carapulse[bot] Aug 11, 2026
8fe10f2
fix(market-making): address release review feedback
prd-carapulse[bot] Aug 11, 2026
c52b802
fix(market-making): address release review follow-ups
prd-carapulse[bot] Aug 11, 2026
344c531
Merge origin/main: adopt the quoter-bot rename across the release sur…
julien-devatom Aug 12, 2026
e452ba9
Merge origin/main: adopt the Railway privilege-boundary hardening
julien-devatom Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .claude/commands/ci-write-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,44 @@ with a comprehensive summary.

The release tags are provided in the `RELEASE_TAGS` environment variable as a space-separated list.

Tag format: `{app-name}-{version}` where version follows CalVer pattern `YYYY.MM.DD-N`
Tag format: `{bot-name}-{version}` where version follows CalVer pattern `YYYY.MM.DD-N`

Example: `curator-app-2025.10.16-1`
Example: `quoter-bot-2026.08.04-1`

Loop through each tag and extract the app name and version. Skip any tags that don't match the
Loop through each tag and extract the bot name and version. Skip any tags that don't match the
expected pattern.

### Step 2: Analyze Each App
### Step 2: Analyze Each Bot

For each release tag:

1. **Find the previous release tag** for that app:
1. **Find the previous release tag** for that bot:

```bash
git tag -l "{app}-*" --sort=-version:refname | head -5
git tag -l "{bot}-*" --sort=-version:refname | grep -Fxv -- "$RELEASE_TAG" | head -5
```

2. **Compare the diff** between the newly-published tag and the previous one:
Exclude the release currently being rewritten (`$RELEASE_TAG`): it already exists locally when
this command runs and must not be selected as its own comparison baseline.

2. **Compare the diff** between the newly-published tag and the previous one. Bots assemble their
behavior from the shared `packages/*` workspace, so include it alongside the bot's own tree:

```bash
git diff {previous-tag}...{new-tag} -- packages/{bot}
git diff {previous-tag}...{new-tag} -- bots/{bot} packages
```

3. **Get commit messages** in the release range for context:

```bash
git log {previous-tag}...{new-tag} --oneline -- packages/{bot}
git log {previous-tag}...{new-tag} --oneline -- bots/{bot} packages
```

4. **Extract PR numbers** from commit messages:

```bash
# Get PR numbers from merge commits and PR references
git log {previous-tag}...{new-tag} --oneline -- packages/{bot} | \
git log {previous-tag}...{new-tag} --oneline -- bots/{bot} packages | \
grep -oE '#[0-9]+' | \
sort -u
```
Expand Down
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,21 @@
**/*.log
**/.env
**/.env.*
# Env files under any name (`docker run --env-file` accepts arbitrary filenames such as
# market-making.env) hold secrets like MAKER_PRIVATE_KEY and must never enter the build context.
**/*.env
# Encrypted keystore files (the documented maker.json example and any keystore-named JSON) hold the
# maker key material and must never bake into an image via `COPY bots`.
**/maker.json
**/*keystore*.json
# No non-example YAML enters the build context: market-making configuration is YAML, may hold a
# private key, and `--config` accepts any operator-chosen filename — not just the default
# market-making.yaml. Images need no YAML at runtime; committed *.example.* templates stay
# copyable, and the root pnpm manifests are re-included because `pnpm install --frozen-lockfile`
# needs them.
**/*.yaml
**/*.yml
!**/*.example.yaml
!**/*.example.yml
!pnpm-lock.yaml
!pnpm-workspace.yaml
68 changes: 68 additions & 0 deletions .github/workflows/claude-write-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Claude write release notes

# Ported from morpho-apps: rewrites the GitHub-generated notes of freshly created bot releases with
# a Claude-authored summary, following the repo command .claude/commands/ci-write-release-notes.md.
# Triggered by the repository_dispatch that tag-releases.yml sends after creating releases. Manual
# re-run for a tag:
# gh api repos/morpho-org/morpho-bots/dispatches --method POST \
# --field event_type=write-release-notes \
# --field 'client_payload[release_tags]=quoter-bot-2026.08.04-1'
#
# The Claude step is skipped — not failed — while ANTHROPIC_API_KEY is not configured, so releasing
# keeps working before that secret exists; the GitHub-generated notes simply remain. Slack:
# release-slack-notify.yml already announced the release at publish time; to re-announce the
# rewritten notes, run that workflow manually with its `tag` input. (morpho-apps instead posts to
# per-app Slack channels from this workflow; this repo's single release channel makes that
# redundant.)

on:
repository_dispatch:
types: [write-release-notes]

jobs:
write-release-notes:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

- name: Fetch all tags
run: git fetch --tags --prune --force

# The secrets context is unavailable in job/step `if` expressions, so presence is probed in a
# step and exported as an output.
- name: Check Claude credentials
id: credentials
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -euo pipefail
if [ -n "$ANTHROPIC_API_KEY" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "ANTHROPIC_API_KEY is not configured; keeping the GitHub-generated release notes."
fi

- name: Write with Claude
if: steps.credentials.outputs.available == 'true'
uses: anthropics/claude-code-action@657fb7c9c986158a19624b357bcbc8c6deb83598 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# show_full_output stays off (unlike the morpho-apps original): the action's own docs
# reserve it for debugging in non-sensitive environments, and this job holds an API key
# and a write token while allowing Bash — a full transcript could retain
# credential-bearing tool output in the Actions logs.
prompt: /ci-write-release-notes
allowed_bots: 'github-actions[bot]'
claude_args: |
--allowedTools Bash,Read,Glob,Grep
env:
# description: "Space-separated list of release tags to update"
RELEASE_TAGS: ${{ github.event.client_payload.release_tags }}
64 changes: 58 additions & 6 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,48 @@ jobs:
github_environment: crossed-books-prod
ref: ${{ github.sha }}

Quoter-bot:
# Preflight BEFORE the Railway deploy: a labeled quoter-bot merge must carry a new CalVer
# package version and usable App credentials, or production would update while the GitHub
# release, Docker Hub image, and Slack announcement never come to exist. Failing here leaves
# production untouched and the operator fixes the version bump or credentials, then re-labels.
Quoter-bot-preflight:
needs: Select
if: ${{ needs.Select.outputs.quoter_bot == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.sha }}
- name: Validate release version and tag availability
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
version="$(node -p "require('./bots/quoter-bot/package.json').version")"
echo "$version" | grep -Eq '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[1-9][0-9]*$' \
|| { echo "quoter-bot package version must use CalVer YYYY.MM.DD-N (got: $version) — bump it in the release PR" >&2; exit 1; }
if gh release view "quoter-bot-$version" >/dev/null 2>&1; then
echo "release quoter-bot-$version already exists — bump the package version in the release PR" >&2
exit 1
fi
# A bare git tag with no release also blocks: `gh release create --target` is ignored for
# a pre-existing tag, so the release would attach to that old commit and the image would
# be built from it instead of this merge.
if git ls-remote --exit-code origin "refs/tags/quoter-bot-$version" >/dev/null 2>&1; then
echo "git tag quoter-bot-$version already exists without a release — delete the stale tag or bump the package version" >&2
exit 1
fi
# Proving the App credentials mint BEFORE deploying: Release-quoter-bot refuses the
# default-token fallback (its releases cannot trigger the image publish), so missing
# credentials must stop the flow while production is still untouched.
- name: Mint app installation token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.GIT_BOT_CLIENT_ID }}
private-key: ${{ secrets.GIT_BOT_PRIVATE_KEY }}

Quoter-bot:
needs: Quoter-bot-preflight
uses: ./.github/workflows/deploy-quoter-bot-production.yml
secrets: inherit
with:
Expand Down Expand Up @@ -199,22 +238,35 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
# Unlike the Railway-only bots above, a quoter-bot release must FIRE downstream `release`
# workflows: deploy-quoter-bot.yml publishes the Docker Hub image and then announces on
# Slack. Events created with the default GITHUB_TOKEN never trigger workflows, so mint the
# same App installation token tag-releases.yml uses — and FAIL here rather than fall back to
# the default token, which would mint a release that can never grow its operator image. The
# preflight job already proved these credentials mint, so a failure here is transient; re-run
# this job once it clears.
- name: Mint app installation token
id: app-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.GIT_BOT_CLIENT_ID }}
private-key: ${{ secrets.GIT_BOT_PRIVATE_KEY }}
- name: Create release
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BOT: quoter-bot
SHA: ${{ github.sha }}
run: |
set -euo pipefail
date="$(date -u +%Y.%m.%d)"
n=$(( $(git tag -l "${BOT}-${date}-*" | wc -l) + 1 ))
tag="${BOT}-${date}-${n}"
version="$(node -p "require('./bots/quoter-bot/package.json').version")"
echo "$version" | grep -Eq '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[1-9][0-9]*$' \
|| { echo "quoter-bot package version must use CalVer YYYY.MM.DD-N" >&2; exit 1; }
tag="${BOT}-${version}"
# `|| true` guards against SIGPIPE aborting the job under `set -o pipefail` (git is
# killed when head closes the pipe early once there are many tags).
prev="$(git tag -l "${BOT}-*" --sort=-version:refname | head -n 1 || true)"
Expand Down
157 changes: 157 additions & 0 deletions .github/workflows/deploy-quoter-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Deploy quoter-bot

# Publishes the quoter-bot bot image to Docker Hub when a `quoter-bot-*` GitHub release is
# published (repo CalVer convention: `quoter-bot-YYYY.MM.DD-N`), or on manual dispatch. Unlike
# the Railway bots (deploy-bot.yml), "deploy" here means publish only: operators pull and run the
# image themselves (see bots/quoter-bot/README.md), so there is no service to restart. The Slack
# announcement comes AFTER a successful publish: release-slack-notify.yml skips quoter-bot
# release events and the final step here re-enters it through its manual `tag` input once every
# image tag is pushed — so an announced release always has its image.
#
# Releases come from tag-releases.yml (a merged PR bumping the bot's package.json version to a new
# CalVer value) or from deploy-production.yml's Release-quoter-bot job after a successful
# `release-quoter-bot`-labeled Railway deploy. Both create the release with a GitHub App
# installation token precisely so this workflow fires — GitHub never runs workflows for events
# raised with the default GITHUB_TOKEN. A user-created release (`gh release create` or the releases
# UI) triggers identically. workflow_dispatch stays as the escape hatch for re-publishing, e.g.
# `gh workflow run deploy-quoter-bot.yml -f tag=latest`.
#
# A release publish builds the tagged commit and pushes `<release tag>` plus `git-<shortsha>`. The
# highest non-prerelease CalVer release also moves `latest`; backfilled older releases and
# prereleases never do. A dispatch builds the dispatched ref and pushes the `tag` input (default
# `latest`) plus `git-<shortsha>`.
#
# Credentials live in the `quoter-bot-dockerhub` GitHub Environment (distinct from
# `quoter-bot-production`, which holds the Railway deploy credentials): secrets
# DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (a Docker Hub access token, write scope) plus variable
# DOCKERHUB_REPOSITORY (`<namespace>/<name>`, e.g. `morphoorg/quoter-bot-bot`). In the
# environment's deployment branches/tags policy allow branch `main` AND tags matching
# `quoter-bot-*` — release runs execute on the tag ref, so a branch-only policy rejects them.

on:
release:
types: [published]
Comment thread
julien-devatom marked this conversation as resolved.
workflow_dispatch:
inputs:
tag:
description: Movable primary tag to publish alongside the immutable git-<sha> tag
required: false
type: string
default: latest

permissions:
contents: read

concurrency:
# Serialize publishes so two runs can't interleave their pushes of the movable tag.
group: deploy-quoter-bot
cancel-in-progress: false

jobs:
Publish:
# Releases are repo-wide (the Railway bots cut `blue-liq-*` etc.); only quoter-bot tags
# concern this image. Other releases simply skip this job.
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'quoter-bot-') }}
runs-on: ubuntu-latest
environment: quoter-bot-dockerhub
permissions:
contents: read
# `gh workflow run` for the post-publish Slack announcement.
actions: write
steps:
# On a release event this checks out the tagged commit (github.sha is the tag's commit), so
# the image is built from exactly the released tree, not main HEAD.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

# The token reaches docker via stdin only — never argv, never a workflow-file literal.
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
set -euo pipefail
: "${DOCKERHUB_USERNAME:?set secret DOCKERHUB_USERNAME on the quoter-bot-dockerhub environment}"
: "${DOCKERHUB_TOKEN:?set secret DOCKERHUB_TOKEN on the quoter-bot-dockerhub environment}"
printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin

# The build context is the repo root so the pnpm workspace (packages/*) resolves — see
# bots/quoter-bot/Dockerfile. The docker release tag is the git tag verbatim, so the
# image, git tag, and GitHub release cross-reference with zero transformation.
- name: Build and push
env:
REPOSITORY: ${{ vars.DOCKERHUB_REPOSITORY }}
EVENT: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
PRERELEASE: ${{ github.event.release.prerelease }}
DISPATCH_TAG: ${{ inputs.tag || 'latest' }}
SHA: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
: "${REPOSITORY:?set variable DOCKERHUB_REPOSITORY on the quoter-bot-dockerhub environment}"
# Docker Hub `<namespace>/<name>` only. A dotted (or `localhost`) first component would be
# read by docker as a registry host and silently push somewhere other than Docker Hub.
echo "$REPOSITORY" | grep -Eq '^[a-z0-9]+((_|__|-+)[a-z0-9]+)*/[a-z0-9]+(([._]|__|-+)[a-z0-9]+)*$' \
|| { echo "DOCKERHUB_REPOSITORY must be a lowercase Docker Hub <namespace>/<name> repository" >&2; exit 1; }
if [ "$EVENT" = "release" ]; then
CALVER_PATTERN="^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[1-9][0-9]*$"
# The release tag must equal quoter-bot-<package version> of the tagged commit:
# a mistyped or version-skewed release would otherwise publish an image (and move
# `latest`) whose tag disagrees with `mm --version` inside it.
package_version="$(node -p "require('./bots/quoter-bot/package.json').version")"
if ! [[ "$package_version" =~ $CALVER_PATTERN ]]; then
echo "package version $package_version is not CalVer (expected YYYY.MM.DD-N) — refusing to publish release image tags" >&2
exit 1
fi
expected="quoter-bot-${package_version}"
if [ "$RELEASE_TAG" != "$expected" ]; then
echo "release tag $RELEASE_TAG does not match the tagged commit's package version ($expected) — delete the release and cut it from a matching version bump" >&2
exit 1
fi
tags=("$RELEASE_TAG")
# `latest` tracks the highest stable CalVer release, not the most recently published
# release. This prevents a later backfill of an older release from downgrading it.
if [ "$PRERELEASE" != "true" ]; then
highest_stable_tag="$(
gh api --paginate "repos/$GITHUB_REPOSITORY/releases?per_page=100" \
--jq '.[] | select(.draft == false and .prerelease == false) | .tag_name' \
| grep -E '^quoter-bot-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[1-9][0-9]*$' \
| sort -V \
| tail -n 1
)"
if [ "$RELEASE_TAG" = "$highest_stable_tag" ]; then
tags+=("latest")
else
echo "Not moving latest: $highest_stable_tag is newer than backfilled release $RELEASE_TAG"
fi
fi
else
if [[ "$DISPATCH_TAG" == quoter-bot-* || "$DISPATCH_TAG" == git-* ]]; then
echo "refusing to overwrite immutable release or commit tag $DISPATCH_TAG from a manual dispatch" >&2
exit 1
fi
tags=("$DISPATCH_TAG")
fi
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
tags+=("git-${SHA:0:7}")
build_args=()
for tag in "${tags[@]}"; do
echo "$tag" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' \
|| { echo "invalid docker tag: $tag" >&2; exit 1; }
build_args+=(--tag "$REPOSITORY:$tag")
done
echo "Building ${tags[*]} from $SHA…"
docker build --file bots/quoter-bot/Dockerfile.release "${build_args[@]}" .
for tag in "${tags[@]}"; do
docker push "$REPOSITORY:$tag"
echo "Published docker.io/$REPOSITORY:$tag"
done

# Announce only now that every image tag exists. release-slack-notify.yml deliberately skips
# quoter-bot release events; its manual `tag` input re-enters it here. workflow_dispatch
# fired with the default GITHUB_TOKEN does start workflows (unlike tag/release events).
- name: Announce release on Slack
if: github.event_name == 'release'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh workflow run release-slack-notify.yml -f tag="$RELEASE_TAG"
Loading
Loading