spaceforge 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl

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.
spaceforge/__init__.py CHANGED
@@ -4,10 +4,18 @@ Spaceforge - Spacelift Plugin Framework
4
4
  A Python framework for building Spacelift plugins with hook-based functionality.
5
5
  """
6
6
 
7
- from ._version import get_version
8
- from .cls import Binary, Context, MountedFile, Parameter, Policy, Variable, Webhook
9
- from .plugin import SpaceforgePlugin
10
- from .runner import PluginRunner
7
+ from spaceforge._version import get_version
8
+ from spaceforge.cls import (
9
+ Binary,
10
+ Context,
11
+ MountedFile,
12
+ Parameter,
13
+ Policy,
14
+ Variable,
15
+ Webhook,
16
+ )
17
+ from spaceforge.plugin import SpaceforgePlugin
18
+ from spaceforge.runner import PluginRunner
11
19
 
12
20
  __version__ = get_version()
13
21
  __all__ = [
spaceforge/__main__.py CHANGED
@@ -4,9 +4,9 @@ Main entry point for spaceforge module.
4
4
 
5
5
  import click
6
6
 
7
- from ._version import get_version
8
- from .generator import generate_command
9
- from .runner import runner_command
7
+ from spaceforge._version import get_version
8
+ from spaceforge.generator import generate_command
9
+ from spaceforge.runner import runner_command
10
10
 
11
11
 
12
12
  @click.group()
spaceforge/_version.py CHANGED
@@ -3,7 +3,6 @@ Dynamic version detection from git tags.
3
3
  """
4
4
 
5
5
  import subprocess
6
- import sys
7
6
  from typing import Optional
8
7
 
