est/ci-workflows v1:公司级 reusable 模板(checkout/node-quality/oci-build-push-verify)与 tools/ci 上移

迁移自 mu-ref/est-ci-reusable(模板三修复版:est-bundle CA、verify 解释器
无关、build-arg 无内嵌引号、revision-arg-name、install+quality 单容器、
verdaccio add-host)+ Est-Infra 的 source-policy/REGISTER/ci-stats 上移;
全部去项目专名。架构依据 coordination runbooks/ci-repo-architecture.md。
This commit is contained in:
zcode_mulm
2026-08-27 12:07:51 +08:00
commit b3b4e2b391
7 changed files with 516 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# CI helper image register (digest-pinned)
Rules:
- Workflows MUST reference helper images by index digest (`@sha256:...`), never by bare tag.
- Every bake updates the table below and pushes `est/<name>:vN` (tag kept for humans; digest is the identity).
- Bake Dockerfiles live under `tools/ci/helper/` (to be added in P1; current images were baked on tn by zcode during M1-416).
| name | tag | index digest | amd64 manifest digest | size | contents |
|---|---|---|---|---|---|
| ci-node-bookworm | v1 | sha256:78c01b0be47ebc60c0fa940110d66c60ca4e57f0fa57178bb88788e4ea131b61 | sha256:76cf59f18cca34a25c864b9305f42983e56aa5d3c918657827822a83d794dc35 | 585MB | node + mirrored apt, docker-ce-cli (tuna docker-ce apt, GPG-verified), chromium runtime GUI libs, git/perl (F1 foundation) |
| ci-node-alpine | v1 | sha256:a08d2cea4d69e3cf831be410e5e62a72a55a9bf8e9b0c3fcef4df553f9f181e6 | sha256:f4795742c2e53d6df221659af81a385e887d60873637ba34ce36c86f13261a6c | 86MB | node + alpine docker-cli, mirrors pre-swapped (F2) |
## Usage conventions (proven on the business-repo pipelines)
- Mounts (verify/build containers): docker.sock, `$HOME/.docker/config.json` (ro), host CA (ro), host buildx plugin (ro), workspace.
- Env: `SSL_CERT_FILE` + `NODE_EXTRA_CA_CERTS` point at the mounted CA.
- Cache dirs: directory level 0777 only; never `chmod -R` (selection files are validated 0600).
- Registry references: bare repo + `@digest` (docker 29 rejects `repo:tag@digest`).
## TODO (P1)
- Add the two Dockerfiles under `tools/ci/helper/` so rebakes are reproducible.
- Record bake receipts (apt/apk mirror used, timings) per bake in a changelog.
+106
View File
@@ -0,0 +1,106 @@
# Canonical source-selection policy for est CI — the SINGLE implementation.
# Canonical home: est/ci-workflows tools/ci/source-policy.sh (est 公司级唯一实现;
# 项目仓经调用或同步使用,不得各自 fork)。
#
# Profiles:
# host — helper-container installs that can reach the local pull-through
# cache (npm.cache.est) and trust the est CA bundle.
# hermetic — in-Dockerfile installs with direct internet only (https only,
# no local cache: build containers cannot resolve or trust it).
#
# Integrity properties (unchanged from the CNB-era contract):
# - candidates are a fixed, policy-ordered allowlist;
# - a candidate is only used after a COMPLETENESS probe: registry metadata
# must serve exact pinned versions+integrity for the probe packages AND
# the heaviest lockfile tarballs (including platform variants) must exist;
# - speed never reorders candidates (a fast-but-incomplete mirror must not
# win — this exact bug shipped npmmirror's missing platform tarballs);
# - probes retry with bounded backoff; consumers fall back per policy order.
#
# NOTE (2026-08-26, cache-first posture): with the tn verdaccio pull-through
# cache live (npm.cache.est:4873), the host profile resolves locally first and
# probes are a FALLBACK only — see coordination runbook
# runbooks/build-download-source-selection.md (cache-first revision).
#
# Outputs:
# policy_npm_candidates <profile> [lockfile]
# -> newline-separated ordered valid registry URLs on stdout.
# also persisted to ${POLICY_STATE_DIR:-$TMPDIR}/npm-candidates for later
# stages (build.sh reuses the same validated order).
POLICY_NPM_HOST_ORDER='https://npm.cache.est:4873 https://registry.npmjs.org https://mirrors.cloud.tencent.com/npm https://registry.npmmirror.com'
POLICY_NPM_HERMETIC_ORDER='https://mirrors.cloud.tencent.com/npm https://registry.npmjs.org'
POLICY_PROBE_ATTEMPTS=2
POLICY_PROBE_BACKOFF_S=10
# Probe packages: exact version+integrity must match (same anchors the
# frozen toolchain uses).
POLICY_PROBE_PACKAGES='pnpm@11.0.0:sha512-W9GHUA5JzGw9iR2XO0MsArhEpetyCRcskKUXo+9PV57Vwj1Am2meap3EGP97KxiQ5j9tdPHT/EmEjzd3nInITA== tsx@4.23.0:sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w=='
policy_probe_registry() {
# $1 registry, $2 lockfile (optional). Prints "ok" or fails silently.
node - "$1" "$2" <<'NODE'
const [registry, lockfile] = process.argv.slice(2);
const probes = [
['pnpm', '11.0.0', 'sha512-W9GHUA5JzGw9iR2XO0MsArhEpetyCRcskKUXo+9PV57Vwj1Am2meap3EGP97KxiQ5j9tdPHT/EmEjzd3nInITA=='],
['tsx', '4.23.0', 'sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w=='],
];
// Heaviest lockfile tarballs (incl. platform variants) — the completeness
// signal that metadata-only probes cannot see.
let heavy = [
'@embedded-postgres/linux-x64/-/linux-x64-18.4.0-beta.17.tgz',
'@embedded-postgres/darwin-arm64/-/darwin-arm64-18.4.0-beta.17.tgz',
];
try {
const lock = require('node:fs').readFileSync(lockfile, 'utf8');
const sizes = [];
for (const m of lock.matchAll(/"resolution":\s*\{[^}]*\}[^}]*"size":\s*(\d+)/g)) sizes.push(Number(m[1]));
} catch { /* lockfile optional; anchors above stay */ }
const targets = [];
for (const [name, version, integrity] of probes) targets.push(`${registry}/${name}/${version}|meta|${version}|${integrity}`);
for (const t of heavy) targets.push(`${registry}/${t}|tar`);
(async () => {
for (const target of targets) {
const [url, kind, version, integrity] = target.split('|');
const r = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(15000) });
if (kind === 'meta') {
if (!r.ok) process.exit(1);
const body = await r.json();
if (body?.version !== version || body?.dist?.integrity !== integrity) process.exit(1);
} else {
await r.body?.cancel();
if (r.status !== 200) process.exit(1);
}
}
process.stdout.write('ok');
})().catch(() => process.exit(1));
NODE
}
policy_npm_candidates() {
profile="${1:-host}"
lockfile="${2:-}"
case "$profile" in
host) order="$POLICY_NPM_HOST_ORDER" ;;
hermetic) order="$POLICY_NPM_HERMETIC_ORDER" ;;
*) printf '' ; return 1 ;;
esac
valid=""
for registry in $order; do
attempt=1
while [ "$attempt" -le "$POLICY_PROBE_ATTEMPTS" ]; do
if [ "$(policy_probe_registry "$registry" "$lockfile" 2>/dev/null)" = "ok" ]; then
valid="${valid}${registry}\n"
printf '%s\n' "policy_probe kind=npm host=$(printf '%s' "$registry" | sed 's#https://##;s#/$##') profile=$profile status=accepted attempt=$attempt" >&2
break
fi
printf '%s\n' "policy_probe kind=npm host=$(printf '%s' "$registry" | sed 's#https://##;s#/$##') profile=$profile status=failed attempt=$attempt" >&2
attempt=$((attempt + 1))
[ "$attempt" -le "$POLICY_PROBE_ATTEMPTS" ] && sleep "$POLICY_PROBE_BACKOFF_S"
done
done
if [ -z "$valid" ]; then
printf '%s\n' "policy_probe kind=npm status=failed all-candidates-rejected" >&2
return 1
fi
printf '%b' "$valid" | tee "${POLICY_STATE_DIR:-${TMPDIR:-/tmp}}/npm-candidates" 2>/dev/null || printf '%b' "$valid"
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# ci-stats.sh — Gitea Actions run statistics (failure watch + duration trend).
# Run from ts, tn, or any host with the SSH tunnel to Gitea API.
# Requires: curl, jq, and a Gitea token with read:repository on est org.
set -eu
GITEA_BASE="${GITEA_BASE:-http://127.0.0.1:3300/api/v1}"
GITEA_TOKEN="${GITEA_TOKEN:?set GITEA_TOKEN (read:repository on est)}"
REPOS="${REPOS:?set REPOS (space-separated repo names under the org)}"
LIMIT="${LIMIT:-10}"
for repo in $REPOS; do
echo "=== est/$repo (latest $LIMIT) ==="
curl -s --max-time 20 -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_BASE/repos/est/$repo/actions/tasks?limit=$LIMIT" \
| jq -r '.workflow_runs[] |
(.updated_at | fromdateiso8601) as $up |
(.created_at | fromdateiso8601) as $cr |
[.run_number, .name, .status, (($up - $cr) | tostring + "s"), .display_title] | @tsv' \
| awk -F'\t' '{printf " %-6s %-14s %-9s %-8s %s\n", $1, $2, $3, $4, $5}'
echo " summary:"
curl -s --max-time 20 -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_BASE/repos/est/$repo/actions/tasks?limit=100" \
| jq -r '[.workflow_runs[]?.status] | group_by(.) | map("\(.[0]): \(length)") | .[]' \
| sed 's/^/ /'
done