Skip to content
Open
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
206 changes: 206 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
chart:
name: Chart contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2

- name: Set up Helm
uses: azure/setup-helm@v4.3.1
with:
version: v3.20.0

- name: Install helm-unittest
run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.0.3

- name: Lint and render both profiles
run: |
set -euo pipefail
helm lint --strict .
helm template forge . --namespace forge-ci >/dev/null
helm template forge . --namespace forge-ci \
--set profile=production \
--set localAuth.enabled=false \
--set database.existingSecret=forge-database \
--set objectStore.existingSecret=forge-object-store \
--set objectStore.endpoint=https://account.r2.cloudflarestorage.com \
--set identity.issuer=https://identity.example.invalid \
--set identity.projectId=forge \
--set-string images.edge.digest=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
--set-string images.site.digest=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
--set-string images.api.digest=sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \
>/dev/null

- name: Prove schema rejection
run: |
set -euo pipefail
if helm template forge . --set unknownTopLevel=true >/dev/null 2>&1; then
echo 'schema accepted an unknown top-level value' >&2
exit 1
fi
if helm template forge . \
--set profile=production \
--set localAuth.enabled=true \
>/dev/null 2>&1; then
echo 'schema accepted an unsafe production profile' >&2
exit 1
fi

- name: Run render contract tests
run: helm unittest --strict .

- name: Reject copied application source
run: |
set -euo pipefail
test ! -e Cargo.toml
test ! -d crates
test ! -d src

security:
name: Security scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2

- name: Scan chart configuration
uses: aquasecurity/trivy-action@v0.35.0
with:
scan-type: config
scan-ref: .
exit-code: '1'
severity: HIGH,CRITICAL

entrypoint:
name: Combined-image entrypoint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2

- name: Build inert fixture bases
run: |
set -euo pipefail
fixture="$RUNNER_TEMP/gitkb-forge-fixture"
mkdir -p "$fixture/harmony" "$fixture/git-api"

cat >"$fixture/harmony/harmony-api" <<'SCRIPT'
#!/bin/sh
set -eu
mkdir -p /tmp/fixture
case "${1:-}" in
migrate)
: > /tmp/fixture/harmony.migrated
;;
serve)
: > /tmp/fixture/harmony.started
trap ': > /tmp/fixture/harmony.terminated; exit 0' TERM INT
if [ "${FAIL_HARMONY:-0}" = 1 ]; then
sleep 2
exit 23
fi
while :; do sleep 1; done
;;
*) exit 64 ;;
esac
SCRIPT
chmod 755 "$fixture/harmony/harmony-api"
cat >"$fixture/harmony/Containerfile" <<'CONTAINERFILE'
FROM alpine:3.22
COPY --chmod=0755 harmony-api /app/harmony-api
CONTAINERFILE

cat >"$fixture/git-api/walgit" <<'SCRIPT'
#!/bin/sh
set -eu
mkdir -p /tmp/fixture
test "${1:-}" = serve
if [ "${PORT+x}" = x ]; then
: > /tmp/fixture/walgit.port-leaked
fi
: > /tmp/fixture/walgit.started
trap ': > /tmp/fixture/walgit.terminated; exit 0' TERM INT
while :; do sleep 1; done
SCRIPT
chmod 755 "$fixture/git-api/walgit"
cat >"$fixture/git-api/Containerfile" <<'CONTAINERFILE'
FROM alpine:3.22
RUN apk add --no-cache tini \
&& addgroup -g 1000 forge \
&& adduser -D -u 1000 -G forge forge
COPY --chmod=0755 walgit /usr/local/bin/walgit
USER 1000:1000
ENTRYPOINT ["tini", "--", "/usr/local/bin/walgit"]
CMD ["serve"]
CONTAINERFILE

docker build -f "$fixture/harmony/Containerfile" -t fixture-harmony-api "$fixture/harmony"
docker build -f "$fixture/git-api/Containerfile" -t fixture-git-api "$fixture/git-api"
docker build -f images/api/Containerfile \
--build-arg HARMONY_API_IMAGE=fixture-harmony-api \
--build-arg GIT_API_IMAGE=fixture-git-api \
--build-arg GITKB_FORGE_REVISION="$GITHUB_SHA" \
-t fixture-gitkb-forge-api .
test "$(docker image inspect fixture-gitkb-forge-api --format '{{.Config.User}}')" = 1000:1000

- name: Prove API supervision and child failure
run: |
set -euo pipefail
trap 'docker rm -f forge-entrypoint-api >/dev/null 2>&1 || true' EXIT
docker run -d --name forge-entrypoint-api \
-e FAIL_HARMONY=1 -e PORT=8080 fixture-gitkb-forge-api api >/dev/null
status=$(docker wait forge-entrypoint-api)
test "$status" = 23
docker cp forge-entrypoint-api:/tmp/fixture "$RUNNER_TEMP/api-fixture"
test -f "$RUNNER_TEMP/api-fixture/harmony.migrated"
test -f "$RUNNER_TEMP/api-fixture/harmony.started"
test -f "$RUNNER_TEMP/api-fixture/walgit.started"
test -f "$RUNNER_TEMP/api-fixture/walgit.terminated"
test ! -e "$RUNNER_TEMP/api-fixture/walgit.port-leaked"

