name: MCP isolated portal acceptance preview '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 permissions: contents: read packages: write jobs: portal-preview: if: ${{ gitea.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 40 env: COMPONENT: portal EXPECTED_SHA: ${{ inputs.expected_sha }} DISPATCH_SHA: ${{ gitea.sha }} DISPATCH_EVENT: ${{ gitea.event_name }} 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 [[ "$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: 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: . file: deploy/docker/authportal/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.IMAGE_NAME }}:${{ env.PREVIEW_TAG }} build-args: |- VERSION=${{ env.PREVIEW_TAG }} GIT_SHA=${{ gitea.sha }} cache-from: type=registry,ref=${{ env.PREVIEW_CACHE }} cache-to: type=registry,ref=${{ env.PREVIEW_CACHE }},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