Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Using hush from an agent needed a clone and docs/MCP.md. It now needs one
command, and the instructions are served by the deployment itself.
`go install github.com/orchard9/hush/cmd/hush-mcp@latest` is the whole
install: cmd/hush-mcp imports only the standard library, so module graph
pruning never reaches the private go-chassis dependency cmd/hushd needs.
Verified against an empty module cache and the public proxy, then create ->
reveal end to end against production with the resulting binary.
The page carries the per-client configuration for Claude Code, Codex CLI,
Gemini CLI, VS Code, Claude Desktop, Cursor and omp. Each command was run
against the installed client rather than copied from documentation, which is
how the differences on it are there at all: VS Code's wrapper key is
`servers`, not `mcpServers`; gemini defaults to project scope, not user;
Claude Code rejects `--env` immediately before the server name.
The shared browser crypto moves from base.html into templates/crypto.html,
which the two pages that encrypt parse and this one does not. An empty
`{{define}}` cannot replace a non-empty one — text/template reads an empty
body as no definition — so the shell holds the call and the partial holds the
code, and the docs page ships no script at all.
Three things this exposed, fixed here:
- The public Ingress enumerates paths, so a handler without one 404s at the
edge while working in `make dev`. The Ingress is now its own manifest:
hush.yaml pins a `:bootstrap` image that does not exist, so re-applying it
to publish a path would roll the workload onto an unpullable image.
`make deploy-ingress` applies the route alone.
- release.sh guarded HEAD against `@{upstream}`, which is the GitHub mirror
here, while Kaniko clones Gitea. A commit pushed to one and not the other
would have built the previous commit silently. It now fetches and compares
the branch that actually gets built.
- smoke.sh checks that /mcp serves the install command, so a stale rollout or
an unexecutable template fails the release instead of being found later.
Confirmed it fails: against production before this deploy it reported 404.
111 lines
4.5 KiB
Bash
Executable File
111 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build the current commit in-cluster and roll it out. No CI credential needed.
|
|
#
|
|
# Woodpecker IS activated for this repo, so a push to main builds and deploys.
|
|
# This is the path for when you do not want to wait for CI, when CI is down, or
|
|
# when you are rolling back — and it is how the first deploy happened, before
|
|
# activation. It does exactly what the pipeline's build and deploy steps do: a
|
|
# Kaniko Job for an amd64 image from the pushed git ref, then
|
|
# `kubectl set image`, then a real end-to-end check.
|
|
#
|
|
# Credentials: none. The Gitea repo is public so the Kaniko git context needs no
|
|
# token, and the rollout uses your kubeconfig.
|
|
#
|
|
# ./scripts/release.sh
|
|
set -euo pipefail
|
|
|
|
export KUBECONFIG="${KUBECONFIG:-$HOME/.kube/orchard9-k3sf.yaml}"
|
|
NS="${NS:-projects}"
|
|
HOST="${HOST:-hush.threesix.ai}"
|
|
# The Gitea repo Kaniko clones, and the remote that points at it. Both are
|
|
# named once: the guard below has to check the ref that gets BUILT, and a
|
|
# guard that checks a different remote is worse than no guard.
|
|
GIT_CONTEXT="${GIT_CONTEXT:-git://git.threesix.ai/jordan/hush.git#refs/heads/main}"
|
|
GIT_REMOTE="${GIT_REMOTE:-origin}"
|
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
# Kaniko builds from the GIT CONTEXT, not from this working tree. So a dirty or
|
|
# unpushed tree would silently build something other than what you are looking
|
|
# at — the single most confusing failure this script can have. Refuse instead.
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
echo "refusing: working tree is dirty. Kaniko builds from the pushed git ref," >&2
|
|
echo "so uncommitted changes would NOT be in the image." >&2
|
|
git status --short >&2
|
|
exit 1
|
|
fi
|
|
# `@{upstream}` is NOT the right comparison: this checkout tracks a mirror, so
|
|
# HEAD can be pushed there while Gitea — the repo Kaniko clones — is behind,
|
|
# and the build would silently produce the previous commit. Compare against the
|
|
# branch that actually gets built.
|
|
git fetch --quiet "$GIT_REMOTE" "$GIT_BRANCH"
|
|
if [ "$(git rev-parse HEAD)" != "$(git rev-parse FETCH_HEAD)" ]; then
|
|
echo "refusing: HEAD is not what $GIT_REMOTE/$GIT_BRANCH points at, and Kaniko clones from there." >&2
|
|
echo " HEAD $(git rev-parse --short=8 HEAD) $(git log -1 --format=%s HEAD)" >&2
|
|
echo " $GIT_REMOTE/$GIT_BRANCH $(git rev-parse --short=8 FETCH_HEAD) $(git log -1 --format=%s FETCH_HEAD)" >&2
|
|
echo "Push to $GIT_REMOTE first: git push $GIT_REMOTE $GIT_BRANCH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
SHA="$(git rev-parse --short=8 HEAD)"
|
|
IMAGE="registry.threesix.ai/hush/api:$SHA"
|
|
JOB="hush-build-$SHA"
|
|
echo "releasing $SHA"
|
|
|
|
# A previous attempt at the same SHA leaves a completed Job that cannot be
|
|
# re-created; replacing it is the idempotent thing to do.
|
|
kubectl -n "$NS" delete job "$JOB" --ignore-not-found >/dev/null
|
|
|
|
kubectl -n "$NS" apply -f - >/dev/null <<EOF
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: $JOB
|
|
labels: { app: hush, component: build }
|
|
spec:
|
|
backoffLimit: 1
|
|
ttlSecondsAfterFinished: 3600
|
|
template:
|
|
metadata:
|
|
labels: { app: hush, component: build }
|
|
spec:
|
|
restartPolicy: Never
|
|
containers:
|
|
- name: kaniko
|
|
image: gcr.io/kaniko-project/executor:v1.23.2
|
|
args:
|
|
# The Gitea repo is public, so the git context needs no credential.
|
|
- --context=$GIT_CONTEXT
|
|
- --dockerfile=Dockerfile
|
|
- --destination=$IMAGE
|
|
# The internal Zot registry serves a self-signed cert.
|
|
- --skip-tls-verify
|
|
- --skip-tls-verify-pull
|
|
- --single-snapshot
|
|
resources:
|
|
requests: { cpu: 500m, memory: 1Gi }
|
|
limits: { cpu: "2", memory: 3Gi }
|
|
EOF
|
|
|
|
echo " building (amd64, in-cluster)…"
|
|
if ! kubectl -n "$NS" wait --for=condition=complete "job/$JOB" --timeout=900s >/dev/null 2>&1; then
|
|
echo "build FAILED — last lines:" >&2
|
|
kubectl -n "$NS" logs "job/$JOB" --tail=30 >&2
|
|
exit 1
|
|
fi
|
|
echo " built $IMAGE"
|
|
|
|
kubectl -n "$NS" set image deployment/hush "hushd=$IMAGE" >/dev/null
|
|
kubectl -n "$NS" rollout status deployment/hush --timeout=180s | sed 's/^/ /'
|
|
|
|
# Prove the rolled pod is the image we just built. `set image` matching nothing
|
|
# is silent, and the rollout would "succeed" on the old pod.
|
|
LIVE="$(kubectl -n "$NS" get deployment hush -o jsonpath='{.spec.template.spec.containers[0].image}')"
|
|
[ "$LIVE" = "$IMAGE" ] || { echo "live image is $LIVE, expected $IMAGE" >&2; exit 1; }
|
|
echo " live image: $LIVE"
|
|
|
|
echo
|
|
echo "verifying end to end against https://$HOST"
|
|
BASE="https://$HOST" "$ROOT/scripts/smoke.sh"
|