diff --git a/.github/workflows/test-dotnet-format-results.yml b/.github/workflows/test-dotnet-format-results.yml new file mode 100644 index 0000000..67a7e94 --- /dev/null +++ b/.github/workflows/test-dotnet-format-results.yml @@ -0,0 +1,18 @@ +name: "Test use workflow" +on: [push] +jobs: + defult_test_job: + runs-on: ubuntu-latest + name: "Test basic usage" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: "Uses" + id: output-convert + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: ./ + with: + json-results: '[{"DocumentId":{"ProjectId":{"Id":"f307936d-8b40-4e7c-897c-0f16889c4163"},"Id":"70e32bdf-23e9-4482-a949-65384ce65fce"},"FileName":"WeatherForecastRepository.cs","FilePath":"F:\\Projekty\\crontab-registry\\src\\CrontabRegistry\\Infrastructure\\Repositories\\WeatherForecastRepository.cs","FileChanges":[{"LineNumber":13,"CharNumber":28,"DiagnosticId":"WHITESPACE","FormatDescription":"Napraw formatowanie odstępów. Replace 1 characters with ''\\r\\n\\s\\s\\s\\s\\s\\s\\s\\s''."}]}]' + - name: "Display Results" + run: echo "HTML output is ${{ steps.output-convert.outputs.html-output }} " diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb11945 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.11-bullseye + +COPY docker/entrypoint.sh /entrypoint.sh +WORKDIR /action + +COPY . . + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 37158cd..5fe6f04 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ # dotnet-format-results GitHub Action reads and creates an HTML report based on the .json file generated by the 'dotnet format' command. + +## Inputs + +## `json-results` + +**Required** JSON content generated by `dotnet format` command. Default `[]`. + +## Outputs + +## `html-output` + +Generated HTML code based on JSON input for the results. + +## Example usage + + +``` +uses: x-coders-team/dotnet-format-results@v2 +id: output-convert +with: + json-results: '[{"DocumentId":{"ProjectId":{"Id":"f307936d-8b40-4e7c-897c-0f16889c4163"},"Id":"70e32bdf-23e9-4482-a949-65384ce65fce"},"FileName":"WeatherForecastRepository.cs","FilePath":"F:\\Projekty\\crontab-registry\\src\\CrontabRegistry\\Infrastructure\\Repositories\\WeatherForecastRepository.cs","FileChanges":[{"LineNumber":13,"CharNumber":28,"DiagnosticId":"WHITESPACE","FormatDescription":"Napraw formatowanie odstępów. Replace 1 characters with ''\\r\\n\\s\\s\\s\\s\\s\\s\\s\\s''."}]}]' +``` \ No newline at end of file diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..2474c99 --- /dev/null +++ b/action.yaml @@ -0,0 +1,27 @@ +name: 'dotnet-format-results' +author: 'Rafał Salamon' +description: 'A GitHub Action creates an HTML report based on the .json file generated by the `dotnet format` command.' +branding: + icon: 'check-circle' + color: 'green' + +inputs: + json-results: + description: "JSON content generated by `dotnet format` command" + required: true + default: '[]' + runner-workdir: + description: "Directory path inside runner help to find correct file in repository" + required: false + default: '' + +outputs: + html-output: + description: Generated HTML code based on JSON input for the results. + +runs: + using: 'docker' + image: 'Dockerfile' + args: + - ${{ inputs.json-results }} + - ${{ inputs.runner-workdir }} diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..235128a --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/sh -l + +echo "JSON Input $1" +echo "Runner work directory $2" + +echo "GITHUB_SHA: ${GITHUB_SHA} \n" +echo "GITHUB_REPOSITORY_OWNER: ${GITHUB_REPOSITORY_OWNER} \n" +echo "GITHUB_REPOSITORY: ${GITHUB_REPOSITORY} \n" +echo "GITHUB_HEAD_REF: ${GITHUB_HEAD_REF} \n" +echo "GITHUB_ACTION_REF: ${GITHUB_ACTION_REF} \n" +echo "GITHUB_TOKEN: ${GITHUB_TOKEN} \n" +echo "GITHUB_REF: ${GITHUB_REF} \n" +echo "GITHUB_REF_NAME: ${GITHUB_REF_NAME} \n" +echo "GITHUB_ACTION_REPOSITORY: ${GITHUB_ACTION_REPOSITORY} \n" +echo "GITHUB_ACTION_REF: ${GITHUB_ACTION_REF} \n" +echo "GITHUB_ACTION: ${GITHUB_ACTION} \n" +echo "GITHUB_WORKFLOW_SHA: ${GITHUB_WORKFLOW_SHA} \n" +echo "GITHUB_REF_TYPE: ${GITHUB_REF_TYPE} \n" + + +if pip install githubkit; then + echo "[OK] pip install githubkit" +else + echo "[KO] pip install githubkit" +fi + +gitHubConfig="{\"SHA\": \"${GITHUB_WORKFLOW_SHA}\", \"REPOSITORY_OWNER\": \"${GITHUB_REPOSITORY_OWNER}\", \"REPOSITORY\": \"${GITHUB_REPOSITORY}\"}" + +# debug_output=$(python /action/src/main.py "$1" "$2" "${gitHubConfig}" "1") +# echo "Run With Debug:" +# echo "$debug_output" + +if html_output=$(python /action/src/main.py "$1" "$2" "${gitHubConfig}" "0"); then + echo "Prepare output" + echo "html-output=$html_output" >> $GITHUB_OUTPUT +else + echo "Unable return HTML output" + exit 1 +fi diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/actions/Action.py b/src/actions/Action.py new file mode 100644 index 0000000..9d35b2e --- /dev/null +++ b/src/actions/Action.py @@ -0,0 +1,15 @@ +class Action(): + _app = None + + def __init__(self, app): + self._app = app + pass + + def exec(self): + pass + + def injectService(self, serviceName): + return self._app._di.getService(serviceName, self._app) + + def injectConfigByName(self, configName): + return self._app.getArgumentByName(configName) diff --git a/src/actions/CreateReportAction.py b/src/actions/CreateReportAction.py new file mode 100644 index 0000000..c6ee2f4 --- /dev/null +++ b/src/actions/CreateReportAction.py @@ -0,0 +1,26 @@ +from actions.Action import Action +from pprint import pprint +from inspect import getmembers + +class CreateReportAction(Action): + exampleService = None + fileJsonEncoderService = None + gitHubChecksService = None + + def __init__(self, app): + super().__init__(app) + self.exampleService = self.injectService('ExampleService') + self.fileJsonEncoderService = self.injectService('FileJsonEncoderService') + self.gitHubChecksService = self.injectService('GitHubChecksService') + + async def exec(self): + json_input = self._app.getArgumentByName('json_input') + documentsCollection = self.fileJsonEncoderService.loadDocumentCollestionFromText(json_input) + await self.gitHubChecksService.createNewCheck() + + #pprint(documentsCollection) + + htmlOutput = self.exampleService.createSampleText(json_input) + + print(htmlOutput) + pass diff --git a/src/actions/__init__.py b/src/actions/__init__.py new file mode 100644 index 0000000..4bebda7 --- /dev/null +++ b/src/actions/__init__.py @@ -0,0 +1 @@ +from actions.CreateReportAction import CreateReportAction \ No newline at end of file diff --git a/src/app/Application.py b/src/app/Application.py new file mode 100644 index 0000000..7eeb2dc --- /dev/null +++ b/src/app/Application.py @@ -0,0 +1,40 @@ +from models import Argument +from actions import * + +class Application(): + _appName = '' + _args = [] + _di = None + _action_module = 'actions' + _service_module = 'services' + + def __init__(self, appName = '', args = [], di = {}): + self._appName = appName + self._args = args + self._di = di + pass + + @staticmethod + def startUp(argv, entrypointAction = 'index'): + pass + + @staticmethod + def createArgumentsCollection(arg, collection): + collection.append(arg) + return collection + + @staticmethod + def createArgument(argName, argValue): + return Argument(argName, argValue) + + def getArgumentByName(self, argName): + for arg in self._args: + if arg.getName() == argName: + return arg.getValue() + + return None + + def runAction(self, actionName): + module = __import__(self._action_module) + actionClass = getattr(module, actionName) + return actionClass(self).exec() diff --git a/src/app/DepedencyInjection.py b/src/app/DepedencyInjection.py new file mode 100644 index 0000000..cd59ad7 --- /dev/null +++ b/src/app/DepedencyInjection.py @@ -0,0 +1,30 @@ +class DepedencyInjection(): + _di_services = [] + _service_module = 'services' + + def __init__(self, containers): + self._di_services = containers + pass + + def registryService(self, serviceName, app): + if (len(self._di_services) == 0): + self._di_services.append(dict()) + + if self.isServiceExists(serviceName) == True: + return + + module = __import__(self._service_module) + serviceClass = getattr(module, serviceName) + self._di_services[0][serviceName] = serviceClass(self, app) + pass + + def isServiceExists(self, serviceName): + if len(self._di_services) > 0: + if serviceName in self._di_services[0]: + return True + + return False + + def getService(self, serviceName, app): + self.registryService(serviceName, app) + return self._di_services[0][serviceName] diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..94338d3 --- /dev/null +++ b/src/app/__init__.py @@ -0,0 +1,2 @@ +from app.Application import Application +from app.DepedencyInjection import DepedencyInjection \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..431b84f --- /dev/null +++ b/src/main.py @@ -0,0 +1,11 @@ +import sys +from myApp import MyApp; +from pprint import pprint +from inspect import getmembers +import asyncio + +# from githubkit import GitHub, ActionAuthStrategy +# github = GitHub(ActionAuthStrategy()) + +containers = [] +asyncio.run(MyApp.startUp(sys.argv, containers, 'CreateReportAction')) \ No newline at end of file diff --git a/src/models/Argument.py b/src/models/Argument.py new file mode 100644 index 0000000..630f210 --- /dev/null +++ b/src/models/Argument.py @@ -0,0 +1,12 @@ +class Argument(): + def __init__(self, name, value): + self._name = name + self._value = value + pass + + def getName(self): + return self._name + + def getValue(self): + return self._value + \ No newline at end of file diff --git a/src/models/DotnetFormatChange.py b/src/models/DotnetFormatChange.py new file mode 100644 index 0000000..eb7032b --- /dev/null +++ b/src/models/DotnetFormatChange.py @@ -0,0 +1,30 @@ +class DotnetFormatChange(): + _lineNumber = '' + _charNumber = '' + _diagnosticId = '' + _formatDescription = '' + + def __init__( + self, + lineNumber, + charNumber, + diagnosticId, + formatDescription + ): + self._lineNumber = lineNumber + self._charNumber = charNumber + self._diagnosticId = diagnosticId + self._formatDescription = formatDescription + pass + + def getLineNumber(self): + return self._lineNumber + + def getCharNumber(self): + return self._charNumber + + def getDiagnosticId(self): + return self._diagnosticId + + def getFormatDescription(self): + return self._formatDescription diff --git a/src/models/DotnetFormatDocument.py b/src/models/DotnetFormatDocument.py new file mode 100644 index 0000000..b69fcf6 --- /dev/null +++ b/src/models/DotnetFormatDocument.py @@ -0,0 +1,30 @@ +class DotnetFormatDocument(): + _documentId = None + _fileName = '' + _filePath = '' + _fileChanges = [] + + def __init__( + self, + documentId, + fileName, + filePath, + fileChanges = [] + ): + self._documentId = documentId + self._fileName = fileName + self._filePath = filePath + self._fileChanges = fileChanges + pass + + def getDocumentId(self): + return self._documentId + + def getFileName(self): + return self._fileName + + def getFilePath(self): + return self._filePath + + def getFileChanges(self): + return self._fileChanges diff --git a/src/models/DotnetFormatDocumentId.py b/src/models/DotnetFormatDocumentId.py new file mode 100644 index 0000000..3d2f27a --- /dev/null +++ b/src/models/DotnetFormatDocumentId.py @@ -0,0 +1,14 @@ +class DotnetFormatDocumentId(): + _projectId = None + _id = '' + + def __init__(self, projectId, id): + self._projectId = projectId + self._id = id + pass + + def getProjectId(self): + return self._projectId + + def getId(self): + return self._id diff --git a/src/models/DotnetFormatProjectId.py b/src/models/DotnetFormatProjectId.py new file mode 100644 index 0000000..311cba5 --- /dev/null +++ b/src/models/DotnetFormatProjectId.py @@ -0,0 +1,9 @@ +class DotnetFormatProjectId(): + _id = '' + + def __init__(self, id): + self._id = id + pass + + def getId(self): + return self._id diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..bcdaaaf --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1,5 @@ +from models.Argument import Argument +from models.DotnetFormatDocument import DotnetFormatDocument +from models.DotnetFormatDocumentId import DotnetFormatDocumentId +from models.DotnetFormatProjectId import DotnetFormatProjectId +from models.DotnetFormatChange import DotnetFormatChange \ No newline at end of file diff --git a/src/myApp.py b/src/myApp.py new file mode 100644 index 0000000..1ad7d50 --- /dev/null +++ b/src/myApp.py @@ -0,0 +1,55 @@ + +from app import Application +from app import DepedencyInjection +import json + +class MyApp(Application): + def __init__(self,appName = '', args=[], services={}): + super().__init__(appName, args, services) + + @staticmethod + async def startUp(argv, containers, entrypointAction = 'index'): + + argumentsCollection = MyApp.prepareArgumentsCollection(argv) + + di = DepedencyInjection(containers); + + app = MyApp('dotnet-format-results', argumentsCollection, di) + await app.runAction(entrypointAction) + pass + + @staticmethod + def prepareArgumentsCollection(argv): + argumentsCollection = [] + argumentsCollection = MyApp.createArgumentsCollection( + arg = MyApp.createArgument('json_input', argv[1]), + collection = argumentsCollection + ) + + argumentsCollection = MyApp.createArgumentsCollection( + arg = MyApp.createArgument('runner_workdir', argv[2]), + collection = argumentsCollection + ) + + argumentsCollection = MyApp.createArgumentsCollection( + arg = MyApp.createArgument('github_config', MyApp.readJsonConfig(argv[3])), + collection = argumentsCollection + ) + + argumentsCollection = MyApp.createArgumentsCollection( + arg = MyApp.createArgument('debug', MyApp.getBooleanValueArgument(argv[4])), + collection = argumentsCollection + ) + + return argumentsCollection + + @staticmethod + def readJsonConfig(rawText): + return json.loads(rawText) + + @staticmethod + def getBooleanValueArgument(value): + if value == '0': + return False + else: + return True diff --git a/src/services/ExampleService.py b/src/services/ExampleService.py new file mode 100644 index 0000000..059da73 --- /dev/null +++ b/src/services/ExampleService.py @@ -0,0 +1,8 @@ +from services.ServiceAbstract import ServiceAbstract + +class ExampleService(ServiceAbstract): + def __init__(self, di = None, app = None): + super().__init__(di, app) + + def createSampleText(self, jsonInput): + return f"Testing from Python

