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
2 changes: 1 addition & 1 deletion .github/actions/security-issues/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ runs:
- name: Install Python Toolbox / Security tool
shell: bash
run: |
pip install exasol-toolbox==10.2.0
pip install exasol-toolbox==10.2.1

- name: Create Security Issue Report
shell: bash
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/cd.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions doc/changes/changelog.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions doc/changes/changes_10.2.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 10.2.1 - 2026-07-08

## Summary

## Bug Fix

* #920: Ensured extracted secrets are unique and alphabetically sorted from the custom workflows
2 changes: 2 additions & 0 deletions exasol/toolbox/templates/github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
tags:
- '**'
- '!v*'
- '!latest'

jobs:
check-release-tag:
Expand All @@ -29,6 +30,7 @@ jobs:
name: Extension
uses: ./.github/workflows/cd-extension.yml
needs:
- check-release-tag
- build-and-publish
(% if custom_workflows["cd-extension"].secrets %)
secrets:
Expand Down
2 changes: 1 addition & 1 deletion exasol/toolbox/util/release/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def _create_versioned_changes(self, initial_content: str) -> None:
section.intro = resolved_vulnerabilities.intro
else:
versioned.add_child(resolved_vulnerabilities)
self.versioned_changes.write_text(versioned.rendered)
self.versioned_changes.write_text(f"{versioned.rendered}\n")