9
8
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.2'
32
- __version_tuple__ = version_tuple = (0, 0, 2)
31
+ __version__ = version = '0.0.3'
32
+ __version_tuple__ = version_tuple = (0, 0, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
spaceforge/cls.py CHANGED
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from typing import Dict, List, Literal, Optional
2
3
 
3
4
  from pydantic import Field
@@ -19,8 +20,7 @@ class Binary:
19
20
 
20
21
  Attributes:
21
22
  name (str): The name of the binary file.
22
- path (str): The path to the binary file.
23
- sensitive (bool): Whether the binary file is sensitive.
23
+ download_urls (Dict[BinaryType, str]): A dictionary mapping binary types to their download URLs.
24
24
  """
25
25
 
26
26
  name: str
@@ -38,6 +38,7 @@ class Parameter:
38
38
  sensitive (bool): Whether the parameter contains sensitive information.
39
39
  required (bool): Whether the parameter is required.
40
40
  default (Optional[str]): The default value of the parameter, if any. (required if sensitive is False)
41
+ id (str): Unique identifier for the parameter.
41
42
  """
42
43
 
43
44
  name: str
@@ -45,6 +46,7 @@ class Parameter:
45
46
  sensitive: bool = False
46
47
  required: bool = False
47
48
  default: Optional[str] = None
49
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
48
50
 
49
51
  def __post_init__(self) -> None:
50
52
  if not self.required and self.default is None:
@@ -171,19 +173,21 @@ class PluginManifest:
171
173
  A class to represent the manifest of a Spacelift plugin.
172
174
 
173
175
  Attributes:
174
- name_prefix (str): The name of the plugin, will be appended with a unique ID.
176
+ name (str): The name of the plugin, will be appended with a unique ID.
175
177
  description (str): A description of the plugin.
176
178
  author (str): The author of the plugin.
179
+ labels (list[str]): List of labels for the plugin.
177
180
  parameters (list[Parameter]): List of parameters for the plugin.
178
181
  contexts (list[Context]): List of contexts for the plugin.
179
182
  webhooks (list[Webhook]): List of webhooks for the plugin.
180
183
  policies (list[Policy]): List of policies for the plugin.
181
184
  """
182
185
 
183
- name_prefix: str
186
+ name: str
184
187
  version: str
185
188
  description: str
186
189
  author: str
190
+ labels: Optional[List[str]] = optional_field
187
191
  parameters: Optional[List[Parameter]] = optional_field
188
192
  contexts: Optional[List[Context]] = optional_field
189
193
  webhooks: Optional[List[Webhook]] = optional_field
spaceforge/conftest.py ADDED
@@ -0,0 +1,89 @@
1
+ """Shared test fixtures for spaceforge tests."""
2
+
3
+ import os
4
+ import tempfile
5
+ from typing import Dict, Generator, List
6
+ from unittest.mock import Mock
7
+
8
+ import pytest
9
+
10
+ from spaceforge.plugin import SpaceforgePlugin
11
+
12
+
13
+ @pytest.fixture
14
+ def temp_dir() -> Generator[str, None, None]:
15
+ """Provide a temporary directory for test files."""
16
+ temp_dir = tempfile.mkdtemp()
17
+ yield temp_dir
18
+ import shutil
19
+
20
+ shutil.rmtree(temp_dir, ignore_errors=True)
21
+
22
+
23
+ @pytest.fixture
24
+ def test_plugin_content() -> str:
25
+ """Basic test plugin content."""
26
+ return """
27
+ from spaceforge import SpaceforgePlugin, Parameter
28
+
29
+ class TestPlugin(SpaceforgePlugin):
30
+ __plugin_name__ = "test"
31
+ __version__ = "1.0.0"
32
+ __author__ = "Test Author"
33
+
34
+ __parameters__ = [
35
+ Parameter(
36
+ name="test_param",
37
+ description="Test parameter",
38
+ required=False,
39
+ default="default_value"
40
+ )
41
+ ]
42
+
43
+ def after_plan(self) -> None:
44
+ pass
45
+ """
46
+
47
+
48
+ @pytest.fixture
49
+ def test_plugin_file(temp_dir: str, test_plugin_content: str) -> str:
50
+ """Create a test plugin file."""
51
+ plugin_path = os.path.join(temp_dir, "plugin.py")
52
+ with open(plugin_path, "w") as f:
53
+ f.write(test_plugin_content)
54
+ return plugin_path
55
+
56
+
57
+ @pytest.fixture
58
+ def mock_env() -> Dict[str, str]:
59
+ """Common test environment variables."""
60
+ return {
61
+ "SPACELIFT_API_TOKEN": "test_token",
62
+ "TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io",
63
+ }
64
+
65
+
66
+ @pytest.fixture
67
+ def mock_api_response() -> Mock:
68
+ """Mock API response for testing."""
69
+ mock_response = Mock()
70
+ mock_response.read.return_value = b'{"data": {"test": "result"}}'
71
+ return mock_response
72
+
73
+
74
+ class ExampleTestPlugin(SpaceforgePlugin):
75
+ """Reusable test plugin class."""
76
+
77
+ __plugin_name__ = "example"
78
+ __version__ = "1.0.0"
79
+ __author__ = "Test"
80
+
81
+ def __init__(self) -> None:
82
+ super().__init__()
83
+ self.hook_calls: List[str] = []
84
+
85
+ def after_plan(self) -> None:
86
+ self.hook_calls.append("after_plan")
87
+
88
+ def before_apply(self) -> None:
89
+ self.hook_calls.append("before_apply")
spaceforge/generator.py CHANGED
@@ -4,7 +4,7 @@ YAML generator for Spacelift plugins.
4
4
 
5
5
  import importlib.util
6
6
  import os
7
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
8
8
 
9
9
  import yaml
10
10
 
@@ -46,6 +46,7 @@ class PluginGenerator:
46
46
  self.plugin_class: Optional[Type[SpaceforgePlugin]] = None
47
47
  self.plugin_instance: Optional[SpaceforgePlugin] = None
48
48
  self.plugin_working_directory: Optional[str] = None
49
+ self.config: Optional[Dict[str, Any]] = None
49
50
 
50
51
  def load_plugin(self) -> None:
51
52
  """Load the plugin class from the specified path."""
@@ -80,8 +81,15 @@ class PluginGenerator:
80
81
  self.plugin_working_directory = (
81
82
  "/mnt/workspace/plugins/" + plugin_class.__plugin_name__.lower()
82
83
  )
84
+ self.config = {
85
+ "setup_virtual_env": (
86
+ f"cd {self.plugin_working_directory} && python -m venv ./venv && "
87
+ + "source venv/bin/activate && pip install spaceforge"
88
+ ),
89
+ "plugin_mounted_path": f"{self.plugin_working_directory}/{os.path.basename(self.plugin_path)}",
90
+ }
83
91
 
84
- def get_plugin_metadata(self) -> Dict[str, str]:
92
+ def get_plugin_metadata(self) -> Dict[str, Union[str, List[str]]]:
85
93
  """Extract metadata from the plugin class."""
86
94
  if self.plugin_class is None:
87
95
  raise ValueError("Plugin class not loaded. Call load_plugin() first.")
@@ -97,6 +105,7 @@ class PluginGenerator:
97
105
  self.plugin_class.__name__.lower().replace("plugin", ""),
98
106
  ),
99
107
  "version": getattr(self.plugin_class, "__version__", "1.0.0"),
108
+ "labels": getattr(self.plugin_class, "__labels__", []),
100
109
  "description": doc
101
110
  or f"A Spacelift plugin built with {self.plugin_class.__name__}",
102
111
  "author": getattr(self.plugin_class, "__author__", "Unknown"),
@@ -128,22 +137,14 @@ class PluginGenerator:
128
137
 
129
138
  return hook_methods
130
139
 
131
- def get_plugin_contexts(self) -> List[Context]:
132
- """Get context definitions from the plugin class."""
133
-
134
- change_to_working_directory = f"cd {self.plugin_working_directory}"
135
- hooks = {
136
- "before_init": [
137
- f"mkdir -p {self.plugin_working_directory}",
138
- ],
139
- }
140
- mounted_files = []
141
-
142
- # Add a virtual environment before_init hook if requirements.txt exists
143
- if os.path.exists("requirements.txt"):
144
- hooks["before_init"].append(
145
- f"{change_to_working_directory} && python -m venv ./venv && source venv/bin/activate && pip install -r requirements.txt"
146
- )
140
+ def _update_with_requirements(
141
+ self, hooks: Dict[str, List[str]], mounted_files: List[MountedFile]
142
+ ) -> None:
143
+ """Update the plugin hooks if there is a requirements.txt"""
144
+ if os.path.exists("requirements.txt") and self.config is not None:
145
+ if self.config["setup_virtual_env"] not in hooks["before_init"]:
146
+ hooks["before_init"].append(self.config["setup_virtual_env"])
147
+ hooks["before_init"].append(f"pip install -r requirements.txt")
147
148
  # read the requirements.txt file
148
149
  with open("requirements.txt", "r") as f:
149
150
  mounted_files.append(
@@ -154,34 +155,77 @@ class PluginGenerator:
154
155
  )
155
156
  )
156
157
 
157
- # Ensure the plugin file itself is mounted
158
- plugin_mounted_path = (
159
- f"{self.plugin_working_directory}/{os.path.basename(self.plugin_path)}"
160
- )
161
- if os.path.exists(self.plugin_path):
158
+ def _update_with_python_file(self, mounted_files: List[MountedFile]) -> None:
159
+ """Ensure the plugin file itself is mounted."""
160
+ if os.path.exists(self.plugin_path) and self.config is not None:
162
161
  with open(self.plugin_path, "r") as f:
163
162
  mounted_files.append(
164
163
  MountedFile(
165
- path=plugin_mounted_path,
164
+ path=self.config["plugin_mounted_path"],
166
165
  content=f.read(),
167
166
  sensitive=False,
168
167
  )
169
168
  )
170
169
 
171
- binary_cmd = self.generate_binary_install_command()
172
- if binary_cmd != "":
173
- hooks["before_init"].append(binary_cmd)
174
-
170
+ def _add_spaceforge_hooks(self, hooks: Dict[str, List[str]]) -> None:
175
171
  # Add the spaceforge hook to actually run the plugin
172
+ if self.config is None:
173
+ raise ValueError("Plugin config not set. Call load_plugin() first.")
174
+
176
175
  available_hooks = self.get_available_hooks()
177
176
  for hook in available_hooks:
178
177
  # Ensure the hook exists in the first context
179
178
  if hook not in hooks:
180
179
  hooks[hook] = []
180
+
181
+ if self.config["setup_virtual_env"] not in hooks[hook]:
182
+ hooks[hook].append(self.config["setup_virtual_env"])
183
+
181
184
  hooks[hook].append(
182
- f"{change_to_working_directory} && python -m spaceforge runner --plugin-file {plugin_mounted_path} {hook}"
185
+ f"cd /mnt/workspace/source/$TF_VAR_spacelift_project_root && python -m spaceforge runner --plugin-file {self.config['plugin_mounted_path']} {hook}"
183
186
  )
184
187
 
188
+ def _map_variables_to_parameters(self, contexts: List[Context]) -> None:
189
+ for context in contexts:
190
+ # Get the variables from the plugin and change the value_from_parameter to the ID of the parameter
191
+ # based on its name.
192
+ if context.env is None:
193
+ continue
194
+
195
+ for variable in context.env:
196
+ if variable.value_from_parameter:
197
+ parameter_name = variable.value_from_parameter
198
+ parameters = self.get_plugin_parameters()
199
+ if parameters:
200
+ parameter = next(
201
+ (
202
+ p
203
+ for p in parameters
204
+ if p.name == parameter_name or p.id == parameter_name
205
+ ),
206
+ None,
207
+ )
208
+ if parameter:
209
+ variable.value_from_parameter = parameter.id
210
+ else:
211
+ raise ValueError(
212
+ f"Parameter {parameter_name} not found for variable {variable.key}"
213
+ )
214
+
215
+ def get_plugin_contexts(self) -> List[Context]:
216
+ """Get context definitions from the plugin class."""
217
+
218
+ f"cd {self.plugin_working_directory}"
219
+ hooks: Dict[str, List[str]] = {
220
+ "before_init": [f"mkdir -p {self.plugin_working_directory}"]
221
+ }
222
+ mounted_files: List[MountedFile] = []
223
+
224
+ self._update_with_requirements(hooks, mounted_files)
225
+ self._update_with_python_file(mounted_files)
226
+ self._generate_binary_install_command(hooks)
227
+ self._add_spaceforge_hooks(hooks)
228
+
185
229
  # Get the contexts and append the hooks and mounted files to it.
186
230
  if self.plugin_class is None:
187
231
  raise ValueError("Plugin class not loaded. Call load_plugin() first.")
@@ -208,12 +252,14 @@ class PluginGenerator:
208
252
  contexts[0].hooks.update(hooks)
209
253
  contexts[0].mounted_files.extend(mounted_files)
210
254
 
255
+ self._map_variables_to_parameters(contexts)
256
+
211
257
  return contexts
212
258
 
213
- def generate_binary_install_command(self) -> str:
259
+ def _generate_binary_install_command(self, hooks: Dict[str, List[str]]) -> None:
214
260
  binaries = self.get_plugin_binaries()
215
261
  if binaries is None:
216
- return ""
262
+ return None
217
263
 
218
264
  binary_cmd = ""
219
265
  if len(binaries) > 0:
@@ -238,12 +284,17 @@ class PluginGenerator:
238
284
  else "echo 'arm64 binary not available' && exit 1"
239
285
  )
240
286
 
241
- binary_cmd += '([[ "$(echo "$(arch)")" == "x86_64" ]] && {} || {})'.format(
242
- amd64_download_command, arm64_download_command
287
+ binary_cmd += (
288
+ '([[ "$(echo "$(arch)")" == "x86_64" ]] && {} || {}) && '.format(
289
+ amd64_download_command, arm64_download_command
290
+ )
243
291
  )
244
- if i < len(binaries) - 1:
245
- binary_cmd += " && "
246
- return binary_cmd
292
+
293
+ if binary_cmd != "":
294
+ binary_cmd += "cd /mnt/workspace/source/$TF_VAR_spacelift_project_root"
295
+
296
+ hooks["before_init"].append(binary_cmd)
297
+ return None
247
298
 
248
299
  def get_plugin_binaries(self) -> Optional[List[Binary]]:
249
300
  """Get binary definitions from the plugin class."""
@@ -265,10 +316,11 @@ class PluginGenerator:
265
316
  metadata = self.get_plugin_metadata()
266
317
 
267
318
  return PluginManifest(
268
- name_prefix=metadata.get("name_prefix", "unknown"),
269
- version=metadata.get("version", "1.0.0"),
270
- description=metadata.get("description", ""),
271
- author=metadata.get("author", "Unknown"),
319
+ name=str(metadata.get("name_prefix", "unknown")),
320
+ version=str(metadata.get("version", "1.0.0")),
321
+ description=str(metadata.get("description", "")),
322
+ author=str(metadata.get("author", "Unknown")),
323
+ labels=list(metadata.get("labels", [])),
272
324
  parameters=self.get_plugin_parameters(),
273
325
  contexts=self.get_plugin_contexts(),
274
326
  webhooks=self.get_plugin_webhooks(),
spaceforge/plugin.py CHANGED
@@ -9,7 +9,7 @@ import os
9
9
  import subprocess
10
10
  import urllib.request
11
11
  from abc import ABC
12
- from typing import Any, Dict, List, Optional, Tuple, Union
12
+ from typing import Any, Dict, List, Optional, Tuple
13
13
 
14
14
 
15
15
  class SpaceforgePlugin(ABC):
@@ -31,14 +31,17 @@ class SpaceforgePlugin(ABC):
31
31
  __author__ = "Spacelift Team"
32
32
 
33
33
  def __init__(self) -> None:
34
+ self._run_id = os.environ.get("TF_VAR_spacelift_run_id", "local")
35
+ self._is_local = self._run_id == "local"
34
36
  self.logger = self._setup_logger()
35
37
 
36
- self._api_token = os.environ.get("SPACELIFT_API_TOKEN", False)
37
- self._spacelift_domain = os.environ.get(
38
- "TF_VAR_spacelift_graphql_endpoint", False
38
+ self._api_token = os.environ.get("SPACELIFT_API_TOKEN") or False
39
+ self._spacelift_domain = (
40
+ os.environ.get("TF_VAR_spacelift_graphql_endpoint") or False
39
41
  )
40
- self._api_enabled = self._api_token != False and self._spacelift_domain != False
41
- self._workspace_root = os.environ.get("WORKSPACE_ROOT", os.getcwd())
42
+ self._api_enabled = bool(self._api_token and self._spacelift_domain)
43
+ self._workspace_root = os.getcwd()
44
+ self._spacelift_markdown_endpoint = None
42
45
 
43
46
  # This should be the last thing we do in the constructor
44
47
  # because we set api_enabled to false if the domain is set up incorrectly.
@@ -54,6 +57,11 @@ class SpaceforgePlugin(ABC):
54
57
  )
55
58
  self._api_enabled = False
56
59
 
60
+ if self._api_enabled:
61
+ self._spacelift_markdown_endpoint = self._spacelift_domain.replace(
62
+ "/graphql", "/worker/plugin_logs_url"
63
+ )
64
+
57
65
  def _setup_logger(self) -> logging.Logger:
58
66
  """Set up logging for the plugin."""
59
67
 
@@ -62,7 +70,7 @@ class SpaceforgePlugin(ABC):
62
70
  warn_color = "\033[33m"
63
71
  error_color = "\033[31m"
64
72
  end_color = "\033[0m"
65
- run_id = os.environ.get("TF_VAR_spacelift_run_id", "local")
73
+ run_id = self._run_id
66
74
  plugin_name = self.__plugin_name__
67
75
 
68
76
  class ColorFormatter(logging.Formatter):
@@ -93,7 +101,7 @@ class SpaceforgePlugin(ABC):
93
101
  handler.setFormatter(ColorFormatter())
94
102
 
95
103
  # Always check for debug mode spacelift variable
96
- if os.environ.get("SPACELIFT_DEBUG"):
104
+ if os.environ.get("SPACELIFT_DEBUG") or self._is_local:
97
105
  logger.setLevel(logging.DEBUG)
98
106
  else:
99
107
  logger.setLevel(logging.INFO)
@@ -175,7 +183,7 @@ class SpaceforgePlugin(ABC):
175
183
  ) -> Dict[str, Any]:
176
184
  if not self._api_enabled:
177
185
  self.logger.error(
178
- 'API is not enabled, please export "SPACELIFT_API_TOKEN" and "SPACELIFT_DOMAIN".'
186
+ 'API is not enabled, please export "SPACELIFT_API_TOKEN" and "TF_VAR_spacelift_graphql_endpoint".'
179
187
  )
180
188
  exit(1)
181
189
 
@@ -192,7 +200,7 @@ class SpaceforgePlugin(ABC):
192
200
  data["variables"] = variables
193
201
 
194
202
  req = urllib.request.Request(
195
- f"{self._spacelift_domain}/graphql",
203
+ self._spacelift_domain, # type: ignore[arg-type]
196
204
  json.dumps(data).encode("utf-8"),
197
205
  headers,
198
206
  )
@@ -226,8 +234,93 @@ class SpaceforgePlugin(ABC):
226
234
  return data
227
235
 
228
236
  def send_markdown(self, markdown: str) -> None:
229
- # TODO
230
- print(markdown)
237
+ """
238
+ Send a markdown message to the Spacelift run.
239
+
240
+ Args:
241
+ markdown: The markdown content to send
242
+ """
243
+ if self._is_local:
244
+ self.logger.info(
245
+ "Spacelift run is local. Not uploading markdown. Below is a preview of what would be sent"
246
+ )
247
+ self.logger.info(markdown)
248
+ return
249
+
250
+ if self._spacelift_markdown_endpoint is None:
251
+ self.logger.error(
252
+ 'API is not enabled, please export "SPACELIFT_API_TOKEN" and "TF_VAR_spacelift_graphql_endpoint".'
253
+ )
254
+ exit(1)
255
+
256
+ headers = {"Authorization": f"Bearer {self._api_token}"}
257
+ body = {
258
+ "plugin_name": self.__plugin_name__,
259
+ }
260
+
261
+ # First we get the signed url for uploading
262
+ req = urllib.request.Request(
263
+ self._spacelift_markdown_endpoint,
264
+ json.dumps(body).encode("utf-8"),
265
+ headers,
266
+ )
267
+
268
+ with urllib.request.urlopen(req) as response:
269
+ if response.status != 200:
270
+ self.logger.error(
271
+ f"Error getting signed URL for markdown upload: {response.status}"
272
+ )
273
+ return
274
+
275
+ raw_response = response.read().decode("utf-8")
276
+ self.logger.debug(raw_response)
277
+ resp: Dict[str, Any] = json.loads(raw_response)
278
+ if "url" not in resp or "headers" not in resp:
279
+ self.logger.error(
280
+ "Markdown signed url response does not contain 'url' or 'headers' key."
281
+ )
282
+ return
283
+
284
+ signed_url = resp["url"]
285
+ headers = resp["headers"]
286
+ headers["Content-Type"] = "text/markdown"
287
+ headers["Content-Length"] = str(len(markdown))
288
+
289
+ # Now we upload the markdown content to the signed URL
290
+ req = urllib.request.Request(
291
+ signed_url,
292
+ data=markdown.encode("utf-8"),
293
+ headers=headers,
294
+ method="PUT",
295
+ )
296
+
297
+ with urllib.request.urlopen(req) as put_response:
298
+ if put_response.status != 200:
299
+ self.logger.error(
300
+ f"Error uploading markdown content: {put_response.status}"
301
+ )
302
+ return
303
+ self.logger.debug("Markdown content uploaded successfully.")
304
+
305
+ def add_to_policy_input(self, input_name: str, data: Dict[str, Any]) -> None:
306
+ """
307
+ Add data to the policy input for the current Spacelift run.
308
+
309
+ Args:
310
+ input_name: The name of the input to add (will be available as input.third_party_metadata.custom.{input_name} to the policy).
311
+ data: Dictionary containing data to add to the policy input
312
+ """
313
+ if self._is_local:
314
+ self.logger.info(
315
+ "Spacelift run is local. Not writing custom policy input. Below is a preview of what would be written"
316
+ )
317
+ self.logger.info(json.dumps(data, indent=2))
318
+ return
319
+
320
+ with open(
321
+ f"{self._workspace_root}/{input_name}.custom.spacelift.json", "w"
322
+ ) as f:
323
+ f.write(json.dumps(data))
231
324
 
232
325
  # Hook methods - override these in your plugin
233
326
  def before_init(self) -> None:
spaceforge/runner.py CHANGED
@@ -4,7 +4,6 @@ Plugin runner for executing hook methods.
4
4
 
5
5
  import importlib.util
6
6
  import os
7
- import sys
8
7
  from typing import Optional
9
8
 
10
9
 
@@ -102,14 +101,3 @@ def runner_command(hook_name: str, plugin_file: str) -> None:
102
101
  """
103
102
  runner = PluginRunner(plugin_file)
104
103
  runner.run_hook(hook_name)
105
-
106
-
107
- def main() -> None:
108
- """Legacy main entry point for backward compatibility."""
109
- if len(sys.argv) != 2:
110
- print("Usage: python -m spaceforge.runner <hook_name>")
111
- sys.exit(1)
112
-
113
- hook_name = sys.argv[1]
114
- runner = PluginRunner()
115
- runner.run_hook(hook_name)
spaceforge/schema.json CHANGED
@@ -118,7 +118,7 @@
118
118
  "type": "object"
119
119
  },
120
120
  "Parameter": {
121
- "description": "A class to represent a parameter with a name and value.\n\nAttributes:\n name (str): The name of the parameter.\n description (str): A description of the parameter.\n sensitive (bool): Whether the parameter contains sensitive information.\n required (bool): Whether the parameter is required.\n default (Optional[str]): The default value of the parameter, if any. (required if sensitive is False)",
121
+ "description": "A class to represent a parameter with a name and value.\n\nAttributes:\n name (str): The name of the parameter.\n description (str): A description of the parameter.\n sensitive (bool): Whether the parameter contains sensitive information.\n required (bool): Whether the parameter is required.\n default (Optional[str]): The default value of the parameter, if any. (required if sensitive is False)\n id (str): Unique identifier for the parameter.",
122
122
  "properties": {
123
123
  "name": {
124
124
  "title": "Name",
@@ -149,6 +149,10 @@
149
149
  ],
150
150
  "default": null,
151
151
  "title": "Default"
152
+ },
153
+ "id": {
154
+ "title": "Id",
155
+ "type": "string"
152
156
  }
153
157
  },
154
158
  "required": [
@@ -285,10 +289,10 @@
285
289
  "type": "object"
286
290
  }
287
291
  },
288
- "description": "A class to represent the manifest of a Spacelift plugin.\n\nAttributes:\n name_prefix (str): The name of the plugin, will be appended with a unique ID.\n description (str): A description of the plugin.\n author (str): The author of the plugin.\n parameters (list[Parameter]): List of parameters for the plugin.\n contexts (list[Context]): List of contexts for the plugin.\n webhooks (list[Webhook]): List of webhooks for the plugin.\n policies (list[Policy]): List of policies for the plugin.",
292
+ "description": "A class to represent the manifest of a Spacelift plugin.\n\nAttributes:\n name (str): The name of the plugin, will be appended with a unique ID.\n description (str): A description of the plugin.\n author (str): The author of the plugin.\n labels (list[str]): List of labels for the plugin.\n parameters (list[Parameter]): List of parameters for the plugin.\n contexts (list[Context]): List of contexts for the plugin.\n webhooks (list[Webhook]): List of webhooks for the plugin.\n policies (list[Policy]): List of policies for the plugin.",
289
293
  "properties": {
290
- "name_prefix": {
291
- "title": "Name Prefix",
294
+ "name": {
295
+ "title": "Name",
292
296
  "type": "string"
293
297
  },
294
298
  "version": {
@@ -303,6 +307,20 @@
303
307
  "title": "Author",
304
308
  "type": "string"
305
309
  },
310
+ "labels": {
311
+ "anyOf": [
312
+ {
313
+ "items": {
314
+ "type": "string"
315
+ },
316
+ "type": "array"
317
+ },
318
+ {
319
+ "type": "null"
320
+ }
321
+ ],
322
+ "title": "Labels"
323
+ },
306
324
  "parameters": {
307
325
  "anyOf": [
308
326
  {
@@ -361,7 +379,7 @@
361
379
  }
362
380
  },
363
381
  "required": [
364
- "name_prefix",
382
+ "name",
365
383
  "version",
366
384
  "description",
367
385
  "author"