Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 4 additions & 5 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,15 @@ The tags (`cache-<sha>`, one per build) are not pruned yet.
`ubuntu-24.04-arm`, publishing only `linux/arm64` images (15 minutes each):

- `ghcr.io/oldsj/mainloop-backend:<first seven commit SHA characters>` (the Go server;
the image keeps its historical name because the production manifests pin it)
the image keeps its historical name because deployments pin it)
- `ghcr.io/oldsj/mainloop-frontend:<first seven commit SHA characters>`

The frontend uses the workflow's production API build argument. There is no
`latest` tag or job-level output. Step outputs provide the short SHA and build
digest; summaries record source, tag, digest and an immutable infrastructure pin.
The image jobs are not restricted to `main`, but the `release` job is: it runs only on
`main`, and only while its commit is still the tip of `main`. It force-pushes the
`release` branch and a `release-<sha7>` tag with the digests pinned in the prod overlay.
Argo tracks `release`; no infrastructure PR is needed per release.
The image jobs are not restricted to `main`, but the `chart` job is: it runs only on
`main` and publishes `oci://ghcr.io/oldsj/charts/mainloop` `0.1.<run number>` with both
digests pinned. Deployments track `0.1.*`; no infrastructure PR is needed per release.

## Shared live-development application images

Expand Down
94 changes: 0 additions & 94 deletions .github/workflows/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,97 +140,3 @@ jobs:
printf 'Backend: `%s`\n\n' "$backend"
printf 'Frontend: `%s`\n' "$frontend"
} >> "$GITHUB_STEP_SUMMARY"

# Pins production to this commit's images. The release branch is the main commit plus one commit
# that adds an images block to the prod overlay; Argo tracks it. Pushes made with GITHUB_TOKEN do
# not start workflows, so this cannot loop. A run releases only while its commit is still the tip of
# main, so a cancelled, failed or re-run older run cannot roll production back. Each release is also
# tagged release-<sha7>: the release branch is rewritten every time, and the tag keeps the
# previous release reachable for a rollback.
release:
name: Release to the release branch
needs: publish
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
packages: read
env:
OVERLAY: k8s/apps/mainloop/overlays/prod
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.sha }}

- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Pin the prod overlay to the published digests
id: pin
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_SHA:0:7}"
main_now="$(git ls-remote origin refs/heads/main | cut -f1)"
if [[ "$main_now" != "$GITHUB_SHA" ]]; then
echo "::notice::main is at ${main_now}, not ${GITHUB_SHA}; the run for the newer commit releases"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if grep -q '^images:' "${OVERLAY}/kustomization.yaml"; then
echo "::error::${OVERLAY}/kustomization.yaml already has an images: key; merge the release block by hand"
exit 1
fi
digest_of() {
local d
d="$(docker buildx imagetools inspect "ghcr.io/oldsj/mainloop-$1:${tag}" --format '{{.Manifest.Digest}}')"
[[ "$d" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "no digest for mainloop-$1:${tag}: '${d}'" >&2; exit 1; }
printf '%s' "$d"
}
backend="$(digest_of backend)"
frontend="$(digest_of frontend)"
{
printf '\nimages:\n'
printf ' - name: ghcr.io/oldsj/mainloop-backend\n digest: %s\n' "$backend"
printf ' - name: ghcr.io/oldsj/mainloop-frontend\n digest: %s\n' "$frontend"
} >> "${OVERLAY}/kustomization.yaml"
rendered="$(kubectl kustomize "${OVERLAY}")"
grep -qF "mainloop-backend@${backend}" <<<"$rendered"
grep -qF "mainloop-frontend@${frontend}" <<<"$rendered"
{
printf '### Release %s\n\n' "$tag"
printf 'Backend: `%s`\n\n' "$backend"
printf 'Frontend: `%s`\n' "$frontend"
} >> "$GITHUB_STEP_SUMMARY"

- name: Update the release branch
if: steps.pin.outputs.skip != 'true'
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_SHA:0:7}"
# The previous release's subject is "Release <sha7>"; its tag is release-<sha7>.
prev=""
if [[ -n "$(git ls-remote origin refs/heads/release)" ]]; then
git fetch --quiet --depth=1 origin refs/heads/release
prev="$(git log -1 --format=%s FETCH_HEAD | sed -n 's/^Release \([0-9a-f]\{7\}\)$/\1/p')"
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Release ${tag}" -m "Source: ${GITHUB_SHA}"
git push --atomic --force origin HEAD:refs/heads/release "HEAD:refs/tags/release-${tag}"
{
printf '\nRelease commit: `%s` (tag `release-%s`)\n\n' "$(git rev-parse HEAD)" "$tag"
if [[ -n "$prev" ]]; then
printf 'Roll back to the previous release:\n\n'
printf '```\ngit fetch origin tag release-%s && git push --force origin release-%s:refs/heads/release\n```\n' "$prev" "$prev"
else
printf 'No previous release to roll back to.\n'
fi
} >> "$GITHUB_STEP_SUMMARY"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ secrets.yaml
gcp-credentials.json

