Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ permissions:

jobs:
test:
runs-on: ubuntu-latest
# Release Sentinel requires the authentic Actions check named CI.
name: CI
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out exact source
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Verify candidate identity
env:
EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
- name: Configure Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: pip
Expand All @@ -37,9 +46,9 @@ jobs:
run: python scripts/p7_acceptance.py
- name: Upload verification evidence
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: certification-forge-evidence-${{ github.sha }}
name: certification-forge-evidence-${{ github.event.pull_request.head.sha || github.sha }}
path: artifacts/
if-no-files-found: error
retention-days: 30
42 changes: 42 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: certification-forge-codeql

"on":
push:
branches:
- main
- feat/p5-adapter-execution
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
analyze:
name: CodeQL
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
security-events: write
steps:
- name: Check out exact source
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Verify candidate identity
env:
EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
- name: Initialize Python analysis
uses: github/codeql-action/init@v4
with:
languages: python
build-mode: none
- name: Analyze and upload results
uses: github/codeql-action/analyze@v4
with:
category: /language:python
ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
sha: ${{ github.event.pull_request.head.sha || github.sha }}
30 changes: 30 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Deployment readiness

`deploy_forge.sh` uses `readiness.sh` for staging, production, and rollback
health polling. Keep the two files together when invoking the deployment script.

`CERTFORGE_READY_TIMEOUT_S` defaults to `90` and accepts a decimal integer from
`1` through `1800`, without leading zeroes. Invalid values fail before locking,
fetching, or changing deployment state. Each wait has a fresh elapsed-time
deadline; failed HTTP probes consume that budget. Each connection/response is
limited to the smaller of two seconds and the remaining budget. A healthy probe
returns immediately. Shell clock granularity and the polling pause can add a
subsecond tolerance; service-manager and listener-ownership commands are separate
from the HTTP budget.

Staging failures retain the service log under
`$STATE_ROOT/deploy-logs/staging-<release>.log`. Listener ownership, signed
readiness verification, live-smoke gates, and rollback requirements still apply.

# Source validation and release evidence

The `CI` Actions job runs the full deterministic suite with coverage and P1, P6,
and P7 verification. The `CodeQL` job performs Python security analysis and uploads
its results. Both check out the immutable PR head (or event SHA on push), verify
that checkout, and bind their output to it. Their names match Release Sentinel's
required authentic checks; neither is a substitute status or a release verdict.

A green source-validation run does not prove current production identity or
provide signed production E2E evidence. Every changed candidate needs fresh
exact-SHA certification. Historical smoke reports and old certified tags must not
be reused as certification for a new commit.
37 changes: 26 additions & 11 deletions deploy/deploy_forge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ CURRENT_LINK="${CERTFORGE_CURRENT_LINK:-/home/forge/echo-certification-forge-cur
STATE_ROOT="${CERTFORGE_STATE_ROOT:-/home/forge/echo-certification-forge/var}"
PRODUCTION_E2E_ATTESTATION_DIR="${ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION_DIR:-$STATE_ROOT/production-e2e/attestations}"
PRODUCTION_E2E_TRUSTED_KEYS="${ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS:-$STATE_ROOT/production-e2e/trusted-public-keys}"
# Readiness waits bound /healthz probes and return on the first healthy probe.
# 20s proved too short for a cold uvicorn boot on a loaded FORGE (staging abort,
# false rollback-restore failure, needless production rollback).
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/readiness.sh"
configure_readiness_timeout "${CERTFORGE_READY_TIMEOUT_S:-90}"
ADAPTER_DIR="${ECHO_CERTFORGE_PROD_ADAPTER_DIR:-$STATE_ROOT/p5}"
ADAPTER_MODE="${CERTFORGE_ADAPTER_MODE:-required}"
# Canonical-JSON digest of policies/mandatory-rules.v2.json (== run_worker._PRODUCTION_MANIFEST_SHA256).
Expand Down Expand Up @@ -265,13 +270,16 @@ ECHO_CERTFORGE_API_KEY_PEPPER="$STAGING_PEPPER" \
STAGING_PID=$!

