Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
0953759
chore(quoter-bot): align compose before main sync
prd-carapulse[bot] Aug 14, 2026
b27c75f
Merge branch main into market-bot-docker-deploy-ddc5d2
prd-carapulse[bot] Aug 14, 2026
0ac9be9
fix(quoter-bot): resolve main compose conflict
prd-carapulse[bot] Aug 14, 2026
e3ed4e6
Merge origin/main: absorb quoter-bot security middleware and retry work
julien-devatom Aug 14, 2026
3eabd40
Merge remote-tracking branch 'origin/market-bot-docker-deploy-ddc5d2'…
julien-devatom Aug 14, 2026
ab9d943
fix(quoter-bot): correct dockerhub tag policy docs and notes baselines
julien-devatom Aug 14, 2026
3bd75a9
test(quoter-bot): cover release review fixes
prd-carapulse[bot] Aug 14, 2026
47d18b9
fix(quoter-bot): harden release label range, notes baselines, app perms
julien-devatom Aug 14, 2026
bd8ffb2
Merge origin/main: relocate operator docker docs into the reference
julien-devatom Aug 14, 2026
f0e3ee3
chore(quoter-bot): sync superseded release branch with main
prd-carapulse[bot] Aug 14, 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
20 changes: 18 additions & 2 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ name: Deploy production
# GitHub release + git tag (CalVer `{bot}-YYYY.MM.DD-N`). Triggered on push:main (not
# pull_request:closed) so the run's ref is refs/heads/main — satisfying a main-only environment
# branch policy and giving the correct main HEAD in github.sha. Releasing publishes the tag, which
# fires release-slack-notify.yml. Runs on every push; when no release-* label is present, the
# deploy jobs are simply skipped.
# fires release-slack-notify.yml. A quoter-bot release also pushes the bot image to Docker Hub
# (commit-hash + `latest` tags) after the Railway deploy succeeds, without gating the release.
# Runs on every push; when no release-* label is present, the deploy jobs are simply skipped.

on:
push:
Expand Down Expand Up @@ -109,6 +110,21 @@ jobs:
with:
ref: ${{ github.sha }}

Quoter-bot-image:
# Wait for both the successful deploy and its GitHub release tag before publishing image tags.
# Release-quoter-bot still depends only on Quoter-bot, so Docker Hub cannot block the release.
needs: [Select, Quoter-bot, Release-quoter-bot]
if: ${{ needs.Select.outputs.quoter_bot == 'true' }}
Comment on lines +113 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep image publishing out of the production lock

When Docker Hub is slow, down, or the quoter-bot-dockerhub environment/OIDC setup is misconfigured, this new post-release job still runs inside the workflow-level deploy-production concurrency group, so the release can be created but subsequent pushes or manual production deploys for any bot remain queued until this publish job finishes or times out. That makes the Docker side channel gate later production releases despite not gating the current Release-quoter-bot; move the image publish to a separate dispatched workflow or otherwise release the production concurrency before publishing.

Useful? React with 👍 / 👎.

# A called workflow can never hold more permissions than its caller job grants, so the OIDC
# token minting (id-token) the Docker Hub login needs must be granted here too.
permissions:
contents: read
id-token: write
uses: ./.github/workflows/publish-quoter-bot-dockerhub.yml
secrets: inherit
with:
ref: ${{ github.sha }}

Release-blue:
# A skipped/failed Blue deploy skips this job too, so a bot is never tagged unless it deployed.
needs: Blue
Expand Down
162 changes: 162 additions & 0 deletions .github/workflows/publish-quoter-bot-dockerhub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: Publish quoter-bot Docker Hub

# Reusable publish-only job for quoter-bot's public Docker Hub image. Production's release selector
# calls this workflow after the Railway deploy succeeds: the repo-root build of
# bots/quoter-bot/Dockerfile is pushed tagged with the release commit hash and, when no newer
# release has shipped, `latest`. The push never gates the GitHub release. Authentication exchanges
# the run's GitHub OIDC token through the Docker org's OIDC connection (docker/login-action v4.5+), so CI stores no static Docker Hub
# credential.

on:
workflow_call:
inputs:
ref:
description: Git SHA/ref to build and publish
required: true
type: string

permissions:
contents: read

