ci: migrate workflows and releases to Gitea
Hassfest / hassfest (push) Failing after 48s
Tests / test (push) Successful in 3m15s
Validate / validate-repository (push) Successful in 3s

This commit is contained in:
2026-07-23 22:21:41 -05:00
parent 552fbf17c8
commit a1da517b4e
10 changed files with 249 additions and 53 deletions
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
archive_path="${1:-relaytv.zip}"
notes_path="${2:-RELEASE_NOTES.md}"
for variable_name in GITEA_SERVER_URL GITEA_REPOSITORY GITEA_REF_NAME GITEA_TOKEN; do
if [[ -z "${!variable_name:-}" ]]; then
echo "Required environment variable is not set: ${variable_name}" >&2
exit 1
fi
done
if [[ ! -f "$archive_path" ]]; then
echo "Release archive does not exist: $archive_path" >&2
exit 1
fi
if [[ ! -f "$notes_path" ]]; then
echo "Release notes do not exist: $notes_path" >&2
exit 1
fi
api_base="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}/releases"
asset_name="${archive_path##*/}"
authorization_header="Authorization: token ${GITEA_TOKEN}"
release_payload="$({
RELEASE_NOTES_PATH="$notes_path" python3 - <<'PY'
import json
import os
from pathlib import Path
tag = os.environ["GITEA_REF_NAME"]
notes = Path(os.environ["RELEASE_NOTES_PATH"]).read_text(encoding="utf-8")
print(json.dumps({
"body": notes,
"draft": False,
"name": tag,
"prerelease": False,
"tag_name": tag,
}))
PY
})"
lookup_response="$(
curl --silent --show-error \
--header "$authorization_header" \
--output - \
--write-out $'\n%{http_code}' \
"$api_base/tags/$GITEA_REF_NAME"
)"
lookup_status="${lookup_response##*$'\n'}"
lookup_body="${lookup_response%$'\n'*}"
case "$lookup_status" in
200)
release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' <<<"$lookup_body")"
curl --fail-with-body --silent --show-error \
--request PATCH \
--header "$authorization_header" \
--header "Content-Type: application/json" \
--data "$release_payload" \
"$api_base/$release_id" >/dev/null
;;
404)
release_response="$(
curl --fail-with-body --silent --show-error \
--request POST \
--header "$authorization_header" \
--header "Content-Type: application/json" \
--data "$release_payload" \
"$api_base"
)"
release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' <<<"$release_response")"
;;
*)
echo "Could not look up Gitea release for $GITEA_REF_NAME (HTTP $lookup_status)." >&2
echo "$lookup_body" >&2
exit 1
;;
esac
asset_id="$({
ASSET_NAME="$asset_name" python3 -c '
import json
import os
import sys
assets = json.load(sys.stdin)
match = next((asset for asset in assets if asset["name"] == os.environ["ASSET_NAME"]), None)
print(match["id"] if match else "")
' < <(
curl --fail-with-body --silent --show-error \
--header "$authorization_header" \
"$api_base/$release_id/assets"
)
})"
if [[ -n "$asset_id" ]]; then
curl --fail-with-body --silent --show-error \
--request DELETE \
--header "$authorization_header" \
"$api_base/$release_id/assets/$asset_id" >/dev/null
fi
curl --fail-with-body --silent --show-error \
--request POST \
--header "$authorization_header" \
--form "attachment=@${archive_path};filename=${asset_name}" \
"$api_base/$release_id/assets?name=$asset_name" >/dev/null
echo "Published $asset_name to Gitea release $GITEA_REF_NAME."
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Validate the local repository structure used to package the integration."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
REPOSITORY_ROOT = Path(__file__).resolve().parent.parent
CUSTOM_COMPONENTS = REPOSITORY_ROOT / "custom_components"
def load_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"Could not read valid JSON from {path.relative_to(REPOSITORY_ROOT)}: {error}") from error
if not isinstance(value, dict):
raise ValueError(f"Expected a JSON object in {path.relative_to(REPOSITORY_ROOT)}")
return value
def main() -> None:
integration_directories = sorted(
path for path in CUSTOM_COMPONENTS.iterdir() if path.is_dir() and path.name != "__pycache__"
)
if len(integration_directories) != 1:
names = ", ".join(path.name for path in integration_directories) or "none"
raise ValueError(f"Expected exactly one integration under custom_components; found: {names}")
integration_directory = integration_directories[0]
manifest = load_json(integration_directory / "manifest.json")
required_manifest_fields = {
"codeowners",
"documentation",
"domain",
"issue_tracker",
"name",
"version",
}
missing_fields = sorted(required_manifest_fields - manifest.keys())
if missing_fields:
raise ValueError(f"manifest.json is missing required fields: {', '.join(missing_fields)}")
if manifest["domain"] != integration_directory.name:
raise ValueError("manifest.json domain must match its custom_components directory")
if not isinstance(manifest["codeowners"], list) or not manifest["codeowners"]:
raise ValueError("manifest.json codeowners must be a non-empty list")
if not (integration_directory / "brand" / "icon.png").is_file():
raise ValueError("The integration must include brand/icon.png")
hacs_manifest = load_json(REPOSITORY_ROOT / "hacs.json")
if not isinstance(hacs_manifest.get("name"), str) or not hacs_manifest["name"].strip():
raise ValueError("hacs.json must include a non-empty name")
if hacs_manifest.get("zip_release") is True:
filename = hacs_manifest.get("filename")
if not isinstance(filename, str) or not filename.endswith(".zip"):
raise ValueError("hacs.json zip_release requires a .zip filename")
if not (REPOSITORY_ROOT / "README.md").is_file():
raise ValueError("The repository must include README.md")
print(f"Repository structure is valid for integration {manifest['domain']} {manifest['version']}.")
if __name__ == "__main__":
main()