Skip to content

Commit e14cf43

Browse files
committed
Split 'build and publish' to 'build' and 'publish_and_verify'. Add (package) repository credentials provider. Extend protocol between WM and ER to request and return response in multiple formats at once.
1 parent 194896d commit e14cf43

37 files changed

Lines changed: 980 additions & 402 deletions

.github/workflows/ci-cd.yml

Lines changed: 51 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
# TODO: check formatting
2+
# TODO: lock files on all platforms
3+
# TODO: test with all supported python versions
14
name: CI
25

36
on:
47
merge_group:
58
push:
69
pull_request:
10+
workflow_dispatch:
11+
inputs:
12+
publish_testpypi:
13+
description: 'Publish to TestPyPI'
14+
required: true
15+
type: boolean
16+
default: false
717

818
defaults:
919
run:
@@ -58,102 +68,61 @@ jobs:
5868
python -m finecode prepare-envs
5969
shell: bash
6070

61-
# TODO: install all other supported python versions. Version can be extracted from finecode
71+
- name: Lint
72+
run: |
73+
source .venvs/dev_workspace/bin/activate
74+
python -m finecode run lint
75+
shell: bash
76+
77+
- name: Build artifacts
78+
id: build
79+
if: runner.os == 'Linux'
80+
run: |
81+
source .venvs/dev_workspace/bin/activate
82+
python -m finecode run build_artifact
83+
shell: bash
6284

63-
# - name: Lint
85+
# - name: Run unit tests
86+
# if: ${{ !cancelled() }}
6487
# run: |
65-
# poetry run python -m finecode run lint
88+
# source .venvs/dev_workspace/bin/activate
89+
# python -m finecode run test
6690
# shell: bash
6791

68-
- name: Build all packages
69-
if: runner.os == 'Linux'
92+
- name: Publish to TestPyPI and verify
93+
if: runner.os == 'Linux' && github.event_name == 'workflow_dispatch' && inputs.publish_testpypi
94+
env:
95+
FINECODE_CONFIG_PUBLISH_AND_VERIFY_ARTIFACT__INIT_REPOSITORY_PROVIDER__REPOSITORIES: '[{"name": "testpypi", "url": "https://test.pypi.org/"}]'
96+
FINECODE_CONFIG_PUBLISH_AND_VERIFY_ARTIFACT__INIT_REPOSITORY_PROVIDER__CREDENTIALS_BY_REPOSITORY: '{"testpypi": {"username": "${{ secrets.TESTPYPI_USERNAME }}", "password": "${{ secrets.TESTPYPI_PASSWORD }}"}}'
7097
run: |
7198
source .venvs/dev_workspace/bin/activate
72-
python -m finecode run build
99+
python -m finecode run \
100+
--map-payload-fields="src-artifact-def-path,dist-artifact-paths" \
101+
publish_and_verify_artifact \
102+
--src-artifact-def-path="build_artifact.src_artifact_def_path" \
103+
--dist-artifact-paths="build_artifact.build_output_paths"
73104
shell: bash
74105

