Docker compose refactor

Added shell interpolation for environment variables
This commit is contained in:
Sonny Bakker 2025-05-05 15:02:03 +02:00
parent e96c6f3528
commit 10affeb32f
16 changed files with 298 additions and 287 deletions

79
Dockerfile Normal file
View file

@ -0,0 +1,79 @@
# stage 1
FROM python:3.11-alpine AS backend
ARG USER_UID=1000
ARG GROUP_UID=1000
RUN apk update \
&& apk add --no-cache \
vim \
curl \
gettext
RUN addgroup -g $USER_UID newsreader && adduser -Du $GROUP_UID -G newsreader newsreader
RUN mkdir --parents /app/src /app/logs /app/media /app/bin /app/static \
&& chown -R newsreader:newsreader /app
WORKDIR /app
USER newsreader
COPY --chown=newsreader:newsreader uv.lock pyproject.toml /app/
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
RUN --mount=type=cache,target=$HOME/.cache/uv \
uv sync --frozen --no-default-groups --no-install-project
COPY --chown=newsreader:newsreader ./bin/docker-entrypoint.sh /app/bin/docker-entrypoint.sh
VOLUME ["/app/logs", "/app/media", "/app/static"]
# stage 2
FROM node:lts-alpine AS frontend-build
ARG BUILD_ARG=prod
WORKDIR /app
RUN chown node:node /app
USER node
COPY --chown=node:node ./package*.json ./webpack.*.js ./babel.config.js /app/
RUN --mount=type=cache,target=$HOME/.npm \
npm ci
COPY --chown=node:node ./src /app/src
RUN npm run build:$BUILD_ARG
# stage 3
FROM backend AS production
COPY --from=frontend-build --chown=newsreader:newsreader \
/app/src/newsreader/static /app/src/newsreader/static
RUN --mount=type=cache,target=$HOME/.cache/uv \
uv sync --frozen --only-group production --extra sentry
COPY --chown=newsreader:newsreader ./src /app/src
# Note that the static volume will have to be recreated to be pre-populated
# correctly with the latest static files. See
# https://docs.docker.com/storage/volumes/#populate-a-volume-using-a-container
RUN uv run --no-sync -- src/manage.py collectstatic --noinput
# (optional) stage 4
FROM backend AS development
RUN --mount=type=cache,target=$HOME/.cache/uv \
uv sync --frozen --group development

View file

@ -1,5 +1,5 @@
#!/bin/bash
/app/.venv/bin/python /app/src/manage.py migrate
uv run --no-sync -- /app/src/manage.py migrate
exec "$@"

View file

@ -1,18 +1,11 @@
volumes:
static-files:
node-modules:
services:
celery:
build:
target: development
volumes:
- ./src/:/app/src
django:
build:
build: &app-development-build
target: development
command: /app/.venv/bin/python /app/src/manage.py runserver 0.0.0.0:8000
command: uv run --no-sync -- /app/src/manage.py runserver 0.0.0.0:8000
ports:
- "${DJANGO_PORT:-8000}:8000"
volumes:
@ -21,12 +14,19 @@ services:
stdin_open: true
tty: true
celery:
build:
<<: *app-development-build
volumes:
- ./src/:/app/src
webpack:
build:
target: frontend-build
context: .
dockerfile: ./docker/webpack
args:
BUILD_ARG: "dev"
command: npm run build:watch
volumes:
- ./src/:/app/src
- static-files:/app/src/newsreader/static
- node-modules:/app/node_modules

View file

@ -9,7 +9,6 @@ services:
django:
condition: service_healthy
ports:
# Note that --env-file should be used to set these correctly
- "${NGINX_HTTP_PORT:-80}:80"
volumes:
- ./config/nginx/conf.d:/etc/nginx/conf.d

View file

