ci: add Gitea gates and repair boundary drill

This commit is contained in:
SimpleTest 2026-07-20 22:17:39 +03:00
parent 378768b8d5
commit a080c3e183
11 changed files with 139 additions and 17 deletions

View File

@ -72,6 +72,9 @@ core.*
/.tools/
/.playwright-cli/
/output/
/.runner
/act_runner
/act_runner-data/
# Local configuration can contain deployment credentials and must not be sent
# to Docker when Compose builds from the repository root.

View File

@ -0,0 +1,29 @@
name: Quality
on:
push:
pull_request:
workflow_dispatch:
jobs:
full-local-gates:
runs-on: who-need-help-ci
steps:
- name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Run isolated quality and security gates
run: ./scripts/quality.sh
- name: Run isolated browser end-to-end tests
run: ./scripts/e2e-run.sh
- name: Run isolated OAuth, SMTP, and push boundary drill
run: ./scripts/external-boundaries-run.sh ci-boundaries
- name: Build, lint, and unit-test the debug Android app
env:
WNH_ENV_FILE: .env.example
run: ./scripts/android-build.sh

3
.github/actionlint.yaml vendored Normal file
View File

@ -0,0 +1,3 @@
self-hosted-runner:
labels:
- who-need-help-ci

5
.gitignore vendored
View File

@ -41,6 +41,11 @@ npm-debug.log
__pycache__/
*.py[cod]
# Gitea act_runner registration state and local binaries must stay outside Git.
/.runner
/act_runner
/act_runner-data/
# Local environment files can contain deployment credentials. Keep only the
# documented template in Git.
/.env

View File

