Skip to content

Commit

Permalink
initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
kamil-olszewski-devskiller committed Dec 13, 2024
0 parents commit 3fecc1f
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM python:3.13-slim
COPY entrypoint.py /entrypoint.py
RUN pip install "requests>=2.32.3"
ENTRYPOINT [ "python3", "/entrypoint.py" ]
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# DevSkiller TalentScore - Upload task action

This GitHub Action uploads custom programming tasks to the DevSkiller TalentScore platform. It allows you to easily integrate the upload process into your CI/CD pipeline.

## Inputs

### `api_key`
**Required**: The TalentScore API key. This key is needed to authenticate the upload request.

### `path`
**Required**: The path to the programming task directory that contains the task source code to upload.

### `id`
**Optional**: The ID of the programming task you want to update on the TalentScore platform. If not provided, the code task directory should contain a [metadata.yaml file](metadata-file-structure.md).

### `publish`
**Optional**: If set to `true` (default), the task will be published automatically after a successful build.

## Example

```yaml
name: Sample task upload

on:
push:
branches:
- master

jobs:
upload-task:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: Devskiller/[email protected]
with:
api_key: ${{ secrets.TALENTSCORE_API_KEY }}
id: fe3217a6-e085-47dd-afff-025be5355d87
path: ./src
```
## How to prepare a custom programming task
Please see: [Creating custom tasks](https://help.devskiller.com/space/TSG/2893873179/Creating+Custom+Tasks)
25 changes: 25 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Upload the TalentScore task
description: Upload your custom tasks to the DevSkiller TalentScore platform
author: DevSkiller
branding:
icon: upload-cloud
color: blue

inputs:
api_key:
description: "TalentScore API key"
required: true
id:
description: "The ID of the programming task to upload"
required: true
path:
description: "The path of the programming task directory to upload"
required: true
publish:
description: "Whether to publish the task if it builds successfully"
required: false
default: 'true'

runs:
using: docker
image: Dockerfile
120 changes: 120 additions & 0 deletions entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import os
import shutil
import requests
import time

TASKS_API = os.environ.get("PLATFORM_URL", "https://api.devskiller.com") + "/tasks"
BUILD_CHECK_MAX_ATTEMPTS = 36

input_api_key = os.environ["INPUT_API_KEY"]
input_id = os.environ["INPUT_ID"]
input_path = os.environ["INPUT_PATH"]
input_publish = os.environ["INPUT_PUBLISH"]

def validate_input_path():
if not os.path.isdir(input_path):
print("::error::PATH parameter is not a directory")
exit(1)

def create_zip_archive():
shutil.make_archive("task", "zip", input_path)

def upload_task():
print(f"::info::Uploading the task")
with open("task.zip", "rb") as f:
upload_url = f"{TASKS_API}/{input_id}/zip" if input_id else f"{TASKS_API}/zip"

upload_response = requests.put(
upload_url,
data=f.read(),
headers={"Content-Type": "application/zip", "Devskiller-Api-Key": input_api_key},
)

if upload_response.status_code != 202:
print(f"::error::Upload failed with status code: {upload_response.status_code}, response: {upload_response.text}")
exit(1)

return upload_response.json().get('taskId')

def check_build_status(taskId):
print(f"::info::Checking the build status for task with id: {taskId}")
for _ in range(BUILD_CHECK_MAX_ATTEMPTS):
status_response = requests.get(f"{TASKS_API}/{taskId}", headers={"Devskiller-Api-Key": input_api_key})
if status_response.status_code == 200:
status_data = status_response.json()
if status_data.get('type') == 'CODE_REVIEW':
print("::info::Code review task detected therefore skipping the build check")
return False, status_data

if status_data.get('buildStatus') is not None:
print(f"::info::Build status is: {status_data.get('buildStatus')}")
if status_data.get('buildLog'):
print("Build log:")
print(status_data.get('buildLog'))
return True, status_data
time.sleep(10)
else:
print(f"::error::Build status check failed with status code: {status_response.status_code}, response: {status_response.text}")
exit(1)

print("::error::Timeout waiting for build status")
exit(1)

def format_summary_test_section(section_name, test_data):
markdown = f"\n\n### {section_name}\n"
if len(test_data) == 0:
markdown += "No tests.\n"
else:
for test_suite, tests in test_data.items():
markdown += f"- {test_suite}:\n"
for test_name, test_result in tests.items():
markdown += f" - {test_name}: **{'✅' if test_result == 'PASS' else '❌'}**\n"

return markdown

def write_summary(status_data):
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as f:
markdown = f"## Build Status: {status_data.get('buildStatus')} {'✅' if status_data.get('buildStatus') == 'TEST_FAILURE' else '❌'}"
markdown += format_summary_test_section("Candidate Tests", status_data.get('candidateTests', {}))
markdown += format_summary_test_section("Verification Tests", status_data.get('verificationTests', {}))

if status_data.get('buildLog'):
markdown += "\n### Build log:\n"
markdown += "```\n"
markdown += status_data.get('buildLog', '')
markdown += "\n```\n"
f.write(markdown)

def verify_build_status(status_data):
if status_data.get('buildStatus') != 'TEST_FAILURE':
print("::error::The initial state of the task should be compiling, but with broken tests")
exit(1)

if not status_data.get('verificationTests', {}):
print("::error::You are missing the verification tests")
exit(1)

def publish_task(taskId):
print(f"::info::Publishing the task with ID: {taskId}")
response = requests.post(
f"{TASKS_API}/{taskId}/publish",
headers={"Devskiller-Api-Key": input_api_key},
)

if response.status_code != 204:
print(f"::error::Publication failed with status code: {response.status_code}, response: {response.text}")
exit(1)

def main():
validate_input_path()
create_zip_archive()
taskId = upload_task()
build_executed, status_data = check_build_status(taskId)
if build_executed:
write_summary(status_data)
verify_build_status(status_data)
if input_publish:
publish_task(taskId)

if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions metadata-file-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Metadata file structure
The file stores programming task details in the form of YAML key-value pairs, with the following fields:

- **uuid**: ID of the task. With it you will be able to update tasks. You can use the existing one or generate an unique value using [generators](https://www.uuidgenerator.net/)
- **title**: title of a task
- **question**: question which will be presented to the candidate
- **type**: `PROGRAMMING` or `CODE_REVIEW`
- **difficulty**: difficulty level, one of `EASY`, `MEDIUM`, `HARD`
- **tags**: task tags, ie. what kind of knowledge this questions tests
- **points**: default number of points for this task
- **duration**: default time duration in the ISO 8601 duration format
- **projectType**: technology associated with the task, e.g.: `CMAKE`, `NODEJS`, `MAVEN`
- **projectTypeVersion**: specific version of the technology, e.g.: `NODEJS16`, `GCC11`

## Example

```yaml
uuid: aee4ae94-5709-4917-8dde-fa54b91f84cf
title: Java | Sample Calculator task
question: |-
Your task is to create a simple calculator capable of adding, subtracting, multiplying and dividing two numbers.
type: PROGRAMMING
difficulty: EASY
tags: ['C++', 'Basics']
points: 20
duration: PT1H
projectType: CMAKE
projectTypeVersion: GCC11
```

0 comments on commit 3fecc1f

Please sign in to comment.