Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c5c84f2
P0: Move wipe-and-replace into atomic transaction for github pull
jmsunseri May 11, 2026
c968a01
Re-enable test_import_archive and add wipe_existing tests
jmsunseri May 11, 2026
02f8583
P1: Parallelize zip download and refactor github_pull into testable h…
jmsunseri May 11, 2026
6c46500
P2: Incremental delta sync with force fallback
jmsunseri May 11, 2026
10e559c
P2: Show last sync time in GitHub pane and pull result alerts
jmsunseri May 11, 2026
3491eea
Add libpng-dev to qemu builder stage for sdl2-decoration.c
jmsunseri May 9, 2026
e7a45e3
SSE: Real-time GitHub pull and build notifications, auto-pull mode se…
jmsunseri May 15, 2026
f9cf057
Restore GITHUB_ID/GITHUB_SECRET env vars in docker-compose
jmsunseri May 15, 2026
13c7b9a
Merge remote-tracking branch 'coredevices/main' into github-performance
jmsunseri May 25, 2026
e4a9e96
Add .env.local to .gitignore
jmsunseri May 25, 2026
ac3ffdd
Add spin animation to refresh icon
jmsunseri May 25, 2026
7ab01ef
Extract SSE event handlers and add tests
jmsunseri May 25, 2026
cdee40e
Remove sse-events.js and update tests to use sse.js
jmsunseri May 25, 2026
067a35d
Overhaul GitHub pull UX and add ngrok webhook support
jmsunseri May 25, 2026
c9dd4e9
Refresh sidebar on GitHub pull instead of page reload
jmsunseri May 25, 2026
5c7c022
Fix resource validation for package.json and subdirs
jmsunseri May 25, 2026
7d0383d
Strip root directory from delta change paths
jmsunseri May 25, 2026
4ea04d3
Fix existing resources dict keying
jmsunseri May 25, 2026
aaf70bc
Fix project.save() indentation
jmsunseri May 25, 2026
916aa53
Use media map to set resource kind on sync
jmsunseri May 25, 2026
fbae7af
Skip auto-build when GitHub pull has no changes
jmsunseri May 25, 2026
b66e2e8
Remove unused ThreadPoolExecutor import
jmsunseri May 25, 2026
2252cb9
Clear pending timer on ShowPending
jmsunseri May 25, 2026
6499608
Remove force wrapper disabled class toggling
jmsunseri May 25, 2026
a5ace9f
Use github_last_sync in pull complete event
jmsunseri May 25, 2026
a2c69e1
Remove unused manifest_content variable
jmsunseri May 25, 2026
40a8dbd
Handle Redis errors in publish_event
jmsunseri May 25, 2026
c7500a6
Add _parse_bool helper for POST bool parsing
jmsunseri May 25, 2026
e2c25d6
Fix GetUnsavedFiles restore in Sidebar.Refresh
jmsunseri May 25, 2026
264ebb3
Reload active editor after sidebar refresh
jmsunseri Jun 2, 2026
d62d08f
Make github SHA update atomic with file changes
jmsunseri Jun 2, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.env
.env.local
.playwright-mcp/
CLAUDE.md
SECURITYPLAN.md
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ All runtime configuration lives in `.env`. Edit it before building:
| `NODE_VERSION_YCMD` | `16.20.2` | Node.js version in ycmd image |
| `NGINX_PORT` | `8080` | Host port nginx binds to |
| `PUBLIC_URL` | `http://localhost:8080` | Public-facing URL (see note below) |
| `GITHUB_HOOK_URL` | *(uses PUBLIC_URL)* | Base URL for GitHub webhook callbacks (see ngrok note below) |
| `EXPECT_SSL` | `no` | Set to `yes` for HTTPS deployments |
| `EMULATOR_FIXED_LIMIT` | `90` | Max concurrent emulators |
| `LIBPEBBLE_PROXY` | `wss://cloudpebble-proxy.repebble.com/tool` | libpebble proxy WebSocket URL |
Expand All @@ -37,6 +38,29 @@ All runtime configuration lives in `.env`. Edit it before building:

> **Note:** `PUBLIC_URL` tells Django how the outside world reaches the site (used for generating callback URLs, media paths, etc.). `NGINX_PORT` controls which host port nginx binds to. In dev, they typically match; in production behind a reverse proxy, `PUBLIC_URL` is the external URL and `NGINX_PORT` may differ.

### Using ngrok for GitHub webhooks in local development

GitHub requires webhook callback URLs to be publicly reachable — it rejects `localhost` URLs. During local development, use [ngrok](https://ngrok.com) to expose your instance:

```bash
ngrok http 8080
```

Copy the HTTPS URL ngrok gives you (e.g. `https://abc123.ngrok-free.dev`). **Do not** set `PUBLIC_URL` to the ngrok URL — Firebase Auth requires `localhost` for sign-in. Instead, set `GITHUB_HOOK_URL` to the ngrok base URL so only webhook callbacks use it:

```bash
# In .env.local (or .env):
GITHUB_HOOK_URL=https://abc123.ngrok-free.dev
```

Then rebuild and restart:

```bash
docker compose build && docker compose up -d
```

Now you can browse CloudPebble at `http://localhost:8080` (Firebase login works) while GitHub can POST webhook callbacks to the ngrok URL.

### Test GitHub Repo Sync locally

The `GitHub Repo Sync` card changes based on `PUBLIC_URL`, not the browser hostname.
Expand Down
1 change: 1 addition & 0 deletions cloudpebble/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ src/
.env/
.idea/
bower_components
node_modules/
3 changes: 2 additions & 1 deletion cloudpebble/cloudpebble/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,8 @@ def _redis_db_url(redis_url, db_index):
GITHUB_SYNC_CLIENT_ID = _environ.get('GITHUB_SYNC_CLIENT_ID', '')
GITHUB_SYNC_CLIENT_SECRET = _environ.get('GITHUB_SYNC_CLIENT_SECRET', '')

GITHUB_HOOK_TEMPLATE = _environ.get('GITHUB_HOOK', PUBLIC_URL.rstrip('/') + '/ide/project/%(project)d/github/push_hook?key=%(key)s')
GITHUB_HOOK_BASE_URL = _environ.get('GITHUB_HOOK_URL', '')
GITHUB_HOOK_TEMPLATE = (GITHUB_HOOK_BASE_URL.rstrip('/') if GITHUB_HOOK_BASE_URL else PUBLIC_URL.rstrip('/')) + '/ide/project/%(project)d/github/push_hook?key=%(key)s'

SDK2_PEBBLE_WAF = _environ.get('SDK2_PEBBLE_WAF', '/sdk2/pebble/waf')
SDK3_PEBBLE_WAF = _environ.get('SDK3_PEBBLE_WAF', '/sdk3/pebble/waf')
Expand Down
22 changes: 19 additions & 3 deletions cloudpebble/ide/api/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@
__author__ = 'katharine'


def _parse_bool(value, default=False):
if value in (True, False):
return value
if isinstance(value, int):
return bool(value)
if isinstance(value, str):
if value in ('1', 'true', 'True'):
return True
if value in ('0', 'false', 'False', ''):
return False
return default


@login_required
@require_POST
@json_view
Expand All @@ -30,7 +43,8 @@ def github_push(request, project_id):
@json_view
def github_pull(request, project_id):
project = get_object_or_404(Project, pk=project_id, owner=request.user)
task = do_github_pull.delay(project.id)
force = _parse_bool(request.POST.get('force', '0'))
task = do_github_pull.delay(project.id, force=force)
return {'task_id': task.task_id}


Expand All @@ -41,8 +55,9 @@ def set_project_repo(request, project_id):
project = get_object_or_404(Project, pk=project_id, owner=request.user)
repo = request.POST['repo']
branch = request.POST['branch']
auto_pull = bool(int(request.POST['auto_pull']))
auto_build = bool(int(request.POST['auto_build']))
auto_pull = _parse_bool(request.POST['auto_pull'])
auto_build = _parse_bool(request.POST['auto_build'])
hook_force = _parse_bool(request.POST.get('hook_force', '0'))

Comment on lines 44 to 61
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be fixed now

repo = ide.git.url_to_repo(repo)
if repo is None:
Expand Down Expand Up @@ -109,6 +124,7 @@ def set_project_repo(request, project_id):
project.github_hook_uuid = None

project.github_hook_build = auto_build
project.github_hook_force = hook_force

project.save()

Expand Down
5 changes: 3 additions & 2 deletions cloudpebble/ide/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,9 @@ def project_info(request, project_id):
'branch': project.github_branch if project.github_branch is not None else None,
'last_sync': str(project.github_last_sync) if project.github_last_sync is not None else None,
'last_commit': project.github_last_commit,
'auto_build': project.github_hook_build,
'auto_pull': project.github_hook_uuid is not None
'auto_build': project.github_hook_build,
'auto_pull': project.github_hook_uuid is not None,
'hook_force': project.github_hook_force
},
'supported_platforms': project.supported_platforms,
'has_embeddedjs': project.has_embeddedjs_files,
Expand Down
54 changes: 54 additions & 0 deletions cloudpebble/ide/api/sse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import json
import logging