75-
- name: Collect all distribution packages
76-
if: runner.os == 'Linux'
106+
- name: Publish to PyPI and verify
107+
if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/')
108+
env:
109+
FINECODE_CONFIG_PUBLISH_AND_VERIFY_ARTIFACT__INIT_REPOSITORY_PROVIDER__REPOSITORIES: '[{"name": "pypi", "url": "https://pypi.org/"}]'
110+
FINECODE_CONFIG_PUBLISH_AND_VERIFY_ARTIFACT__INIT_REPOSITORY_PROVIDER__CREDENTIALS_BY_REPOSITORY: '{"pypi": {"username": "${{ secrets.PYPI_USERNAME }}", "password": "${{ secrets.PYPI_PASSWORD }}"}}'
77111
run: |
78-
# TODO: finecode action to copy only updated packages in dist
79-
mkdir -p dist
80-
cp finecode_extension_api/dist/* dist/
81-
cp extensions/fine_python_ast/dist/* dist/
82-
cp extensions/fine_python_black/dist/* dist/
83-
cp extensions/fine_python_flake8/dist/* dist/
84-
cp extensions/fine_python_isort/dist/* dist/
85-
cp extensions/fine_python_module_exports/dist/* dist/
86-
cp extensions/fine_python_mypy/dist/* dist/
87-
cp presets/fine_python_format/dist/* dist/
88-
cp presets/fine_python_lint/dist/* dist/
89-
cp presets/fine_python_recommended/dist/* dist/
112+
# TODO: make sure git tag exists (for manual trigger)
113+
source .venvs/dev_workspace/bin/activate
114+
python -m finecode run \
115+
--map-payload-fields="src-artifact-def-path,dist-artifact-paths" \
116+
publish_and_verify_artifact \
117+
--src-artifact-def-path="build_artifact.src_artifact_def_path" \
118+
--dist-artifact-paths="build_artifact.build_output_paths"
119+
90120
shell: bash
91121

122+
# TODO: try to replace by finecode action
92123
- name: Store the distribution packages
93124
uses: actions/upload-artifact@v4
94125
if: runner.os == 'Linux'
95126
with:
96127
name: python-package-distributions
97128
path: dist/
98-
99-
# - name: Run unit tests
100-
# if: ${{ !cancelled() }}
101-
# run: |
102-
# poetry run python -m pytest tests/
103-
# shell: bash
104-
105-
publish-to-pypi:
106-
name: >-
107-
Publish Python 🐍 distribution 📦 to PyPI
108-
if: startsWith(github.ref, 'refs/tags/')
109-
needs:
110-
- build
111-
runs-on: ubuntu-24.04
112-
environment:
113-
name: pypi
114-
url: https://pypi.org/p/finecode
115-
permissions:
116-
id-token: write # IMPORTANT: mandatory for trusted publishing
117-
118-
steps:
119-
- name: Download all the dists
120-
uses: actions/download-artifact@v4
121-
with:
122-
name: python-package-distributions
123-
path: dist/
124-
- name: Publish distribution 📦 to PyPI
125-
uses: pypa/gh-action-pypi-publish@release/v1
126-
with:
127-
# temporary skip existing packages, because not always all packages at once
128-
# are updated.
129-
# TODO: implement publishing only of changed in finecode
130-
skip-existing: true
131-
132-
publish-to-testpypi:
133-
name: Publish Python 🐍 distribution 📦 to TestPyPI
134-
needs:
135-
- build
136-
runs-on: ubuntu-24.04
137-
138-
environment:
139-
name: testpypi
140-
url: https://test.pypi.org/p/finecode
141-
142-
permissions:
143-
id-token: write # IMPORTANT: mandatory for trusted publishing
144-
145-
steps:
146-
- name: Download all the dists
147-
uses: actions/download-artifact@v4
148-
with:
149-
name: python-package-distributions
150-
path: dist/
151-
- name: Publish distribution 📦 to TestPyPI
152-
uses: pypa/gh-action-pypi-publish@release/v1
153-
with:
154-
repository-url: https://test.pypi.org/legacy/
155-
verbose: true
156-
# temporary skip existing packages, because not always all packages at once
157-
# are updated.
158-
# TODO: implement publishing only of changed in finecode
159-
skip-existing: true

extensions/fine_python_package_info/fine_python_package_info/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from .build_artifact_py_handler import BuildArtifactPyHandler
2+
from .get_dist_artifact_version_py_handler import \
3+
GetDistArtifactVersionPyHandler
24
from .get_src_artifact_registries_py_handler import \
35
GetSrcArtifactRegistriesPyHandler
46
from .get_src_artifact_version_py_handler import GetSrcArtifactVersionPyHandler
@@ -14,6 +16,7 @@
1416

1517
__all__ = [
1618
"BuildArtifactPyHandler",
19+
"GetDistArtifactVersionPyHandler",
1720
"GroupSrcArtifactFilesByLangPythonHandler",
1821
"ListSrcArtifactFilesByLangPythonHandler",
1922
"PyPackageLayoutInfoProvider",

extensions/fine_python_package_info/fine_python_package_info/build_artifact_py_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,6 @@ async def run(
9191
self.logger.info(f"Build completed. Output: {build_output_paths}")
9292

9393
return build_artifact_action.BuildArtifactRunResult(
94-
build_output_paths=build_output_paths
94+
src_artifact_def_path=src_artifact_def_path,
95+
build_output_paths=build_output_paths,
9596
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import dataclasses
2+
3+
from finecode_extension_api import code_action
4+
from finecode_extension_api.actions import \
5+
get_dist_artifact_version as get_dist_artifact_version_action
6+
from finecode_extension_api.interfaces import ilogger
7+
8+
9+
@dataclasses.dataclass
10+
class GetDistArtifactVersionPyHandlerConfig(code_action.ActionHandlerConfig): ...
11+
12+
13+
class GetDistArtifactVersionPyHandler(
14+
code_action.ActionHandler[
15+
get_dist_artifact_version_action.GetDistArtifactVersionAction,
16+
GetDistArtifactVersionPyHandlerConfig,
17+
]
18+
):
19+
def __init__(
20+
self,
21+
config: GetDistArtifactVersionPyHandlerConfig,
22+
logger: ilogger.ILogger,
23+
) -> None:
24+
self.config = config
25+
self.logger = logger
26+
27+
async def run(
28+
self,
29+
payload: get_dist_artifact_version_action.GetDistArtifactVersionRunPayload,
30+
run_context: get_dist_artifact_version_action.GetDistArtifactVersionRunContext,
31+
) -> get_dist_artifact_version_action.GetDistArtifactVersionRunResult:
32+
filename = payload.dist_artifact_path.name
33+
version = self._extract_version_from_filename(filename)
34+
35+
if version is None:
36+
raise code_action.ActionFailedException(
37+
f"Could not extract version from dist filename: {filename}"
38+
)
39+
40+
return get_dist_artifact_version_action.GetDistArtifactVersionRunResult(
41+
version=version
42+
)
43+
44+
def _extract_version_from_filename(self, filename: str) -> str | None:
45+
if filename.endswith('.whl'):
46+
# Wheel: name-version-python-abi-platform.whl
47+
parts = filename[:-4].split('-')
48+
if len(parts) >= 5:
49+
return parts[1]
50+
elif filename.endswith('.tar.gz'):
51+
# Source dist: name-version.tar.gz
52+
parts = filename[:-7].split('-')
53+
if len(parts) >= 2:
54+
return parts[1]
55+
elif filename.endswith('.zip'):
56+
# Source dist: name-version.zip
57+
parts = filename[:-4].split('-')
58+
if len(parts) >= 2:
59+
return parts[1]
60+
return None

extensions/fine_python_package_info/fine_python_package_info/get_src_artifact_registries_py_handler.py

Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from finecode_extension_api import code_action
44
from finecode_extension_api.actions import \
55
get_src_artifact_registries as get_src_artifact_registries_action
6-
from finecode_extension_api.interfaces import ilogger, iprojectinfoprovider
6+
from finecode_extension_api.interfaces import (
7+
ilogger,
8+
irepositorycredentialsprovider,
9+
)
710

811

912
@dataclasses.dataclass
@@ -19,65 +22,24 @@ class GetSrcArtifactRegistriesPyHandler(
1922
def __init__(
2023
self,
2124
config: GetSrcArtifactRegistriesPyHandlerConfig,
22-
project_info_provider: iprojectinfoprovider.IProjectInfoProvider,
25+
repository_credentials_provider: irepositorycredentialsprovider.IRepositoryCredentialsProvider,
2326
logger: ilogger.ILogger,
2427
) -> None:
2528
self.config = config
26-
self.project_info_provider = project_info_provider
29+
self.repository_credentials_provider = repository_credentials_provider
2730
self.logger = logger
2831

2932
async def run(
3033
self,
3134
payload: get_src_artifact_registries_action.GetSrcArtifactRegistriesRunPayload,
3235
run_context: get_src_artifact_registries_action.GetSrcArtifactRegistriesRunContext,
3336
) -> get_src_artifact_registries_action.GetSrcArtifactRegistriesRunResult:
34-
src_artifact_raw_def = await self.project_info_provider.get_project_raw_config(
35-
project_def_path=payload.src_artifact_def_path
36-
)
37-
38-
# Registries are in tool.finecode.registries
39-
tool_config = src_artifact_raw_def.get("tool", {})
40-
finecode_config = tool_config.get("finecode", {})
41-
registries_raw = finecode_config.get("registries", [])
42-
43-
if not isinstance(registries_raw, list):
44-
raise code_action.ActionFailedException(
45-
f"tool.finecode.registries in {payload.src_artifact_def_path} expected to be a list, but is {type(registries_raw)}"
46-
)
47-
48-
registries = []
49-
for idx, registry_dict in enumerate(registries_raw):
50-
if not isinstance(registry_dict, dict):
51-
raise code_action.ActionFailedException(
52-
f"Registry at index {idx} in {payload.src_artifact_def_path} expected to be a dict, but is {type(registry_dict)}"
53-
)
54-
55-
url = registry_dict.get("url")
56-
name = registry_dict.get("name")
57-
58-
if url is None:
59-
raise code_action.ActionFailedException(
60-
f"Registry at index {idx} in {payload.src_artifact_def_path} is missing 'url' field"
61-
)
62-
63-
if name is None:
64-
raise code_action.ActionFailedException(
65-
f"Registry at index {idx} in {payload.src_artifact_def_path} is missing 'name' field"
66-
)
67-
68-
if not isinstance(url, str):
69-
raise code_action.ActionFailedException(
70-
f"Registry url at index {idx} in {payload.src_artifact_def_path} expected to be a string, but is {type(url)}"
71-
)
72-
73-
if not isinstance(name, str):
74-
raise code_action.ActionFailedException(
75-
f"Registry name at index {idx} in {payload.src_artifact_def_path} expected to be a string, but is {type(name)}"
76-
)
37+
repositories = self.repository_credentials_provider.get_all_repositories()
7738

78-
registries.append(
79-
get_src_artifact_registries_action.Registry(url=url, name=name)
80-
)
39+
registries = [
40+
get_src_artifact_registries_action.Registry(url=repo.url, name=repo.name)
41+
for repo in repositories
42+
]
8143

8244
return get_src_artifact_registries_action.GetSrcArtifactRegistriesRunResult(
8345
registries=registries

extensions/fine_python_package_info/fine_python_package_info/is_artifact_published_to_registry_py_handler.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ async def run(
8888
f"Registry '{payload.registry_name}' not found in configuration"
8989
)
9090

91-
# Check if package version exists using PyPI JSON API
92-
check_url = f"{registry_url.rstrip('/')}/{package_name}/"
91+
# Check if package version exists using PyPI Simple API
92+
check_url = f"{registry_url.rstrip('/')}/simple/{package_name}/"
9393

9494
self.logger.debug(
9595
f"Checking if {package_name} {payload.version} is published to {payload.registry_name} at {check_url}"
@@ -98,12 +98,19 @@ async def run(
9898
try:
9999
async with self.http_client.session() as session:
100100
response = await session.get(check_url, headers={"Accept": "application/vnd.pypi.simple.v1+json"}, timeout=10.0)
101-
response_json = response.json()
102101
except Exception as exception:
103102
raise code_action.ActionFailedException(
104103
f"Error checking publication status: {str(exception)}"
105104
) from exception
106-
105+
106+
if response.status_code == 404:
107+
# Package does not exist in the registry yet
108+
is_published_by_dist_path = {dist_path: False for dist_path in payload.dist_artifact_paths}
109+
return is_artifact_published_to_registry_action.IsArtifactPublishedToRegistryRunResult(
110+
is_published_by_dist_path=is_published_by_dist_path
111+
)
112+
113+
response_json = response.json()
107114
version_list = response_json.get('versions', None)
108115
if version_list is None:
109116
raise code_action.ActionFailedException("No 'versions' key in response from registry")

0 commit comments

Comments
 (0)