feat(standards): add Drone CI standard (PyPI package + Docker deployment)
Document the two `.drone.yml` shapes used across the org, derived from the repos: - Type A `python-lib.yaml`: build-and-publish-pypi + backfill-pypi-from-tags - Type B `service-docker.yaml`: build-and-deploy-service + build-and-deploy-site - add reference page `10_drone_pipelines` - vendor `src/templates/drone/` into the MCP bundle (`drone.json`) - expose `registry://drone` + `list_drone_templates` / `get_drone_template` - wire `build.py` to vendor drone templates; rebuild nav/index (14 modules)
This commit is contained in:
164
standards/mcp/templates/drone/python-lib.yaml
Normal file
164
standards/mcp/templates/drone/python-lib.yaml
Normal file
@@ -0,0 +1,164 @@
|
||||
# Drone standard — Type A: PyPI package
|
||||
#
|
||||
# Canonical pipeline for internal Python libraries that ship a wheel/sdist to
|
||||
# the private index (dagpipe, doc-forge, hexa, mail-intake, mongo-ops,
|
||||
# omniread, openapi-first, py-jwt).
|
||||
#
|
||||
# Two pipelines ship together:
|
||||
# build-and-publish-pypi — tag-triggered build + quality gate + publish.
|
||||
# backfill-pypi-from-tags — custom-triggered replay of every git tag that is
|
||||
# missing from the index (disaster recovery).
|
||||
#
|
||||
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD.
|
||||
# Copy to <repo>/.drone.yml unchanged; the package name and version are read
|
||||
# from pyproject.toml, so no per-repo edits are required.
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-publish-pypi
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
workspace:
|
||||
path: /drone/src
|
||||
|
||||
steps:
|
||||
- name: check-version
|
||||
image: curlimages/curl:latest
|
||||
environment:
|
||||
PIP_REPO_URL:
|
||||
from_secret: PIP_REPO_URL
|
||||
PIP_USERNAME:
|
||||
from_secret: PIP_USERNAME
|
||||
PIP_PASSWORD:
|
||||
from_secret: PIP_PASSWORD
|
||||
commands:
|
||||
- PACKAGE_NAME=$(grep -E '^name\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||
- VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||
- echo "Checking if $PACKAGE_NAME==$VERSION exists on $PIP_REPO_URL ..."
|
||||
- |
|
||||
if curl -fsSL -u "$PIP_USERNAME:$PIP_PASSWORD" "$PIP_REPO_URL/simple/$PACKAGE_NAME/" | grep -qE "${PACKAGE_NAME}-${VERSION}(-|\.tar\.gz|\.whl)"; then
|
||||
echo "$PACKAGE_NAME==$VERSION already exists; skipping build."
|
||||
exit 78
|
||||
else
|
||||
echo "New version detected: $PACKAGE_NAME==$VERSION"
|
||||
fi
|
||||
|
||||
- name: quality-gate
|
||||
image: python:3.13-slim
|
||||
environment:
|
||||
PIP_REPO_URL:
|
||||
from_secret: PIP_REPO_URL
|
||||
PIP_USERNAME:
|
||||
from_secret: PIP_USERNAME
|
||||
PIP_PASSWORD:
|
||||
from_secret: PIP_PASSWORD
|
||||
commands:
|
||||
- pip install --upgrade pip build
|
||||
- |
|
||||
AUTH_URL="https://${PIP_USERNAME}:${PIP_PASSWORD}@$(echo "${PIP_REPO_URL#*://}" | sed 's:/*$::')/simple"
|
||||
pip install --index-url "$AUTH_URL" --extra-index-url https://pypi.org/simple/ -U ".[dev]" "pydoclint>=0.9.1"
|
||||
- echo "Running quality gate..."
|
||||
- python -m black --check .
|
||||
- python -m ruff check .
|
||||
- python -m mypy
|
||||
- pydoclint .
|
||||
- python -m pytest
|
||||
|
||||
- name: build-package
|
||||
image: python:3.13-slim
|
||||
commands:
|
||||
- pip install --upgrade pip build
|
||||
- echo "Building Python package..."
|
||||
- python -m build
|
||||
- ls -l dist
|
||||
|
||||
- name: upload-to-private-pypi
|
||||
image: python:3.13-slim
|
||||
environment:
|
||||
PIP_REPO_URL:
|
||||
from_secret: PIP_REPO_URL
|
||||
PIP_USERNAME:
|
||||
from_secret: PIP_USERNAME
|
||||
PIP_PASSWORD:
|
||||
from_secret: PIP_PASSWORD
|
||||
commands:
|
||||
- pip install --upgrade twine
|
||||
- echo "Uploading to private PyPI at $PIP_REPO_URL ..."
|
||||
- |
|
||||
twine upload \
|
||||
--repository-url "$PIP_REPO_URL" \
|
||||
-u "$PIP_USERNAME" \
|
||||
-p "$PIP_PASSWORD" \
|
||||
dist/*
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: backfill-pypi-from-tags
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
workspace:
|
||||
path: /drone/src
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
|
||||
- name: build-and-upload-missing
|
||||
image: python:3.13-slim
|
||||
environment:
|
||||
PIP_REPO_URL:
|
||||
from_secret: PIP_REPO_URL
|
||||
PIP_USERNAME:
|
||||
from_secret: PIP_USERNAME
|
||||
PIP_PASSWORD:
|
||||
from_secret: PIP_PASSWORD
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y git curl ca-certificates
|
||||
- pip install --upgrade pip build twine
|
||||
- |
|
||||
set -e
|
||||
|
||||
PACKAGE_NAME=$(grep -E '^name\s*=' pyproject.toml | cut -d'"' -f2)
|
||||
echo "Package: $PACKAGE_NAME"
|
||||
|
||||
for TAG in $(git tag --sort=version:refname); do
|
||||
VERSION="$TAG"
|
||||
echo "Version: $VERSION"
|
||||
|
||||
if curl -fsSL -u "$PIP_USERNAME:$PIP_PASSWORD" \
|
||||
"$PIP_REPO_URL/simple/$PACKAGE_NAME/" | grep -qE "${PACKAGE_NAME}-${VERSION}(-|\.tar\.gz|\.whl)"; then
|
||||
echo "Exists, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
git checkout --force "$TAG"
|
||||
|
||||
echo "Building $VERSION"
|
||||
rm -rf dist
|
||||
python -m build
|
||||
|
||||
echo "Uploading $VERSION"
|
||||
twine upload \
|
||||
--repository-url "$PIP_REPO_URL" \
|
||||
-u "$PIP_USERNAME" \
|
||||
-p "$PIP_PASSWORD" \
|
||||
dist/*
|
||||
done
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- custom
|
||||
257
standards/mcp/templates/drone/service-docker.yaml
Normal file
257
standards/mcp/templates/drone/service-docker.yaml
Normal file
@@ -0,0 +1,257 @@
|
||||
# Drone standard — Type B: Docker deployment
|
||||
#
|
||||
# Canonical pipelines for services that build an image, push it to the private
|
||||
# registry, and run it on the Docker host (auth-server, docs, mcp).
|
||||
#
|
||||
# Two variants ship together:
|
||||
# build-and-deploy-service — a long-running API/worker service: fetch the git
|
||||
# tag, skip if the image already exists, run the
|
||||
# quality gate, build, push, restart the container.
|
||||
# build-and-deploy-site — a static/HTTP site: build, push, restart, on
|
||||
# every push (no tag, no quality gate).
|
||||
#
|
||||
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD (service only),
|
||||
# REGISTRY_HOST, REGISTRY_USER, REGISTRY_PASS, plus the service's runtime env
|
||||
# (for example MONGO_* / JWT_SECRET).
|
||||
#
|
||||
# Placeholders to set per repo:
|
||||
# <app> image / container / network name (e.g. auth-server).
|
||||
# aetos/<app> image repository on the private registry.
|
||||
# <host-port> published host port; <container-port> the in-image port.
|
||||
# <site> site image name for the site variant (e.g. docs, mcp).
|
||||
# <env-var> runtime environment variable consumed by the service.
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-deploy-service
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
workspace:
|
||||
path: /drone/src
|
||||
|
||||
volumes:
|
||||
- name: dockersock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- git fetch --tags
|
||||
- |
|
||||
# Get latest Git tag and trim newline
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null | tr -d '\n')
|
||||
echo "Latest Git tag fetched: $LATEST_TAG"
|
||||
|
||||
# Save to file for downstream steps
|
||||
echo "$LATEST_TAG" > /drone/src/LATEST_TAG.txt
|
||||
|
||||
# Read back for verification
|
||||
IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
|
||||
echo "Image tag read from file: $IMAGE_TAG"
|
||||
|
||||
# Validate
|
||||
if [ -z "$IMAGE_TAG" ]; then
|
||||
echo "No git tags found! Cannot continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: check-remote-image
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
|
||||
- echo "Checking if aetos/<app>:$IMAGE_TAG exists on remote Docker..."
|
||||
- echo "Existing Docker tags for aetos/<app>:"
|
||||
- docker images --format "{{.Repository}}:{{.Tag}}" | grep "^aetos/<app>" || echo "(none)"
|
||||
- |
|
||||
if docker image inspect aetos/<app>:$IMAGE_TAG > /dev/null 2>&1; then
|
||||
echo "Docker image aetos/<app>:$IMAGE_TAG already exists; skipping build"
|
||||
exit 78
|
||||
else
|
||||
echo "Docker image aetos/<app>:$IMAGE_TAG not found; proceeding to build..."
|
||||
fi
|
||||
|
||||
- name: quality-gate
|
||||
image: python:3.13-slim
|
||||
environment:
|
||||
PIP_REPO_URL:
|
||||
from_secret: PIP_REPO_URL
|
||||
PIP_USERNAME:
|
||||
from_secret: PIP_USERNAME
|
||||
PIP_PASSWORD:
|
||||
from_secret: PIP_PASSWORD
|
||||
commands:
|
||||
- pip install --upgrade pip
|
||||
- pip install --extra-index-url "https://$PIP_USERNAME:$PIP_PASSWORD@$PIP_REPO_URL/simple" -r requirements.txt
|
||||
- pip install black ruff mypy
|
||||
- echo "Running quality gate..."
|
||||
- python -m black --check .
|
||||
- python -m ruff check .
|
||||
- python -m mypy .
|
||||
- python -m pytest
|
||||
|
||||
- name: build-image
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
PIP_REPO_URL:
|
||||
from_secret: PIP_REPO_URL
|
||||
PIP_USERNAME:
|
||||
from_secret: PIP_USERNAME
|
||||
PIP_PASSWORD:
|
||||
from_secret: PIP_PASSWORD
|
||||
commands:
|
||||
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
|
||||
- echo "Building Docker image aetos/<app>:$IMAGE_TAG ..."
|
||||
- |
|
||||
docker build \
|
||||
--network=host \
|
||||
--build-arg PIP_REPO_URL="$PIP_REPO_URL" \
|
||||
--build-arg PIP_USERNAME="$PIP_USERNAME" \
|
||||
--build-arg PIP_PASSWORD="$PIP_PASSWORD" \
|
||||
-t aetos/<app>:$IMAGE_TAG \
|
||||
-t aetos/<app>:latest \
|
||||
/drone/src
|
||||
|
||||
- name: push-image
|
||||
image: docker:24
|
||||
environment:
|
||||
REGISTRY_HOST:
|
||||
from_secret: REGISTRY_HOST
|
||||
REGISTRY_USER:
|
||||
from_secret: REGISTRY_USER
|
||||
REGISTRY_PASS:
|
||||
from_secret: REGISTRY_PASS
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
|
||||
- echo "Logging into registry $REGISTRY_HOST ..."
|
||||
- echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin
|
||||
- echo "Tagging images with registry prefix..."
|
||||
- docker tag aetos/<app>:$IMAGE_TAG $REGISTRY_HOST/aetos/<app>:$IMAGE_TAG
|
||||
- docker tag aetos/<app>:$IMAGE_TAG $REGISTRY_HOST/aetos/<app>:latest
|
||||
- echo "Pushing aetos/<app>:$IMAGE_TAG ..."
|
||||
- docker push $REGISTRY_HOST/aetos/<app>:$IMAGE_TAG
|
||||
- echo "Pushing aetos/<app>:latest ..."
|
||||
- docker push $REGISTRY_HOST/aetos/<app>:latest
|
||||
|
||||
- name: stop-old
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Stopping old container..."
|
||||
- docker rm -f <app> || true
|
||||
|
||||
- name: run-container
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
<ENV_VAR>:
|
||||
from_secret: <ENV_VAR>
|
||||
commands:
|
||||
- IMAGE_TAG=$(cat /drone/src/LATEST_TAG.txt | tr -d '\n')
|
||||
- echo "Starting container aetos/<app>:$IMAGE_TAG ..."
|
||||
- |
|
||||
docker run -d \
|
||||
--name <app> \
|
||||
-p <host-port>:<container-port> \
|
||||
--add-host private-pi:192.168.1.111 \
|
||||
--restart always \
|
||||
-e <ENV_VAR>=$<ENV_VAR> \
|
||||
aetos/<app>:$IMAGE_TAG
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-deploy-site
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
workspace:
|
||||
path: /drone/src
|
||||
|
||||
volumes:
|
||||
- name: dockersock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: build-image
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Building Docker image for aetos/<site>..."
|
||||
- docker build -t aetos/<site>:latest .
|
||||
|
||||
- name: push-image
|
||||
image: docker:24
|
||||
environment:
|
||||
REGISTRY_HOST:
|
||||
from_secret: REGISTRY_HOST
|
||||
REGISTRY_USER:
|
||||
from_secret: REGISTRY_USER
|
||||
REGISTRY_PASS:
|
||||
from_secret: REGISTRY_PASS
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Logging into registry $REGISTRY_HOST ..."
|
||||
- echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin
|
||||
- docker tag aetos/<site>:latest $REGISTRY_HOST/aetos/<site>:latest
|
||||
- echo "Pushing image to $REGISTRY_HOST..."
|
||||
- docker push $REGISTRY_HOST/aetos/<site>:latest
|
||||
|
||||
- name: stop-old
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Stopping old container..."
|
||||
- docker rm -f <site> || true
|
||||
|
||||
- name: run-container
|
||||
image: docker:24
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- echo "Starting aetos/<site>:latest ..."
|
||||
- |
|
||||
docker run -d \
|
||||
--name <site> \
|
||||
-p <host-port>:<container-port> \
|
||||
--restart always \
|
||||
aetos/<site>:latest
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- push
|
||||
Reference in New Issue
Block a user