diff --git a/TODO.md b/TODO.md index a3b6fe0..50a3468 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,45 @@ # TODO: -- add CI -- add flake +- publish to gitea repo: [container](https://docs.gitea.io/en-us/usage/packages/container/) [badge](https://docs.gitea.io/en-us/usage/packages/generic/) - use bool value for `generate_gitea_project` when the feature is available + +### Package + +``` +curl -i --upload-file README.md --user "histausse:`secret-tool lookup Title 'Gitea Token'`" https://git.pains-perdus.fr/api/packages/histausse/generic/test_flake_poetry2nix/latest/README.md +curl https://git.pains-perdus.fr/api/packages/histausse/generic/test_flake_poetry2nix/latest/README.md +curl -i -X DELETE --user "histausse:`secret-tool lookup Title 'Gitea Token'`" https://git.pains-perdus.fr/api/packages/histausse/generic/test_flake_poetry2nix/latest/pyproject.toml +``` + +``` +podman login -u histausse -p `secret-tool lookup Title 'Gitea Token'` git.pains-perdus.fr +podman push test_flake_poetry2nix:latest git.pains-perdus.fr/histausse/test_flake_poetry2nix:latest + +``` + +Put secret in CI: + +``` + connection.request( + "POST", + f"/api/repos/{repo}/secrets", + json.dumps( + { + "name": "test_token", + "value": "loren ispum", + "image": ["test"], + "event": ["push", "tag"], + } + ), + { + "Authorization": f"Bearer {API_KEY}", + "content-type": "application/json", + }, + ) +``` + + +Gen badge: +``` +https://github.com/smarie/python-genbadge/issues +``` diff --git a/cookiecutter.json b/cookiecutter.json index 34664c8..7441fc8 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -5,12 +5,12 @@ "email": "", "git_user": "", "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}", - "gitea_url": "", + "gitea_url": "https://git.pains-perdus.fr", "project_url": "{{ cookiecutter.gitea_url }}/{{ cookiecutter.git_user }}/{{ cookiecutter.project_slug }}", "generate_gitea_project": [ true, false ], "configure_ci": [ true, false ], "woodpecker_gitea_user": "ci", - "woodpecker_server": "ci.pains-perdus.fr", + "woodpecker_server": "https://ci.pains-perdus.fr", "git_origin": "{{ cookiecutter.gitea_url.replace('https://', 'gitea@').replace('http://', 'gitea') }}:{{ cookiecutter.git_user }}/{{ cookiecutter.project_slug }}.git", "version": "0.1.0", "open_source_license": ["AGPL-3.0-only", "GPL-3.0-only", "MIT", "BSD-3-Clause", "ISC", "Apache-2.0", "Proprietary"], diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 745f65c..9aae08c 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,11 +1,28 @@ import subprocess +import http.client +import json +import os +from base64 import b64encode +REMOVE_PATHS = [ + {% if cookiecutter.open_source_license == "Proprietary" %} "LICENSE", {% endif %} + {% if cookiecutter.configure_ci == "False" %} ".woodpecker.yml", {% endif %} +] + +for path in REMOVE_PATHS: + if path and os.path.exists(path): + if os.path.isdir(path): + os.rmdir(path) + else: + os.unlink(path) + +subprocess.call(["poetry", "lock"]) subprocess.call(["git", "init"]) subprocess.call(["git", "checkout", "-b", "main"]) subprocess.call(["git", "add", "*"]) -subprocess.call(["git", "commit", "-m", "Initial commit"]) subprocess.call(["git", "config", "user.name", "{{ cookiecutter.git_user }}"]) subprocess.call(["git", "config", "user.email", "{{ cookiecutter.email }}"]) +subprocess.call(["git", "commit", "-m", "Initial commit"]) # subprocess.call(["python", "-m", "venv", "venv"]) @@ -28,6 +45,69 @@ def get_secret(secret_name: str): "please enter you gitea api token:" ) +def get_new_gitea_token( + connection: http.client.HTTPSConnection, new_token_name: str, user: str, token: str +) -> str: + auth = b64encode(f"{user}:{token}".encode("utf-8")).decode("ascii") + connection.request( + "POST", + f"/api/v1/users/{user}/tokens", + json.dumps( + { + "name": new_token_name, + } + ), + { + "Authorization": f"Basic {auth}", + "content-type": "application/json", + }, + ) + response = connection.getresponse() + status = response.status + if status != 201: + print( + f"\033[38;2;255;0;0mInvalid response from gitea while creating a new token: {status} ({response.reason})\033[0m" + ) + print(response.read()) + exit(1) + data = json.load(response) + return data["sha1"] + +def push_woodpecker_secret( + connection: http.client.HTTPSConnection, + repo: str, + secret_name: str, + secret: str, + woodpecker_token: str, + images: list[str] | None = None +): + if images is None: images = [] + connection.request( + "POST", + f"/api/repos/{repo}/secrets", + json.dumps( + { + "name": secret_name, + "value": secret, + "image": images, + "event": ["push", "tag"], + } + ), + { + "Authorization": f"Bearer {woodpecker_token}", + "content-type": "application/json", + }, + ) + response = connection.getresponse() + status = response.status + if status != 200: + print( + f"\033[38;2;255;0;0mInvalid response from woodpecker when sending a new secret: {status} ({response.reason})\033[0m" + ) + print(response.read()) + exit(1) + response.read() + if {{cookiecutter.generate_gitea_project}}: try: @@ -35,9 +115,9 @@ if {{cookiecutter.generate_gitea_project}}: except ModuleNotFoundError: print("module `giteapy` is not availabled, repository not created") exit() - API_KEY = get_secret("Gitea Token") + GITEA_API_KEY = get_secret("Gitea Token") configuration = giteapy.Configuration() - configuration.api_key["access_token"] = API_KEY + configuration.api_key["access_token"] = GITEA_API_KEY client = giteapy.ApiClient(configuration) client.configuration.host = "{{ cookiecutter.gitea_url }}/api/v1" api_instance = giteapy.UserApi(client) @@ -48,10 +128,6 @@ if {{cookiecutter.generate_gitea_project}}: if {{cookiecutter.generate_gitea_project}} and {{cookiecutter.configure_ci}}: - import http.client - - # import json - api_instance = giteapy.RepositoryApi(client) options = giteapy.AddCollaboratorOption("read") api_instance.repo_add_collaborator( @@ -60,16 +136,18 @@ if {{cookiecutter.generate_gitea_project}} and {{cookiecutter.configure_ci}}: "{{ cookiecutter.woodpecker_gitea_user }}", body=options, ) - - API_KEY = get_secret("Woodpecker Token") + connection_gitea = http.client.HTTPSConnection("git.pains-perdus.fr") + GITEA_API_KEY = get_new_gitea_token(connection_gitea, "CI {{ cookiecutter.git_user }}/{{ cookiecutter.project_slug }}", "{{ cookiecutter.git_user }}", GITEA_API_KEY) + WOODPECKER_API_KEY = get_secret("Woodpecker Token") WOODPECKER_SERVER = "{{ cookiecutter.woodpecker_server }}".removeprefix("https://") origin = "{{ cookiecutter.git_origin }}" repo = "/".join(origin.removesuffix(".git").split(":")[-1].split("/")[-2:]) + connection = http.client.HTTPSConnection(WOODPECKER_SERVER) connection.request( "GET", "/api/user/repos?all=true&flush=true", - headers={"Authorization": f"Bearer {API_KEY}"}, + headers={"Authorization": f"Bearer {WOODPECKER_API_KEY}"}, ) response = connection.getresponse() status = response.status @@ -84,7 +162,7 @@ if {{cookiecutter.generate_gitea_project}} and {{cookiecutter.configure_ci}}: "POST", f"/api/repos/{repo}", headers={ - "Authorization": f"Bearer {API_KEY}", + "Authorization": f"Bearer {WOODPECKER_API_KEY}", "referer": f"https://{WOODPECKER_SERVER}/repo/add", }, ) @@ -94,7 +172,16 @@ if {{cookiecutter.generate_gitea_project}} and {{cookiecutter.configure_ci}}: print( f"\033[38;2;255;0;0mInvalid response from woodpecker while linking repo: {status} ({response.reason})\033[0m" ) + print(response.read()) exit(1) + response.read() + push_woodpecker_secret( + connection, + repo, + "gitea_token", + GITEA_API_KEY, + WOODPECKER_API_KEY + ) if {{cookiecutter.generate_gitea_project}}: subprocess.call(["git", "remote", "add", "origin", "{{ cookiecutter.git_origin }}"]) diff --git a/{{ cookiecutter.project_slug }}/.woodpecker.yml b/{{ cookiecutter.project_slug }}/.woodpecker.yml index 300eafc..5cafcce 100644 --- a/{{ cookiecutter.project_slug }}/.woodpecker.yml +++ b/{{ cookiecutter.project_slug }}/.woodpecker.yml @@ -1,5 +1,39 @@ pipeline: - a-test-step: - image: debian + test: + group: test + image: python:${PYTHON_VERSION} + pull: true + environment: + - POETRY_VIRTUALENVS_IN_PROJECT=true commands: - - echo "Testing.." + - pip install poetry + - poetry install + - poetry run pytest + + nix: + group: test + image: nixos/nix:latest + pull: true + commands: + - nix build --experimental-features 'nix-command flakes' + - nix build -o image_link --experimental-features 'nix-command flakes' .#docker + - cp image_link image + when: + matrix: + PYTHON_VERSION: {{ cookiecutter.python_min_version}} # Still not sure about how to make flake for different python version + + push_image: + image: quay.io/podman/stable:latest + pull: true + commands: + - podman login -u {{ cookiecutter.git_user }} -p $GITEA_TOKEN {{ cookiecutter.gitea_url.removeprefix('https://') }} + - podman load < image + - podman push {{ cookiecutter.project_slug }}:latest {{ cookiecutter.gitea_url.removeprefix('https://') }}/{{ cookiecutter.git_user }}/{{ cookiecutter.project_slug }}:latest + secrets: [ gitea_token ] + when: + matrix: + PYTHON_VERSION: {{ cookiecutter.python_min_version}} # Still not sure about how to make flake for different python version + +matrix: + PYTHON_VERSION: + - {{ cookiecutter.python_min_version}} diff --git a/{{ cookiecutter.project_slug }}/README.md b/{{ cookiecutter.project_slug }}/README.md index 7a4eaf5..b1419ec 100644 --- a/{{ cookiecutter.project_slug }}/README.md +++ b/{{ cookiecutter.project_slug }}/README.md @@ -1,16 +1,50 @@ {% set is_open_source = cookiecutter.open_source_license != 'Proprietary' -%} +{% set repo = "/".join(cookiecutter.git_origin.removesuffix(".git").split(":")[-1].split("/")[-2:]) %} # {{ cookiecutter.project_name }} +{% if cookiecutter.configure_ci == "True" %}[![CI status badge]({{ cookiecutter.woodpecker_server }}/api/badges/{{ repo }}/status.svg)](https://ci.pains-perdus.fr/{{ repo }}){% endif %} + {{ cookiecutter.project_short_description }} ## Install + +### With pip + This project can be installed using pip: ``` pip install git+{{ cookiecutter.project_url }}.git ``` +### With Nix + +There is a `flake.nix`, so you can clone the repo and use `nix shell` if you want. + +### Docker/Podman + +{% if cookiecutter.configure_ci == "True" %} +You can run this projet with docker or podman: + +``` +podman run --rm -it {{ cookiecutter.gitea_url.removeprefix('https://') }}/{{ cookiecutter.git_user }}/{{ cookiecutter.project_slug }}:latest {{ cookiecutter.project_slug }} +``` +{% else %} +You can build a container image using nix. To build the image, in the repo, run: + +``` +nix build -o {{ cookiecutter.project_slug }}.img .#docker +``` + +You can then load the image with: + +``` +podman load < {{ cookiecutter.project_slug }}.img +``` +{% endif %} + +Notice the image is build with nix and is very minimalist. + {% if is_open_source %} ## License diff --git a/{{ cookiecutter.project_slug }}/flake.nix b/{{ cookiecutter.project_slug }}/flake.nix new file mode 100644 index 0000000..fbaaabc --- /dev/null +++ b/{{ cookiecutter.project_slug }}/flake.nix @@ -0,0 +1,36 @@ +{ + description = "{{ cookiecutter.project_short_description }}"; + + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs.poetry2nix = { + url = "github:nix-community/poetry2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, flake-utils, poetry2nix }: + flake-utils.lib.eachDefaultSystem (system: + let + inherit (poetry2nix.legacyPackages.${system}) mkPoetryApplication; + pkgs = nixpkgs.legacyPackages.${system}; + in + { + packages = { + {{ cookiecutter.project_slug }} = mkPoetryApplication { projectDir = self; }; + docker = pkgs.dockerTools.buildImage { + name = "{{ cookiecutter.project_slug }}"; + tag = "latest"; + copyToRoot = pkgs.buildEnv { + name = "{{ cookiecutter.project_slug }}_root_img"; + paths = [ self.packages.${system}.{{ cookiecutter.project_slug }} ]; + pathsToLink = [ "/bin" ]; + }; + }; + default = self.packages.${system}.{{ cookiecutter.project_slug }}; + }; + + devShells.default = pkgs.mkShell { + packages = [ poetry2nix.packages.${system}.poetry ]; + }; + }); +}