Renamed artifacts.

This commit is contained in:
Patrick Lehmann
2022-11-05 15:11:14 +01:00
parent 7ef3bc0a4a
commit 96bccfbd18
5 changed files with 277 additions and 111 deletions

View File

@@ -100,32 +100,6 @@ jobs:
currentAlphaVersion = "3.12" currentAlphaVersion = "3.12"
currentAlphaRelease = "3.12.0-alpha.1" currentAlphaRelease = "3.12.0-alpha.1"
artifact_names = {
"unittesting": f"{name}-TestReport",
"codecoverage": f"{name}-Coverage",
"statictyping": f"{name}-Typing",
"package": f"{name}-Package",
"documentation": f"{name}-Documentation",
}
# Deprecated structure
params = {
"python_version": python_version,
"artifacts": {
"unittesting": f"{artifact_names['unittesting']}",
"coverage": f"{artifact_names['codecoverage']}",
"typing": f"{artifact_names['statictyping']}",
"package": f"{artifact_names['package']}",
"doc": f"{artifact_names['documentation']}",
}
}
print("Parameters:")
print(f" python_version: {python_version}")
print(f" artifact_names ({len(artifact_names)}):")
for id, name in artifact_names.items():
print(f" {id:>14}: {name}")
if systems == "": if systems == "":
print("::error title=Parameter::system_list is empty.") print("::error title=Parameter::system_list is empty.")
else: else:
@@ -238,16 +212,38 @@ jobs:
for runtime, version in combinations if runtime not in data["sys"] for runtime, version in combinations if runtime not in data["sys"]
] ]
# Format jobs as list of dictionaries artifact_names = {
buffer = "" "unittesting_xml": f"{name}-TestReportSummary-XML",
for job in jobs: "codecoverage_xml": f"{name}-CodeCoverage-XML",
buffer += f" {{ " + ", ".join([f"\"{key}\": \"{value}\"" for key, value in job.items()]) + f" }},\n" "codecoverage_html": f"{name}-CodeCoverage-HTML",
"statictyping_html": f"{name}-StaticTyping-HTML",
"package_all": f"{name}-Packages",
"documentation_pdf": f"{name}-Documentation-PDF",
"documentation_html": f"{name}-Documentation-HTML",
}
# Deprecated structure
params = {
"python_version": python_version,
"artifacts": {
"unittesting": f"{artifact_names['unittesting_xml']}",
"coverage": f"{artifact_names['codecoverage_html']}",
"typing": f"{artifact_names['statictyping_html']}",
"package": f"{artifact_names['package_all']}",
"doc": f"{artifact_names['documentation_html']}",
}
}
print("Parameters:")
print(f" python_version: {python_version}")
print(f" artifact_names ({len(artifact_names)}):")
for id, name in artifact_names.items():
print(f" {id:>14}: {name}")
buffer = "".join([f" {{ " + ", ".join([f"\"{key}\": \"{value}\"" for key, value in job.items()]) + f" }},\n" for job in jobs])
print(dedent(f"""\ print(dedent(f"""\
Python jobs ({len(jobs)}): Python jobs ({len(jobs)}):
[ {buffer}"""))
{buffer} ]
"""))
# Write jobs to special file # Write jobs to special file
github_output = Path(getenv("GITHUB_OUTPUT")) github_output = Path(getenv("GITHUB_OUTPUT"))

View File

@@ -60,23 +60,30 @@ jobs:
run: run:
shell: python shell: python
steps: steps:
- name: Install dependencies
shell: bash
run: pip install pyTooling
# Params_Default # Params_Default
- name: Checking results from 'Params_Default' - name: Checking results from 'Params_Default'
run: | run: |
from json import loads as json_loads from json import loads as json_loads
from sys import exit from sys import exit
from pyTooling.Common import zipdicts
expectedPythonVersion = "3.11" expectedPythonVersion = "3.11"
expectedPythons = ["3.7", "3.8", "3.9", "3.10", "3.11"] expectedPythons = ["3.7", "3.8", "3.9", "3.10", "3.11"]
expectedSystems = ["ubuntu", "windows", "macos"] expectedSystems = ["ubuntu", "windows", "macos"]
expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["mingw64:3.10"] expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["mingw64:3.10"]
expectedName = "Example" expectedName = "Example"
expectedArtifacts = { expectedArtifacts = {
"unittesting": f"{expectedName}-TestReport", "unittesting_xml": f"{expectedName}-TestReportSummary-XML",
"codecoverage": f"{expectedName}-Coverage", "codecoverage_xml": f"{expectedName}-CodeCoverage-XML",
"statictyping": f"{expectedName}-Typing", "codecoverage_html": f"{expectedName}-CodeCoverage-HTML",
"package": f"{expectedName}-Package", "statictyping_html": f"{expectedName}-StaticTyping-HTML",
"documentation": f"{expectedName}-Documentation" "package_all": f"{expectedName}-Packages",
"documentation_pdf": f"{expectedName}-Documentation-PDF",
"documentation_html": f"{expectedName}-Documentation-HTML",
} }
actualPythonVersion = """${{ needs.Params_Default.outputs.python_version }}""" actualPythonVersion = """${{ needs.Params_Default.outputs.python_version }}"""
@@ -94,8 +101,11 @@ jobs:
errors += 1 errors += 1
if len(actualArtifactNames) != len(expectedArtifacts): if len(actualArtifactNames) != len(expectedArtifacts):
print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.") print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.")
for name in actualArtifactNames: errors += 1
print(f" {name}") else:
for key, actual, expected in zipdicts(actualArtifactNames, expectedArtifacts):
if actual != expected:
print(f"Artifact name '{key}' does not match: {actual} != {expected}.")
errors += 1 errors += 1
if errors == 0: if errors == 0:
@@ -108,17 +118,21 @@ jobs:
from json import loads as json_loads from json import loads as json_loads
from sys import exit from sys import exit
from pyTooling.Common import zipdicts
expectedPythonVersion = "3.11" expectedPythonVersion = "3.11"
expectedPythons = ["3.9", "3.10", "pypy-3.8", "pypy-3.9"] expectedPythons = ["3.9", "3.10", "pypy-3.8", "pypy-3.9"]
expectedSystems = ["ubuntu", "windows", "macos"] expectedSystems = ["ubuntu", "windows", "macos"]
expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["mingw64:3.10"] expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["mingw64:3.10"]
expectedName = "Example" expectedName = "Example"
expectedArtifacts = { expectedArtifacts = {
"unittesting": f"{expectedName}-TestReport", "unittesting_xml": f"{expectedName}-TestReportSummary-XML",
"codecoverage": f"{expectedName}-Coverage", "codecoverage_xml": f"{expectedName}-CodeCoverage-XML",
"statictyping": f"{expectedName}-Typing", "codecoverage_html": f"{expectedName}-CodeCoverage-HTML",
"package": f"{expectedName}-Package", "statictyping_html": f"{expectedName}-StaticTyping-HTML",
"documentation": f"{expectedName}-Documentation" "package_all": f"{expectedName}-Packages",
"documentation_pdf": f"{expectedName}-Documentation-PDF",
"documentation_html": f"{expectedName}-Documentation-HTML",
} }
actualPythonVersion = """${{ needs.Params_PythonVersions.outputs.python_version }}""" actualPythonVersion = """${{ needs.Params_PythonVersions.outputs.python_version }}"""
@@ -136,8 +150,11 @@ jobs:
errors += 1 errors += 1
if len(actualArtifactNames) != len(expectedArtifacts): if len(actualArtifactNames) != len(expectedArtifacts):
print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.") print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.")
for name in actualArtifactNames: errors += 1
print(f" {name}") else:
for key, actual, expected in zipdicts(actualArtifactNames, expectedArtifacts):
if actual != expected:
print(f"Artifact name '{key}' does not match: {actual} != {expected}.")
errors += 1 errors += 1
if errors == 0: if errors == 0:
@@ -150,17 +167,21 @@ jobs:
from json import loads as json_loads from json import loads as json_loads
from sys import exit from sys import exit
from pyTooling.Common import zipdicts
expectedPythonVersion = "3.11" expectedPythonVersion = "3.11"
expectedPythons = ["3.7", "3.8", "3.9", "3.10", "3.11"] expectedPythons = ["3.7", "3.8", "3.9", "3.10", "3.11"]
expectedSystems = ["windows"] expectedSystems = ["windows"]
expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["mingw32:3.10", "mingw64:3.10"] expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["mingw32:3.10", "mingw64:3.10"]
expectedName = "Example" expectedName = "Example"
expectedArtifacts = { expectedArtifacts = {
"unittesting": f"{expectedName}-TestReport", "unittesting_xml": f"{expectedName}-TestReportSummary-XML",
"codecoverage": f"{expectedName}-Coverage", "codecoverage_xml": f"{expectedName}-CodeCoverage-XML",
"statictyping": f"{expectedName}-Typing", "codecoverage_html": f"{expectedName}-CodeCoverage-HTML",
"package": f"{expectedName}-Package", "statictyping_html": f"{expectedName}-StaticTyping-HTML",
"documentation": f"{expectedName}-Documentation" "package_all": f"{expectedName}-Packages",
"documentation_pdf": f"{expectedName}-Documentation-PDF",
"documentation_html": f"{expectedName}-Documentation-HTML",
} }
actualPythonVersion = """${{ needs.Params_Systems.outputs.python_version }}""" actualPythonVersion = """${{ needs.Params_Systems.outputs.python_version }}"""
@@ -178,8 +199,11 @@ jobs:
errors += 1 errors += 1
if len(actualArtifactNames) != len(expectedArtifacts): if len(actualArtifactNames) != len(expectedArtifacts):
print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.") print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.")
for name in actualArtifactNames: errors += 1
print(f" {name}") else:
for key, actual, expected in zipdicts(actualArtifactNames, expectedArtifacts):
if actual != expected:
print(f"Artifact name '{key}' does not match: {actual} != {expected}.")
errors += 1 errors += 1
if errors == 0: if errors == 0:
@@ -192,17 +216,21 @@ jobs:
from json import loads as json_loads from json import loads as json_loads
from sys import exit from sys import exit
from pyTooling.Common import zipdicts
expectedPythonVersion = "3.11" expectedPythonVersion = "3.11"
expectedPythons = ["3.10"] expectedPythons = ["3.10"]
expectedSystems = ["ubuntu", "windows", "macos"] expectedSystems = ["ubuntu", "windows", "macos"]
expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["ubuntu:3.11", "ubuntu:3.12"] expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["ubuntu:3.11", "ubuntu:3.12"]
expectedName = "Example" expectedName = "Example"
expectedArtifacts = { expectedArtifacts = {
"unittesting": f"{expectedName}-TestReport", "unittesting_xml": f"{expectedName}-TestReportSummary-XML",
"codecoverage": f"{expectedName}-Coverage", "codecoverage_xml": f"{expectedName}-CodeCoverage-XML",
"statictyping": f"{expectedName}-Typing", "codecoverage_html": f"{expectedName}-CodeCoverage-HTML",
"package": f"{expectedName}-Package", "statictyping_html": f"{expectedName}-StaticTyping-HTML",
"documentation": f"{expectedName}-Documentation" "package_all": f"{expectedName}-Packages",
"documentation_pdf": f"{expectedName}-Documentation-PDF",
"documentation_html": f"{expectedName}-Documentation-HTML",
} }
actualPythonVersion = """${{ needs.Params_Include.outputs.python_version }}""" actualPythonVersion = """${{ needs.Params_Include.outputs.python_version }}"""
@@ -220,8 +248,11 @@ jobs:
errors += 1 errors += 1
if len(actualArtifactNames) != len(expectedArtifacts): if len(actualArtifactNames) != len(expectedArtifacts):
print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.") print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.")
for name in actualArtifactNames: errors += 1
print(f" {name}") else:
for key, actual, expected in zipdicts(actualArtifactNames, expectedArtifacts):
if actual != expected:
print(f"Artifact name '{key}' does not match: {actual} != {expected}.")
errors += 1 errors += 1
if errors == 0: if errors == 0:
@@ -234,17 +265,21 @@ jobs:
from json import loads as json_loads from json import loads as json_loads
from sys import exit from sys import exit
from pyTooling.Common import zipdicts
expectedPythonVersion = "3.11" expectedPythonVersion = "3.11"
expectedPythons = ["3.10"] expectedPythons = ["3.10"]
expectedSystems = ["ubuntu", "macos"] expectedSystems = ["ubuntu", "macos"]
expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons]
expectedName = "Example" expectedName = "Example"
expectedArtifacts = { expectedArtifacts = {
"unittesting": f"{expectedName}-TestReport", "unittesting_xml": f"{expectedName}-TestReportSummary-XML",
"codecoverage": f"{expectedName}-Coverage", "codecoverage_xml": f"{expectedName}-CodeCoverage-XML",
"statictyping": f"{expectedName}-Typing", "codecoverage_html": f"{expectedName}-CodeCoverage-HTML",
"package": f"{expectedName}-Package", "statictyping_html": f"{expectedName}-StaticTyping-HTML",
"documentation": f"{expectedName}-Documentation" "package_all": f"{expectedName}-Packages",
"documentation_pdf": f"{expectedName}-Documentation-PDF",
"documentation_html": f"{expectedName}-Documentation-HTML",
} }
actualPythonVersion = """${{ needs.Params_Exclude.outputs.python_version }}""" actualPythonVersion = """${{ needs.Params_Exclude.outputs.python_version }}"""
@@ -262,8 +297,11 @@ jobs:
errors += 1 errors += 1
if len(actualArtifactNames) != len(expectedArtifacts): if len(actualArtifactNames) != len(expectedArtifacts):
print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.") print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.")
for name in actualArtifactNames: errors += 1
print(f" {name}") else:
for key, actual, expected in zipdicts(actualArtifactNames, expectedArtifacts):
if actual != expected:
print(f"Artifact name '{key}' does not match: {actual} != {expected}.")
errors += 1 errors += 1
if errors == 0: if errors == 0:
@@ -276,17 +314,21 @@ jobs:
from json import loads as json_loads from json import loads as json_loads
from sys import exit from sys import exit
from pyTooling.Common import zipdicts
expectedPythonVersion = "3.11" expectedPythonVersion = "3.11"
expectedPythons = ["3.10", "3.11"] expectedPythons = ["3.10", "3.11"]
expectedSystems = ["ubuntu", "windows"] expectedSystems = ["ubuntu", "windows"]
expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["windows:3.8", "windows:3.9", "windows:3.12"] expectedJobs = [f"{system}:{python}" for system in expectedSystems for python in expectedPythons] + ["windows:3.8", "windows:3.9", "windows:3.12"]
expectedName = "Example" expectedName = "Example"
expectedArtifacts = { expectedArtifacts = {
"unittesting": f"{expectedName}-TestReport", "unittesting_xml": f"{expectedName}-TestReportSummary-XML",
"codecoverage": f"{expectedName}-Coverage", "codecoverage_xml": f"{expectedName}-CodeCoverage-XML",
"statictyping": f"{expectedName}-Typing", "codecoverage_html": f"{expectedName}-CodeCoverage-HTML",
"package": f"{expectedName}-Package", "statictyping_html": f"{expectedName}-StaticTyping-HTML",
"documentation": f"{expectedName}-Documentation" "package_all": f"{expectedName}-Packages",
"documentation_pdf": f"{expectedName}-Documentation-PDF",
"documentation_html": f"{expectedName}-Documentation-HTML",
} }
actualPythonVersion = """${{ needs.Params_All.outputs.python_version }}""" actualPythonVersion = """${{ needs.Params_All.outputs.python_version }}"""
@@ -304,8 +346,11 @@ jobs:
errors += 1 errors += 1
if len(actualArtifactNames) != len(expectedArtifacts): if len(actualArtifactNames) != len(expectedArtifacts):
print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.") print(f"Number of 'artifact_names' does not match: {len(actualArtifactNames)} != {len(expectedArtifacts)}.")
for name in actualArtifactNames: errors += 1
print(f" {name}") else:
for key, actual, expected in zipdicts(actualArtifactNames, expectedArtifacts):
if actual != expected:
print(f"Artifact name '{key}' does not match: {actual} != {expected}.")
errors += 1 errors += 1
if errors == 0: if errors == 0:

