From b3b4e2b391bc02b10ea0c4b6c33bb713217178dc Mon Sep 17 00:00:00 2001 From: zcode_mulm Date: Thu, 27 Aug 2026 12:07:51 +0800 Subject: [PATCH] =?UTF-8?q?est/ci-workflows=20v1=EF=BC=9A=E5=85=AC?= =?UTF-8?q?=E5=8F=B8=E7=BA=A7=20reusable=20=E6=A8=A1=E6=9D=BF=EF=BC=88chec?= =?UTF-8?q?kout/node-quality/oci-build-push-verify=EF=BC=89=E4=B8=8E=20too?= =?UTF-8?q?ls/ci=20=E4=B8=8A=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移自 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。 --- .gitea/workflows/reusable/checkout.yml | 38 ++++ .gitea/workflows/reusable/node-quality.yml | 100 ++++++++++ .../reusable/oci-build-push-verify.yml | 187 ++++++++++++++++++ README.md | 37 ++++ tools/ci/helper/REGISTER.md | 22 +++ tools/ci/source-policy.sh | 106 ++++++++++ tools/ci/stats.sh | 26 +++ 7 files changed, 516 insertions(+) create mode 100644 .gitea/workflows/reusable/checkout.yml create mode 100644 .gitea/workflows/reusable/node-quality.yml create mode 100644 .gitea/workflows/reusable/oci-build-push-verify.yml create mode 100644 README.md create mode 100644 tools/ci/helper/REGISTER.md create mode 100755 tools/ci/source-policy.sh create mode 100755 tools/ci/stats.sh diff --git a/.gitea/workflows/reusable/checkout.yml b/.gitea/workflows/reusable/checkout.yml new file mode 100644 index 0000000..a85154f --- /dev/null +++ b/.gitea/workflows/reusable/checkout.yml @@ -0,0 +1,38 @@ +# Unified checkout for host-mode jobs (build-docker): mu-ref/actions-checkout +# (local JS-action mirror) with the Caddy CA and a HEAD==SHA guard. +# +# Requirements (verified 2026-08-27 on Gitea 1.27.2): the runner host must +# have a node runtime for JS actions (tn does), and the action repos must be +# anonymously readable (public). NOT usable from container jobs whose image +# lacks node — those callers keep a host `git clone` step instead. +name: reusable-checkout + +on: + workflow_call: + inputs: + fetch-depth: + required: false + type: number + default: 1 + verify-sha: + required: false + type: boolean + default: true + description: "Fail unless the checked-out HEAD equals the triggering commit" + +jobs: + checkout: + runs-on: build-docker + steps: + - name: checkout (mu-ref/actions-checkout) + uses: https://git.moneywood.site/mu-ref/actions-checkout@v4 + env: + GIT_SSL_CAINFO: /usr/local/share/ca-certificates/caddy-root-ca.crt + with: + fetch-depth: ${{ inputs.fetch-depth }} + + - name: verify HEAD equals triggering commit + if: inputs.verify-sha + run: | + set -eu + test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" diff --git a/.gitea/workflows/reusable/node-quality.yml b/.gitea/workflows/reusable/node-quality.yml new file mode 100644 index 0000000..f386966 --- /dev/null +++ b/.gitea/workflows/reusable/node-quality.yml @@ -0,0 +1,100 @@ +# Reusable quality gate for est CI (node toolchain in digest-pinned helper image). +# +# P1-7 verification status (2026-08-27, Gitea 1.27.2): +# - reusable workflow calls verified working (1.27.2 probe + this workflow's +# selftest caller: .gitea/workflows/selftest.yml) +# - checkout uses host git clone (NOT mu-ref/actions-checkout): act_runner +# runs JS actions with a host node runtime, which tn does not install — +# the JS-action path is unavailable for host jobs until node is installed +# on the runner host (coordination review fallback decision) +# - no actions/cache layer for the same reason; the persistent /data/cache/ci +# directory is the primary (and only) cache layer +# +# Conventions proven on the business-repo pipelines: +# - runs-on: build-docker (host job, root) + docker run for the toolchain +# - CA: GIT_SSL_CAINFO (host git) + SSL_CERT_FILE/NODE_EXTRA_CA_CERTS (container; +# helper images have their apt/apk sources baked in, so replacing the public +# trust store is safe here — see playbook pitfall 13) +# - workspace mounted at ${inputs.workspace} (legacy CNB scripts expect /workspace) +# - cache dirs: 0777 on the DIRECTORY ONLY; never chmod -R (selection files stay 0600) + +name: reusable-node-quality + +on: + workflow_call: + inputs: + node-image: + required: true + type: string + description: "Digest-pinned est helper image, e.g. est/ci-node-bookworm@sha256:78c01b0be47ebc60c0fa940110d66c60ca4e57f0fa57178bb88788e4ea131b61" + workspace: + required: false + type: string + default: /workspace + description: "Mount point for the workspace inside the helper container" + install-command: + required: true + type: string + description: "Shell command executed inside the helper container (frozen lockfile install)" + quality-command: + required: true + type: string + description: "Shell command executed inside the helper container (lint/typecheck/test/build)" + cache-dir: + required: false + type: string + default: /data/cache/ci/quality + description: "Persistent host-side cache dir (pnpm store, tools, browsers)" + secrets: + REGISTRY_PASSWORD: + required: false + +jobs: + quality: + runs-on: build-docker + steps: + - name: checkout (host git; JS actions need a host node runtime) + env: + CI_ACTOR: ${{ github.actor }} + CI_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eu + export GIT_SSL_CAINFO=/usr/local/share/ca-certificates/caddy-root-ca.crt + git config --global credential.helper \ + '!f() { echo "username=${CI_ACTOR}"; echo "password=${CI_TOKEN}"; }; f' + git config --global --add safe.directory '*' + timeout 120 git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ + "https://git.moneywood.site/${GITHUB_REPOSITORY}.git" . + test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" + + - name: authenticate to gitea registry (private helper image pulls) + env: + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -eu + if [ -n "${REGISTRY_PASSWORD:-}" ]; then + printf '%s' "${REGISTRY_PASSWORD}" \ + | docker login git.moneywood.site -u "${{ github.actor }}" --password-stdin + fi + + - name: prepare cache dir (dirs 0777, files untouched) + run: | + set -eu + mkdir -p "${{ inputs.cache-dir }}" + chmod 0777 "${{ inputs.cache-dir }}" + find "${{ inputs.cache-dir }}" -mindepth 1 -maxdepth 1 -type d -exec chmod 0777 {} + 2>/dev/null || true + + # install and quality run in ONE container: toolchains installed by the + # install command (global pnpm, local bins) must still be on PATH for the + # quality command — two `docker run --rm` invocations would not share + # anything but the workspace and /cache mounts. + - name: install and quality (containerized toolchain) + run: | + set -eu + docker run --rm --add-host npm.cache.est:172.17.0.1 \ + -v "$PWD:${{ inputs.workspace }}" -w "${{ inputs.workspace }}" \ + -v "${{ inputs.cache-dir }}:/cache" -e TMPDIR=/cache \ + -v /usr/local/share/ca-certificates/est-bundle.crt:/ca/est-bundle.crt:ro \ + -e SSL_CERT_FILE=/ca/est-bundle.crt \ + -e NODE_EXTRA_CA_CERTS=/ca/est-bundle.crt \ + "${{ inputs.node-image }}" sh -euxc '${{ inputs.install-command }} && ${{ inputs.quality-command }}' diff --git a/.gitea/workflows/reusable/oci-build-push-verify.yml b/.gitea/workflows/reusable/oci-build-push-verify.yml new file mode 100644 index 0000000..c93af15 --- /dev/null +++ b/.gitea/workflows/reusable/oci-build-push-verify.yml @@ -0,0 +1,187 @@ +# Reusable build+push+verify for est CI (host docker daemon). +# +# P1-7 verification status (2026-08-27, Gitea 1.27.2): +# - reusable workflow calls verified working (selftest caller: +# .gitea/workflows/selftest.yml) +# - checkout uses host git clone (JS actions need a host node runtime that +# tn does not install — see reusable/node-quality.yml header) +# - registry identity is the triggering actor (github.actor), credentials +# flow exclusively through the REGISTRY_PASSWORD secret (no hardcoded +# usernames — coordination review pitfall 21) +# - proven conventions from the business-repo pipelines: +# verify() mounts + ) + +name: reusable-oci-build-push-verify + +on: + workflow_call: + inputs: + image-root: + required: false + type: string + default: git.moneywood.site/est + node-image: + required: true + type: string + description: "Digest-pinned helper image with node + docker CLI + verifier tooling" + image-matrix: + required: true + type: string + description: "Multiline matrix: service|dockerfile|target per line" + platform: + required: false + type: string + default: linux/amd64 + build-args: + required: false + type: string + default: "" + description: "Multiline KEY=VALUE build args (static values only — caller-side expressions are NOT evaluated in with:)" + revision-arg-name: + required: false + type: string + default: "" + description: "ARG name that receives the triggering commit SHA (a SOURCE_REVISION-style ARG); empty disables" + verify-command: + required: false + type: string + default: "" + description: "Repo verify script run inside the helper per image (receives --image/--index/--target/--architecture/--revision)" + cache-registry-ref: + required: false + type: string + default: "" + description: "e.g. est/ci-cache-; empty disables registry build cache" + registry-host: + required: false + type: string + default: git.moneywood.site + secrets: + REGISTRY_PASSWORD: + required: true + +jobs: + build: + runs-on: build-docker + steps: + - name: checkout (host git; JS actions need a host node runtime) + env: + CI_ACTOR: ${{ github.actor }} + CI_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eu + export GIT_SSL_CAINFO=/usr/local/share/ca-certificates/caddy-root-ca.crt + git config --global credential.helper \ + '!f() { echo "username=${CI_ACTOR}"; echo "password=${CI_TOKEN}"; }; f' + git config --global --add safe.directory '*' + timeout 120 git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ + "https://git.moneywood.site/${GITHUB_REPOSITORY}.git" . + test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" + + - name: authenticate to gitea registry + env: + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -eu + printf '%s' "${REGISTRY_PASSWORD}" \ + | docker login "${{ inputs.registry-host }}" -u "${{ github.actor }}" --password-stdin + + - name: build push and verify image matrix + env: + REGISTRY_AUTH_USER: ${{ github.actor }} + REGISTRY_AUTH_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + NODE_IMAGE: ${{ inputs.node-image }} + IMAGE_ROOT: ${{ inputs.image-root }} + PLATFORM: ${{ inputs.platform }} + VERIFY_COMMAND: ${{ inputs.verify-command }} + CACHE_REGISTRY_REF: ${{ inputs.cache-registry-ref }} + REVISION_ARG_NAME: ${{ inputs.revision-arg-name }} + run: | + set -eu + IMAGE_TAG="git-${GITHUB_SHA}" + printf '%s' "${GITHUB_SHA}" | grep -Eq '^[0-9a-f]{40}$' + mkdir -p .tmp-ci + digest_file=".tmp-ci/digests.txt" + : >"$digest_file" + cache_args="" + if [ -n "${CACHE_REGISTRY_REF}" ]; then + cache_args="--cache-from type=registry,ref=${CACHE_REGISTRY_REF} --cache-to type=registry,ref=${CACHE_REGISTRY_REF},mode=max" + fi + # verify() keeps stdin closed so it cannot steal the outer while-read heredoc. + verify() { + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$HOME/.docker/config.json:/root/.docker/config.json:ro" \ + -v /usr/local/share/ca-certificates/est-bundle.crt:/ca/est-bundle.crt:ro \ + -v /usr/libexec/docker/cli-plugins/docker-buildx:/root/.docker/cli-plugins/docker-buildx:ro \ + -v "$PWD:/w" -w /w \ + -e SSL_CERT_FILE=/ca/est-bundle.crt \ + -e NODE_EXTRA_CA_CERTS=/ca/est-bundle.crt \ + -e IMAGE_CONFIG -e EXPECTED_REVISION -e EXPECTED_BASELINE \ + -e EXPECTED_ARCHITECTURE -e MANIFEST_FILE \ + -e REGISTRY_AUTH_USER -e REGISTRY_AUTH_PASSWORD \ + "${NODE_IMAGE}" "$@" < /dev/null + } + build_args="" + while IFS= read -r line; do + # Values must not contain spaces: expansion results do not go + # through quote removal, so embedded quotes would end up in the + # ARG name (observed: '"KEY=value' never matches a Dockerfile ARG). + [ -n "$line" ] && build_args="${build_args} --build-arg ${line}" + done <"$manifest_file" + if [ -n "${VERIFY_COMMAND}" ]; then + # VERIFY_COMMAND carries its own interpreter (e.g. "node x.mjs" + # or "sh x.sh"); flags below are appended for the callee. + # shellcheck disable=SC2086 + verify ${VERIFY_COMMAND} \ + --image "${image}" \ + --index "$manifest_file" \ + --target "${target}" \ + --architecture "${PLATFORM##*/}" \ + --revision "${GITHUB_SHA}" + fi + index_digest=$(docker buildx imagetools inspect "${image}" | awk '/^Digest:/{print $2}') + printf '%s|%s|%s\n' "${service}" "${IMAGE_TAG}" "${index_digest}" >>"$digest_file" + done <<'IMAGES' + ${{ inputs.image-matrix }} + IMAGES + { + echo "## V4 fresh image digests" + echo + echo "revision: ${GITHUB_SHA}" + echo + echo '```' + cat "$digest_file" + echo '```' + } >>"${GITHUB_STEP_SUMMARY:-/dev/stdout}" + cat "$digest_file" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e2c70d --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# est/ci-workflows + +公司级 CI 共享资产(est org)——跨项目复用的 reusable workflow 模板与工具。 +架构决策与铁律见 coordination `runbooks/ci-repo-architecture.md`(2026-08-27 用户拍板)。 + +## 内容 + +| 路径 | 用途 | +|---|---| +| `.gitea/workflows/reusable/checkout.yml` | 统一 checkout(mu-ref/actions-checkout + CA + HEAD==SHA 守卫;host job 专用) | +| `.gitea/workflows/reusable/node-quality.yml` | node 质量门(digest helper、持久缓存、rank 源序) | +| `.gitea/workflows/reusable/oci-build-push-verify.yml` | 镜像构建+推送+verify-command 校验链 | +| `tools/ci/source-policy.sh` | 唯一选源实现(公司级 canonical) | +| `tools/ci/helper/REGISTER.md` | ci-node-* helper 镜像 digest 注册 | +| `tools/ci/stats.sh` | Gitea Actions 运行统计(REPOS 必填) | + +## 铁律 + +1. 本仓**不得出现项目专名**(reef/seabed 等)与任何 secrets——项目参数(矩阵、build args、verify 期望值)一律由调用方 `with:`/`secrets: inherit` 传入。 +2. workflow 文件只保留 `workflow_call` 触发(不可直接触发);`has_actions=false`。 +3. **版本化调用**:调用方一律 `uses: est/ci-workflows/.gitea/workflows/reusable/.yml@vN`;重大变更升新 tag 并逐项目回归。 +4. 可见性:limited(org 成员可读,匿名不可读)。跨仓 reusable 读取按触发用户 token 校验——调用方触发者须为 est org 成员。 + +## 调用样例 + +```yaml +jobs: + quality: + uses: https://git.moneywood.site/est/ci-workflows/.gitea/workflows/reusable/node-quality.yml@v1 + with: { ... } + secrets: inherit +``` + +## 迁移与同步 + +- 2026-08-27 自 mu-ref/est-ci-reusable(已删)与 est/Est-Infra 上移建立;Est-Infra 留 README 指针。 +- helper Dockerfile 的可复现 bake 与 ops 巡检脚本仍属项目级(Est-Infra / gitea-host-setup)。 diff --git a/tools/ci/helper/REGISTER.md b/tools/ci/helper/REGISTER.md new file mode 100644 index 0000000..d355b94 --- /dev/null +++ b/tools/ci/helper/REGISTER.md @@ -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/: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. diff --git a/tools/ci/source-policy.sh b/tools/ci/source-policy.sh new file mode 100755 index 0000000..f128580 --- /dev/null +++ b/tools/ci/source-policy.sh @@ -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 [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" +} \ No newline at end of file diff --git a/tools/ci/stats.sh b/tools/ci/stats.sh new file mode 100755 index 0000000..e28e08e --- /dev/null +++ b/tools/ci/stats.sh @@ -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