@ -4,33 +4,43 @@ volumes:
postgres-data:
static-files:
x-db-env: &db-env
POSTGRES_HOST:
POSTGRES_PORT:
POSTGRES_DB:
POSTGRES_USER:
POSTGRES_PASSWORD:
x-db-connection-env: &db-connection-env
POSTGRES_HOST: ${POSTGRES_HOST:-db}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_DB: &pg-database ${POSTGRES_DB:-newsreader}
POSTGRES_USER: &pg-user ${POSTGRES_USER:-newsreader}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-newsreader}
x-django-build-env: &django-build-env
<<: *db-env
DJANGO_SECRET_KEY:
DJANGO_SETTINGS_MODULE:
x-db-env: &db-env
<<: *db-connection-env
PGUSER: *pg-user
PGDATABASE: *pg-database
x-django-env: &django-env
<<: *django-build-env
VERSION:
<<: *db-connection-env
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,django}
INTERNAL_IPS: ${INTERNAL_IPS:-localhost,127.0.0.1,django}
# see token_urlsafe from python's secret module to generate one
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-Ojg68lYsP3kq2r5JgozUzKVSRFywm17BTMS5iwpLM44}
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-newsreader.conf.production}
ADMINS: ${ADMINS:-""}
VERSION: ${VERSION:-""}
# Email
EMAIL_HOST:
EMAIL_PORT:
EMAIL_HOST_USER:
EMAIL_HOST_PASSWORD:
EMAIL_USE_TLS:
EMAIL_USE_SSL:
EMAIL_DEFAULT_FROM:
EMAIL_HOST: ${EMAIL_HOST:-localhost}
EMAIL_PORT: ${EMAIL_PORT:-25}
EMAIL_HOST_USER: ${EMAIL_HOST_USER:-""}
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD:-""}
EMAIL_USE_TLS: ${EMAIL_USE_TLS:-no}
EMAIL_USE_SSL: ${EMAIL_USE_SSL:-no}
EMAIL_DEFAULT_FROM: ${EMAIL_DEFAULT_FROM:-webmaster@localhost}
# Sentry
SENTRY_DSN:
SENTRY_DSN: ${SENTRY_DSN:-""}
services:
db:
@ -38,8 +48,8 @@ services:
<<: *db-env
image: postgres:15
healthcheck:
# Note that --env-file should be used to set these correctly
test: /usr/bin/pg_isready --username="${POSTGRES_USER}" --dbname="${POSTGRES_DB}"
test: /usr/bin/pg_isready
start_period: 10s
interval: 5s
timeout: 10s
retries: 10
@ -55,58 +65,23 @@ services:
- memcached
- -m 64
celery:
build:
context: .
dockerfile: ./docker/django
target: production
args:
<<: *django-build-env
environment:
<<: *django-env
command: |
/app/.venv/bin/celery --app newsreader
--workdir /app/src/
worker --loglevel INFO
--concurrency 2
--beat
--scheduler django
-n worker1@%h
-n worker2@%h
healthcheck:
test: celery --app newsreader status || exit 1
interval: 10s
timeout: 10s
retries: 5
depends_on:
rabbitmq:
condition: service_started
memcached:
condition: service_started
db:
condition: service_healthy
django:
condition: service_healthy
volumes:
- logs:/app/logs
django:
build:
build: &app-build
context: .
dockerfile: ./docker/django
target: production
args:
<<: *django-build-env
environment:
<<: *django-env
entrypoint: /app/bin/docker-entrypoint.sh
entrypoint: ["/bin/sh", "/app/bin/docker-entrypoint.sh"]
command: |
/app/.venv/bin/gunicorn --bind 0.0.0.0:8000
uv run --no-sync --
gunicorn
--bind 0.0.0.0:8000
--workers 3
--chdir /app/src/
newsreader.wsgi:application
healthcheck:
test: /usr/bin/curl --fail http://django:8000 || exit 1
start_period: 10s
interval: 10s
timeout: 10s
retries: 5
@ -119,3 +94,33 @@ services:
- logs:/app/logs
- media:/app/media
- static-files:/app/static
celery:
build:
<<: *app-build
environment:
<<: *django-env
command: |
uv run --no-sync --
celery
--app newsreader
--workdir /app/src/
worker --loglevel INFO
--concurrency 2
--beat
--scheduler django
-n worker1@%h
-n worker2@%h
healthcheck:
test: uv run --no-sync -- celery --app newsreader status || exit 1
start_period: 10s
interval: 10s
timeout: 10s
retries: 5
depends_on:
rabbitmq:
condition: service_started
django:
condition: service_healthy
volumes:
- logs:/app/logs

