Compare commits

...

2 Commits

Author SHA1 Message Date
Robert Resch
aae63cb397 Adjust docker file to be multistage 2026-03-27 17:32:58 +01:00
Robert Resch
78aa5b9913 Improve build pipeline by triggering translation download async 2026-03-27 17:01:58 +01:00
5 changed files with 170 additions and 89 deletions

View File

@@ -35,6 +35,7 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
translation_download_process: ${{ steps.translations.outputs.translation_download_process }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
@@ -47,6 +48,26 @@ jobs:
with:
python-version-file: ".python-version"
- name: Fail if translations files are checked in
run: |
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
echo "Translations files are checked in, please remove the following files:"
find homeassistant/components/*/translations -type f
exit 1
fi
- name: Start translation download
id: translations
shell: bash
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
run: |
pip install -r script/translations/requirements.txt
PROCESS_OUTPUT=$(python3 -m script.translations download --async-start)
echo "$PROCESS_OUTPUT"
TRANSLATION_DOWNLOAD_PROCESS=$(echo "$PROCESS_OUTPUT" | sed -n 's/.*Process ID: //p')
echo "translation_download_process=$TRANSLATION_DOWNLOAD_PROCESS" >> "$GITHUB_OUTPUT"
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
@@ -62,30 +83,6 @@ jobs:
with:
ignore-dev: true
- name: Fail if translations files are checked in
run: |
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
echo "Translations files are checked in, please remove the following files:"
find homeassistant/components/*/translations -type f
exit 1
fi
- name: Download Translations
run: python3 -m script.translations download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
if-no-files-found: error
build_base:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
@@ -133,7 +130,6 @@ jobs:
name: package
- name: Set up Python
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: ".python-version"
@@ -181,22 +177,35 @@ jobs:
sed -i "s|home-assistant-intents==.*||" requirements_all.txt requirements.txt
fi
- name: Download translations
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Write meta info file
shell: bash
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Build base image
- name: Build base image (deps stage only)
uses: edenhaus/builder/actions/build-image@b3ea9d4b1d98f979d671aa8442ad8373e38aea11
with:
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
push: false
load: true
target: deps
- name: Download translations
shell: bash
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
TRANSLATION_PROCESS_ID: ${{ needs.init.outputs.translation_download_process }}
run: |
pip install -r script/translations/requirements.txt
python3 -m script.translations download --process-id "$TRANSLATION_PROCESS_ID"
- name: Build base image (fully)
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
arch: ${{ matrix.arch }}
@@ -485,14 +494,13 @@ jobs:
python-version-file: ".python-version"
- name: Download translations
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
- name: Extract translations
shell: bash
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
TRANSLATION_PROCESS_ID: ${{ needs.init.outputs.translation_download_process }}
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
pip install -r script/translations/requirements.txt
python3 -m script.translations download --process-id "$TRANSLATION_PROCESS_ID"
- name: Build package
shell: bash

4
Dockerfile generated
View File

@@ -2,7 +2,7 @@
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM
FROM ${BUILD_FROM}
FROM ${BUILD_FROM} as deps
LABEL \
io.hass.type="core" \
@@ -50,6 +50,8 @@ RUN \
--no-build \
-r homeassistant/requirements_all.txt
FROM deps
## Setup Home Assistant Core
COPY . homeassistant/
RUN \

View File

@@ -17,7 +17,7 @@ DOCKERFILE_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM
FROM ${{BUILD_FROM}}
FROM ${{BUILD_FROM}} as deps
LABEL \
io.hass.type="core" \
@@ -65,6 +65,8 @@ RUN \
--no-build \
-r homeassistant/requirements_all.txt
FROM deps
## Setup Home Assistant Core
COPY . homeassistant/
RUN \

View File

@@ -3,15 +3,22 @@
from __future__ import annotations
import argparse
import io
import json
from pathlib import Path
import subprocess
import time
from typing import Any
from zipfile import ZipFile
from .const import CLI_2_DOCKER_IMAGE, CORE_PROJECT_ID, INTEGRATIONS_DIR
import lokalise
import requests
from .const import CORE_PROJECT_ID, INTEGRATIONS_DIR
from .error import ExitApp
from .util import (
flatten_translations,
get_base_arg_parser,
get_lokalise_token,
load_json_from_path,
substitute_references,
@@ -20,44 +27,95 @@ from .util import (
DOWNLOAD_DIR = Path("build/translations-download").absolute()
def run_download_docker() -> None:
"""Run the Docker image to download the translations."""
print("Running Docker to download latest translations.")
result = subprocess.run(
[
"docker",
"run",
"-v",
f"{DOWNLOAD_DIR}:/opt/dest/locale",
"--rm",
f"lokalise/lokalise-cli-2:{CLI_2_DOCKER_IMAGE}",
# Lokalise command
"lokalise2",
"--token",
get_lokalise_token(),
"--project-id",
CORE_PROJECT_ID,
"file",
"download",
CORE_PROJECT_ID,
"--original-filenames=false",
"--replace-breaks=false",
"--filter-data",
"nonfuzzy",
"--disable-references",
"--export-empty-as",
"skip",
"--format",
"json",
"--unzip-to",
"/opt/dest",
],
check=False,
)
print()
POLL_INTERVAL = 5
if result.returncode != 0:
raise ExitApp("Failed to download translations")
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
parser = get_base_arg_parser()
parser.add_argument(
"--async-start",
action="store_true",
help="Start an async download and return the process ID.",
)
parser.add_argument(
"--process-id",
type=str,
help="Process ID to wait for, then download and unzip the result.",
)
return parser.parse_args()
def get_client() -> lokalise.Client:
"""Get an authenticated Lokalise client."""
return lokalise.Client(get_lokalise_token())
def start_async_download(client: lokalise.Client) -> str:
"""Start an async download and return the process ID."""
process = client.download_files_async(
CORE_PROJECT_ID,
{
"format": "json",
"original_filenames": False,
"replace_breaks": False,
"filter_data": "nonfuzzy",
"disable_references": True,
"export_empty_as": "skip",
},
)
return process.process_id
def wait_for_process(client: lokalise.Client, process_id: str) -> str:
"""Wait for a queued process to complete and return the process download URL."""
while True:
process_info = client.queued_process(CORE_PROJECT_ID, process_id)
# Current status of the process. Can be queued, pre_processing, running,
# post_processing, cancelled, finished or failed.
status = process_info.status
additional_info = ""
if process_info.details is not None and (details := dict(process_info.details)):
if (
status == "running"
and (done := details.get("items_processed")) is not None
and (total := details.get("items_to_process")) is not None
):
additional_info = f" ({done}/{total})"
elif status == "finished":
additional_info = f" total_keys={details.get('total_number_of_keys')}"
else:
additional_info = f" details={details}"
print(f"Process {process_id}: status={status}{additional_info}")
if status == "finished":
return process_info.details["download_url"]
if status in ("cancelled", "failed"):
raise ExitApp(
f"Process {process_id} ended with status: {status}{additional_info}"
)
time.sleep(POLL_INTERVAL)
def download_and_unzip(bundle_url: str) -> None:
"""Download a zip bundle and extract it to the download directory."""
print("Downloading translations from lokalise...")
response = requests.get(bundle_url, timeout=120)
response.raise_for_status()
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
with ZipFile(io.BytesIO(response.content)) as zf:
zf.extractall(DOWNLOAD_DIR)
print(f"Extracted translations to {DOWNLOAD_DIR}")
def fetch_translations(client: lokalise.Client, process_id: str) -> None:
"""Wait for a process to finish, then download and unzip the bundle."""
download_url = wait_for_process(client, process_id)
download_and_unzip(download_url)
def save_json(filename: Path, data: list | dict) -> None:
@@ -136,14 +194,23 @@ def delete_old_translations() -> None:
fil.unlink()
def run() -> None:
def run() -> int:
"""Run the script."""
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
args = get_arguments()
client = get_client()
run_download_docker()
process_id = args.process_id
if not process_id:
# if no process ID provided, start a new async download and print the process ID
process_id = start_async_download(client)
print(f"Async download started. Process ID: {process_id}")
if args.async_start:
# If --async-start is provided, exit after starting the download
return 0
fetch_translations(client, process_id)
delete_old_translations()
save_integrations_translations()
return 0

2
script/translations/requirements.txt generated Normal file
View File

@@ -0,0 +1,2 @@
python-lokalise-api==4.0.4
requests