Skip to content
Merged
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
159 changes: 159 additions & 0 deletions .github/workflows/azure-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Deploy reviewed Azure portfolio revision

on:
workflow_dispatch:
inputs:
release_id:
description: 'Existing release-evidence identifier, for example v0.14.0-demo.1'
required: true
type: string

# OIDC is used instead of an Azure client secret. This workflow has no cloud
# credentials until the repository owner configures the azure-demo environment.
permissions:
contents: read
id-token: write

concurrency:
group: azure-demo
cancel-in-progress: false

jobs:
deploy:
name: Deploy reviewed single-instance revision
runs-on: ubuntu-latest
environment: azure-demo
env:
AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }}
AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
AZURE_CONTAINERAPP_NAME: ${{ vars.AZURE_CONTAINERAPP_NAME }}
AZURE_MANAGED_ENVIRONMENT_ID: ${{ vars.AZURE_MANAGED_ENVIRONMENT_ID }}
GATEWAY_IMAGE_DIGEST: ${{ vars.GATEWAY_IMAGE_DIGEST }}
FRONTEND_IMAGE_DIGEST: ${{ vars.FRONTEND_IMAGE_DIGEST }}
BACKEND_IMAGE_DIGEST: ${{ vars.BACKEND_IMAGE_DIGEST }}
STORAGE_BUCKET_NAME: ${{ vars.STORAGE_BUCKET_NAME }}
STORAGE_SERVICE_URL: ${{ vars.STORAGE_SERVICE_URL }}
STORAGE_REGION: ${{ vars.STORAGE_REGION }}
RECAPTCHA_SITE_KEY: ${{ vars.RECAPTCHA_SITE_KEY }}
EMAIL_FROM_NAME: ${{ vars.EMAIL_FROM_NAME }}
DATABASE_CONNECTION_STRING: ${{ secrets.DATABASE_CONNECTION_STRING }}
MIGRATION_DATABASE_CONNECTION_STRING: ${{ secrets.MIGRATION_DATABASE_CONNECTION_STRING }}
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
LOBBY_CREDENTIAL_KEY: ${{ secrets.LOBBY_CREDENTIAL_KEY }}
RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
EMAIL_FROM: ${{ secrets.EMAIL_FROM }}
EMAIL_SMTP_HOST: ${{ secrets.EMAIL_SMTP_HOST }}
EMAIL_SMTP_USERNAME: ${{ secrets.EMAIL_SMTP_USERNAME }}
EMAIL_SMTP_PASSWORD: ${{ secrets.EMAIL_SMTP_PASSWORD }}
STORAGE_ACCESS_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
APP_ORIGIN: ${{ secrets.APP_ORIGIN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Validate deployment inputs are release digests
shell: bash
run: |
set -euo pipefail
for image in "$GATEWAY_IMAGE_DIGEST" "$FRONTEND_IMAGE_DIGEST" "$BACKEND_IMAGE_DIGEST"; do
[[ "$image" == *@sha256:* ]] || { echo 'Every image must be image@sha256:digest.'; exit 1; }
done
[[ -n "$AZURE_RESOURCE_GROUP" && -n "$AZURE_CONTAINERAPP_NAME" && -n "$AZURE_MANAGED_ENVIRONMENT_ID" ]]

- name: Sign in to Azure using GitHub OIDC
uses: azure/login@eec3c95657c1536435858eda1f3ff5437fee8474 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Preview Azure changes
shell: bash
run: |
set -euo pipefail
az deployment group what-if \
--resource-group "$AZURE_RESOURCE_GROUP" \
--template-file infra/azure/main.bicep \
--parameters \
location="$AZURE_LOCATION" \
containerAppName="$AZURE_CONTAINERAPP_NAME" \
managedEnvironmentId="$AZURE_MANAGED_ENVIRONMENT_ID" \
gatewayImage="$GATEWAY_IMAGE_DIGEST" \
frontendImage="$FRONTEND_IMAGE_DIGEST" \
backendImage="$BACKEND_IMAGE_DIGEST" \
databaseConnectionString="$DATABASE_CONNECTION_STRING" \
migrationDatabaseConnectionString="$MIGRATION_DATABASE_CONNECTION_STRING" \
jwtSecretKey="$JWT_SECRET_KEY" \
lobbyCredentialKey="$LOBBY_CREDENTIAL_KEY" \
recaptchaSecretKey="$RECAPTCHA_SECRET_KEY" \
googleClientId="$GOOGLE_CLIENT_ID" \
emailFrom="$EMAIL_FROM" \
emailSmtpHost="$EMAIL_SMTP_HOST" \
emailSmtpUsername="$EMAIL_SMTP_USERNAME" \
emailSmtpPassword="$EMAIL_SMTP_PASSWORD" \
storageAccessKey="$STORAGE_ACCESS_KEY" \
storageSecretKey="$STORAGE_SECRET_KEY" \
appOrigin="$APP_ORIGIN" \
storageBucketName="$STORAGE_BUCKET_NAME" \
storageServiceUrl="$STORAGE_SERVICE_URL" \
storageRegion="$STORAGE_REGION" \
recaptchaSiteKey="$RECAPTCHA_SITE_KEY" \
emailFromName="$EMAIL_FROM_NAME"

- name: Deploy the reviewed revision
shell: bash
run: |
set -euo pipefail
az deployment group create \
--name "${{ inputs.release_id }}-${GITHUB_RUN_ID}" \
--resource-group "$AZURE_RESOURCE_GROUP" \
--template-file infra/azure/main.bicep \
--parameters \
location="$AZURE_LOCATION" \
containerAppName="$AZURE_CONTAINERAPP_NAME" \
managedEnvironmentId="$AZURE_MANAGED_ENVIRONMENT_ID" \
gatewayImage="$GATEWAY_IMAGE_DIGEST" \
frontendImage="$FRONTEND_IMAGE_DIGEST" \
backendImage="$BACKEND_IMAGE_DIGEST" \
databaseConnectionString="$DATABASE_CONNECTION_STRING" \
migrationDatabaseConnectionString="$MIGRATION_DATABASE_CONNECTION_STRING" \
jwtSecretKey="$JWT_SECRET_KEY" \
lobbyCredentialKey="$LOBBY_CREDENTIAL_KEY" \
recaptchaSecretKey="$RECAPTCHA_SECRET_KEY" \
googleClientId="$GOOGLE_CLIENT_ID" \
emailFrom="$EMAIL_FROM" \
emailSmtpHost="$EMAIL_SMTP_HOST" \
emailSmtpUsername="$EMAIL_SMTP_USERNAME" \
emailSmtpPassword="$EMAIL_SMTP_PASSWORD" \
storageAccessKey="$STORAGE_ACCESS_KEY" \
storageSecretKey="$STORAGE_SECRET_KEY" \
appOrigin="$APP_ORIGIN" \
storageBucketName="$STORAGE_BUCKET_NAME" \
storageServiceUrl="$STORAGE_SERVICE_URL" \
storageRegion="$STORAGE_REGION" \
recaptchaSiteKey="$RECAPTCHA_SITE_KEY" \
emailFromName="$EMAIL_FROM_NAME" \
--query properties.outputs -o json > deployment-outputs.json

- name: Verify public readiness through Caddy
shell: bash
run: |
set -euo pipefail
origin=$(jq -r '.publicOrigin.value' deployment-outputs.json)
for attempt in {1..36}; do
if curl --fail --silent --show-error "$origin/health/ready" | jq -e '.status == "healthy"' >/dev/null; then
echo "public_origin=$origin" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 10
done
echo "Readiness failed for $origin" >&2
exit 1

- name: Publish safe deployment summary
shell: bash
run: |
echo '### Azure portfolio deployment' >> "$GITHUB_STEP_SUMMARY"
echo "Release evidence ID: \`${{ inputs.release_id }}\`" >> "$GITHUB_STEP_SUMMARY"
jq -r '.publicOrigin.value, .deployedBackendImage.value, .deployedFrontendImage.value, .gatewayImageDigest.value' deployment-outputs.json | sed 's/^/- `/' | sed 's/$/`/' >> "$GITHUB_STEP_SUMMARY"
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
push:
branches: [main, "feature/**"]
pull_request:
branches: [main]

# This is the status check required by the Project repository's protect-main
# ruleset. Deployment remains manual and separately environment-gated.
permissions:
contents: read

jobs:
delivery-config:
name: Validate delivery configuration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Validate portable Compose configuration
run: docker compose --env-file ops/staging/.env.example -f ops/staging/compose.yaml config --quiet

- name: Validate staging Caddy routes
run: |
docker run --rm \
-v "$PWD/ops/staging/Caddyfile:/etc/caddy/Caddyfile:ro" \
caddy:2.10.2-alpine \
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

- name: Validate Azure gateway Caddy routes
run: |
docker run --rm \
-v "$PWD/ops/gateway/Caddyfile:/etc/caddy/Caddyfile:ro" \
caddy:2.10.2-alpine \
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

- name: Compile Azure Bicep
run: |
docker run --rm \
--mount "type=bind,source=$PWD/infra/azure,target=/work,readonly" \
mcr.microsoft.com/azure-cli:2.79.0 \
az bicep build --file /work/main.bicep --outfile /tmp/main.json
47 changes: 47 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Deploy portfolio evidence hub

on:
push:
branches: [main]
paths:
- portfolio/**
- .github/workflows/deploy-pages.yml
workflow_dispatch:

permissions: {}

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Upload static evidence hub
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
steps:
- name: Check out static site
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure GitHub Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: portfolio

deploy:
name: Deploy static evidence hub
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy Pages artifact
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
45 changes: 45 additions & 0 deletions .github/workflows/publish-gateway-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Publish immutable gateway image

on:
workflow_dispatch:

permissions:
contents: read
packages: write
attestations: write
id-token: write

jobs:
publish:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
IMAGE_NAME: ghcr.io/simpleplatform/simple-gateway
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push immutable gateway
id: push
uses: docker/build-push-action@ee4ca427a2f43b6a16632044ca514c076267da23 # v6.19.0
with:
context: ops/gateway
push: true
tags: ${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Attest image build provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
- name: Record digest in job summary
run: |
echo '### Immutable gateway image' >> "$GITHUB_STEP_SUMMARY"
echo "\`${IMAGE_NAME}@${{ steps.push.outputs.digest }}\`" >> "$GITHUB_STEP_SUMMARY"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ Thumbs.db
*.bak
*.swp
docs/handoffs/

# Local staging configuration. Copy ops/staging/.env.example to ops/staging/.env
# and keep the resulting values on the operator machine only.
ops/staging/.env
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ authenticated pages in the shared `AppShell` layout. Feature components live in
`src/features/`, UI primitives in `src/components/ui/`, and current planned
product data lives in `src/mock/`.

## Engineering Evidence & Delivery

The [DevOps evidence hub](docs/devops/README.md) records current verification levels, portable-staging
guidance, release-evidence rules, and the manual GitHub hardening checklist. It uses explicit local/CI/
staging/deployment levels so documentation never turns a local check into a cloud-deployment claim.

The zero-dependency [portfolio evidence page](portfolio/index.html) can be published through GitHub Pages
after the repository owner enables it manually. It is a public navigation page, not an application deployment
or availability monitor.

---

## Current Status
Expand Down
47 changes: 47 additions & 0 deletions docs/devops/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# DevOps evidence hub

This folder is the permanent, public record of how SimPle is built and reviewed. It is intentionally
evidence-led: a document or workflow is not proof that a cloud deployment, CI run, security scan, or
backup restore has happened.

## Current truthful status

| Area | Status |
|---|---|
| Modules 1–4 | Completed locally and merged, with module-level evidence in this repository |
| Modules 5–6 | Locally complete; hosted CI, portable-staging, and deployment evidence is deferred to Module 14 |
| Modules 7–13 | Planned or in progress; no cloud resource should be created for normal module work |
| Module 14 delivery foundation | Repository-owned implementation added; full portable-staging and hosted-CI evidence is still pending |
| Cloud deployment | Not created and not claimed |

Read the [portable staging guide](../../ops/staging/README.md), the
[GitHub hardening checklist](github-hardening-checklist.md), the
[release-evidence guide](release-evidence.md), and the
[GitHub Pages guide](github-pages.md), and the
[Azure student deployment runbook](azure-student-deployment.md) before describing the project publicly.

## Evidence levels

Keep these independent in every release note and LinkedIn post:

| Field | It means | It does not mean |
|---|---|---|
| `LocalComplete` | Required local checks for the exact candidate were recorded | Hosted CI or a deployed environment passed |
| `CiVerified` | The exact committed release tuple passed hosted CI | Portable staging or cloud deployment passed |
| `PortableStagingVerified` | The exact tuple passed documented disposable Compose staging checks | A public cloud environment is available |
| `DeploymentVerified` | A human-approved cloud environment was checked against that tuple | High availability, scale-out, or perpetual hosting |

The project is a **portfolio demo / single-instance system** until a separately evidenced change says
otherwise. Do not call it "production ready," "highly available," or deployed based only on this folder.

## What recruiters can inspect

- Module documentation, technical flows, test reports, and security audits under `docs/modules/` and
`docs/security/`.
- The source-controlled local staging topology under `ops/staging/`.
- A no-secret static overview under `portfolio/`, suitable for GitHub Pages after the repository owner
enables Pages manually.
- Future release evidence under `docs/releases/<release-id>/`, where every passing check must identify the
backend, frontend, and Project SHAs plus image digests.

The static portfolio is a navigation aid, not a replacement for source records or CI artifacts.
Loading
Loading