Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 60 additions & 38 deletions .github/scripts/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

Currently supports:
- Studio projects
- Agent projects
- Operations Manager automations
- Lifecycle Manager resources
- Configuration Manager golden configs
Expand Down Expand Up @@ -58,6 +59,7 @@ def __init__(self, environment: str, members: list[dict[str, str]] | None = None
# bucket key, emoji, and label used when reporting found files.
_ASSET_DIR_MAP = {
"Studio Projects": ("projects", "📦", "Studio project"),
"Agent Projects": ("agent_projects", "🧠", "agent project"),
"Automations": ("automations", "🤖", "automation"),
"LCM Resource Models": ("lifecycle_manager_resources", "🔧", "LCM resource model"),
"Golden Configs": ("configurations", "⚙️ ", "golden config"),
Expand All @@ -78,6 +80,7 @@ def find_asset_files(self) -> dict[str, list[Path]]:

assets: dict[str, list[Path]] = {
"projects": [],
"agent_projects": [],
"automations": [],
"lifecycle_manager_resources": [],
"configurations": [],
Expand Down Expand Up @@ -166,30 +169,44 @@ async def deploy_projects(self, client: Any, project_files: list[Path]) -> None:
print(f"👤 Added {member.type} {member_identifier} as {member.role}")
except Exception as e:
print(f"❌ Failed to import project {project_name}: {e}")
raise

# async def deploy_agent_projects(
# self, client: Any, bundle_files: list[Path]
# ) -> None:
# agent_projects_resource = client.resource("agent_projects")
# for bundle_file in bundle_files:
# with open(bundle_file, "r") as f:
# bundle_data = json.load(f)
# bundle_name = bundle_data.get("name", bundle_file.stem)
# try:
# members = []
# for member in self.members:
# member_data = {"type": member["type"], "role": member["role"]}
# if member["type"] == "account":
# member_data["username"] = member["username"]
# else:
# member_data["name"] = member["name"]
# members.append(ProjectMember(**member_data))
# await agent_projects_resource.importer(bundle_data, members=members)
# print(f"✅ Successfully imported Agent project: {bundle_name}")
# except Exception as e:
# print(f"❌ Failed to import Agent project {bundle_name}: {e}")
# raise
print(f"⚠️ Skipping {project_name} and continuing deployment")

async def deploy_agent_projects(
self, client: Any, bundle_files: list[Path]
) -> None:
"""Deploy Agent projects to the platform.

Args:
client: Asyncplatform client instance
bundle_files: List of agent project bundle file paths
"""
if not bundle_files:
print("ℹ️ No Agent projects to deploy")
return

agent_projects_resource = client.resource("agent_projects")
for bundle_file in bundle_files:
with open(bundle_file, "r") as f:
bundle_data = json.load(f)
bundle_name = bundle_data.get("name", bundle_file.stem)
try:
members = []
for member in self.members:
member_data = {"type": member["type"], "role": member["role"]}
if member["type"] == "account":
member_data["username"] = member["username"]
else:
member_data["name"] = member["name"]
members.append(ProjectMember(**member_data))

print(f"📥 Importing agent project: {bundle_name}")
await agent_projects_resource.importer(
bundle_data, members=members, overwrite=True
)
print(f"✅ Successfully imported Agent project: {bundle_name}")
except Exception as e:
print(f"❌ Failed to import Agent project {bundle_name}: {e}")
print(f"⚠️ Skipping {bundle_name} and continuing deployment")

async def deploy_automations(
self, client: Any, automation_files: list[Path]
Expand Down Expand Up @@ -217,8 +234,9 @@ async def deploy_automations(
)

if existing_automation:
print(f"ℹ️ Automation already exists, skipping: {automation_name}")
continue
print(f"📝 Automation exists, deleting existing version: {automation_name}")
await automations_resource.delete(automation_name)
print(f"🗑️ Deleted existing automation: {automation_name}")

print(f"📥 Importing automation: {automation_name}")
result = await automations_resource.importer(automation_data)
Expand Down Expand Up @@ -253,15 +271,16 @@ async def deploy_lifecycle_manager_resources(
try:
existing_resource = await lm_resource.get_resource_by_name(resource_name)
if existing_resource:
print(f"ℹ️ Resource model already exists, skipping: {resource_name}")
continue
print(f"📝 LCM Resource exists, deleting existing version: {resource_name}")
await lm_resource.delete(resource_name)
print(f"🗑️ Deleted existing LCM resource: {resource_name}")

print(f"📥 Importing resource model: {resource_name}")
result = await lm_resource.importer(lifecyle_manager_resource_payload)
print(f"✅ Successfully imported resource model: {result['data']['name']}")
except Exception as e:
print(f"❌ Failed to import resource model {resource_name}: {e}")
raise
print(f"⚠️ Skipping {resource_name} and continuing deployment")

async def deploy_configurations(
self, client: Any, config_files: list[Path]
Expand All @@ -278,22 +297,25 @@ async def deploy_configurations(

cm_resource = client.resource("configuration_manager")


for config_file in config_files:
with open(config_file, "r") as f:
config_data = json.load(f)
config_name = config_data.get("data", [{}])[0].get("name", config_file.stem)

config_name = config_data.get("name",config_file.stem)
try:
if await cm_resource.check_if_golden_config_exists(config_name):
print(f"ℹ️ Golden config already exists, skipping: {config_name}")
continue
# Check if golden config already exists and delete before re-importing
existing_config = await cm_resource.check_if_golden_config_exists(config_name)
if existing_config:
print(f"📝 Golden config exists, deleting existing version: {config_name}")
await cm_resource.delete(config_name)
print(f"🗑️ Deleted existing golden config: {config_name}")

print(f"📥 Importing golden config: {config_name}")
result = await cm_resource.import_golden_config([config_data])
print(f"✅ {result.get('message', f'Successfully imported golden config: {config_name}')}")
result = await cm_resource.importer([config_data])
print(f"✅ Successfully imported golden config: {config_name}")
except Exception as e:
print(f"❌ Failed to import golden config {config_name}: {e}")
raise
print(f"⚠️ Skipping {config_name} and continuing deployment")

async def deploy(self) -> None:
"""Execute the deployment process."""
Expand All @@ -319,7 +341,7 @@ async def deploy(self) -> None:

await self.deploy_projects(client, assets["projects"])

# await self.deploy_agent_projects(client, assets["agent_projects"])
await self.deploy_agent_projects(client, assets["agent_projects"])

await self.deploy_lifecycle_manager_resources(
client, assets["lifecycle_manager_resources"]
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Using an AI coding tool to contribute? Start with [`AGENTS.md`](./AGENTS.md) —

## Contribution Standards

Every asset type in this repo — Studio Projects, Automations, Golden Configurations, OpenAPIs, LCM Resource Models — has its own naming, versioning, and structural rules, plus a set of requirements that apply repo-wide (branding, README structure, no sensitive data, etc.). These all live in [`STANDARDS.md`](./STANDARDS.md).
Every asset type in this repo — Studio Projects, Agent Projects, Automations, Golden Configurations, OpenAPIs, LCM Resource Models — has its own naming, versioning, and structural rules, plus a set of requirements that apply repo-wide (branding, README structure, no sensitive data, etc.). These all live in [`STANDARDS.md`](./STANDARDS.md).

**Read it before opening a PR** — most review feedback traces back to one of these rules.

Expand All @@ -51,7 +51,7 @@ Title your PR (and its commits) as `<type>(<scope>): <summary>`.

| Type | Use for |
|---|---|
| `feat` | A new asset: a new vendor, product, or asset (OpenAPI spec, Studio Project, Automation, Golden Configuration) |
| `feat` | A new asset: a new vendor, product, or asset (OpenAPI spec, Studio Project, Agent Project, Automation, Golden Configuration) |
| `fix` | A correction to an existing asset (bad workflow task, wrong variable reference, invalid operationId, broken spec field, etc.) |
| `chore` | Non-functional maintenance (renaming, removing duplicates, reorganizing folders, metadata-only edits) |
| `docs` | Changes to `README.md`, `CONTRIBUTING.md`, `STANDARDS.md`, or a product's own `README.md` only |
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Assets are organized by vendor and product. Each folder may contain one or more
| Asset Type | Description |
|---|---|
| **Studio Projects** | Bundles of related automation assets (workflows, forms, templates, and transformations) for a specific use case |
| **Agent Projects** | Bundles of FlowAI agents (instructions, input schema, and tools) for a specific use case |
| **OpenAPIs** | JSON-based definitions imported as Integration Models that specify how Itential Platform connects to external APIs, databases, and systems |
| **Golden Configurations** | Config Manager compliance trees for auditing device configuration drift |
| **device-drivers** | Netmiko-based drivers for connecting Itential Gateway to physical and virtual devices |
Expand Down Expand Up @@ -76,6 +77,7 @@ Assets are organized by vendor and product. Each folder may contain one or more
```
Vendor/
└── Product/
├── Agent Projects/
├── Automations/
├── Configuration Parsers/
├── device-drivers/
Expand All @@ -100,6 +102,11 @@ See [Create and manage projects](https://docs.itential.com/itential-platform/stu
2. Click the **Import** button on the Projects homepage.
3. Upload the `.json` file by drag-and-drop or browse the file system.

### Import an Agent Project
1. In Itential Platform, go to **Agent Projects**.
2. Click the **Import** button on the Agent Projects homepage.
3. Upload the `.agent_project.json` file by drag-and-drop or browse the file system.

### Import an OpenAPI spec/Integration Model
See [Integration models](https://docs.itential.com/itential-platform/6/admin-essentials/integration-models) for full details.
1. In Itential Platform, go to **Admin Essentials**.
Expand Down
5 changes: 5 additions & 0 deletions STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All asset types below live at `{Vendor}/[{Product}/]{AssetType}/`. The `{Product

- [General Principles](#general-principles)
- [Studio Projects](#studio-projects-vendorproductstudio-projects)
- [Agent Projects](#agent-projects-vendorproductagent-projects)
- [Automations](#automations-vendorproductautomations)
- [Golden Configurations](#golden-configurations-vendorproductgolden-configurations)
- [OpenAPIs](#openapis-vendorproductopenapis)
Expand All @@ -29,6 +30,10 @@ All asset types below live at `{Vendor}/[{Product}/]{AssetType}/`. The `{Product
- Files are exported Studio projects in `.project.json` format.
- **Target the `-latest` Integration Model**: when building or updating a project against an OpenAPI-backed integration, wire its tasks to the `-latest` spec's Integration Model, not a pinned dated version — so projects automatically pick up the curated, actively-maintained spec rather than drifting to a version that will eventually be superseded.

## Agent Projects (`{Vendor}/[{Product}/]Agent Projects/`)
- Bundles of FlowAI agents (instructions, input schema, and tools) for a specific use case.
- Files are exported Agent Project bundles in `.agent_project.json` format.

## Automations (`{Vendor}/[{Product}/]Automations/`)
- Exported automation definitions that correspond to a Studio Project submission.
- Should be paired with a Trigger where applicable.
Expand Down