Skip to content

Commit fe79d4e

Browse files
feat: validate labels exist in repo before syncing
Adds a pre-flight check in execute_sync that fetches all label names from the target repository and raises a ValueError listing any labels referenced in TASKS.md that don't exist yet. This prevents the perpetual-update loop where the tool detects a label diff, silently fails to resolve the label IDs, and reports the same 5 updates on every run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dba4599 commit fe79d4e

File tree

2 files changed

+45
-0
lines changed

2 files changed

+45
-0
lines changed

tasksmd_sync/github_projects.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,24 @@ def set_issue_labels(self, issue_id: str, label_ids: list[str]) -> None:
600600
"""
601601
self._graphql(mutation, {"issueId": issue_id, "labelIds": label_ids})
602602

603+
def list_label_names(self, repo_owner: str, repo_name: str) -> set[str]:
604+
"""Return the set of label names (lowercased) defined in a repository."""
605+
query = """
606+
query($owner: String!, $name: String!) {
607+
repository(owner: $owner, name: $name) {
608+
labels(first: 100) {
609+
nodes { name }
610+
}
611+
}
612+
}
613+
"""
614+
data = self._graphql(query, {"owner": repo_owner, "name": repo_name})
615+
return {
616+
node["name"].lower()
617+
for node in data["repository"]["labels"]["nodes"]
618+
if node.get("name")
619+
}
620+
603621
def resolve_label_ids(
604622
self, repo_owner: str, repo_name: str, label_names: list[str]
605623
) -> list[str]:

tasksmd_sync/sync.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ def execute_sync(
159159
"""
160160
result = SyncResult()
161161

162+
if repo_owner and repo_name:
163+
_validate_labels(client, task_file, repo_owner, repo_name)
164+
162165
logger.info("Fetching current board state...")
163166
board_items = client.list_items()
164167
logger.info("Found %d items on the board", len(board_items))
@@ -382,6 +385,30 @@ def _title_fallback_match(
382385
return None
383386

384387

388+
def _validate_labels(
389+
client: GitHubProjectClient,
390+
task_file: TaskFile,
391+
repo_owner: str,
392+
repo_name: str,
393+
) -> None:
394+
"""Raise ValueError if any labels in TASKS.md don't exist in the repository.
395+
396+
This prevents silent label-sync failures caused by referencing label names
397+
that haven't been created yet.
398+
"""
399+
wanted = {label for task in task_file.tasks for label in task.labels}
400+
if not wanted:
401+
return
402+
existing = client.list_label_names(repo_owner, repo_name)
403+
missing = sorted(label for label in wanted if label.lower() not in existing)
404+
if missing:
405+
raise ValueError(
406+
f"TASKS.md references label(s) that don't exist in "
407+
f"{repo_owner}/{repo_name}: {', '.join(missing)}. "
408+
f"Create them first or remove them from TASKS.md."
409+
)
410+
411+
385412
def _needs_update(task: Task, board_item: ProjectItem) -> bool:
386413
"""Check if a task's fields differ from the board item."""
387414
if task.title != board_item.title:

0 commit comments

Comments
 (0)