mirror of
https://github.com/hanzoai/cal.git
synced 2026-10-16 20:43:01 +00:00
build: self-contained multi-stage Dockerfile + native CI/CD
- Dockerfile builds the Vite bundle (pinned pnpm) in stage 1, serves it via hanzoai/spa in stage 2 — a fresh docker build no longer needs a pre-built app/dist (gitignored), so CI and local builds match. - .dockerignore keeps the build context lean (deps/bundle rebuilt in-image). - .github/workflows/cicd.yml builds+pushes ghcr.io/hanzoai/cal:sha-<short> on the self-hosted hanzo-build-linux-amd64 pool — zero GitHub-hosted minutes. - deploy/mirror-to-forge.sh mirrors any repo to git.hanzo.ai/hanzo/<name>. - deploy/base-native/cicd.yml: paired base image workflow (prepared, not applied).
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Keep the build context lean and let the image build its own deps/bundle.
|
||||
# node_modules is re-installed in-container (host symlinks/arch would not port);
|
||||
# dist is rebuilt by `pnpm build` in stage 1.
|
||||
**/node_modules
|
||||
**/dist
|
||||
*.tsbuildinfo
|
||||
.git
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
deploy
|
||||
LLM.md
|
||||
@@ -0,0 +1,78 @@
|
||||
name: cicd
|
||||
|
||||
# Canonical home = github.com/hanzoai/cal. This SAME file also runs natively on
|
||||
# the git.hanzo.ai forge, whose runner scans WORKFLOW_DIRS = .hanzo/workflows,
|
||||
# .github/workflows, .gitea/workflows — so no .hanzo/workflows copy is needed:
|
||||
# .github/workflows is both GitHub-discoverable AND executed by the Hanzo runner.
|
||||
#
|
||||
# The invariant that makes this "native, our way" is the COMPUTE, not the folder:
|
||||
# every job pins a self-hosted Hanzo pool via `runs-on` — NEVER a GitHub-hosted
|
||||
# runner (ubuntu-latest, etc.). Zero GitHub Actions minutes are ever consumed.
|
||||
#
|
||||
# The SPA bundle is built INSIDE the multi-stage Dockerfile (stage 1, pinned
|
||||
# pnpm), so this workflow is one build — checkout + buildx + push. The FE has no
|
||||
# private modules (all public npm: @calcom/atoms MIT, react), so no GH_PAT
|
||||
# build-secret is needed here, unlike the Go repos.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
image:
|
||||
# Self-hosted Hanzo pool (arc-system / git.hanzo.ai runner). Same label the
|
||||
# hanzoai/base release job uses. NEVER a GitHub-hosted runner.
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve short SHA
|
||||
id: sha
|
||||
run: echo "short=$(echo "${GITHUB_SHA}" | cut -c1-7)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# GHCR push. On the ARC (GitHub-context) pool the auto GITHUB_TOKEN carries
|
||||
# packages:write. When this file runs on the native git.hanzo.ai gitea
|
||||
# runner, the forge's auto token is scoped to the forge, not ghcr.io — set
|
||||
# a repo/org secret GH_PAT (write:packages) and swap the password below.
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
# PRs build (validate the Dockerfile) but do not push; main/tags push.
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
provenance: false
|
||||
tags: |
|
||||
ghcr.io/hanzoai/cal:sha-${{ steps.sha.outputs.short }}
|
||||
ghcr.io/hanzoai/cal:latest
|
||||
|
||||
- name: Report image + deploy note
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
{
|
||||
echo "### cal FE image"
|
||||
echo "Built + pushed \`ghcr.io/hanzoai/cal:sha-${{ steps.sha.outputs.short }}\` on \`${{ runner.name }}\`."
|
||||
echo ""
|
||||
echo "Deploy is a **gated CTO step**: pin this tag on the \`cal\` App CR;"
|
||||
echo "the operator rolls it out. This workflow never deploys."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
+29
-7
@@ -1,13 +1,35 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# cal.hanzo.ai — Cal's <Booker> (debranded as @hanzo/calendar) as a static SPA,
|
||||
# served by hanzoai/spa (Go static server, :3000). `pnpm build` writes the Vite
|
||||
# bundle to app/dist. The booking (/{host}/{event}) and manage (/booking/{uid})
|
||||
# routes are client-rendered SPA shells; hanzoai/spa falls back to index.html for
|
||||
# any client-routed path so the router resolves them and the atoms fetch on mount.
|
||||
# served by hanzoai/spa (Go static server, :3000). Self-contained multi-stage:
|
||||
# stage 1 compiles the Vite bundle with the pinned pnpm; stage 2 serves it. A
|
||||
# fresh `docker build .` needs no pre-built dist on the host (app/dist is
|
||||
# gitignored), so CI and local builds are identical.
|
||||
|
||||
# --- stage 1: build app/dist ---
|
||||
FROM node:22-slim AS build
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
# Manifests first: the install layer caches independently of source edits.
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY app/package.json app/package.json
|
||||
COPY packages/calendar/package.json packages/calendar/package.json
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
# Sources, then build → app/dist. @hanzo/calendar is workspace TS source that
|
||||
# Vite transpiles; @calcom/atoms is consumed as its pre-built ESM bundle.
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# --- stage 2: serve the bundle with the Hanzo static SPA server ---
|
||||
# hanzoai/spa falls back to index.html for any client-routed path so the SPA
|
||||
# router resolves /{owner}/{slug} and /booking/{uid} on the client.
|
||||
FROM ghcr.io/hanzoai/spa:sha-b9d3a13-amd64
|
||||
COPY app/dist /public
|
||||
COPY --from=build /app/app/dist /public
|
||||
# Ship the fork attribution alongside the served bundle (MIT @calcom/atoms).
|
||||
COPY NOTICE /NOTICE
|
||||
ENV PORT=3000
|
||||
ENV ROOT=/public
|
||||
# Allow same-origin + inline bootstrap (React hydration) and connect-src https: so
|
||||
# the browser can reach the Base booking API at cal.hanzo.ai/v1/calendar.
|
||||
# Allow same-origin + inline bootstrap (React hydration) and connect-src https:
|
||||
# so the browser can reach the Base booking API at cal.hanzo.ai/v1/calendar.
|
||||
ENV HANZO_STATIC_CSP="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-src 'self'; media-src 'self' https:"
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -12,7 +12,21 @@ booking is created in Base.
|
||||
SPA imports ONLY from here, never `@calcom/atoms` directly.
|
||||
- `app` — the Vite + React SPA (`@hanzo/cal-app`). Routes: `/` landing,
|
||||
`/{owner}/{slug}` the Booker, `/booking/{uid}` confirmation + cancel.
|
||||
- `Dockerfile` — `FROM ghcr.io/hanzoai/spa`, `COPY app/dist /public` (SPA fallback).
|
||||
- `Dockerfile` — self-contained multi-stage: stage 1 builds the Vite bundle with
|
||||
the pinned pnpm; stage 2 is `FROM ghcr.io/hanzoai/spa` and copies the bundle to
|
||||
`/public` (SPA fallback). A fresh `docker build .` needs no pre-built `app/dist`
|
||||
(it is gitignored), so CI and local builds are identical.
|
||||
- `.github/workflows/cicd.yml` — native CI. Every job pins the self-hosted Hanzo
|
||||
pool `hanzo-build-linux-amd64` (`runs-on`) — never a GitHub-hosted runner, so
|
||||
zero GHA minutes. On push it builds+pushes `ghcr.io/hanzoai/cal:sha-<short>`.
|
||||
The same `.github` file also runs on the git.hanzo.ai forge, whose runner scans
|
||||
`WORKFLOW_DIRS = .hanzo/workflows,.github/workflows,.gitea/workflows`. Deploy is
|
||||
a gated CTO step (pin the tag on the `cal` App CR); CI never deploys.
|
||||
- `deploy/mirror-to-forge.sh` — point the git.hanzo.ai mirror at the GitHub
|
||||
canonical via server-side mirror-IN (the forge fetches from GitHub; idempotent
|
||||
resync). Auth is the `hanzo auth token` JWT, kept out of argv.
|
||||
- `deploy/base-native/cicd.yml` — the paired base image workflow, prepared for
|
||||
post-red apply to `hanzoai/base/.github/workflows/`.
|
||||
|
||||
## Backend contract
|
||||
`<Provider apiUrl>` sets `axios.defaults.baseURL`, so every atom request hits our
|
||||
@@ -31,4 +45,10 @@ stubs (`app/src/stripe-*.ts`) — the bundle stays self-contained and CSP-clean.
|
||||
## Fork discipline
|
||||
Only MIT `@calcom/atoms` is used; no `ee/` / non-MIT Cal code. Cal's copyright stays
|
||||
in `NOTICE`, Hanzo's added. Phase-1 milestone is a local/workspace `@hanzo/calendar`;
|
||||
the immutable npm publish + `github.com/hanzoai/calendar` repo are the follow-on.
|
||||
the immutable npm publish is the follow-on.
|
||||
|
||||
## Homing
|
||||
Canonical origin is GitHub `github.com/hanzoai/cal`; git.hanzo.ai `hanzo/cal` is a
|
||||
downstream mirror and the native CI executor. (The repo is `cal`, not `calendar`,
|
||||
because `github.com/hanzoai/calendar` is a full fork of `calcom/cal.diy` — a
|
||||
different artifact; the forge org is `hanzo`, it has no `hanzoai` org.)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: cicd
|
||||
|
||||
# PREPARED, NOT APPLIED — copy to hanzoai/base/.github/workflows/cicd.yml
|
||||
# post-red (the red review owns the base working tree right now).
|
||||
#
|
||||
# This is the base half of the calendar stack: a push→image build for the Go
|
||||
# Base binary that carries the /v1/calendar contract, producing
|
||||
# ghcr.io/hanzoai/base:cal-<sha> from base's ./Dockerfile.
|
||||
#
|
||||
# Same invariant as the cal FE: every job pins a self-hosted Hanzo pool
|
||||
# (runs-on) — NEVER a GitHub-hosted runner, so zero GHA minutes. Because the
|
||||
# git.hanzo.ai forge scans WORKFLOW_DIRS = .hanzo/workflows,.github/workflows,
|
||||
# .gitea/workflows, this same .github file also executes natively on the forge.
|
||||
#
|
||||
# base already ships native CI (release.yaml = goreleaser binary;
|
||||
# platform-build.yml = platform-dispatched image). This ADDS an on-push image
|
||||
# build for the cal tag; those keep working via the legacy dirs — no flag day.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, feat/calendar-cal-contract]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
image:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# base Dockerfile runs `git describe --tags` for -X ...Version.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve short SHA
|
||||
id: sha
|
||||
run: echo "short=$(echo "${GITHUB_SHA}" | cut -c1-7)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
provenance: false
|
||||
tags: |
|
||||
ghcr.io/hanzoai/base:cal-${{ steps.sha.outputs.short }}
|
||||
# Private cross-org modules (hanzoai/*, luxfi/*) resolve via authed git
|
||||
# in the builder stage — base's Dockerfile mounts this as secret id
|
||||
# `gh_token`. Same mechanism release.yaml uses; without it the build
|
||||
# fails SECURITY ERROR on force-moved luxfi tags.
|
||||
secrets: |
|
||||
gh_token=${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Report image + deploy note
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
{
|
||||
echo "### base (cal contract) image"
|
||||
echo "Built + pushed \`ghcr.io/hanzoai/base:cal-${{ steps.sha.outputs.short }}\`."
|
||||
echo ""
|
||||
echo "Deploy is a **gated CTO step**: pin this tag on the base App CR"
|
||||
echo "that serves cal.hanzo.ai/v1/calendar. This workflow never deploys."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# mirror-to-forge.sh — mirror a GitHub-canonical repo INTO git.hanzo.ai.
|
||||
#
|
||||
# Canonical origin is GitHub (github.com/hanzoai/<name>). git.hanzo.ai is a
|
||||
# downstream mirror + native CI executor, so it FOLLOWS GitHub: the forge pulls
|
||||
# from the canonical source (a server-side `git fetch +refs/*:refs/*`) rather
|
||||
# than us push-seeding a second write target. Idempotent — the first call clones
|
||||
# the source, a repeat call resyncs it.
|
||||
#
|
||||
# Auth: the live Hanzo IAM JWT (`hanzo auth token`) as a Bearer, written into a
|
||||
# mode-600 curl config at call time — it never appears in argv, the URL, or the
|
||||
# shell history. Requires a logged-in `hanzo` CLI (`hanzo auth whoami` resolves).
|
||||
#
|
||||
# Usage: deploy/mirror-to-forge.sh <name> [github-org]
|
||||
# e.g.: deploy/mirror-to-forge.sh cal # public → anonymous fetch
|
||||
# deploy/mirror-to-forge.sh base # (see private-source note)
|
||||
#
|
||||
# A PRIVATE GitHub source needs the forge's server-side GIT_MIRROR_TOKEN
|
||||
# (allowlisted to github.com); that is forge deploy config, not set here.
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${1:?forge/github repo name required, e.g. cal}"
|
||||
GH_ORG="${2:-hanzoai}"
|
||||
API="https://api.hanzo.ai/v1/git"
|
||||
SRC="https://github.com/${GH_ORG}/${NAME}.git"
|
||||
|
||||
CFG="$(mktemp)"; trap 'rm -f "$CFG"' EXIT; chmod 600 "$CFG"
|
||||
# printf is a shell builtin → the token never appears in any process argv.
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$(hanzo auth token)" > "$CFG"
|
||||
|
||||
# 1) Ensure the forge repo exists under org `hanzo` (idempotent).
|
||||
code="$(curl -sS -o /dev/null -m 30 -w '%{http_code}' -K "$CFG" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST "${API}/repos" -d "{\"name\":\"${NAME}\"}" || true)"
|
||||
case "$code" in
|
||||
201) echo "created forge repo hanzo/${NAME}" ;;
|
||||
409) echo "forge repo hanzo/${NAME} already exists" ;;
|
||||
*) echo "note: create returned HTTP ${code} (continuing)" ;;
|
||||
esac
|
||||
|
||||
# 2) Mirror-IN: the forge force-fetches every ref from the GitHub canonical.
|
||||
echo "mirroring ${SRC} -> git.hanzo.ai/hanzo/${NAME} ..."
|
||||
curl -fsS -m 300 -K "$CFG" -H 'Content-Type: application/json' \
|
||||
-X POST "${API}/repos/${NAME}/mirror" -d "{\"source\":\"${SRC}\"}" \
|
||||
-w '\n<<HTTP %{http_code}>>\n'
|
||||
|
||||
echo "done — clone at https://git.hanzo.ai/hanzo/${NAME}.git"
|
||||
Reference in New Issue
Block a user