-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3fecc1f
Showing
5 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |