ca21b6e70d
ci / lint (push) Successful in 18s
ci / dockerfile (push) Successful in 18s
ci / chart (push) Successful in 19s
ci / security (push) Successful in 1m7s
ci / integration (push) Successful in 48s
ci / unit (push) Successful in 37s
ci / types (push) Successful in 50s
ci / image (api) (push) Successful in 2m17s
ci / image (reconciler) (push) Successful in 2m36s
ci / image (worker) (push) Successful in 1m30s
ci / bump (push) Successful in 14s
The secret-scanning gate has been green and meaningless. `docker run -v "$PWD:/repo"`
resolves the bind source on the dind sidecar's daemon, not in the job container, so
gitleaks received an empty mount:
ERR [git] fatal: not a git repository (or any parent up to mount point /)
ERR failed to scan Git repository error="stderr is not empty"
INF scan completed in 35.2ms
INF no leaks found <- exit 0
It logged the failure and exited 0. A scanner that reports success without looking is
worse than no scanner. The 35ms runtime was the tell.
Now a checksum-pinned binary, plus a `git rev-list --count HEAD` assertion so an
unscannable checkout fails the job instead of passing it.
Same root cause and same fix for yq in bump-digests.sh, which failed the bump job with
"stat deploy/chart/values.yaml: no such file or directory". helm was fixed this way
earlier. Nothing on this runner should mount $PWD into a container.
111 lines
4.0 KiB
Bash
Executable File
111 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# CI's last act.
|
|
#
|
|
# Resolves the digest each service's commit-SHA tag points at, writes those digests into
|
|
# deploy/chart/values.yaml, and commits. That commit is the deploy: ArgoCD is watching
|
|
# master and picks it up. This script does not, and must not, talk to the cluster.
|
|
#
|
|
# Called by .gitea/workflows/ci.yaml on master only. Runnable by hand for a re-bump:
|
|
# REGISTRY=gitea.oci-oci.duckdns.org IMAGE_NS=gitea_admin IMAGE_TAG=<sha> ./scripts/bump-digests.sh
|
|
#
|
|
# -e a failed inspect must not lead to committing a stale digest
|
|
# -u an unset REGISTRY would silently resolve the wrong image
|
|
# -o pipefail the digest comes out of a pipe; without this, a failing inspect that pipes
|
|
# into a successful grep exits 0 and writes garbage
|
|
set -euo pipefail
|
|
|
|
: "${REGISTRY:?REGISTRY must be set}"
|
|
: "${IMAGE_NS:?IMAGE_NS must be set}"
|
|
: "${IMAGE_TAG:?IMAGE_TAG must be set (the commit sha the images were built from)}"
|
|
|
|
SERVICES=(api worker reconciler)
|
|
CHART_VALUES="deploy/chart/values.yaml"
|
|
|
|
WORKDIR="$(mktemp -d)"
|
|
cleanup() {
|
|
rm -rf "${WORKDIR}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# yq as a checksum-pinned binary, NOT `docker run -v "$PWD:/work"`.
|
|
#
|
|
# The container form cannot see the checkout on the Gitea runner: the -v source path is
|
|
# resolved by the dind sidecar's daemon, not by the job container, so yq reported
|
|
# "stat deploy/chart/values.yaml: no such file or directory" for a file that plainly
|
|
# exists. The same boundary silently broke helm and gitleaks.
|
|
#
|
|
# Not python+pyyaml: a yaml round-trip strips every comment in values.yaml, and those
|
|
# comments are the only thing explaining why the digests are there. yq edits in place.
|
|
YQ_VERSION="4.44.6"
|
|
YQ_SHA256="9477ac3cc447b6c083986129e35af8122eb2b938fe55c9c3e40436fb966e5813"
|
|
|
|
ensure_yq() {
|
|
if command -v yq >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
local arch
|
|
case "$(uname -m)" in
|
|
aarch64|arm64) arch=arm64 ;;
|
|
x86_64) arch=amd64 ;;
|
|
*) echo "!! unsupported arch $(uname -m)" >&2; exit 1 ;;
|
|
esac
|
|
curl -fsSL -o "${WORKDIR}/yq" \
|
|
"https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_${arch}"
|
|
# Only the arm64 digest is pinned; this runner is Ampere. On another arch the download
|
|
# is still version-pinned, and the mismatch is reported rather than silently accepted.
|
|
if [ "${arch}" = "arm64" ]; then
|
|
echo "${YQ_SHA256} ${WORKDIR}/yq" | sha256sum -c -
|
|
else
|
|
echo "warning: no pinned checksum for ${arch}; version-pinned only" >&2
|
|
fi
|
|
chmod +x "${WORKDIR}/yq"
|
|
PATH="${WORKDIR}:${PATH}"
|
|
export PATH
|
|
}
|
|
|
|
ensure_yq
|
|
|
|
echo "==> resolving digests for tag ${IMAGE_TAG}"
|
|
for svc in "${SERVICES[@]}"; do
|
|
image="${REGISTRY}/${IMAGE_NS}/svcforge-${svc}"
|
|
digest="$(docker buildx imagetools inspect "${image}:${IMAGE_TAG}" --format '{{.Manifest.Digest}}')"
|
|
|
|
# Defence against a silently empty inspect. Without this, `yq` would happily write an
|
|
# empty digest and the chart's own guard would fail the release later, further from
|
|
# the cause.
|
|
if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
|
echo "!! ${svc}: refusing to write a non-digest: '${digest}'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo " ${svc} -> ${digest}"
|
|
echo "${digest}" > "${WORKDIR}/${svc}.digest"
|
|
done
|
|
|
|
echo "==> writing ${CHART_VALUES}"
|
|
for svc in "${SERVICES[@]}"; do
|
|
digest="$(cat "${WORKDIR}/${svc}.digest")"
|
|
# env(...) rather than string interpolation: a digest is attacker-controlled only in
|
|
# theory, but yq expression injection is not a thing worth leaving open.
|
|
DIGEST="${digest}" yq -i ".image.${svc}.digest = strenv(DIGEST)" "${CHART_VALUES}"
|
|
done
|
|
|
|
if git diff --quiet -- "${CHART_VALUES}"; then
|
|
echo "==> no digest changed; nothing to commit"
|
|
exit 0
|
|
fi
|
|
|
|
echo "==> committing"
|
|
git config user.name "svcforge-ci"
|
|
git config user.email "ci@svcforge.invalid"
|
|
git add "${CHART_VALUES}"
|
|
git commit -m "ci: bump image digests to ${IMAGE_TAG}
|
|
|
|
Built and scanned by ${IMAGE_TAG}. ArgoCD syncs from this commit.
|
|
|
|
[skip ci]"
|
|
git push origin HEAD:master
|
|
|
|
echo "==> done. ArgoCD owns it from here."
|