ci: require manual gated releases from main
All checks were successful
Build and deploy / Validate source (push) Successful in 1m28s
Build and deploy / Integration suite on a real stack (push) Successful in 4m3s
Build and deploy / Secret scan and release gate (push) Successful in 11s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
All checks were successful
Build and deploy / Validate source (push) Successful in 1m28s
Build and deploy / Integration suite on a real stack (push) Successful in 4m3s
Build and deploy / Secret scan and release gate (push) Successful in 11s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
This commit is contained in:
@@ -48,6 +48,9 @@ jobs:
|
|||||||
# by whoever follows them. The suites run inside the network, so it has to
|
# by whoever follows them. The suites run inside the network, so it has to
|
||||||
# be the service name, not a published port on the host.
|
# be the service name, not a published port on the host.
|
||||||
S3_PUBLIC_ENDPOINT: http://storage:9000
|
S3_PUBLIC_ENDPOINT: http://storage:9000
|
||||||
|
PUBLIC_ORIGIN: http://site
|
||||||
|
ALLOWED_HOSTS: localhost,127.0.0.1,site,kanban
|
||||||
|
ALLOWED_ORIGINS: http://site,http://kanban,http://localhost:28080,http://localhost:28081
|
||||||
COMPOSE: docker compose -f compose.local.yaml
|
COMPOSE: docker compose -f compose.local.yaml
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -70,7 +73,7 @@ jobs:
|
|||||||
# one a developer exercises on localhost.
|
# one a developer exercises on localhost.
|
||||||
- name: API and workflow regressions
|
- name: API and workflow regressions
|
||||||
run: |
|
run: |
|
||||||
for suite in smoke_test workflow_test security_test scanning_test payment_test; do
|
for suite in smoke_test workflow_test security_test scanning_test payment_test quote_pagination_test; do
|
||||||
echo "--- $suite"
|
echo "--- $suite"
|
||||||
$COMPOSE exec -T \
|
$COMPOSE exec -T \
|
||||||
-e SITE_BASE_URL=http://site \
|
-e SITE_BASE_URL=http://site \
|
||||||
@@ -83,35 +86,13 @@ jobs:
|
|||||||
$COMPOSE exec -T api python -m tests.retention_test
|
$COMPOSE exec -T api python -m tests.retention_test
|
||||||
$COMPOSE exec -T api python -m tests.runtime_security_test
|
$COMPOSE exec -T api python -m tests.runtime_security_test
|
||||||
|
|
||||||
# These need a real Chrome. They are the only coverage for the artwork
|
# Run Chrome on the Compose network. It must resolve the same storage:9000
|
||||||
# editor and the full customer journey, so install google-chrome-stable
|
# hostname used in presigned URLs, and absence of Chrome must fail CI.
|
||||||
# (or set CHROME_BIN) on the runner to make them gate deployments. The
|
|
||||||
# suites above stay hard gates either way.
|
|
||||||
- name: Browser regressions
|
- name: Browser regressions
|
||||||
run: |
|
run: |
|
||||||
for candidate in "$CHROME_BIN" /usr/bin/google-chrome-stable \
|
$COMPOSE build browser-tests
|
||||||
/usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser; do
|
$COMPOSE run --rm --no-deps browser-tests sh -ec \
|
||||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
'node tests/artwork_browser_test.mjs && node tests/browser_test.mjs'
|
||||||
export CHROME_BIN="$candidate"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ ! -x "${CHROME_BIN:-}" ]; then
|
|
||||||
echo "::warning::No Chrome on this runner; browser regressions were NOT run."
|
|
||||||
echo "Install google-chrome-stable or set CHROME_BIN to gate on them."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# Chrome runs here, in the runner container, and reaches the stack only
|
|
||||||
# through ports published on the host. When the runner is itself a
|
|
||||||
# container those are in another namespace, so check before running
|
|
||||||
# rather than failing with a bare connection error. See ROADMAP 5.10.
|
|
||||||
if ! wget -q -T 5 -O /dev/null "http://localhost:${SITE_PORT}/health"; then
|
|
||||||
echo "::warning::Stack not reachable from the runner; browser regressions were NOT run."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Using $CHROME_BIN"
|
|
||||||
node tests/artwork_browser_test.mjs
|
|
||||||
node tests/browser_test.mjs
|
|
||||||
|
|
||||||
- name: Diagnostics on failure
|
- name: Diagnostics on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
@@ -145,13 +126,9 @@ jobs:
|
|||||||
fs --scanners secret --exit-code 1 --severity HIGH,CRITICAL \
|
fs --scanners secret --exit-code 1 --severity HIGH,CRITICAL \
|
||||||
--no-progress /src
|
--no-progress /src
|
||||||
|
|
||||||
# docs/PORTAINER.md described this as blocking publication. It never ran at
|
# Keep push feedback advisory while the provider adapters are fake.
|
||||||
# all, and turning it on unconditionally would block every deploy: the
|
# The manual release job enforces the source preflight unconditionally.
|
||||||
# source preflight refuses a release while the payment and messaging
|
# ENFORCE_PRODUCTION_PREFLIGHT can make push checks fail on blockers too.
|
||||||
# adapters are fake, which is the deliberate state the stack runs in
|
|
||||||
# today. So its verdict is always printed, and enforcement is opt-in.
|
|
||||||
# Set the repository variable ENFORCE_PRODUCTION_PREFLIGHT to "true" once
|
|
||||||
# real adapters land, and this becomes the gate the documentation claims.
|
|
||||||
- name: Production source preflight
|
- name: Production source preflight
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
@@ -171,7 +148,7 @@ jobs:
|
|||||||
publish-and-deploy:
|
publish-and-deploy:
|
||||||
name: Publish images and notify Portainer
|
name: Publish images and notify Portainer
|
||||||
needs: [validate, integration, scan]
|
needs: [validate, integration, scan]
|
||||||
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
if: gitea.event_name == 'workflow_dispatch' && gitea.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
env:
|
env:
|
||||||
@@ -181,6 +158,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
- name: Require production readiness
|
||||||
|
env:
|
||||||
|
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
|
||||||
|
run: |
|
||||||
|
python3 deploy/production_preflight.py --source-only
|
||||||
|
test -n "$PORTAINER_WEBHOOK"
|
||||||
- name: Sign in to the Gitea Container Registry
|
- name: Sign in to the Gitea Container Registry
|
||||||
env:
|
env:
|
||||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
@@ -190,7 +173,7 @@ jobs:
|
|||||||
test -n "$REGISTRY_TOKEN"
|
test -n "$REGISTRY_TOKEN"
|
||||||
echo "$REGISTRY_TOKEN" | docker login gitea.blyzer.com.br \
|
echo "$REGISTRY_TOKEN" | docker login gitea.blyzer.com.br \
|
||||||
--username "$REGISTRY_USERNAME" --password-stdin
|
--username "$REGISTRY_USERNAME" --password-stdin
|
||||||
- name: Build and publish API
|
- name: Build API
|
||||||
run: |
|
run: |
|
||||||
image="gitea.blyzer.com.br/blyzer/dtf-api"
|
image="gitea.blyzer.com.br/blyzer/dtf-api"
|
||||||
# The Dockerfiles pin digests themselves; these variables let a base be
|
# The Dockerfiles pin digests themselves; these variables let a base be
|
||||||
@@ -201,9 +184,7 @@ jobs:
|
|||||||
docker build --file deploy/Dockerfile.api "$@" \
|
docker build --file deploy/Dockerfile.api "$@" \
|
||||||
--build-arg VCS_REF="${{ gitea.sha }}" \
|
--build-arg VCS_REF="${{ gitea.sha }}" \
|
||||||
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
|
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
|
||||||
docker push "$image:latest"
|
- name: Build web
|
||||||
docker push "$image:${{ gitea.sha }}"
|
|
||||||
- name: Build and publish web
|
|
||||||
run: |
|
run: |
|
||||||
image="gitea.blyzer.com.br/blyzer/dtf-web"
|
image="gitea.blyzer.com.br/blyzer/dtf-web"
|
||||||
set --
|
set --
|
||||||
@@ -212,8 +193,6 @@ jobs:
|
|||||||
docker build --file deploy/Dockerfile.web "$@" \
|
docker build --file deploy/Dockerfile.web "$@" \
|
||||||
--build-arg VCS_REF="${{ gitea.sha }}" \
|
--build-arg VCS_REF="${{ gitea.sha }}" \
|
||||||
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
|
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
|
||||||
docker push "$image:latest"
|
|
||||||
docker push "$image:${{ gitea.sha }}"
|
|
||||||
# CRITICAL blocks, HIGH is reported. Both images carry zero CRITICAL after
|
# CRITICAL blocks, HIGH is reported. Both images carry zero CRITICAL after
|
||||||
# the base pinning and OS upgrades, so this gate holds the line already
|
# the base pinning and OS upgrades, so this gate holds the line already
|
||||||
# reached. The remaining HIGH findings have no upstream fix, so failing on
|
# reached. The remaining HIGH findings have no upstream fix, so failing on
|
||||||
@@ -227,25 +206,29 @@ jobs:
|
|||||||
"gitea.blyzer.com.br/blyzer/dtf-web:${{ gitea.sha }}"; do
|
"gitea.blyzer.com.br/blyzer/dtf-web:${{ gitea.sha }}"; do
|
||||||
echo "--- $target (HIGH, reported)"
|
echo "--- $target (HIGH, reported)"
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
||||||
image --scanners vuln --severity HIGH --no-progress \
|
image --image-src docker --scanners vuln --severity HIGH --no-progress \
|
||||||
--format table --exit-code 0 "$target" ||
|
--format table --exit-code 0 "$target" ||
|
||||||
echo "::warning::Could not scan $target for HIGH findings"
|
echo "::warning::Could not scan $target for HIGH findings"
|
||||||
echo "--- $target (CRITICAL, blocking)"
|
echo "--- $target (CRITICAL, blocking)"
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \
|
||||||
image --scanners vuln --severity CRITICAL --no-progress \
|
image --image-src docker --scanners vuln --severity CRITICAL --no-progress \
|
||||||
--format table --exit-code 1 "$target" || failed=1
|
--format table --exit-code 1 "$target" || failed=1
|
||||||
done
|
done
|
||||||
if [ "$failed" -ne 0 ]; then
|
if [ "$failed" -ne 0 ]; then
|
||||||
echo "::error::A CRITICAL vulnerability was found in a published image."
|
echo "::error::A CRITICAL vulnerability was found in a release image."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Publish validated images
|
||||||
|
run: |
|
||||||
|
for name in dtf-api dtf-web; do
|
||||||
|
image="gitea.blyzer.com.br/blyzer/$name"
|
||||||
|
docker push "$image:${{ gitea.sha }}"
|
||||||
|
docker push "$image:latest"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Trigger Portainer redeployment
|
- name: Trigger Portainer redeployment
|
||||||
env:
|
env:
|
||||||
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
|
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$PORTAINER_WEBHOOK" ]; then
|
|
||||||
echo "PORTAINER_WEBHOOK is not configured; images were published but deployment was skipped."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
curl --fail --silent --show-error --max-time 30 --request POST "$PORTAINER_WEBHOOK"
|
curl --fail --silent --show-error --max-time 30 --request POST "$PORTAINER_WEBHOOK"
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ x-app: &app
|
|||||||
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only}
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only}
|
||||||
# The browser reaches the API through the Site gateway, so the published
|
# The browser reaches the API through the Site gateway, so the published
|
||||||
# Site/Kanban origins must be accepted or every write is rejected 403.
|
# Site/Kanban origins must be accepted or every write is rejected 403.
|
||||||
PUBLIC_ORIGIN: http://localhost:${SITE_PORT:-8080}
|
PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:-http://localhost:${SITE_PORT:-8080}}
|
||||||
ALLOWED_HOSTS: localhost,127.0.0.1
|
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1}
|
||||||
ALLOWED_ORIGINS: http://localhost:${SITE_PORT:-8080},http://localhost:${KANBAN_PORT:-8081},http://127.0.0.1:${SITE_PORT:-8080},http://127.0.0.1:${KANBAN_PORT:-8081}
|
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:${SITE_PORT:-8080},http://localhost:${KANBAN_PORT:-8081},http://127.0.0.1:${SITE_PORT:-8080},http://127.0.0.1:${KANBAN_PORT:-8081}}
|
||||||
COOKIE_SECURE: "false"
|
COOKIE_SECURE: "false"
|
||||||
PAYMENT_ADAPTER: fake
|
PAYMENT_ADAPTER: fake
|
||||||
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-local-webhook-secret}
|
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-local-webhook-secret}
|
||||||
@@ -208,6 +208,28 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 12
|
retries: 12
|
||||||
|
|
||||||
|
browser-tests:
|
||||||
|
profiles: [ci]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: infra/Dockerfile.browser-tests
|
||||||
|
environment:
|
||||||
|
CHROME_BIN: /usr/bin/chromium
|
||||||
|
CHROME_NO_SANDBOX: "1"
|
||||||
|
CHROME_TRUST_TEST_ORIGINS: "1"
|
||||||
|
SITE_BROWSER_ORIGIN: http://site
|
||||||
|
KANBAN_BROWSER_ORIGIN: http://kanban
|
||||||
|
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-operator@example.test}
|
||||||
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only}
|
||||||
|
shm_size: 1gb
|
||||||
|
networks: [local]
|
||||||
|
depends_on:
|
||||||
|
site: {condition: service_healthy}
|
||||||
|
kanban: {condition: service_healthy}
|
||||||
|
storage: {condition: service_healthy}
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
cap_drop: [ALL]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
storage-data:
|
storage-data:
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .production_preflight import config_errors, source_errors
|
from .production_preflight import config_errors, source_errors
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseWorkflowTests(unittest.TestCase):
|
||||||
|
def test_main_push_cannot_publish_and_manual_release_is_gated(self):
|
||||||
|
workflow = (Path(__file__).resolve().parents[1] /
|
||||||
|
'.gitea/workflows/deploy.yml').read_text()
|
||||||
|
release = workflow.split(' publish-and-deploy:\n', 1)[1]
|
||||||
|
self.assertIn("if: gitea.event_name == 'workflow_dispatch' && "
|
||||||
|
"gitea.ref == 'refs/heads/main'", release)
|
||||||
|
preflight = release.index('python3 deploy/production_preflight.py --source-only')
|
||||||
|
webhook = release.index('test -n "$PORTAINER_WEBHOOK"')
|
||||||
|
scan = release.index('- name: Image vulnerabilities')
|
||||||
|
publish = release.index('- name: Publish validated images')
|
||||||
|
redeploy = release.index('- name: Trigger Portainer redeployment')
|
||||||
|
self.assertLess(preflight, scan)
|
||||||
|
self.assertLess(webhook, scan)
|
||||||
|
self.assertLess(scan, publish)
|
||||||
|
self.assertLess(publish, redeploy)
|
||||||
|
|
||||||
|
|
||||||
def valid_config():
|
def valid_config():
|
||||||
digest = '1' * 64
|
digest = '1' * 64
|
||||||
values = {
|
values = {
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ Cloudflare R2, so MinIO is not part of this stack.
|
|||||||
|
|
||||||
The single workflow is `.gitea/workflows/deploy.yml`. Every push and pull request
|
The single workflow is `.gitea/workflows/deploy.yml`. Every push and pull request
|
||||||
runs static validation, the integration suite against a real stack, and a secret
|
runs static validation, the integration suite against a real stack, and a secret
|
||||||
scan. A push to `main` then builds the production images, publishes both `latest`
|
scan. A push to `main` does not publish or deploy. A manual workflow run on
|
||||||
and the full commit SHA, reports their vulnerabilities, and calls Portainer.
|
`main` repeats those checks, builds and scans the images, then publishes both
|
||||||
|
`latest` and the full commit SHA and calls Portainer.
|
||||||
|
|
||||||
What actually gates a deployment:
|
What actually gates a deployment:
|
||||||
|
|
||||||
@@ -26,17 +27,19 @@ What actually gates a deployment:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `py_compile` and the unit tests | yes |
|
| `py_compile` and the unit tests | yes |
|
||||||
| Integration suite on a live stack (smoke, workflow, security, scanning, retention, runtime) | yes |
|
| Integration suite on a live stack (smoke, workflow, security, scanning, retention, runtime) | yes |
|
||||||
| Browser suites | only when the runner has Chrome; otherwise warns and continues |
|
| Browser suites | yes; Chrome runs in the Compose test container |
|
||||||
| Trivy secret scan (HIGH/CRITICAL) | yes |
|
| Trivy secret scan (HIGH/CRITICAL) | yes |
|
||||||
| Source preflight (`deploy/production_preflight.py --source-only`) | only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true` |
|
| Source preflight (`deploy/production_preflight.py --source-only`) | yes for manual release; advisory on pushes unless `ENFORCE_PRODUCTION_PREFLIGHT` is `true` |
|
||||||
| Trivy image vulnerabilities, CRITICAL | yes |
|
| Trivy image vulnerabilities, CRITICAL | yes |
|
||||||
| Trivy image vulnerabilities, HIGH | no — reported after the build |
|
| Trivy image vulnerabilities, HIGH | no — reported before publication |
|
||||||
|
| Configured Portainer webhook | yes for manual release |
|
||||||
|
|
||||||
The source preflight is advisory by default because it refuses a release while
|
The source preflight refuses a release while the payment and messaging adapters
|
||||||
the payment and messaging adapters are fake, which is the deliberate state the
|
are fake. It remains advisory on push checks so development can continue, but a
|
||||||
stack runs in today. Enforcing it now would block every deployment. Set the
|
manual release is blocked until those adapters are replaced. Set the repository
|
||||||
repository variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` once real adapters
|
variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` when all pushes should also
|
||||||
land, and it becomes a hard gate.
|
fail on those blockers. Before a manual release, run the full configuration
|
||||||
|
preflight below against the actual Portainer values; CI checks source only.
|
||||||
|
|
||||||
CRITICAL image findings block. Both images carry none: the bases are pinned by
|
CRITICAL image findings block. Both images carry none: the bases are pinned by
|
||||||
digest, both Dockerfiles upgrade their OS packages, and the web image moved off
|
digest, both Dockerfiles upgrade their OS packages, and the web image moved off
|
||||||
@@ -60,10 +63,10 @@ Repository secrets:
|
|||||||
- `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials.
|
- `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials.
|
||||||
- `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack.
|
- `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack.
|
||||||
|
|
||||||
The webhook is called only after the gating checks in the table above pass.
|
The webhook is called only after the release gates above pass.
|
||||||
`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository
|
`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository
|
||||||
variables; without them the preflight is advisory and a pinned default scanner
|
variables; the former affects push checks and the latter defaults to a pinned
|
||||||
image is used.
|
scanner image.
|
||||||
|
|
||||||
## 2. One-time Portainer resources
|
## 2. One-time Portainer resources
|
||||||
|
|
||||||
@@ -124,10 +127,13 @@ The `db-init` service completing and stopping is expected. The other six
|
|||||||
services must be healthy. A failed `db-init` task or an unhealthy service blocks
|
services must be healthy. A failed `db-init` task or an unhealthy service blocks
|
||||||
acceptance.
|
acceptance.
|
||||||
|
|
||||||
## 4. Normal deployment
|
## 4. Manual deployment
|
||||||
|
|
||||||
Push to `main`. Gitea validates, tests, scans, publishes these images, and calls
|
Push the reviewed commit to `main` and wait for its validation workflow to pass.
|
||||||
the webhook:
|
After validating the actual Portainer configuration with the full preflight in
|
||||||
|
section 3, use Gitea Actions to manually run **Build and deploy** on `main` at
|
||||||
|
that commit. The workflow repeats validation, tests and scans, then publishes
|
||||||
|
these images and calls the webhook:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
gitea.blyzer.com.br/blyzer/dtf-api:latest
|
gitea.blyzer.com.br/blyzer/dtf-api:latest
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ kept through the approved order; 4.6 now pages pending and approved unpaid
|
|||||||
quotes, including a tested 101st pending quote. Operational entrypoints in
|
quotes, including a tested 101st pending quote. Operational entrypoints in
|
||||||
5.12 are repaired and locally exercised. Image decoding, mixed-sheet grading,
|
5.12 are repaired and locally exercised. Image decoding, mixed-sheet grading,
|
||||||
rotation-sensitive DPI, and PDF page geometry are corrected in 3.9/4.4.
|
rotation-sensitive DPI, and PDF page geometry are corrected in 3.9/4.4.
|
||||||
|
`main` pushes now validate without publishing; manual release requires a passing
|
||||||
|
source preflight. The containerized browser gate passes locally, pending a Gitea
|
||||||
|
runner run.
|
||||||
Next address the upload/scanner safety gate and unsupported PDF image evidence.
|
Next address the upload/scanner safety gate and unsupported PDF image evidence.
|
||||||
The customer/API upload admission now stops above the scanner's effective limit
|
The customer/API upload admission now stops above the scanner's effective limit
|
||||||
before transfer; the 5 GiB large-file product path still needs agreement and
|
before transfer; the 5 GiB large-file product path still needs agreement and
|
||||||
@@ -598,14 +601,9 @@ print-file evidence still need correction before this item can close.
|
|||||||
integration job should run `down -v` before `up` — or a dedicated step should
|
integration job should run `down -v` before `up` — or a dedicated step should
|
||||||
apply `schema.sql` twice to a fresh database, proving both a first install and
|
apply `schema.sql` twice to a fresh database, proving both a first install and
|
||||||
a re-run.
|
a re-run.
|
||||||
- `[ ]` 5.10 — The browser suites do not run in CI. Chrome runs in the runner
|
- `[ ]` 5.10 — The browser suites were moved into a Chrome container on the
|
||||||
container and can only reach the stack through ports published on the host, which
|
Compose network and made required in CI. Confirm the complete checkout journey
|
||||||
is a different network namespace when the runner is itself a container. The API,
|
passes in that topology and on the actual Gitea runner before closing this item.
|
||||||
workflow, security, scanning, retention and runtime suites were moved inside the
|
|
||||||
stack's network and do gate. The browser suites are the only coverage for the
|
|
||||||
artwork editor and the full customer journey, so they need either Chrome in a
|
|
||||||
container on that network, or a runner with host networking. Until then they gate
|
|
||||||
locally only, and CI warns when it skips them.
|
|
||||||
- `[ ]` 5.9 — `local/browser_test.mjs` failed once and passed on an immediate
|
- `[ ]` 5.9 — `local/browser_test.mjs` failed once and passed on an immediate
|
||||||
re-run, with no code change in between (2026-09-21). It is a deploy gate when the
|
re-run, with no code change in between (2026-09-21). It is a deploy gate when the
|
||||||
runner has Chrome, so an intermittent failure there blocks releases for no reason.
|
runner has Chrome, so an intermittent failure there blocks releases for no reason.
|
||||||
@@ -627,10 +625,12 @@ print-file evidence still need correction before this item can close.
|
|||||||
- `[ ]` 5.13 — Define production recovery: scheduled encrypted offsite database
|
- `[ ]` 5.13 — Define production recovery: scheduled encrypted offsite database
|
||||||
and object backups, a consistent snapshot boundary, Swarm data placement and
|
and object backups, a consistent snapshot boundary, Swarm data placement and
|
||||||
a restore rehearsal that opens every required live order file.
|
a restore rehearsal that opens every required live order file.
|
||||||
- `[ ]` 5.14 — Promote and verify one immutable release. Scan before publishing
|
- `[ ]` 5.14 — Promote and verify one immutable release. Normal `main` pushes
|
||||||
mutable tags, make the source preflight validate the active stack, require the
|
now run checks only; manual dispatch requires source preflight and a configured
|
||||||
browser tests to run, test clean install and upgrade, and check application
|
webhook, and scans images before publishing. Still make the full preflight
|
||||||
readiness after Portainer redeploys. Isolate concurrent CI stacks.
|
validate the active stack, deploy the tested immutable image references, test
|
||||||
|
clean install and upgrade, check application readiness after Portainer
|
||||||
|
redeploys, and isolate concurrent CI stacks.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -132,15 +132,12 @@ change when the advisory database or selected base digest changes.
|
|||||||
The files in `deploy/` and `.gitea/workflows/` are a guarded delivery mechanism,
|
The files in `deploy/` and `.gitea/workflows/` are a guarded delivery mechanism,
|
||||||
not an approval to operate the current application on the public internet.
|
not an approval to operate the current application on the public internet.
|
||||||
|
|
||||||
Corrected 2026-09-21: an earlier version of this section described gates the
|
Updated 2026-09-23: pushes to `main` run checks only. A manual workflow run on
|
||||||
workflow did not contain. The workflow now runs static validation, the
|
`main` requires the source preflight and a configured Portainer webhook before
|
||||||
integration suite against a live stack, and a blocking Trivy secret scan before
|
building. It scans built images before publication; CRITICAL findings block and
|
||||||
publishing. The source preflight is advisory unless
|
HIGH findings are reported. The browser suites run in a required Compose Chrome
|
||||||
`ENFORCE_PRODUCTION_PREFLIGHT` is set, and image vulnerabilities are reported
|
container. `PORTAINER.md` holds the authoritative gate table. A permitted release
|
||||||
rather than enforced, because the current bases carry HIGH/CRITICAL findings
|
publishes both `latest` and the full commit SHA, then calls the Portainer
|
||||||
with no upstream fix. Base images are still mutable tags, not digests.
|
|
||||||
`PORTAINER.md` holds the authoritative table of what gates and what does not.
|
|
||||||
It publishes both `latest` and the full commit SHA, then calls the Portainer
|
|
||||||
webhook. Application/provider secrets are created directly as versioned external
|
webhook. Application/provider secrets are created directly as versioned external
|
||||||
Swarm secrets and never cross the workflow. Rollback selects the prior commit SHA
|
Swarm secrets and never cross the workflow. Rollback selects the prior commit SHA
|
||||||
in Portainer and does not roll back the database.
|
in Portainer and does not roll back the database.
|
||||||
|
|||||||
13
infra/Dockerfile.browser-tests
Normal file
13
infra/Dockerfile.browser-tests
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends chromium ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
COPY tests /workspace/tests
|
||||||
|
COPY web /workspace/web
|
||||||
|
RUN mkdir -p /workspace/output/local \
|
||||||
|
&& chown -R node:node /workspace/output
|
||||||
|
USER node
|
||||||
|
ENV CHROME_BIN=/usr/bin/chromium
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost site kanban;
|
||||||
if ($host !~ ^(localhost|127\.0\.0\.1)$) { return 400; }
|
if ($host !~ ^(localhost|127\.0\.0\.1|site|kanban)$) { return 400; }
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index ${WEB_INDEX};
|
index ${WEB_INDEX};
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
@@ -23,8 +23,8 @@ server {
|
|||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 81;
|
listen 81;
|
||||||
server_name localhost;
|
server_name localhost site kanban;
|
||||||
if ($host !~ ^(localhost|127\.0\.0\.1)$) { return 400; }
|
if ($host !~ ^(localhost|127\.0\.0\.1|site|kanban)$) { return 400; }
|
||||||
client_max_body_size 2m;
|
client_max_body_size 2m;
|
||||||
location / {
|
location / {
|
||||||
limit_req zone=api_limit burst=100 nodelay;
|
limit_req zone=api_limit burst=100 nodelay;
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const profile=await mkdtemp(tmpdir()+'/dtf-browser-');
|
|||||||
const chrome=spawn(process.env.CHROME_BIN||'/usr/bin/google-chrome-stable',[
|
const chrome=spawn(process.env.CHROME_BIN||'/usr/bin/google-chrome-stable',[
|
||||||
'--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check',
|
'--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check',
|
||||||
...(process.env.CHROME_NO_SANDBOX==='1'?['--no-sandbox']:[]),
|
...(process.env.CHROME_NO_SANDBOX==='1'?['--no-sandbox']:[]),
|
||||||
|
// Production uses HTTPS; internal Compose HTTP names need a secure context
|
||||||
|
// for Web Crypto during the upload and checkout journey.
|
||||||
|
...(process.env.CHROME_TRUST_TEST_ORIGINS==='1'
|
||||||
|
? ['--unsafely-treat-insecure-origin-as-secure=http://site,http://kanban'] : []),
|
||||||
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
|
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
|
||||||
],{stdio:['ignore','ignore','pipe']});
|
],{stdio:['ignore','ignore','pipe']});
|
||||||
const pause=ms=>new Promise(r=>setTimeout(r,ms));
|
const pause=ms=>new Promise(r=>setTimeout(r,ms));
|
||||||
|
|||||||
Reference in New Issue
Block a user