Merge branch 'feat/footer-share-ci' into dev
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
CI keeps the 'test once per code state' plan but records commit and git tree as image labels instead of extra :sha-/:tree- tags, so the registry only holds dev, latest, vX.Y.Z and stable; the plan test now asserts the expected decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
141f87f3a0
22 files changed
+735
-61
No files matched your search
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Decides per CI run: build with tests, build without tests, or only retag an existing image.
|
||||
# Tests run once per code state: the images carry the git tree and commit as labels
|
||||
# (io.mindsift.tree, org.opencontainers.image.revision), so no extra image tags are needed.
|
||||
# dev -> build and test, tag :dev
|
||||
# main -> build :latest; tests are skipped when :dev was built from the same tree
|
||||
# v* -> retag :latest when it was built from this commit, else build (tests skipped when
|
||||
# :dev or :latest has the same tree); :stable only for releases without "-"
|
||||
# Inputs (environment): REF, COMMIT (default HEAD), IMAGE, PKG_VERSION (default from package.json),
|
||||
# LABEL_CMD (prints a label of a remote image: LABEL_CMD <image> <label>; for tests)
|
||||
set -euo pipefail
|
||||
|
||||
REF="${REF:-}"
|
||||
COMMIT="${COMMIT:-HEAD}"
|
||||
IMAGE="${IMAGE:-}"
|
||||
PKG_VERSION="${PKG_VERSION:-}"
|
||||
if [ -z "${PKG_VERSION}" ] && [ -f frontend/package.json ]; then
|
||||
PKG_VERSION="$(sed -nE 's/^ "version": "([^"]+)".*/\1/p' frontend/package.json | head -n1)"
|
||||
fi
|
||||
|
||||
SHA="$(git rev-parse "${COMMIT}^{commit}")"
|
||||
TREE="$(git rev-parse "${COMMIT}^{tree}")"
|
||||
|
||||
# Label of an image in the registry ("" when the image or label does not exist)
|
||||
image_label() {
|
||||
if [ -n "${LABEL_CMD:-}" ]; then
|
||||
${LABEL_CMD} "$1" "$2" 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
docker pull -q "$1" >/dev/null 2>&1 || return 0
|
||||
docker inspect -f "{{ index .Config.Labels \"$2\" }}" "$1" 2>/dev/null | sed 's/^<no value>$//' || true
|
||||
}
|
||||
|
||||
ACTION="build"
|
||||
SKIP_TESTS="0"
|
||||
APP_VERSION=""
|
||||
TAGS=""
|
||||
SOURCE_IMAGE=""
|
||||
REASON=""
|
||||
|
||||
case "${REF}" in
|
||||
refs/heads/dev)
|
||||
TAGS="dev"
|
||||
REASON="dev: build with all tests"
|
||||
;;
|
||||
refs/heads/main)
|
||||
TAGS="latest"
|
||||
APP_VERSION="v${PKG_VERSION}"
|
||||
if [ -n "${IMAGE}" ] && [ "$(image_label "${IMAGE}:dev" io.mindsift.tree)" = "${TREE}" ]; then
|
||||
SKIP_TESTS="1"
|
||||
REASON="main: same code as the tested :dev image, tests skipped"
|
||||
else
|
||||
REASON="main: code not tested as :dev yet, build with all tests"
|
||||
fi
|
||||
;;
|
||||
refs/tags/v*)
|
||||
TAG_NAME="${REF#refs/tags/}"
|
||||
TAGS="${TAG_NAME}"
|
||||
case "${TAG_NAME}" in *-*) ;; *) TAGS="${TAG_NAME} stable" ;; esac
|
||||
if [ -n "${IMAGE}" ] && [ "$(image_label "${IMAGE}:latest" org.opencontainers.image.revision)" = "${SHA}" ]; then
|
||||
ACTION="retag"
|
||||
SOURCE_IMAGE="${IMAGE}:latest"
|
||||
SKIP_TESTS="1"
|
||||
REASON="tag ${TAG_NAME}: :latest was built from this commit, retag without building"
|
||||
else
|
||||
APP_VERSION="${TAG_NAME}"
|
||||
if [ -n "${IMAGE}" ] && { [ "$(image_label "${IMAGE}:dev" io.mindsift.tree)" = "${TREE}" ] ||
|
||||
[ "$(image_label "${IMAGE}:latest" io.mindsift.tree)" = "${TREE}" ]; }; then
|
||||
SKIP_TESTS="1"
|
||||
REASON="tag ${TAG_NAME}: code already tested, build without tests"
|
||||
else
|
||||
REASON="tag ${TAG_NAME}: build with all tests"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
TAGS="$(echo "${REF#refs/heads/}" | sed 's#/#-#g' | tr '[:upper:]' '[:lower:]')"
|
||||
REASON="branch ${REF}: build with all tests"
|
||||
;;
|
||||
esac
|
||||
|
||||
{
|
||||
echo "action=${ACTION}"
|
||||
echo "skip_tests=${SKIP_TESTS}"
|
||||
echo "app_version=${APP_VERSION}"
|
||||
echo "tags=${TAGS}"
|
||||
echo "source_image=${SOURCE_IMAGE}"
|
||||
echo "sha=${SHA}"
|
||||
echo "tree=${TREE}"
|
||||
echo "reason=${REASON}"
|
||||
} | tee -a "${GITHUB_OUTPUT:-/dev/null}"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Checks build-plan.sh against a fake registry (labels given per case). Usage: .gitea/scripts/test-build-plan.sh
|
||||
set -euo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
PLAN=.gitea/scripts/build-plan.sh
|
||||
IMAGE=registry.example/mindsift
|
||||
SHA="$(git rev-parse HEAD)"
|
||||
TREE="$(git rev-parse HEAD^{tree})"
|
||||
MOCK="$(mktemp)"
|
||||
trap 'rm -f "$MOCK"' EXIT
|
||||
# The fake registry answers from $DEV_TREE, $LATEST_TREE and $LATEST_SHA
|
||||
cat >"$MOCK" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
case "$1 $2" in
|
||||
*:dev\ io.mindsift.tree) echo "${DEV_TREE:-}" ;;
|
||||
*:latest\ io.mindsift.tree) echo "${LATEST_TREE:-}" ;;
|
||||
*:latest\ org.opencontainers.image.revision) echo "${LATEST_SHA:-}" ;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$MOCK"
|
||||
|
||||
fails=0
|
||||
expect() { # name, REF, expected "action skip_tests tags", env assignments…
|
||||
local name="$1" ref="$2" want="$3"
|
||||
shift 3
|
||||
local out
|
||||
out="$(env "$@" REF="$ref" IMAGE="$IMAGE" PKG_VERSION=0.10.0 LABEL_CMD="$MOCK" GITHUB_OUTPUT= "$PLAN")"
|
||||
local got
|
||||
got="$(echo "$out" | sed -n 's/^action=//p') $(echo "$out" | sed -n 's/^skip_tests=//p') $(echo "$out" | sed -n 's/^tags=//p')"
|
||||
if [ "$got" = "$want" ]; then
|
||||
echo "OK $name: $got"
|
||||
else
|
||||
echo "FEHL $name: erwartet '$want', bekommen '$got'"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
expect "dev baut mit Tests" refs/heads/dev "build 0 dev"
|
||||
expect "main nach getestetem dev" refs/heads/main "build 1 latest" DEV_TREE="$TREE"
|
||||
expect "main mit anderem Stand" refs/heads/main "build 0 latest" DEV_TREE=0000
|
||||
expect "Tag auf latest-Commit" refs/tags/v0.10.0 "retag 1 v0.10.0 stable" LATEST_SHA="$SHA" LATEST_TREE="$TREE"
|
||||
expect "Tag, latest fehlt, dev getestet" refs/tags/v0.10.0 "build 1 v0.10.0 stable" DEV_TREE="$TREE"
|
||||
expect "Tag auf unbekanntem Stand" refs/tags/v0.10.0 "build 0 v0.10.0 stable"
|
||||
expect "Vorabversion ohne stable" refs/tags/v0.11.0-rc.1 "retag 1 v0.11.0-rc.1" LATEST_SHA="$SHA"
|
||||
expect "Feature-Branch" refs/heads/feat/x "build 0 feat-x"
|
||||
|
||||
if [ "$fails" -gt 0 ]; then
|
||||
echo "$fails FEHLGESCHLAGEN"
|
||||
exit 1
|
||||
fi
|
||||
echo "ALLE FÄLLE BESTANDEN"
|
||||
@@ -5,6 +5,9 @@ name: MindSift CI & Docker Release
|
||||
# push/merge main -> :latest
|
||||
# git tag v1.2.0 -> :v1.2.0 and :stable
|
||||
# git tag v1.2.0-rc.1 (pre-release) -> :v1.2.0-rc.1 only, :stable stays
|
||||
# Tests run once per code state (.gitea/scripts/build-plan.sh): main skips them when :dev was built
|
||||
# from the same code, a tag on the commit of :latest only retags it. The images carry commit and git
|
||||
# tree as labels for this, so no extra tags end up in the registry.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -21,45 +24,16 @@ jobs:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine Registry & Docker Tag
|
||||
- name: Determine Registry & Image
|
||||
id: prep
|
||||
run: |
|
||||
SERVER_URL="${GITHUB_SERVER_URL:-https://git.dsit.me}"
|
||||
REGISTRY_HOST=$(echo "${SERVER_URL}" | sed -E 's#^https?://##' | sed 's#/$##')
|
||||
if [ -z "${REGISTRY_HOST}" ]; then
|
||||
REGISTRY_HOST="git.dsit.me"
|
||||
fi
|
||||
|
||||
[ -z "${REGISTRY_HOST}" ] && REGISTRY_HOST="git.dsit.me"
|
||||
REPO_PATH="${GITHUB_REPOSITORY:-sandrodach/mindsift}"
|
||||
IMAGE_NAME=$(echo "${REGISTRY_HOST}/${REPO_PATH}" | tr '[:upper:]' '[:lower:]')
|
||||
REF_NAME="${GITHUB_REF_NAME:-latest}"
|
||||
ACTOR_NAME="${GITHUB_ACTOR:-sandrodach}"
|
||||
|
||||
# Ersetze evtl. Slashes in Branch-Namen (z.B. rc/1.1 -> rc-1.1) für valide Docker-Tags
|
||||
DOCKER_TAG=$(echo "${REF_NAME}" | sed 's#/#-#g' | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# All tags of this run (space separated)
|
||||
case "${GITHUB_REF:-}" in
|
||||
refs/tags/*-*) TAGS="${DOCKER_TAG}" ;;
|
||||
refs/tags/*) TAGS="${DOCKER_TAG} stable" ;;
|
||||
refs/heads/main) TAGS="latest" ;;
|
||||
*) TAGS="${DOCKER_TAG}" ;;
|
||||
esac
|
||||
|
||||
echo "registry=${REGISTRY_HOST}" >> "$GITHUB_OUTPUT"
|
||||
echo "image=${IMAGE_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAGS%% *}" >> "$GITHUB_OUTPUT"
|
||||
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
|
||||
echo "actor=${ACTOR_NAME}" >> "$GITHUB_OUTPUT"
|
||||
# Version in the app header: tags show the tag, main the package version, everything else
|
||||
# (dev, feature branches) keeps the "-dev" suffix from vite.config.ts
|
||||
APP_VERSION=""
|
||||
case "${GITHUB_REF:-}" in
|
||||
refs/tags/*) APP_VERSION="${REF_NAME}" ;;
|
||||
refs/heads/main) APP_VERSION="v$(sed -nE 's/^ "version": "([^"]+)".*/\1/p' frontend/package.json | head -n1)" ;;
|
||||
esac
|
||||
echo "app_version=${APP_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Target Docker image: ${IMAGE_NAME} tags: ${TAGS} (Registry: ${REGISTRY_HOST}, Actor: ${ACTOR_NAME})"
|
||||
echo "image=$(echo "${REGISTRY_HOST}/${REPO_PATH}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
|
||||
echo "actor=${GITHUB_ACTOR:-sandrodach}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -68,12 +42,30 @@ jobs:
|
||||
username: ${{ steps.prep.outputs.actor }}
|
||||
password: ${{ secrets.PUSH_TOKEN || secrets.GITEA_TOKEN || github.token }}
|
||||
|
||||
- name: Build & Run All Checks in Container, then Push
|
||||
- name: Build Plan
|
||||
id: plan
|
||||
run: REF="${GITHUB_REF:-}" COMMIT="${GITHUB_SHA:-HEAD}" IMAGE="${{ steps.prep.outputs.image }}" .gitea/scripts/build-plan.sh
|
||||
|
||||
- name: Build (with or without tests) or Retag, then Push
|
||||
run: |
|
||||
IMAGE="${{ steps.prep.outputs.image }}"
|
||||
TAG_ARGS=""
|
||||
for t in ${{ steps.prep.outputs.tags }}; do TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:${t}"; done
|
||||
echo "🚀 Building container & executing Frontend/Backend checks inside image: ${IMAGE} (tags: ${{ steps.prep.outputs.tags }})"
|
||||
DOCKER_BUILDKIT=1 docker build --progress=plain --build-arg APP_VERSION="${{ steps.prep.outputs.app_version }}" ${TAG_ARGS} .
|
||||
echo "✅ All checks & tests passed inside container! Pushing..."
|
||||
for t in ${{ steps.prep.outputs.tags }}; do docker push "${IMAGE}:${t}"; done
|
||||
TAGS="${{ steps.plan.outputs.tags }}"
|
||||
echo "ℹ️ ${{ steps.plan.outputs.reason }}"
|
||||
if [ "${{ steps.plan.outputs.action }}" = "retag" ]; then
|
||||
SOURCE="${{ steps.plan.outputs.source_image }}"
|
||||
docker pull "${SOURCE}"
|
||||
for t in ${TAGS}; do
|
||||
docker tag "${SOURCE}" "${IMAGE}:${t}"
|
||||
docker push "${IMAGE}:${t}"
|
||||
done
|
||||
else
|
||||
TAG_ARGS=""
|
||||
for t in ${TAGS}; do TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:${t}"; done
|
||||
DOCKER_BUILDKIT=1 docker build --progress=plain \
|
||||
--build-arg APP_VERSION="${{ steps.plan.outputs.app_version }}" \
|
||||
--build-arg SKIP_TESTS="${{ steps.plan.outputs.skip_tests }}" \
|
||||
--build-arg GIT_SHA="${{ steps.plan.outputs.sha }}" \
|
||||
--build-arg GIT_TREE="${{ steps.plan.outputs.tree }}" \
|
||||
${TAG_ARGS} .
|
||||
for t in ${TAGS}; do docker push "${IMAGE}:${t}"; done
|
||||
fi
|
||||
Reference in new issue
Block a user