View File

@@ -1,32 +1,97 @@
.. _JOBTMPL:
Overview Overview
######## ########
**Global Templates** The following list categorizes all pre-defined job templates, which can be instantiated in a pipeline (GitHub Action
Workflow). They can also serve as an example for creating or driving own job templates.
**Table of Contents:**
.. hlist::
:columns: 2
* **Global Templates**
* :ref:`JOBTMPL/Parameters` * :ref:`JOBTMPL/Parameters`
**Unit Tests, Code Coverage, ...** * **Unit Tests, Code Coverage, Code Quality, ...**
* :ref:`JOBTMPL/UnitTesting` * :ref:`JOBTMPL/UnitTesting`
* :ref:`JOBTMPL/CodeCoverage` * :ref:`JOBTMPL/CodeCoverage`
* :ref:`JOBTMPL/StaticTypeChecking` * :ref:`JOBTMPL/StaticTypeChecking`
* *code formatting (planned)*
* *coding style (planned)*
* *code linting (planned)*
**Build and Packaging** * **Build and Packaging**
* :ref:`JOBTMPL/Package` * :ref:`JOBTMPL/Package`
**Documentation** * **Documentation**
* :ref:`JOBTMPL/VerifyDocumentation` * :ref:`JOBTMPL/VerifyDocumentation`
* :ref:`JOBTMPL/BuildTheDocs` * :ref:`JOBTMPL/BuildTheDocs`
**Publishing** * **Releasing, Publishing**
* :ref:`JOBTMPL/GitHubReleasePage` * :ref:`JOBTMPL/GitHubReleasePage`
* :ref:`JOBTMPL/PyPI` * :ref:`JOBTMPL/PyPI`
* :ref:`JOBTMPL/PublishTestResults` * :ref:`JOBTMPL/PublishTestResults`
* :ref:`JOBTMPL/PublishToGitHubPages` * :ref:`JOBTMPL/PublishToGitHubPages`
**Cleanups** * **Cleanups**
* :ref:`JOBTMPL/ArtifactCleanup` * :ref:`JOBTMPL/ArtifactCleanup`
Instantiation
*************
The job templates (GitHub Action *Reusable Workflows*) need to be stored in the same directory where normal pipelines
(GitHub Action *Workflows*) are located: ``.github/workflows/<template>.yml``. These template files are distinguished
from a normal pipeline by a ``on:workflow_call:`` section compared to an ``on:push`` section.
**Job Template Definition:**
The ``workflow_call`` allows the definition of input and output parameters.
.. code-block:: yaml
on:
workflow_call:
inputs:
<Param1>:
# ...
outputs:
# ...
jobs:
<JobName>:
# ...
**Job Template Instantiation:**
When instantiating a template, a ``jobs:<Name>:uses`` is used to refer to a template file. Unfortunately, besides the
GitHub SLUG (*<Organization>/<Repository>*), also the full path to the template needs to be gives, but still it can't be
outside of ``.github/workflows`` to create a cleaner repository structure. Finally, the path contains a branch name
postfixed by ``@<branch>`` (tags are still not supported by GitHub Actions). A ``jobs:<Name>:with:`` section can be used
to handover input parameters to the template.
.. code-block:: yaml
on:
push:
workflow_dispatch:
jobs:
<InstanceName>:
uses: <GitHubOrganization>/<Repository>/.github/workflows/<Template>.yml@v0
with:
<Param1>: <Value>
Development
***********
.. todo:: JobTemplate:Development Needs documentation