jobs:
Publish:
runs-on: ubuntu-latest
timeout-minutes: 30
# Holds secret DOCKERHUB_OIDC_CONNECTIONID plus vars DOCKER_USERNAME and DOCKER_REPOSITORY;
# scope its deployment branches to `main` so the OIDC exchange is unreachable from arbitrary PR
# branches. Referencing an environment also makes GitHub mint the OIDC token with an
# environment-based subject (`repo:morpho-org/morpho-bots:environment:quoter-bot-dockerhub`) —
# the Docker-side connection ruleset must match that subject, not a `ref:refs/heads/*` one.
environment: quoter-bot-dockerhub
permissions:
contents: read
# Mints the GitHub OIDC token docker/login-action exchanges for a Docker Hub access token.
id-token: write
concurrency:
# Serialize pushes so `latest` cannot land out of order across overlapping runs.
group: publish-quoter-bot-dockerhub
cancel-in-progress: false
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ inputs.ref }}
# Full history and tags: the latest-tag gate below needs release tags and ancestry.
fetch-depth: 0

# Fail loud up front on a misconfigured environment instead of failing late at push time with
# an opaque invalid-reference error.
- name: Check environment configuration
env:
DOCKER_USERNAME: ${{ vars.DOCKER_USERNAME }}
DOCKER_REPOSITORY: ${{ vars.DOCKER_REPOSITORY }}
DOCKERHUB_OIDC_CONNECTIONID: ${{ secrets.DOCKERHUB_OIDC_CONNECTIONID }}
run: |
set -euo pipefail
: "${DOCKER_USERNAME:?var unset in the quoter-bot-dockerhub environment}"
: "${DOCKER_REPOSITORY:?var unset in the quoter-bot-dockerhub environment}"
: "${DOCKERHUB_OIDC_CONNECTIONID:?secret unset in the quoter-bot-dockerhub environment}"

# Tag with the resolved hash so a branch/tag `ref` input still produces an immutable tag.
- name: Resolve commit
id: commit
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
env:
DOCKERHUB_OIDC_CONNECTIONID: ${{ secrets.DOCKERHUB_OIDC_CONNECTIONID }}
# The exchanged access token must outlive the image build that runs between login and
# push; the 300s default is shorter than this pnpm install + workspace build.
DOCKERHUB_OIDC_EXPIREIN: '1800'
with:
username: ${{ vars.DOCKER_USERNAME }}

# Docker tags are mutable by default. Check through the authenticated Docker session so a
# first publish can distinguish a missing repository from an existing private tag. An existing
# immutable tag is reused on reruns so a failed/missed `latest` update can recover safely.
- name: Check immutable SHA tag
id: sha-tag
env:
DOCKER_USERNAME: ${{ vars.DOCKER_USERNAME }}
DOCKER_REPOSITORY: ${{ vars.DOCKER_REPOSITORY }}
COMMIT_SHA: ${{ steps.commit.outputs.sha }}
run: |
set -euo pipefail
image="${DOCKER_USERNAME}/${DOCKER_REPOSITORY}:${COMMIT_SHA}"
inspect_error="$(mktemp)"
trap 'rm -f "$inspect_error"' EXIT
if docker buildx imagetools inspect "$image" >/dev/null 2>"$inspect_error"; then
echo 'commit SHA tag already exists; reusing immutable image'
echo "exists=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if grep -Eqi 'manifest unknown|not found|pull access denied|repository does not exist' "$inspect_error"; then
echo 'commit SHA tag is not readable; proceeding with first publish'
exit 0
fi
echo 'unexpected Docker registry inspection failure'
exit 1

# `latest` must only move forward. A rerun may legitimately backfill an older release's
# commit tag, but when a quoter-bot release tag descends from the built commit a newer
# release already shipped — leave `latest` where it is. Release tags are cut by
# deploy-production.yml, whose concurrency group serializes release runs, so the newest
# release cannot change mid-run.
- name: Gate the latest tag
id: tags
env:
DOCKER_USERNAME: ${{ vars.DOCKER_USERNAME }}
DOCKER_REPOSITORY: ${{ vars.DOCKER_REPOSITORY }}
COMMIT_SHA: ${{ steps.commit.outputs.sha }}
run: |
set -euo pipefail
tags="${DOCKER_USERNAME}/${DOCKER_REPOSITORY}:${COMMIT_SHA}"
newer=''
for tag in $(git tag -l 'quoter-bot-*'); do
commit="$(git rev-parse "${tag}^{commit}")"
if [ "$commit" != "$COMMIT_SHA" ] \
&& git merge-base --is-ancestor "$COMMIT_SHA" "$commit"; then
newer="$tag"
break
fi
done
if [ -n "$newer" ]; then
echo "::notice::release ${newer} descends from ${COMMIT_SHA}; not moving latest"
echo "move_latest=false" >> "$GITHUB_OUTPUT"
else
tags="${tags},${DOCKER_USERNAME}/${DOCKER_REPOSITORY}:latest"
echo "move_latest=true" >> "$GITHUB_OUTPUT"
fi
echo "tags=${tags}" >> "$GITHUB_OUTPUT"