from django.contrib.auth.decorators import login_required
from django.http import StreamingHttpResponse
from django.views.decorators.http import require_GET

from utils.redis_helper import redis_client

logger = logging.getLogger(__name__)


class SSEEventStream:
def __init__(self, project_id):
self.channel = 'project_events:{}'.format(project_id)
self.pubsub = redis_client.pubsub()
self.pubsub.subscribe(self.channel)

def __iter__(self):
try:
for message in self.pubsub.listen():
if message['type'] == 'message':
data = message['data']
if isinstance(data, bytes):
data = data.decode('utf-8')
parsed = json.loads(data)
event_type = parsed.pop('type', 'message')
yield 'event: {}\ndata: {}\n\n'.format(event_type, json.dumps(parsed))
except GeneratorExit:
pass
finally:
try:
self.pubsub.unsubscribe(self.channel)
self.pubsub.close()
except Exception:
pass


@login_required
@require_GET
def project_events(request, project_id):
from ide.models.project import Project
project = Project.objects.filter(pk=project_id, owner=request.user)
if not project.exists():
from django.http import HttpResponseNotFound
return HttpResponseNotFound()

response = StreamingHttpResponse(
SSEEventStream(project_id),
content_type='text/event-stream',
)
response['Cache-Control'] = 'no-cache'
response['X-Accel-Buffering'] = 'no'
return response
16 changes: 16 additions & 0 deletions cloudpebble/ide/migrations/0013_github_hook_force.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('ide', '0012_env_vars'),
]

operations = [
migrations.AddField(
model_name='project',
name='github_hook_force',
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions cloudpebble/ide/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Project(IdeModel):
github_last_commit = models.CharField(max_length=40, blank=True, null=True)
github_hook_uuid = models.CharField(max_length=36, blank=True, null=True)
github_hook_build = models.BooleanField(default=False)
github_hook_force = models.BooleanField(default=False)

project_dependencies = models.ManyToManyField("Project", db_table='cloudpebble_project_dependencies')

Expand Down
10 changes: 10 additions & 0 deletions cloudpebble/ide/static/ide/css/ide.css
Original file line number Diff line number Diff line change
Expand Up @@ -1616,4 +1616,14 @@ label .text-icon {
}
.spinner-light {
background-image: url("/static/ide/img/spinner.gif");
}

.sidebar-pending {
display: inline-block;
font-size: 11px;
color: #ff4700;
margin-left: 6px;
font-family: sans-serif;
font-weight: bold;
vertical-align: middle;
}
Loading
Loading