View file

@ -1,102 +0,0 @@
# stage 1
FROM python:3.11-bookworm AS backend
RUN apt-get update && apt-get install --yes --no-install-recommends \
vim \
curl \
gettext \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN mkdir /app/src
RUN mkdir /app/logs
RUN mkdir /app/media
COPY uv.lock pyproject.toml /app/
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
RUN uv sync --frozen --no-default-groups --no-install-project
# stage 2
FROM node:lts AS frontend-build
WORKDIR /app
COPY ./*.json ./*.js ./babel.config.js /app/
RUN npm ci
COPY ./src /app/src
RUN npm run build:prod
# stage 3
FROM python:3.11-bookworm AS production
RUN apt-get update && apt-get install --yes --no-install-recommends \
postgresql-client \
vim \
curl \
gettext \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN mkdir /app/logs
RUN mkdir /app/media
RUN mkdir /app/bin
COPY --from=backend /app/.venv /app/.venv
COPY --from=backend /app/uv.lock /app/pyproject.toml /app/
COPY --from=backend /bin/uv /bin/uv
COPY ./bin/docker-entrypoint.sh /app/bin/docker-entrypoint.sh
COPY --from=frontend-build /app/src/newsreader/static /app/src/newsreader/static
COPY ./src /app/src
RUN uv sync --frozen --only-group production --extra sentry
RUN useradd --no-create-home --uid 1000 newsreader
RUN chown --recursive newsreader:newsreader /app
USER newsreader
ARG POSTGRES_HOST
ARG POSTGRES_PORT
ARG POSTGRES_DB
ARG POSTGRES_USER
ARG POSTGRES_PASSWORD
ARG DJANGO_SECRET_KEY
ARG DJANGO_SETTINGS_MODULE
# Note that the static volume will have to be recreated to be pre-populated
# correctly with the latest static files. See
# https://docs.docker.com/storage/volumes/#populate-a-volume-using-a-container
RUN /app/.venv/bin/python src/manage.py collectstatic --noinput
# (optional) stage 4
FROM python:3.11-bookworm AS development
RUN apt-get update && apt-get install --yes --no-install-recommends \
vim \
curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN mkdir /app/logs
RUN mkdir /app/media
RUN mkdir /app/bin
COPY --from=backend /app/.venv /app/.venv
COPY --from=backend /app/uv.lock /app/pyproject.toml /app/
COPY ./bin/docker-entrypoint.sh /app/bin/docker-entrypoint.sh
COPY --from=backend /app/src/ /app/src/
COPY --from=backend /bin/uv /bin/uv
RUN uv sync --frozen --group development

View file

@ -1,10 +0,0 @@
FROM node:lts
WORKDIR /app
RUN mkdir /app/src
COPY package*.json webpack.*.js babel.config.js /app/
RUN npm install
COPY ./src /app/src

View file

@ -5,8 +5,8 @@
"scripts": {
"lint": "npx prettier \"src/newsreader/js/**/*.js\" --check",
"format": "npx prettier \"src/newsreader/js/**/*.js\" --write",
"build": "npx webpack --config webpack.dev.babel.js",
"build:watch": "npx webpack --config webpack.dev.babel.js --watch",
"build:dev": "npx webpack --config webpack.dev.babel.js",
"build:prod": "npx webpack --config webpack.prod.babel.js",
"test": "npx jest",
"test:watch": "npm test -- --watch"

View file