ready=0
for _ in $(seq 1 40); do
readiness_start
while readiness_pending; do
kill -0 "$STAGING_PID" 2>/dev/null || {
echo "!! staging process exited before readiness"
tail -20 "$STAGING_ROOT/service.log"
mkdir -p "$STATE_ROOT/deploy-logs" &&
cp -f "$STAGING_ROOT/service.log" "$STATE_ROOT/deploy-logs/staging-$RELEASE_ID.log" 2>/dev/null || true
tail -40 "$STAGING_ROOT/service.log"
exit 1
}
curl -sf "http://127.0.0.1:$STAGING_PORT/healthz" >/dev/null 2>&1 && {
readiness_probe "$STAGING_PORT" >/dev/null 2>&1 && {
if ss -H -ltnp "sport = :$STAGING_PORT" | grep -q "pid=$STAGING_PID,"; then
ready=1
else
Expand All @@ -280,11 +288,16 @@ for _ in $(seq 1 40); do
fi
break
}
sleep 0.5
readiness_pause
done
if [ "$ready" != 1 ]; then
echo "!! staging never became healthy"
tail -20 "$STAGING_ROOT/service.log"
echo " waited ${READY_TIMEOUT_S}s; set CERTFORGE_READY_TIMEOUT_S to adjust"
curl -sS -m 5 -o /dev/null -w " /healthz probe: HTTP %{http_code}\n" \
"http://127.0.0.1:$STAGING_PORT/healthz" 2>&1 || true
mkdir -p "$STATE_ROOT/deploy-logs" &&
cp -f "$STAGING_ROOT/service.log" "$STATE_ROOT/deploy-logs/staging-$RELEASE_ID.log" 2>/dev/null || true
tail -40 "$STAGING_ROOT/service.log"
exit 1
fi

Expand Down Expand Up @@ -427,13 +440,14 @@ rollback_production() {
if [ "$PREV_ACTIVE" = "active" ]; then
sudo systemctl start "$SERVICE.service" || rollback_status=1
restored=0
for _ in $(seq 1 40); do
readiness_start
while readiness_pending; do
service_owns_port "$PROD_PORT" &&
curl -sf "http://127.0.0.1:$PROD_PORT/healthz" >/dev/null 2>&1 && {
readiness_probe "$PROD_PORT" >/dev/null 2>&1 && {
restored=1
break
}
sleep 0.5
readiness_pause
done
[ "$restored" = 1 ] || rollback_status=1
else
Expand Down Expand Up @@ -634,13 +648,14 @@ sudo systemctl enable "$DISPATCH_SERVICE.service"

echo "== [8/9] production health + live-smoke =="
ready=0
for _ in $(seq 1 40); do
readiness_start
while readiness_pending; do
service_owns_port "$PROD_PORT" &&
curl -sf "http://127.0.0.1:$PROD_PORT/healthz" >/dev/null 2>&1 && {
readiness_probe "$PROD_PORT" >/dev/null 2>&1 && {
ready=1
break
}
sleep 0.5
readiness_pause
done
if [ "$ready" != 1 ] || ! ECHO_CERTFORGE_DB="$DB_PATH" \
ECHO_CERTFORGE_SUBSCRIBER_POLICY="$RELEASE_DIR/policies/subscriber-governance.v1.json" \
Expand Down
40 changes: 40 additions & 0 deletions deploy/readiness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Sourced by deploy_forge.sh; defining these helpers has no side effects.

configure_readiness_timeout() {
local value="$1"
# Validate before arithmetic: Bash otherwise accepts expressions and overflows.
if [[ ! "$value" =~ ^[1-9][0-9]{0,3}$ ]] || (( value > 1800 )); then
echo "!! CERTFORGE_READY_TIMEOUT_S must be an integer between 1 and 1800" >&2
return 1
fi
READY_TIMEOUT_S="$value"
}

readiness_start() {
READY_DEADLINE=$(( SECONDS + READY_TIMEOUT_S ))
}

readiness_pending() {
(( SECONDS < READY_DEADLINE ))
}

readiness_probe() {
local port="$1"
local remaining=$(( READY_DEADLINE - SECONDS ))
(( remaining > 0 )) || return 1
# Each probe consumes the existing deadline, never a fresh total budget.
# Small individual probes allow the caller to recheck process ownership.
local probe_timeout="$remaining"
(( probe_timeout <= 2 )) || probe_timeout=2
curl -sf "http://127.0.0.1:$port/healthz" \
--connect-timeout "$probe_timeout" --max-time "$probe_timeout"
}

readiness_pause() {
# SECONDS has one-second resolution; the polling pause adds at most 0.5s.
# Callers retain their process ownership checks outside these HTTP helpers.
if readiness_pending; then
sleep 0.5
fi
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.12"
license = {text = "Proprietary"}
authors = [{name = "ECHO-OMEGA-PRIME", email = "bobbymcwilliams@echo-op.com"}]
dependencies = [
"cryptography>=44.0.0",
"cryptography>=50.0.0",
"fastapi>=0.115.0",
"pydantic>=2.10.0",
"uvicorn>=0.34.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_deploy_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def test_deploy_gate_rejects_dead_staging_process() -> None:
)
process_check = DEPLOY_SCRIPT.index('kill -0 "$STAGING_PID"')
health_check = DEPLOY_SCRIPT.index(
'curl -sf "http://127.0.0.1:$STAGING_PORT/healthz"'
'readiness_probe "$STAGING_PORT"'
)
ownership_check = DEPLOY_SCRIPT.index(
'grep -q "pid=$STAGING_PID,"'
Expand Down
Loading