Harden inter-hub production deploy trigger
This commit is contained in:
50
.gitea/workflows/inter-hub-production-deploy.yaml
Normal file
50
.gitea/workflows/inter-hub-production-deploy.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Inter-Hub production deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
image_tag:
|
||||||
|
description: "Immutable inter-hub image tag to deploy, for example 5101eb5"
|
||||||
|
required: true
|
||||||
|
confirm:
|
||||||
|
description: "Type deploy-inter-hub-production to confirm Railiance01 production deploy"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: [self-hosted, haskelseed, linux_amd64]
|
||||||
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
INTER_HUB_IMAGE_TAG: ${{ inputs.image_tag }}
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate dispatch inputs
|
||||||
|
env:
|
||||||
|
CONFIRM: ${{ inputs.confirm }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "$CONFIRM" != "deploy-inter-hub-production" ]; then
|
||||||
|
echo "Refusing deploy: confirmation text did not match." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! [[ "$INTER_HUB_IMAGE_TAG" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
echo "Refusing deploy: image_tag contains unsupported characters." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Verify image manifest exists
|
||||||
|
run: make check-inter-hub-image
|
||||||
|
|
||||||
|
- name: Helm server dry-run
|
||||||
|
run: make inter-hub-server-dry-run
|
||||||
|
|
||||||
|
- name: Deploy Inter-Hub
|
||||||
|
run: make inter-hub-deploy
|
||||||
|
|
||||||
|
- name: Show release status
|
||||||
|
run: make inter-hub-status
|
||||||
|
|
||||||
|
- name: Run public smoke checks
|
||||||
|
run: make inter-hub-smoke
|
||||||
36
Makefile
36
Makefile
@@ -18,6 +18,7 @@ INTER_HUB_RELEASE ?= inter-hub
|
|||||||
INTER_HUB_NAMESPACE ?= inter-hub
|
INTER_HUB_NAMESPACE ?= inter-hub
|
||||||
INTER_HUB_CHART ?= charts/inter-hub
|
INTER_HUB_CHART ?= charts/inter-hub
|
||||||
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
|
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
|
||||||
|
INTER_HUB_IMAGE_REPOSITORY ?= gitea.coulomb.social/coulomb/inter-hub
|
||||||
INTER_HUB_IMAGE_TAG ?=
|
INTER_HUB_IMAGE_TAG ?=
|
||||||
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
|
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ REUSE_CERTIFICATE ?= reuse-surface-tls
|
|||||||
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-hosteurope
|
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-hosteurope
|
||||||
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||||
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||||
|
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
|
||||||
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
|
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
|
||||||
|
|
||||||
SOPS_SENTINEL ?=
|
SOPS_SENTINEL ?=
|
||||||
@@ -71,6 +73,9 @@ check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for produc
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
|
||||||
|
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
|
||||||
|
|
||||||
##@ Vergabe Teilnahme
|
##@ Vergabe Teilnahme
|
||||||
|
|
||||||
vergabe-dry-run: ## helm template render (no apply) for inspection
|
vergabe-dry-run: ## helm template render (no apply) for inspection
|
||||||
@@ -113,12 +118,22 @@ vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
|
|||||||
|
|
||||||
##@ Inter-Hub
|
##@ Inter-Hub
|
||||||
|
|
||||||
inter-hub-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for inter-hub
|
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
|
||||||
|
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||||
|
--namespace $(INTER_HUB_NAMESPACE) \
|
||||||
|
-f $(INTER_HUB_VALUES)
|
||||||
|
|
||||||
|
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
|
||||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||||
--namespace $(INTER_HUB_NAMESPACE) \
|
--namespace $(INTER_HUB_NAMESPACE) \
|
||||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
|
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
|
||||||
|
|
||||||
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image-tag ## Deploy / upgrade inter-hub Helm release on Railiance01
|
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
|
||||||
|
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||||
|
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||||
|
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||||
|
|
||||||
|
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
|
||||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||||
@@ -133,20 +148,7 @@ inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm hist
|
|||||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
|
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
|
||||||
|
|
||||||
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
|
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
|
||||||
@status="$$(curl -sS -o /tmp/inter-hub-api-v2-hubs.body -w "%{http_code}" "$(INTER_HUB_BASE_URL)/api/v2/hubs")"; \
|
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
|
||||||
if [ "$$status" != "401" ]; then \
|
|
||||||
echo "expected $(INTER_HUB_BASE_URL)/api/v2/hubs to return 401, got $$status" >&2; \
|
|
||||||
cat /tmp/inter-hub-api-v2-hubs.body >&2; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
echo "ok: /api/v2/hubs returned 401"
|
|
||||||
@tmp="$$(mktemp)"; \
|
|
||||||
trap 'rm -f "$$tmp"' EXIT; \
|
|
||||||
curl -fsS "$(INTER_HUB_BASE_URL)/openapi.json" > "$$tmp"; \
|
|
||||||
for route in /hubs /hub-capability-manifests /api-consumers /policy-scopes; do \
|
|
||||||
grep -q "$$route" "$$tmp" || { echo "missing OpenAPI route: $$route" >&2; exit 1; }; \
|
|
||||||
done; \
|
|
||||||
echo "ok: OpenAPI lists expected v2 resources"
|
|
||||||
|
|
||||||
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
|
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
|
||||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
|
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
|
||||||
@@ -181,4 +183,4 @@ help: ## Show this help
|
|||||||
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
||||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status check-railiance01-kubeconfig check-inter-hub-image-tag vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret inter-hub-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs help
|
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs help
|
||||||
|
|||||||
@@ -35,10 +35,15 @@ Read-only checks on 2026-06-15 showed:
|
|||||||
target.
|
target.
|
||||||
- The pod is Running and `certificate/inter-hub-tls` is Ready on Railiance01.
|
- The pod is Running and `certificate/inter-hub-tls` is Ready on Railiance01.
|
||||||
- `GET https://hub.coulomb.social/api/v2/hubs` returned `200` unauthenticated,
|
- `GET https://hub.coulomb.social/api/v2/hubs` returned `200` unauthenticated,
|
||||||
proving the route is present but not satisfying the expected `401` auth gate.
|
matching the public discovery contract.
|
||||||
- `/openapi.json`, `/api/openapi.json`, `/swagger.json`, and
|
- `GET https://hub.coulomb.social/api/v2/widgets` and
|
||||||
`/api/swagger.json` returned `404`; the public OpenAPI route still needs to
|
`GET https://hub.coulomb.social/api/v2/hub-registry` returned `401`
|
||||||
be confirmed by the upstream inter-hub owner.
|
unauthenticated with `invalid_api_key`.
|
||||||
|
- OpenAPI is published at `/api/v2/openapi.json`; `/openapi.json` is not the
|
||||||
|
Inter-Hub v2 OpenAPI route.
|
||||||
|
- `gitea.coulomb.social/coulomb/inter-hub:5101eb5` returned
|
||||||
|
`manifest unknown` on 2026-06-15, so production deploy must wait for the
|
||||||
|
image publication gate to pass.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -46,23 +51,54 @@ Use the Railiance01 kubeconfig. The Makefile defaults to
|
|||||||
`~/.kube/config-hosteurope` and fails fast when it is missing.
|
`~/.kube/config-hosteurope` and fails fast when it is missing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make inter-hub-dry-run
|
make inter-hub-render-baseline
|
||||||
INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy
|
INTER_HUB_IMAGE_TAG=5101eb5 make check-inter-hub-image
|
||||||
|
INTER_HUB_IMAGE_TAG=5101eb5 make inter-hub-dry-run
|
||||||
|
INTER_HUB_IMAGE_TAG=5101eb5 make inter-hub-server-dry-run
|
||||||
|
INTER_HUB_IMAGE_TAG=5101eb5 make inter-hub-deploy
|
||||||
make inter-hub-status
|
make inter-hub-status
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy automation should pass the image tag without rewriting
|
Deploy automation should pass the image tag without rewriting
|
||||||
`helm/inter-hub-values.yaml`. The production deploy target requires this
|
`helm/inter-hub-values.yaml`. The production deploy target requires this
|
||||||
explicit tag:
|
explicit tag and refuses to continue if the registry manifest does not exist:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy
|
INTER_HUB_IMAGE_TAG=5101eb5 make inter-hub-deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
Use a tag at or after the commit that contains the required v2 bootstrap API
|
Use a tag at or after the commit that contains the required v2 bootstrap API
|
||||||
routes. The upstream inter-hub workplan names `91037a4` as the first expected
|
routes and any required follow-up fix. The upstream inter-hub workplan names
|
||||||
tag for the IHUB-WP-0019 route surface; verify the actual registry tag before
|
`5101eb5` for the API count-decoding fix needed by the current ops-hub
|
||||||
announcing rollout.
|
bootstrap path; verify the actual registry tag before announcing rollout.
|
||||||
|
|
||||||
|
`make inter-hub-render-baseline` exists only for chart validation with the
|
||||||
|
checked-in values file. Production-facing dry-runs and deploys must use an
|
||||||
|
explicit `INTER_HUB_IMAGE_TAG`.
|
||||||
|
|
||||||
|
## Workflow Dispatch
|
||||||
|
|
||||||
|
`.gitea/workflows/inter-hub-production-deploy.yaml` provides a manual
|
||||||
|
production trigger. It requires:
|
||||||
|
|
||||||
|
- `image_tag`, the immutable inter-hub image tag to deploy;
|
||||||
|
- `confirm`, with the exact value `deploy-inter-hub-production`;
|
||||||
|
- an approved self-hosted runner with Railiance01 kubeconfig access at
|
||||||
|
`~/.kube/config-hosteurope` or an equivalent `RAILIANCE01_KUBECONFIG`
|
||||||
|
override;
|
||||||
|
- registry access through `skopeo`, `crane`, `docker manifest inspect`, or the
|
||||||
|
registry HTTP API.
|
||||||
|
|
||||||
|
The workflow runs the same local targets an attended operator would run:
|
||||||
|
|
||||||
|
1. `make check-inter-hub-image`
|
||||||
|
2. `make inter-hub-server-dry-run`
|
||||||
|
3. `make inter-hub-deploy`
|
||||||
|
4. `make inter-hub-status`
|
||||||
|
5. `make inter-hub-smoke`
|
||||||
|
|
||||||
|
If the image manifest is missing, stop and hand back to the inter-hub/forge
|
||||||
|
image publication path. Do not run Helm against a tag that fails preflight.
|
||||||
|
|
||||||
## Release verification
|
## Release verification
|
||||||
|
|
||||||
@@ -97,7 +133,8 @@ or another committed migration mechanism.
|
|||||||
|
|
||||||
## Smoke checks
|
## Smoke checks
|
||||||
|
|
||||||
After rollout, the unauthenticated v2 API gate should be present:
|
After rollout, the current v2 public-read/authenticated-write contract should
|
||||||
|
be present:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make inter-hub-smoke
|
make inter-hub-smoke
|
||||||
@@ -105,6 +142,11 @@ make inter-hub-smoke
|
|||||||
|
|
||||||
Expected results:
|
Expected results:
|
||||||
|
|
||||||
- `GET https://hub.coulomb.social/api/v2/hubs` returns `401`, not `404`.
|
- `GET https://hub.coulomb.social/api/v2/hubs` returns `200` public discovery
|
||||||
- OpenAPI lists `/hubs`, `/hub-capability-manifests`, `/api-consumers`, and
|
JSON.
|
||||||
`/policy-scopes`.
|
- `GET https://hub.coulomb.social/api/v2/widgets` returns `401` without a key.
|
||||||
|
- `GET https://hub.coulomb.social/api/v2/hub-registry` returns `401` without a
|
||||||
|
key.
|
||||||
|
- `GET https://hub.coulomb.social/api/v2/openapi.json` returns OpenAPI JSON
|
||||||
|
listing `/hubs`, `/hub-capability-manifests`, `/api-consumers`,
|
||||||
|
`/policy-scopes`, `/widgets`, and `/hub-registry`.
|
||||||
|
|||||||
80
tools/check-oci-image.sh
Executable file
80
tools/check-oci-image.sh
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
image_ref="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$image_ref" ]]; then
|
||||||
|
echo "usage: $0 <registry/repository:tag>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
failures=()
|
||||||
|
|
||||||
|
try_tool() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if ! command -v "$name" >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
if output="$("$@" 2>&1 >/dev/null)"; then
|
||||||
|
echo "ok: found image manifest with $name: $image_ref"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
failures+=("$name: $output")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
try_registry_api() {
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ref_no_digest="${image_ref%@*}"
|
||||||
|
local ref_without_tag tag registry repo url output
|
||||||
|
|
||||||
|
if [[ "$ref_no_digest" != *:* ]]; then
|
||||||
|
failures+=("registry-api: image ref must include an explicit tag")
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tag="${ref_no_digest##*:}"
|
||||||
|
ref_without_tag="${ref_no_digest%:*}"
|
||||||
|
registry="${ref_without_tag%%/*}"
|
||||||
|
repo="${ref_without_tag#*/}"
|
||||||
|
|
||||||
|
if [[ -z "$registry" || -z "$repo" || "$registry" == "$repo" ]]; then
|
||||||
|
failures+=("registry-api: image ref must include registry and repository")
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
url="https://${registry}/v2/${repo}/manifests/${tag}"
|
||||||
|
if output="$(curl -fsSL \
|
||||||
|
-H "Accept: application/vnd.oci.image.index.v1+json" \
|
||||||
|
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
|
||||||
|
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
||||||
|
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||||
|
-o /dev/null "$url" 2>&1)"; then
|
||||||
|
echo "ok: found image manifest with registry API: $image_ref"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
failures+=("registry-api: $output")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
try_tool skopeo skopeo inspect --raw "docker://${image_ref}" || true
|
||||||
|
try_tool crane crane manifest "$image_ref" || true
|
||||||
|
try_tool docker docker manifest inspect "$image_ref" || true
|
||||||
|
try_registry_api || true
|
||||||
|
|
||||||
|
echo "ERROR: image manifest not found or not accessible: $image_ref" >&2
|
||||||
|
if ((${#failures[@]} > 0)); then
|
||||||
|
printf '%s\n' "${failures[@]}" >&2
|
||||||
|
else
|
||||||
|
echo "No supported manifest inspection tool was available." >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
87
tools/inter-hub-smoke.sh
Executable file
87
tools/inter-hub-smoke.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
base_url="${INTER_HUB_BASE_URL:-https://hub.coulomb.social}"
|
||||||
|
base_url="${base_url%/}"
|
||||||
|
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
request() {
|
||||||
|
local name="$1"
|
||||||
|
local url="$2"
|
||||||
|
local expected_status="$3"
|
||||||
|
local body="$tmpdir/${name}.json"
|
||||||
|
local status
|
||||||
|
|
||||||
|
status="$(curl -sS -o "$body" -w "%{http_code}" "$url")"
|
||||||
|
if [[ "$status" != "$expected_status" ]]; then
|
||||||
|
echo "ERROR: expected $url to return $expected_status, got $status" >&2
|
||||||
|
cat "$body" >&2
|
||||||
|
echo >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
hubs_body="$(request hubs "${base_url}/api/v2/hubs" 200)"
|
||||||
|
python3 - "$hubs_body" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as fh:
|
||||||
|
payload = json.load(fh)
|
||||||
|
|
||||||
|
if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
|
||||||
|
raise SystemExit("/api/v2/hubs did not return a paginated data list")
|
||||||
|
|
||||||
|
print("ok: /api/v2/hubs returned public discovery JSON")
|
||||||
|
PY
|
||||||
|
|
||||||
|
widgets_body="$(request widgets "${base_url}/api/v2/widgets" 401)"
|
||||||
|
hub_registry_body="$(request hub-registry "${base_url}/api/v2/hub-registry" 401)"
|
||||||
|
|
||||||
|
python3 - "$widgets_body" "$hub_registry_body" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
for path, filename in (
|
||||||
|
("/api/v2/widgets", sys.argv[1]),
|
||||||
|
("/api/v2/hub-registry", sys.argv[2]),
|
||||||
|
):
|
||||||
|
with open(filename, encoding="utf-8") as fh:
|
||||||
|
payload = json.load(fh)
|
||||||
|
code = payload.get("code") if isinstance(payload, dict) else None
|
||||||
|
if code != "invalid_api_key":
|
||||||
|
raise SystemExit(f"{path} returned 401 but not invalid_api_key JSON")
|
||||||
|
print(f"ok: {path} requires an API key")
|
||||||
|
PY
|
||||||
|
|
||||||
|
openapi_body="$(request openapi "${base_url}/api/v2/openapi.json" 200)"
|
||||||
|
python3 - "$openapi_body" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
required_paths = {
|
||||||
|
"/hubs",
|
||||||
|
"/hub-capability-manifests",
|
||||||
|
"/api-consumers",
|
||||||
|
"/policy-scopes",
|
||||||
|
"/widgets",
|
||||||
|
"/hub-registry",
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as fh:
|
||||||
|
payload = json.load(fh)
|
||||||
|
|
||||||
|
paths = payload.get("paths")
|
||||||
|
if not isinstance(paths, dict):
|
||||||
|
raise SystemExit("/api/v2/openapi.json did not include an OpenAPI paths object")
|
||||||
|
|
||||||
|
missing = sorted(required_paths - set(paths))
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("OpenAPI missing paths: " + ", ".join(missing))
|
||||||
|
|
||||||
|
print("ok: /api/v2/openapi.json lists expected v2 resources")
|
||||||
|
PY
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0011
|
||||||
|
type: workplan
|
||||||
|
title: "Inter-Hub production trigger hardening"
|
||||||
|
domain: railiance
|
||||||
|
repo: railiance-apps
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
created: "2026-06-15"
|
||||||
|
updated: "2026-06-15"
|
||||||
|
state_hub_workstream_id: "98cf42ae-9b64-4736-97e1-bae325ded1f9"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Inter-Hub production trigger hardening
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Turn the local Inter-Hub deploy surface into a safe production trigger for
|
||||||
|
Railiance01. The trigger must refuse missing images before Helm, use the
|
||||||
|
current Inter-Hub v2 API smoke contract, and expose a manual workflow path that
|
||||||
|
has the same gates as an attended local operator deploy.
|
||||||
|
|
||||||
|
## Add OCI Image Preflight
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0011-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "10e27372-fb8b-40ac-b1f8-1c2c78fea0da"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a reusable image manifest preflight for
|
||||||
|
`gitea.coulomb.social/coulomb/inter-hub:<tag>` and wire production deploys to
|
||||||
|
fail before Helm when the requested tag is absent or inaccessible.
|
||||||
|
|
||||||
|
## Split Baseline Render From Production Dry-Run
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0011-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "c48320db-9ed7-4792-89a6-f55691919891"
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep a baseline render target for chart validation with checked-in values, but
|
||||||
|
make production-facing Inter-Hub dry-runs require an explicit
|
||||||
|
`INTER_HUB_IMAGE_TAG`.
|
||||||
|
|
||||||
|
## Update Inter-Hub Smoke Contract
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0011-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "b3260f7a-6dcb-4bb4-ae53-bf81c0081e86"
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `inter-hub-smoke` to match the current public-read/authenticated-write
|
||||||
|
contract: `/api/v2/hubs` returns public discovery, protected resources reject
|
||||||
|
anonymous access, and OpenAPI is served from `/api/v2/openapi.json`.
|
||||||
|
|
||||||
|
## Add Manual Production Deploy Workflow
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0011-T04
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "32ca0b17-fb7c-4cd5-a846-ff92933daf89"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a `workflow_dispatch` Gitea Actions workflow that requires an immutable
|
||||||
|
image tag and confirmation text, verifies the image manifest, runs Helm
|
||||||
|
server-side dry-run, deploys, shows status, and runs smoke checks.
|
||||||
|
|
||||||
|
## Update Runbook And Closure Evidence
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0011-T05
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
state_hub_task_id: "0369b47a-09f0-4780-9c91-556049a0d505"
|
||||||
|
```
|
||||||
|
|
||||||
|
Document the local and workflow production paths, failure classification for a
|
||||||
|
missing image tag, current smoke expectations, and validation evidence for the
|
||||||
|
implemented deploy surface.
|
||||||
Reference in New Issue
Block a user