@ -7,7 +7,7 @@ requires-python = ">=3.11"
dependencies = [
"django~=4.2",
"celery~=5.4",
"psycopg",
"psycopg[binary]",
"django-axes",
"django-celery-beat~=2.7.0",
"django-rest-framework",
@ -36,7 +36,7 @@ production = ["gunicorn~=23.0"]
sentry = ["sentry-sdk~=2.0"]
[tool.uv]
environments = ["sys_platform == "linux""]
environments = ["sys_platform == 'linux'"]
default-groups = ["test-tools"]
[tool.ruff]
@ -68,7 +68,7 @@ django = ["django"]
[tool.coverage.run]
source = ["./src/newsreader/"]
omit = [
"**/tests/**"
"**/tests/**",
"**/migrations/**",
"**/conf/**",
"**/apps.py",
@ -77,5 +77,5 @@ omit = [
"**/urls.py",
"**/wsgi.py",
"**/celery.py",
"**/__init__.py
"**/__init__.py"
]

View file

@ -4,6 +4,8 @@ from pathlib import Path
from dotenv import load_dotenv
from newsreader.conf.utils import get_env, get_root_dir
load_dotenv()
@ -15,16 +17,13 @@ except ImportError:
DjangoIntegration = None
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
BASE_DIR = get_root_dir()
DJANGO_PROJECT_DIR = BASE_DIR / "src" / "newsreader"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: don"t run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
INTERNAL_IPS = ["127.0.0.1", "localhost"]
ALLOWED_HOSTS = get_env("ALLOWED_HOSTS", split=",", default=["127.0.0.1", "localhost"])
INTERNAL_IPS = get_env("INTERNAL_IPS", split=",", default=["127.0.0.1", "localhost"])
# Application definition
INSTALLED_APPS = [
@ -48,7 +47,7 @@ INSTALLED_APPS = [
"newsreader.news.collection",
]
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
SECRET_KEY = get_env("DJANGO_SECRET_KEY", default="")
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesBackend",
@ -73,11 +72,10 @@ FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(DJANGO_PROJECT_DIR, "templates")],
"DIRS": [DJANGO_PROJECT_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
@ -88,16 +86,14 @@ TEMPLATES = [
WSGI_APPLICATION = "newsreader.wsgi.application"
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": os.environ["POSTGRES_HOST"],
"PORT": os.environ["POSTGRES_PORT"],
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": get_env("POSTGRES_HOST", default=""),
"PORT": get_env("POSTGRES_PORT", default=""),
"NAME": get_env("POSTGRES_DB", default=""),
"USER": get_env("POSTGRES_USER", default=""),
"PASSWORD": get_env("POSTGRES_PASSWORD", default=""),
}
}
@ -114,8 +110,6 @@ CACHES = {
},
}
# Logging
# https://docs.djangoproject.com/en/2.2/topics/logging/#configuring-logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@ -169,8 +163,6 @@ LOGGING = {
},
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
@ -185,8 +177,6 @@ AUTH_USER_MODEL = "accounts.User"
LOGIN_REDIRECT_URL = "/"
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/Amsterdam"
@ -194,20 +184,33 @@ USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [os.path.join(DJANGO_PROJECT_DIR, "static")]
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_DIRS = (
DJANGO_PROJECT_DIR / "static",
)
# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-STATICFILES_FINDERS
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# Email
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = get_env(
"EMAIL_DEFAULT_FROM", required=False, default="webmaster@localhost"
)
EMAIL_HOST = get_env("EMAIL_HOST", required=False, default="localhost")
EMAIL_PORT = get_env("EMAIL_PORT", cast=int, required=False, default=25)
EMAIL_HOST_USER = get_env("EMAIL_HOST_USER", required=False, default="")
EMAIL_HOST_PASSWORD = get_env("EMAIL_HOST_PASSWORD", required=False, default="")
EMAIL_USE_TLS = get_env("EMAIL_USE_TLS", required=False, default=False)
EMAIL_USE_SSL = get_env("EMAIL_USE_SSL", required=False, default=False)
# Third party settings
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
@ -216,7 +219,6 @@ AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 3 # in hours
AXES_RESET_ON_SUCCESS = True
# TODO: verify parser works correctly
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
@ -247,7 +249,7 @@ CELERY_BROKER_URL = "amqp://guest@rabbitmq:5672"
# Sentry
SENTRY_CONFIG = {
"dsn": os.environ.get("SENTRY_DSN"),
"dsn": get_env("SENTRY_DSN", default="", required=False),
"send_default_pii": False,
"integrations": [DjangoIntegration(), CeleryIntegration()]
if DjangoIntegration and CeleryIntegration

View file

@ -1,5 +1,5 @@
from .base import * # noqa: F403
from .version import get_current_version
from .utils import get_current_version
DEBUG = True

View file

@ -1,5 +1,5 @@
from .base import * # noqa: F403
from .version import get_current_version
from .utils import get_current_version
SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl"
@ -10,6 +10,11 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
INSTALLED_APPS += ["debug_toolbar", "django_extensions"] # noqa: F405
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
"django.template.context_processors.debug",
)
# Project settings
VERSION = get_current_version()

