Files
openclaw/.github/workflows/openclaw-release-telegram-qa.yml
Peter Steinberger 4efe056d32 fix(ci): stop Telegram release checks from fetching history (#105219)
* fix(ci): bound Telegram provenance lookups

* docs(agents): note linked worktree invocations
2026-07-12 10:18:39 +01:00

2529 lines
107 KiB
YAML

name: OpenClaw Release Telegram QA
run-name: ${{ github.event_name == 'workflow_dispatch' && format('OpenClaw Release Telegram QA {0}', inputs.dispatch_id) || 'OpenClaw Release Telegram QA' }}
on:
# Transitional compatibility for supported release refs whose parent still calls @main.
# Current main dispatches this workflow so qa-live-shared secrets stay in this run.
workflow_call:
inputs:
expected_trusted_workflow_sha:
description: Resolved main SHA authorized for this trusted workflow
required: true
type: string
target_ref:
description: Trusted release ref whose exact candidate SHA should be validated
required: true
type: string
target_sha:
description: Exact full candidate commit SHA
required: true
type: string
secrets:
OPENCLAW_QA_CONVEX_SECRET_CI:
description: Credential-lease coordinator secret supplied by qa-live-shared
required: false
OPENCLAW_QA_CONVEX_SITE_URL:
description: Credential-lease coordinator URL supplied by qa-live-shared
required: false
workflow_dispatch:
inputs:
dispatch_id:
description: Unique parent release-check dispatch identifier
required: true
type: string
expected_trusted_workflow_sha:
description: Resolved main SHA authorized for this trusted workflow
required: true
type: string
target_ref:
description: Trusted release ref whose exact candidate SHA should be validated
required: true
type: string
target_sha:
description: Exact full candidate commit SHA
required: true
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
jobs:
trusted_identity:
name: Resolve trusted dispatched workflow identity
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
id-token: write
outputs:
status: ${{ steps.record_status.outputs.status }}
workflow_ref: ${{ steps.identity.outputs.workflow_ref }}
workflow_repository: ${{ steps.identity.outputs.workflow_repository }}
workflow_sha: ${{ steps.identity.outputs.workflow_sha }}
steps:
- name: Verify dispatched-main identity
id: identity
env:
CALLER_WORKFLOW_REF: ${{ github.workflow_ref }}
CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }}
EXPECTED_TRUSTED_WORKFLOW_SHA: ${{ inputs.expected_trusted_workflow_sha }}
JOB_CONTEXT: ${{ toJSON(job) }}
TARGET_REF: ${{ inputs.target_ref }}
TARGET_SHA: ${{ inputs.target_sha }}
WORKFLOW_REF: ${{ github.workflow_ref }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
shell: bash
run: |
set -euo pipefail
expected_repository="openclaw/openclaw"
expected_ref="${expected_repository}/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main"
if [[ "$GITHUB_REPOSITORY" != "$expected_repository" ||
! "$EXPECTED_TRUSTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ ||
! "$TARGET_SHA" =~ ^[a-f0-9]{40}$ ||
-z "${TARGET_REF// }" ]]; then
echo "Telegram QA identity or target is malformed." >&2
exit 1
fi
INVOCATION_MODE=reusable
if [[ "$WORKFLOW_REF" == "$expected_ref" ]]; then
INVOCATION_MODE=dispatch
[[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" &&
"$GITHUB_REF" == "refs/heads/main" &&
"$GITHUB_SHA" == "$EXPECTED_TRUSTED_WORKFLOW_SHA" &&
"$WORKFLOW_SHA" == "$EXPECTED_TRUSTED_WORKFLOW_SHA" ]]
fi
export INVOCATION_MODE
oidc_json="$(
curl --fail --silent --show-error \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=openclaw-release-telegram-qa"
)"
oidc_token="$(jq -er '.value' <<<"$oidc_json")"
EXPECTED_WORKFLOW_REF="$expected_ref" OIDC_TOKEN="$oidc_token" \
node --input-type=module <<'NODE'
import { appendFileSync } from "node:fs";
const payloadPart = process.env.OIDC_TOKEN?.split(".")[1];
if (!payloadPart) {
throw new Error("OIDC token is malformed");
}
const payload = JSON.parse(
Buffer.from(payloadPart.replace(/-/g, "+").replace(/_/g, "/"), "base64url").toString(
"utf8",
),
);
const expectedTrustedWorkflowSha = process.env.EXPECTED_TRUSTED_WORKFLOW_SHA;
const eventName = process.env.GITHUB_EVENT_NAME;
const expected = {
aud: "openclaw-release-telegram-qa",
event_name: eventName,
iss: "https://token.actions.githubusercontent.com",
ref: process.env.GITHUB_REF,
repository: process.env.GITHUB_REPOSITORY,
runner_environment: "github-hosted",
sha: process.env.GITHUB_SHA,
workflow_ref: process.env.CALLER_WORKFLOW_REF,
workflow_sha: process.env.CALLER_WORKFLOW_SHA,
};
for (const [key, value] of Object.entries(expected)) {
if (payload[key] !== value) {
throw new Error(`OIDC ${key} mismatch`);
}
}
if (process.env.INVOCATION_MODE === "reusable") {
const job = JSON.parse(process.env.JOB_CONTEXT ?? "{}");
const reusableExpected = {
job_workflow_ref: process.env.EXPECTED_WORKFLOW_REF,
job_workflow_sha: expectedTrustedWorkflowSha,
};
for (const [key, value] of Object.entries(reusableExpected)) {
if (payload[key] !== value) {
throw new Error(`OIDC ${key} mismatch`);
}
}
if (
job.workflow_ref !== reusableExpected.job_workflow_ref ||
job.workflow_repository !== payload.repository ||
job.workflow_sha !== expectedTrustedWorkflowSha
) {
throw new Error("job context reusable workflow identity mismatch");
}
} else if (
payload.workflow_ref !== process.env.EXPECTED_WORKFLOW_REF ||
payload.workflow_sha !== expectedTrustedWorkflowSha
) {
throw new Error("OIDC dispatched workflow identity mismatch");
}
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`workflow_ref=${process.env.EXPECTED_WORKFLOW_REF}`,
`workflow_repository=${payload.repository}`,
`workflow_sha=${expectedTrustedWorkflowSha}`,
"",
].join("\n"),
);
NODE
- name: Record identity status
id: record_status
if: always()
env:
IDENTITY_OUTCOME: ${{ steps.identity.outcome }}
shell: bash
run: |
set -euo pipefail
if [[ "$IDENTITY_OUTCOME" == "success" ]]; then
echo "status=success" >>"$GITHUB_OUTPUT"
else
echo "status=failure" >>"$GITHUB_OUTPUT"
fi
build_candidate:
name: Build secretless Telegram candidate
needs: trusted_identity
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
actions: read
contents: read
pull-requests: read
outputs:
archive_name: ${{ steps.archive.outputs.archive_name }}
archive_sha256: ${{ steps.archive.outputs.archive_sha256 }}
artifact_digest: ${{ steps.upload.outputs.artifact-digest }}
artifact_id: ${{ steps.upload.outputs.artifact-id }}
candidate_tree: ${{ steps.archive.outputs.candidate_tree }}
candidate_version: ${{ steps.archive.outputs.candidate_version }}
manifest_sha256: ${{ steps.archive.outputs.manifest_sha256 }}
status: ${{ steps.record_status.outputs.status }}
steps:
- name: Require trusted dispatched workflow identity
id: require_identity
env:
IDENTITY_STATUS: ${{ needs.trusted_identity.outputs.status }}
shell: bash
run: |
set -euo pipefail
[[ "$IDENTITY_STATUS" == "success" ]]
- name: Checkout trusted Telegram QA bootstrap
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.trusted_identity.outputs.workflow_repository }}
ref: ${{ needs.trusted_identity.outputs.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify trusted bootstrap SHA
env:
EXPECTED_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
shell: bash
run: |
set -euo pipefail
[[ "$(git rev-parse HEAD)" == "$EXPECTED_SHA" ]]
- name: Setup candidate build toolchain
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
install-deps: "false"
use-actions-cache: "false"
- name: Checkout candidate runtime
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.target_sha }}
fetch-depth: 1
path: .candidate
persist-credentials: false
- name: Verify candidate checkout SHA
id: verify_candidate
env:
EXPECTED_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
[[ "$(git -C .candidate rev-parse HEAD)" == "$EXPECTED_SHA" ]]
- name: Validate candidate release provenance
id: provenance
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.target_ref }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
candidate_sha="$(git -C .candidate rev-parse HEAD)"
[[ "$candidate_sha" == "$TARGET_SHA" ]]
pr_head_count="$(
gh api \
-H "Accept: application/vnd.github+json" \
"repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/pulls" \
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${candidate_sha}"'")] | length'
)"
if [[ "$pr_head_count" != "0" ]]; then
echo "Telegram candidate ${candidate_sha} is an open same-repository PR head." >&2
exit 1
fi
compare_status="$(
gh api \
"repos/${GITHUB_REPOSITORY}/compare/${candidate_sha}...main" \
--jq '.status'
)"
trusted_reason=""
trusted_release_branch=""
if [[ "$compare_status" == "ahead" || "$compare_status" == "identical" ]]; then
trusted_reason="main-ancestor"
else
normalized_ref="${TARGET_REF#refs/heads/}"
if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]]; then
branch_sha="$(
git -C .candidate ls-remote --exit-code --refs origin \
"refs/heads/${normalized_ref}" |
awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }'
)"
[[ "$branch_sha" == "$candidate_sha" ]]
trusted_reason="release-branch-head"
trusted_release_branch="$normalized_ref"
elif [[ "$TARGET_REF" =~ ^refs/tags/v ]] || [[ "$TARGET_REF" =~ ^v ]]; then
normalized_tag="${TARGET_REF#refs/tags/}"
tag_refs="$(
git -C .candidate ls-remote --exit-code origin \
"refs/tags/${normalized_tag}" "refs/tags/${normalized_tag}^{}"
)"
awk -v sha="$candidate_sha" '$1 == sha { found = 1 } END { exit(found ? 0 : 1) }' \
<<<"$tag_refs"
trusted_reason="release-tag"
elif [[ "$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$candidate_sha" ]]; then
matching_release_branches="$(
gh api --paginate \
"repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \
--jq '.[].name' |
awk '$0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ ||
$0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/ { print }'
)"
if [[ "$(wc -l <<<"$matching_release_branches" | tr -d ' ')" == "1" && -n "$matching_release_branches" ]]; then
trusted_reason="release-branch-head"
trusted_release_branch="$matching_release_branches"
else
matching_release_tags="$(
git -C .candidate ls-remote origin 'refs/tags/v*' |
awk -v sha="$candidate_sha" '$1 == sha { sub(/\^\{\}$/, "", $2); print $2 }' |
sort -u
)"
if [[ -n "$matching_release_tags" ]]; then
trusted_reason="release-tag"
fi
fi
fi
fi
if [[ -z "$trusted_reason" ]]; then
echo "Telegram candidate ${candidate_sha} is not trusted release provenance." >&2
exit 1
fi
if [[ "$trusted_reason" != "main-ancestor" ]]; then
repository_owner="${GITHUB_REPOSITORY%%/*}"
repository_name="${GITHUB_REPOSITORY#*/}"
signature_json="$(
gh api graphql \
-f query='query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{oid signature{isValid state signer{login}} associatedPullRequests(first:10){nodes{state baseRefName baseRepository{nameWithOwner} mergeCommit{oid} mergedBy{login}}}}}}}' \
-f owner="$repository_owner" \
-f name="$repository_name" \
-f oid="$candidate_sha"
)"
signature_status="$(
jq -er \
--arg sha "$candidate_sha" \
'.data.repository.object |
select(.oid == $sha) |
if .signature == null then "missing"
elif .signature.isValid == true and .signature.state == "VALID" and
(.signature.signer.login // "") != "" then "valid"
else "invalid"
end' \
<<<"$signature_json"
)"
if [[ "$signature_status" == "invalid" ]]; then
echo "Release candidate ${candidate_sha} has an invalid commit signature." >&2
exit 1
fi
signer="$(jq -r '.data.repository.object.signature.signer.login // ""' <<<"$signature_json")"
permission_actor="$signer"
if [[ "$signature_status" == "missing" || "$signer" == "web-flow" ]]; then
if [[ "$trusted_reason" != "release-branch-head" || -z "$trusted_release_branch" ]]; then
echo "Unsigned or GitHub web-flow candidates require an exact release branch head." >&2
exit 1
fi
permission_actor="$(
jq -er \
--arg base "$trusted_release_branch" \
--arg repo "$GITHUB_REPOSITORY" \
--arg sha "$candidate_sha" \
'[.data.repository.object.associatedPullRequests.nodes[] |
select(.state == "MERGED" and .baseRefName == $base and
.baseRepository.nameWithOwner == $repo and .mergeCommit.oid == $sha) |
.mergedBy.login] | unique | select(length == 1) | .[0]' \
<<<"$signature_json"
)"
fi
role_name="$(
gh api \
"repos/${GITHUB_REPOSITORY}/collaborators/${permission_actor}/permission" \
--jq '.role_name'
)"
if [[ "$role_name" != "admin" && "$role_name" != "maintain" ]]; then
echo "Release candidate actor ${permission_actor} lacks maintain/admin access." >&2
exit 1
fi
fi
echo "Telegram candidate trust reason: ${trusted_reason}"
- name: Install candidate dependencies without runner credentials
id: install_candidate
shell: bash
run: |
set -euo pipefail
candidate_home="${RUNNER_TEMP}/openclaw-telegram-candidate-home"
mkdir -p "$candidate_home"
env -i \
CI=true \
COREPACK_HOME="${candidate_home}/.cache/corepack" \
HOME="$candidate_home" \
LANG=C.UTF-8 \
NPM_CONFIG_USERCONFIG=/dev/null \
PATH="$PATH" \
RUNNER_TEMP="$RUNNER_TEMP" \
pnpm --dir .candidate install \
--store-dir "$RUNNER_TEMP/openclaw-telegram-candidate-pnpm-store" \
--prefer-offline \
--frozen-lockfile \
--ignore-scripts=false \
--config.engine-strict=false \
--config.enable-pre-post-scripts=true \
--config.package-import-method=copy \
--config.side-effects-cache=true
- name: Build candidate runtime without runner credentials
id: build_candidate
shell: bash
run: |
set -euo pipefail
candidate_home="${RUNNER_TEMP}/openclaw-telegram-candidate-home"
env -i \
CI=true \
COREPACK_HOME="${candidate_home}/.cache/corepack" \
HOME="$candidate_home" \
LANG=C.UTF-8 \
NODE_OPTIONS=--max-old-space-size=8192 \
NPM_CONFIG_USERCONFIG=/dev/null \
PATH="$PATH" \
RUNNER_TEMP="$RUNNER_TEMP" \
pnpm --dir .candidate exec node scripts/build-all.mjs qaRuntime
- name: Move built candidate outside trusted workspace
id: move_candidate
env:
EXPECTED_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
candidate_root="${RUNNER_TEMP}/openclaw-telegram-candidate-build"
candidate_sha="$(git -C .candidate rev-parse HEAD)"
candidate_tree="$(git -C .candidate rev-parse 'HEAD^{tree}')"
tracked_status="$(git -C .candidate status --porcelain --untracked-files=no)"
if [[ "$candidate_sha" != "$EXPECTED_SHA" || -n "$tracked_status" ]]; then
echo "Candidate build did not preserve the exact clean target." >&2
exit 1
fi
rm -rf "$candidate_root"
mv .candidate "$candidate_root"
{
echo "candidate_root=$candidate_root"
echo "candidate_tree=$candidate_tree"
} >>"$GITHUB_OUTPUT"
- name: Restore trusted Telegram QA bootstrap
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.trusted_identity.outputs.workflow_repository }}
ref: ${{ needs.trusted_identity.outputs.workflow_sha }}
clean: true
fetch-depth: 1
persist-credentials: false
- name: Archive bounded candidate tree
id: archive
env:
CALLED_WORKFLOW_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
CANDIDATE_ROOT: ${{ steps.move_candidate.outputs.candidate_root }}
CANDIDATE_TREE: ${{ steps.move_candidate.outputs.candidate_tree }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
[[ "$(git rev-parse HEAD)" == "$CALLED_WORKFLOW_SHA" ]]
[[ -d "$CANDIDATE_ROOT/node_modules" && -f "$CANDIDATE_ROOT/dist/index.js" ]]
bundle_root="${RUNNER_TEMP}/openclaw-telegram-candidate-bundle"
archive_root="openclaw-telegram-candidate"
candidate_root="${bundle_root}/${archive_root}"
manifest_path="${bundle_root}/manifest.json"
rm -rf "$bundle_root"
mkdir -p "$bundle_root"
mv "$CANDIDATE_ROOT" "$candidate_root"
rm -rf "$candidate_root/.git"
candidate_version="$(jq -er '.version' "$candidate_root/package.json")"
python3 scripts/release-telegram-candidate-archive.py validate-tree "$candidate_root"
node_version="$(node --version)"
pnpm_version="$(pnpm --version)"
jq -n \
--arg archiveRoot "$archive_root" \
--arg buildCommand "node scripts/build-all.mjs qaRuntime" \
--arg candidateSha "$TARGET_SHA" \
--arg candidateTree "$CANDIDATE_TREE" \
--arg nodeVersion "$node_version" \
--arg pnpmVersion "$pnpm_version" \
--arg repository "$GITHUB_REPOSITORY" \
--arg sourceJob "build_candidate" \
--arg workflowSha "$CALLED_WORKFLOW_SHA" \
'{
version: 1,
repository: $repository,
workflowSha: $workflowSha,
candidateSha: $candidateSha,
candidateTree: $candidateTree,
archiveRoot: $archiveRoot,
nodeVersion: $nodeVersion,
pnpmVersion: $pnpmVersion,
buildCommand: $buildCommand,
sourceJob: $sourceJob
}' >"$manifest_path"
archive_name="release-telegram-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TARGET_SHA}.tar.zst"
archive_path="${RUNNER_TEMP}/${archive_name}"
LC_ALL=C tar \
--create \
--format=posix \
--sort=name \
--one-file-system \
--numeric-owner \
--owner=0 \
--group=0 \
--no-xattrs \
--no-acls \
--pax-option=delete=atime,delete=ctime \
-C "$bundle_root" \
manifest.json \
"$archive_root" |
zstd -T0 -3 -q -o "$archive_path"
{
echo "archive_name=$archive_name"
echo "archive_path=$archive_path"
echo "archive_sha256=$(sha256sum "$archive_path" | awk '{print $1}')"
echo "candidate_tree=$CANDIDATE_TREE"
echo "candidate_version=$candidate_version"
echo "manifest_sha256=$(sha256sum "$manifest_path" | awk '{print $1}')"
} >>"$GITHUB_OUTPUT"
- name: Upload raw candidate archive
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ steps.archive.outputs.archive_name }}
path: ${{ steps.archive.outputs.archive_path }}
archive: false
retention-days: 14
if-no-files-found: error
- name: Verify uploaded archive digest
id: verify_upload
env:
ACTUAL_DIGEST: ${{ steps.upload.outputs.artifact-digest }}
EXPECTED_DIGEST: ${{ steps.archive.outputs.archive_sha256 }}
shell: bash
run: |
set -euo pipefail
[[ "$ACTUAL_DIGEST" == "$EXPECTED_DIGEST" ]]
- name: Record candidate build status
id: record_status
if: always()
env:
OUTCOMES: >-
${{ steps.require_identity.outcome }}
${{ steps.verify_candidate.outcome }}
${{ steps.provenance.outcome }}
${{ steps.install_candidate.outcome }}
${{ steps.build_candidate.outcome }}
${{ steps.move_candidate.outcome }}
${{ steps.archive.outcome }}
${{ steps.upload.outcome }}
${{ steps.verify_upload.outcome }}
shell: bash
run: |
set -euo pipefail
status=success
for outcome in $OUTCOMES; do
[[ "$outcome" == "success" ]] || status=failure
done
echo "status=$status" >>"$GITHUB_OUTPUT"
attest_candidate:
name: Attest verified Telegram candidate
needs: [trusted_identity, build_candidate]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
actions: read
attestations: write
contents: read
id-token: write
outputs:
status: ${{ steps.record_status.outputs.status }}
steps:
- name: Require successful candidate build
id: require_build
env:
BUILD_STATUS: ${{ needs.build_candidate.outputs.status }}
IDENTITY_STATUS: ${{ needs.trusted_identity.outputs.status }}
shell: bash
run: |
set -euo pipefail
[[ "$IDENTITY_STATUS" == "success" && "$BUILD_STATUS" == "success" ]]
- name: Checkout trusted Telegram QA verifier
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.trusted_identity.outputs.workflow_repository }}
ref: ${{ needs.trusted_identity.outputs.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- name: Validate candidate artifact metadata
id: metadata
env:
ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
ARTIFACT_DIGEST: ${{ needs.build_candidate.outputs.artifact_digest }}
ARTIFACT_ID: ${{ needs.build_candidate.outputs.artifact_id }}
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ && "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]]
artifact_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")"
jq -e \
--arg digest "sha256:${ARTIFACT_DIGEST}" \
--argjson id "$ARTIFACT_ID" \
--argjson runId "$GITHUB_RUN_ID" \
--arg name "$ARCHIVE_NAME" \
--arg repositoryId "$GITHUB_REPOSITORY_ID" \
--arg sourceSha "$GITHUB_SHA" \
'
.id == $id and
.name == $name and
.expired == false and
.digest == $digest and
.workflow_run.id == $runId and
.workflow_run.head_sha == $sourceSha and
(.workflow_run.repository_id | tostring) == $repositoryId and
(.workflow_run.head_repository_id | tostring) == $repositoryId
' <<<"$artifact_json" >/dev/null
run_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}")"
jq -e \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
--arg sourceSha "$GITHUB_SHA" \
'
.id == $runId and
.run_attempt == $runAttempt and
.head_sha == $sourceSha
' <<<"$run_json" >/dev/null
- name: Download raw candidate archive
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
artifact-ids: ${{ needs.build_candidate.outputs.artifact_id }}
path: ${{ runner.temp }}/openclaw-telegram-attestor-download
skip-decompress: true
digest-mismatch: error
- name: Bounded extract and validate candidate
id: verify_candidate
env:
ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
ARCHIVE_SHA256: ${{ needs.build_candidate.outputs.archive_sha256 }}
CANDIDATE_TREE: ${{ needs.build_candidate.outputs.candidate_tree }}
MANIFEST_SHA256: ${{ needs.build_candidate.outputs.manifest_sha256 }}
TARGET_SHA: ${{ inputs.target_sha }}
WORKFLOW_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
shell: bash
run: |
set -euo pipefail
download_dir="${RUNNER_TEMP}/openclaw-telegram-attestor-download"
archive_path="${download_dir}/${ARCHIVE_NAME}"
extract_root="${RUNNER_TEMP}/openclaw-telegram-attestor-extract"
candidate_root="${extract_root}/openclaw-telegram-candidate"
[[ "$(sha256sum "$archive_path" | awk '{print $1}')" == "$ARCHIVE_SHA256" ]]
rm -rf "$extract_root"
python3 scripts/release-telegram-candidate-archive.py extract-zstd \
"$archive_path" \
"$extract_root" \
--allowed-root openclaw-telegram-candidate
manifest_path="${extract_root}/manifest.json"
[[ "$(sha256sum "$manifest_path" | awk '{print $1}')" == "$MANIFEST_SHA256" ]]
jq -e \
--arg candidateSha "$TARGET_SHA" \
--arg candidateTree "$CANDIDATE_TREE" \
--arg repository "$GITHUB_REPOSITORY" \
--arg workflowSha "$WORKFLOW_SHA" \
'
.version == 1 and
.repository == $repository and
.workflowSha == $workflowSha and
.candidateSha == $candidateSha and
.candidateTree == $candidateTree and
.archiveRoot == "openclaw-telegram-candidate" and
.buildCommand == "node scripts/build-all.mjs qaRuntime" and
.sourceJob == "build_candidate"
' "$manifest_path" >/dev/null
[[ -d "$candidate_root/node_modules" && -f "$candidate_root/dist/index.js" ]]
echo "archive_path=$archive_path" >>"$GITHUB_OUTPUT"
- name: Checkout independent candidate source
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.target_sha }}
fetch-depth: 1
path: .candidate-source
persist-credentials: false
- name: Compare candidate tracked source and tree
id: compare_source
env:
CANDIDATE_ROOT: ${{ runner.temp }}/openclaw-telegram-attestor-extract/openclaw-telegram-candidate
CANDIDATE_TREE: ${{ needs.build_candidate.outputs.candidate_tree }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
[[ "$(git -C .candidate-source rev-parse HEAD)" == "$TARGET_SHA" ]]
[[ "$(git -C .candidate-source rev-parse 'HEAD^{tree}')" == "$CANDIDATE_TREE" ]]
SOURCE_ROOT="$GITHUB_WORKSPACE/.candidate-source" node --input-type=module <<'NODE'
import { execFileSync } from "node:child_process";
import { lstatSync, readFileSync, readlinkSync } from "node:fs";
import { join } from "node:path";
const sourceRoot = process.env.SOURCE_ROOT;
const candidateRoot = process.env.CANDIDATE_ROOT;
// Node defaults to 1 MiB, smaller than this repo's tracked-file index output.
const trackedFileOutput = execFileSync(
"git",
["-C", sourceRoot, "ls-files", "-s", "-z"],
{ maxBuffer: 16 * 1024 * 1024 },
);
const records = trackedFileOutput
.toString("utf8")
.split("\0")
.filter(Boolean);
for (const record of records) {
const tab = record.indexOf("\t");
const [mode] = record.slice(0, tab).split(" ");
const relativePath = record.slice(tab + 1);
const sourcePath = join(sourceRoot, relativePath);
const candidatePath = join(candidateRoot, relativePath);
const sourceStat = lstatSync(sourcePath);
const candidateStat = lstatSync(candidatePath);
if (mode === "120000") {
if (
!sourceStat.isSymbolicLink() ||
!candidateStat.isSymbolicLink() ||
readlinkSync(sourcePath) !== readlinkSync(candidatePath)
) {
throw new Error(`tracked symlink mismatch: ${relativePath}`);
}
continue;
}
if (!sourceStat.isFile() || !candidateStat.isFile()) {
throw new Error(`tracked file type mismatch: ${relativePath}`);
}
if (!readFileSync(sourcePath).equals(readFileSync(candidatePath))) {
throw new Error(`tracked file content mismatch: ${relativePath}`);
}
const expectedExecutable = mode === "100755";
const actualExecutable = (candidateStat.mode & 0o111) !== 0;
if (expectedExecutable !== actualExecutable) {
throw new Error(`tracked executable mode mismatch: ${relativePath}`);
}
}
NODE
- name: Attest exact candidate archive bytes
id: attest
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.verify_candidate.outputs.archive_path }}
- name: Record candidate attestation status
id: record_status
if: always()
env:
OUTCOMES: >-
${{ steps.require_build.outcome }}
${{ steps.metadata.outcome }}
${{ steps.download.outcome }}
${{ steps.verify_candidate.outcome }}
${{ steps.compare_source.outcome }}
${{ steps.attest.outcome }}
shell: bash
run: |
set -euo pipefail
status=success
for outcome in $OUTCOMES; do
[[ "$outcome" == "success" ]] || status=failure
done
echo "status=$status" >>"$GITHUB_OUTPUT"
run_telegram:
name: Run isolated Telegram candidate
needs: [trusted_identity, build_candidate, attest_candidate]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 60
environment: qa-live-shared
permissions:
actions: read
attestations: read
contents: read
pull-requests: read
outputs:
evidence_artifact_digest: ${{ steps.upload_evidence.outputs.artifact-digest }}
evidence_artifact_id: ${{ steps.upload_evidence.outputs.artifact-id }}
evidence_artifact_name: ${{ steps.create_sut.outputs.evidence_artifact_name }}
status: ${{ steps.record_status.outputs.status }}
steps:
- name: Require verified candidate attestation
id: require_attestation
env:
ATTESTATION_STATUS: ${{ needs.attest_candidate.outputs.status }}
BUILD_STATUS: ${{ needs.build_candidate.outputs.status }}
IDENTITY_STATUS: ${{ needs.trusted_identity.outputs.status }}
shell: bash
run: |
set -euo pipefail
[[ "$IDENTITY_STATUS" == "success" &&
"$BUILD_STATUS" == "success" &&
"$ATTESTATION_STATUS" == "success" ]]
- name: Checkout trusted Telegram QA harness
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.trusted_identity.outputs.workflow_repository }}
ref: ${{ needs.trusted_identity.outputs.workflow_sha }}
fetch-depth: 1
persist-credentials: false
- name: Setup trusted harness Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
install-deps: "true"
use-actions-cache: "false"
- name: Build trusted QA harness
id: build_harness
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
- name: Validate candidate artifact metadata
id: metadata
env:
ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
ARTIFACT_DIGEST: ${{ needs.build_candidate.outputs.artifact_digest }}
ARTIFACT_ID: ${{ needs.build_candidate.outputs.artifact_id }}
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ && "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]]
artifact_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")"
jq -e \
--arg digest "sha256:${ARTIFACT_DIGEST}" \
--argjson id "$ARTIFACT_ID" \
--argjson runId "$GITHUB_RUN_ID" \
--arg name "$ARCHIVE_NAME" \
--arg repositoryId "$GITHUB_REPOSITORY_ID" \
--arg sourceSha "$GITHUB_SHA" \
'
.id == $id and
.name == $name and
.expired == false and
.digest == $digest and
.workflow_run.id == $runId and
.workflow_run.head_sha == $sourceSha and
(.workflow_run.repository_id | tostring) == $repositoryId and
(.workflow_run.head_repository_id | tostring) == $repositoryId
' <<<"$artifact_json" >/dev/null
run_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}")"
jq -e \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
--arg sourceSha "$GITHUB_SHA" \
'
.id == $runId and
.run_attempt == $runAttempt and
.head_sha == $sourceSha
' <<<"$run_json" >/dev/null
- name: Download raw candidate archive
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
artifact-ids: ${{ needs.build_candidate.outputs.artifact_id }}
path: ${{ runner.temp }}/openclaw-telegram-executor-download
skip-decompress: true
digest-mismatch: error
- name: Verify attestation and bounded extract
id: extract_candidate
env:
ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
ARCHIVE_SHA256: ${{ needs.build_candidate.outputs.archive_sha256 }}
CALLED_WORKFLOW_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
CANDIDATE_TREE: ${{ needs.build_candidate.outputs.candidate_tree }}
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
download_dir="${RUNNER_TEMP}/openclaw-telegram-executor-download"
archive_path="${download_dir}/${ARCHIVE_NAME}"
extract_root="${RUNNER_TEMP}/openclaw-telegram-executor-extract"
candidate_root="${extract_root}/openclaw-telegram-candidate"
[[ "$(sha256sum "$archive_path" | awk '{print $1}')" == "$ARCHIVE_SHA256" ]]
gh attestation verify "$archive_path" \
--repo "$GITHUB_REPOSITORY" \
--cert-identity "https://github.com/openclaw/openclaw/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main" \
--signer-digest "$CALLED_WORKFLOW_SHA" \
--source-ref "$GITHUB_REF" \
--source-digest "$GITHUB_SHA" \
--predicate-type "https://slsa.dev/provenance/v1" \
--deny-self-hosted-runners >/dev/null
rm -rf "$extract_root"
python3 scripts/release-telegram-candidate-archive.py extract-zstd \
"$archive_path" \
"$extract_root" \
--allowed-root openclaw-telegram-candidate
jq -e \
--arg candidateSha "$TARGET_SHA" \
--arg candidateTree "$CANDIDATE_TREE" \
--arg repository "$GITHUB_REPOSITORY" \
--arg workflowSha "$CALLED_WORKFLOW_SHA" \
'
.version == 1 and
.repository == $repository and
.workflowSha == $workflowSha and
.candidateSha == $candidateSha and
.candidateTree == $candidateTree and
.archiveRoot == "openclaw-telegram-candidate" and
.sourceJob == "build_candidate"
' "${extract_root}/manifest.json" >/dev/null
[[ -d "$candidate_root/node_modules" && -f "$candidate_root/dist/index.js" ]]
sudo chown root:root "$extract_root"
sudo chmod 0711 "$extract_root"
echo "candidate_root=$candidate_root" >>"$GITHUB_OUTPUT"
- name: Revalidate candidate release provenance
id: revalidate_provenance
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.target_ref }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
candidate_sha="$TARGET_SHA"
pr_head_count="$(
gh api \
-H "Accept: application/vnd.github+json" \
"repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/pulls" \
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${candidate_sha}"'")] | length'
)"
[[ "$pr_head_count" == "0" ]]
compare_status="$(
gh api \
"repos/${GITHUB_REPOSITORY}/compare/${candidate_sha}...main" \
--jq '.status'
)"
trusted_reason=""
trusted_release_branch=""
if [[ "$compare_status" == "ahead" || "$compare_status" == "identical" ]]; then
trusted_reason="main-ancestor"
else
normalized_ref="${TARGET_REF#refs/heads/}"
if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]]; then
branch_sha="$(
git ls-remote --exit-code --refs origin "refs/heads/${normalized_ref}" |
awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }'
)"
[[ "$branch_sha" == "$candidate_sha" ]]
trusted_reason="release-branch-head"
trusted_release_branch="$normalized_ref"
elif [[ "$TARGET_REF" =~ ^refs/tags/v ]] || [[ "$TARGET_REF" =~ ^v ]]; then
normalized_tag="${TARGET_REF#refs/tags/}"
tag_refs="$(
git ls-remote --exit-code origin \
"refs/tags/${normalized_tag}" "refs/tags/${normalized_tag}^{}"
)"
awk -v sha="$candidate_sha" '$1 == sha { found = 1 } END { exit(found ? 0 : 1) }' \
<<<"$tag_refs"
trusted_reason="release-tag"
elif [[ "$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$candidate_sha" ]]; then
matching_release_branches="$(
gh api --paginate \
"repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \
--jq '.[].name' |
awk '$0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ ||
$0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/ { print }'
)"
if [[ "$(wc -l <<<"$matching_release_branches" | tr -d ' ')" == "1" && -n "$matching_release_branches" ]]; then
trusted_reason="release-branch-head"
trusted_release_branch="$matching_release_branches"
else
matching_release_tags="$(
git ls-remote origin 'refs/tags/v*' |
awk -v sha="$candidate_sha" '$1 == sha { sub(/\^\{\}$/, "", $2); print $2 }' |
sort -u
)"
if [[ -n "$matching_release_tags" ]]; then
trusted_reason="release-tag"
fi
fi
fi
fi
[[ -n "$trusted_reason" ]]
if [[ "$trusted_reason" != "main-ancestor" ]]; then
repository_owner="${GITHUB_REPOSITORY%%/*}"
repository_name="${GITHUB_REPOSITORY#*/}"
signature_json="$(
gh api graphql \
-f query='query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{oid signature{isValid state signer{login}} associatedPullRequests(first:10){nodes{state baseRefName baseRepository{nameWithOwner} mergeCommit{oid} mergedBy{login}}}}}}}' \
-f owner="$repository_owner" \
-f name="$repository_name" \
-f oid="$candidate_sha"
)"
signature_status="$(
jq -er \
--arg sha "$candidate_sha" \
'.data.repository.object |
select(.oid == $sha) |
if .signature == null then "missing"
elif .signature.isValid == true and .signature.state == "VALID" and
(.signature.signer.login // "") != "" then "valid"
else "invalid"
end' \
<<<"$signature_json"
)"
if [[ "$signature_status" == "invalid" ]]; then
echo "Release candidate ${candidate_sha} has an invalid commit signature." >&2
exit 1
fi
signer="$(jq -r '.data.repository.object.signature.signer.login // ""' <<<"$signature_json")"
permission_actor="$signer"
if [[ "$signature_status" == "missing" || "$signer" == "web-flow" ]]; then
if [[ "$trusted_reason" != "release-branch-head" || -z "$trusted_release_branch" ]]; then
echo "Unsigned or GitHub web-flow candidates require an exact release branch head." >&2
exit 1
fi
permission_actor="$(
jq -er \
--arg base "$trusted_release_branch" \
--arg repo "$GITHUB_REPOSITORY" \
--arg sha "$candidate_sha" \
'[.data.repository.object.associatedPullRequests.nodes[] |
select(.state == "MERGED" and .baseRefName == $base and
.baseRepository.nameWithOwner == $repo and .mergeCommit.oid == $sha) |
.mergedBy.login] | unique | select(length == 1) | .[0]' \
<<<"$signature_json"
)"
fi
role_name="$(
gh api \
"repos/${GITHUB_REPOSITORY}/collaborators/${permission_actor}/permission" \
--jq '.role_name'
)"
[[ "$role_name" == "admin" || "$role_name" == "maintain" ]]
fi
- name: Create isolated Telegram SUT identity and launcher
id: create_sut
env:
CALLED_WORKFLOW_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
CANDIDATE_ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
CANDIDATE_ARCHIVE_SHA256: ${{ needs.build_candidate.outputs.archive_sha256 }}
CANDIDATE_ARTIFACT_DIGEST: ${{ needs.build_candidate.outputs.artifact_digest }}
CANDIDATE_ARTIFACT_ID: ${{ needs.build_candidate.outputs.artifact_id }}
CANDIDATE_VERSION: ${{ needs.build_candidate.outputs.candidate_version }}
CANDIDATE_ROOT: ${{ steps.extract_candidate.outputs.candidate_root }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
sut_user="ocqatg${GITHUB_RUN_ID}${GITHUB_RUN_ATTEMPT}"
candidate_extract_root="$(dirname "$CANDIDATE_ROOT")"
candidate_runtime_root="/opt/openclaw-telegram-sut-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
runtime_root="/var/lib/openclaw-telegram-sut-runtime-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
node_bin="${runtime_root}/node"
launcher="/usr/local/bin/openclaw-telegram-sut-launcher"
config="/etc/openclaw-telegram-sut.conf"
preload="${runtime_root}/openclaw-telegram-preentry.mjs"
runner_uid="$(id -u)"
runner_gid="$(id -g)"
runner_home="$(realpath -e "$HOME")"
runner_temp="$(realpath -e "$RUNNER_TEMP")"
source_node_bin="$(realpath -e "$(command -v node)")"
[[ "$candidate_extract_root" == "${RUNNER_TEMP}/openclaw-telegram-executor-extract" ]]
case "$runner_temp/" in
"$runner_home"/*) ;;
*) echo "GitHub runner temp must be contained by runner home." >&2; exit 1 ;;
esac
[[ ! -e "$candidate_runtime_root" && ! -e "$runtime_root" ]]
sudo useradd \
--system \
--user-group \
--no-create-home \
--home-dir /nonexistent \
--shell /usr/sbin/nologin \
"$sut_user"
sudo passwd --lock "$sut_user" >/dev/null
sut_uid="$(id -u "$sut_user")"
sut_gid="$(id -g "$sut_user")"
sudo mv "$CANDIDATE_ROOT" "$candidate_runtime_root"
CANDIDATE_ROOT="$candidate_runtime_root"
sudo install -d -o root -g root -m 0755 "$runtime_root"
sudo install -o root -g root -m 0555 "$source_node_bin" "$node_bin"
[[ "$(stat -c '%F:%a:%u:%g' "$node_bin")" == "regular file:555:0:0" ]]
"$node_bin" --version >/dev/null
sudo install -d -o "$runner_uid" -g "$runner_gid" -m 0711 "$runtime_root/tmp"
# Frozen candidates prefer /tmp/openclaw before TMPDIR. Make that shared path
# unavailable to the runner so they use the isolated runtime fallback instead.
sudo rm -rf -- /tmp/openclaw
sudo install -d -o root -g root -m 0700 /tmp/openclaw
sudo install \
-d \
-o "$runner_uid" \
-g "$runner_gid" \
-m 0711 \
"${runtime_root}/tmp/openclaw-${runner_uid}"
sudo chown -R root:root "$CANDIDATE_ROOT"
sudo chmod -R go-w "$CANDIDATE_ROOT"
sudo chmod -R a+rX "$CANDIDATE_ROOT"
evidence_relative=".trusted-qa-evidence/release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
evidence_root="${CANDIDATE_ROOT}/${evidence_relative}"
boundary_evidence_dir="${evidence_root}/process-boundary"
evidence_artifact_name="release-qa-live-telegram-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TARGET_SHA}"
sudo install -d -o "$runner_uid" -g "$runner_gid" -m 0700 "$evidence_root"
sudo install -d -o "$runner_uid" -g "$runner_gid" -m 0700 "$boundary_evidence_dir"
chmod 0700 "$GITHUB_WORKSPACE"
runner_sentinel="${runtime_root}/runner-sentinel"
printf 'trusted-runner-only\n' | sudo tee "$runner_sentinel" >/dev/null
sudo chown "$runner_uid:$runner_gid" "$runner_sentinel"
sudo chmod 0600 "$runner_sentinel"
printf '%s\n' "$sut_user" | sudo tee -a /etc/cron.deny /etc/at.deny >/dev/null
sudo rm -f \
"/var/lib/systemd/linger/${sut_user}" \
"/var/spool/cron/crontabs/${sut_user}" \
"/var/spool/cron/${sut_user}"
sudo tee "/etc/security/limits.d/99-openclaw-telegram-sut.conf" >/dev/null <<EOF
${sut_user} hard core 0
${sut_user} soft core 0
${sut_user} hard nproc 256
${sut_user} soft nproc 256
EOF
sudo tee "$preload" >/dev/null <<'PRELOAD'
import process from "node:process";
import { isMainThread } from "node:worker_threads";
if (isMainThread) {
if (process.env.OPENCLAW_QA_SUT_PREENTRY_STOP !== "1") {
throw new Error("trusted Telegram SUT preload was not armed");
}
delete process.env.OPENCLAW_QA_SUT_PREENTRY_STOP;
process.kill(process.pid, "SIGSTOP");
}
PRELOAD
sudo chown root:root "$preload"
sudo chmod 0555 "$preload"
{
printf 'SUT_UID=%q\n' "$sut_uid"
printf 'SUT_GID=%q\n' "$sut_gid"
printf 'RUNNER_UID=%q\n' "$runner_uid"
printf 'RUNNER_GID=%q\n' "$runner_gid"
printf 'RUNNER_HOME=%q\n' "$runner_home"
printf 'RUNNER_TEMP_DIR=%q\n' "$runner_temp"
printf 'CANDIDATE_ROOT=%q\n' "$CANDIDATE_ROOT"
printf 'RUNTIME_ROOT=%q\n' "$runtime_root"
printf 'NODE_BIN=%q\n' "$node_bin"
printf 'PRELOAD_PATH=%q\n' "$preload"
printf 'RUNNER_SENTINEL=%q\n' "$runner_sentinel"
printf 'TRUSTED_WORKSPACE=%q\n' "$GITHUB_WORKSPACE"
printf 'EVIDENCE_ROOT=%q\n' "$evidence_root"
} | sudo tee "$config" >/dev/null
sudo chown root:root "$config"
sudo chmod 0600 "$config"
jq -n \
--arg workflowSha "$CALLED_WORKFLOW_SHA" \
--arg targetSha "$TARGET_SHA" \
--arg runId "$GITHUB_RUN_ID" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--arg candidateArtifactId "$CANDIDATE_ARTIFACT_ID" \
--arg candidateArtifactName "$CANDIDATE_ARCHIVE_NAME" \
--arg candidateArtifactDigest "$CANDIDATE_ARTIFACT_DIGEST" \
--arg candidateFileName "$CANDIDATE_ARCHIVE_NAME" \
--arg candidateSha256 "$CANDIDATE_ARCHIVE_SHA256" \
--arg candidateVersion "$CANDIDATE_VERSION" \
'{
version: 1,
kind: "telegram-sut-boundary",
runId: $runId,
runAttempt: $runAttempt,
workflowSha: $workflowSha,
targetSha: $targetSha,
candidateArtifact: {
id: $candidateArtifactId,
name: $candidateArtifactName,
digest: $candidateArtifactDigest,
runId: $runId,
runAttempt: $runAttempt,
fileName: $candidateFileName,
sha256: $candidateSha256,
sourceSha: $targetSha,
version: $candidateVersion
}
}' >"${boundary_evidence_dir}/context.json"
chmod 0600 "${boundary_evidence_dir}/context.json"
sudo tee "$launcher" >/dev/null <<'LAUNCHER'
#!/bin/bash
set -Eeuo pipefail
launcher_stage=entry
trap 'exit_status=$?; trap - ERR; if [[ -n "${launcher_stage_file:-}" && -r "$launcher_stage_file" ]]; then launcher_stage="$(<"$launcher_stage_file")"; fi; printf "Telegram SUT launcher failed: stage=%s line=%s status=%s\n" "$launcher_stage" "$LINENO" "$exit_status" >&2; exit "$exit_status"' ERR
transport_keys=(
HOME
LANG
LC_ALL
OPENCLAW_CLI
OPENAI_API_KEY
OPENAI_BASE_URL
OPENCLAW_ALLOW_SLOW_REPLY_TESTS
OPENCLAW_BUNDLED_PLUGINS_DIR
OPENCLAW_COMPATIBILITY_HOST_VERSION
OPENCLAW_CONFIG_PATH
OPENCLAW_EMBEDDED_ABORT_SETTLE_TIMEOUT_MS
OPENCLAW_GATEWAY_TOKEN
OPENCLAW_HOME
OPENCLAW_NO_RESPAWN
OPENCLAW_OAUTH_DIR
OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER
OPENCLAW_QA_PARENT_PID
OPENCLAW_QA_STAGED_RUNTIME_ROOT
OPENCLAW_QA_TEMP_ROOT
OPENCLAW_SKIP_BROWSER_CONTROL_SERVER
OPENCLAW_SKIP_CANVAS_HOST
OPENCLAW_SKIP_GMAIL_WATCHER
OPENCLAW_SKIP_STARTUP_MODEL_PREWARM
OPENCLAW_STATE_DIR
OPENCLAW_TEST_FAST
PATH
TMPDIR
XDG_CACHE_HOME
XDG_CONFIG_HOME
XDG_DATA_HOME
)
control_keys=(
OPENCLAW_QA_SUT_BOUNDARY_COMMAND_FILE
OPENCLAW_QA_SUT_BOUNDARY_COMMAND_SHA256
OPENCLAW_QA_SUT_BOUNDARY_GENERATION
OPENCLAW_QA_SUT_BOUNDARY_IDENTITY_FILE
OPENCLAW_QA_SUT_BOUNDARY_SANDBOX_FILE
)
load_process_identity() {
source /etc/openclaw-telegram-sut.conf
identity_file="$(realpath -e "$1")"
case "$identity_file" in
"$RUNTIME_ROOT"/tmp/openclaw-qa-suite-*/process-boundary/identity-*.json | \
"$RUNTIME_ROOT"/tmp/openclaw-"$RUNNER_UID"/openclaw-qa-suite-*/process-boundary/identity-*.json) ;;
*) echo "Telegram SUT identity escaped the runtime root." >&2; exit 1 ;;
esac
[[ "$(stat -c '%F:%a:%u:%g' "$identity_file")" == "regular file:640:0:${RUNNER_GID}" ]]
generation="$(jq -er '.generation' "$identity_file")"
pid="$(jq -er '.pid' "$identity_file")"
uid="$(jq -er '.uid' "$identity_file")"
gid="$(jq -er '.gid' "$identity_file")"
start_ticks="$(jq -er '.procStartTicks' "$identity_file")"
pgrp="$(jq -er '.pgrp' "$identity_file")"
command_file="$(jq -er '.commandFile.path' "$identity_file")"
command_sha256="$(jq -er '.commandFile.sha256' "$identity_file")"
[[ "$generation" =~ ^[a-f0-9-]{36}$ &&
"$pid" =~ ^[1-9][0-9]+$ &&
"$uid" == "$SUT_UID" &&
"$gid" == "$SUT_GID" &&
"$start_ticks" =~ ^[0-9]+$ &&
"$pgrp" =~ ^[1-9][0-9]+$ &&
"$command_sha256" =~ ^[a-f0-9]{64}$ ]]
command_file="$(realpath -e "$command_file")"
[[ "$(dirname "$command_file")" == "$(dirname "$identity_file")" ]]
[[ "$(stat -c '%F:%a:%u:%g' "$command_file")" == "regular file:600:${RUNNER_UID}:${RUNNER_GID}" ]]
[[ "$(sha256sum "$command_file" | awk '{print $1}')" == "$command_sha256" ]]
}
assert_live_process_identity() {
proc_stat="$(cat "/proc/${pid}/stat")"
proc_tail="${proc_stat##*) }"
[[ "$(awk '{print $3}' <<<"$proc_tail")" == "$pgrp" ]]
[[ "$(awk '{print $20}' <<<"$proc_tail")" == "$start_ticks" ]]
read -r real_uid effective_uid saved_uid fs_uid < <(
awk '/^Uid:/{print $2, $3, $4, $5}' "/proc/${pid}/status"
)
read -r real_gid effective_gid saved_gid fs_gid < <(
awk '/^Gid:/{print $2, $3, $4, $5}' "/proc/${pid}/status"
)
[[ "$real_uid" == "$SUT_UID" &&
"$effective_uid" == "$SUT_UID" &&
"$saved_uid" == "$SUT_UID" &&
"$fs_uid" == "$SUT_UID" &&
"$real_gid" == "$SUT_GID" &&
"$effective_gid" == "$SUT_GID" &&
"$saved_gid" == "$SUT_GID" &&
"$fs_gid" == "$SUT_GID" ]]
}
send_verified_process_signal() {
signal_name="$1"
/usr/bin/python3 - \
"$pid" "$start_ticks" "$pgrp" "$SUT_UID" "$SUT_GID" "$signal_name" <<'PY'
import os
import signal
import sys
pid = int(sys.argv[1])
expected_start = sys.argv[2]
expected_pgrp = int(sys.argv[3])
expected_uid = int(sys.argv[4])
expected_gid = int(sys.argv[5])
signal_name = sys.argv[6]
signal_value = getattr(signal, signal_name, None)
if signal_value is None:
raise SystemExit(f"unsupported signal: {signal_name}")
pidfd = os.pidfd_open(pid)
try:
stat_tail = open(f"/proc/{pid}/stat", encoding="utf-8").read().rsplit(") ", 1)[1].split()
status = {}
with open(f"/proc/{pid}/status", encoding="utf-8") as handle:
for line in handle:
key, separator, value = line.partition(":")
if separator:
status[key] = value.strip()
uids = [int(value) for value in status["Uid"].split()]
gids = [int(value) for value in status["Gid"].split()]
if (
stat_tail[19] != expected_start
or int(stat_tail[2]) != expected_pgrp
or uids != [expected_uid] * 4
or gids != [expected_gid] * 4
):
raise SystemExit("process identity changed before pidfd signal")
signal.pidfd_send_signal(pidfd, signal_value)
finally:
os.close(pidfd)
PY
}
verify_runtime_identity() {
verification_mode="$1"
case "$verification_mode" in
preentry|live) ;;
*) echo "Unsupported Telegram SUT verification mode: ${verification_mode}" >&2; exit 1 ;;
esac
verification_deadline=$((SECONDS + 30))
while true; do
proc_stat="$(cat "/proc/${pid}/stat")"
proc_tail="${proc_stat##*) }"
state="$(awk '{print $1}' <<<"$proc_tail")"
if [[ "$verification_mode" == "preentry" && ("$state" == "T" || "$state" == "t") ]]; then
break
fi
if [[ "$verification_mode" == "live" &&
"$state" != "T" && "$state" != "t" &&
"$state" != "Z" && "$state" != "X" && "$state" != "x" ]]; then
break
fi
if ((SECONDS >= verification_deadline)); then
echo "Telegram SUT did not reach the required ${verification_mode} state." >&2
exit 1
fi
sleep 0.1
done
assert_live_process_identity
state="$(awk '{print $1}' <<<"$proc_tail")"
if [[ "$verification_mode" == "preentry" ]]; then
[[ "$state" == "T" || "$state" == "t" ]]
else
[[ "$state" != "T" && "$state" != "t" &&
"$state" != "Z" && "$state" != "X" && "$state" != "x" ]]
fi
status="$(cat "/proc/${pid}/status")"
[[ -z "$(awk '/^Groups:/{ $1=""; sub(/^ /, ""); print }' <<<"$status")" ]]
grep -Eq '^NoNewPrivs:[[:space:]]+1$' <<<"$status"
for capability in CapInh CapPrm CapEff CapBnd CapAmb; do
grep -Eq "^${capability}:[[:space:]]+0+$" <<<"$status"
done
temp_root="$(dirname "$(dirname "$identity_file")")"
cwd_relative="$(jq -er '.cwdRelative | select(type == "string" and length > 0)' "$command_file")"
expected_cwd="$(realpath -e "${temp_root}/${cwd_relative}")"
case "$expected_cwd" in
"$temp_root"|"$temp_root"/*) ;;
*) echo "Telegram SUT cwd escaped the runtime root." >&2; exit 1 ;;
esac
actual_cwd="$(realpath -e "/proc/${pid}/cwd")"
[[ "$actual_cwd" == "$expected_cwd" ]]
expected_executable="$(
jq -er '.executable | select(type == "string" and length > 0)' "$command_file"
)"
expected_executable="$(realpath -e "$expected_executable")"
actual_executable="$(realpath -e "/proc/${pid}/exe")"
[[ "$actual_executable" == "$expected_executable" ]]
cmdline_sha256="$(
"$NODE_BIN" --input-type=commonjs - "$pid" "$command_file" <<'NODE'
const { createHash } = require("node:crypto");
const fs = require("node:fs");
const [pid, commandFile] = process.argv.slice(2);
const command = JSON.parse(fs.readFileSync(commandFile, "utf8"));
if (
typeof command.executable !== "string" ||
!Array.isArray(command.argv) ||
!command.argv.every((arg) => typeof arg === "string")
) {
throw new Error("invalid process-boundary command");
}
const expected = Buffer.from(`${[command.executable, ...command.argv].join("\0")}\0`);
const actual = fs.readFileSync(`/proc/${pid}/cmdline`);
if (!actual.equals(expected)) {
throw new Error("process-boundary command line mismatch");
}
process.stdout.write(createHash("sha256").update(actual).digest("hex"));
NODE
)"
jq -cn \
--arg generation "$generation" \
--argjson pid "$pid" \
--argjson uid "$SUT_UID" \
--argjson gid "$SUT_GID" \
--arg procStartTicks "$start_ticks" \
--argjson pgrp "$pgrp" \
--arg state "$state" \
--arg cwd "$actual_cwd" \
--arg executablePath "$actual_executable" \
--arg cmdlineSha256 "$cmdline_sha256" \
'{
version: 1,
generation: $generation,
status: "pass",
pid: $pid,
uid: $uid,
gid: $gid,
procStartTicks: $procStartTicks,
pgrp: $pgrp,
state: $state,
cwd: $cwd,
executablePath: $executablePath,
cmdlineSha256: $cmdlineSha256
}'
}
target_processes_alive() {
sut_uid_processes_alive ||
ps -eo pgrp= | awk -v expected="$pgrp" '$1 == expected { found = 1 } END { exit !found }'
}
sut_uid_processes_alive() {
pgrep -U "$SUT_UID" >/dev/null 2>&1 ||
pgrep -u "$SUT_UID" >/dev/null 2>&1
}
terminate_sut_uid() {
pkill -TERM -U "$SUT_UID" >/dev/null 2>&1 || true
pkill -TERM -u "$SUT_UID" >/dev/null 2>&1 || true
sleep 2
pkill -KILL -U "$SUT_UID" >/dev/null 2>&1 || true
pkill -KILL -u "$SUT_UID" >/dev/null 2>&1 || true
consecutive_empty=0
for _ in $(seq 1 30); do
if sut_uid_processes_alive; then
consecutive_empty=0
else
consecutive_empty=$((consecutive_empty + 1))
if [[ "$consecutive_empty" -ge 2 ]]; then
return 0
fi
fi
sleep 1
done
echo "Telegram SUT UID remained alive after forced shutdown." >&2
return 1
}
if [[ "${1:-}" == "--signal" ]]; then
shift
exec sudo -n -- "$0" --root-signal "$@"
fi
if [[ "${1:-}" == "--terminate" ]]; then
shift
exec sudo -n -- "$0" --root-terminate "$@"
fi
if [[ "${1:-}" == "--terminate-uid" ]]; then
shift
exec sudo -n -- "$0" --root-terminate-uid
fi
if [[ "${1:-}" == "--verify" ]]; then
shift
exec sudo -n -- "$0" --root-verify "$@"
fi
if [[ "${1:-}" == "--root-verify" ]]; then
[[ "$EUID" == "0" ]]
shift
verification_mode="${1:?}"
requested_identity="${2:?}"
load_process_identity "$requested_identity"
verify_runtime_identity "$verification_mode"
exit 0
fi
if [[ "${1:-}" == "--root-signal" ]]; then
[[ "$EUID" == "0" ]]
shift
signal_name="${1:?}"
requested_identity="${2:?}"
case "$signal_name" in
SIGCONT|SIGUSR1|SIGUSR2) ;;
*) echo "Unsupported Telegram SUT signal: ${signal_name}" >&2; exit 1 ;;
esac
load_process_identity "$requested_identity"
assert_live_process_identity
send_verified_process_signal "$signal_name"
exit 0
fi
if [[ "${1:-}" == "--root-terminate" ]]; then
[[ "$EUID" == "0" ]]
shift
load_process_identity "${1:?}"
proxy_pgrp="$(ps -o pgrp= -p "$$" | tr -d ' ')"
[[ "$pgrp" != "$proxy_pgrp" ]]
if [[ -r "/proc/${pid}/stat" ]]; then
assert_live_process_identity
fi
pkill -TERM -U "$SUT_UID" >/dev/null 2>&1 || true
pkill -TERM -u "$SUT_UID" >/dev/null 2>&1 || true
sleep 2
if [[ -r "/proc/${pid}/stat" ]]; then
assert_live_process_identity
fi
pkill -KILL -U "$SUT_UID" >/dev/null 2>&1 || true
pkill -KILL -u "$SUT_UID" >/dev/null 2>&1 || true
consecutive_empty=0
for _ in $(seq 1 30); do
if target_processes_alive; then
consecutive_empty=0
else
consecutive_empty=$((consecutive_empty + 1))
if [[ "$consecutive_empty" -ge 2 ]]; then
exit 0
fi
fi
sleep 1
done
echo "Telegram SUT process group or UID remained alive after forced shutdown." >&2
exit 1
fi
if [[ "${1:-}" == "--root-terminate-uid" ]]; then
[[ "$EUID" == "0" ]]
source /etc/openclaw-telegram-sut.conf
terminate_sut_uid
exit 0
fi
if [[ "${1:-}" != "--root-run" ]]; then
present=()
for key in "${transport_keys[@]}" "${control_keys[@]}"; do
if [[ -v "$key" ]]; then
present+=("$key")
fi
done
sudo_args=(-n)
if ((${#present[@]})); then
preserve_csv="$(IFS=,; printf '%s' "${present[*]}")"
sudo_args+=(--preserve-env="$preserve_csv")
fi
exec sudo "${sudo_args[@]}" -- "$0" --root-run "$@"
fi
shift
launcher_stage=root-run-setup
[[ "$EUID" == "0" ]]
source /etc/openclaw-telegram-sut.conf
declare -A keep_env=()
for key in "${transport_keys[@]}" "${control_keys[@]}"; do
keep_env["$key"]=1
done
while IFS= read -r key; do
if [[ ! -v "keep_env[$key]" ]]; then
unset "$key"
fi
done < <(compgen -e)
unset OLDPWD
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
temp_root="$(realpath -e "${OPENCLAW_QA_TEMP_ROOT:?}")"
case "$temp_root" in
"$RUNTIME_ROOT"/tmp/openclaw-qa-suite-* | \
"$RUNTIME_ROOT"/tmp/openclaw-"$RUNNER_UID"/openclaw-qa-suite-*) ;;
*) echo "SUT temp root escaped the workflow runtime root." >&2; exit 1 ;;
esac
config_path="${temp_root}/openclaw.json"
[[ -f "$config_path" && ! -L "$config_path" ]]
[[ "$(realpath -e "$config_path")" == "$config_path" ]]
export OPENCLAW_QA_TEMP_ROOT="$temp_root"
export HOME="${temp_root}/home"
export OPENCLAW_HOME="$HOME"
export OPENCLAW_STATE_DIR="${temp_root}/state"
export OPENCLAW_OAUTH_DIR="${OPENCLAW_STATE_DIR}/credentials"
export OPENCLAW_CONFIG_PATH="$config_path"
export XDG_CACHE_HOME="${temp_root}/xdg-cache"
export XDG_CONFIG_HOME="${temp_root}/xdg-config"
export XDG_DATA_HOME="${temp_root}/xdg-data"
chown "$RUNNER_UID:$RUNNER_GID" "$temp_root"
chmod 0711 "$temp_root"
chown "$RUNNER_UID:$SUT_GID" "$config_path"
chmod 0640 "$config_path"
sut_tmp="${temp_root}/sut-tmp"
install -d -o "$SUT_UID" -g "$SUT_GID" -m 0700 "$sut_tmp"
export TMPDIR="$sut_tmp"
for path in \
"$temp_root/workspace" \
"${OPENCLAW_HOME:?}" \
"${OPENCLAW_STATE_DIR:?}" \
"${XDG_CACHE_HOME:?}" \
"${XDG_CONFIG_HOME:?}" \
"${XDG_DATA_HOME:?}"; do
[[ -d "$path" && ! -L "$path" ]]
[[ "$(realpath -e "$path")" == "$path" ]]
chown -R "$SUT_UID:$SUT_GID" "$path"
chmod -R u=rwX,go= "$path"
done
if [[ -n "${OPENCLAW_BUNDLED_PLUGINS_DIR:-}" ]]; then
chown -R root:root "$OPENCLAW_BUNDLED_PLUGINS_DIR"
chmod -R a+rX,go-w "$OPENCLAW_BUNDLED_PLUGINS_DIR"
fi
boundary_mode=false
for key in "${control_keys[@]}"; do
if [[ -v "$key" ]]; then
boundary_mode=true
fi
done
expected_env_keys_b64=""
sandbox_payload_b64=""
if [[ "$boundary_mode" == "true" ]]; then
for key in "${control_keys[@]}"; do
[[ -v "$key" ]]
done
command_file="$(realpath -e "$OPENCLAW_QA_SUT_BOUNDARY_COMMAND_FILE")"
identity_file="$(realpath -m "$OPENCLAW_QA_SUT_BOUNDARY_IDENTITY_FILE")"
sandbox_file="$(realpath -m "$OPENCLAW_QA_SUT_BOUNDARY_SANDBOX_FILE")"
generation="$OPENCLAW_QA_SUT_BOUNDARY_GENERATION"
command_sha256="$OPENCLAW_QA_SUT_BOUNDARY_COMMAND_SHA256"
control_dir="$(dirname "$command_file")"
[[ "$control_dir" == "$temp_root/process-boundary" &&
"$(dirname "$identity_file")" == "$control_dir" &&
"$(dirname "$sandbox_file")" == "$control_dir" &&
"$(stat -c '%F:%a:%u:%g' "$command_file")" == "regular file:600:${RUNNER_UID}:${RUNNER_GID}" &&
"$(sha256sum "$command_file" | awk '{print $1}')" == "$command_sha256" ]]
jq -e \
--arg generation "$generation" \
--arg executable "$NODE_BIN" \
--arg preload "$PRELOAD_PATH" \
--arg entry "$CANDIDATE_ROOT/dist/index.js" \
'.version == 1 and
.generation == $generation and
.executable == $executable and
.argv[0:3] == ["--import", $preload, $entry] and
(.envKeys | type == "array" and length > 0)' \
"$command_file" >/dev/null
expected_env_keys_b64="$(jq -r '.envKeys[]' "$command_file" | base64 -w0)"
sandbox_payload_b64="$(
jq -c \
--arg generation "$generation" \
'{version: 1, generation: $generation, status: "pass", envKeys: .envKeys}' \
"$command_file" |
base64 -w0
)"
: >"$sandbox_file"
chown root:"$RUNNER_GID" "$sandbox_file"
chmod 0640 "$sandbox_file"
export OPENCLAW_QA_SUT_PREENTRY_STOP=1
else
generation=""
command_file=""
identity_file=""
sandbox_file=""
command_sha256=""
fi
for key in "${control_keys[@]}"; do
unset "$key"
done
export SUT_UID SUT_GID RUNNER_UID RUNNER_GID RUNNER_HOME RUNNER_TEMP_DIR
export CANDIDATE_ROOT RUNTIME_ROOT NODE_BIN
export PRELOAD_PATH RUNNER_SENTINEL TRUSTED_WORKSPACE EVIDENCE_ROOT
export boundary_mode generation command_file identity_file sandbox_file
export command_sha256 expected_env_keys_b64 sandbox_payload_b64
launcher_stage=enter-mount-namespace
launcher_stage_file="${RUNTIME_ROOT}/launcher-stage-${BASHPID}"
printf '%s\n' "$launcher_stage" >"$launcher_stage_file"
chmod 0600 "$launcher_stage_file"
trap 'rm -f "$launcher_stage_file"' EXIT
export launcher_stage_file
/usr/bin/unshare \
--mount \
--fork \
--propagation private \
/bin/bash -ceu '
set_launcher_stage() {
launcher_stage="$1"
printf "%s\n" "$launcher_stage" >"$launcher_stage_file"
}
set_launcher_stage mask-host-paths
trap "exit_status=\$?; trap - ERR; printf \"Telegram SUT launcher failed: stage=%s line=%s status=%s\\n\" \"\$launcher_stage\" \"\$LINENO\" \"\$exit_status\" >&2; exit \"\$exit_status\"" ERR
for masked_path in "$RUNNER_HOME" /tmp /var/tmp /dev/shm; do
set_launcher_stage "mask-host-path:${masked_path}"
[[ -d "$masked_path" ]]
mount \
-t tmpfs \
-o mode=000,nosuid,nodev,noexec \
openclaw-telegram-mask \
"$masked_path"
done
set_launcher_stage mount-proc
mount -t proc proc /proc -o nosuid,nodev,noexec,hidepid=2
if [[ "$boundary_mode" == "true" ]]; then
set_launcher_stage write-identity
pid="$$"
proc_stat="$(cat "/proc/${pid}/stat")"
proc_tail="${proc_stat##*) }"
pgrp="$(awk "{print \$3}" <<<"$proc_tail")"
start_ticks="$(awk "{print \$20}" <<<"$proc_tail")"
identity_tmp="${identity_file}.tmp-${pid}"
jq -n \
--arg generation "$generation" \
--argjson pid "$pid" \
--argjson uid "$SUT_UID" \
--argjson gid "$SUT_GID" \
--arg procStartTicks "$start_ticks" \
--argjson pgrp "$pgrp" \
--arg commandPath "$command_file" \
--arg commandSha256 "$command_sha256" \
"{
version: 1,
generation: \$generation,
pid: \$pid,
uid: \$uid,
gid: \$gid,
procStartTicks: \$procStartTicks,
pgrp: \$pgrp,
commandFile: {path: \$commandPath, sha256: \$commandSha256}
}" >"$identity_tmp"
chown root:"$RUNNER_GID" "$identity_tmp"
chmod 0640 "$identity_tmp"
mv "$identity_tmp" "$identity_file"
exec 3>"$sandbox_file"
fi
set_launcher_stage launch-runtime
unset launcher_stage_file
exec /usr/bin/prlimit \
--core=0:0 \
--nproc=256:256 \
-- \
/usr/bin/setpriv \
--reuid="$SUT_UID" \
--regid="$SUT_GID" \
--clear-groups \
--no-new-privs \
--inh-caps=-all \
--ambient-caps=-all \
--bounding-set=-all \
--pdeathsig=KILL \
/bin/bash -ceu '\''
runtime_stage=verify-runtime-identity
fail_runtime_stage() {
exit_status="$1"
failure_line="$2"
trap - ERR
printf "Telegram SUT runtime preflight failed: stage=%s line=%s status=%s\n" \
"$runtime_stage" "$failure_line" "$exit_status" >&2
exit "$exit_status"
}
trap "fail_runtime_stage \$? \$LINENO" ERR
status="$(cat /proc/self/status)"
read -r real_uid effective_uid saved_uid fs_uid < <(
awk "/^Uid:/{print \$2, \$3, \$4, \$5}" <<<"$status"
)
read -r real_gid effective_gid saved_gid fs_gid < <(
awk "/^Gid:/{print \$2, \$3, \$4, \$5}" <<<"$status"
)
[[ "$real_uid" == "$SUT_UID" &&
"$effective_uid" == "$SUT_UID" &&
"$saved_uid" == "$SUT_UID" &&
"$fs_uid" == "$SUT_UID" &&
"$real_gid" == "$SUT_GID" &&
"$effective_gid" == "$SUT_GID" &&
"$saved_gid" == "$SUT_GID" &&
"$fs_gid" == "$SUT_GID" ]]
runtime_stage=verify-runtime-privileges
[[ -z "$(awk "/^Groups:/{\$1=\"\"; sub(/^ /, \"\"); print}" <<<"$status")" ]]
[[ "$(id -G)" == "$SUT_GID" ]]
grep -Eq "^NoNewPrivs:[[:space:]]+1$" <<<"$status"
for capability in CapInh CapPrm CapEff CapBnd CapAmb; do
grep -Eq "^${capability}:[[:space:]]+0+$" <<<"$status"
done
[[ "$(ulimit -Sc)" == "0" && "$(ulimit -Hc)" == "0" ]]
! /usr/bin/sudo -n true >/dev/null 2>&1
runtime_stage=verify-parent-proc-hidden
for proc_path in environ mem fd cmdline; do
[[ ! -e "/proc/${OPENCLAW_QA_PARENT_PID:?}/${proc_path}" ]]
done
runtime_stage=verify-proc-visibility
OPENCLAW_QA_PROC_CONTROL=visible sleep 30 &
control_pid="$!"
trap "kill ${control_pid} >/dev/null 2>&1 || true" EXIT
proc_marker_visible=false
# The background child can still expose the shell pre-exec environment.
for _ in {1..100}; do
if tr "\0" "\n" <"/proc/${control_pid}/environ" 2>/dev/null |
grep -Fxq "OPENCLAW_QA_PROC_CONTROL=visible"; then
proc_marker_visible=true
break
fi
kill -0 "$control_pid" >/dev/null 2>&1 || break
sleep 0.05
done
[[ "$proc_marker_visible" == "true" ]]
# Reading the marker is the proof; the probe may exit before cleanup.
kill "$control_pid" >/dev/null 2>&1 || true
wait "$control_pid" || true
trap - EXIT
runtime_stage=verify-secret-env-hidden
! tr "\0" "\n" </proc/self/environ |
grep -Eq "^(OPENCLAW_QA_CONVEX_SECRET[^=]*|OPENCLAW_QA_SUT_FORBIDDEN_SENTINEL|OPENCLAW_QA_TELEGRAM_(GROUP_ID|DRIVER_BOT_TOKEN|SUT_BOT_TOKEN)|GITHUB_[^=]*|ACTIONS_[^=]*|RUNNER_[^=]*|NPM_TOKEN|NODE_AUTH_TOKEN)="
runtime_stage=verify-runner-fds-hidden
while IFS= read -r fd_target; do
[[ "$fd_target" != *"/_runner_file_commands/"* &&
"$fd_target" != *"/github/file_commands/"* ]]
done < <(
for fd in /proc/self/fd/*; do
readlink "$fd" 2>/dev/null || true
done
)
runtime_stage=verify-runtime-files
[[ -r "$CANDIDATE_ROOT/dist/index.js" &&
! -w "$CANDIDATE_ROOT/dist/index.js" &&
-r "${OPENCLAW_CONFIG_PATH:?}" &&
! -w "$OPENCLAW_CONFIG_PATH" ]]
for writable_path in \
"${OPENCLAW_QA_TEMP_ROOT:?}/workspace" \
"${OPENCLAW_HOME:?}" \
"${OPENCLAW_STATE_DIR:?}" \
"${XDG_CACHE_HOME:?}" \
"${XDG_CONFIG_HOME:?}" \
"${XDG_DATA_HOME:?}" \
"${TMPDIR:?}"; do
[[ -d "$writable_path" && -w "$writable_path" ]]
done
runtime_stage=verify-host-paths-hidden
for protected_path in \
"$RUNNER_HOME" \
"$RUNNER_TEMP_DIR" \
"$RUNNER_SENTINEL" \
"$TRUSTED_WORKSPACE" \
"$EVIDENCE_ROOT" \
/tmp \
/var/tmp \
/dev/shm; do
[[ ! -r "$protected_path" && ! -w "$protected_path" ]]
done
runtime_stage=sanitize-runtime-env
runtime_boundary_mode="$boundary_mode"
runtime_candidate_root="$CANDIDATE_ROOT"
runtime_expected_env_keys_b64="$expected_env_keys_b64"
runtime_node_bin="$NODE_BIN"
runtime_preload_path="$PRELOAD_PATH"
runtime_sandbox_payload_b64="$sandbox_payload_b64"
unset \
CANDIDATE_ROOT \
EVIDENCE_ROOT \
NODE_BIN \
PRELOAD_PATH \
RUNNER_GID \
RUNNER_HOME \
RUNNER_SENTINEL \
RUNNER_TEMP_DIR \
RUNNER_UID \
RUNTIME_ROOT \
SUT_GID \
SUT_UID \
TRUSTED_WORKSPACE \
boundary_mode \
command_file \
command_sha256 \
expected_env_keys_b64 \
generation \
identity_file \
sandbox_file \
sandbox_payload_b64
if [[ "$runtime_boundary_mode" == "true" ]]; then
runtime_stage=verify-runtime-env
actual_env_keys_b64="$(
env |
cut -d= -f1 |
grep -Ev "^(PWD|SHLVL|_)$" |
sort -u |
base64 -w0
)"
[[ "$actual_env_keys_b64" == "$runtime_expected_env_keys_b64" ]]
runtime_stage=write-sandbox-proof
printf "%s" "$runtime_sandbox_payload_b64" | base64 -d >&3
exec 3>&-
runtime_stage=exec-runtime
exec "$runtime_node_bin" \
--import "$runtime_preload_path" \
"$runtime_candidate_root/dist/index.js" \
"$@"
fi
runtime_stage=exec-runtime
exec "$runtime_node_bin" "$runtime_candidate_root/dist/index.js" "$@"
'\'' openclaw-sut "$@"
' openclaw-sut "$@"
LAUNCHER
sudo chown root:root "$launcher"
sudo chmod 0755 "$launcher"
forwarded_env_keys="$(
sed -n '/^transport_keys=(/,/^)/p' "$launcher" |
sed -n '2,$p' |
sed '$d' |
sed 's/^[[:space:]]*//'
)"
forwarded_env_keys="$(paste -sd, <<<"$forwarded_env_keys")"
{
echo "candidate_root=$CANDIDATE_ROOT"
echo "candidate_extract_root=$candidate_extract_root"
echo "boundary_evidence_dir=$boundary_evidence_dir"
echo "evidence_relative=$evidence_relative"
echo "evidence_root=$evidence_root"
echo "evidence_artifact_name=$evidence_artifact_name"
echo "forwarded_env_keys=$forwarded_env_keys"
echo "gid=$sut_gid"
echo "launcher=$launcher"
echo "preload=$preload"
echo "runtime_root=$runtime_root"
echo "runtime_executable=$node_bin"
echo "uid=$sut_uid"
echo "user=$sut_user"
} >>"$GITHUB_OUTPUT"
- name: Validate required QA credential env
id: validate_credentials
env:
CREDENTIAL_ACQUIRE_TIMEOUT_MS: "60000"
JOB_TIMEOUT_MINUTES: "60"
LEASE_TTL_MS: "7200000"
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
RUNNER_ENVIRONMENT: ${{ runner.environment }}
shell: bash
run: |
set -euo pipefail
[[ "$RUNNER_ENVIRONMENT" == "github-hosted" ]]
((JOB_TIMEOUT_MINUTES * 60 * 1000 < LEASE_TTL_MS))
[[ -n "${OPENCLAW_QA_CONVEX_SITE_URL// }" ]]
[[ -n "${OPENCLAW_QA_CONVEX_SECRET_CI// }" ]]
- name: Run Telegram live lane
id: run_lane
env:
CANDIDATE_ROOT: ${{ steps.create_sut.outputs.candidate_root }}
EVIDENCE_RELATIVE: ${{ steps.create_sut.outputs.evidence_relative }}
EVIDENCE_ROOT: ${{ steps.create_sut.outputs.evidence_root }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "60000"
OPENCLAW_QA_CREDENTIAL_LEASE_TTL_MS: "7200000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_SUT_FORBIDDEN_SENTINEL: trusted-parent-${{ github.run_id }}-${{ github.run_attempt }}
OPENCLAW_QA_TELEGRAM_SUT_FORWARDED_ENV_KEYS: ${{ steps.create_sut.outputs.forwarded_env_keys }}
OPENCLAW_QA_TELEGRAM_SUT_CLEANUP_TIMEOUT_MS: "60000"
OPENCLAW_QA_TELEGRAM_SUT_GID: ${{ steps.create_sut.outputs.gid }}
OPENCLAW_QA_TELEGRAM_SUT_OPENCLAW_COMMAND: ${{ steps.create_sut.outputs.launcher }}
OPENCLAW_QA_TELEGRAM_SUT_PRELOAD_PATH: ${{ steps.create_sut.outputs.preload }}
OPENCLAW_QA_TELEGRAM_SUT_PROCESS_BOUNDARY_DIR: ${{ steps.create_sut.outputs.boundary_evidence_dir }}
OPENCLAW_QA_TELEGRAM_SUT_RUNTIME_EXECUTABLE: ${{ steps.create_sut.outputs.runtime_executable }}
OPENCLAW_QA_TELEGRAM_SUT_UID: ${{ steps.create_sut.outputs.uid }}
SUT_RUNTIME_ROOT: ${{ steps.create_sut.outputs.runtime_root }}
SUT_UID: ${{ steps.create_sut.outputs.uid }}
TARGET_SHA: ${{ inputs.target_sha }}
shell: bash
run: |
set -euo pipefail
output_dir="$EVIDENCE_ROOT"
terminate_sut_uid() {
"$OPENCLAW_QA_TELEGRAM_SUT_OPENCLAW_COMMAND" --terminate-uid
}
terminate_sut_uid_on_exit() {
status="$?"
trap - EXIT
if ! terminate_sut_uid; then
status=1
fi
exit "$status"
}
trap terminate_sut_uid_on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
{
printf 'trusted_harness_sha=%s\n' "${{ needs.trusted_identity.outputs.workflow_sha }}"
printf 'candidate_runtime_plugin_sha=%s\n' "$TARGET_SHA"
printf 'trusted_scenario_source=verified_trusted_workflow_sha\n'
} >"${output_dir}/source-attestation.txt"
echo "output_dir=$output_dir" >>"$GITHUB_OUTPUT"
run_qa_attempt() (
set -euo pipefail
exec 2>&1
output_name="$1"
shift
workflow_command_token="$(openssl rand -hex 32)"
printf '::stop-commands::%s\n' "$workflow_command_token"
trap 'printf "::%s::\n" "$workflow_command_token"' EXIT
set +e
TMPDIR="${SUT_RUNTIME_ROOT}/tmp" \
pnpm openclaw qa telegram \
--repo-root "$CANDIDATE_ROOT" \
--output-dir "${EVIDENCE_RELATIVE}/${output_name}" \
--provider-mode mock-openai \
--model mock-openai/gpt-5.6-luna \
--alt-model mock-openai/gpt-5.6-luna-alt \
--fast \
--credential-source convex \
--credential-role ci \
"$@"
status="$?"
set -e
exit "$status"
)
if ! run_qa_attempt preflight --scenario channel-canary; then
echo "Telegram channel canary failed; skipping the remaining scenarios." >&2
exit 1
fi
terminate_sut_uid
remaining_scenarios=()
while IFS=$'\t' read -r scenario_id default_label _; do
if [[ "$default_label" == "default" && "$scenario_id" != "channel-canary" ]]; then
remaining_scenarios+=(--scenario "$scenario_id")
fi
done < <(
TMPDIR="${SUT_RUNTIME_ROOT}/tmp" \
pnpm openclaw qa telegram \
--repo-root "$CANDIDATE_ROOT" \
--provider-mode mock-openai \
--list-scenarios
)
((${#remaining_scenarios[@]} > 0))
for attempt in 1 2; do
if run_qa_attempt "attempt-${attempt}" "${remaining_scenarios[@]}"; then
exit 0
fi
terminate_sut_uid
if [[ "$attempt" == "2" ]]; then
exit 1
fi
echo "Telegram live lane failed on attempt ${attempt}; retrying once..." >&2
done
- name: Terminate isolated Telegram SUT
id: terminate_sut
if: always() && steps.create_sut.outputs.uid != ''
env:
SUT_UID: ${{ steps.create_sut.outputs.uid }}
shell: bash
run: |
set -euo pipefail
sudo pkill -TERM -U "$SUT_UID" >/dev/null 2>&1 || true
sudo pkill -TERM -u "$SUT_UID" >/dev/null 2>&1 || true
sleep 3
sudo pkill -KILL -U "$SUT_UID" >/dev/null 2>&1 || true
sudo pkill -KILL -u "$SUT_UID" >/dev/null 2>&1 || true
consecutive_empty=0
for _ in $(seq 1 30); do
pids="$(
{
pgrep -U "$SUT_UID" || true
pgrep -u "$SUT_UID" || true
} | sort -u
)"
if [[ -z "$pids" ]]; then
consecutive_empty=$((consecutive_empty + 1))
if [[ "$consecutive_empty" -ge 2 ]]; then
echo "quiescent=true" >>"$GITHUB_OUTPUT"
exit 0
fi
else
consecutive_empty=0
sudo kill -KILL $pids >/dev/null 2>&1 || true
fi
sleep 1
done
echo "Isolated Telegram SUT UID still owns processes after terminal cleanup." >&2
exit 1
- name: Finalize trusted Telegram process-boundary evidence
id: finalize_boundary
if: always() && steps.terminate_sut.outputs.quiescent == 'true' && steps.run_lane.outputs.output_dir != ''
env:
BOUNDARY_EVIDENCE_DIR: ${{ steps.create_sut.outputs.boundary_evidence_dir }}
OUTPUT_DIR: ${{ steps.run_lane.outputs.output_dir }}
shell: bash
run: |
set -euo pipefail
context_path="${BOUNDARY_EVIDENCE_DIR}/context.json"
runtime_path="${BOUNDARY_EVIDENCE_DIR}/runtime-boundary.json"
aggregate_name="telegram-sut-boundary-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json"
aggregate_path="${OUTPUT_DIR}/${aggregate_name}"
shopt -s nullglob
retain_lease_paths=("${BOUNDARY_EVIDENCE_DIR}"/retain-credential-lease-*.json)
((${#retain_lease_paths[@]} == 0))
runtime_parts=("${BOUNDARY_EVIDENCE_DIR}"/runtime-boundary-*.json)
((${#runtime_parts[@]} > 0))
jq -s \
'{
version: 1,
kind: "qa-gateway-process-boundary",
launches: [.[].launches[]]
}' \
"${runtime_parts[@]}" >"${runtime_path}.tmp"
chmod 0600 "${runtime_path}.tmp"
mv "${runtime_path}.tmp" "$runtime_path"
jq -e \
--arg runId "$GITHUB_RUN_ID" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
'
.version == 1 and
.kind == "telegram-sut-boundary" and
.runId == $runId and
.runAttempt == $runAttempt and
(.workflowSha | test("^[a-f0-9]{40}$")) and
(.targetSha | test("^[a-f0-9]{40}$")) and
(.candidateArtifact.id | test("^[1-9][0-9]*$")) and
(.candidateArtifact.name | length > 0) and
(.candidateArtifact.digest | test("^[a-f0-9]{64}$")) and
.candidateArtifact.runId == $runId and
.candidateArtifact.runAttempt == $runAttempt and
(.candidateArtifact.fileName | length > 0) and
(.candidateArtifact.sha256 | test("^[a-f0-9]{64}$")) and
(.candidateArtifact.sourceSha | test("^[a-f0-9]{40}$")) and
(.candidateArtifact.version | length > 0)
' "$context_path" >/dev/null
jq -e \
'
.version == 1 and
.kind == "qa-gateway-process-boundary" and
(.launches | type == "array" and length > 0) and
all(
.launches[];
(.generation | test("^[a-f0-9-]{36}$")) and
(.pid | type == "number" and . > 1) and
(.procStartTicks | test("^[0-9]+$")) and
(.pgrp | type == "number" and . > 1) and
(.executablePath | length > 0) and
(.preEntryCmdlineSha256 | test("^[a-f0-9]{64}$")) and
(.commandFile.path | length > 0) and
(.commandFile.sha256 | test("^[a-f0-9]{64}$")) and
(.identityFile | length > 0) and
(.sandboxFile | length > 0) and
(.acceptedAt | length > 0) and
(.exitedAt | length > 0) and
(.quiescedAt | length > 0) and
(
(
.terminalState == "ready-exited" and
(.readyAt | length > 0)
) or
(
.terminalState == "failed-before-ready" and
(.readyAt == null)
)
)
) and
([.launches[] | select(.terminalState == "ready-exited")] | length >= 2)
' "$runtime_path" >/dev/null
while IFS=$'\t' read -r relative_path expected_sha256; do
evidence_path="$(realpath -e "${BOUNDARY_EVIDENCE_DIR}/${relative_path}")"
case "$evidence_path" in
"${BOUNDARY_EVIDENCE_DIR}"/*) ;;
*) echo "Process-boundary evidence escaped its trusted directory." >&2; exit 1 ;;
esac
[[ "$(stat -c '%F:%a' "$evidence_path")" == "regular file:600" ]]
if [[ -n "$expected_sha256" ]]; then
[[ "$(sha256sum "$evidence_path" | awk '{print $1}')" == "$expected_sha256" ]]
fi
done < <(
jq -r '
.launches[] |
[.commandFile.path, .commandFile.sha256],
[.identityFile, ""],
[.sandboxFile, ""] |
@tsv
' "$runtime_path"
)
aggregate_tmp="${aggregate_path}.tmp"
jq -s '
.[0] + {
runtimeBoundary: (
.[1] |
.launches |= map(
.commandFile.path = ("process-boundary/" + .commandFile.path) |
.identityFile = ("process-boundary/" + .identityFile) |
.sandboxFile = ("process-boundary/" + .sandboxFile)
)
)
}
' \
"$context_path" \
"$runtime_path" >"$aggregate_tmp"
chmod 0600 "$aggregate_tmp"
mv "$aggregate_tmp" "$aggregate_path"
rm -f "$context_path" "$runtime_path" "${runtime_parts[@]}"
echo "aggregate_path=$aggregate_path" >>"$GITHUB_OUTPUT"
- name: Validate trusted Telegram evidence
id: validate_evidence
if: always() && steps.finalize_boundary.outputs.aggregate_path != ''
env:
OUTPUT_DIR: ${{ steps.run_lane.outputs.output_dir }}
shell: bash
run: |
set -euo pipefail
python3 scripts/release-telegram-candidate-archive.py validate-tree \
"$OUTPUT_DIR" \
--max-entries 100000 \
--max-apparent-bytes 2147483648
- name: Remove isolated Telegram SUT identity
id: remove_sut
if: always() && steps.terminate_sut.outputs.quiescent == 'true'
env:
SUT_USER: ${{ steps.create_sut.outputs.user }}
shell: bash
run: |
set -euo pipefail
sudo userdel "$SUT_USER"
sudo rm -f \
/etc/openclaw-telegram-sut.conf \
/etc/security/limits.d/99-openclaw-telegram-sut.conf \
/usr/local/bin/openclaw-telegram-sut-launcher
- name: Upload Telegram QA artifacts
id: upload_evidence
if: always() && steps.validate_evidence.outcome == 'success'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ steps.create_sut.outputs.evidence_artifact_name }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: error
- name: Remove isolated Telegram runtime files
id: cleanup_runtime
if: always() && steps.create_sut.outputs.runtime_root != ''
env:
CANDIDATE_ROOT: ${{ steps.create_sut.outputs.candidate_root }}
CANDIDATE_EXTRACT_ROOT: ${{ steps.create_sut.outputs.candidate_extract_root }}
RUNTIME_ROOT: ${{ steps.create_sut.outputs.runtime_root }}
shell: bash
run: |
set -euo pipefail
[[ "$RUNTIME_ROOT" == "/var/lib/openclaw-telegram-sut-runtime-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" ]]
[[ "$CANDIDATE_ROOT" == "/opt/openclaw-telegram-sut-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" ]]
[[ "$CANDIDATE_EXTRACT_ROOT" == "${RUNNER_TEMP}/openclaw-telegram-executor-extract" ]]
sudo rm -rf -- "$RUNTIME_ROOT"
sudo rm -rf -- "$CANDIDATE_ROOT"
sudo rm -rf -- "$CANDIDATE_EXTRACT_ROOT"
- name: Record Telegram execution status
id: record_status
if: always()
env:
OUTCOMES: >-
${{ steps.require_attestation.outcome }}
${{ steps.build_harness.outcome }}
${{ steps.metadata.outcome }}
${{ steps.download.outcome }}
${{ steps.extract_candidate.outcome }}
${{ steps.revalidate_provenance.outcome }}
${{ steps.create_sut.outcome }}
${{ steps.validate_credentials.outcome }}
${{ steps.run_lane.outcome }}
${{ steps.terminate_sut.outcome }}
${{ steps.finalize_boundary.outcome }}
${{ steps.validate_evidence.outcome }}
${{ steps.remove_sut.outcome }}
${{ steps.upload_evidence.outcome }}
${{ steps.cleanup_runtime.outcome }}
shell: bash
run: |
set -euo pipefail
status=success
for outcome in $OUTCOMES; do
[[ "$outcome" == "success" ]] || status=failure
done
echo "status=$status" >>"$GITHUB_OUTPUT"
advisory_status:
name: Record Telegram release-check status
needs: [trusted_identity, build_candidate, attest_candidate, run_telegram]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
contents: read
steps:
- name: Record advisory status
id: record_status
env:
ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
ARCHIVE_SHA256: ${{ needs.build_candidate.outputs.archive_sha256 }}
ATTESTATION_STATUS: ${{ needs.attest_candidate.outputs.status }}
BUILD_RESULT: ${{ needs.build_candidate.result }}
BUILD_STATUS: ${{ needs.build_candidate.outputs.status }}
CANDIDATE_ARTIFACT_DIGEST: ${{ needs.build_candidate.outputs.artifact_digest }}
CANDIDATE_ARTIFACT_ID: ${{ needs.build_candidate.outputs.artifact_id }}
CANDIDATE_VERSION: ${{ needs.build_candidate.outputs.candidate_version }}
EVIDENCE_ARTIFACT_DIGEST: ${{ needs.run_telegram.outputs.evidence_artifact_digest }}
EVIDENCE_ARTIFACT_ID: ${{ needs.run_telegram.outputs.evidence_artifact_id }}
EVIDENCE_ARTIFACT_NAME: ${{ needs.run_telegram.outputs.evidence_artifact_name }}
EXECUTION_STATUS: ${{ needs.run_telegram.outputs.status }}
IDENTITY_RESULT: ${{ needs.trusted_identity.result }}
IDENTITY_STATUS: ${{ needs.trusted_identity.outputs.status }}
RUN_RESULT: ${{ needs.run_telegram.result }}
TARGET_SHA: ${{ inputs.target_sha }}
WORKFLOW_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
ATTESTATION_RESULT: ${{ needs.attest_candidate.result }}
shell: bash
run: |
set -euo pipefail
status=failure
if [[ "$IDENTITY_RESULT" == "cancelled" ||
"$BUILD_RESULT" == "cancelled" ||
"$ATTESTATION_RESULT" == "cancelled" ||
"$RUN_RESULT" == "cancelled" ]]; then
status=cancelled
elif [[ "$IDENTITY_STATUS" == "success" &&
"$BUILD_STATUS" == "success" &&
"$ATTESTATION_STATUS" == "success" &&
"$EXECUTION_STATUS" == "success" ]]; then
status=success
fi
candidate_artifact=null
if [[ "$CANDIDATE_ARTIFACT_ID" =~ ^[1-9][0-9]*$ &&
"$CANDIDATE_ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ &&
"$ARCHIVE_NAME" == "release-telegram-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TARGET_SHA}.tar.zst" &&
"$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ &&
-n "$CANDIDATE_VERSION" ]]; then
candidate_artifact="$(
jq -cn \
--arg id "$CANDIDATE_ARTIFACT_ID" \
--arg name "$ARCHIVE_NAME" \
--arg digest "$CANDIDATE_ARTIFACT_DIGEST" \
--arg runId "$GITHUB_RUN_ID" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--arg fileName "$ARCHIVE_NAME" \
--arg sha256 "$ARCHIVE_SHA256" \
--arg sourceSha "$TARGET_SHA" \
--arg version "$CANDIDATE_VERSION" \
'{
id: $id,
name: $name,
digest: $digest,
runId: $runId,
runAttempt: $runAttempt,
fileName: $fileName,
sha256: $sha256,
sourceSha: $sourceSha,
version: $version
}'
)"
fi
if [[ "$status" == "success" ]]; then
[[ "$candidate_artifact" != "null" &&
"$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ &&
"$TARGET_SHA" =~ ^[a-f0-9]{40}$ &&
"$EVIDENCE_ARTIFACT_ID" =~ ^[1-9][0-9]*$ &&
"$EVIDENCE_ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ &&
"$EVIDENCE_ARTIFACT_NAME" == "release-qa-live-telegram-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${TARGET_SHA}" ]]
fi
status_dir=".artifacts/release-check-status"
status_file="${status_dir}/qa_live_telegram_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.env"
evidence_file="${status_dir}/qa_live_telegram_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json"
mkdir -p "$status_dir"
step_outcomes="identity:${IDENTITY_STATUS} build:${BUILD_STATUS} attest:${ATTESTATION_STATUS} execute:${EXECUTION_STATUS}"
{
printf 'run_id=%s\n' "$GITHUB_RUN_ID"
printf 'run_attempt=%s\n' "$GITHUB_RUN_ATTEMPT"
printf 'target_sha=%s\n' "$TARGET_SHA"
printf 'workflow_sha=%s\n' "$WORKFLOW_SHA"
printf 'job=%s\n' "qa_live_telegram_release_checks"
printf 'variant=\n'
printf 'status=%s\n' "$status"
printf 'job_status=%s\n' "$RUN_RESULT"
printf 'step_outcomes=%s\n' "$step_outcomes"
} >"$status_file"
jq -n \
--arg status "$status" \
--arg jobStatus "$RUN_RESULT" \
--arg identityOutcome "identity:${IDENTITY_STATUS}" \
--arg buildOutcome "build:${BUILD_STATUS}" \
--arg attestOutcome "attest:${ATTESTATION_STATUS}" \
--arg executeOutcome "execute:${EXECUTION_STATUS}" \
--arg runId "$GITHUB_RUN_ID" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--arg workflowSha "$WORKFLOW_SHA" \
--arg targetSha "$TARGET_SHA" \
--arg evidenceId "$EVIDENCE_ARTIFACT_ID" \
--arg evidenceName "$EVIDENCE_ARTIFACT_NAME" \
--arg evidenceDigest "$EVIDENCE_ARTIFACT_DIGEST" \
--argjson candidateArtifact "$candidate_artifact" \
'{
version: 1,
kind: "release-check-status",
job: "qa_live_telegram_release_checks",
status: $status,
jobStatus: $jobStatus,
stepOutcomes: [
$identityOutcome,
$buildOutcome,
$attestOutcome,
$executeOutcome
],
runId: $runId,
runAttempt: $runAttempt,
workflowSha: $workflowSha,
targetSha: $targetSha,
evidenceArtifact: {
id: $evidenceId,
name: $evidenceName,
digest: $evidenceDigest,
runId: $runId,
runAttempt: $runAttempt
},
candidateArtifact: $candidateArtifact
}' >"$evidence_file"
{
echo "status=$status"
echo "status_file=$status_file"
echo "evidence_file=$evidence_file"
} >>"$GITHUB_OUTPUT"
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-telegram-${{ inputs.target_sha }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ steps.record_status.outputs.status_file }}
${{ steps.record_status.outputs.evidence_file }}
retention-days: 14
if-no-files-found: error
- name: Require successful Telegram release check
if: always()
env:
STATUS: ${{ steps.record_status.outputs.status }}
shell: bash
run: |
set -euo pipefail
[[ "$STATUS" == "success" ]]