Skip to content
Merged
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
36 changes: 36 additions & 0 deletions .github/workflows/static-deploy-lint-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Static Deploy Scripts Lint and Test

on:
pull_request:
paths:
- 'scripts/static-deploy/**'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.ref_name != 'edge' || github.run_id}}-${{ github.ref_type != 'tag' || github.run_id }}
cancel-in-progress: true

jobs:
lint-and-test:
timeout-minutes: 5
runs-on: ubuntu-latest

steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: Setup UV
uses: astral-sh/setup-uv@v6
with:
python-version: '3.10'
enable-cache: true
cache-dependency-glob: 'scripts/static-deploy/uv.lock'
- name: Setup Deploy Dependencies
working-directory: ./scripts/static-deploy
run: make setup

- name: Lint Check
working-directory: ./scripts/static-deploy
run: make lint

- name: Run tests
working-directory: ./scripts/static-deploy
run: make test
23 changes: 23 additions & 0 deletions scripts/static-deploy/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.PHONY: setup teardown install format lint test print

setup:
uv sync --frozen

teardown:
rm -rf .venv

install:
uv install

format:
uv run ruff format
uv run ruff check --fix

lint:
uv run ruff check

test:
uv run pytest tests/ -v

print:
uv run python deploy_config.py
12 changes: 12 additions & 0 deletions scripts/static-deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Static Deploy