View file

@ -1,8 +1,8 @@
from .base import * # noqa: F403
from .version import get_current_version
from .utils import get_current_version
ALLOWED_HOSTS = ["django", "127.0.0.1"]
DEBUG = True
INSTALLED_APPS += ["debug_toolbar", "django_extensions"] # noqa: F405
@ -16,7 +16,10 @@ LOGGING["loggers"].update( # noqa: F405
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEBUG = True
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
"django.template.context_processors.debug",
)
# Project settings
VERSION = get_current_version()

View file

@ -1,49 +1,20 @@
import os
from newsreader.conf.utils import get_env
from .base import * # noqa: F403
from .version import get_current_version
from .utils import get_current_version
DEBUG = False
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
ALLOWED_HOSTS = ["127.0.0.1", "localhost", "rss.fudiggity.nl", "django"]
ADMINS = [
("", email)
for email in os.getenv("ADMINS", "").split(",")
if os.environ.get("ADMINS")
for email in get_env("ADMINS", split=",", required=False, default=[])
]
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(DJANGO_PROJECT_DIR, "templates")], # noqa: F405
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}
]
# Email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_DEFAULT_FROM", "webmaster@localhost")
EMAIL_HOST = os.environ.get("EMAIL_HOST", "localhost")
EMAIL_PORT = os.environ.get("EMAIL_PORT", 25)
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = bool(os.environ.get("EMAIL_USE_TLS"))
EMAIL_USE_SSL = bool(os.environ.get("EMAIL_USE_SSL"))
# Project settings
VERSION = get_current_version(debug=False)
ENVIRONMENT = "production"

View file

@ -0,0 +1,85 @@
import logging
import os
import subprocess
from pathlib import Path
from typing import Any, Iterable, Type
logger = logging.getLogger(__name__)
def get_env(
name: str,
cast: Type = str,
required: bool = True,
default: Any = None,
split: str = ""
) -> Any:
if cast is not str and split:
raise TypeError(f"Split is not possible with {cast}")
value = os.getenv(name)
if not value:
if required:
logger.warning(f"Missing environment variable: {name}")
return default
bool_mapping = {"yes": True, "true": True, "false": False, "no": False}
if cast is bool:
_value = bool_mapping.get(value.lower())
if not value:
raise ValueError(f"Unknown boolean value: {_value}")
return _value
value = value if not cast else cast(value)
return value if not split else value.split(split)
def get_current_version(debug: bool = True) -> str:
version = get_env("VERSION", required=False)
if version:
return version
if debug:
try:
output = subprocess.check_output(
["git", "show", "--no-patch", "--format=%H"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""
try:
output = subprocess.check_output(
["git", "describe", "--tags"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""
ROOT_MARKERS = ("pyproject.toml", "package.json", "README.md", "CHANGELOG.md")
def get_root_dir() -> Path:
file = Path(__file__)
return _traverse_dirs(file.parent, ROOT_MARKERS)
def _traverse_dirs(path: Path, root_markers: Iterable[str]) -> Path:
if path.parent == path:
raise OSError("Root directory detected")
files = (file.name for file in path.iterdir())
if not any((marker for marker in root_markers if marker in files)):
return _traverse_dirs(path.parent, root_markers)
return path

View file

@ -1,26 +0,0 @@
import os
import subprocess
def get_current_version(debug=True):
version = os.environ.get("VERSION")
if version:
return version
if debug:
try:
output = subprocess.check_output(
["git", "show", "--no-patch", "--format=%H"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""
try:
output = subprocess.check_output(
["git", "describe", "--tags"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""