Contribution Guide
Thanks for considering contributing to Cloudy Pad! This guide will help you get started contributing to various parts of the project.
Here's a typical workflow outline for contributions:
- Understand project architecture
- Fork Cloudy Pad on GitHub or clone project locally
- Setup your development environment
- Development environment setup is fully automated! Don't worry about tooling installation and setup
- Write code
- Use of AI is encouraged, we provide contextual AI
AGENTS.md
and Cursor rules files. - You can also pass this contribution guide directly to AI context
- Make sure to review and test your code before going forward though. Unsupervised IA contribution are unlikely to pass review.
- Use of AI is encouraged, we provide contextual AI
- Run tests and review
- A simple
task test-unit
and human is enough for most situations - Detailed testing and debugging instructions are provided for more complex situation
- A simple
- Submit Pull Request
Table of Content
- Getting started: setup development environment, build and run tests
- Architecture Overview
- Using AI is encouraged - with caveats
- Where and how to contribute ?
- Updating a provider (AWS, Azure, GCP, etc.)
cloudypad
CLI: fixing bugs, adding arguments, etc.- Instance configuration: NVIDIA driver install, Sunshine / Wolf deployment, etc. via Ansible Playbook
- Documentation, troubleshooting guide and other non-code related tasks
- Cloudy Pad desktop and Sunshine server container image (Docker)
- Running / writing tests and verifying your changes locally
- Debugging
- Create a Pull Request with your changes
- Useful commands
- Get help on Discord !
Getting started: setup development environment, build and run tests
A fully automated development environment is provided by Nix Flake (see flake.nix
at project root). It will setup the required tools with desired versions in a kind of virtual development environment which will not impact or change your own tool version anywhere else on your machine.
You only need to install:
Start a Nix development shell: it will download packages and setup your development environment. The first run may take some time - don't worry subsequent runs are almost instantaneous:
nix develop
You can then run commands:
# Build Node CLI package
task build-npm
# Run unit tests
task test-unit
Most commands are wrapped as a task
- See Taskfile.yml
at project root.
You're ready to contribute !
Architecture Overview
Cloudy Pad code has 3 main parts:
- Core - Internal components to manage instances and deployment
- Provider implementations - implementation of instance deployment and management for various Providers
- A Provider is a specialization of Core components to deploy instances in a given context
- For example
aws
Provider is the implementation of instance deployment on AWS (Amazon Web Services)
- CLI - Wrapper around Core components to provide the Cloudy Pad CLI with interactive prompts
Instance lifecycle and Core components
Instance lifecycle:
Initialization (or creation)
An Instance State is initialized with various Inputs (e.g. provider, region, instance type and specs, etc.). On initialization, the instance is:
- Provision - Cloud infrastructure is provisioned (creation of virtual machine, disk storage, etc. for instance) - most of the time with Pulumi (an Infrastructure as Code tool). Note a Cloud infrastructure is not always required, such as with SSH provider, in which case provisioning is no-op.
- Configuration - Installation of OS-level components: NVIDAI drivers, Sunshine / Wolf streaming server, etc. via Ansible (an Infrastructure as Code tool)
- Pairing - Instance is paired with Moonlight client
These steps can also be run separately (e.g. cloudypad provision
)
Related code:
core/initializer.ts
-InstanceInitializer
- Manage overall initialization and deployment withInstanceManager
core/manager.ts
-InstanceManager
- manages instance lifecycle with:core/provisioner.ts
-InstanceProvisioner
core/configurator.ts
-InstanceConfigurator
core/moonlight/pairer.ts
-MoonlightPairer
Usage lifecycle: start and stop
After initilialization, instance can be started and stopped.
Related code:
core/manager.ts
-InstanceManager
- manages instance lifecycle withRunner
.core/runner.ts
-InstanceRunner
- Manages instance start/stop actions
Deletion
Destroy instance. Instance deletion deletes infrastructure (instance, disk, etc.) and related Instance State
Related code:
core/manager.ts
-InstanceManager
- manages instance lifecycle withRunner
.core/provisioner.ts
-InstanceProvisioner
Instance State
The Instance State represents the state in which an instance is. It roughly contains:
- Instance name
- Provider name (AWS, Azure, etc.)
- Inputs: desired instance configurations (instance type, region, streaming server, etc.)
- Outputs: actual infrastructure state (disk storage unique ID, IP address, etc.)
Each Provider implements its own state interface based on a common State implementation. State is by default persisted as a local YAML file under ~/.cloudypad/instance/<name>/state.yml
Example State file:
name: aws-instance
version: '1'
# Provision inputs and outputs
provision:
provider: aws # provider name
# Desired instance state for AWS
input:
diskSize: 200
instanceType: g4dn.2xlarge
publicIpType: static
region: eu-central-1
ssh:
user: ubuntu
privateKeyContentBase64: xxx
# Actual infrastructure state
# for AWS, host is instance static IP address and instanceId the EC2 instance ID
output:
host: 18.199.182.227
instanceId: i-0ae901f1799b17fdf
# Configuration inputs and outputs
# used to configure instance
configuration:
configurator: ansible
input:
sunshine:
enable: true
passwordBase64: xxx
username: sunshine
As state is written and read externally, Zod is used to enforce TypeScript typing.
See src/core/state
Provider implementation
A Provider is a specialization of Core components to deploy instances in a given context (usually a Cloud provider). For example aws
Provider is the implementation of instance deployment on AWS (Amazon Web Services).
Each provider is based on the same pattern. Example for AWS provider in src/providers/aws
:
pulumi.ts
- Pulumi is an Infrastructure as Code and provisioning tool (like Terraform) to manage Cloud resources. This file defines the Stack program used for each provider.state.ts
- ExtendsInstanceStateV1
with Inputs and Outputs specific to this providerprovisioner.ts
- ImplementsInstanceProvisioner
for this provider. Use State Inputs to configure and run Pulumi stacks (or create infra with Provider API directly)runner.ts
- ImplementsInstanceRunner
for this provider. Use a Provider specific client to call Provider API to start/stop/... instance infrastructure.cli.ts
- CLI implementation for this provider. Define Provider specific CLI args and command implementation (e.g.create
arguments which are specific per providers)sdk-client.ts
- or in aclient/
folder. Provider specific SDK clients to interact with various resources (get server status, list available regions, etc.)
More internal-oriented files:
provider.ts
- ImplementsProviderClient
for this Provider.factory.ts
- ImplementsProvisionerFactory
for this Provider.
Using AI is encouraged - with caveats
You are encouraged to use AI to contribute to Cloudy Pad. AGENTS.md
-like files are present under .cursor/rules
- make sure to include them. They describe expected best practices and context for AI to better understand the project.
Please note:
- Even though AI use is encouraged, please review your code and follow best practices
- Do not contribute a PR without reviewing and testing your changes. A PR with low-effort and/or broken AI-generated code will slow down the merging process
Where and how to contribute ?
Depending on your contribution, here are various guides to get started: where is code located, guidelines to get started in code and various tips.
Make sure to read Architecture overview first 😉
Updating a provider (AWS, Azure, GCP, etc.)
Providers are located under src/providers/<provider>
with a similar structure. You might want to
- Update infrastructure used for provider in
pulumi.ts
- Improve CLI arguments and prompting for a provider in
cli.ts
See Provider implementation to get a better understanding of Providers.
cloudypad
CLI: fixing bugs, adding arguments, etc.
CLI entrypoint is src/cli/main.ts
. CLI is constructed by:
- Building
Commands
objects based on common arguments (as often same arguments are present in multiple commands) program.ts
describes various commands and available optionsinitializer.ts
andupdater.ts
are wrappers around their Core equivalents to prompt users for specific details duringcloudypad create
andcloudypad update
On running CLI:
- CLI args are parsed into a
CliArgs
interface CliArgs
is transformed into a Core interfaceInstanceInputs
describing inputs (~configurations) for an instance byprompter.ts
cliArgsIntoPartialInput()
To add or update CLI arguments you can:
- Add/update arguments in main program
- You may also need to update provider-specific
cli.ts
, e.g.src/providers/aws/cli.ts
for AWS. These files define CLI arguments specific to each provider.
- You may also need to update provider-specific
- Update
prompter.ts
to pass new arguments intoInstanceInputs
- If needed, update
prompter.ts:completeCliInput()
to prompt the user for input if missing from CLI args - From this point on,
InstanceInputs
are passed through to internal components. You can now change code to use your changes.
Instance configuration: NVIDIA driver install, Sunshine / Wolf deployment, etc. via Ansible Playbook
Ansible (an Infrastructure as Code tool) is used to configure instances via playbooks in ansible/
.
You can update playbook to:
- Change how NVIDIA drivers are installed
- Update Sunshine server setup
- Update Wolf server setup
- Customize per-provider behavior (e.g. package installation, add exceptions for some providers)
- Manage auto-stop mechanism (detect inactivity to automatically stop instance, avoiding unwanted Cloud cost)
Documentation, troubleshooting guide and other non-code related tasks
Documentation at https://docs.cloudypad.gg is auto-generated from Markdown files under docs/src
.
- Updating existing files: on merge, the documentation will be updated
- Adding new files: make sure to reference them in
docs/src/SUMMARY.md
to have them included
Run a local doc server to see the result of your updates via http://localhost:3000:
task mdbook-server
Cloudy Pad desktop and Sunshine server container image (Docker)
The Sunshine streaming server or desktop container is what you see when you connect to Cloudy Pad via Moonlight (using Sunshine streaming server). Source code is located at containers/sunshine
Container is roughly built by:
- Installing a desktop environment on an Ubuntu base image
- Installing game launchers: Steam, Heroic, etc.
- Installing various tools and libraries (Proton, text editor, etc.)
- Adding an overlay with various scripts and default config (Sunshine server config template, desktop environment config, etc.)
On startup, container starts Sunshine, the desktop and all required components.
Running / writing tests and verifying your changes locally
You'll want to test your changes and verify them along the way. Typical ways include:
Running tests
Running unit tests:
task test-unit
Building application (plain node build or Docker build):
# Node build
task build-npm
# CLI Docker container build
task build-core-container-local
Running the CLI directly from your code - e.g. deploy an instance to verify your changes are applied correctly:
# Equivalent of `cloudypad --help` for development
npx tsx src/cli/main.ts --help
# Create a test instance
npx tsx src/cli/main.ts create aws my-test-instance [...]
See examples of fully automated instance deployment via CLI in test/integ/unstable/cli-full-lifecycle
Adding and writing tests
Cloudy Pad has several layer of tests - most of them run on CI:
Unit tests
Run unit tests via task test-unit
. Unit tests are under test/unit
and follow these global coding practices:
- Use Mocha for testing
Test
structure matchessrc
structure, e.g.src/core/manager.ts
is tested viatest/unit/core/manager.spec.ts
- Each test is self-contained. A test should not change or update data, variable or context which is also used by another test. If test needs setup (such as an existing instance State file), test setup should be done either in the
it()
function or in a before hook. - Avoid using
before
in a spec file to set a global variable used by all tests. Prefer generating data in each test. - Do not hesitate to look at similar tests to match their structure
About side effects:
- You must avoid side effects with unit tests (e.g. don't contact an external service or a database, etc.). Writing local files is accepted, but the method must guarantee proper reproducibility and debugging analysis (e.g. don't write a temporary test that can't be verified if something happens)
- If the component under tests relies on side effect, use Sinon to stub calls causing side effect
- See
test/unit/hooks.ts
for globally defined stubs, e.g. for Pulumi or AWS clients
Unit tests have a few utility functions in test/unit/utils.ts
to help:
createDummyState()
- Create dummy instance State for testing with optional overridesinitializeDummyInstanceState()
- Initialize a dummy instance state and return itDEFAULT_COMMON_INPUT
- Instance inputs to use during test require Inputs or StateDEFAULT_COMMON_CLI_ARGS
- CLI argument matching DEFAULT_COMMON_INPUTcreateTempTestDir(prefix)
- Create temporary directory for testinggetUnitTestCoreClient()
- Create a Core client suitable for testing with local data backendgetUnitTestCoreConfig()
- Create CoreConfig with local state backend for testinggetUnitTestDummyProviderClient()
- Create a dummy ProviderClient for testing
Integration tests
Stable integration tests are under test/integ/stable
- they create real Cloud infrastructure to test instance lifecycles. They are not currently run on CI, only locally using your own credentials.
⚠️ Make sure to understand what these tests are doing before running them.
Example:
# Run all integration tests
task test-integ-stable
# Run a specific provider integration tests
test-integ-provider-scaleway
Other tests
Various tests on CI also:
- Check documentation and dead links
- Verify build, compilation and linting
- Verify installation script
You can run them as task
locally if CI reports issues.
Debugging
Various debugging methods:
CLI log level
Set environment variable export CLOUDYPAD_LOG_LEVEL=2
(DEBUG) or 3
(INFO) to show more information
Ansible playbook
Ansible playbook is written locally under a temp directory at runtime. Ansible playbook path is shown at DEBUG log level. To debug configuration more easily:
- Run a deployment or configuration command, e.g.
CLOUDYPAD_LOG_LEVEL=2 npx tsx src/cli/main.ts configure my-instance
- Logs will show something like
2025-10-09 14:14:18.448 DEBUG src/tools/ansible.ts:15 AnsibleClient Ansible command: ansible-playbook ["-i","/tmp/nix-shell.iuM9BJ/cloudypad-qBivGz/inventory.yml","/home/me/cloudypad/ansible/sunshine.yml","-e","'ansible_ssh_common_args=\"-o StrictHostKeyChecking=no\"'"]
- Use or edit Ansible playbook directly and run:
ansible-playbook -i /tmp/nix-shell.iuM9BJ/cloudypad-qBivGz/inventory.yml ansible/sunshine.yml
Local Pulumi stack manipulation
Nix development shell automatically sets PULUMI_BACKEND_URL
and PULUMI_CONFIG_PASSPHRASE
environment variables, allowing you to manipulate Pulumi stacks locally.
# List stacks
pulumi stack ls -a
# Show stack resources
pulumi stack -s <organization/CloudyPad-XXX/STACK> --show-ids
# Destroy stack
pulumi destroy -s <organization/CloudyPad-XXX/STACK>
Local Virtual Machine as Cloudy Pad instance
A local debug Virtual Machine (VM) can be set up with Vagrant and Vagrantfile
. This machine matches a real-world Cloudy Pad instance except it does not use provisioning and may not have an available GPU (Cloudy Pad still works using CPU encoding).
Local VM goals:
- Test changes impacting configuration, typically:
- Changes to Ansible Playbook in
ansible/
- Changes to Sunshine container image (Docker image) in
containers/
Local VM setup
Debug VM can be setup with:
# Create VM
vagrant up
# Run configuration from Ansible playbook
task dev-ansible-config
# Might fail as :local tag doesn't exist
# Push local container image to VM
task dev-docker-sunshine-to-vm
Using local VM
Instance is reachable via 192.168.56.43
.
To pair, connect to the Sunshine web UI and use Moonlight manually:
https://192.168.56.43:47990/
Debug container and Dockerfile changes
During Sunshine container development, it's possible to use a Vagrant-specific Docker Compose which will mount folders from the host machine directly in the Cloudy Pad container.
For example, if you change containers/sunshine/overlay/cloudy/conf/sunshine/sunshine.conf.template
locally, you don't need to rebuild the container image, you can mount this file directly in the VM container for faster testing:
- Update
test/resources/docker-compose.vagrant.yml
to mount desired files, for example:
volumes:
# [...]
#
# Mount local containers/sunshine/overlay/cloudy/conf/sunshine/sunshine.conf.template
# Local project is mounted at /vagrant, hence using /vagrant/<project/path>
#
- "/vagrant/containers/sunshine/overlay/cloudy/conf/sunshine/sunshine.conf.template:/cloudy/conf/sunshine/sunshine.conf.template"
- Update container with custom mounts
vagrant ssh
$ docker compose -f /vagrant/test/resources/docker-compose.vagrant.yml -p sunshine up -d --force-recreate
Create a Pull Request with your changes
When your changes are ready, you can create a Pull Request. Please make sure to:
- Follow conventional commits
- Eg. commit messages like
feat: added a nice feature
- See Git history as example
- Eg. commit messages like
- Verify your code: test, compilation, review, etc.
- Explain your changes
Review process
Once your PR is created you should get an update within 3 days. If your PR is not acknowledged, please ping Pierre.
While we'll do our best to merge your code ASAP, review process may take a bit of time and typically involves:
- Questions about your code
- Fixing a few things like design, refactoring, or adding tests
Useful commands
Taskfile
contains lots of commands available in development (see comments on tasks). Run with task xxx
, e.g.:
task test-unit
Get help on Discord !
Get in touch on Discord to get help for contributing - we'll happily guide you through !