Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
!mkdocs.yml
!Makefile
!mypy.ini
!poetry.lock
!poetry.toml
!uv.lock
!pyproject.toml
!README.md

Expand Down
4 changes: 2 additions & 2 deletions .gitpod.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
image: gitpod/workspace-python-3.11:2023-10-19-14-24-02
image: gitpod/workspace-python-3.12:2025-10-03-12-40-36
tasks:
- init: make init && export COLUMNS=80 && poetry run gitopscli --help
- init: make init && export COLUMNS=80 && uv run gitopscli --help
34 changes: 22 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# =========
FROM alpine:3.18 AS base
FROM alpine:3.22 AS base

ENV PATH="/app/.venv/bin:$PATH" \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
UV_COMPILE_BYTECODE=1 \
PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PYTHONUNBUFFERED=1
Expand All @@ -13,26 +12,34 @@ RUN apk add --no-cache git python3
FROM base AS dev

WORKDIR /app
RUN apk add --no-cache gcc linux-headers musl-dev make poetry python3-dev
COPY pyproject.toml poetry.lock poetry.toml ./
RUN apk add --no-cache gcc linux-headers musl-dev make python3-dev

# =========
FROM dev AS deps

RUN poetry install --only main
RUN --mount=from=ghcr.io/astral-sh/uv:0.8,source=/uv,target=/bin/uv \
--mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-editable

# =========
FROM deps AS test

RUN poetry install --with test
COPY . .
RUN pip install .
COPY --from=ghcr.io/astral-sh/uv:0.8 /uv /uvx /bin/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --group=test
RUN make checks

# =========
FROM deps AS docs

RUN poetry install --with docs
RUN --mount=from=ghcr.io/astral-sh/uv:0.8,source=/uv,target=/bin/uv \
--mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --group=docs
COPY docs ./docs
COPY CONTRIBUTING.md mkdocs.yml ./
RUN mkdocs build
Expand All @@ -46,11 +53,14 @@ COPY --from=docs /app/site /site
FROM deps AS install

COPY . .
RUN poetry build
RUN pip install dist/gitopscli-*.whl
RUN --mount=from=ghcr.io/astral-sh/uv:0.8,source=/uv,target=/bin/uv \
--mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-editable

# =========
FROM base as final
FROM base AS final

COPY --from=install /app/.venv /app/.venv
ENTRYPOINT ["gitopscli"]
26 changes: 13 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
init:
poetry install
pre-commit install
uv sync --locked --all-groups
uv run pre-commit install

format:
poetry run ruff format gitopscli tests
poetry run ruff gitopscli tests --fix
uv run ruff format gitopscli tests
uv run ruff check gitopscli tests --fix

format-check:
poetry run ruff format gitopscli tests --check
uv run ruff format gitopscli tests --check

lint:
poetry run ruff gitopscli tests
uv run ruff check gitopscli tests

mypy:
poetry run mypy --install-types --non-interactive .
uv run mypy --install-types --non-interactive .


test:
poetry run pytest -vv -s --typeguard-packages=gitopscli
uv run pytest -vv -s --typeguard-packages=gitopscli

coverage:
coverage run -m pytest
coverage html
coverage report
uv run coverage run -m pytest
uv run coverage html
uv run coverage report

checks: format-check lint mypy test

image:
DOCKER_BUILDKIT=1 docker build --progress=plain -t gitopscli:latest .

docs:
mkdocs serve
uv run mkdocs serve

update:
poetry lock
uv lock -U

.PHONY: init format format-check lint mypy test coverage checks image docs update
8 changes: 8 additions & 0 deletions dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2

updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"

57 changes: 26 additions & 31 deletions docs/setup.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Setup

Currently there are two different ways to setup and use the GitOps CLI.
The GitOps CLI can be used in multiple ways depending on your needs: via Docker, using Astral’s uvx, or by installing it from source for local development.

## Docker
## Option 1: Use via Docker (Recommended)

The official GitOps CLI Docker image comes with all dependencies pre-installed and ready-to-use. Pull it with:
```bash
Expand All @@ -13,45 +13,40 @@ Start the CLI and the print the help page with:
docker run --rm -it baloise/gitopscli --help
```

## From Source With Virtualenv
## Option 2: Run as Tool via uvx

Use this for developement and if you want to prevent dependency clashes with other programs in a user installation.
Astral's [uvx](https://docs.astral.sh/uv/guides/tools/) allows you to run the CLI without installing it.

Clone the repository and install the GitOps CLI on your machine:
```bash
git clone https://github.com/baloise/gitopscli.git
cd gitopscli/
poetry install
```
You can now use it from the command line:
```bash
poetry run gitopscli --help
```
If you don't need the CLI anymore, you can uninstall it with
```bash
poetry env remove --all
uvx https://github.com/baloise/gitopscli.git --help
```

Note: if your poetry is not up to date to handle the files you can use a locally updated version.
Execute the following command in your cloned gitopscli directory to use an updated poetry without changing your system installation:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip3 install poetry # installs it in the venv
```
## Option 3: From Source For Local Development

## From Source Into User Installation
Use this method if you're contributing to the project or want to develop it on your own.

Clone the repository and install the GitOps CLI on your machine:
### Prerequisites

Install [uv](https://docs.astral.sh/uv/getting-started/installation/) if it's not yet available.

### Steps

1. Clone the repository:
```bash
git clone https://github.com/baloise/gitopscli.git
pip3 install gitopscli/
cd gitopscli/
```
You can now use it from the command line:
2. Install dependencies:
```bash
gitopscli --help
uv sync --locked --all-groups
```
If you don't need the CLI anymore, you can uninstall it with

3. Run the CLI:
```bash
uv run gitopscli --help
```
### Updating uv
If you're using an older version of uv, you can update it with:
```bash
pip3 uninstall gitopscli
```
uv self upgrade
```
3 changes: 1 addition & 2 deletions gitopscli/commands/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@

