primitive 0.2.1__tar.gz → 0.2.3__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.
Files changed (110) hide show
  1. {primitive-0.2.1 → primitive-0.2.3}/PKG-INFO +1 -1
  2. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/__about__.py +1 -1
  3. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/agent/actions.py +23 -58
  4. primitive-0.2.3/src/primitive/agent/runner.py +315 -0
  5. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/cli.py +1 -1
  6. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/git/actions.py +13 -4
  7. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/actions.py +35 -0
  8. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/graphql/fragments.py +3 -0
  9. {primitive-0.2.1 → primitive-0.2.3}/uv.lock +59 -54
  10. primitive-0.2.1/src/primitive/agent/process.py +0 -125
  11. primitive-0.2.1/src/primitive/agent/provision.py +0 -52
  12. primitive-0.2.1/src/primitive/agent/runner.py +0 -283
  13. primitive-0.2.1/src/primitive/utils/files.py +0 -26
  14. primitive-0.2.1/src/primitive/utils/git.py +0 -15
  15. primitive-0.2.1/src/primitive/utils/verible.py +0 -57
  16. {primitive-0.2.1 → primitive-0.2.3}/.git-hooks/pre-commit +0 -0
  17. {primitive-0.2.1 → primitive-0.2.3}/.gitattributes +0 -0
  18. {primitive-0.2.1 → primitive-0.2.3}/.github/workflows/lint.yml +0 -0
  19. {primitive-0.2.1 → primitive-0.2.3}/.github/workflows/publish.yml +0 -0
  20. {primitive-0.2.1 → primitive-0.2.3}/.gitignore +0 -0
  21. {primitive-0.2.1 → primitive-0.2.3}/.vscode/settings.json +0 -0
  22. {primitive-0.2.1 → primitive-0.2.3}/LICENSE.txt +0 -0
  23. {primitive-0.2.1 → primitive-0.2.3}/Makefile +0 -0
  24. {primitive-0.2.1 → primitive-0.2.3}/README.md +0 -0
  25. {primitive-0.2.1 → primitive-0.2.3}/linux setup.md +0 -0
  26. {primitive-0.2.1 → primitive-0.2.3}/pyproject.toml +0 -0
  27. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/__init__.py +0 -0
  28. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/agent/__init__.py +0 -0
  29. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/agent/commands.py +0 -0
  30. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/agent/uploader.py +0 -0
  31. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/auth/__init__.py +0 -0
  32. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/auth/actions.py +0 -0
  33. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/auth/commands.py +0 -0
  34. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/auth/graphql/__init__.py +0 -0
  35. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/auth/graphql/queries.py +0 -0
  36. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/client.py +0 -0
  37. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/daemons/__init__.py +0 -0
  38. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/daemons/actions.py +0 -0
  39. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/daemons/commands.py +0 -0
  40. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/daemons/launch_agents.py +0 -0
  41. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/daemons/launch_service.py +0 -0
  42. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/exec/__init__.py +0 -0
  43. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/exec/actions.py +0 -0
  44. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/exec/commands.py +0 -0
  45. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/exec/interactive.py +0 -0
  46. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/__init__.py +0 -0
  47. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/actions.py +0 -0
  48. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/commands.py +0 -0
  49. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/graphql/__init__.py +0 -0
  50. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/graphql/fragments.py +0 -0
  51. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/graphql/mutations.py +0 -0
  52. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/files/graphql/queries.py +0 -0
  53. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/git/__init__.py +0 -0
  54. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/git/commands.py +0 -0
  55. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/git/graphql/__init__.py +0 -0
  56. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/git/graphql/queries.py +0 -0
  57. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/graphql/__init__.py +0 -0
  58. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/graphql/relay.py +0 -0
  59. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/graphql/sdk.py +0 -0
  60. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/graphql/utility_fragments.py +0 -0
  61. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/__init__.py +0 -0
  62. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/actions.py +0 -0
  63. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/android.py +0 -0
  64. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/commands.py +0 -0
  65. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/graphql/__init__.py +0 -0
  66. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/graphql/fragments.py +0 -0
  67. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/graphql/mutations.py +0 -0
  68. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/hardware/graphql/queries.py +0 -0
  69. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/__init__.py +0 -0
  70. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/commands.py +0 -0
  71. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/graphql/__init__.py +0 -0
  72. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/graphql/mutations.py +0 -0
  73. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/jobs/graphql/queries.py +0 -0
  74. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/__init__.py +0 -0
  75. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/actions.py +0 -0
  76. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/commands.py +0 -0
  77. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/graphql/__init__.py +0 -0
  78. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/graphql/fragments.py +0 -0
  79. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/graphql/mutations.py +0 -0
  80. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/organizations/graphql/queries.py +0 -0
  81. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/__init__.py +0 -0
  82. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/actions.py +0 -0
  83. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/commands.py +0 -0
  84. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/graphql/__init__.py +0 -0
  85. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/graphql/fragments.py +0 -0
  86. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/graphql/mutations.py +0 -0
  87. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/projects/graphql/queries.py +0 -0
  88. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/provisioning/__init__.py +0 -0
  89. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/provisioning/actions.py +0 -0
  90. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/provisioning/graphql/__init__.py +0 -0
  91. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/provisioning/graphql/queries.py +0 -0
  92. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/__init__.py +0 -0
  93. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/actions.py +0 -0
  94. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/commands.py +0 -0
  95. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/graphql/__init__.py +0 -0
  96. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/graphql/fragments.py +0 -0
  97. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/graphql/mutations.py +0 -0
  98. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/reservations/graphql/queries.py +0 -0
  99. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/__init__.py +0 -0
  100. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/actions.py +0 -0
  101. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/auth.py +0 -0
  102. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/cache.py +0 -0
  103. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/chunk_size.py +0 -0
  104. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/config.py +0 -0
  105. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/exceptions.py +0 -0
  106. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/memory_size.py +0 -0
  107. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/printer.py +0 -0
  108. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/shell.py +0 -0
  109. {primitive-0.2.1 → primitive-0.2.3}/src/primitive/utils/text.py +0 -0
  110. {primitive-0.2.1 → primitive-0.2.3}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.1