@ -148,14 +148,14 @@ boundary entirely inside an isolated Docker network:
```
The command publishes no host ports, generates independent one-run credentials
in an ignored mode-`0600` file, verifies success, rejection, retry, replay, and
timeout paths, retains only non-secret evidence, and removes its exact Compose
project, database volume, and images. It also runs two Oban worker replicas and
verifies that request acceptance and new-chat events are delivered from their
real domain transactions, including an Oban retry and pre-transport replay
deduplication. The adapter is not presented as an FCM or APNs implementation;
an external provider must resolve the stable user recipient to registered
devices.
and a separate metrics bearer token in an ignored mode-`0600` file, verifies
success, rejection, retry, replay, and timeout paths, retains only non-secret
evidence, and removes its exact Compose project, database volume, and images.
It also runs two Oban worker replicas and verifies that request acceptance and
new-chat events are delivered from their real domain transactions, including
an Oban retry and pre-transport replay deduplication. The adapter is not
presented as an FCM or APNs implementation; an external provider must resolve
the stable user recipient to registered devices.
The commands, boundaries, and unclaimed production properties are documented
in [the operations runbook](docs/operations.md).

View File

@ -4,6 +4,7 @@ x-boundary-app-environment: &boundary-app-environment
DATABASE_URL: ${EXTERNAL_DATABASE_URL:?Set EXTERNAL_DATABASE_URL}
SECRET_KEY_BASE: ${EXTERNAL_SECRET_KEY_BASE:?Set EXTERNAL_SECRET_KEY_BASE}
HANDOVER_SECRET: ${EXTERNAL_HANDOVER_SECRET:?Set EXTERNAL_HANDOVER_SECRET}
METRICS_TOKEN: ${EXTERNAL_METRICS_TOKEN:?Set EXTERNAL_METRICS_TOKEN}
PHX_HOST: boundary.local
PHX_SCHEME: http
PHX_URL_PORT: "80"

View File

@ -6,6 +6,34 @@ time objective, retention period, storage capacity, or high-availability model;
those values require product policy and measurements from the eventual
production environment.
## Private Gitea continuous integration
The observed private Git remote is hosted by Gitea 1.22.0. Gitea 1.22 reads
repository workflows from `.gitea/workflows/`, so the repository keeps a
Gitea-specific quality workflow in addition to the GitHub-compatible workflow.
Both files are checked by the pinned local actionlint gate.
The Gitea workflow deliberately targets only the custom
`who-need-help-ci` runner label. Its jobs build and run many Docker images and
must not be scheduled onto an unrelated shared runner or onto the production
application host. Its gates run sequentially in one job so a single push cannot
start four heavy Docker suites concurrently. Before enabling it:
1. enable Actions in the private repository settings;
2. register a trusted runner controlled by this project;
3. assign `who-need-help-ci:host` only to a Linux host where the exact Docker
Engine and Compose commands used by the repository have been verified;
4. install the runtime required by the pinned checkout action on that host;
5. keep the runner registration state and any Docker socket access outside the
repository and away from untrusted projects.
The runner's availability, labels, software versions, free resources and
repository Actions setting cannot be established from the unauthenticated
repository clone. Do not treat the presence of the workflow file as evidence
that remote CI has run. After provisioning the runner, trigger the workflow
manually, require the full job and all four gate steps to pass, and record the
run URL and commit before protecting `main`.
## First production Compose environment
Run the initializer on the target Docker host after its final public hostname
@ -140,8 +168,9 @@ credentials, a server, or host-published ports:
The script creates a uniquely named Compose project on an internal-only Docker
network. It generates independent one-run OAuth and push credentials in an
ignored mode-`0600` environment file, builds the production release plus a
non-root standard-library Python protocol mock, and then verifies:
ignored mode-`0600` environment file, including a separate bearer token for the
protected metrics endpoint, builds the production release plus a non-root
standard-library Python protocol mock, and then verifies:
`compose.external-boundaries.yaml` is the only deployment file that sets
`ALLOW_INSECURE_EXTERNAL_HTTP=true`. Ordinary runtime configuration requires

View File

@ -982,6 +982,43 @@ messages. One PostgreSQL error in the audit window came from an incorrect
column name in a read-only operator query; PostgreSQL aborted that transaction.
The subsequent log window contained no matched error/warning entry.
## Private-Git and final boundary replay on 2026-07-20
The newly configured private `origin` and its `main` branch both resolved to
commit `378768b` before this replay. The observed server API identified Gitea
1.22.0. Because that release reads repository workflows from
`.gitea/workflows/`, a separate Gitea workflow now runs the same four local
gates sequentially on the dedicated `who-need-help-ci` label. Both the GitHub
and Gitea workflows passed pinned actionlint locally. Remote Actions enablement,
runner registration and an actual Gitea run remain unverified until a trusted
runner is provisioned.
This replay found and corrected two integration regressions that the ordinary
Phoenix suite did not cover: the external-boundary workers were missing the
new required metrics credential, and the drill's product request was missing
the new required safety acknowledgement. The repaired drill then passed OAuth,
SMTP, provider-neutral push, two Oban worker replicas, retry, replay
deduplication and product-event delivery. Its non-secret mode-`0600` evidence
is `output/external-boundaries/preprod-gitea-pass`.
- The final isolated quality/security gate passed with 244/244 ExUnit tests and
zero configured-threshold source/image findings. The Gitea runner state,
binary and local data directory are now excluded from Git and Docker build
contexts and covered by the quality script.
- The isolated Playwright suite passed all 30 scenarios across Chromium,
Firefox and WebKit, including medicine matching, chat, tracking, handover,
blind reviews, privacy, moderation, account changes and replica restart
recovery. Evidence is `output/e2e/20260720190130-1182`.
- The debug Android build passed unit tests, lint, debug APK and test APK
assembly using `.env.example`; no generated APK is tracked.
- The temporary HTTPS origin returned HTTP 200 from both `/healthz/live` and
`/healthz/ready`. The observed local Compose project had two healthy web and
two healthy worker replicas with zero restarts.
- One idle `docker stats --no-stream` snapshot measured approximately
1,040 MiB combined across the database, Docker API proxy, Mailpit, edge
proxy, two web replicas and two worker replicas. This workstation snapshot is
not a production minimum, limit or capacity claim.
## Known work before a public production launch
- Replace the temporary staging origin with the production-owned domain and

View File

@ -390,7 +390,8 @@ defmodule WhoNeedHelp.ExternalBoundaryDrill do
"location_visibility" => "approximate_public",
"structured_data" => %{"pickup_status" => "reserved"},
"expires_at" => DateTime.utc_now(:second) |> DateTime.add(3, :hour),
"category_id" => category.id
"category_id" => category.id,
"safety_confirmed" => true
})
{:ok, assignment} = Help.accept_request(helper_scope, request.id)

View File

@ -35,6 +35,7 @@ chmod 700 "$ROOT/tmp" "$ROOT/tmp/external-boundaries" "$runtime_dir" \
oauth_client_id="local-$(openssl rand -hex 12)"
oauth_client_secret=$(openssl rand -hex 32)
push_bearer_token=$(openssl rand -hex 32)
metrics_token=$(openssl rand -hex 32)
database_password=$(openssl rand -hex 24)
secret_key_base=$(openssl rand -hex 64)
handover_secret=$(openssl rand -hex 32)
@ -51,6 +52,7 @@ external_boundary_mock_image="who-need-help:boundary-mock-$run_id"
printf 'EXTERNAL_OAUTH_CLIENT_ID=%s\n' "$oauth_client_id"
printf 'EXTERNAL_OAUTH_CLIENT_SECRET=%s\n' "$oauth_client_secret"
printf 'EXTERNAL_PUSH_BEARER_TOKEN=%s\n' "$push_bearer_token"
printf 'EXTERNAL_METRICS_TOKEN=%s\n' "$metrics_token"
printf 'EXTERNAL_POSTGRES_PASSWORD=%s\n' "$database_password"
printf 'EXTERNAL_DATABASE_URL=ecto://boundary:%s@boundary-db/boundary\n' "$database_password"
printf 'EXTERNAL_SECRET_KEY_BASE=%s\n' "$secret_key_base"
@ -167,8 +169,8 @@ jq -e '
.push_product_integration.database_scope == "isolated_ephemeral_volume"
' "$output_dir/summary.json" >/dev/null
for secret in "$oauth_client_secret" "$push_bearer_token" "$database_password" \
"$secret_key_base" "$handover_secret"; do
for secret in "$oauth_client_secret" "$push_bearer_token" "$metrics_token" \
"$database_password" "$secret_key_base" "$handover_secret"; do
if grep -R -F -q -- "$secret" "$output_dir"; then
echo "Generated secret appeared in retained boundary evidence." >&2
exit 1
@ -177,7 +179,7 @@ done
test "$(stat -c '%a' "$output_dir/summary.json")" = "600"
unset oauth_client_id oauth_client_secret push_bearer_token database_password \
secret_key_base handover_secret
unset oauth_client_id oauth_client_secret push_bearer_token metrics_token \
database_password secret_key_base handover_secret
printf 'External boundary evidence: %s\n' "$output_dir"

View File

@ -68,16 +68,27 @@ for dockerfile in Dockerfile Dockerfile.backup Dockerfile.minio \
hadolint --failure-threshold warning - <"$dockerfile"
done
echo "Checking the GitHub Actions workflow with actionlint 1.7.12"
echo "Checking the GitHub and Gitea Actions workflows with actionlint 1.7.12"
docker run --rm \
--volume "$ROOT:/repo:ro" \
--workdir /repo \
"$ACTIONLINT_IMAGE"
"$ACTIONLINT_IMAGE" \
-config-file .github/actionlint.yaml \
.github/workflows/quality.yml \
.gitea/workflows/quality.yml
echo "Checking crash dumps are excluded from the Docker build context"
grep -Fx 'core' .dockerignore >/dev/null
grep -Fx 'core.*' .dockerignore >/dev/null
echo "Checking local Gitea runner state is excluded from Git and Docker contexts"
grep -Fx '/.runner' .gitignore >/dev/null
grep -Fx '/act_runner' .gitignore >/dev/null
grep -Fx '/act_runner-data/' .gitignore >/dev/null
grep -Fx '/.runner' .dockerignore >/dev/null
grep -Fx '/act_runner' .dockerignore >/dev/null
grep -Fx '/act_runner-data/' .dockerignore >/dev/null
echo "Checking production environment initialization and validation"
production_env="$scan_dir/.env.production"
PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
@ -202,6 +213,7 @@ EXTERNAL_BOUNDARY_HOST_GID="$(id -g)" \
EXTERNAL_OAUTH_CLIENT_ID=render-client \
EXTERNAL_OAUTH_CLIENT_SECRET=render-secret \
EXTERNAL_PUSH_BEARER_TOKEN=render-push-token \
EXTERNAL_METRICS_TOKEN=render-metrics-token \
EXTERNAL_POSTGRES_PASSWORD=render-database-secret \
EXTERNAL_DATABASE_URL=ecto://boundary:render-database-secret@boundary-db/boundary \
EXTERNAL_SECRET_KEY_BASE=render-secret-key-base \