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:
2026-08-27 12:07:51 +08:00
commit b3b4e2b391
7 changed files with 516 additions and 0 deletions
+38
View File
@@ -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}"
+100
View File
@@ -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"
+37
View 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` | 统一 checkoutmu-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)。
+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