- name: Prove API signal handling
run: |
set -euo pipefail
trap 'docker rm -f forge-entrypoint-signal >/dev/null 2>&1 || true' EXIT
docker run -d --name forge-entrypoint-signal \
-e PORT=8080 fixture-gitkb-forge-api api >/dev/null
for attempt in $(seq 1 20); do
if docker exec forge-entrypoint-signal \
sh -c 'test -f /tmp/fixture/harmony.started && test -f /tmp/fixture/walgit.started'; then
break
fi
test "$attempt" -lt 20
sleep 1
done
docker stop --time 10 forge-entrypoint-signal >/dev/null
docker cp forge-entrypoint-signal:/tmp/fixture "$RUNNER_TEMP/signal-fixture"
test -f "$RUNNER_TEMP/signal-fixture/harmony.terminated"
test -f "$RUNNER_TEMP/signal-fixture/walgit.terminated"
test ! -e "$RUNNER_TEMP/signal-fixture/walgit.port-leaked"

- name: Prove worker runs only Git maintenance
run: |
set -euo pipefail
trap 'docker rm -f forge-entrypoint-worker >/dev/null 2>&1 || true' EXIT
docker run -d --name forge-entrypoint-worker fixture-gitkb-forge-api worker >/dev/null
for attempt in $(seq 1 20); do
if docker exec forge-entrypoint-worker test -f /tmp/fixture/walgit.started; then
break
fi
test "$attempt" -lt 20
sleep 1
done
docker exec forge-entrypoint-worker test ! -e /tmp/fixture/harmony.migrated
docker exec forge-entrypoint-worker test ! -e /tmp/fixture/harmony.started
docker stop --time 10 forge-entrypoint-worker >/dev/null
docker cp forge-entrypoint-worker:/tmp/fixture "$RUNNER_TEMP/worker-fixture"
test -f "$RUNNER_TEMP/worker-fixture/walgit.terminated"
11 changes: 11 additions & 0 deletions Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v2
name: gitkb-forge
description: Four-container GitKB Forge appliance
type: application
version: 0.1.0
appVersion: "0.1.0"
home: https://github.com/gitkb/gitkb-forge
sources:
- https://github.com/gitkb/gitkb-forge
annotations:
artifacthub.io/license: MIT
173 changes: 169 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,173 @@
# GitKB Forge

Public packaging for the GitKB Forge Helm appliance.
GitKB Forge is a four-container Helm appliance for hosting and composing
Git/knowledge repositories. It packages existing GitKB binaries; it does not
copy Harmony API or Git API source.

The chart, combined API image assembly, site shell, operator documentation, and
hermetic appliance proof are developed on scoped task branches before review.
The internal-alpha topology is explicit:

Implements [[tasks/harmony-1329]].
| Product workload | Responsibility |
| --- | --- |
| `edge` | Public HTTP routing and streaming reverse proxy |
| `site` | Dependency-free status and entry shell |
| `api` | Harmony API on 8080 plus read-serving Git API on 8081 |
| `worker` | Git writes, maintenance, and ref events on 8082 |

The default `appliance` profile also runs PostgreSQL and RustFS as persistent
backing services. They are not additional GitKB product workloads. The chart
contains no Forgejo, Redis, identity server, SMTP service, billing service,
runner, sidecar, or Git SSH endpoint.

## Install the appliance

A default dynamic StorageClass is the only prerequisite. Once the release
image exists, no values file or external service is required:

```sh
helm install forge . \
--namespace gitkb-forge \
--create-namespace \
--wait \
--wait-for-jobs \
--timeout 5m

helm test forge --namespace gitkb-forge --logs
kubectl port-forward --namespace gitkb-forge \
service/forge-gitkb-forge-edge 8080:8080
```

On first install, a normal Kubernetes Job creates the RustFS bucket after the
Helm-managed Secret exists; API readiness stays false until PostgreSQL and the
bucket are usable. An in-place upgrade runs the same check as a pre-upgrade
hook, so `--wait` cannot report a dependency-incomplete appliance as Ready.

The site is then available at <http://127.0.0.1:8080>. Generated credentials
and the appliance's separated PostgreSQL bootstrap/migration/runtime roles are
stored in `forge-gitkb-forge-secrets`, retained across upgrades through Helm
`lookup`, and never printed by the chart or test.

Capture the Git token directly into the shell environment without displaying
it, then use a temporary askpass helper:

