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
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@

COURSE_RERUN_STATE_SUCCEEDED = "succeeded"
REPOSITORY_NAME_MAX_LENGTH = 100 # Max length from GitHub for repo name

# Debounce settings for the signal handler.
# A single course save triggers 10-30 COURSE_PUBLISHED signals in one request.
# cache.add() on this key ensures only the first signal schedules a task; all
# subsequent signals within the window are silently dropped before hitting the broker.
# The task is scheduled with countdown=EXPORT_DEBOUNCE_DELAY so it runs after
# the burst window has closed and the course state is fully settled.
EXPORT_DEBOUNCE_DELAY = 5 # seconds — must exceed the publish burst window
EXPORT_DEBOUNCE_CACHE_KEY = "git_export_debounce:{course_key}"
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from ol_openedx_git_auto_export.models import CourseGitRepository
from ol_openedx_git_auto_export.utils import (
clear_stale_git_lock,
export_course_to_git,
github_repo_name_format,
is_auto_repo_creation_enabled,
Expand All @@ -39,6 +40,10 @@ def async_export_to_git(course_key_string, user=None):
"Starting async course content export to git (course id: %s)",
course_module.id,
)
# Remove any stale .git/index.lock left by a previously crashed worker.
# Dirty working-tree files from a prior crash are cleaned by the
# `git reset --hard origin/<branch>` + `git clean` inside export_to_git.
clear_stale_git_lock(course_repo.git_url)
export_to_git(course_module.id, course_repo.git_url, user=user)
else:
LOGGER.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
import logging
import os
import re
from pathlib import Path

from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from xmodule.modulestore.django import modulestore

from ol_openedx_git_auto_export.constants import (
ENABLE_AUTO_GITHUB_REPO_CREATION,
ENABLE_GIT_AUTO_EXPORT,
EXPORT_DEBOUNCE_CACHE_KEY,
EXPORT_DEBOUNCE_DELAY,
REPOSITORY_NAME_MAX_LENGTH,
)

Expand Down Expand Up @@ -100,7 +104,41 @@ def export_course_to_git(course_key):
)

user = get_publisher_username(course_module)
async_export_to_git.delay(str(course_key), user)

debounce_key = EXPORT_DEBOUNCE_CACHE_KEY.format(course_key=str(course_key))
if cache.add(debounce_key, "1", timeout=EXPORT_DEBOUNCE_DELAY):
log.info(
"Scheduling git export for course %s with %ds debounce delay",
course_key,
EXPORT_DEBOUNCE_DELAY,
)
async_export_to_git.apply_async(
args=[str(course_key), user],
countdown=EXPORT_DEBOUNCE_DELAY,
)
else:
log.info(
"Git export already scheduled for course %s, skipping duplicate signal",
course_key,
)


def clear_stale_git_lock(git_url):
"""
Remove a stale .git/index.lock file for the local clone of git_url, if present.

A stale lock file can be left behind when a worker process is killed mid-operation.
"""
git_repo_export_dir = getattr(
settings, "GIT_REPO_EXPORT_DIR", "/openedx/export_course_repos"
)
rdir = git_url.rsplit("/", 1)[-1].rsplit(".git", 1)[0]
index_lock = Path(git_repo_export_dir) / rdir / ".git" / "index.lock"
if index_lock.exists():
log.warning(
"Removing stale .git/index.lock for repo %s at %s", git_url, index_lock
)
index_lock.unlink()
Comment on lines +137 to +141
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

clear_stale_git_lock calls index_lock.unlink() after an existence check but doesn’t handle filesystem errors (permission issues, transient disappearance, read-only volumes), which would fail the whole export. Consider wrapping the unlink in a try/except OSError and logging a warning (and optionally continuing), and using is_file()/missing_ok=True to avoid TOCTOU issues.

Suggested change
if index_lock.exists():
log.warning(
"Removing stale .git/index.lock for repo %s at %s", git_url, index_lock
)
index_lock.unlink()
if index_lock.is_file():
log.warning(
"Removing stale .git/index.lock for repo %s at %s", git_url, index_lock
)
try:
index_lock.unlink(missing_ok=True)
except OSError:
log.warning(
"Failed to remove stale .git/index.lock for repo %s at %s",
git_url,
index_lock,
exc_info=True,
)

Copilot uses AI. Check for mistakes.


def is_auto_repo_creation_enabled():
Expand Down
2 changes: 1 addition & 1 deletion src/ol_openedx_git_auto_export/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ol-openedx-git-auto-export"
version = "0.7.1"
version = "0.7.2"
description = "A plugin that auto saves the course OLX to git when an author publishes it"
authors = [
{name = "MIT Office of Digital Learning"}
Expand Down
Loading