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
10 changes: 10 additions & 0 deletions .github/workflows/commit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ name: Commit tests
on:
push:
workflow_dispatch:
inputs:
architecture_mode:
description: Run the fast amd64 gate or the final amd64 plus arm64 gate
type: choice
required: false
default: amd64
options:
- amd64
- multiarch

permissions:
contents: read
Expand All @@ -22,3 +31,4 @@ jobs:
uses: ./.github/workflows/quality-gates.yml
with:
context: commit
architecture_mode: ${{ inputs.architecture_mode || 'amd64' }}
10 changes: 10 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ on:
- reopened
- ready_for_review
workflow_dispatch:
inputs:
architecture_mode:
description: Run the fast amd64 gate or the final amd64 plus arm64 gate
type: choice
required: false
default: amd64
options:
- amd64
- multiarch

permissions:
contents: read
Expand All @@ -25,3 +34,4 @@ jobs:
uses: ./.github/workflows/quality-gates.yml
with:
context: pull-request
architecture_mode: ${{ inputs.architecture_mode || 'amd64' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require ARM64 validation before merge

Checked both caller workflows: automatic pull-request and push events leave inputs.architecture_mode unset, so this expression always selects amd64; multiarch is reachable only through a manual dispatch. Moreover, both modes emit the same reusable-job check name, so branch protection cannot specifically require the manual ARM64 result. A successful amd64 check can therefore permit merging without any ARM64 validation, contrary to the new arm64RequiredBeforeMergeOrRelease contract; expose ARM64 as a distinct automatically triggered required check or otherwise make the dependency enforceable.

Useful? React with 👍 / 👎.

75 changes: 43 additions & 32 deletions .github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ on:
description: Execution context for the quality gates
required: true
type: string
architecture_mode:
description: Fast amd64 validation by default; select multiarch for the final arm64 gate
required: false
default: amd64
type: string

permissions:
contents: read
Expand All @@ -22,6 +27,9 @@ jobs:
name: Repository quality gates (${{ inputs.context }})
runs-on: ubuntu-latest
timeout-minutes: 60
env:
ARCHITECTURE_MODE: ${{ inputs.architecture_mode || 'amd64' }}
BUILD_PLATFORMS: ${{ inputs.architecture_mode == 'multiarch' && 'linux/arm64' || 'linux/amd64' }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -59,6 +67,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
[[ "$ARCHITECTURE_MODE" == "amd64" || "$ARCHITECTURE_MODE" == "multiarch" ]]
test -f LICENSE
grep -Fq 'MIT License' LICENSE
test -f README.md
Expand Down Expand Up @@ -145,6 +154,9 @@ jobs:
test -f .github/workflows/commit-tests.yml
test -f .github/workflows/pr-validation.yml
test -f .github/workflows/quality-gates.yml
grep -Fq 'architecture_mode' .github/workflows/commit-tests.yml
grep -Fq 'architecture_mode' .github/workflows/pr-validation.yml
grep -Fq 'BUILD_PLATFORMS' .github/workflows/quality-gates.yml
grep -Fq 'uses: actions/attest-build-provenance@v4' .github/workflows/quality-gates.yml
grep -Fq 'id-token: write' .github/workflows/quality-gates.yml
grep -Fq 'attestations: write' .github/workflows/quality-gates.yml
Expand Down Expand Up @@ -476,7 +488,7 @@ jobs:
node -e "const t=require('./release/local-proof-topology.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP1' || t.networkPolicy !== 'offline_runtime' || t.internalNetwork !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.dockerSocketMounted !== false) process.exit(1)"
node -e "const t=require('./release/lp2-local-services.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP2' || t.proofType !== 'local_synthetic' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.hostPortsPublished !== false || t.dockerSocketMounted !== false) process.exit(1)"
node -e "const t=require('./release/lp3-local-mail.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP3' || t.proofType !== 'local_synthetic_mail' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.syntheticDataOnly !== true || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.externalDeliveryEnabled !== false || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.policy?.catchAll !== false || t.policy?.automaticForwarding !== false || t.policy?.scanFailureMode !== 'fail_closed' || t.policy?.trashRetentionDays !== 28 || t.protocols?.imap?.idle !== true || t.protocols?.lmtp?.quotaReservationBeforeAck !== true || t.protocols?.sieve?.redirect !== false) process.exit(1)"
node -e "const t=require('./release/lp4-local-web.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP4' || t.proofType !== 'local_synthetic_web_dav' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.syntheticDataOnly !== true || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.credentialsCommitted !== false || t.web?.sameOriginOnly !== true || t.session?.csrfOnAuthenticatedMutations !== true || t.dav?.tenantBoundEtags !== true || t.dav?.tenantBoundSyncTokens !== true || t.dav?.masterContentAccess !== false || t.discovery?.tenantBound !== true) process.exit(1)"
node -e "const t=require('./release/lp4-local-web.json'); if (t.spdxLicenseIdentifier !== 'MIT' || t.author !== 'Sythos (https://www.sythos.net)' || t.milestone !== 'LP4' || t.proofType !== 'local_synthetic_web_dav' || t.networkPolicy !== 'offline_dependencies' || t.internalNetwork !== true || t.enableIpv6 !== true || t.ipFamilies?.join(',') !== 'ipv4,ipv6' || t.syntheticDataOnly !== true || t.publicDnsRequired !== false || t.publicAcmeEnabled !== false || t.hostPortsPublished !== false || t.dockerSocketMounted !== false || t.credentialsCommitted !== false || t.web?.sameOriginOnly !== true || t.session?.csrfOnAuthenticatedMutations !== true || t.dav?.tenantBoundEtags !== true || t.dav?.tenantBoundSyncTokens !== true || t.dav?.masterContentAccess !== false || t.discovery?.tenantBound !== true || t.architectureValidation?.defaultWorkflowMode !== 'amd64' || t.architectureValidation?.finalWorkflowMode !== 'multiarch' || t.architectureValidation?.finalModePlatforms?.join(',') !== 'linux/arm64' || t.architectureValidation?.arm64RequiredBeforeMergeOrRelease !== true || t.architectureValidation?.composeProofPlatform !== 'linux/amd64') process.exit(1)"
bash -n docker/lp1-network/entrypoint-ca.sh
bash -n docker/lp1-network/entrypoint-dns.sh
bash -n docker/lp2-tls/entrypoint-tls.sh
Expand All @@ -493,14 +505,14 @@ jobs:
bash -n docker/lp3-clamav/healthcheck-clamav.sh

- name: Set up Node.js when package metadata exists
if: hashFiles('package.json') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('package.json') != ''
uses: actions/setup-node@v6
with:
node-version: latest
check-latest: true

- name: Install dependencies and run the test script when available
if: hashFiles('package.json') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('package.json') != ''
shell: bash
run: |
set -euo pipefail
Expand All @@ -525,12 +537,12 @@ jobs:
fi

- name: Run Docker Compose M0 smoke checks
if: hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != ''
if: inputs.architecture_mode != 'multiarch' && (hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != '')
shell: pwsh
run: ./scripts/m0-smoke.ps1

- name: Set up QEMU for Ubuntu multi-architecture builds
if: hashFiles('Dockerfile') != ''
if: hashFiles('Dockerfile') != '' && inputs.architecture_mode == 'multiarch'
uses: docker/setup-qemu-action@v4.2.0
with:
platforms: amd64,arm64
Expand All @@ -539,14 +551,14 @@ jobs:
if: hashFiles('Dockerfile') != ''
uses: docker/setup-buildx-action@v4.2.0

- name: Validate Ubuntu 26.04 LTS amd64 and arm64 images
- name: Validate Ubuntu 26.04 LTS images (${{ inputs.architecture_mode || 'amd64' }})
if: hashFiles('Dockerfile') != ''
shell: bash
run: |
set -euo pipefail
output="$RUNNER_TEMP/gulogulo-ubuntu-26.04.oci.tar"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform "$BUILD_PLATFORMS" \
--pull \
--build-arg NODE_VERSION=26.7.0 \
--provenance=false \
Expand All @@ -556,14 +568,14 @@ jobs:
.
test -s "$output"

- name: Validate LP1 network utility image on amd64 and arm64
- name: Validate LP1 network utility image (${{ inputs.architecture_mode || 'amd64' }})
if: hashFiles('docker/lp1-network/Dockerfile') != ''
shell: bash
run: |
set -euo pipefail
output="$RUNNER_TEMP/gulogulo-lp1-network-ubuntu-26.04.oci.tar"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform "$BUILD_PLATFORMS" \
--pull \
--provenance=false \
--sbom=false \
Expand All @@ -574,7 +586,7 @@ jobs:
test -s "$output"

- name: Run Compose test profile
if: hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != ''
if: inputs.architecture_mode != 'multiarch' && (hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != '')
shell: bash
run: |
set -euo pipefail
Expand All @@ -592,24 +604,24 @@ jobs:
"${compose[@]}" --profile test run --rm --no-deps gulogulo-test

- name: Run LP1 isolated local proof topology
if: hashFiles('docker/lp1-network/Dockerfile') != '' && hashFiles('scripts/lp1-proof-smoke.mjs') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('docker/lp1-network/Dockerfile') != '' && hashFiles('scripts/lp1-proof-smoke.mjs') != ''
shell: bash
run: npm run test:lp1:docker

- name: Run LP2 static Compose audit
if: hashFiles('scripts/lp2-compose-audit.mjs') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp2-compose-audit.mjs') != ''
shell: bash
run: node scripts/lp2-compose-audit.mjs

- name: Validate LP2 dependency images on amd64 and arm64
- name: Validate LP2 dependency images (${{ inputs.architecture_mode || 'amd64' }})
if: hashFiles('docker/lp2-tls/Dockerfile') != '' && hashFiles('docker/lp2-ldap/Dockerfile') != '' && hashFiles('docker/lp2-postgres/Dockerfile') != ''
shell: bash
run: |
set -euo pipefail
while IFS='|' read -r name dockerfile context; do
output="$RUNNER_TEMP/gulogulo-${name}-ubuntu-26.04.oci.tar"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform "$BUILD_PLATFORMS" \
--pull \
--provenance=false \
--sbom=false \
Expand All @@ -625,45 +637,44 @@ jobs:
EOF

- name: Run LP2 synthetic LDAP and PostgreSQL proof
if: hashFiles('scripts/lp2-compose-smoke.mjs') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp2-compose-smoke.mjs') != ''
shell: bash
run: npm run test:lp2:docker

- name: Run LP3 static Compose and TypeScript audit
if: hashFiles('scripts/lp3-compose-audit.mjs') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp3-compose-audit.mjs') != ''
shell: bash
run: node scripts/lp3-compose-audit.mjs

- name: Run LP3 typed mail contract gates
if: hashFiles('src/mail/mail-core.test.ts') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('src/mail/mail-core.test.ts') != ''
shell: bash
run: |
set -euo pipefail
npm run typecheck:server
npm run test:lp3

- name: Run LP4 static and typed web/DAV contract gates
if: hashFiles('scripts/lp4-compose-audit.ts') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp4-compose-audit.ts') != ''
shell: bash
run: npm run test:lp4

# The Compose proof runs on the amd64 GitHub runner. Keep it before the
# emulated arm64 build so a functional regression fails quickly without
# spending another long QEMU build first.
# In the default amd64 mode, the Compose proofs run on the amd64 GitHub
# runner before any architecture image work that could be expensive.
- name: Run LP3 local mail Compose proof (amd64)
if: hashFiles('scripts/lp3-compose-smoke.mjs') != '' && hashFiles('compose.yaml') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp3-compose-smoke.mjs') != '' && hashFiles('compose.yaml') != ''
shell: bash
run: node scripts/lp3-compose-smoke.mjs

# LP4 remains an amd64 live proof on GitHub; the root image is already
# validated for both architectures by the multi-architecture build.
# LP4 remains an amd64 live proof on GitHub. The explicit multiarch mode
# is a short arm64 artifact/attestation gate after this proof is green.
- name: Run LP4 local web and DAV Compose proof (amd64)
if: hashFiles('scripts/lp4-compose-smoke.ts') != '' && hashFiles('compose.yaml') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('scripts/lp4-compose-smoke.ts') != '' && hashFiles('compose.yaml') != ''
shell: bash
run: npm run test:lp4:docker

- name: Validate LP3 mail images on amd64
if: hashFiles('docker/lp3-tls/Dockerfile') != '' && hashFiles('docker/lp3-postfix/Dockerfile') != '' && hashFiles('docker/lp3-dovecot/Dockerfile') != '' && hashFiles('docker/lp3-rspamd/Dockerfile') != '' && hashFiles('docker/lp3-clamav/Dockerfile') != '' && hashFiles('docker/lp3-proof/Dockerfile') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('docker/lp3-tls/Dockerfile') != '' && hashFiles('docker/lp3-postfix/Dockerfile') != '' && hashFiles('docker/lp3-dovecot/Dockerfile') != '' && hashFiles('docker/lp3-rspamd/Dockerfile') != '' && hashFiles('docker/lp3-clamav/Dockerfile') != '' && hashFiles('docker/lp3-proof/Dockerfile') != ''
shell: bash
run: |
set -euo pipefail
Expand All @@ -688,8 +699,8 @@ jobs:
lp3-proof|docker/lp3-proof/Dockerfile|docker/lp3-proof
EOF

- name: Validate LP3 mail images on arm64
if: hashFiles('docker/lp3-tls/Dockerfile') != '' && hashFiles('docker/lp3-postfix/Dockerfile') != '' && hashFiles('docker/lp3-dovecot/Dockerfile') != '' && hashFiles('docker/lp3-rspamd/Dockerfile') != '' && hashFiles('docker/lp3-clamav/Dockerfile') != '' && hashFiles('docker/lp3-proof/Dockerfile') != ''
- name: Validate LP3 mail images on arm64 (final multiarch gate)
if: inputs.architecture_mode == 'multiarch' && hashFiles('docker/lp3-tls/Dockerfile') != '' && hashFiles('docker/lp3-postfix/Dockerfile') != '' && hashFiles('docker/lp3-dovecot/Dockerfile') != '' && hashFiles('docker/lp3-rspamd/Dockerfile') != '' && hashFiles('docker/lp3-clamav/Dockerfile') != '' && hashFiles('docker/lp3-proof/Dockerfile') != ''
shell: bash
run: |
set -euo pipefail
Expand Down Expand Up @@ -736,7 +747,7 @@ jobs:
done

- name: Run disposable PostgreSQL M2 integration contract
if: hashFiles('src/integrations/postgres.integration.test.ts') != ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('src/integrations/postgres.integration.test.ts') != ''
shell: bash
run: |
set -euo pipefail
Expand All @@ -756,16 +767,16 @@ jobs:
GULOGULO_M2_POSTGRES_DSN="postgresql://gulogulo@$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container"):5432/gulogulo" npm run test:m2:postgres

- name: Run M1 fixture, health, and metrics smoke checks
if: hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != ''
if: inputs.architecture_mode != 'multiarch' && (hashFiles('compose.yaml') != '' || hashFiles('compose.yml') != '' || hashFiles('docker-compose.yml') != '' || hashFiles('docker-compose.yaml') != '')
shell: pwsh
run: ./scripts/m1-fixture-smoke.ps1 -RequireMetrics

- name: Report Docker smoke-check status when Compose is not defined
if: hashFiles('compose.yaml') == '' && hashFiles('compose.yml') == '' && hashFiles('docker-compose.yml') == '' && hashFiles('docker-compose.yaml') == ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('compose.yaml') == '' && hashFiles('compose.yml') == '' && hashFiles('docker-compose.yml') == '' && hashFiles('docker-compose.yaml') == ''
shell: bash
run: echo "No Compose file exists yet; M0 Docker smoke checks are deferred until the Docker-first scaffold is added."

- name: Report package-test status when no package exists
if: hashFiles('package.json') == ''
if: inputs.architecture_mode != 'multiarch' && hashFiles('package.json') == ''
shell: bash
run: echo "No package.json exists yet; package tests are deferred until implementation is added."
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ where the item depends on external infrastructure.
- [x] offline synthetic LP2 LDAP and PostgreSQL dependency proof with verified TLS;
- [x] offline synthetic mail proof with Postfix, Dovecot, Rspamd, and ClamAV;
- [x] offline synthetic web/session/DAV/discovery proof with restart continuity;
- [x] fast amd64-first CI with an explicit multiarch amd64+arm64 final gate;
- [x] tenant-bound DAV ETags and sync tokens;
- [x] OCI build-provenance attestations generated and verified;
- [x] log rotation;
Expand Down
22 changes: 22 additions & 0 deletions doc/lp4-local-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,28 @@ The intended integrated npm entry points are `npm run test:lp4` and
checks, all typed web and DAV tests, and the static audit. The second runs
`scripts/lp4-compose-smoke.ts`.

## Architecture-gated CI

The normal pull-request and push path uses the fast `amd64` architecture mode.
This keeps the functional LP4 static, typed, and Compose gates quick and avoids
starting QEMU when a change is still being iterated. The reusable quality gate
accepts `architecture_mode=amd64` (the default) or `architecture_mode=multiarch`.

The final LP4 verification uses the explicit `multiarch` mode. It is deliberately
the short final architecture gate: it skips the already-green functional suite,
builds the arm64 artifacts, and verifies their OCI attestations. The ARM64 pass
is therefore a release/merge gate, not a hidden best-effort job. The Compose
proof itself remains an amd64 GitHub runner proof; the prior amd64 run plus this
arm64 artifact pass cover both target architectures.

For the manual commit workflow, choose **multiarch** only after the default
amd64 run is green. This is the intended two-stage path:

1. run the default amd64 quality gate;
2. after it passes, dispatch the same commit with `architecture_mode=multiarch`
for the short arm64-only final gate;
3. merge or publish only after that final run is green.

The live smoke builds the application image, starts `gulogulo-lp4-web`, runs
the internal proof client, inspects network/container/volume safety, restarts
the web service, and repeats the proof against the preserved DAV continuity
Expand Down
Loading