```sh
export WALGIT_TOKEN_MVP=$(kubectl get secret forge-gitkb-forge-secrets \
--namespace gitkb-forge \
-o 'go-template={{ index .data "WALGIT_TOKEN_MVP" | base64decode }}')

ASKPASS=$(mktemp)
chmod 700 "$ASKPASS"
trap 'rm -f "$ASKPASS"' EXIT
printf '%s\n' '#!/bin/sh' \
'case "$1" in' \
' *Username*) printf "%s\\n" mvp-admin ;;' \
' *) printf "%s\\n" "$WALGIT_TOKEN_MVP" ;;' \
'esac' >"$ASKPASS"
export GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0

git clone http://127.0.0.1:8080/OWNER/REPOSITORY.git
```

Credentials must not be embedded in Git URLs, command arguments, values files,
or diagnostic output. An authenticated first push creates a repository.

## External PostgreSQL and R2

The `production` profile renders only the four product workloads. It requires
an existing PostgreSQL URL, S3-compatible credentials, an external OIDC
issuer/project, and digest-selected product images. Cloudflare R2 uses region
`auto`, explicit path-style requests, and separate knowledge/Git prefixes.

Create the Secrets from existing environment variables through stdin so values
do not appear in process arguments or terminal output:

```sh
: "${DATABASE_URL:?required}"
: "${AWS_ACCESS_KEY_ID:?required}"
: "${AWS_SECRET_ACCESS_KEY:?required}"
: "${WALGIT_TOKEN_MVP:?required}"
: "${INVITATION_HMAC_SECRET:?required}"

printf '%s\n' "DATABASE_URL=$DATABASE_URL" | \
kubectl create secret generic forge-database \
--namespace gitkb-forge --from-env-file=/dev/stdin

printf '%s\n' \
"AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" \
"AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" \
"WALGIT_TOKEN_MVP=$WALGIT_TOKEN_MVP" \
"INVITATION_HMAC_SECRET=$INVITATION_HMAC_SECRET" | \
kubectl create secret generic forge-object-store \
--namespace gitkb-forge --from-env-file=/dev/stdin
```

Install with non-secret endpoints and immutable image digests:

```sh
helm install forge . --namespace gitkb-forge \
--set profile=production \
--set localAuth.enabled=false \
--set database.existingSecret=forge-database \
--set objectStore.existingSecret=forge-object-store \
--set objectStore.endpoint=https://ACCOUNT.r2.cloudflarestorage.com \
--set objectStore.bucket=gitkb-forge \
--set identity.issuer=https://identity.example.com \
--set identity.projectId=gitkb-forge \
--set-string images.edge.digest=sha256:EDGE_DIGEST \
--set-string images.site.digest=sha256:SITE_DIGEST \
--set-string images.api.digest=sha256:API_DIGEST \
--wait --wait-for-jobs --timeout 5m
```

`values.schema.json` rejects missing production inputs, local development auth,
non-digest product images, unknown top-level values, and path-style behavior
that differs from the qualified Git engine profile.

## Build the combined API image

The assembly consumes two immutable base-image references:

```sh
docker build -f images/api/Containerfile \
--build-arg HARMONY_API_IMAGE=REGISTRY/harmony-api@sha256:HARMONY_DIGEST \
--build-arg GIT_API_IMAGE=REGISTRY/git-api@sha256:GIT_API_DIGEST \
--build-arg GITKB_FORGE_REVISION=$(git rev-parse HEAD) \
-t gitkb-forge-api:0.1.0 .
```

The resulting non-root image contains `/usr/local/bin/harmony-api` and
`/usr/local/bin/walgit`. `api` mode migrates the appliance database, starts
both HTTP processes, and terminates the survivor if either exits. `worker`
mode executes only `walgit`. `tini` remains PID 1 and delegates signals.

## Verify

Static pull-request gates are secret-free:

```sh
helm lint --strict .
helm template forge . >/tmp/gitkb-forge.yaml
helm unittest .
trivy config . --severity HIGH,CRITICAL --exit-code 1
```

The full local gate builds both private source images through their supported
paths and runs the persistent appliance against RustFS in a disposable kind
cluster:

```sh
scripts/kind-smoke.sh
```

The script refuses to reuse another cluster, keeps credentials out of URLs and
arguments, removes only `gitkb-forge-mvp` unless `KEEP_CLUSTER=1`, and emits a
redacted JSON receipt. Live R2 is optional and is not required by this task.

## Alpha limits

This release is single-replica and not an HA claim. It does not provide SSH,
LFS, pull requests, reviews, issues, Actions, hosted runners, public signup,
multi-user RBAC, backup automation, billing, quotas, or an enterprise support
matrix. The site is an operator entry shell, not a product dashboard.

Implements [[tasks/harmony-1329]], [[specs/forge/runtime-topology]], and
[[specs/forge/bets/hosted-git-appliance]].
Loading
Loading