primitive 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {primitive-0.2.0 → primitive-0.2.2}/PKG-INFO +1 -1
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/__about__.py +1 -1
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/agent/actions.py +24 -48
- primitive-0.2.2/src/primitive/agent/runner.py +315 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/cli.py +1 -1
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/git/actions.py +10 -1
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/actions.py +35 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/graphql/fragments.py +3 -0
- {primitive-0.2.0 → primitive-0.2.2}/uv.lock +59 -54
- primitive-0.2.0/src/primitive/agent/process.py +0 -125
- primitive-0.2.0/src/primitive/agent/provision.py +0 -52
- primitive-0.2.0/src/primitive/agent/runner.py +0 -282
- primitive-0.2.0/src/primitive/utils/files.py +0 -26
- primitive-0.2.0/src/primitive/utils/git.py +0 -15
- primitive-0.2.0/src/primitive/utils/verible.py +0 -57
- {primitive-0.2.0 → primitive-0.2.2}/.git-hooks/pre-commit +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/.gitattributes +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/.github/workflows/lint.yml +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/.github/workflows/publish.yml +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/.gitignore +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/.vscode/settings.json +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/LICENSE.txt +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/Makefile +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/README.md +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/linux setup.md +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/pyproject.toml +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/agent/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/agent/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/agent/uploader.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/auth/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/auth/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/auth/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/auth/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/auth/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/client.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/daemons/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/daemons/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/daemons/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/daemons/launch_agents.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/daemons/launch_service.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/exec/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/exec/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/exec/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/exec/interactive.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/graphql/fragments.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/graphql/mutations.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/files/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/git/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/git/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/git/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/git/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/graphql/relay.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/graphql/sdk.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/graphql/utility_fragments.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/android.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/graphql/fragments.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/graphql/mutations.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/hardware/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/graphql/mutations.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/jobs/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/graphql/fragments.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/graphql/mutations.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/organizations/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/graphql/fragments.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/graphql/mutations.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/projects/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/provisioning/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/provisioning/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/provisioning/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/provisioning/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/commands.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/graphql/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/graphql/fragments.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/graphql/mutations.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/reservations/graphql/queries.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/__init__.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/actions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/auth.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/cache.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/chunk_size.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/config.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/exceptions.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/memory_size.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/printer.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/shell.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/src/primitive/utils/text.py +0 -0
- {primitive-0.2.0 → primitive-0.2.2}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: primitive
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
|
5
5
|
Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
|
6
6
|
Project-URL: Source, https://github.com//primitivecorp/primitive-cli
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import shutil
|
2
1
|
import sys
|
3
2
|
from time import sleep
|
4
3
|
|
@@ -7,9 +6,8 @@ from loguru import logger
|
|
7
6
|
from primitive.__about__ import __version__
|
8
7
|
from primitive.utils.actions import BaseAction
|
9
8
|
|
10
|
-
from ..utils.cache import get_sources_cache
|
11
9
|
from ..utils.exceptions import P_CLI_100
|
12
|
-
from .runner import
|
10
|
+
from .runner import Runner
|
13
11
|
from .uploader import Uploader
|
14
12
|
|
15
13
|
|
@@ -28,9 +26,6 @@ class Agent(BaseAction):
|
|
28
26
|
logger.info(" [*] primitive")
|
29
27
|
logger.info(f" [*] Version: {__version__}")
|
30
28
|
|
31
|
-
# Create cache dir if it doesn't exist
|
32
|
-
cache_dir = get_sources_cache()
|
33
|
-
|
34
29
|
# Create uploader
|
35
30
|
uploader = Uploader(primitive=self.primitive)
|
36
31
|
|
@@ -51,12 +46,12 @@ class Agent(BaseAction):
|
|
51
46
|
active_reservation_pk = None
|
52
47
|
|
53
48
|
while True:
|
54
|
-
logger.debug("Syncing children...")
|
55
|
-
self.primitive.hardware._sync_children()
|
56
|
-
|
57
49
|
logger.debug("Scanning for files to upload...")
|
58
50
|
uploader.scan()
|
59
51
|
|
52
|
+
logger.debug("Syncing children...")
|
53
|
+
self.primitive.hardware._sync_children()
|
54
|
+
|
60
55
|
hardware = self.primitive.hardware.get_own_hardware_details()
|
61
56
|
|
62
57
|
if hardware["activeReservation"]:
|
@@ -125,59 +120,40 @@ class Agent(BaseAction):
|
|
125
120
|
logger.debug(f"Job Run ID: {job_run['id']}")
|
126
121
|
logger.debug(f"Job Name: {job_run['job']['name']}")
|
127
122
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
)
|
133
|
-
|
134
|
-
github_access_token = (
|
135
|
-
self.primitive.jobs.github_access_token_for_job_run(
|
136
|
-
job_run["id"]
|
137
|
-
)
|
123
|
+
runner = Runner(
|
124
|
+
primitive=self.primitive,
|
125
|
+
job_run=job_run,
|
126
|
+
max_log_size=500 * 1024,
|
138
127
|
)
|
139
128
|
|
140
129
|
try:
|
141
|
-
|
142
|
-
self.primitive.git.download_git_repository_at_ref(
|
143
|
-
git_repo_full_name=git_repo_full_name,
|
144
|
-
git_ref=git_ref,
|
145
|
-
github_access_token=github_access_token,
|
146
|
-
destination=cache_dir,
|
147
|
-
)
|
148
|
-
)
|
130
|
+
runner.setup()
|
149
131
|
except Exception as exception:
|
150
|
-
logger.
|
132
|
+
logger.exception(
|
133
|
+
f"Exception while initializing runner: {exception}"
|
134
|
+
)
|
151
135
|
self.primitive.jobs.job_run_update(
|
152
|
-
job_run["id"],
|
136
|
+
id=job_run["id"],
|
153
137
|
status="request_completed",
|
154
138
|
conclusion="failure",
|
155
139
|
)
|
156
140
|
continue
|
157
141
|
|
158
|
-
source_dir = downloaded_git_repository_dir.joinpath(
|
159
|
-
job_run["jobSettings"]["rootDirectory"]
|
160
|
-
)
|
161
|
-
|
162
142
|
try:
|
163
|
-
|
164
|
-
runner = AgentRunner(
|
165
|
-
primitive=self.primitive,
|
166
|
-
source_dir=source_dir,
|
167
|
-
job_run=job_run,
|
168
|
-
max_log_size=500 * 1024,
|
169
|
-
)
|
143
|
+
runner.execute()
|
170
144
|
except Exception as exception:
|
171
|
-
|
172
|
-
|
173
|
-
|
145
|
+
logger.exception(f"Exception while executing job: {exception}")
|
146
|
+
self.primitive.jobs.job_run_update(
|
147
|
+
id=job_run["id"],
|
148
|
+
status="request_completed",
|
149
|
+
conclusion="failure",
|
174
150
|
)
|
175
|
-
else:
|
176
|
-
# Execute job
|
177
|
-
runner.execute()
|
178
151
|
finally:
|
179
|
-
|
180
|
-
|
152
|
+
runner.cleanup()
|
153
|
+
|
154
|
+
# NOTE: also run scan here to force upload of artifacts
|
155
|
+
# This should probably eventuall be another daemon?
|
156
|
+
uploader.scan()
|
181
157
|
|
182
158
|
sleep(5)
|
183
159
|
except KeyboardInterrupt:
|
@@ -0,0 +1,315 @@
|
|
1
|
+
import os
|
2
|
+
import typing
|
3
|
+
import re
|
4
|
+
import shutil
|
5
|
+
from typing import Dict, TypedDict, List
|
6
|
+
from abc import abstractmethod
|
7
|
+
from enum import IntEnum, Enum
|
8
|
+
from pathlib import Path, PurePath
|
9
|
+
from loguru import logger
|
10
|
+
import yaml
|
11
|
+
import asyncio
|
12
|
+
from primitive.utils.shell import env_string_to_dict
|
13
|
+
from ..utils.cache import get_artifacts_cache, get_sources_cache, get_logs_cache
|
14
|
+
|
15
|
+
try:
|
16
|
+
from yaml import CLoader as Loader
|
17
|
+
except ImportError:
|
18
|
+
from yaml import Loader
|
19
|
+
|
20
|
+
if typing.TYPE_CHECKING:
|
21
|
+
import primitive.client
|
22
|
+
|
23
|
+
ENV_VAR_LOOKUP_START = "_ENV_VAR_LOOKUP_START"
|
24
|
+
ENV_VAR_LOOKUP_END = "_ENV_VAR_LOOKUP_END"
|
25
|
+
|
26
|
+
|
27
|
+
class Task(TypedDict):
|
28
|
+
label: str
|
29
|
+
workdir: str
|
30
|
+
tags: Dict
|
31
|
+
cmd: str
|
32
|
+
|
33
|
+
|
34
|
+
class JobConfig(TypedDict):
|
35
|
+
requires: List[str]
|
36
|
+
executes: List[Task]
|
37
|
+
stores: List[str]
|
38
|
+
|
39
|
+
|
40
|
+
# NOTE This must match FailureLevel subclass in JobSettings model
|
41
|
+
class FailureLevel(IntEnum):
|
42
|
+
ERROR = 1
|
43
|
+
WARNING = 2
|
44
|
+
|
45
|
+
|
46
|
+
class LogLevel(Enum):
|
47
|
+
INFO = "INFO"
|
48
|
+
ERROR = "ERROR"
|
49
|
+
WARNING = "WARNING"
|
50
|
+
|
51
|
+
|
52
|
+
class Runner:
|
53
|
+
def __init__(
|
54
|
+
self,
|
55
|
+
primitive: "primitive.client.Primitive",
|
56
|
+
job_run: Dict,
|
57
|
+
max_log_size: int = 10 * 1024 * 1024, # 10MB
|
58
|
+
) -> None:
|
59
|
+
self.primitive = primitive
|
60
|
+
self.job = job_run["job"]
|
61
|
+
self.job_run = job_run
|
62
|
+
self.job_settings = job_run["jobSettings"]
|
63
|
+
self.config = None
|
64
|
+
self.source_dir = None
|
65
|
+
self.initial_env = {}
|
66
|
+
self.modified_env = {}
|
67
|
+
self.file_logger = None
|
68
|
+
|
69
|
+
logger.enable("primitive")
|
70
|
+
|
71
|
+
# If max_log_size set to <= 0, disable file logging
|
72
|
+
if max_log_size > 0:
|
73
|
+
log_name = f"{self.job['slug']}_{self.job_run['jobRunNumber']}_{{time}}.primitive.log"
|
74
|
+
|
75
|
+
self.file_logger = logger.add(
|
76
|
+
Path(get_logs_cache(self.job_run["id"]) / log_name),
|
77
|
+
rotation=max_log_size,
|
78
|
+
format=Runner.fmt,
|
79
|
+
backtrace=True,
|
80
|
+
diagnose=True,
|
81
|
+
)
|
82
|
+
|
83
|
+
def setup(self) -> None:
|
84
|
+
# Attempt to download the job source code
|
85
|
+
git_repo_full_name = self.job_run["gitCommit"]["repoFullName"]
|
86
|
+
git_ref = self.job_run["gitCommit"]["sha"]
|
87
|
+
logger.info(f"Downloading repository {git_repo_full_name} at ref {git_ref}")
|
88
|
+
|
89
|
+
github_access_token = self.primitive.jobs.github_access_token_for_job_run(
|
90
|
+
self.job_run["id"]
|
91
|
+
)
|
92
|
+
|
93
|
+
downloaded_git_repository_dir = (
|
94
|
+
self.primitive.git.download_git_repository_at_ref(
|
95
|
+
git_repo_full_name=git_repo_full_name,
|
96
|
+
git_ref=git_ref,
|
97
|
+
github_access_token=github_access_token,
|
98
|
+
destination=get_sources_cache(),
|
99
|
+
)
|
100
|
+
)
|
101
|
+
|
102
|
+
self.source_dir = downloaded_git_repository_dir.joinpath(
|
103
|
+
self.job_settings["rootDirectory"]
|
104
|
+
)
|
105
|
+
|
106
|
+
# Attempt to parse the job yaml file
|
107
|
+
logger.info(f"Scanning directory for job file {self.job['slug']}")
|
108
|
+
yaml_file = Path(self.source_dir / ".primitive" / f"{self.job['slug']}.yaml")
|
109
|
+
yml_file = Path(self.source_dir / ".primitive" / f"{self.job['slug']}.yml")
|
110
|
+
|
111
|
+
if yaml_file.exists() and yml_file.exists():
|
112
|
+
logger.error(
|
113
|
+
f"Found two job descriptions with the same slug: {self.job['slug']}"
|
114
|
+
)
|
115
|
+
raise FileExistsError
|
116
|
+
|
117
|
+
if yaml_file.exists() or yml_file.exists():
|
118
|
+
logger.info(f"Found job description for {self.job['slug']}")
|
119
|
+
config_file = yaml_file if yaml_file.exists() else yml_file
|
120
|
+
self.config = yaml.load(open(config_file, "r"), Loader=Loader)[
|
121
|
+
self.job["name"]
|
122
|
+
]
|
123
|
+
else:
|
124
|
+
logger.error(
|
125
|
+
f"No job description with matching slug '{self.job['slug']}' found"
|
126
|
+
)
|
127
|
+
raise FileNotFoundError
|
128
|
+
|
129
|
+
# Setup initial process environment
|
130
|
+
self.initial_env = os.environ
|
131
|
+
self.initial_env["PRIMITIVE_GIT_SHA"] = str(self.job_run["gitCommit"]["sha"])
|
132
|
+
self.initial_env["PRIMITIVE_GIT_BRANCH"] = str(
|
133
|
+
self.job_run["gitCommit"]["branch"]
|
134
|
+
)
|
135
|
+
self.initial_env["PRIMITIVE_GIT_REPO"] = str(
|
136
|
+
self.job_run["gitCommit"]["repoFullName"]
|
137
|
+
)
|
138
|
+
|
139
|
+
def execute(self) -> None:
|
140
|
+
logger.info(f"Executing {self.job['slug']} job")
|
141
|
+
self.primitive.jobs.job_run_update(
|
142
|
+
self.job_run["id"], status="request_in_progress"
|
143
|
+
)
|
144
|
+
self.modified_env = {**self.initial_env}
|
145
|
+
|
146
|
+
task_failed = False
|
147
|
+
for task in self.config["executes"]:
|
148
|
+
with logger.contextualize(label=task["label"]):
|
149
|
+
with asyncio.Runner() as async_runner:
|
150
|
+
if task_failed := async_runner.run(self.run_task(task)):
|
151
|
+
break
|
152
|
+
|
153
|
+
if not task_failed:
|
154
|
+
self.primitive.jobs.job_run_update(
|
155
|
+
self.job_run["id"], status="request_completed", conclusion="success"
|
156
|
+
)
|
157
|
+
logger.success(f"Completed {self.job['slug']} job")
|
158
|
+
|
159
|
+
async def run_task(self, task: Task) -> bool:
|
160
|
+
for cmd in task["cmd"].strip().split("\n"):
|
161
|
+
args = [
|
162
|
+
"/bin/bash",
|
163
|
+
"-c",
|
164
|
+
f"{cmd} && echo '{ENV_VAR_LOOKUP_START}' && env && echo '{ENV_VAR_LOOKUP_END}'",
|
165
|
+
]
|
166
|
+
|
167
|
+
process = await asyncio.create_subprocess_exec(
|
168
|
+
*args,
|
169
|
+
env=self.modified_env,
|
170
|
+
cwd=str(Path(self.source_dir / task["workdir"])),
|
171
|
+
stdout=asyncio.subprocess.PIPE,
|
172
|
+
stderr=asyncio.subprocess.PIPE,
|
173
|
+
)
|
174
|
+
|
175
|
+
stdout_failed, stderr_failed, cancelled = await asyncio.gather(
|
176
|
+
self.log_cmd(
|
177
|
+
process=process, stream=process.stdout, tags=task.get("tags", {})
|
178
|
+
),
|
179
|
+
self.log_cmd(
|
180
|
+
process=process, stream=process.stderr, tags=task.get("tags", {})
|
181
|
+
),
|
182
|
+
asyncio.create_task(self.monitor_cmd(process=process)),
|
183
|
+
)
|
184
|
+
|
185
|
+
returncode = await process.wait()
|
186
|
+
|
187
|
+
if cancelled:
|
188
|
+
return True
|
189
|
+
|
190
|
+
if returncode > 0 or stdout_failed or stderr_failed:
|
191
|
+
await self.primitive.jobs.ajob_run_update(
|
192
|
+
self.job_run["id"], status="request_completed", conclusion="failure"
|
193
|
+
)
|
194
|
+
|
195
|
+
if returncode > 0:
|
196
|
+
logger.error(
|
197
|
+
f"Task {task['label']} failed on '{cmd}' with return code {returncode}"
|
198
|
+
)
|
199
|
+
else:
|
200
|
+
logger.error(f"Task {task['label']} failed on '{cmd}'")
|
201
|
+
|
202
|
+
return True
|
203
|
+
|
204
|
+
return False
|
205
|
+
|
206
|
+
async def log_cmd(self, process, stream, tags: Dict = {}) -> bool:
|
207
|
+
failure_detected = False
|
208
|
+
while line := await stream.readline():
|
209
|
+
raw_data = line.decode()
|
210
|
+
|
211
|
+
# handle env vars
|
212
|
+
if ENV_VAR_LOOKUP_START in raw_data:
|
213
|
+
env_vars_string = ""
|
214
|
+
while env_line := await stream.readline():
|
215
|
+
if ENV_VAR_LOOKUP_END in env_line.decode():
|
216
|
+
break
|
217
|
+
env_vars_string += env_line.decode()
|
218
|
+
|
219
|
+
self.modified_env = env_string_to_dict(env_vars_string)
|
220
|
+
continue
|
221
|
+
|
222
|
+
# Handle logging
|
223
|
+
parse_logs = self.job_settings["parseLogs"]
|
224
|
+
parse_stderr = self.job_settings["parseStderr"]
|
225
|
+
|
226
|
+
level = LogLevel.INFO
|
227
|
+
tag = None
|
228
|
+
if (parse_logs and "error" in raw_data.lower()) or (
|
229
|
+
parse_stderr and stream is process.stderr
|
230
|
+
):
|
231
|
+
level = LogLevel.ERROR
|
232
|
+
elif parse_logs and "warning" in raw_data.lower():
|
233
|
+
level = LogLevel.WARNING
|
234
|
+
|
235
|
+
failure_detected = (
|
236
|
+
level == LogLevel.ERROR
|
237
|
+
and self.job_settings["failureLevel"] >= FailureLevel.ERROR
|
238
|
+
) or (
|
239
|
+
level == LogLevel.WARNING
|
240
|
+
and self.job_settings["failureLevel"] >= FailureLevel.WARNING
|
241
|
+
)
|
242
|
+
|
243
|
+
# Tag on the first matching regex in the list
|
244
|
+
for tag_key, regex in tags.items():
|
245
|
+
pattern = re.compile(regex)
|
246
|
+
if pattern.match(raw_data):
|
247
|
+
tag = tag_key
|
248
|
+
break
|
249
|
+
|
250
|
+
logger.bind(tag=tag).log(level.value, raw_data.rstrip())
|
251
|
+
|
252
|
+
return failure_detected
|
253
|
+
|
254
|
+
async def monitor_cmd(self, process) -> bool:
|
255
|
+
while process.returncode is None:
|
256
|
+
status = await self.primitive.jobs.aget_job_status(self.job_run["id"])
|
257
|
+
status_value = status.data["jobRun"]["status"]
|
258
|
+
conclusion_value = status.data["jobRun"]["conclusion"]
|
259
|
+
|
260
|
+
if status_value == "completed" and conclusion_value == "cancelled":
|
261
|
+
logger.warning("Job cancelled by user")
|
262
|
+
try:
|
263
|
+
process.terminate()
|
264
|
+
except ProcessLookupError:
|
265
|
+
pass
|
266
|
+
|
267
|
+
return True
|
268
|
+
|
269
|
+
await asyncio.sleep(5)
|
270
|
+
|
271
|
+
return False
|
272
|
+
|
273
|
+
def cleanup(self) -> None:
|
274
|
+
logger.remove(self.file_logger)
|
275
|
+
|
276
|
+
if "stores" not in self.config:
|
277
|
+
return
|
278
|
+
|
279
|
+
for glob in self.config["stores"]:
|
280
|
+
path = Path(glob)
|
281
|
+
|
282
|
+
if path.is_dir():
|
283
|
+
files = [str(f) for f in path.rglob("*") if f.is_file()]
|
284
|
+
else:
|
285
|
+
files = [str(f) for f in Path().glob(glob) if f.is_file()]
|
286
|
+
|
287
|
+
for file in files:
|
288
|
+
relative_path = PurePath(file).relative_to(self.source_dir)
|
289
|
+
destination = Path(
|
290
|
+
get_artifacts_cache(self.job_run["id"]) / relative_path
|
291
|
+
)
|
292
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
293
|
+
Path(file).replace(destination)
|
294
|
+
|
295
|
+
shutil.rmtree(path=self.source_dir)
|
296
|
+
|
297
|
+
@abstractmethod
|
298
|
+
def fmt(record) -> str:
|
299
|
+
extra = record["extra"]
|
300
|
+
# Delimiters with empty space MUST exist for LogQL pattern matching
|
301
|
+
label = extra.get("label", None)
|
302
|
+
tag = extra.get("tag", None)
|
303
|
+
context = f"{label} | " if label else " | "
|
304
|
+
context += f"{tag} | " if tag else " | "
|
305
|
+
|
306
|
+
log = (
|
307
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS!UTC}</green> | "
|
308
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
309
|
+
"<level>{level}</level> | "
|
310
|
+
f"{context}"
|
311
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
312
|
+
"<level>{message}</level>\n"
|
313
|
+
)
|
314
|
+
|
315
|
+
return log
|
@@ -48,7 +48,7 @@ from .reservations.commands import cli as reservations_commands
|
|
48
48
|
@click.version_option(__version__)
|
49
49
|
@click.pass_context
|
50
50
|
def cli(context, host, yes, debug, json, verbose):
|
51
|
-
"""primitive - a CLI tool for https://primitive.
|
51
|
+
"""primitive - a CLI tool for https://primitive.tech"""
|
52
52
|
context.ensure_object(dict)
|
53
53
|
context.obj["YES"] = yes
|
54
54
|
context.obj["DEBUG"] = debug
|
@@ -38,8 +38,17 @@ class Git(BaseAction):
|
|
38
38
|
)
|
39
39
|
source_dir = Path(destination).joinpath(git_repo_full_name.split("/")[-1])
|
40
40
|
|
41
|
+
# Clone with throw an exception if the directory already exists
|
42
|
+
if source_dir.exists():
|
43
|
+
shutil.rmtree(path=source_dir)
|
44
|
+
|
41
45
|
try:
|
42
|
-
run(
|
46
|
+
run(
|
47
|
+
["git", "clone", url, source_dir, "--no-checkout"],
|
48
|
+
check=True,
|
49
|
+
stdout=DEVNULL,
|
50
|
+
stderr=DEVNULL,
|
51
|
+
)
|
43
52
|
except CalledProcessError:
|
44
53
|
raise Exception("Failed to download repository")
|
45
54
|
|
@@ -128,6 +128,30 @@ class Jobs(BaseAction):
|
|
128
128
|
)
|
129
129
|
return result
|
130
130
|
|
131
|
+
@guard
|
132
|
+
async def ajob_run_update(
|
133
|
+
self,
|
134
|
+
id: str,
|
135
|
+
status: str = None,
|
136
|
+
conclusion: str = None,
|
137
|
+
file_ids: Optional[List[str]] = [],
|
138
|
+
):
|
139
|
+
mutation = gql(job_run_update_mutation)
|
140
|
+
input = {"id": id}
|
141
|
+
if status:
|
142
|
+
input["status"] = status
|
143
|
+
if conclusion:
|
144
|
+
input["conclusion"] = conclusion
|
145
|
+
if file_ids and len(file_ids) > 0:
|
146
|
+
input["files"] = file_ids
|
147
|
+
variables = {"input": input}
|
148
|
+
|
149
|
+
async with self.primitive.session as session:
|
150
|
+
result = await session.execute(
|
151
|
+
mutation, variable_values=variables, get_execution_result=True
|
152
|
+
)
|
153
|
+
return result
|
154
|
+
|
131
155
|
@guard
|
132
156
|
def github_access_token_for_job_run(self, job_run_id: str):
|
133
157
|
query = gql(github_app_token_for_job_run_query)
|
@@ -160,3 +184,14 @@ class Jobs(BaseAction):
|
|
160
184
|
query, variable_values=variables, get_execution_result=True
|
161
185
|
)
|
162
186
|
return result
|
187
|
+
|
188
|
+
@guard
|
189
|
+
async def aget_job_status(self, id: str):
|
190
|
+
query = gql(job_run_status_query)
|
191
|
+
variables = {"id": id}
|
192
|
+
|
193
|
+
async with self.primitive.session as session:
|
194
|
+
result = await session.execute(
|
195
|
+
query, variable_values=variables, get_execution_result=True
|
196
|
+
)
|
197
|
+
return result
|
@@ -13,6 +13,7 @@ job_run_fragment = """
|
|
13
13
|
fragment JobRunFragment on JobRun {
|
14
14
|
id
|
15
15
|
pk
|
16
|
+
jobRunNumber
|
16
17
|
createdAt
|
17
18
|
updatedAt
|
18
19
|
completedAt
|
@@ -31,6 +32,7 @@ fragment JobRunFragment on JobRun {
|
|
31
32
|
containerArgs
|
32
33
|
rootDirectory
|
33
34
|
parseLogs
|
35
|
+
parseStderr
|
34
36
|
failureLevel
|
35
37
|
}
|
36
38
|
gitCommit {
|
@@ -45,5 +47,6 @@ job_run_status_fragment = """
|
|
45
47
|
fragment JobRunStatusFragment on JobRun {
|
46
48
|
id
|
47
49
|
status
|
50
|
+
conclusion
|
48
51
|
}
|
49
52
|
"""
|