chore: add local quality and security gates
This commit is contained in:
parent
d80fb231fb
commit
477517527f
9
.dialyzer_ignore.exs
Normal file
9
.dialyzer_ignore.exs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Ecto.Multi stores operation names in a MapSet. OTP 28+ can report
|
||||
# call_without_opaque at the application call site even though only Ecto's
|
||||
# public constructors are used. Keep these filters path- and warning-specific;
|
||||
# `list_unused_filters: true` makes Dialyxir fail when either workaround is no
|
||||
# longer needed.
|
||||
[
|
||||
{"lib/who_need_help/help.ex", :call_without_opaque},
|
||||
{"lib/who_need_help/tracking.ex", :call_without_opaque}
|
||||
]
|
||||
49
.github/workflows/quality.yml
vendored
Normal file
49
.github/workflows/quality.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: Quality
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: quality-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend-quality:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run isolated quality and security gates
|
||||
run: ./scripts/quality.sh
|
||||
|
||||
browser-e2e:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run isolated browser end-to-end tests
|
||||
run: ./scripts/e2e-run.sh
|
||||
|
||||
android-build:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build, lint, and unit-test the debug Android app
|
||||
env:
|
||||
WNH_ENV_FILE: .env.example
|
||||
run: ./scripts/android-build.sh
|
||||
31
Dockerfile
31
Dockerfile
|
|
@ -35,7 +35,9 @@ FROM ${BUILDER_IMAGE} AS builder
|
|||
|
||||
# install build dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential git \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential=12.12 \
|
||||
git=1:2.47.3-0+deb13u1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# prepare build dir
|
||||
|
|
@ -88,7 +90,14 @@ RUN mix release
|
|||
FROM ${RUNNER_IMAGE} AS final
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 libsctp1 locales ca-certificates curl \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates=20250419 \
|
||||
curl=8.14.1-2+deb13u4 \
|
||||
libncurses6=6.5+20250216-2 \
|
||||
libsctp1=1.0.21+dfsg-1 \
|
||||
libstdc++6=14.2.0-19 \
|
||||
locales=2.41-12+deb13u3 \
|
||||
openssl=3.5.6-1~deb13u2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the locale
|
||||
|
|
@ -120,7 +129,10 @@ CMD ["/app/bin/server"]
|
|||
FROM ${BUILDER_IMAGE} AS test
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential git ca-certificates \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential=12.12 \
|
||||
ca-certificates=20250419 \
|
||||
git=1:2.47.3-0+deb13u1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
|
@ -135,7 +147,7 @@ RUN mix deps.get --only test
|
|||
COPY config config
|
||||
RUN mix deps.compile
|
||||
|
||||
COPY .formatter.exs ./
|
||||
COPY .dialyzer_ignore.exs .formatter.exs ./
|
||||
COPY priv priv
|
||||
COPY lib lib
|
||||
COPY test test
|
||||
|
|
@ -144,6 +156,15 @@ RUN mix compile
|
|||
|
||||
CMD ["mix", "test"]
|
||||
|
||||
FROM test AS quality
|
||||
|
||||
# PLTs are expensive to create but deterministic for the pinned Erlang,
|
||||
# Elixir, dependency lock, and test environment. Cache them in this dedicated
|
||||
# target so every quality command uses the same analyzed dependency set.
|
||||
RUN mix dialyzer --plt
|
||||
|
||||
CMD ["mix", "dialyzer"]
|
||||
|
||||
# Keep the production release as the default build result while exposing the
|
||||
# dedicated `test` target to scripts/test.sh.
|
||||
# dedicated `test` and `quality` targets to local verification scripts.
|
||||
FROM final AS release
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -141,6 +141,21 @@ project's PostGIS service:
|
|||
|
||||
It creates/updates only the project-scoped `who_need_help_test` database.
|
||||
|
||||
The complete local CI-equivalent command uses pinned containerized tools and
|
||||
an isolated PostgreSQL volume:
|
||||
|
||||
```bash
|
||||
./scripts/quality.sh
|
||||
```
|
||||
|
||||
It checks shell scripts, Dockerfiles, the GitHub Actions workflow, every Compose
|
||||
profile, the rendered Helm chart, tracked-source secrets and infrastructure
|
||||
misconfigurations, Elixir formatting/compilation/xref/Credo/Sobelow/Dialyzer,
|
||||
retired Hex packages, locked npm dependencies, all Phoenix tests, and the
|
||||
production release image. Its generated database credentials are random and
|
||||
exist only for that run. The exact database volume, networks, temporary source
|
||||
snapshot, and one-run images are removed automatically.
|
||||
|
||||
The browser E2E command creates a uniquely named, isolated Compose project with
|
||||
its own PostGIS volume, Mailpit instance, Traefik proxy, two web replicas, and
|
||||
two worker replicas:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
FROM gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819c235db886d0fae7 AS android-base
|
||||
|
||||
USER root
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=15859902
|
||||
ARG ANDROID_COMMAND_LINE_TOOLS_SHA1=040d3996a65543d22ec4bf73e4c37aa37a8d4af4
|
||||
|
|
@ -10,7 +11,9 @@ ENV ANDROID_SDK_ROOT=/opt/android-sdk
|
|||
ENV PATH="${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl unzip \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl=8.18.0-1ubuntu2.3 \
|
||||
unzip=6.0-29ubuntu1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \
|
||||
|
|
@ -28,6 +31,8 @@ RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
|
|||
"platforms/android-37.0" \
|
||||
&& chown -R gradle:gradle "${ANDROID_HOME}"
|
||||
|
||||
USER gradle
|
||||
|
||||
FROM android-base AS android-sdk
|
||||
|
||||
USER gradle
|
||||
|
|
@ -50,6 +55,7 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
|||
FROM android-base AS emulator
|
||||
|
||||
USER root
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
|
||||
"emulator" \
|
||||
|
|
@ -58,19 +64,19 @@ RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
|
|||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libdbus-1-3 \
|
||||
libfontconfig1 \
|
||||
libgl1 \
|
||||
libnss3 \
|
||||
libpulse0 \
|
||||
libx11-6 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxi6 \
|
||||
libxkbfile1 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxtst6 \
|
||||
libdbus-1-3=1.16.2-2ubuntu4 \
|
||||
libfontconfig1=2.17.1-3ubuntu1 \
|
||||
libgl1=1.7.0-3 \
|
||||
libnss3=2:3.120-1ubuntu2.1 \
|
||||
libpulse0=1:17.0+dfsg1-2ubuntu4 \
|
||||
libx11-6=2:1.8.13-1 \
|
||||
libxcomposite1=1:0.4.6-1build1 \
|
||||
libxcursor1=1:1.2.3-1build1 \
|
||||
libxi6=2:1.8.2-2 \
|
||||
libxkbfile1=1:1.1.0-1build5 \
|
||||
libxrandr2=2:1.5.4-1build1 \
|
||||
libxrender1=1:0.9.12-1build1 \
|
||||
libxtst6=2:1.2.5-1build1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The container is operated from the host through `docker exec ... adb`.
|
||||
|
|
@ -108,6 +114,8 @@ CMD ["/opt/android-sdk/emulator/emulator", \
|
|||
|
||||
FROM scratch AS artifact
|
||||
|
||||
USER 65532:65532
|
||||
|
||||
COPY --from=android-sdk \
|
||||
/workspace/android/app/build/outputs/apk/debug/app-debug.apk \
|
||||
/who-need-help-debug.apk
|
||||
|
|
@ -137,6 +145,8 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
|||
|
||||
FROM scratch AS staging-artifact
|
||||
|
||||
USER 65532:65532
|
||||
|
||||
COPY --from=android-staging-sdk \
|
||||
/workspace/android/app/build/outputs/apk/staging/app-staging.apk \
|
||||
/who-need-help-staging.apk
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ services:
|
|||
context: e2e
|
||||
init: true
|
||||
ipc: host
|
||||
user: "${E2E_UID:-1000}:${E2E_GID:-1000}"
|
||||
environment:
|
||||
BASE_URL: ${E2E_BASE_URL:?Set E2E_BASE_URL in the generated E2E environment}
|
||||
MAILPIT_URL: ${E2E_MAILPIT_URL:?Set E2E_MAILPIT_URL in the generated E2E environment}
|
||||
HOME: /tmp
|
||||
CI: ${CI:-}
|
||||
NO_PROXY: web,mailpit,db,proxy,localhost,127.0.0.1
|
||||
no_proxy: web,mailpit,db,proxy,localhost,127.0.0.1
|
||||
|
|
|
|||
22
compose.quality.yaml
Normal file
22
compose.quality.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
services:
|
||||
db:
|
||||
image: postgis/postgis:18-3.6-alpine@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: ${QUALITY_POSTGRES_USER:?Set QUALITY_POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${QUALITY_POSTGRES_PASSWORD:?Set QUALITY_POSTGRES_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
volumes:
|
||||
- quality_postgres_data:/var/lib/postgresql
|
||||
networks: [internal]
|
||||
|
||||
networks:
|
||||
internal:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
quality_postgres_data:
|
||||
|
|
@ -28,7 +28,6 @@ config :who_need_help,
|
|||
ecto_repos: [WhoNeedHelp.Repo],
|
||||
generators: [timestamp_type: :utc_datetime, binary_id: true],
|
||||
app_role: :web,
|
||||
handover_secret: "development-only-handover-secret",
|
||||
codex_session_id: "not-configured",
|
||||
rate_limit_policies: %{},
|
||||
map_tile_url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import Config
|
||||
|
||||
config :who_need_help, :handover_secret, System.fetch_env!("HANDOVER_SECRET")
|
||||
|
||||
# Configure your database
|
||||
config :who_need_help, WhoNeedHelp.Repo,
|
||||
username: "postgres",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import Config
|
||||
|
||||
config :who_need_help, :handover_secret, "isolated-test-handover-secret"
|
||||
|
||||
# Only in tests, remove the complexity from the password hashing algorithm
|
||||
config :bcrypt_elixir, :log_rounds, 1
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ spec:
|
|||
env:
|
||||
- name: APP_ROLE
|
||||
value: migrate
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
containers:
|
||||
- name: {{ $component }}
|
||||
image: "{{ $root.Values.image.repository }}:{{ $root.Values.image.tag }}"
|
||||
|
|
|
|||
|
|
@ -11,10 +11,23 @@ spec:
|
|||
metadata:
|
||||
labels: {app: postgis}
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 70
|
||||
runAsGroup: 70
|
||||
fsGroup: 70
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: postgis
|
||||
image: postgis/postgis:18-3.6-alpine
|
||||
image: postgis/postgis:18-3.6-alpine@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
|
|
@ -39,11 +52,17 @@ spec:
|
|||
periodSeconds: 3
|
||||
volumeMounts:
|
||||
- {name: data, mountPath: /var/lib/postgresql}
|
||||
- {name: runtime, mountPath: /var/run/postgresql}
|
||||
- {name: tmp, mountPath: /tmp}
|
||||
volumes:
|
||||
- name: data
|
||||
hostPath:
|
||||
path: /var/local/who-need-help/postgres
|
||||
type: DirectoryOrCreate
|
||||
- name: runtime
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
|
@ -66,13 +85,29 @@ spec:
|
|||
metadata:
|
||||
labels: {app: mailpit}
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: mailpit
|
||||
image: axllent/mailpit:v1.30.4
|
||||
image: axllent/mailpit:v1.30.4@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
ports:
|
||||
- {name: smtp, containerPort: 1025}
|
||||
- {name: http, containerPort: 8025}
|
||||
volumeMounts:
|
||||
- {name: tmp, mountPath: /tmp}
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
|
|
|||
|
|
@ -68,6 +68,23 @@ release feeds reported those same versions as current during verification.
|
|||
Android 17 API 37 is the stable target used here. API 37.1 was not selected
|
||||
because the official SDK channel identifies it as a QPR beta.
|
||||
|
||||
## Local quality and security tools
|
||||
|
||||
| Component | Pinned version |
|
||||
| --- | --- |
|
||||
| ShellCheck | 0.11.0 |
|
||||
| Hadolint | 2.14.0 |
|
||||
| actionlint | 1.7.12 |
|
||||
| Trivy | 0.72.0 |
|
||||
| Credo | 1.7.19 |
|
||||
| Dialyxir | 1.4.7 |
|
||||
| Sobelow | 0.14.1 |
|
||||
| GitHub checkout action | 7.0.0 / commit `9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` |
|
||||
|
||||
The four containerized command-line tools are pinned by both exact tag and OCI
|
||||
digest in `scripts/quality.sh`. The checkout action is pinned by commit in the
|
||||
workflow. Credo, Dialyxir, and Sobelow are locked in `mix.lock`.
|
||||
|
||||
## Primary version sources
|
||||
|
||||
- [Elixir releases](https://github.com/elixir-lang/elixir/releases)
|
||||
|
|
@ -91,6 +108,11 @@ because the official SDK channel identifies it as a QPR beta.
|
|||
- [Docker Engine releases](https://github.com/moby/moby/releases)
|
||||
- [Docker Compose releases](https://github.com/docker/compose/releases)
|
||||
- [Docker Buildx releases](https://github.com/docker/buildx/releases)
|
||||
- [ShellCheck releases](https://github.com/koalaman/shellcheck/releases)
|
||||
- [Hadolint releases](https://github.com/hadolint/hadolint/releases)
|
||||
- [actionlint releases](https://github.com/rhysd/actionlint/releases)
|
||||
- [Trivy releases](https://github.com/aquasecurity/trivy/releases)
|
||||
- [checkout releases](https://github.com/actions/checkout/releases)
|
||||
|
||||
## Repeatable checks
|
||||
|
||||
|
|
@ -100,6 +122,8 @@ docker build --target node_deps --tag who-need-help:node-deps .
|
|||
docker run --rm who-need-help:node-deps npm --version
|
||||
docker run --rm who-need-help:node-deps npm outdated --json
|
||||
./scripts/test.sh
|
||||
./scripts/quality.sh
|
||||
./scripts/e2e-run.sh
|
||||
./scripts/android-build.sh
|
||||
./scripts/android-instrumentation-test.sh
|
||||
```
|
||||
|
|
|
|||
|
|
@ -44,6 +44,20 @@ The goal remains open while any row lacks reproducible local evidence.
|
|||
cleanup, and a disconnected Stop request with visible retry state. It uses an
|
||||
in-process loopback fixture, runs its emulator without external networking,
|
||||
retains diagnostics on failure, and removes its exact container and image.
|
||||
- The containerized `scripts/quality.sh` gate passes ShellCheck, Hadolint,
|
||||
actionlint, all Compose renders, Helm lint, a Trivy scan of tracked source and
|
||||
the rendered Kubernetes manifest, compiler/xref/Credo/Sobelow/Dialyzer/Hex
|
||||
checks, 149 Phoenix tests, both npm audits, and a Trivy scan of the production
|
||||
release image. It creates random one-run database credentials and removes its
|
||||
exact volume, networks, images, and source snapshot.
|
||||
- The checked-in GitHub Actions workflow runs the same isolated backend/security
|
||||
gate, browser E2E suite, and Android unit/lint/APK build. The workflow itself
|
||||
passes actionlint and uses a commit-pinned checkout action without persisted
|
||||
credentials.
|
||||
- The hardened local kind manifests also passed a runtime rollout: PostGIS,
|
||||
Mailpit, both web replicas, and both worker replicas became Ready as non-root
|
||||
containers with read-only root filesystems, dropped capabilities, and
|
||||
RuntimeDefault seccomp where configured. The cross-node PubSub probe passed.
|
||||
- The remaining rows above are still pending. Browser accessibility,
|
||||
responsive, offline, and reconnect checks remain part of their respective
|
||||
later rows; this document is not a completion claim.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,18 @@ results from product limits and unknown production properties.
|
|||
on Elixir 1.20.2 and Erlang/OTP 29.0.3.
|
||||
- `mix compile --force --warnings-as-errors` and
|
||||
`mix format --check-formatted`: passed against the same final source.
|
||||
- `./scripts/quality.sh` passed ShellCheck 0.11.0, Hadolint 2.14.0 at warning
|
||||
threshold, actionlint 1.7.12, all four Compose renders, Helm lint, Trivy
|
||||
source/rendered-manifest scanning, xref, Credo high-priority checks, Sobelow
|
||||
strict/private checks, Hex audit, 149 Phoenix tests, both npm audits, and the
|
||||
production-image vulnerability scan. The rendered Helm manifest and Debian
|
||||
13.6 release image each reported zero HIGH/CRITICAL findings under the
|
||||
configured gates.
|
||||
- Dialyzer passed with two exact, documented filters and zero unused filters.
|
||||
Both findings are opaque-type warnings at Ecto `Multi`/`MapSet` call sites;
|
||||
the filters are visible in `.dialyzer_ignore.exs` rather than hidden by a
|
||||
broad pattern. This is not a claim that third-party dependency compilation is
|
||||
warning-free.
|
||||
- The generated Activity migration was rolled back by exactly one step and
|
||||
migrated forward again against `who_need_help_test`; both directions passed.
|
||||
- The Activity-report migration was also rolled back and migrated forward. The
|
||||
|
|
@ -45,6 +57,16 @@ results from product limits and unknown production properties.
|
|||
intended; release configuration accepts only an explicit HTTPS origin.
|
||||
- Helm lint, template rendering, server-side dry-run, rollout waits, readiness
|
||||
checks, and cross-node PubSub verification passed in the local kind cluster.
|
||||
- The hardened kind rollout then ran PostGIS as UID/GID 70 and Mailpit as
|
||||
UID/GID 10001. Both used read-only root filesystems, dropped all Linux
|
||||
capabilities, disabled privilege escalation, and used RuntimeDefault seccomp.
|
||||
Both web and both worker replicas were Ready; their migration-wait init
|
||||
containers also used read-only root filesystems and dropped capabilities.
|
||||
The cross-node probe observed all four BEAM nodes, and live/readiness returned
|
||||
`ok`/`ready`. Database counts remained `0 users / 0 help requests`.
|
||||
- The committed browser suite was repeated after CSP and non-root E2E-runner
|
||||
changes: its bootstrap test passed 1/1 and its three Chromium scenarios
|
||||
passed 3/3. The uniquely named Compose volume and networks were removed.
|
||||
- Browser verification used headed Chrome. The authenticated matched-request
|
||||
page rendered its chat, MapLibre marker, and live-location controls with no
|
||||
console errors or warnings after the foreground-service rollout.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ FROM mcr.microsoft.com/playwright:v1.61.1-noble@sha256:5b8f294aff9041b7191c34a4b
|
|||
|
||||
WORKDIR /work
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY --chown=pwuser:pwuser package.json package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY playwright.config.ts playwright.bootstrap.config.ts ./
|
||||
COPY tests tests
|
||||
COPY setup setup
|
||||
COPY --chown=pwuser:pwuser playwright.config.ts playwright.bootstrap.config.ts ./
|
||||
COPY --chown=pwuser:pwuser tests tests
|
||||
COPY --chown=pwuser:pwuser setup setup
|
||||
|
||||
USER pwuser
|
||||
|
||||
CMD ["npx", "playwright", "test"]
|
||||
|
|
|
|||
|
|
@ -21,9 +21,8 @@ defmodule WhoNeedHelp.SocialOAuth.AssentAdapter do
|
|||
{:ok, %{user: user}} <-
|
||||
config
|
||||
|> Keyword.put(:session_params, session_params)
|
||||
|> GithubStrategy.callback(params),
|
||||
{:ok, identity} <- normalize_github_identity(user) do
|
||||
{:ok, identity}
|
||||
|> GithubStrategy.callback(params) do
|
||||
normalize_github_identity(user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ defmodule WhoNeedHelpWeb.MetricsController do
|
|||
|
||||
@content_type "text/plain; version=0.0.4"
|
||||
|
||||
# Sobelow's HTML-oriented SendResp check cannot infer this fixed Prometheus
|
||||
# text content type. The response is not rendered in an HTML context.
|
||||
# sobelow_skip ["XSS.SendResp"]
|
||||
def show(conn, _params) do
|
||||
token = Application.get_env(:who_need_help, :metrics_token)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ defmodule WhoNeedHelpWeb.Router do
|
|||
|
||||
import WhoNeedHelpWeb.UserAuth
|
||||
|
||||
@secure_browser_headers %{
|
||||
"content-security-policy" => "base-uri 'self'; frame-ancestors 'self';"
|
||||
}
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_live_flash
|
||||
plug :put_root_layout, html: {WhoNeedHelpWeb.Layouts, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :put_secure_browser_headers, @secure_browser_headers
|
||||
plug :fetch_current_scope_for_user
|
||||
plug WhoNeedHelpWeb.Locale
|
||||
end
|
||||
|
|
@ -22,7 +26,7 @@ defmodule WhoNeedHelpWeb.Router do
|
|||
plug :accepts, ["json"]
|
||||
plug :fetch_session
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :put_secure_browser_headers, @secure_browser_headers
|
||||
plug :fetch_current_scope_for_user
|
||||
end
|
||||
|
||||
|
|
|
|||
18
mix.exs
18
mix.exs
|
|
@ -11,7 +11,12 @@ defmodule WhoNeedHelp.MixProject do
|
|||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
compilers: [:phoenix_live_view] ++ Mix.compilers(),
|
||||
listeners: [Phoenix.CodeReloader]
|
||||
listeners: [Phoenix.CodeReloader],
|
||||
dialyzer: [
|
||||
plt_add_apps: [:ex_unit, :mix],
|
||||
ignore_warnings: ".dialyzer_ignore.exs",
|
||||
list_unused_filters: true
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -51,6 +56,9 @@ defmodule WhoNeedHelp.MixProject do
|
|||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 1.2.0"},
|
||||
{:lazy_html, ">= 0.1.0", only: :test},
|
||||
{:credo, "~> 1.7.19", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.4.7", only: [:dev, :test], runtime: false},
|
||||
{:sobelow, "~> 0.14.1", only: [:dev, :test], runtime: false},
|
||||
{:phoenix_live_dashboard, "~> 0.8.3"},
|
||||
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
|
||||
{:tailwind, "~> 0.5", runtime: Mix.env() == :dev},
|
||||
|
|
@ -101,7 +109,13 @@ defmodule WhoNeedHelp.MixProject do
|
|||
"esbuild who_need_help --minify",
|
||||
"phx.digest"
|
||||
],
|
||||
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
|
||||
precommit: [
|
||||
"compile --warnings-as-errors",
|
||||
"format --check-formatted",
|
||||
"credo --strict --min-priority high",
|
||||
"sobelow --exit --strict --private --skip",
|
||||
"test"
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
5
mix.lock
5
mix.lock
|
|
@ -2,15 +2,19 @@
|
|||
"assent": {:hex, :assent, "0.3.1", "7e7b04f4ab5d07b497b80c04f009a0f1efc409787e68b8e086512b9098f3095b", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "3597b31f9eb556d97e64cf60c00d3451f7353d7b465a71d33530b870ebed1ff1"},
|
||||
"bandit": {:hex, :bandit, "1.12.0", "6c5214daa2469644ac4ab0113b98abc24f75e348378e6a974c6343b3e5da22ef", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "45dac82dc86f45cf4a196dee9cc5a8b791d9c9469d996055f055e6ee36c66e20"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
"credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"},
|
||||
"daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "374d63758f997317e92e93e547569a5f9ad11250", [tag: "v5.6.18", sparse: "packages/bundle", depth: 1]},
|
||||
"db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
|
||||
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.14.1", "7b740d87bdf45996aa0c2c2e081640906f10caa7ce5ba328fd294c7d49d0cc6f", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24b991956796700f467d0a3ef3d303138a3ef9ddddf8b98f43758ee067b20a30"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||
|
|
@ -43,6 +47,7 @@
|
|||
"postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"req": {:hex, :req, "0.6.3", "7fe5e68792ff0546e45d5919104fa1764a13694cfe3e48c8a0f32ad051ae77e4", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "e85b5c6c990e6c3f52bbba68e6f099118f2b8252825f96c7c3636b97a3de307d"},
|
||||
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
|
||||
"swoosh": {:hex, :swoosh, "1.26.3", "9d8b60077305ce259298d9a1102e5be67cd3c41d1ea930c29e9288af195ca017", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c7683d070fe8f8aa9d174e61b01f2d527be73cd8ac40037b7109184941eb569f"},
|
||||
"tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE=${WNH_ENV_FILE:-"$ROOT/.env"}
|
||||
|
||||
case "$ENV_FILE" in
|
||||
/*) ;;
|
||||
*) ENV_FILE="$ROOT/$ENV_FILE" ;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Missing $ENV_FILE. Copy .env.example to .env and configure WNH_DEBUG_BASE_URL." >&2
|
||||
|
|
@ -10,6 +15,7 @@ if [ ! -f "$ENV_FILE" ]; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ANDROID_ENV="$ROOT/.env"
|
||||
TEST_ENV="$ROOT/.env.android-test"
|
||||
run_id=$(date -u +%Y%m%d%H%M%S)-$$
|
||||
|
|
@ -23,7 +23,9 @@ fi
|
|||
"$ROOT/scripts/ensure-local-android-test-env.sh"
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ANDROID_ENV"
|
||||
# shellcheck source=/dev/null
|
||||
. "$TEST_ENV"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
"$ROOT/scripts/ensure-local-public-origin.sh"
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
set -eu
|
||||
umask 077
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
if [ ! -f "$ROOT/.env" ]; then
|
||||
|
|
@ -11,6 +11,7 @@ if [ ! -f "$ROOT/.env" ]; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ROOT/.env"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ case "$MODE" in
|
|||
docker exec "$TARGET" /app/bin/who_need_help rpc "$EXPRESSION"
|
||||
;;
|
||||
kind)
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
PATH="$ROOT/.tools/bin:$PATH"
|
||||
export PATH
|
||||
TARGET=$(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
TOOLS="$ROOT/.tools/bin"
|
||||
mkdir -p "$TOOLS"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
INPUT=${1:-}
|
||||
OUTPUT=${2:-}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
CODEX_SESSION_ID=${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-not-configured}}
|
||||
export CODEX_SESSION_ID
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
"$ROOT/scripts/ensure-local-e2e-env.sh"
|
||||
|
|
@ -24,7 +24,11 @@ export TRAEFIK_PROJECT_CONSTRAINT="$project"
|
|||
export TRAEFIK_APP_NAME="who-need-help-e2e-$run_id"
|
||||
export TRAEFIK_DOCKER_NETWORK="${project}_internal"
|
||||
export E2E_OUTPUT_DIR="$output_dir"
|
||||
export CODEX_SESSION_ID=${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-local-e2e}}
|
||||
export E2E_UID
|
||||
E2E_UID=$(id -u)
|
||||
export E2E_GID
|
||||
E2E_GID=$(id -g)
|
||||
export CODEX_SESSION_ID="${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-local-e2e}}"
|
||||
|
||||
compose() {
|
||||
docker compose \
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ case "$project" in
|
|||
;;
|
||||
esac
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
|
||||
docker compose \
|
||||
--project-name "$project" \
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
target="$ROOT/.env.android-test"
|
||||
|
||||
if [ -f "$target" ]; then
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
set -eu
|
||||
umask 077
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
target="$ROOT/.env.e2e"
|
||||
|
||||
if [ -f "$target" ]; then
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
TEMPLATE="$ROOT/.env.load.example"
|
||||
ENV_FILE="$ROOT/.env.load"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
|
|
@ -15,6 +15,7 @@ if ! command -v perl >/dev/null 2>&1; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
"$ROOT/scripts/bootstrap-kubernetes-tools.sh"
|
||||
PATH="$ROOT/.tools/bin:$PATH"
|
||||
export PATH
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env.load"
|
||||
K6_IMAGE="grafana/k6@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669"
|
||||
LABEL=${1:-"run-$(date -u +%Y%m%dT%H%M%SZ)"}
|
||||
|
|
@ -12,6 +12,7 @@ if [[ ! -f "$ENV_FILE" ]]; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
@ -81,6 +82,8 @@ touch "$running_marker"
|
|||
snapshot_database() {
|
||||
local destination=$1
|
||||
|
||||
# The variables below are intentionally expanded inside the database container.
|
||||
# shellcheck disable=SC2016
|
||||
"${compose[@]}" exec -T db sh -c \
|
||||
'psql --no-psqlrc --set ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"' \
|
||||
>"$destination" <<'SQL'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env.load"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
|
|
@ -10,6 +10,7 @@ if [ ! -f "$ENV_FILE" ]; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env.load"
|
||||
REPLICAS=${1:-}
|
||||
|
||||
"$ROOT/scripts/ensure-local-load-env.sh"
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
137
scripts/quality.sh
Executable file
137
scripts/quality.sh
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
SHELLCHECK_IMAGE="koalaman/shellcheck-alpine:v0.11.0@sha256:9955be09ea7f0dbf7ae942ac1f2094355bb30d96fffba0ec09f5432207544002"
|
||||
HADOLINT_IMAGE="hadolint/hadolint:v2.14.0-debian@sha256:158cd0184dcaa18bd8ec20b61f4c1cabdf8b32a592d062f57bdcb8e4c1d312e2"
|
||||
ACTIONLINT_IMAGE="rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667"
|
||||
TRIVY_IMAGE="aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f"
|
||||
|
||||
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
||||
project="wnh_quality_$(printf '%s' "$run_id" | tr -d '-')"
|
||||
quality_image="who-need-help:quality-$run_id"
|
||||
assets_image="who-need-help:assets-audit-$run_id"
|
||||
e2e_image="who-need-help:e2e-audit-$run_id"
|
||||
release_image="who-need-help:security-$run_id"
|
||||
scan_dir=$(mktemp -d "${TMPDIR:-/tmp}/wnh-quality-scan.XXXXXX")
|
||||
scan_list="${scan_dir}.files"
|
||||
scan_tar="${scan_dir}.tar"
|
||||
|
||||
umask 077
|
||||
QUALITY_POSTGRES_USER="wnh_quality_$(openssl rand -hex 6)"
|
||||
QUALITY_POSTGRES_PASSWORD=$(openssl rand -base64 48 | tr -d '\n')
|
||||
export QUALITY_POSTGRES_USER QUALITY_POSTGRES_PASSWORD
|
||||
|
||||
compose="docker compose -p $project -f $ROOT/compose.quality.yaml"
|
||||
|
||||
cleanup() {
|
||||
$compose down --volumes --remove-orphans >/dev/null 2>&1 || true
|
||||
docker image rm "$quality_image" "$assets_image" "$e2e_image" "$release_image" \
|
||||
>/dev/null 2>&1 || true
|
||||
rm -rf "$scan_dir" "$scan_list" "$scan_tar"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
echo "Checking shell scripts with ShellCheck 0.11.0"
|
||||
# Word splitting is intentional: find emits repository-controlled paths and
|
||||
# ShellCheck expects each file as a separate argument.
|
||||
# shellcheck disable=SC2046
|
||||
docker run --rm \
|
||||
--volume "$ROOT:/mnt:ro" \
|
||||
--workdir /mnt \
|
||||
--entrypoint shellcheck \
|
||||
"$SHELLCHECK_IMAGE" \
|
||||
$(find scripts -type f -name '*.sh' -print | sort)
|
||||
|
||||
echo "Checking Dockerfiles with Hadolint 2.14.0"
|
||||
for dockerfile in Dockerfile android/Dockerfile e2e/Dockerfile; do
|
||||
docker run --rm --interactive "$HADOLINT_IMAGE" \
|
||||
hadolint --failure-threshold warning - <"$dockerfile"
|
||||
done
|
||||
|
||||
echo "Checking the GitHub Actions workflow with actionlint 1.7.12"
|
||||
docker run --rm \
|
||||
--volume "$ROOT:/repo:ro" \
|
||||
--workdir /repo \
|
||||
"$ACTIONLINT_IMAGE"
|
||||
|
||||
echo "Rendering every Docker Compose profile"
|
||||
docker compose --env-file .env.example -f compose.yaml config --quiet
|
||||
mkdir -p "$scan_dir/e2e-output"
|
||||
E2E_OUTPUT_DIR="$scan_dir/e2e-output" docker compose --env-file .env.e2e.example \
|
||||
-f compose.yaml -f compose.e2e.yaml config --quiet
|
||||
docker compose --env-file .env.load.example \
|
||||
-f compose.yaml -f compose.load.yaml config --quiet
|
||||
docker compose -p "$project" -f compose.quality.yaml config --quiet
|
||||
|
||||
echo "Linting the Helm chart"
|
||||
"$ROOT/scripts/bootstrap-kubernetes-tools.sh" >/dev/null
|
||||
"$ROOT/.tools/bin/helm" lint \
|
||||
--values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \
|
||||
"$ROOT/deploy/helm/who-need-help"
|
||||
|
||||
echo "Scanning only tracked and non-ignored source files"
|
||||
git ls-files --cached --others --exclude-standard -z >"$scan_list"
|
||||
tar --null --no-recursion --files-from="$scan_list" --create --file="$scan_tar"
|
||||
tar --extract --file="$scan_tar" --directory "$scan_dir"
|
||||
"$ROOT/.tools/bin/helm" template who-need-help \
|
||||
--values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \
|
||||
"$ROOT/deploy/helm/who-need-help" \
|
||||
>"$scan_dir/rendered-helm.yaml"
|
||||
mkdir -p "$ROOT/.tools/trivy-cache"
|
||||
docker run --rm \
|
||||
--volume "$scan_dir:/scan:ro" \
|
||||
--volume "$ROOT/.tools/trivy-cache:/root/.cache/trivy" \
|
||||
"$TRIVY_IMAGE" fs \
|
||||
--scanners misconfig,secret \
|
||||
--severity HIGH,CRITICAL \
|
||||
--exit-code 1 \
|
||||
/scan
|
||||
|
||||
echo "Building the pinned quality image and cached Dialyzer PLTs"
|
||||
docker build --target quality --tag "$quality_image" .
|
||||
|
||||
echo "Running Elixir format, compiler, xref, Credo, Sobelow, Dialyzer, and Hex audit"
|
||||
docker run --rm "$quality_image" sh -euc '
|
||||
mix format --check-formatted
|
||||
mix compile --force --warnings-as-errors
|
||||
mix xref graph --label compile-connected --fail-above 0
|
||||
mix credo --strict --min-priority high
|
||||
mix sobelow --exit --strict --private --skip
|
||||
mix dialyzer
|
||||
mix hex.audit
|
||||
'
|
||||
|
||||
echo "Starting an isolated PostgreSQL/PostGIS volume for the Phoenix suite"
|
||||
$compose up --detach --wait db
|
||||
docker run --rm \
|
||||
--network "${project}_internal" \
|
||||
--env MIX_ENV=test \
|
||||
--env DB_HOST=db \
|
||||
--env "DB_USER=$QUALITY_POSTGRES_USER" \
|
||||
--env "DB_PASSWORD=$QUALITY_POSTGRES_PASSWORD" \
|
||||
--env TEST_POOL_SIZE=10 \
|
||||
"$quality_image" \
|
||||
mix test
|
||||
|
||||
echo "Auditing locked browser dependencies"
|
||||
docker build --target node_deps --tag "$assets_image" .
|
||||
docker run --rm "$assets_image" npm audit --audit-level=high
|
||||
docker build --tag "$e2e_image" e2e
|
||||
docker run --rm "$e2e_image" npm audit --audit-level=high
|
||||
|
||||
echo "Building and scanning the production release image"
|
||||
docker build --target release --tag "$release_image" .
|
||||
docker run --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$ROOT/.tools/trivy-cache:/root/.cache/trivy" \
|
||||
"$TRIVY_IMAGE" image \
|
||||
--scanners vuln \
|
||||
--severity HIGH,CRITICAL \
|
||||
--ignore-unfixed \
|
||||
--exit-code 1 \
|
||||
"$release_image"
|
||||
|
||||
echo "All isolated quality and security gates passed."
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
|
|
@ -28,6 +28,7 @@ if [ ! -f "$ROOT/.env" ]; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ROOT/.env"
|
||||
set +a
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ set +a
|
|||
: "${POSTGRES_USER:?Set POSTGRES_USER in .env}"
|
||||
: "${DATABASE_URL:?Set DATABASE_URL in .env}"
|
||||
|
||||
dump_dir=$(CDPATH= cd -- "$(dirname -- "$dump")" && pwd)
|
||||
dump_dir=$(CDPATH='' cd -- "$(dirname -- "$dump")" && pwd)
|
||||
dump_name=$(basename -- "$dump")
|
||||
|
||||
(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
TEST_IMAGE="${TEST_IMAGE:-who-need-help:test}"
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ for command in docker openssl perl; do
|
|||
done
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
if [ ! -f "$ROOT/.env" ]; then
|
||||
|
|
@ -10,6 +10,7 @@ if [ ! -f "$ROOT/.env" ]; then
|
|||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ROOT/.env"
|
||||
set +a
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
MODE=${1:-compose}
|
||||
COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-who_need_help}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user