Skip to content

Commit 14c27c4

Browse files
committed
Adop workflow generation for OSA agents. Update organization. Increment OSA version
1 parent 605c598 commit 14c27c4

File tree

13 files changed

+245
-54
lines changed

13 files changed

+245
-54
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ Docker, ensure that you upload the PDF file to the OSA folder before building th
186186
/app/OSA/... or just use volume mounting to access the file.
187187

188188
The --generate-workflows option is intended to create customizable CI/CD pipelines for Python repositories. For detailed
189-
documentation, see the [Workflow Generator README](./osa_tool/workflow/README.md).
189+
documentation, see the [Workflow Generator README](./osa_tool/operations/codebase/workflow_generation/README.md).
190190

191191
### Configuration
192192

@@ -208,9 +208,12 @@ documentation, see the [Workflow Generator README](./osa_tool/workflow/README.md
208208
| `--no-fork` | Avoid create fork for target repository | `False` |
209209
| `--no-pull-request` | Avoid create pull request for target repository | `False` |
210210

211-
Also OSA supports custom configuration via TOML files. Use the `--config-file` option to specify a path to custom configuration file. If no custom configuration file is provided, OSA will use the default configuration.
211+
Also OSA supports custom configuration via TOML files. Use the `--config-file` option to specify a path to custom
212+
configuration file. If no custom configuration file is provided, OSA will use the default configuration.
212213

213-
By default, OSA uses a single model for all tasks (specified via `--model`). If you want to use different models for different types of tasks, disable the `--use-single-model` flag and specify models for each task type (`--model-docstring`, `--model-readme`, `--model-validation`, `--model-general`).
214+
By default, OSA uses a single model for all tasks (specified via `--model`). If you want to use different models for
215+
different types of tasks, disable the `--use-single-model` flag and specify models for each task type (
216+
`--model-docstring`, `--model-readme`, `--model-validation`, `--model-general`).
214217

215218
To learn how to work with the interactive CLI and view descriptions of all available keys, visit
216219
the [CLI usage guide](./osa_tool/scheduler/README.md).

docs/core/operations/OPERATIONS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ This document is auto-generated. Do not edit manually.
66

77
| Name | Priority | Intents | Scopes | Args Schema | Executor | Method |
88
|------|----------|---------|--------|-------------|----------|--------|
9-
| `generate_report` | 5 | new_task | full_repo, analysis || `ReportGenerator` | `build_pdf` |
9+
| `generate_report` | 5 | new_task | full_repo, analysis || `ReportGenerator` | `run` |
10+
| `validate_doc` | 10 | new_task | full_repo, analysis || `DocValidator` | `run` |
11+
| `validate_paper` | 15 | new_task | full_repo, analysis || `PaperValidator` | `run` |
1012
| `convert_notebooks` | 30 | new_task | full_repo, codebase | ConvertNotebooksArgs | `NotebookConverter` | `convert_notebooks` |
1113
| `translate_dirs` | 40 | new_task | full_repo, codebase || `RepositoryStructureTranslator` | `rename_directories_and_files` |
1214
| `generate_docstrings` | 50 | new_task, feedback | full_repo, codebase | GenerateDocstringsArgs | `DocstringsGenerator` | `run` |
@@ -16,4 +18,5 @@ This document is auto-generated. Do not edit manually.
1618
| `generate_readme` | 70 | new_task, feedback | full_repo, docs || `ReadmeAgent` | `generate_readme` |
1719
| `translate_readme` | 75 | new_task, feedback | full_repo, docs | TranslateReadmeArgs | `ReadmeTranslator` | `translate_readme` |
1820
| `generate_about` | 80 | new_task | full_repo, docs || `AboutGenerator` | `generate_about_content` |
21+
| `generate_workflows` | 85 | new_task, feedback | full_repo, codebase | GenerateWorkflowsArgs | `WorkflowsExecutor` | `generate` |
1922
| `organize` | 90 | new_task | full_repo, codebase || `RepoOrganizer` | `organize` |

osa_tool/operations/codebase/requirements_generation/requirements_generation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def generate(self) -> dict:
4242
pyproject_path = self.repo_path / "pyproject.toml"
4343

4444
old_context = self._get_existing_context(req_file_path, pyproject_path)
45+
if old_context:
46+
self._add_event(EventKind.ANALYZED, mode="existing-context")
4547

4648
# Scan with notebooks
4749
try:

osa_tool/workflow/README.md renamed to osa_tool/operations/codebase/workflow_generation/README.md

File renamed without changes.

osa_tool/workflow/__init__.py renamed to osa_tool/operations/codebase/workflow_generation/__init__.py

File renamed without changes.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from typing import List
2+
3+
from osa_tool.config.settings import ConfigManager
4+
from osa_tool.core.models.event import EventKind, OperationEvent
5+
from osa_tool.scheduler.workflow_manager import WorkflowManager
6+
from osa_tool.utils.logger import logger
7+
8+
9+
class WorkflowsExecutor:
10+
"""
11+
Executor for CI/CD workflow generation in the agentic pipeline.
12+
13+
Bridges WorkflowManager to the Operation/Executor pattern,
14+
bypassing the legacy Plan-based config path.
15+
"""
16+
17+
def __init__(
18+
self,
19+
config_manager: ConfigManager,
20+
workflow_manager: WorkflowManager,
21+
include_black: bool = True,
22+
include_tests: bool = True,
23+
include_pep8: bool = True,
24+
include_autopep8: bool = False,
25+
include_fix_pep8: bool = False,
26+
include_pypi: bool = False,
27+
pep8_tool: str = "flake8",
28+
use_poetry: bool = False,
29+
include_codecov: bool = True,
30+
python_versions: List[str] = None,
31+
branches: List[str] = None,
32+
):
33+
self.config_manager = config_manager
34+
self.workflow_manager = workflow_manager
35+
self.events: list[OperationEvent] = []
36+
self._requested = {
37+
"generate_workflows": True,
38+
"include_black": include_black,
39+
"include_tests": include_tests,
40+
"include_pep8": include_pep8,
41+
"include_autopep8": include_autopep8,
42+
"include_fix_pep8": include_fix_pep8,
43+
"include_pypi": include_pypi,
44+
"pep8_tool": pep8_tool,
45+
"use_poetry": use_poetry,
46+
"include_codecov": include_codecov,
47+
"python_versions": python_versions or ["3.9", "3.10"],
48+
"branches": branches or ["main", "master"],
49+
}
50+
51+
def generate(self) -> dict:
52+
if not self.workflow_manager.has_python_code():
53+
logger.info("No Python code detected. Skipping workflow generation.")
54+
self.events.append(
55+
OperationEvent(
56+
kind=EventKind.SKIPPED,
57+
target="workflows",
58+
data={"reason": "no_python_code"},
59+
)
60+
)
61+
return {"result": {"generated": False}, "events": self.events}
62+
63+
logger.debug("Requested workflow settings: %s", self._requested)
64+
effective = self._skip_existing_jobs(self._requested)
65+
logger.debug("Effective workflow settings after filtering: %s", effective)
66+
WorkflowManager.apply_workflow_settings(self.config_manager, effective)
67+
success = self.workflow_manager.generate_workflow(self.config_manager)
68+
69+
if success:
70+
enabled = [k for k, v in effective.items() if k.startswith("include_") and v is True]
71+
logger.info("CI/CD workflow generation succeeded. Enabled jobs: %s", enabled)
72+
self.events.append(
73+
OperationEvent(
74+
kind=EventKind.GENERATED,
75+
target="workflows",
76+
data={
77+
"include_black": effective.get("include_black"),
78+
"include_tests": effective.get("include_tests"),
79+
"include_pep8": effective.get("include_pep8"),
80+
"include_autopep8": effective.get("include_autopep8"),
81+
"include_fix_pep8": effective.get("include_fix_pep8"),
82+
"include_pypi": effective.get("include_pypi"),
83+
},
84+
)
85+
)
86+
else:
87+
logger.error("CI/CD workflow generation failed. Check previous log messages for details.")
88+
self.events.append(
89+
OperationEvent(
90+
kind=EventKind.FAILED,
91+
target="workflows",
92+
data={"reason": "generation_error"},
93+
)
94+
)
95+
96+
return {"result": {"generated": success, "settings": effective}, "events": self.events}
97+
98+
def _skip_existing_jobs(self, settings: dict) -> dict:
99+
"""Disable generation for jobs that already exist in the repository."""
100+
result = dict(settings)
101+
for key, job_names in self.workflow_manager.job_name_for_key.items():
102+
if key not in result:
103+
continue
104+
names = [job_names] if isinstance(job_names, str) else job_names
105+
if any(job in self.workflow_manager.existing_jobs for job in names):
106+
result[key] = False
107+
logger.warning("Skipping '%s' workflow: job already exists in the repository.", key)
108+
self.events.append(
109+
OperationEvent(
110+
kind=EventKind.SKIPPED,
111+
target=key,
112+
data={"reason": "already_exists"},
113+
)
114+
)
115+
return result

osa_tool/workflow/workflow_generator.py renamed to osa_tool/operations/codebase/workflow_generation/workflow_generator.py

File renamed without changes.

osa_tool/operations/docs/community_docs_generation/docs_run.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,54 +25,59 @@ def generate_documentation(config_manager: ConfigManager, metadata: RepositoryMe
2525
events: list[OperationEvent] = []
2626
generated_files: list[str] = []
2727
contributing = ContributingBuilder(config_manager, metadata)
28-
contributing.build()
29-
events.append(OperationEvent(kind=EventKind.GENERATED, target="CONTRIBUTING"))
30-
generated_files.append("CONTRIBUTING.md")
31-
3228
community = CommunityTemplateBuilder(config_manager, metadata)
33-
community.build_code_of_conduct()
34-
events.append(OperationEvent(kind=EventKind.GENERATED, target="CODE_OF_CONDUCT"))
35-
generated_files.append("CODE_OF_CONDUCT.md")
3629

37-
community.build_security()
38-
events.append(OperationEvent(kind=EventKind.GENERATED, target="SECURITY"))
39-
generated_files.append("SECURITY.md")
30+
try:
31+
contributing.build()
32+
events.append(OperationEvent(kind=EventKind.GENERATED, target="CONTRIBUTING"))
33+
generated_files.append("CONTRIBUTING.md")
34+
except Exception as e:
35+
logger.error("Failed to generate CONTRIBUTING: %s", repr(e), exc_info=True)
36+
events.append(OperationEvent(kind=EventKind.FAILED, target="CONTRIBUTING", data={"error": repr(e)}))
4037

41-
if config_manager.get_git_settings().host in ["github", "gitlab"]:
42-
community.build_pull_request()
43-
community.build_bug_issue()
44-
community.build_documentation_issue()
45-
community.build_feature_issue()
38+
try:
39+
community.build_code_of_conduct()
40+
events.append(OperationEvent(kind=EventKind.GENERATED, target="CODE_OF_CONDUCT"))
41+
generated_files.append("CODE_OF_CONDUCT.md")
42+
except Exception as e:
43+
logger.error("Failed to generate CODE_OF_CONDUCT: %s", repr(e), exc_info=True)
44+
events.append(OperationEvent(kind=EventKind.FAILED, target="CODE_OF_CONDUCT", data={"error": repr(e)}))
4645

47-
events.extend(
48-
[
49-
OperationEvent(kind=EventKind.GENERATED, target="PULL_REQUEST_TEMPLATE"),
50-
OperationEvent(kind=EventKind.GENERATED, target="ISSUE_TEMPLATE:bug"),
51-
OperationEvent(kind=EventKind.GENERATED, target="ISSUE_TEMPLATE:documentation"),
52-
OperationEvent(kind=EventKind.GENERATED, target="ISSUE_TEMPLATE:feature"),
53-
]
54-
)
55-
generated_files.extend(
56-
[
57-
"PULL_REQUEST_TEMPLATE.md",
58-
"BUG_ISSUE.md",
59-
"DOCUMENTATION_ISSUE.md",
60-
"FEATURE_ISSUE.md",
61-
]
62-
)
46+
try:
47+
community.build_security()
48+
events.append(OperationEvent(kind=EventKind.GENERATED, target="SECURITY"))
49+
generated_files.append("SECURITY.md")
50+
except Exception as e:
51+
logger.error("Failed to generate SECURITY: %s", repr(e), exc_info=True)
52+
events.append(OperationEvent(kind=EventKind.FAILED, target="SECURITY", data={"error": repr(e)}))
6353

64-
if config_manager.get_git_settings().host == "gitlab":
65-
community.build_vulnerability_disclosure()
54+
if config_manager.get_git_settings().host in ["github", "gitlab"]:
55+
for method, target, filename in [
56+
(community.build_pull_request, "PULL_REQUEST_TEMPLATE", "PULL_REQUEST_TEMPLATE.md"),
57+
(community.build_bug_issue, "ISSUE_TEMPLATE:bug", "BUG_ISSUE.md"),
58+
(community.build_documentation_issue, "ISSUE_TEMPLATE:documentation", "DOCUMENTATION_ISSUE.md"),
59+
(community.build_feature_issue, "ISSUE_TEMPLATE:feature", "FEATURE_ISSUE.md"),
60+
]:
61+
try:
62+
method()
63+
events.append(OperationEvent(kind=EventKind.GENERATED, target=target))
64+
generated_files.append(filename)
65+
except Exception as e:
66+
logger.error("Failed to generate %s: %s", target, repr(e), exc_info=True)
67+
events.append(OperationEvent(kind=EventKind.FAILED, target=target, data={"error": repr(e)}))
6668

67-
events.append(
68-
OperationEvent(
69-
kind=EventKind.GENERATED,
70-
target="VULNERABILITY_DISCLOSURE",
69+
if config_manager.get_git_settings().host == "gitlab":
70+
try:
71+
community.build_vulnerability_disclosure()
72+
events.append(OperationEvent(kind=EventKind.GENERATED, target="VULNERABILITY_DISCLOSURE"))
73+
generated_files.append("Vulnerability_Disclosure.md")
74+
except Exception as e:
75+
logger.error("Failed to generate VULNERABILITY_DISCLOSURE: %s", repr(e), exc_info=True)
76+
events.append(
77+
OperationEvent(kind=EventKind.FAILED, target="VULNERABILITY_DISCLOSURE", data={"error": repr(e)})
7178
)
72-
)
73-
generated_files.append("Vulnerability_Disclosure.md")
7479

75-
logger.info("All additional documentation successfully generated.")
80+
logger.info("Additional documentation generation completed.")
7681

7782
return {
7883
"result": {

osa_tool/operations/operations_catalog.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from osa_tool.operations.codebase.notebook_conversion.notebook_converter import NotebookConverter
1313
from osa_tool.operations.codebase.organization.repo_organizer import RepoOrganizer
1414
from osa_tool.operations.codebase.requirements_generation.requirements_generation import RequirementsGenerator
15+
from osa_tool.operations.codebase.workflow_generation.workflow_executor import WorkflowsExecutor
1516
from osa_tool.operations.docs.about_generation.about_generator import AboutGenerator
1617
from osa_tool.operations.docs.community_docs_generation.docs_run import generate_documentation
1718
from osa_tool.operations.docs.community_docs_generation.license_generation import LicenseCompiler
@@ -229,6 +230,42 @@ class GenerateAboutOperation(Operation):
229230
executor_dependencies = ["config_manager", "git_agent"]
230231

231232

233+
class GenerateWorkflowsArgs(BaseModel):
234+
include_black: bool = Field(True, description="Generate Black code formatter workflow.")
235+
include_tests: bool = Field(True, description="Generate unit tests workflow.")
236+
include_pep8: bool = Field(True, description="Generate PEP 8 compliance check workflow.")
237+
include_autopep8: bool = Field(False, description="Generate autopep8 auto-fix workflow.")
238+
include_fix_pep8: bool = Field(False, description="Generate fix-pep8 slash-command workflow.")
239+
include_pypi: bool = Field(False, description="Generate PyPI publish workflow.")
240+
pep8_tool: Literal["flake8", "pylint"] = Field("flake8", description="Tool for PEP 8 checking.")
241+
use_poetry: bool = Field(False, description="Use Poetry for PyPI packaging.")
242+
include_codecov: bool = Field(True, description="Include Codecov coverage upload step.")
243+
python_versions: List[str] = Field(
244+
default_factory=lambda: ["3.9", "3.10"],
245+
description="Python versions to test against. Example: ['3.10', '3.11', '3.12']",
246+
)
247+
branches: List[str] = Field(
248+
default_factory=lambda: ["main", "master"],
249+
description="Git branches to trigger workflows on.",
250+
)
251+
252+
253+
class GenerateWorkflowsOperation(Operation):
254+
name = "generate_workflows"
255+
description = "Generate CI/CD workflow files (GitHub Actions / GitLab CI) for the repository."
256+
257+
supported_intents = ["new_task", "feedback"]
258+
supported_scopes = ["full_repo", "codebase"]
259+
priority = 85
260+
261+
args_schema = GenerateWorkflowsArgs
262+
args_policy = "auto"
263+
264+
executor = WorkflowsExecutor
265+
executor_method = "generate"
266+
executor_dependencies = ["config_manager", "workflow_manager"]
267+
268+
232269
class OrganizeRepositoryOperation(Operation):
233270
name = "organize"
234271
description = (

osa_tool/scheduler/workflow_manager.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import os
22
from abc import ABC, abstractmethod
3+
from pathlib import Path
34
from typing import Optional
45

56
import yaml
67

78
from osa_tool.config.settings import ConfigManager
8-
from osa_tool.scheduler.plan import Plan
99
from osa_tool.core.git.metadata import RepositoryMetadata
10+
from osa_tool.operations.codebase.workflow_generation.workflow_generator import (
11+
GitHubWorkflowGenerator,
12+
GitLabWorkflowGenerator,
13+
)
14+
from osa_tool.scheduler.plan import Plan
1015
from osa_tool.tools.repository_analysis.sourcerank import SourceRank
1116
from osa_tool.utils.arguments_parser import get_keys_from_group_in_yaml
1217
from osa_tool.utils.logger import logger
1318
from osa_tool.utils.utils import parse_folder_name
14-
from osa_tool.workflow.workflow_generator import GitHubWorkflowGenerator, GitLabWorkflowGenerator
1519

1620

1721
class WorkflowManager(ABC):
@@ -71,12 +75,21 @@ def has_python_code(self) -> bool:
7175
"""
7276
Checks whether the repository contains Python code.
7377
78+
First checks the repository metadata language field. If that is absent or
79+
does not mention Python, falls back to counting ``.py`` files on disk.
80+
7481
Returns:
7582
True if Python code is present, False otherwise.
7683
"""
77-
if not self.metadata.language:
78-
return False
79-
return "Python" in self.metadata.language
84+
if self.metadata.language and "Python" in self.metadata.language:
85+
return True
86+
87+
py_count = sum(1 for _ in Path(self.base_path).rglob("*.py"))
88+
if py_count > 0:
89+
logger.info("Metadata did not report Python, but found %d .py file(s) on disk.", py_count)
90+
return True
91+
92+
return False
8093

8194
def build_actual_plan(self, sourcerank: SourceRank) -> dict:
8295
"""
@@ -122,6 +135,19 @@ def build_actual_plan(self, sourcerank: SourceRank) -> dict:
122135

123136
return result_plan
124137

138+
@staticmethod
139+
def apply_workflow_settings(config_manager: ConfigManager, settings: dict) -> None:
140+
"""
141+
Apply workflow settings directly from a dict, bypassing the legacy Plan.
142+
Used by the agentic pipeline.
143+
144+
Args:
145+
config_manager: Configuration manager to update.
146+
settings: Dict of workflow settings keys and values.
147+
"""
148+
config_manager.config.workflows = config_manager.config.workflows.model_copy(update=settings)
149+
logger.info("Config successfully updated with workflow settings")
150+
125151
def update_workflow_config(self, config_manager: ConfigManager, plan: Plan) -> None:
126152
"""
127153
Update workflow configuration settings in the config loader based on the given plan.

0 commit comments

Comments
 (0)