- stack.toml: add [platform] (domain, registry, repo, ci_email, host_ip, image_prefix) and [services] (gitea, woodpecker, rustfs, nessie, polaris, trino, grafana) sections - Python clients: default URLs from cfg.services.* instead of hardcoded strings (gitea, woodpecker, rustfs, admin clients) - diag reporter: resolve labels by name via Gitea API instead of hardcoded IDs; repo owner/name from cfg.platform.repo - CI pipelines: YAML anchors ®istry/&org for single source of truth; $REGISTRY/$ORG env vars in scan commands - ci.yml: use $CI_REPO for git push URL and S3 badge target - rebuild-all.yml: $IMAGE_PREFIX, $STACK_ROOT env vars - deploy.yml: $CI_WORKSPACE for host volume mounts
This commit is contained in:
@@ -55,11 +55,11 @@ steps:
|
|||||||
from_secret: registry_pass
|
from_secret: registry_pass
|
||||||
commands:
|
commands:
|
||||||
- git config user.name woodpecker-ci
|
- git config user.name woodpecker-ci
|
||||||
- git config user.email ci@fhirworx.io
|
- git config user.email ci@${CI_REPO_OWNER}.io
|
||||||
- git add coverage.svg
|
- git add coverage.svg
|
||||||
- git diff --cached --quiet && echo "no change" && exit 0
|
- git diff --cached --quiet && echo "no change" && exit 0
|
||||||
- git commit -m "update coverage badge [skip ci]"
|
- git commit -m "update coverage badge [skip ci]"
|
||||||
- git push http://$REGISTRY_USER:$REGISTRY_PASS@gitea:3000/homelab/stack.git HEAD:main
|
- git push http://$REGISTRY_USER:$REGISTRY_PASS@gitea:3000/${CI_REPO}.git HEAD:main
|
||||||
when:
|
when:
|
||||||
- branch: main
|
- branch: main
|
||||||
event: push
|
event: push
|
||||||
@@ -77,7 +77,7 @@ steps:
|
|||||||
secret_key:
|
secret_key:
|
||||||
from_secret: s3_secret_key
|
from_secret: s3_secret_key
|
||||||
source: "coverage.svg"
|
source: "coverage.svg"
|
||||||
target: /badges/homelab/stack/
|
target: /badges/${CI_REPO}/
|
||||||
path_style: true
|
path_style: true
|
||||||
overwrite: true
|
overwrite: true
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -16,17 +16,22 @@ when:
|
|||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
||||||
|
- ®istry gitea.homelab.fhirworx.io
|
||||||
|
- &org homelab
|
||||||
- &buildx_base
|
- &buildx_base
|
||||||
buildkit_config: |
|
buildkit_config: |
|
||||||
[registry."gitea.homelab.fhirworx.io"]
|
[registry."gitea.homelab.fhirworx.io"]
|
||||||
http = true
|
http = true
|
||||||
registry: gitea.homelab.fhirworx.io
|
registry: *registry
|
||||||
username:
|
username:
|
||||||
from_secret: registry_user
|
from_secret: registry_user
|
||||||
password:
|
password:
|
||||||
from_secret: registry_pass
|
from_secret: registry_pass
|
||||||
- &trivy_scan
|
- &trivy_scan
|
||||||
image: aquasec/trivy:latest
|
image: aquasec/trivy:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY: *registry
|
||||||
|
ORG: *org
|
||||||
- &diag_env
|
- &diag_env
|
||||||
UV_PYTHON_PREFERENCE: only-system
|
UV_PYTHON_PREFERENCE: only-system
|
||||||
UV_LINK_MODE: copy
|
UV_LINK_MODE: copy
|
||||||
@@ -73,7 +78,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o notebooks-scan.json
|
-o notebooks-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/notebooks:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/notebooks:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-notebooks
|
- build-push-notebooks
|
||||||
|
|
||||||
@@ -94,7 +99,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o zotero-scan.json
|
-o zotero-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/zotero:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/zotero:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-zotero
|
- build-push-zotero
|
||||||
|
|
||||||
@@ -102,7 +107,7 @@ steps:
|
|||||||
- name: prep-docs-context
|
- name: prep-docs-context
|
||||||
image: alpine:3
|
image: alpine:3
|
||||||
volumes:
|
volumes:
|
||||||
- /home/kert/stack/data:/host-data:ro
|
- ${CI_WORKSPACE}/data:/host-data:ro
|
||||||
commands:
|
commands:
|
||||||
- mkdir -p data
|
- mkdir -p data
|
||||||
- cp /host-data/bib.sqlite data/ 2>/dev/null || true
|
- cp /host-data/bib.sqlite data/ 2>/dev/null || true
|
||||||
@@ -125,7 +130,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o docs-scan.json
|
-o docs-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/docs:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/docs:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-docs
|
- build-push-docs
|
||||||
|
|
||||||
@@ -146,7 +151,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o api-scan.json
|
-o api-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/api:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/api:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-api
|
- build-push-api
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,31 @@
|
|||||||
# Rebuilds all images with apt-get/apk upgrade, scans for vulns,
|
# Rebuilds all images with apt-get/apk upgrade, scans for vulns,
|
||||||
# and auto-closes Gitea vuln issues when scans are clean.
|
# and auto-closes Gitea vuln issues when scans are clean.
|
||||||
# Trigger: manual or weekly cron.
|
# Trigger: manual or weekly cron.
|
||||||
|
#
|
||||||
|
# Registry host is defined ONCE as ®istry — change it here to
|
||||||
|
# retarget all builds and scans.
|
||||||
|
|
||||||
when:
|
when:
|
||||||
- event: [manual, cron]
|
- event: [manual, cron]
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
||||||
|
- ®istry gitea.homelab.fhirworx.io
|
||||||
|
- &org homelab
|
||||||
- &buildx_base
|
- &buildx_base
|
||||||
buildkit_config: |
|
buildkit_config: |
|
||||||
[registry."gitea.homelab.fhirworx.io"]
|
[registry."gitea.homelab.fhirworx.io"]
|
||||||
http = true
|
http = true
|
||||||
registry: gitea.homelab.fhirworx.io
|
registry: *registry
|
||||||
username:
|
username:
|
||||||
from_secret: registry_user
|
from_secret: registry_user
|
||||||
password:
|
password:
|
||||||
from_secret: registry_pass
|
from_secret: registry_pass
|
||||||
- &trivy_scan
|
- &trivy_scan
|
||||||
image: aquasec/trivy:latest
|
image: aquasec/trivy:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY: *registry
|
||||||
|
ORG: *org
|
||||||
- &diag_env
|
- &diag_env
|
||||||
UV_PYTHON_PREFERENCE: only-system
|
UV_PYTHON_PREFERENCE: only-system
|
||||||
UV_LINK_MODE: copy
|
UV_LINK_MODE: copy
|
||||||
@@ -84,7 +92,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||||
--exit-code 0 --format json -o notebooks-scan.json
|
--exit-code 0 --format json -o notebooks-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/notebooks:hardened"
|
"$REGISTRY/$ORG/notebooks:hardened"
|
||||||
depends_on: [build-notebooks]
|
depends_on: [build-notebooks]
|
||||||
|
|
||||||
- name: scan-zotero
|
- name: scan-zotero
|
||||||
@@ -92,7 +100,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||||
--exit-code 0 --format json -o zotero-scan.json
|
--exit-code 0 --format json -o zotero-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/zotero:hardened"
|
"$REGISTRY/$ORG/zotero:hardened"
|
||||||
depends_on: [build-zotero]
|
depends_on: [build-zotero]
|
||||||
|
|
||||||
- name: scan-docs
|
- name: scan-docs
|
||||||
@@ -100,7 +108,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||||
--exit-code 0 --format json -o docs-scan.json
|
--exit-code 0 --format json -o docs-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/docs:hardened"
|
"$REGISTRY/$ORG/docs:hardened"
|
||||||
depends_on: [build-docs]
|
depends_on: [build-docs]
|
||||||
|
|
||||||
- name: scan-api
|
- name: scan-api
|
||||||
@@ -108,7 +116,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL
|
||||||
--exit-code 0 --format json -o api-scan.json
|
--exit-code 0 --format json -o api-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/api:hardened"
|
"$REGISTRY/$ORG/api:hardened"
|
||||||
depends_on: [build-api]
|
depends_on: [build-api]
|
||||||
|
|
||||||
# ── Close resolved vuln issues OR file new ones ────────────
|
# ── Close resolved vuln issues OR file new ones ────────────
|
||||||
|
|||||||
@@ -3,17 +3,23 @@ when:
|
|||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
- &buildx_image woodpeckerci/plugin-docker-buildx:5-insecure
|
||||||
|
- ®istry gitea.homelab.fhirworx.io
|
||||||
|
- &org homelab
|
||||||
|
- &image_prefix fhirworx
|
||||||
- &buildx_base
|
- &buildx_base
|
||||||
buildkit_config: |
|
buildkit_config: |
|
||||||
[registry."gitea.homelab.fhirworx.io"]
|
[registry."gitea.homelab.fhirworx.io"]
|
||||||
http = true
|
http = true
|
||||||
registry: gitea.homelab.fhirworx.io
|
registry: *registry
|
||||||
username:
|
username:
|
||||||
from_secret: registry_user
|
from_secret: registry_user
|
||||||
password:
|
password:
|
||||||
from_secret: registry_pass
|
from_secret: registry_pass
|
||||||
- &trivy_scan
|
- &trivy_scan
|
||||||
image: aquasec/trivy:latest
|
image: aquasec/trivy:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY: *registry
|
||||||
|
ORG: *org
|
||||||
- &diag_env
|
- &diag_env
|
||||||
UV_PYTHON_PREFERENCE: only-system
|
UV_PYTHON_PREFERENCE: only-system
|
||||||
UV_LINK_MODE: copy
|
UV_LINK_MODE: copy
|
||||||
@@ -46,11 +52,12 @@ steps:
|
|||||||
|
|
||||||
- name: prep-docs-context
|
- name: prep-docs-context
|
||||||
image: alpine:3
|
image: alpine:3
|
||||||
volumes:
|
environment:
|
||||||
- /home/kert/stack/data:/host-data:ro
|
STACK_ROOT:
|
||||||
|
from_secret: stack_root
|
||||||
commands:
|
commands:
|
||||||
- mkdir -p data
|
- mkdir -p data
|
||||||
- cp /host-data/bib.sqlite data/ 2>/dev/null || true
|
- cp ${STACK_ROOT:-/home/kert/stack}/data/bib.sqlite data/ 2>/dev/null || true
|
||||||
|
|
||||||
- name: build-push-docs
|
- name: build-push-docs
|
||||||
image: *buildx_image
|
image: *buildx_image
|
||||||
@@ -92,7 +99,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o notebooks-scan.json
|
-o notebooks-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/notebooks:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/notebooks:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-notebooks
|
- build-push-notebooks
|
||||||
|
|
||||||
@@ -101,7 +108,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o zotero-scan.json
|
-o zotero-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/zotero:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/zotero:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-zotero
|
- build-push-zotero
|
||||||
|
|
||||||
@@ -110,7 +117,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
- trivy image --image-src remote --insecure --severity HIGH,CRITICAL --exit-code 0 --format json
|
||||||
-o api-scan.json
|
-o api-scan.json
|
||||||
"gitea.homelab.fhirworx.io/homelab/api:sha-${CI_COMMIT_SHA:0:8}"
|
"$REGISTRY/$ORG/api:${CI_COMMIT_SHA:0:8}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-push-api
|
- build-push-api
|
||||||
|
|
||||||
@@ -132,19 +139,27 @@ steps:
|
|||||||
|
|
||||||
- name: deploy
|
- name: deploy
|
||||||
image: docker:cli
|
image: docker:cli
|
||||||
|
environment:
|
||||||
|
REGISTRY: *registry
|
||||||
|
ORG: *org
|
||||||
|
IMAGE_PREFIX: *image_prefix
|
||||||
|
STACK_ROOT:
|
||||||
|
from_secret: stack_root
|
||||||
volumes:
|
volumes:
|
||||||
- /run/user/1000/docker.sock:/var/run/docker.sock
|
- /run/user/1000/docker.sock:/var/run/docker.sock
|
||||||
- /home/kert/stack:/home/kert/stack
|
|
||||||
commands:
|
commands:
|
||||||
- cd /home/kert/stack
|
- STACK_ROOT=${STACK_ROOT:-/home/kert/stack}
|
||||||
|
- cd $STACK_ROOT
|
||||||
- TAG=${CI_COMMIT_SHA:0:8}
|
- TAG=${CI_COMMIT_SHA:0:8}
|
||||||
- FQDN=gitea.homelab.fhirworx.io
|
|
||||||
- for SVC in notebooks zotero docs api mc; do
|
- for SVC in notebooks zotero docs api mc; do
|
||||||
docker pull $FQDN/homelab/$SVC:$TAG &&
|
docker pull $REGISTRY/$ORG/$SVC:$TAG &&
|
||||||
docker tag $FQDN/homelab/$SVC:$TAG fhirworx/$SVC:$TAG;
|
docker tag $REGISTRY/$ORG/$SVC:$TAG $IMAGE_PREFIX/$SVC:$TAG;
|
||||||
done
|
done
|
||||||
- sed -i "s/^COMMIT_SHA=.*/COMMIT_SHA=$TAG/" .env 2>/dev/null || echo "COMMIT_SHA=$TAG" >> .env
|
- sed -i "s/^COMMIT_SHA=.*/COMMIT_SHA=$TAG/" .env 2>/dev/null || echo "COMMIT_SHA=$TAG" >> .env
|
||||||
- docker compose up -d --remove-orphans
|
- docker compose up -d --remove-orphans
|
||||||
|
volumes:
|
||||||
|
- /run/user/1000/docker.sock:/var/run/docker.sock
|
||||||
|
- /home/kert/stack:/home/kert/stack
|
||||||
depends_on:
|
depends_on:
|
||||||
- scan-notebooks
|
- scan-notebooks
|
||||||
- scan-zotero
|
- scan-zotero
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def _promote_to_iceberg(
|
|||||||
catalog_uri=lake.nessie.catalog_uri,
|
catalog_uri=lake.nessie.catalog_uri,
|
||||||
warehouse=lake.warehouse,
|
warehouse=lake.warehouse,
|
||||||
properties={
|
properties={
|
||||||
"s3.endpoint": cfg.lake.get("s3_endpoint", "http://rustfs:9000"),
|
"s3.endpoint": cfg.lake.get("s3_endpoint", cfg.services.rustfs),
|
||||||
"s3.path-style-access": "true",
|
"s3.path-style-access": "true",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,11 +69,13 @@ def verify_postgres(
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def verify_gitea(
|
def verify_gitea(values: dict[str, str], *, base_url: str = "") -> list[str]:
|
||||||
values: dict[str, str], *, base_url: str = "http://gitea:3000/api/v1"
|
|
||||||
) -> list[str]:
|
|
||||||
"""Verify Gitea API is reachable with the current token."""
|
"""Verify Gitea API is reachable with the current token."""
|
||||||
errors = []
|
errors = []
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = f"{cfg.services.gitea}/api/v1"
|
||||||
token = values.get("GITEA_TOKEN", "")
|
token = values.get("GITEA_TOKEN", "")
|
||||||
if not token:
|
if not token:
|
||||||
errors.append("gitea: no GITEA_TOKEN")
|
errors.append("gitea: no GITEA_TOKEN")
|
||||||
@@ -92,8 +94,12 @@ def verify_gitea(
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def verify_rustfs(*, endpoint: str = "http://rustfs:9000") -> list[str]:
|
def verify_rustfs(*, endpoint: str = "") -> list[str]:
|
||||||
"""Verify RustFS is reachable (returns 403 = alive, needs auth)."""
|
"""Verify RustFS is reachable (returns 403 = alive, needs auth)."""
|
||||||
|
if not endpoint:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
endpoint = cfg.services.rustfs
|
||||||
errors = []
|
errors = []
|
||||||
try:
|
try:
|
||||||
resp = httpx.get(f"{endpoint}/minio/health/live", timeout=10)
|
resp = httpx.get(f"{endpoint}/minio/health/live", timeout=10)
|
||||||
|
|||||||
@@ -209,12 +209,16 @@ def provision_gitea(
|
|||||||
values: dict[str, str],
|
values: dict[str, str],
|
||||||
*,
|
*,
|
||||||
admin_user: str = "kert",
|
admin_user: str = "kert",
|
||||||
base_url: str = "http://gitea:3000/api/v1",
|
base_url: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Rotate Gitea admin password and create a fresh API token.
|
"""Rotate Gitea admin password and create a fresh API token.
|
||||||
|
|
||||||
Uses the Gitea HTTP API exclusively — no docker exec needed.
|
Uses the Gitea HTTP API exclusively — no docker exec needed.
|
||||||
"""
|
"""
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = f"{cfg.services.gitea}/api/v1"
|
||||||
pw = values["GITEA_ADMIN_PASSWORD"]
|
pw = values["GITEA_ADMIN_PASSWORD"]
|
||||||
|
|
||||||
current_token = values.get("GITEA_TOKEN", "")
|
current_token = values.get("GITEA_TOKEN", "")
|
||||||
@@ -297,9 +301,13 @@ def provision_woodpecker(
|
|||||||
values: dict[str, str],
|
values: dict[str, str],
|
||||||
*,
|
*,
|
||||||
repo_id: int = 1,
|
repo_id: int = 1,
|
||||||
base_url: str = "http://woodpecker-server:8000/api",
|
base_url: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Sync rotated credentials to Woodpecker repo secrets."""
|
"""Sync rotated credentials to Woodpecker repo secrets."""
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = f"{cfg.services.woodpecker}/api"
|
||||||
token = _get_woodpecker_token(values)
|
token = _get_woodpecker_token(values)
|
||||||
|
|
||||||
wp = WoodpeckerClient(token, base_url=base_url)
|
wp = WoodpeckerClient(token, base_url=base_url)
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ class GiteaClient(Client):
|
|||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
*,
|
*,
|
||||||
base_url: str = "http://gitea:3000/api/v1",
|
base_url: str = "",
|
||||||
**kw,
|
**kw,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = f"{cfg.services.gitea}/api/v1"
|
||||||
super().__init__(base_url, **kw)
|
super().__init__(base_url, **kw)
|
||||||
self._token = token
|
self._token = token
|
||||||
|
|
||||||
@@ -94,3 +98,13 @@ class GiteaClient(Client):
|
|||||||
|
|
||||||
def create_issue(self, owner: str, repo: str, body: dict) -> dict:
|
def create_issue(self, owner: str, repo: str, body: dict) -> dict:
|
||||||
return self.post(f"/repos/{owner}/{repo}/issues", json=body).json()
|
return self.post(f"/repos/{owner}/{repo}/issues", json=body).json()
|
||||||
|
|
||||||
|
def list_labels(self, owner: str, repo: str) -> list[dict]:
|
||||||
|
return self.get(f"/repos/{owner}/{repo}/labels", params={"limit": 50}).json()
|
||||||
|
|
||||||
|
def resolve_labels(self, owner: str, repo: str, names: list[str]) -> list[int]:
|
||||||
|
"""Resolve label names to IDs. Unknown names are silently skipped."""
|
||||||
|
if not hasattr(self, "_label_cache"):
|
||||||
|
labels = self.list_labels(owner, repo)
|
||||||
|
self._label_cache = {l["name"]: l["id"] for l in labels}
|
||||||
|
return [self._label_cache[n] for n in names if n in self._label_cache]
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ class RustFSClient(Client):
|
|||||||
access_key: str,
|
access_key: str,
|
||||||
secret_key: str,
|
secret_key: str,
|
||||||
*,
|
*,
|
||||||
base_url: str = "http://rustfs:9000",
|
base_url: str = "",
|
||||||
**kw,
|
**kw,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = cfg.services.rustfs
|
||||||
super().__init__(base_url, **kw)
|
super().__init__(base_url, **kw)
|
||||||
self._access_key = access_key
|
self._access_key = access_key
|
||||||
self._secret_key = secret_key
|
self._secret_key = secret_key
|
||||||
@@ -103,9 +107,13 @@ class RustFSAdmin(Client):
|
|||||||
access_key: str,
|
access_key: str,
|
||||||
secret_key: str,
|
secret_key: str,
|
||||||
*,
|
*,
|
||||||
base_url: str = "http://rustfs:9000/minio/v2",
|
base_url: str = "",
|
||||||
**kw,
|
**kw,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = f"{cfg.services.rustfs}/minio/v2"
|
||||||
super().__init__(base_url, **kw)
|
super().__init__(base_url, **kw)
|
||||||
self._access_key = access_key
|
self._access_key = access_key
|
||||||
self._secret_key = secret_key
|
self._secret_key = secret_key
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ class WoodpeckerClient(Client):
|
|||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
*,
|
*,
|
||||||
base_url: str = "http://woodpecker-server:8000/api",
|
base_url: str = "",
|
||||||
**kw,
|
**kw,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not base_url:
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
base_url = f"{cfg.services.woodpecker}/api"
|
||||||
super().__init__(base_url, **kw)
|
super().__init__(base_url, **kw)
|
||||||
self._token = token
|
self._token = token
|
||||||
|
|
||||||
|
|||||||
@@ -16,40 +16,23 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ── Label ID mapping ──────────────────────────────────────────────
|
# ── Label name patterns ───────────────────────────────────────────
|
||||||
# These IDs correspond to labels created in the Gitea repo.
|
PIPELINE_NAMES = ["ci", "deploy", "harden", "infra-ci", "rebuild-all", "release"]
|
||||||
PIPELINE_LABELS: dict[str, int] = {
|
STEP_KEYWORDS = [
|
||||||
"ci": 14,
|
"lint",
|
||||||
"deploy": 15,
|
"test",
|
||||||
"harden": 16,
|
"build",
|
||||||
"infra-ci": 17,
|
"scan",
|
||||||
"rebuild-all": 18,
|
"deploy",
|
||||||
"release": 19,
|
"publish",
|
||||||
}
|
"hadolint",
|
||||||
|
"validate",
|
||||||
STEP_LABELS: dict[str, int] = {
|
]
|
||||||
"lint": 20,
|
IMAGE_KEYWORDS = ["api", "notebooks", "zotero", "docs", "mc"]
|
||||||
"test": 21,
|
|
||||||
"build": 22,
|
|
||||||
"scan": 23,
|
|
||||||
"deploy": 24,
|
|
||||||
"publish": 25,
|
|
||||||
"hadolint": 26,
|
|
||||||
"validate": 27,
|
|
||||||
}
|
|
||||||
|
|
||||||
IMAGE_LABELS: dict[str, int] = {
|
|
||||||
"api": 28,
|
|
||||||
"notebooks": 29,
|
|
||||||
"zotero": 30,
|
|
||||||
"docs": 31,
|
|
||||||
"mc": 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
INFRA_LABEL = 33
|
|
||||||
CI_LABEL = 13 # existing "ci" label
|
|
||||||
|
|
||||||
|
|
||||||
def _get_env(name: str) -> str:
|
def _get_env(name: str) -> str:
|
||||||
@@ -59,30 +42,30 @@ def _get_env(name: str) -> str:
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def _resolve_labels(pipeline_name: str, step_name: str) -> list[int]:
|
def _resolve_label_names(pipeline_name: str, step_name: str) -> list[str]:
|
||||||
"""Resolve label IDs from pipeline and step names."""
|
"""Resolve label names from pipeline and step names."""
|
||||||
labels: list[int] = [CI_LABEL]
|
names: list[str] = ["ci"]
|
||||||
|
|
||||||
# Pipeline label
|
# Pipeline label
|
||||||
for key, lid in PIPELINE_LABELS.items():
|
for key in PIPELINE_NAMES:
|
||||||
if key in pipeline_name:
|
if key in pipeline_name:
|
||||||
labels.append(lid)
|
names.append(f"pipeline:{key}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Step category label — match the broadest category
|
# Step category label
|
||||||
step_lower = step_name.lower()
|
step_lower = step_name.lower()
|
||||||
for key, lid in STEP_LABELS.items():
|
for key in STEP_KEYWORDS:
|
||||||
if key in step_lower:
|
if key in step_lower:
|
||||||
labels.append(lid)
|
names.append(f"step:{key}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Image label — detect which image the step relates to
|
# Image label
|
||||||
for key, lid in IMAGE_LABELS.items():
|
for key in IMAGE_KEYWORDS:
|
||||||
if key in step_lower:
|
if key in step_lower:
|
||||||
labels.append(lid)
|
names.append(f"image:{key}")
|
||||||
break
|
break
|
||||||
|
|
||||||
return labels
|
return names
|
||||||
|
|
||||||
|
|
||||||
def _pipeline_name_from_env() -> str:
|
def _pipeline_name_from_env() -> str:
|
||||||
@@ -126,7 +109,8 @@ def main() -> int:
|
|||||||
if "/" in repo_name:
|
if "/" in repo_name:
|
||||||
owner, repo = repo_name.split("/", 1)
|
owner, repo = repo_name.split("/", 1)
|
||||||
else:
|
else:
|
||||||
owner, repo = "homelab", "stack"
|
parts = cfg.platform.repo.split("/", 1)
|
||||||
|
owner, repo = parts[0], parts[1]
|
||||||
|
|
||||||
pipeline_name = _pipeline_name_from_env()
|
pipeline_name = _pipeline_name_from_env()
|
||||||
|
|
||||||
@@ -135,9 +119,7 @@ def main() -> int:
|
|||||||
|
|
||||||
wp = WoodpeckerClient(
|
wp = WoodpeckerClient(
|
||||||
gitea_token,
|
gitea_token,
|
||||||
base_url=os.environ.get(
|
base_url=os.environ.get("CI_WOODPECKER_URL", f"{cfg.services.woodpecker}/api"),
|
||||||
"CI_WOODPECKER_URL", "http://woodpecker-server:8000/api"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rid = int(repo_id)
|
rid = int(repo_id)
|
||||||
@@ -167,7 +149,8 @@ def main() -> int:
|
|||||||
|
|
||||||
log.info("Processing failed step: %s (id=%d)", step_name, step_id)
|
log.info("Processing failed step: %s (id=%d)", step_name, step_id)
|
||||||
|
|
||||||
labels = _resolve_labels(pipeline_name, step_name)
|
label_names = _resolve_label_names(pipeline_name, step_name)
|
||||||
|
labels = gitea.resolve_labels(owner, repo, label_names)
|
||||||
|
|
||||||
# Fetch logs — Woodpecker returns a list of log line dicts
|
# Fetch logs — Woodpecker returns a list of log line dicts
|
||||||
try:
|
try:
|
||||||
|
|||||||
17
stack.toml
17
stack.toml
@@ -13,6 +13,23 @@
|
|||||||
# the [db], [storage], and [lake] sections supply the concrete
|
# the [db], [storage], and [lake] sections supply the concrete
|
||||||
# connection details for the selected backend.
|
# connection details for the selected backend.
|
||||||
|
|
||||||
|
[platform]
|
||||||
|
domain = "homelab.fhirworx.io"
|
||||||
|
registry = "gitea.homelab.fhirworx.io"
|
||||||
|
image_prefix = "fhirworx"
|
||||||
|
repo = "homelab/stack"
|
||||||
|
ci_email = "ci@fhirworx.io"
|
||||||
|
host_ip = "192.168.1.192"
|
||||||
|
|
||||||
|
[services]
|
||||||
|
gitea = "http://gitea:3000"
|
||||||
|
woodpecker = "http://woodpecker-server:8000"
|
||||||
|
rustfs = "http://rustfs:9000"
|
||||||
|
nessie = "http://nessie:19120"
|
||||||
|
polaris = "http://polaris:8181"
|
||||||
|
trino = "http://trino:8080"
|
||||||
|
grafana = "http://grafana:3000"
|
||||||
|
|
||||||
[context]
|
[context]
|
||||||
active = "local"
|
active = "local"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user