View File

@@ -0,0 +1,50 @@
Repository Structure
####################
pyTooling Actions assumes a certain repository structure and usage of technologies. Besides assumed directory or file
names in default parameters to job templates, almost all can be overwritten if the target repository has a differing
structure.
* Python source code is located in a directory named after the Python package name.
* All tests are located in a ``/tests`` directory and separated by testing approach.
* E.g. unit tests are located in a ``/tests/unit`` directory.
* The package documentation is located in a ``/doc`` directory.
* Documentation is written with ReStructured Text (ReST) and translated using Sphinx.
* Documentation requirements are listed in a ``/doc/requirements.txt``.
* Dependencies are listed in a ``/requirements.txt``.
* If the build process requires separate dependencies, a ``/build/requirements.txt`` is used.
* If the publishing/distribution process requires separate dependencies, a ``/dist/requirements.txt`` is used.
* All Python project settings are stored in a ``pyproject.toml``.
* The Python package is described in a ``setup.py``.
* A repository overview is given in a ``README.md``.
.. code-block::
<Repository>/
.github/
workflows/
Pipeline.yml
dependabot.yml
.vscode/
settings.json
build/
requirements.txt
dist/
requirements.txt
doc/
conf.py
index.rst
requirements.txt
<package>
__init__.py
tests/
unit/
requirements.txt
.editorconfig
.gitignore
LICENSE.md
pyproject.toml
README.md
requirements.txt
setup.py