This package manages **configuration and deployment scripts** for Opentrons static websites such as **Labware Library** and **Protocol Designer**.
Python and dependencies are managed with [uv](https://github.com/astral-sh/uv).

---

## Philosophy

- **One workflow**: scripts are designed to run in **GitHub Actions** as the default deployment path.
- **Local fallback**: every script can be run locally with the proper flags (e.g. `make PROFILE=the_profile ENV=sandbox APPLICATION=protocol_designer deploy`) in case CI is unavailable.
- **Consistency**: all commands are standardized through the `Makefile`
1 change: 1 addition & 0 deletions scripts/static-deploy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# deploy package
166 changes: 166 additions & 0 deletions scripts/static-deploy/deploy_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Deployment configuration for Opentrons applications."""

import os
from dataclasses import dataclass
from typing import Literal

from rich.console import Console
from rich.panel import Panel
from rich.tree import Tree

DEFAULT = "not_set"


class InvalidEnvironmentError(ValueError):
"""Raised when an invalid environment is requested."""


class InvalidApplicationError(ValueError):
"""Raised when an invalid application is requested."""


Environment = Literal["sandbox", "staging", "production"]
Application = Literal["labware_library", "protocol_designer"]


@dataclass(frozen=True)
class ApplicationConfig:
"""Configuration for a single application deployment."""

s3_bucket: str
cloudfront_id: str


@dataclass(frozen=True)
class EnvironmentConfig:
"""Configuration for all applications in a specific environment."""

labware_library: ApplicationConfig
protocol_designer: ApplicationConfig


@dataclass(frozen=True)
class DeployConfig:
"""Complete deployment configuration for all environments."""

sandbox: EnvironmentConfig
staging: EnvironmentConfig
production: EnvironmentConfig


# TODO: Populate with actual configuration values
def get_deploy_config() -> DeployConfig:
"""Get the complete deployment configuration from environment variables."""

# Sandbox configuration
sandbox_config = EnvironmentConfig(
labware_library=ApplicationConfig(
s3_bucket=os.getenv("SANDBOX_LABWARE_LIBRARY_S3_BUCKET", DEFAULT),
cloudfront_id=os.getenv("SANDBOX_LABWARE_LIBRARY_CLOUDFRONT_ID", DEFAULT),
),
protocol_designer=ApplicationConfig(
s3_bucket=os.getenv("SANDBOX_PROTOCOL_DESIGNER_S3_BUCKET", DEFAULT),
cloudfront_id=os.getenv("SANDBOX_PROTOCOL_DESIGNER_CLOUDFRONT_ID", DEFAULT),
),
)

# Staging configuration
staging_config = EnvironmentConfig(
labware_library=ApplicationConfig(
s3_bucket=os.getenv("STAGING_LABWARE_LIBRARY_S3_BUCKET", DEFAULT),
cloudfront_id=os.getenv("STAGING_LABWARE_LIBRARY_CLOUDFRONT_ID", DEFAULT),
),
protocol_designer=ApplicationConfig(
s3_bucket=os.getenv("STAGING_PROTOCOL_DESIGNER_S3_BUCKET", DEFAULT),
cloudfront_id=os.getenv("STAGING_PROTOCOL_DESIGNER_CLOUDFRONT_ID", DEFAULT),
),
)

# Production configuration
production_config = EnvironmentConfig(
labware_library=ApplicationConfig(
s3_bucket=os.getenv("PRODUCTION_LABWARE_LIBRARY_S3_BUCKET", DEFAULT),
cloudfront_id=os.getenv("PRODUCTION_LABWARE_LIBRARY_CLOUDFRONT_ID", DEFAULT),
),
protocol_designer=ApplicationConfig(
s3_bucket=os.getenv("PRODUCTION_PROTOCOL_DESIGNER_S3_BUCKET", DEFAULT),
cloudfront_id=os.getenv("PRODUCTION_PROTOCOL_DESIGNER_CLOUDFRONT_ID", DEFAULT),
),
)

return DeployConfig(sandbox=sandbox_config, staging=staging_config, production=production_config)


def get_config(environment: str, application: str) -> ApplicationConfig:
"""Get configuration for a specific application in an environment.

Args:
environment: The environment name (sandbox, staging, production) - case insensitive
application: The application name (labware_library, protocol_designer) - case insensitive

Returns:
ApplicationConfig for the specified application and environment

Raises:
InvalidEnvironmentError: If the environment is not valid
InvalidApplicationError: If the application is not valid
"""
# Convert to lowercase for case-insensitive comparison
environment = environment.lower()
application = application.lower()

valid_environments = ["sandbox", "staging", "production"]
valid_applications = ["labware_library", "protocol_designer"]

if environment not in valid_environments:
raise InvalidEnvironmentError(f"Invalid environment '{environment}'. Valid environments are: {', '.join(valid_environments)}")

if application not in valid_applications:
raise InvalidApplicationError(f"Invalid application '{application}'. Valid applications are: {', '.join(valid_applications)}")

config = get_deploy_config()
env_config = getattr(config, environment)

if not hasattr(env_config, application):
raise InvalidApplicationError(f"Application '{application}' not found in environment '{environment}'")

return getattr(env_config, application)


def print_deploy_config() -> None:
"""Pretty print the complete deployment configuration using rich."""

console = Console()
config = get_deploy_config()

# Create the main tree
tree = Tree("🚀 [bold blue]Deploy Configuration[/bold blue]")

# Add each environment as a branch
for env_name in ["sandbox", "staging", "production"]:
env_config = getattr(config, env_name)

# Choose emoji based on environment
emoji = {"sandbox": "🏗️", "staging": "🧪", "production": "🌟"}[env_name]
env_branch = tree.add(f"{emoji} [bold green]{env_name.title()}[/bold green]")

# Add applications under each environment
for app_name in ["labware_library", "protocol_designer"]:
app_config = getattr(env_config, app_name)
app_display_name = app_name.replace("_", " ").title()

app_branch = env_branch.add(f"📦 [yellow]{app_display_name}[/yellow]")
app_branch.add(f"🪣 S3 Bucket: [cyan]{app_config.s3_bucket}[/cyan]")
app_branch.add(f"☁️ CloudFront: [magenta]{app_config.cloudfront_id}[/magenta]")

# Print with a nice panel
console.print(Panel(tree, title="Deployment Configuration", border_style="blue"))


def main() -> None:
"""Main entry point for the deploy configuration module."""
print_deploy_config()


if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions scripts/static-deploy/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "static-deploy"
version = "0.0.0"
description = "Tools for deploying our static web applications Labware Library and Protocol Designer"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"pytest>=8.4.1",
"rich>=14.1.0",
"ruff>=0.12.11",
]

[tool.hatch.build.targets.wheel]
packages = ["."]

[tool.ruff]
# Like Black
line-length = 140
# Like Black
indent-width = 4
target-version = "py310"
exclude = ["files"]
src = ["*.py", "test"]

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
]
fixable = ["ALL"]

[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"

# Like Black, indent with spaces, rather than tabs.
indent-style = "space"

# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false

# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
1 change: 1 addition & 0 deletions scripts/static-deploy/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# tests package
Loading
Loading