class Command(metaclass=ABCMeta):
@abstractmethod
def execute(self) -> None:
...
def execute(self) -> None: ...
7 changes: 4 additions & 3 deletions gitopscli/commands/sync_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ def execute(self) -> None:
def _sync_apps_command(args: SyncAppsCommand.Args) -> None:
team_config_git_repo_api = GitRepoApiFactory.create(args, args.organisation, args.repository_name)
root_config_git_repo_api = GitRepoApiFactory.create(args, args.root_organisation, args.root_repository_name)
with GitRepo(team_config_git_repo_api) as team_config_git_repo, GitRepo(
root_config_git_repo_api
) as root_config_git_repo:
with (
GitRepo(team_config_git_repo_api) as team_config_git_repo,
GitRepo(root_config_git_repo_api) as root_config_git_repo,
):
__sync_apps(
team_config_git_repo,
root_config_git_repo,
Expand Down
14 changes: 7 additions & 7 deletions gitopscli/git_api/azure_devops_git_repo_api_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def create_pull_request(
f"Repository '{self.__project_name}/{self.__repository_name}' does not exist"
) from ex
raise GitOpsException(f"Error creating pull request: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex

def merge_pull_request(
Expand Down Expand Up @@ -146,7 +146,7 @@ def merge_pull_request(
if "404" in error_msg:
raise GitOpsException(f"Pull request with ID '{pr_id}' does not exist") from ex
raise GitOpsException(f"Error merging pull request: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex

def add_pull_request_comment(self, pr_id: int, text: str, parent_id: int | None = None) -> None: # noqa: ARG002
Expand Down Expand Up @@ -174,7 +174,7 @@ def add_pull_request_comment(self, pr_id: int, text: str, parent_id: int | None
if "404" in error_msg:
raise GitOpsException(f"Pull request with ID '{pr_id}' does not exist") from ex
raise GitOpsException(f"Error adding comment: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex

def delete_branch(self, branch: str) -> None:
Expand Down Expand Up @@ -215,7 +215,7 @@ def _raise_branch_not_found() -> None:
if "404" in error_msg:
raise GitOpsException(f"Branch '{branch}' does not exist") from ex
raise GitOpsException(f"Error deleting branch: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex

def get_branch_head_hash(self, branch: str) -> str:
Expand Down Expand Up @@ -243,7 +243,7 @@ def _raise_branch_not_found() -> None:
if "404" in error_msg:
raise GitOpsException(f"Branch '{branch}' does not exist") from ex
raise GitOpsException(f"Error getting branch hash: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex

def get_pull_request_branch(self, pr_id: int) -> str:
Expand All @@ -266,7 +266,7 @@ def get_pull_request_branch(self, pr_id: int) -> str:
if "404" in error_msg:
raise GitOpsException(f"Pull request with ID '{pr_id}' does not exist") from ex
raise GitOpsException(f"Error getting pull request: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex

def add_pull_request_label(self, pr_id: int, pr_labels: list[str]) -> None:
Expand Down Expand Up @@ -296,5 +296,5 @@ def __get_default_branch(self) -> str:
f"Repository '{self.__project_name}/{self.__repository_name}' does not exist"
) from ex
raise GitOpsException(f"Error getting repository info: {error_msg}") from ex
except Exception as ex: # noqa: BLE001
except Exception as ex:
raise GitOpsException(f"Error connecting to '{self.__base_url}'") from ex
33 changes: 11 additions & 22 deletions gitopscli/git_api/git_repo_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,21 @@ class PullRequestIdAndUrl(NamedTuple):
url: str

@abstractmethod
def get_username(self) -> str | None:
...
def get_username(self) -> str | None: ...

@abstractmethod
def get_password(self) -> str | None:
...
def get_password(self) -> str | None: ...

@abstractmethod
def get_clone_url(self) -> str:
...
def get_clone_url(self) -> str: ...

@abstractmethod
def create_pull_request_to_default_branch(
self,
from_branch: str,
title: str,
description: str,
) -> "PullRequestIdAndUrl":
...
) -> "PullRequestIdAndUrl": ...

@abstractmethod
def create_pull_request(
Expand All @@ -35,34 +31,27 @@ def create_pull_request(
to_branch: str,
title: str,
description: str,
) -> "PullRequestIdAndUrl":
...
) -> "PullRequestIdAndUrl": ...

@abstractmethod
def merge_pull_request(
self,
pr_id: int,
merge_method: Literal["squash", "rebase", "merge"] = "merge",
merge_parameters: dict[str, Any] | None = None,
) -> None:
...
) -> None: ...

@abstractmethod
def add_pull_request_comment(self, pr_id: int, text: str, parent_id: int | None = None) -> None:
...
def add_pull_request_comment(self, pr_id: int, text: str, parent_id: int | None = None) -> None: ...

@abstractmethod
def delete_branch(self, branch: str) -> None:
...
def delete_branch(self, branch: str) -> None: ...

@abstractmethod
def get_branch_head_hash(self, branch: str) -> str:
...
def get_branch_head_hash(self, branch: str) -> str: ...

@abstractmethod
def get_pull_request_branch(self, pr_id: int) -> str:
...
def get_pull_request_branch(self, pr_id: int) -> str: ...

@abstractmethod
def add_pull_request_label(self, pr_id: int, pr_labels: list[str]) -> None:
...
def add_pull_request_label(self, pr_id: int, pr_labels: list[str]) -> None: ...
Loading