Add Gitea CI workflow and harden credential generation
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 2s

This commit is contained in:
ILay
2026-06-12 12:55:36 +02:00
parent 0173ceecb3
commit 69bfa34628
6 changed files with 213 additions and 24 deletions

View File

@@ -0,0 +1,74 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-push:
runs-on: orchestration-platform
defaults:
run:
shell: sh
env:
REGISTRY: gitea.dataprovider01.sandbox-cat-dat.simpl-europe.eu
IMAGE_REPO: gitea.dataprovider01.sandbox-cat-dat.simpl-europe.eu/j.r/application-template-code-location
steps:
- name: Checkout repository (shell)
run: |
REPO_DIR="repo"
REPO_CLONE_URL="https://gitea.dataprovider01.sandbox-cat-dat.simpl-europe.eu/j.r/application-template-code-location.git"
CLONE_USER="${{ secrets.REGISTRY_USERNAME }}"
CLONE_PASS="${{ secrets.REGISTRY_PASSWORD }}"
REF_NAME="${GITHUB_REF_NAME}"
if [ -z "${REF_NAME}" ]; then
REF_NAME="${GITHUB_REF#refs/heads/}"
fi
if [ -z "${CLONE_USER}" ] || [ -z "${CLONE_PASS}" ]; then
echo "Missing REGISTRY_USERNAME or REGISTRY_PASSWORD secret"
exit 1
fi
rm -rf "${REPO_DIR}"
AUTH_HEADER="$(printf '%s:%s' "${CLONE_USER}" "${CLONE_PASS}" | base64 | tr -d '\n')"
git clone --depth 1 --branch "${REF_NAME}" \
-c "http.extraHeader=Authorization: Basic ${AUTH_HEADER}" \
"${REPO_CLONE_URL}" \
"${REPO_DIR}"
if [ ! -f "${REPO_DIR}/Dockerfile" ]; then
echo "Dockerfile not found after clone"
exit 1
fi
- name: Validate registry secrets
run: |
if [ -z "${{ secrets.REGISTRY_USERNAME }}" ] || [ -z "${{ secrets.REGISTRY_PASSWORD }}" ]; then
echo "Missing REGISTRY_USERNAME or REGISTRY_PASSWORD secret"
exit 1
fi
- name: Login to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
-u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Build image
run: |
COMMIT_SHA="${GITHUB_SHA:-$GITEA_SHA}"
SHORT_SHA="$(echo "${COMMIT_SHA}" | cut -c1-12)"
cd repo
docker build \
-t "${IMAGE_REPO}:latest" \
-t "${IMAGE_REPO}:${SHORT_SHA}" \
.
- name: Push image tags
run: |
COMMIT_SHA="${GITHUB_SHA:-$GITEA_SHA}"
SHORT_SHA="$(echo "${COMMIT_SHA}" | cut -c1-12)"
docker push "${IMAGE_REPO}:latest"
docker push "${IMAGE_REPO}:${SHORT_SHA}"

View File

@@ -81,7 +81,7 @@ dagster dev
The workflow accepts the following high-level inputs:
- `repository_identifier`
- `consumer_email` with default literal `${CONSUMER_EMAIL}`
- `consumer_email` (required, must be a valid email address)
- `access_level`
Email subject and body templates are read from environment variables:

View File

@@ -1,5 +1,5 @@
repository_identifier: simpl-open/platform-automation
consumer_email: "${CONSUMER_EMAIL}"
consumer_email: "consumer@example.local"
access_level: read-only
token_name_prefix: simpl-open
token_expiration_days: 30

View File