def prepare_release(self) -> Changelog:
"""
Expand Down
2 changes: 1 addition & 1 deletion exasol/toolbox/util/release/cookiecutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ def update_cookiecutter_default(version: Version) -> None:
contents_as_dict["exasol_toolbox_version_range"] = f">={version},<{version.major+1}"

updated_contents = dumps(contents_as_dict, indent=2)
COOKIECUTTER_JSON.write_text(updated_contents)
COOKIECUTTER_JSON.write_text(f"{updated_contents}\n")
30 changes: 19 additions & 11 deletions exasol/toolbox/util/workflows/custom_workflow_extractor.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
from __future__ import annotations

from pathlib import Path
from typing import TypedDict

from pydantic import (
BaseModel,
ConfigDict,
field_validator,
)

from exasol.toolbox.util.workflows.custom_workflow import CustomWorkflow


class CustomWorkflowEntry(TypedDict):
class CustomWorkflowEntry(BaseModel):
model_config = ConfigDict(frozen=True)

exists: bool
secrets: tuple[str, ...]

@field_validator("secrets", mode="before")
@classmethod
def _normalize_secrets(cls, secrets: tuple[str, ...]) -> list[str]:
"""Return unique secret names in alphabetical order."""
return sorted(set(secrets))


class CustomWorkflowExtractor(BaseModel):
model_config = ConfigDict(frozen=True)
Expand All @@ -39,21 +47,21 @@ def _build_custom_workflow_entry(
custom_workflow = CustomWorkflow.load_from_file(file_path=file_path)
secrets = custom_workflow.extract_secrets()

return {
"exists": file_path.exists(),
"secrets": secrets,
}
return CustomWorkflowEntry(
exists=file_path.exists(),
secrets=secrets,
)

def _build_merge_gate_entry(
self, custom_workflows_dict: dict[str, CustomWorkflowEntry]
) -> CustomWorkflowEntry:
return {
"exists": True,
"secrets": custom_workflows_dict["merge-gate-extension"]["secrets"]
+ custom_workflows_dict["slow-checks"]["secrets"]
return CustomWorkflowEntry(
exists=True,
secrets=custom_workflows_dict["merge-gate-extension"].secrets
+ custom_workflows_dict["slow-checks"].secrets
# from the `report.yml`
+ (self.sonar_token_name,),
}
)

def build_custom_workflow_dict(
self,
Expand Down
2 changes: 1 addition & 1 deletion project-template/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"author_email": "opensource@exasol.com",
"project_short_tag": "",
"python_version_min": "3.10",
"exasol_toolbox_version_range": ">=10.2.0,<11",
"exasol_toolbox_version_range": ">=10.2.1,<11",
"license_year": "{% now 'utc', '%Y' %}",
"__repo_name_slug": "{{cookiecutter.package_name}}",
"__package_name_slug": "{{cookiecutter.package_name}}",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "exasol-toolbox"
version = "10.2.0"
version = "10.2.1"
description = "Your one-stop solution for managing all standard tasks and core workflows of your Python project."
authors = [
{ name = "Nicola Coretti", email = "nicola.coretti@exasol.com" },
Expand Down
21 changes: 5 additions & 16 deletions test/integration/util/dependencies/audit_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,6 @@ def create_poetry_project(
return project.dir


def without_vuln_descriptions(dep: PipAuditEntry):
def strip_description(entry: PipAuditEntry):
return {k: v for k, v in entry.items() if k != "description"}

def without_descriptions(vulnerabilities):
return [strip_description(v) for v in vulnerabilities]

return {k: (without_descriptions(v) if k == "vulns" else v) for k, v in dep.items()}


def find_dependency(dependencies: list[PipAuditEntry], name: str) -> PipAuditEntry:
generator = (d for d in dependencies if d["name"] == name)
return next(generator)
Expand Down Expand Up @@ -200,9 +190,8 @@ def test_pip_audit(create_poetry_project, sample_vulnerability):
result = json.loads(audit_output)
actual = find_dependency(result["dependencies"], vuln.package_name)

expected = {
"name": vuln.package_name,
"version": vuln.version,
"vulns": [vuln.pip_audit_vuln_entry],
}
assert without_vuln_descriptions(actual) == without_vuln_descriptions(expected)
assert actual.keys() == {"name", "version", "vulns"}
assert actual["vulns"][0].keys() == {"id", "fix_versions", "aliases", "description"}
assert actual["name"] == vuln.package_name
assert actual["version"] == vuln.version
assert vuln.fix_version in actual["vulns"][0]["fix_versions"]
78 changes: 54 additions & 24 deletions test/unit/util/workflows/custom_workflow_extractor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from exasol.toolbox.util.workflows.custom_workflow_extractor import (
CustomWorkflowEntry,
CustomWorkflowExtractor,
)

Expand All @@ -22,14 +23,31 @@ def custom_workflow_extractor(workflow_directory):
)


class TestCustomWorkflowEntry:
@staticmethod
def test_secrets_are_normalized_on_creation():
custom_workflow_entry = CustomWorkflowEntry(
exists=True,
secrets=("ZETA_SECRET", "ALPHA_SECRET", "ALPHA_SECRET"),
)

assert custom_workflow_entry == CustomWorkflowEntry(
exists=True,
secrets=("ALPHA_SECRET", "ZETA_SECRET"),
)


class TestBuildCustomWorkflowEntry:
@staticmethod
def test_file_does_not_exist(custom_workflow_extractor):
custom_workflow_entry = custom_workflow_extractor._build_custom_workflow_entry(
workflow="slow-checks"
)

assert custom_workflow_entry == {"exists": False, "secrets": ()}
assert custom_workflow_entry == CustomWorkflowEntry(
exists=False,
secrets=(),
)

@staticmethod
def test_file_does_not_contain_secrets(
Expand All @@ -48,7 +66,10 @@ def test_file_does_not_contain_secrets(
workflow=workflow
)

assert custom_workflow_entry == {"exists": True, "secrets": ()}
assert custom_workflow_entry == CustomWorkflowEntry(
exists=True,
secrets=(),
)

@staticmethod
def test_file_contains_secret(custom_workflow_extractor, workflow_directory):
Expand All @@ -68,37 +89,43 @@ def test_file_contains_secret(custom_workflow_extractor, workflow_directory):
workflow=workflow
)

assert custom_workflow_entry == {
"exists": True,
"secrets": ("SLOW_CHECK_SECRET",),
}
assert custom_workflow_entry == CustomWorkflowEntry(
exists=True,
secrets=("SLOW_CHECK_SECRET",),
)


class TestBuildMergeGateEntry:
@staticmethod
def test_build_merge_gate_entry(custom_workflow_extractor):
custom_workflows_dict = {
"merge-gate-extension": {"exists": True, "secrets": ("EXT_SECRET",)},
"slow-checks": {"exists": True, "secrets": ("SLOW_SECRET",)},
"merge-gate-extension": CustomWorkflowEntry(
exists=True,
secrets=("EXT_SECRET",),
),
"slow-checks": CustomWorkflowEntry(
exists=True,
secrets=("SLOW_SECRET",),
),
}

merge_gate_entry = custom_workflow_extractor._build_merge_gate_entry(
custom_workflows_dict
)

assert merge_gate_entry == {
"exists": True,
"secrets": ("EXT_SECRET", "SLOW_SECRET", "SONAR_TOKEN"),
}
assert merge_gate_entry == CustomWorkflowEntry(
exists=True,
secrets=("EXT_SECRET", "SLOW_SECRET", "SONAR_TOKEN"),
)


class TestBuildCustomWorkflowDict:
default_custom_workflow_dict = {
"cd-extension": {"exists": False, "secrets": ()},
"fast-tests-extension": {"exists": False, "secrets": ()},
"merge-gate-extension": {"exists": False, "secrets": ()},
"slow-checks": {"exists": False, "secrets": ()},
"merge-gate": {"exists": True, "secrets": ("SONAR_TOKEN",)},
"cd-extension": CustomWorkflowEntry(exists=False, secrets=()),
"fast-tests-extension": CustomWorkflowEntry(exists=False, secrets=()),
"merge-gate-extension": CustomWorkflowEntry(exists=False, secrets=()),
"slow-checks": CustomWorkflowEntry(exists=False, secrets=()),
"merge-gate": CustomWorkflowEntry(exists=True, secrets=("SONAR_TOKEN",)),
}

def test_no_custom_workflows_exist(self, custom_workflow_extractor):
Expand Down Expand Up @@ -136,14 +163,17 @@ def test_custom_workflow_written_to(
custom_workflow_dict = custom_workflow_extractor.build_custom_workflow_dict()

expected_custom_workflow_dict = self.default_custom_workflow_dict.copy()
expected_custom_workflow_dict[workflow] = custom_workflow_dict[workflow] = {
"exists": True,
"secrets": (secret,),
}
expected_custom_workflow_dict[workflow] = CustomWorkflowEntry(
exists=True,
secrets=(secret,),
)
if workflow in ("merge-gate-extension", "slow-checks"):
expected_custom_workflow_dict["merge-gate"]["secrets"] = (
secret,
"SONAR_TOKEN",
expected_custom_workflow_dict["merge-gate"] = CustomWorkflowEntry(
exists=True,
secrets=(
secret,
"SONAR_TOKEN",
),
)

assert custom_workflow_dict == expected_custom_workflow_dict
2 changes: 1 addition & 1 deletion test/unit/util/workflows/render_yaml_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,8 @@ def test_includes_extension_with_multiple_secrets(test_yml, project_config):
merge-gate-extension:
uses: ./.github/workflows/merge-gate-extension.yml
secrets:
MERGE_GATE_SECRET: ${{ secrets.MERGE_GATE_SECRET }}
ANOTHER_SECRET: ${{ secrets.ANOTHER_SECRET }}
MERGE_GATE_SECRET: ${{ secrets.MERGE_GATE_SECRET }}
permissions:
contents: read

Expand Down