3
+ Version: 0.2.3
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,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.1"
4
+ __version__ = "0.2.3"
@@ -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 AgentRunner
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,29 +120,18 @@ 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
- git_repo_full_name = job_run["gitCommit"]["repoFullName"]
129
- git_ref = job_run["gitCommit"]["sha"]
130
- logger.debug(
131
- f"Downloading repository {git_repo_full_name} at ref {git_ref}"
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
- downloaded_git_repository_dir = (
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.error(f"Error downloading source: {exception}")
132
+ logger.exception(
133
+ f"Exception while initializing runner: {exception}"
134
+ )
151
135
  self.primitive.jobs.job_run_update(
152
136
  id=job_run["id"],
153
137
  status="request_completed",
@@ -155,40 +139,21 @@ class Agent(BaseAction):
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
- # Initialize Runner
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
- # Log Error
172
- logger.exception(
173
- f"Error initializing agent runner: {exception}"
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
- try:
177
- # Execute job
178
- runner.execute()
179
- except Exception as exception:
180
- # Log Error
181
- logger.exception(
182
- f"AgentRunner exception while running executing customer job: {exception}"
183
- )
184
- self.primitive.jobs.job_run_update(
185
- id=job_run["id"],
186
- status="request_completed",
187
- conclusion="failure",
188
- )
189
151
  finally:
190
- # Clean up
191
- shutil.rmtree(path=downloaded_git_repository_dir)
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()
192
157
 
193
158
  sleep(5)
194
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.get("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.design"""
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
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
  import shutil
3
- from subprocess import run, CalledProcessError, DEVNULL
3
+ from subprocess import run, CalledProcessError
4
4
 
5
5
  from gql import gql
6
6
  from loguru import logger
@@ -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(["git", "clone", url, source_dir, "--no-checkout"], check=True)
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
 
@@ -48,8 +57,8 @@ class Git(BaseAction):
48
57
  ["git", "checkout", git_ref],
49
58
  check=True,
50
59
  cwd=source_dir,
51
- stdout=DEVNULL,
52
- stderr=DEVNULL,
60
+ # stdout=DEVNULL,
61
+ # stderr=DEVNULL,
53
62
  )
54
63
  except CalledProcessError:
55
64
  # Clean up directory if checkout failed
@@ -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
  """