@@ -31,7 +31,6 @@ simpl_open_gitea_workflow_config_mapping = config_mapping(
),
"consumer_email": Field(
str,
default_value="${CONSUMER_EMAIL}",
description="Email address where credentials must be delivered",
),
"access_level": Field(

View File

@@ -3,6 +3,7 @@ Generate Gitea credentials for a consumer request.
"""
import os
import re
from dagster import Field, OpExecutionContext, Out, op # pylint: disable=import-error
@@ -10,6 +11,10 @@ from simpl_open_credential_delivery.ops.retry_policy import DEFAULT_RETRY_POLICY
from simpl_open_credential_delivery.services import GiteaAPIClient
def _is_valid_email(value: str) -> bool:
return bool(re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", value))
@op(
config_schema={
"repository_identifier": Field(
@@ -54,6 +59,12 @@ def generate_gitea_credentials(context: OpExecutionContext) -> dict:
"Missing required environment variable: GITEA_ADMIN_TOKEN"
)
consumer_email = str(config["consumer_email"]).strip()
if not _is_valid_email(consumer_email) or consumer_email.startswith("${"):
raise ValueError(
"Invalid consumer_email. Provide a real email address (for example, consumer@example.local)."
)
context.log.info("Generating Gitea credentials for repository %s", config["repository_identifier"])
client = GiteaAPIClient(
base_url=base_url,
@@ -64,7 +75,7 @@ def generate_gitea_credentials(context: OpExecutionContext) -> dict:
credential_bundle = client.generate_credentials(
repository_identifier=config["repository_identifier"],
access_level=config["access_level"],
consumer_email=config["consumer_email"],
consumer_email=consumer_email,
token_name_prefix=config["token_name_prefix"],
token_expiration_days=config["token_expiration_days"],
)
@@ -77,7 +88,7 @@ def generate_gitea_credentials(context: OpExecutionContext) -> dict:
return {
"repository_identifier": config["repository_identifier"],
"consumer_email": config["consumer_email"],
"consumer_email": consumer_email,
"access_level": config["access_level"],
"username": credential_bundle.username,
"access_token": credential_bundle.access_token,

View File

@@ -6,6 +6,7 @@ from __future__ import annotations
from dataclasses import dataclass
import re
import time
from typing import Any
import requests
@@ -55,6 +56,29 @@ class GiteaAPIClient:
)
return response
def _request_as_user(
self,
method: str,
path: str,
username: str,
password: str,
expected_status: tuple[int, ...] = (200, 201, 204),
**kwargs: Any,
) -> requests.Response:
response = requests.request(
method=method,
url=f"{self.base_url}{path}",
headers={"Accept": "application/json"},
auth=(username, password),
timeout=self.timeout_seconds,
**kwargs,
)
if response.status_code not in expected_status:
raise GiteaAPIError(
f"Gitea API call to {path} failed with status {response.status_code}: {response.text}"
)
return response
def _resolve_repository_identifier(self, repository_identifier: str) -> str:
if "/" in repository_identifier:
return repository_identifier
@@ -71,6 +95,40 @@ class GiteaAPIClient:
return str(payload[key])
raise GiteaAPIError("Gitea token response did not contain a token value")
def _create_user_token(
self,
username: str,
password: str,
token_name: str,
token_expiration_days: int,
) -> requests.Response:
base_payload = {
"name": token_name,
"expires_in_days": token_expiration_days,
}
# Newer Gitea versions require at least one scope for access tokens.
try:
return self._request_as_user(
"POST",
f"/api/v1/users/{username}/tokens",
username=username,
password=password,
json={**base_payload, "scopes": ["all"]},
)
except GiteaAPIError as exc:
# Backward compatibility for older versions that do not support scopes.
if "unknown field" not in str(exc).lower() and "invalid character" not in str(exc).lower():
raise
return self._request_as_user(
"POST",
f"/api/v1/users/{username}/tokens",
username=username,
password=password,
json=base_payload,
)
def generate_credentials(
self,
repository_identifier: str,
@@ -84,38 +142,85 @@ class GiteaAPIClient:
owner, repository_name = resolved_repository.split("/", 1)
username_source = consumer_email.split("@", 1)[0] if "@" in consumer_email else consumer_email
username = self._slugify(username_source)
token_name = f"{token_name_prefix}-{self._slugify(repository_name)}-{username}"
token_name = f"{token_name_prefix}-{self._slugify(repository_name)}-{username}-{int(time.time())}"
temporary_password = f"{username}-temporary-password"
self._request(
create_user_response = self._request(
"POST",
"/api/v1/admin/users",
expected_status=(201, 409),
expected_status=(201, 409, 422),
json={
"username": username,
"email": consumer_email,
"login_name": username,
"password": f"{username}-temporary-password",
"must_change_password": True,
"password": temporary_password,
"must_change_password": False,
},
)
token_response = self._request(
"POST",
f"/api/v1/users/{username}/tokens",
# Gitea may return 422 for an existing user depending on version.
if create_user_response.status_code == 422 and "already exists" not in create_user_response.text.lower():
raise GiteaAPIError(
"Gitea user creation failed with validation error: "
f"{create_user_response.text}"
)
try:
token_response = self._create_user_token(
username=username,
password=temporary_password,
token_name=token_name,
token_expiration_days=token_expiration_days,
)
except GiteaAPIError as exc:
# Existing users created by older runs may still have "must change password" set.
# Reset the user password policy via admin API, then retry token creation once.
if "must change your password" not in str(exc).lower():
raise
self._request(
"PATCH",
f"/api/v1/admin/users/{username}",
expected_status=(200, 204),
json={
"name": token_name,
"expires_in_days": token_expiration_days,
"password": temporary_password,
"must_change_password": False,
},
)
token_response = self._create_user_token(
username=username,
password=temporary_password,
token_name=token_name,
token_expiration_days=token_expiration_days,
)
token_payload = token_response.json() if token_response.content else {}
access_token = self._extract_token(token_payload)
self._request(
"POST",
f"/api/v1/repos/{owner}/{repository_name}/collaborators/{username}",
expected_status=(201, 204),
collaborator_path = f"/api/v1/repos/{owner}/{repository_name}/collaborators/{username}"
try:
collaborator_response = self._request(
"PUT",
collaborator_path,
expected_status=(201, 204, 422),
json={"permission": access_level},
)
except GiteaAPIError as exc:
# Some deployments expose a POST variant; retry once for compatibility.
if "status 405" not in str(exc):
raise
collaborator_response = self._request(
"POST",
collaborator_path,
expected_status=(201, 204, 422),
json={"permission": access_level},
)
if collaborator_response.status_code == 422 and "already" not in collaborator_response.text.lower():
raise GiteaAPIError(
"Failed to grant repository collaborator access: "
f"{collaborator_response.text}"
)
return GiteaCredentialBundle(
username=username,