mirror of
https://github.com/hanzoai/hanzo.network.git
synced 2026-09-25 02:14:24 +00:00
serve hanzo.network from hanzoai/static, not Pages or CF Pages
.github/workflows now holds sync.yml alone, carrying refs to git.hanzo.ai; every build runs natively there from .hanzo/workflows. .hanzo/workflows/deploy.yml publishes ghcr.io/hanzoai/hanzo-network:<sha> and does not deploy — the operator CR in hanzoai/universe names which tag is live. The GitHub Pages workflow it replaces is dead, not moved: Pages did not survive the hanzoai -> hanzo-apps transfer, so the repo's pages API is 404 and every run since 2026-03-04 ends "Failed to create deployment (status: 404) … Ensure GitHub Pages has been enabled". Relocating a guaranteed 404 to the forge buys nothing. public/CNAME goes with it: that file is a Pages control file, and neither hanzoai/static nor Cloudflare reads it. The live host is a Cloudflare Pages project built through CF's Git integration, outside every workflow file here — nothing in this commit changes what serves today. The Dockerfile ends FROM ghcr.io/hanzoai/static:v0.5.1 with dist at /public and runs it with -spa: 98 BrowserRouter routes plus the app's own path="*" NotFound need the fallback, or every deep link 404s. Measured against the image: -spa returns 200 for /account/billing, and without it a missing path 404s. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
+87
-45
@@ -1,60 +1,102 @@
|
||||
name: Sync to git.hanzo.ai
|
||||
# DIRECTION: this repo on git.hanzo.ai is a PULL MIRROR fetching github.com/hanzoai/hanzo.network.
|
||||
# GitHub is UPSTREAM, git.hanzo.ai is DOWNSTREAM. The giveaway is what this file does:
|
||||
# mirror-sync tells Gitea to PULL, which only makes sense if GitHub is the source.
|
||||
name: sync
|
||||
|
||||
# Carries pushed refs to git.hanzo.ai, the canonical forge, and does nothing
|
||||
# else: build, test and release run natively there.
|
||||
#
|
||||
# This matters beyond labeling: a pull mirror force-updates its refs from upstream on
|
||||
# every fetch, so anything committed directly on git.hanzo.ai is DISCARDED on the next
|
||||
# sync. Do not treat the mirror as writable, and do not reword this into a "native on
|
||||
# git.hanzo.ai" claim — that is the intended end state, not today's.
|
||||
#
|
||||
# What IS true: CI/CD runs on git.hanzo.ai — arcd executes .hanzo/workflows/ on the
|
||||
# Hanzo git-runner. Making git.hanzo.ai canonical means converting the repo
|
||||
# pull-mirror -> native and flipping the write path; an ops migration, not a config edit.
|
||||
#
|
||||
# So this nudge is a LATENCY fix, not a canonicality one: it asks Gitea to pull now
|
||||
# instead of waiting out the ~10-minute mirror interval, so arcd sees the new commit
|
||||
# sooner. Without this file the repo still mirrors — just later.
|
||||
#
|
||||
# IDEMPOTENT: mirror-sync just tells Gitea to pull HEAD, so N nudges == 1 (same latest
|
||||
# state); concurrency coalesces a burst of pushes to a single in-flight sync. FAST: no
|
||||
# checkout, one bounded curl, hard timeouts. FAIL-SOFT: a missing token or hiccup never
|
||||
# fails the push (the App webhook still mirrors). Set HANZO_GIT_TOKEN to enable the nudge.
|
||||
# Repo-agnostic. owner/name come from ${{ github.repository }}, so this file is
|
||||
# dropped into every repo unchanged as .github/workflows/sync.yml and is the
|
||||
# only workflow GitHub is permitted to run.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# '**' matches slashes; '*' does not, and would skip feature/foo.
|
||||
branches: ['**']
|
||||
tags: ['**']
|
||||
workflow_dispatch:
|
||||
|
||||
# Only the newest push's sync needs to run — cancel any older in-flight one (it would
|
||||
# pull a now-stale HEAD). This is what makes rapid pushes idempotent + cheap.
|
||||
# Serialize per ref. Never cancel: a cancelled run drops that ref and nothing
|
||||
# reconciles it afterwards.
|
||||
concurrency:
|
||||
group: sync-git-hanzo-ai
|
||||
cancel-in-progress: true
|
||||
group: sync-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
# `.github/workflows` is also on Gitea's WORKFLOW_DIRS, so with Actions enabled on
|
||||
# the mirror this file runs THERE too on every sync — where asking git.hanzo.ai to
|
||||
# pull is circular. github.server_url is setting.AppURL on Gitea
|
||||
# (services/actions/context.go:89), so this is the exact test for "am I upstream".
|
||||
if: ${{ github.server_url == 'https://github.com' }}
|
||||
# .github/workflows is also on the forge's own workflow search path, so this
|
||||
# file executes there too in any repo without .hanzo/workflows, where it
|
||||
# would push the forge to itself. server_url is the forge's AppURL, which
|
||||
# makes this the test for "running on GitHub".
|
||||
if: github.server_url == 'https://github.com'
|
||||
# GitHub-hosted on purpose. This is the one workflow that must run ON GitHub,
|
||||
# and the self-hosted pool that used to serve it is retired — a job asking for
|
||||
# it now queues forever against a label nothing answers. Carrying refs is a
|
||||
# push, not a build, so it does not belong on the build fabric anyway: builds
|
||||
# run natively on git.hanzo.ai, which is the whole point of this file.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Nudge git.hanzo.ai to pull HEAD (mirror-sync)
|
||||
continue-on-error: true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Push refs to git.hanzo.ai
|
||||
env:
|
||||
HANZO_GIT_TOKEN: ${{ secrets.HANZO_GIT_TOKEN }}
|
||||
FORGE: https://git.hanzo.ai
|
||||
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
run: |
|
||||
set -u
|
||||
if [ -z "${HANZO_GIT_TOKEN:-}" ]; then
|
||||
echo "HANZO_GIT_TOKEN not set — relying on the mirror App webhook. Skipping nudge."
|
||||
exit 0
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${GIT_TOKEN}" ]; then
|
||||
echo "::error::GIT_TOKEN is unset, so this push never reached the canonical forge. Add the org secret."
|
||||
exit 1
|
||||
fi
|
||||
if curl -fsS --max-time 20 --retry 2 --retry-delay 2 -X POST \
|
||||
-H "Authorization: token ${HANZO_GIT_TOKEN}" \
|
||||
"https://git.hanzo.ai/api/v1/repos/hanzoai/hanzo.network/mirror-sync"; then
|
||||
echo "git.hanzo.ai mirror-sync triggered"
|
||||
else
|
||||
echo "mirror-sync nudge failed (non-fatal) — the App webhook still mirrors"
|
||||
|
||||
REMOTE="${FORGE}/${GITHUB_REPOSITORY}.git"
|
||||
|
||||
# The forge reads the token from the basic-auth password and ignores
|
||||
# the username. base64 of a secret is not masked automatically.
|
||||
AUTH="$(printf 'x-access-token:%s' "${GIT_TOKEN}" | base64 -w0)"
|
||||
echo "::add-mask::${AUTH}"
|
||||
# Host-scoped, so the forge token is never sent to github.com.
|
||||
git config --local "http.${FORGE}/.extraheader" "Authorization: Basic ${AUTH}"
|
||||
|
||||
# Reachability is the only transient failure here, so it is the only
|
||||
# thing retried. ls-remote exits 0 on an existing empty repo and
|
||||
# non-zero when the forge is down or the repo is absent.
|
||||
ERR="$(mktemp)"
|
||||
REACHED=""
|
||||
for attempt in 1 2 3; do
|
||||
if git ls-remote "${REMOTE}" >/dev/null 2>"${ERR}"; then
|
||||
REACHED=1
|
||||
break
|
||||
fi
|
||||
echo "forge probe failed (attempt ${attempt}/3): $(cat "${ERR}")"
|
||||
if [ "${attempt}" -lt 3 ]; then
|
||||
sleep $((attempt * 5))
|
||||
fi
|
||||
done
|
||||
if [ -z "${REACHED}" ]; then
|
||||
echo "::error::git.hanzo.ai is unreachable or ${GITHUB_REPOSITORY} does not exist there. $(cat "${ERR}")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Additive and fast-forward-only: no leading '+', no --force, no
|
||||
# --prune, no --mirror. Tags go every run so a tag that missed its own
|
||||
# event still lands; git refuses to move one that already differs.
|
||||
REFS=('refs/tags/*:refs/tags/*')
|
||||
case "${GITHUB_REF}" in
|
||||
refs/heads/*) REFS+=("${GITHUB_REF}:${GITHUB_REF}") ;;
|
||||
esac
|
||||
|
||||
# Not --atomic: a diverged ref must not hold back the refs that are
|
||||
# clean. Rejections still fail the job.
|
||||
if ! git push "${REMOTE}" "${REFS[@]}"; then
|
||||
echo "::error::git.hanzo.ai rejected a ref for ${GITHUB_REPOSITORY}. The forge is canonical and is never force-updated from here; reconcile the diverged branch or tag by hand."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: deploy
|
||||
|
||||
# Builds hanzo.network and publishes the image the operator runs. This replaces
|
||||
# the GitHub Pages workflow that used to live in .github/workflows: the site is
|
||||
# served by hanzoai/static behind hanzoai/ingress, so there is no Pages and no
|
||||
# Cloudflare Pages in the path.
|
||||
#
|
||||
# git.hanzo.ai runs this file natively. The image tag is the run's commit, and
|
||||
# the operator CR in hanzoai/universe names which tag is live — a build never
|
||||
# deploys itself.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: build and push
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${REGISTRY_TOKEN:-}" ]; then
|
||||
echo "::error::REGISTRY_TOKEN is unset, so nothing was published. Add the org secret."
|
||||
exit 1
|
||||
fi
|
||||
tag="$(git rev-parse --short HEAD)"
|
||||
image="ghcr.io/hanzoai/hanzo-network:${tag}"
|
||||
echo "${REGISTRY_TOKEN}" | docker login ghcr.io -u hanzo-dev --password-stdin
|
||||
docker build -t "${image}" .
|
||||
docker push "${image}"
|
||||
echo "published ${image} — set spec.image.tag in universe crs/hanzo-network.yaml to ${tag}"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# hanzo.network — a Vite SPA served by hanzoai/static, the house static server (a
|
||||
# Go binary on scratch). No GitHub Pages, no Cloudflare Pages: the site is an
|
||||
# image the operator runs behind hanzoai/ingress like every other Hanzo surface.
|
||||
|
||||
FROM node:22 AS build
|
||||
WORKDIR /src
|
||||
RUN npm i -g pnpm@9
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# hanzoai/static defaults to -port 3000 -root /public. -spa is required: this is a
|
||||
# 98-route BrowserRouter with its own `path="*"` NotFound, so an unknown path must
|
||||
# reach index.html for the router to resolve it. Without it every deep link 404s.
|
||||
FROM ghcr.io/hanzoai/static:v0.5.1
|
||||
COPY --from=build /src/dist /public
|
||||
ENTRYPOINT ["/static", "-spa"]
|
||||
@@ -46,8 +46,51 @@ The homepage highlights:
|
||||
- Provider marketplace (contribute idle compute, set prices)
|
||||
- Stats: 100K+ GPU hours, 50+ regions, 99.9% SLA, $0.10/GPU-hour
|
||||
|
||||
## Serving chain
|
||||
|
||||
```
|
||||
push to main
|
||||
-> .github/workflows/sync.yml carries refs to git.hanzo.ai (nothing else)
|
||||
-> .hanzo/workflows/deploy.yml runs natively on the forge
|
||||
docker build . Dockerfile: pnpm build
|
||||
-> FROM ghcr.io/hanzoai/static:v0.5.1
|
||||
docker push ghcr.io/hanzoai/hanzo-network:<short-sha>
|
||||
-> hanzoai/universe infra/k8s/operator/crs/hanzo-network.yaml
|
||||
spec.image.tag: <short-sha> set by a human, never by the build
|
||||
-> hanzoai/ingress hanzo.network -> Service hanzo-network:80 -> :3000
|
||||
```
|
||||
|
||||
`hanzoai/static` is a Go binary on scratch, run with `-spa` because this is a
|
||||
98-route `BrowserRouter`: without the fallback every deep link 404s instead of
|
||||
reaching `index.html` for the router (and for the app's own `path="*"`
|
||||
NotFound). `HANZO_STATIC_CSP` in the CR allows `fonts.googleapis.com` /
|
||||
`fonts.gstatic.com` for the Inter stylesheet `index.html` links, and
|
||||
`api.hanzo.ai` in `connect-src` for the pricing fetch and the chat widgets.
|
||||
|
||||
## Not live from here yet
|
||||
|
||||
hanzo.network is served today by the Cloudflare Pages project `hanzo-network`
|
||||
(`hanzo-network.pages.dev` is byte-identical to the live host), built through CF's
|
||||
Git integration — outside every workflow file in this repo. That is the deploy
|
||||
path this migration retires.
|
||||
|
||||
Reconcile BEFORE anyone pins a tag in the CR: **the live bytes are not built from
|
||||
`main`.** The served `index.html` links `/favicon.svg`, `/favicon.ico` and
|
||||
`/logo.svg`; `main`'s still links `img/28d53ec4-….png`. Those exact
|
||||
lines come from `origin/fix/broken-links` (`0efe650`, the real @hanzo/brand
|
||||
blocky-H). Building an image from `main` as it stands would regress branding, so
|
||||
land that branch first. CF Pages is not rebuilding `main` on push — `main` has
|
||||
been pushed repeatedly since 2026-06-23 and the live bytes never changed — which
|
||||
is also why pushing this migration cannot disturb the live site.
|
||||
|
||||
Promotion order: publish an image -> set `spec.image.tag` in
|
||||
`crs/hanzo-network.yaml` -> add `- hanzo-network.yaml` to
|
||||
`crs/kustomization.yaml` -> confirm the pod is Running and a deep link returns
|
||||
200 -> only then repoint hanzo.network DNS off CF Pages. The CR is committed
|
||||
INERT (empty tag, absent from `kustomization.yaml`); promoting an App with no
|
||||
image tag takes the host down instead of leaving it alone.
|
||||
|
||||
## Notes
|
||||
|
||||
- Shares the same component library and routes as hanzo.app, hanzo.id, hanzo.one, and sensei.group. Only `NetworkLanding.tsx` and `index.html` metadata are unique.
|
||||
- No Cloudflare Pages middleware (unlike hanzo.id). Standard static SPA deployment.
|
||||
- Brand color is cyan (#06b6d4) in the dev console message, vs red for other Hanzo sites.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
hanzo.network
|
||||
Reference in New Issue
Block a user