281 lines
16 KiB
YAML
281 lines
16 KiB
YAML
name: MCP isolated acceptance preview images
|
|
'on':
|
|
workflow_dispatch:
|
|
inputs:
|
|
expected_sha:
|
|
description: Exact reviewed full commit SHA on the selected ref; includes all accepted source changes
|
|
required: true
|
|
type: string
|
|
auth_portal_origin:
|
|
description: Canonical isolated browser-facing Auth Portal HTTP(S) origin; no path, credentials or query
|
|
required: true
|
|
type: string
|
|
default: http://127.0.0.1:55175
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
jobs:
|
|
backend-preview:
|
|
if: ${{ gitea.event_name == 'workflow_dispatch' && gitea.ref == 'refs/heads/codex/mcp-acceptance-current-20261103' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 40
|
|
env:
|
|
COMPONENT: backend
|
|
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
|
DISPATCH_SHA: ${{ gitea.sha }}
|
|
DISPATCH_EVENT: ${{ gitea.event_name }}
|
|
DISPATCH_REF: ${{ gitea.ref }}
|
|
REPOSITORY: ${{ gitea.repository }}
|
|
RUN_ID: ${{ gitea.run_id }}
|
|
RUN_ATTEMPT: ${{ gitea.run_attempt }}
|
|
steps:
|
|
- name: Checkout exact dispatch commit
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ gitea.sha }}
|
|
persist-credentials: false
|
|
- name: Use existing Linux Node22 build toolchain
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
- name: Verify source and isolated preview destination
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
test "$DISPATCH_EVENT" = workflow_dispatch
|
|
test "$DISPATCH_REF" = refs/heads/codex/mcp-acceptance-current-20261103
|
|
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
|
|
test "$EXPECTED_SHA" = "$DISPATCH_SHA"
|
|
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
|
|
git diff --exit-code
|
|
git diff --cached --exit-code
|
|
[[ "$RUN_ID" =~ ^[0-9]+$ && "$RUN_ATTEMPT" =~ ^[0-9]+$ ]]
|
|
owner=$(printf '%s' "$REPOSITORY" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]')
|
|
name=$(printf '%s' "$REPOSITORY" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]')
|
|
[[ "$owner" =~ ^[a-z0-9][a-z0-9._-]*$ && "$name" =~ ^[a-z0-9][a-z0-9._-]*$ ]]
|
|
stem=$(printf '%s' "$name" | sed -E 's/-[0-9]+$//')
|
|
echo "IMAGE_NAME=git.shx.one/$owner/$stem-$COMPONENT-mcp-acceptance" >> "$GITHUB_ENV"
|
|
echo "PREVIEW_TAG=mcp-acceptance-$EXPECTED_SHA-$RUN_ID-$RUN_ATTEMPT" >> "$GITHUB_ENV"
|
|
echo "PREVIEW_CACHE=git.shx.one/$owner/$stem-$COMPONENT-mcp-acceptance-buildcache:node-buildcache" >> "$GITHUB_ENV"
|
|
mkdir -p .ci/acceptance-evidence
|
|
node <<'NODE'
|
|
const fs=require('node:fs'),cp=require('node:child_process'),crypto=require('node:crypto');
|
|
const files=cp.execFileSync('git',['ls-files','-z'],{encoding:'utf8'}).split('\0').filter(Boolean);
|
|
const inputs=files.map(path=>{const info=fs.lstatSync(path);if(!info.isFile()||info.isSymbolicLink())throw Error('Regular tracked source inputs required');const data=fs.readFileSync(path);return {path,sha256:crypto.createHash('sha256').update(data).digest('hex'),bytes:data.length}});
|
|
fs.writeFileSync('.ci/acceptance-evidence/source.json',JSON.stringify({format:'mcp-preview-source-v1',repository:process.env.REPOSITORY,sourceSha:process.env.EXPECTED_SHA,sourceTree:cp.execFileSync('git',['rev-parse','HEAD^{tree}'],{encoding:'utf8'}).trim(),platform:'linux/amd64',component:process.env.COMPONENT,runId:process.env.RUN_ID,runAttempt:process.env.RUN_ATTEMPT,authPortalOrigin:process.env.AUTH_PORTAL_ORIGIN??null,inputs},null,2)+'\n');
|
|
NODE
|
|
- name: Prepare existing scoped Docker context
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
STAGING=".ci/docker/backend"
|
|
rm -rf "$STAGING"
|
|
mkdir -p "$STAGING/packages/contracts" "$STAGING/backend"
|
|
cp package-lock.json "$STAGING/"
|
|
node <<'NODE'
|
|
const fs = require("node:fs")
|
|
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
|
pkg.workspaces = ["packages/*", "backend"]
|
|
pkg.dependencies = {}
|
|
pkg.devDependencies = {}
|
|
delete pkg.scripts
|
|
fs.writeFileSync(".ci/docker/backend/package.json", `${JSON.stringify(pkg, null, 2)}\n`)
|
|
NODE
|
|
cp packages/contracts/package.json packages/contracts/tsconfig.json "$STAGING/packages/contracts/"
|
|
cp -R packages/contracts/src "$STAGING/packages/contracts/"
|
|
cp backend/package.json backend/tsconfig.json backend/mcp-coverage.json "$STAGING/backend/"
|
|
cp -R backend/src "$STAGING/backend/"
|
|
mkdir -p "$STAGING/deploy/netflow-agent"
|
|
cp deploy/netflow-agent/install.sh "$STAGING/deploy/netflow-agent/"
|
|
if [ -d backend/drizzle ]; then
|
|
cp -R backend/drizzle "$STAGING/backend/"
|
|
fi
|
|
mkdir -p "$STAGING/backend/scripts/writer-profile"
|
|
cp backend/scripts/build-database-operator.mjs backend/scripts/database-writer-bootstrap-operator.mjs backend/scripts/database-writer-profile-operator.mts "$STAGING/backend/scripts/"
|
|
cp backend/scripts/writer-profile/operator-files.mjs backend/scripts/writer-profile/operator-contract.mjs backend/scripts/writer-profile/publisher.mjs backend/scripts/writer-profile/context.mjs backend/scripts/writer-profile/catalogue.mts backend/scripts/writer-profile/catalogue-value-bridge.ts backend/scripts/writer-profile/catalogue-projection.ts backend/scripts/writer-profile/catalogue-leaves.ts backend/scripts/writer-profile/application-writer-profile-catalogue.ts backend/scripts/writer-profile/application-writer-profile-catalogue-values.ts backend/scripts/writer-profile/application-writer-profile-catalogue-parent-policy.ts backend/scripts/writer-profile/application-writer-profile-canonical.ts backend/scripts/writer-profile/fixed44-build-sql.ts backend/scripts/writer-profile/fixed44-build-definition.ts backend/scripts/writer-profile/expanded44-data-policy.ts "${STAGING}/backend/scripts/writer-profile/"
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Log in to existing Gitea registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: git.shx.one
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.ACTIONS_PAT }}
|
|
- name: Build and push isolated acceptance image
|
|
id: build
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .ci/docker/backend
|
|
file: backend/Dockerfile
|
|
platforms: linux/amd64
|
|
push: true
|
|
tags: ${{ env.IMAGE_NAME }}:${{ env.PREVIEW_TAG }}
|
|
build-args: APP_VERSION=${{ env.PREVIEW_TAG }}
|
|
cache-from: type=gha,scope=mcp-acceptance-backend
|
|
cache-to: type=gha,scope=mcp-acceptance-backend,mode=max
|
|
provenance: mode=min
|
|
sbom: true
|
|
labels: |-
|
|
org.opencontainers.image.revision=${{ gitea.sha }}
|
|
org.opencontainers.image.version=${{ env.PREVIEW_TAG }}
|
|
io.mmapp.acceptance-only=true
|
|
- name: Record exact digest and build metadata
|
|
shell: bash
|
|
env:
|
|
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
|
|
BUILD_METADATA: ${{ steps.build.outputs.metadata }}
|
|
run: |
|
|
set -euo pipefail
|
|
node <<'NODE'
|
|
const fs=require('node:fs'),crypto=require('node:crypto');
|
|
const digest=process.env.IMAGE_DIGEST;if(!/^sha256:[a-f0-9]{64}$/.test(digest))throw Error('Exact registry digest required');
|
|
const metadata=JSON.parse(process.env.BUILD_METADATA);const source=fs.readFileSync('.ci/acceptance-evidence/source.json');
|
|
fs.writeFileSync('.ci/acceptance-evidence/image.json',JSON.stringify({format:'mcp-preview-image-v1',sourceSha:process.env.EXPECTED_SHA,sourceProofSha256:crypto.createHash('sha256').update(source).digest('hex'),image:process.env.IMAGE_NAME,tag:process.env.PREVIEW_TAG,digest,pinnedReference:process.env.IMAGE_NAME+'@'+digest,platform:'linux/amd64',component:process.env.COMPONENT,authPortalOrigin:process.env.AUTH_PORTAL_ORIGIN??null,buildMetadata:metadata,productionDeployed:false,wholeInstallation2GbAccepted:false},null,2)+'\n');
|
|
console.log('Acceptance image digest: '+process.env.IMAGE_NAME+'@'+digest);
|
|
NODE
|
|
- name: Retain source and image binding artifacts
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: mcp-acceptance-${{ env.COMPONENT }}-${{ gitea.sha }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}
|
|
path: .ci/acceptance-evidence/*.json
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
frontend-preview:
|
|
if: ${{ gitea.event_name == 'workflow_dispatch' && gitea.ref == 'refs/heads/codex/mcp-acceptance-current-20261103' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 40
|
|
env:
|
|
COMPONENT: frontend
|
|
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
|
DISPATCH_SHA: ${{ gitea.sha }}
|
|
DISPATCH_EVENT: ${{ gitea.event_name }}
|
|
DISPATCH_REF: ${{ gitea.ref }}
|
|
REPOSITORY: ${{ gitea.repository }}
|
|
RUN_ID: ${{ gitea.run_id }}
|
|
RUN_ATTEMPT: ${{ gitea.run_attempt }}
|
|
AUTH_PORTAL_ORIGIN_REQUEST: ${{ inputs.auth_portal_origin }}
|
|
steps:
|
|
- name: Checkout exact dispatch commit
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ gitea.sha }}
|
|
persist-credentials: false
|
|
- name: Use existing Linux Node22 build toolchain
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
- name: Validate explicit isolated portal origin
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
node <<'NODE'
|
|
const fs=require('node:fs'),assert=require('node:assert/strict');
|
|
const raw=process.env.AUTH_PORTAL_ORIGIN_REQUEST;
|
|
assert(typeof raw==='string'&&raw.length>0&&raw.length<=2048&&!/[\x00-\x20\x7f]/.test(raw),'Bounded canonical HTTP(S) origin required');
|
|
const origin=new URL(raw);
|
|
assert(['http:','https:'].includes(origin.protocol)&&!origin.username&&!origin.password&&origin.pathname==='/'&&!origin.search&&!origin.hash&&origin.origin!=='null'&&origin.origin===raw,'Canonical origin only; credentials/path/query/hash refused');
|
|
fs.appendFileSync(process.env.GITHUB_ENV,'AUTH_PORTAL_ORIGIN='+origin.origin+'\n');
|
|
NODE
|
|
- name: Verify source and isolated preview destination
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
test "$DISPATCH_EVENT" = workflow_dispatch
|
|
test "$DISPATCH_REF" = refs/heads/codex/mcp-acceptance-current-20261103
|
|
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
|
|
test "$EXPECTED_SHA" = "$DISPATCH_SHA"
|
|
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
|
|
git diff --exit-code
|
|
git diff --cached --exit-code
|
|
[[ "$RUN_ID" =~ ^[0-9]+$ && "$RUN_ATTEMPT" =~ ^[0-9]+$ ]]
|
|
owner=$(printf '%s' "$REPOSITORY" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]')
|
|
name=$(printf '%s' "$REPOSITORY" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]')
|
|
[[ "$owner" =~ ^[a-z0-9][a-z0-9._-]*$ && "$name" =~ ^[a-z0-9][a-z0-9._-]*$ ]]
|
|
stem=$(printf '%s' "$name" | sed -E 's/-[0-9]+$//')
|
|
echo "IMAGE_NAME=git.shx.one/$owner/$stem-$COMPONENT-mcp-acceptance" >> "$GITHUB_ENV"
|
|
echo "PREVIEW_TAG=mcp-acceptance-$EXPECTED_SHA-$RUN_ID-$RUN_ATTEMPT" >> "$GITHUB_ENV"
|
|
echo "PREVIEW_CACHE=git.shx.one/$owner/$stem-$COMPONENT-mcp-acceptance-buildcache:node-buildcache" >> "$GITHUB_ENV"
|
|
mkdir -p .ci/acceptance-evidence
|
|
node <<'NODE'
|
|
const fs=require('node:fs'),cp=require('node:child_process'),crypto=require('node:crypto');
|
|
const files=cp.execFileSync('git',['ls-files','-z'],{encoding:'utf8'}).split('\0').filter(Boolean);
|
|
const inputs=files.map(path=>{const info=fs.lstatSync(path);if(!info.isFile()||info.isSymbolicLink())throw Error('Regular tracked source inputs required');const data=fs.readFileSync(path);return {path,sha256:crypto.createHash('sha256').update(data).digest('hex'),bytes:data.length}});
|
|
fs.writeFileSync('.ci/acceptance-evidence/source.json',JSON.stringify({format:'mcp-preview-source-v1',repository:process.env.REPOSITORY,sourceSha:process.env.EXPECTED_SHA,sourceTree:cp.execFileSync('git',['rev-parse','HEAD^{tree}'],{encoding:'utf8'}).trim(),platform:'linux/amd64',component:process.env.COMPONENT,runId:process.env.RUN_ID,runAttempt:process.env.RUN_ATTEMPT,authPortalOrigin:process.env.AUTH_PORTAL_ORIGIN??null,inputs},null,2)+'\n');
|
|
NODE
|
|
- name: Prepare existing scoped Docker context
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
STAGING=".ci/docker/frontend"
|
|
rm -rf "$STAGING"
|
|
mkdir -p "$STAGING/packages/contracts" "$STAGING/public"
|
|
cp package-lock.json next.config.ts tsconfig.json postcss.config.mjs eslint.config.mjs components.json "$STAGING/"
|
|
node <<'NODE'
|
|
const fs = require("node:fs")
|
|
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
|
pkg.workspaces = ["packages/*"]
|
|
fs.writeFileSync(".ci/docker/frontend/package.json", `${JSON.stringify(pkg, null, 2)}\n`)
|
|
NODE
|
|
cp packages/contracts/package.json packages/contracts/tsconfig.json "$STAGING/packages/contracts/"
|
|
cp -R packages/contracts/src "$STAGING/packages/contracts/"
|
|
for dir in app components lib shared entities public hooks; do
|
|
if [ -d "$dir" ]; then
|
|
cp -R "$dir" "$STAGING/"
|
|
fi
|
|
done
|
|
test -f "$STAGING/public/release-manifest.json"
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Log in to existing Gitea registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: git.shx.one
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.ACTIONS_PAT }}
|
|
- name: Build and push isolated acceptance image
|
|
id: build
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .ci/docker/frontend
|
|
file: Dockerfile.frontend
|
|
platforms: linux/amd64
|
|
push: true
|
|
tags: ${{ env.IMAGE_NAME }}:${{ env.PREVIEW_TAG }}
|
|
build-args: |-
|
|
BACKEND_INTERNAL_URL=http://backend:8000
|
|
NEXT_PUBLIC_BACKEND_URL=same-origin
|
|
NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
|
NEXT_PUBLIC_ALLOW_MOCK_DATA=false
|
|
NEXT_PUBLIC_AUTH_PORTAL_URL=${{ env.AUTH_PORTAL_ORIGIN }}
|
|
NEXT_PUBLIC_APP_VERSION=${{ env.PREVIEW_TAG }}
|
|
NEXT_PUBLIC_RELEASE_URL=
|
|
cache-from: type=gha,scope=mcp-acceptance-frontend
|
|
cache-to: type=gha,scope=mcp-acceptance-frontend,mode=max
|
|
provenance: mode=min
|
|
sbom: true
|
|
labels: |-
|
|
org.opencontainers.image.revision=${{ gitea.sha }}
|
|
org.opencontainers.image.version=${{ env.PREVIEW_TAG }}
|
|
io.mmapp.acceptance-only=true
|
|
- name: Record exact digest and build metadata
|
|
shell: bash
|
|
env:
|
|
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
|
|
BUILD_METADATA: ${{ steps.build.outputs.metadata }}
|
|
run: |
|
|
set -euo pipefail
|
|
node <<'NODE'
|
|
const fs=require('node:fs'),crypto=require('node:crypto');
|
|
const digest=process.env.IMAGE_DIGEST;if(!/^sha256:[a-f0-9]{64}$/.test(digest))throw Error('Exact registry digest required');
|
|
const metadata=JSON.parse(process.env.BUILD_METADATA);const source=fs.readFileSync('.ci/acceptance-evidence/source.json');
|
|
fs.writeFileSync('.ci/acceptance-evidence/image.json',JSON.stringify({format:'mcp-preview-image-v1',sourceSha:process.env.EXPECTED_SHA,sourceProofSha256:crypto.createHash('sha256').update(source).digest('hex'),image:process.env.IMAGE_NAME,tag:process.env.PREVIEW_TAG,digest,pinnedReference:process.env.IMAGE_NAME+'@'+digest,platform:'linux/amd64',component:process.env.COMPONENT,authPortalOrigin:process.env.AUTH_PORTAL_ORIGIN??null,buildMetadata:metadata,productionDeployed:false,wholeInstallation2GbAccepted:false},null,2)+'\n');
|
|
console.log('Acceptance image digest: '+process.env.IMAGE_NAME+'@'+digest);
|
|
NODE
|
|
- name: Retain source and image binding artifacts
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: mcp-acceptance-${{ env.COMPONENT }}-${{ gitea.sha }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}
|
|
path: .ci/acceptance-evidence/*.json
|
|
if-no-files-found: error
|
|
retention-days: 14
|