View File

@@ -38,33 +38,42 @@ This repository gathers reusable CI tooling for testing, packaging and distribut
GitHub Action Job Templates GitHub Action Job Templates
*************************** ***************************
**Global Templates** The following list categorizes all pre-defined job templates, which can be instantiated in a pipeline (GitHub Action
Workflow):
.. hlist::
:columns: 2
* **Global Templates**
* :ref:`JOBTMPL/Parameters` * :ref:`JOBTMPL/Parameters`
**Unit Tests, Code Coverage, ...** * **Unit Tests, Code Coverage, Code Quality, ...**
* :ref:`JOBTMPL/UnitTesting` * :ref:`JOBTMPL/UnitTesting`
* :ref:`JOBTMPL/CodeCoverage` * :ref:`JOBTMPL/CodeCoverage`
* :ref:`JOBTMPL/StaticTypeChecking` * :ref:`JOBTMPL/StaticTypeChecking`
* *code formatting (planned)*
* *coding style (planned)*
* *code linting (planned)*
**Build and Packaging** * **Build and Packaging**
* :ref:`JOBTMPL/Package` * :ref:`JOBTMPL/Package`
**Documentation** * **Documentation**
* :ref:`JOBTMPL/VerifyDocumentation` * :ref:`JOBTMPL/VerifyDocumentation`
* :ref:`JOBTMPL/BuildTheDocs` * :ref:`JOBTMPL/BuildTheDocs`
**Publishing** * **Releasing, Publishing**
* :ref:`JOBTMPL/GitHubReleasePage` * :ref:`JOBTMPL/GitHubReleasePage`
* :ref:`JOBTMPL/PyPI` * :ref:`JOBTMPL/PyPI`
* :ref:`JOBTMPL/PublishTestResults` * :ref:`JOBTMPL/PublishTestResults`
* :ref:`JOBTMPL/PublishToGitHubPages` * :ref:`JOBTMPL/PublishToGitHubPages`
**Cleanups** * **Cleanups**
* :ref:`JOBTMPL/ArtifactCleanup` * :ref:`JOBTMPL/ArtifactCleanup`
@@ -132,6 +141,7 @@ License
:hidden: :hidden:
Background Background
RepositoryStructure
Releases Releases
Dependency Dependency