# Personal k8s config (for open source - keeps your domains private)
k8s/apps/mainloop/overlays/*/personal-config-patch.yaml

# Logs
*.log
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ When documents differ, do not silently blend future design with current behavior
api/ OpenAPI contract (openapi.yaml) and MCP tool manifest (mcp-tools.json)
server/ Go server: one binary, cmd/mainloop, with a subcommand per process
web/ React app (Vite, TanStack Router, Tailwind); design system in web/design/
k8s/ Kubernetes bases, overlays and kagent integration (apps/mainloop, integrations/kagent)
charts/ Helm chart for Mainloop (charts/mainloop)
k8s/ kagent integration example (integrations/kagent)
docs/specs/ User-visible behavior specifications
scripts/ Development and CI helper scripts
```
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ limitation are in [Pull requests](docs/specs/pull-requests.md). The `get_issue`,

## Production Deployment

The Kubernetes manifests under `k8s/apps/mainloop/` provide reusable bases and example overlays. Supply environment-specific images, domains, credentials, and storage through your deployment configuration, and apply production changes through your GitOps workflow.
The [Helm chart](charts/mainloop) deploys Mainloop. Supply environment-specific domains, credentials, and storage through your own values file, and apply production changes through your GitOps workflow ([details](docs/production-rollout.md)).

The [Publish images workflow](.github/workflows/images.yml) builds and pushes
`linux/arm64` production images on every push to `main`, using native
Expand All @@ -86,15 +86,14 @@ context); it receives `VITE_API_URL=https://mainloop-api.olds.network` at build
Images are published to `ghcr.io/oldsj/mainloop-backend` and
`ghcr.io/oldsj/mainloop-frontend`, tagged with the first seven characters of the
source commit SHA. Each image's job summary records the full source SHA, tag,
digest, and copyable `image@sha256:...` reference. When both image jobs succeed and
the commit is still the tip of `main`, the `release` job force-pushes the `release`
branch (the `main` commit plus one commit pinning both digests in the prod overlay) and
tags it `release-<sha7>`. Once the deployment repository points Argo at `release`, a merge to `main` deploys. Rolling
back is pushing the previous release tag back to `release`
digest, and copyable `image@sha256:...` reference. When both image jobs succeed on `main`,
the `chart` job publishes `oci://ghcr.io/oldsj/charts/mainloop` version `0.1.<run number>`
with both digests pinned, so a deployment tracking `0.1.*` deploys each merge. Rolling back
is pinning the previous chart version
([details](docs/production-rollout.md#production-changes-are-gitops-only)).

The image jobs authenticate with `GITHUB_TOKEN` (`contents: read`,
`packages: write`; the `release` job has `contents: write`) and use separate GitHub Actions caches for each image. Existing
`packages: write`) and use separate GitHub Actions caches for each image. Existing
GHCR packages must grant this repository Actions write access. New pushes cancel
older image builds for the same ref; a cancelled run may have published only one
image, so use a fully successful run for a pin update.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Messages receive stable identities and are persisted before delivery. Retries wi

After a timeout or disconnect, Mainloop reconciles native-session and workspace state before deciding whether to retry. Unknown delivery is not equivalent to failure. Recovery never replays transcript commands automatically.

Production delivery is mechanical: a merge to `main` publishes images, and CI force-pushes a `release` branch carrying their digests, which Argo tracks. Rollback is pushing a previous release commit back to `release`.
Production delivery is mechanical: a merge to `main` publishes images and a chart version pinning their digests, which Argo tracks. Rollback is pinning a previous chart version.

Check results and review verdicts bind to an exact candidate identity. Integration review covers combined-system concerns such as duplicated foundations, conflicting abstractions, contract drift, and inconsistent security or data handling—not only whether each feature works independently.

Expand Down
Loading
Loading