- name: Build and push
if: ${{ steps.sha-tag.outputs.exists != 'true' }}
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
# Path context (the checkout above, honoring inputs.ref) — the default Git context would
# rebuild from the triggering ref instead. The context must be the repository root so
# workspace packages resolve.
context: .
file: bots/quoter-bot/Dockerfile
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ steps.commit.outputs.sha }}

# A previous attempt may have published the immutable SHA before failing to update `latest`.
# Retag the existing manifest instead of rebuilding or mutating the immutable tag.
- name: Recover latest from immutable SHA tag
if: ${{ steps.sha-tag.outputs.exists == 'true' && steps.tags.outputs.move_latest == 'true' }}
env:
DOCKER_USERNAME: ${{ vars.DOCKER_USERNAME }}
DOCKER_REPOSITORY: ${{ vars.DOCKER_REPOSITORY }}
COMMIT_SHA: ${{ steps.commit.outputs.sha }}
run: |
set -euo pipefail
image="${DOCKER_USERNAME}/${DOCKER_REPOSITORY}:${COMMIT_SHA}"
latest="${DOCKER_USERNAME}/${DOCKER_REPOSITORY}:latest"
docker buildx imagetools create --tag "$latest" "$image"
39 changes: 26 additions & 13 deletions bots/quoter-bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# syntax=docker/dockerfile:1
# Image for the quoter-bot bot. The build context MUST be the repository root so workspace
# packages resolve. Node only: pnpm installs, esbuild bundles, node runs.
FROM node:24.14.1-slim
#
# Two stages so the shipped image carries only this bot: `build` installs the workspace and bundles
# the bot; the runtime stage starts from a fresh base and receives nothing but the bot's
# self-contained bundle. The image publishes publicly on release (morphoorg/quoter), so no other
# bot's code, no workspace source, and no package manager may ship in the final stage.
FROM node:24.14.1-slim AS build
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
ENV HOME=/home/node

# Runtime configuration may include a funded maker key. The entrypoint starts as root only long
# enough to repair Railway's root-owned volume, then setpriv atomically execs the bot without root
# identities or capabilities. Corepack writes its pnpm shim to /usr/local/bin, so enable it while
# building as root. Install util-linux explicitly so the runtime always provides setpriv.
RUN /usr/bin/apt-get update \
&& /usr/bin/apt-get install -y --no-install-recommends util-linux \
&& /usr/bin/rm -rf /var/lib/apt/lists/*
# Corepack writes its pnpm shim to /usr/local/bin, so enable it while still root.
RUN /usr/local/bin/corepack enable pnpm
RUN /usr/bin/mkdir -p /repo /state && /usr/bin/chown node:node /repo
RUN /usr/bin/mkdir -p /repo && /usr/bin/chown node:node /repo

# Install dependencies and run workspace build scripts without root privileges. The official image
# owns /home/node, so Corepack and pnpm can write their user-level cache under HOME.
Expand All @@ -31,10 +30,24 @@ COPY --chown=node:node bots ./bots
RUN pnpm install --frozen-lockfile
RUN pnpm -r --if-present run build

# Railway must start the container as root so the entrypoint can repair its root-owned mount. Install
# a fresh root-owned, non-writable copy only after all node-controlled install and build steps, then
# immediately drop to node with setpriv before executing application code.
USER root
# Runtime stage. dist/src/index.js is a self-contained esbuild bundle (scripts/build.ts,
# bundle: true), so no node_modules ships; the bot's package.json rides along only so node treats
# the bundle's .js as ESM ("type": "module"). Files stay root-owned and read-only to the runtime
# user. Install util-linux explicitly so the runtime always provides setpriv.
FROM node:24.14.1-slim
ENV HOME=/home/node

RUN /usr/bin/apt-get update \
&& /usr/bin/apt-get install -y --no-install-recommends util-linux \
&& /usr/bin/rm -rf /var/lib/apt/lists/*
RUN /usr/bin/mkdir -p /state

COPY --from=build --chown=0:0 --chmod=0555 /repo/bots/quoter-bot/package.json /repo/bots/quoter-bot/package.json
COPY --from=build --chown=0:0 --chmod=0555 /repo/bots/quoter-bot/dist /repo/bots/quoter-bot/dist

# Railway must start the container as root so the entrypoint can repair its root-owned mount. The
# entrypoint stays root only long enough to chown the volume, then setpriv atomically execs the bot
# without root identities or capabilities — runtime configuration may include a funded maker key.
COPY --chown=0:0 --chmod=0555 bots/quoter-bot/scripts/railway-entrypoint.sh /usr/local/sbin/railway-entrypoint.sh
WORKDIR /repo/bots/quoter-bot
CMD ["/usr/local/sbin/railway-entrypoint.sh", "start", "--verbose"]
Loading
Loading