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:
@@ -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}"
|
||||
@@ -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 }}'
|
||||
@@ -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 + </dev/null stdin discipline, registry-neutral
|
||||
# verifier contract (Gitea 1.27 sends Bearer AND Basic WWW-Authenticate
|
||||
# headers joined by fetch — callers' verifiers must accept the boundary,
|
||||
# see the business repos' verifier implementations), bare-repo@digest
|
||||
# references (docker 29 rejects repo:tag@digest)
|
||||
# - buildx registry cache optional (cache-registry-ref empty disables);
|
||||
# flat two-level naming rule applies (est/ci-cache-<repo>)
|
||||
|
||||
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-<repo>; 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 <<EOF
|
||||
${{ inputs.build-args }}
|
||||
EOF
|
||||
if [ -n "${REVISION_ARG_NAME}" ]; then
|
||||
build_args="${build_args} --build-arg ${REVISION_ARG_NAME}=${GITHUB_SHA}"
|
||||
fi
|
||||
while IFS='|' read -r service dockerfile target; do
|
||||
# Multiline inputs can carry a trailing empty line — skip it
|
||||
# instead of building an invalid "est/:tag" reference.
|
||||
[ -n "${service:-}" ] || continue
|
||||
image_repo="${IMAGE_ROOT}/${service}"
|
||||
image="${image_repo}:${IMAGE_TAG}"
|
||||
# shellcheck disable=SC2086
|
||||
docker buildx build \
|
||||
--file "${dockerfile}" \
|
||||
--target "${target}" \
|
||||
--platform "${PLATFORM}" \
|
||||
${build_args} \
|
||||
${cache_args} \
|
||||
--tag "${image}" \
|
||||
--provenance=mode=max \
|
||||
--sbom=true \
|
||||
--push .
|
||||
manifest_file=".tmp-ci/index-${service}.json"
|
||||
docker buildx imagetools inspect "${image}" --raw >"$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"
|
||||
@@ -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/<name>.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)。
|
||||
@@ -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.
|
||||
Executable
+106
@@ -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"
|
||||
}
|
||||
Executable
+26
@@ -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
|
||||
Reference in New Issue
Block a user