Testing workflow

{jsonInput}
" diff --git a/src/services/FileJsonEncoderService.py b/src/services/FileJsonEncoderService.py new file mode 100644 index 0000000..8d62597 --- /dev/null +++ b/src/services/FileJsonEncoderService.py @@ -0,0 +1,24 @@ +from services.ServiceAbstract import ServiceAbstract +from models import DotnetFormatChange, DotnetFormatProjectId, DotnetFormatDocumentId, DotnetFormatDocument +import json +from pprint import pprint +from inspect import getmembers + +class FileJsonEncoderService(ServiceAbstract): + mapperService = None + + def __init__(self, di = None, app = None): + super().__init__(di, app) + self.mapperService = self.injectService('MapperDotnetFormatService') + + def loadDocumentCollestionFromText(self, documentCollestionText): + documentsCollection = [] + rawData = json.loads(documentCollestionText) + + #pprint(rawData); + + documentsCollection = self.mapperService.mapDocumentsCollectionFromJson(rawData) + + return documentsCollection + + \ No newline at end of file diff --git a/src/services/GitHubChecksService.py b/src/services/GitHubChecksService.py new file mode 100644 index 0000000..8296b48 --- /dev/null +++ b/src/services/GitHubChecksService.py @@ -0,0 +1,55 @@ +from services.ServiceAbstract import ServiceAbstract +from githubkit import GitHub, ActionAuthStrategy, Response +from pprint import pprint +from inspect import getmembers + +class GitHubChecksService(ServiceAbstract): + _github = None + _gitHubConfig = None + _checkName = ".NET Format Results [beta]" + _externalId = "action-dotnet-format-results" + _debug = False + + def __init__(self, di = None, app = None): + """_summary_ + + Args: + di (DepedencyInjection, optional): _description_. Defaults to None. + """ + + super().__init__(di, app) + self._github = GitHub(ActionAuthStrategy()) + self._gitHubConfig = self.injectConfigByName('github_config') + self._debug = self.injectConfigByName('debug') + + async def createNewCheck(self): + repositoryMetadata = self._getRepositoryName(self._gitHubConfig['REPOSITORY']) + + resp = await self._github.rest.checks.async_create( + head_sha = self._gitHubConfig['SHA'], + owner = repositoryMetadata['owner'], + repo = repositoryMetadata['repository'], + name = self._checkName, + status = 'completed', + conclusion = 'success', + output = { + "title": self._checkName, + "summary": "summary OK", + "text": "text OK" + } + ) + + if self._debug: + pprint(resp) + + pass + + def _getRepositoryName(self, ownerAndRepositorySetting): + splitData = ownerAndRepositorySetting.split('/') + + return { + "owner": splitData[0], + "repository": splitData[1], + "full": ownerAndRepositorySetting + } + \ No newline at end of file diff --git a/src/services/MapperDotnetFormatService.py b/src/services/MapperDotnetFormatService.py new file mode 100644 index 0000000..08c7df7 --- /dev/null +++ b/src/services/MapperDotnetFormatService.py @@ -0,0 +1,120 @@ +from services.ServiceAbstract import ServiceAbstract +from models import DotnetFormatChange, DotnetFormatProjectId, DotnetFormatDocumentId, DotnetFormatDocument +from pprint import pprint +from inspect import getmembers + +class MapperDotnetFormatService(ServiceAbstract): + def __init__(self, di = None, app = None): + """The MapperDotnetFormatService class is responsible for mapping raw data transformed from a JSON string + into a list of objects in the MapperDotnetFormatService format. This service encapsulates the logic for transforming + JSON data into a specific format defined by the MapperDotnetFormatService, providing a convenient and reusable way + to process and work with data in a standardized manner. + + Args: + di (DepedencyInjection, optional): _description_. Defaults to None. + """ + + super().__init__(di, app) + + def mapDocumentsCollectionFromJson(self, rawData): + """Maps a collection of documents from raw JSON data to an array of objects in the DotnetFormatDocument format. + + Args: + rawData (List): An array returned from an external source in JSON format containing document data. + + Returns: + List: An array of objects representing the transformed documents in the DotnetFormatDocument format. + """ + + documentsCollection = [] + + for rawDocument in rawData: + documentsCollection.append(self._createDocument(rawDocument)) + + return documentsCollection + + def _createDocument(self, rawDocument): + """Private function that creates and returns an array of DotnetFormatDocument objects + from an associative array retrieved from an external source in JSON format. + + Args: + rawDocument (dict): An associative array containing raw document data in JSON format. + + Returns: + DotnetFormatDocument: objects representing the transformed documents in the DotnetFormatDocument format. + """ + + documentId = self._createDocumentId(rawDocument['DocumentId']) + + return DotnetFormatDocument( + documentId = documentId, + fileName = rawDocument['FileName'], + filePath = rawDocument['FilePath'], + fileChanges = self._createChangesCollection(rawDocument['FileChanges']) + ) + + def _createDocumentId(self, rawDocumentId): + """Private function that creates and returns a DotnetFormatDocumentId object + from an associative array retrieved from an external source in JSON format. + + Args: + rawDocumentId (Dict): An associative array containing raw document ID data in JSON format. + + Returns: + DotnetFormatDocumentId: An object representing the transformed document ID in the DotnetFormatDocumentId format. + """ + + return DotnetFormatDocumentId( + projectId = self._createProjectId(rawDocumentId['ProjectId']), + id = rawDocumentId['Id'] + ) + + def _createProjectId(self, rawProjectId): + """Private function that creates and returns a DotnetFormatProjectId object + from an associative array retrieved from an external source in JSON format. + + Args: + rawProjectId (Dict): An associative array containing raw project ID data in JSON format. + + Returns: + DotnetFormatProjectId: An object representing the transformed project ID in the DotnetFormatProjectId format. + """ + + return DotnetFormatProjectId(id = rawProjectId['Id']) + + def _createChangesCollection(self, rawFileChanges): + """Private function that creates and returns an array of DotnetFormatChange objects + from an array of associative arrays retrieved from an external source in JSON format. + + Args: + rawFileChanges (List[Dict[str, Any]]): An array of associative arrays containing raw file change data in JSON format. + + Returns: + List: An array of objects representing the transformed file changes in the DotnetFormatChange format. + """ + + changesCollection = [] + + for rawFileChange in rawFileChanges: + changesCollection.append(self._createFileChange(rawFileChange)) + + return changesCollection + + def _createFileChange(self, rawFileChange): + """Private function that creates and returns a DotnetFormatChange object + from an associative array retrieved from an external source in JSON format. + + Args: + rawFileChange (Dict): An associative array containing raw file change data in JSON format. + + Returns: + DotnetFormatChange: An object representing the transformed file change in the DotnetFormatChange format. + """ + + return DotnetFormatChange( + lineNumber = rawFileChange['LineNumber'], + charNumber = rawFileChange['CharNumber'], + diagnosticId = rawFileChange['DiagnosticId'], + formatDescription = rawFileChange['FormatDescription'] + ) + \ No newline at end of file diff --git a/src/services/ServiceAbstract.py b/src/services/ServiceAbstract.py new file mode 100644 index 0000000..bb86a83 --- /dev/null +++ b/src/services/ServiceAbstract.py @@ -0,0 +1,15 @@ +class ServiceAbstract(): + _di = None + _app = None + + def __init__(self, di = None, app = None): + self._di = di + self._app = app + pass + + def injectService(self, serviceName): + return self._di.getService(serviceName, self._app) + + def injectConfigByName(self, configName): + return self._app.getArgumentByName(configName) + diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..c4a9be3 --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1,4 @@ +from services.ExampleService import ExampleService +from services.FileJsonEncoderService import FileJsonEncoderService +from services.MapperDotnetFormatService import MapperDotnetFormatService +from services.GitHubChecksService import GitHubChecksService \ No newline at end of file