Skip to content
Open
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
13 changes: 13 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ curl -X PUT \
-u "you@example.com:YOUR_API_TOKEN" \
-d '["org/repo1", "org/repo2"]'

# Alternatively, configure a repository with additional metadata (like enabling draft PRs) using an object:
curl -X PUT \
"https://your-org.atlassian.net/rest/api/3/project/MYPROJ/properties/forge.repos" \
-H "Content-Type: application/json" \
-u "you@example.com:YOUR_API_TOKEN" \
-d '[
"org/repo1",
{
"name": "org/repo2",
"draft": true
}
]'

# Default repo when no explicit assignment is made
curl -X PUT \
"https://your-org.atlassian.net/rest/api/3/project/MYPROJ/properties/forge.default_repo" \
Expand Down
45 changes: 36 additions & 9 deletions src/forge/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,15 +502,42 @@ async def cmd_project_setup(args: argparse.Namespace) -> int:
try:
# forge.repos
if args.repo:
invalid = [r for r in args.repo if "/" not in r]
if invalid:
print(
f"Error: invalid repo format (expected owner/repo): {invalid}",
file=sys.stderr,
)
return 1
await jira.set_project_property(project_key, "forge.repos", args.repo)
print(f"[OK] forge.repos = {args.repo}")
parsed_repos = []
for r in args.repo:
if r.startswith("{"):
try:
repo_dict = json.loads(r)
except Exception as e:
print(
f"Error: failed to parse JSON repo config {r!r}: {e}",
file=sys.stderr,
)
return 1
if not isinstance(repo_dict, dict) or "name" not in repo_dict:
print(
f"Error: JSON repo config must be a dictionary with a 'name' key, got: {r!r}",
file=sys.stderr,
)
return 1
name = repo_dict["name"]
if not isinstance(name, str) or "/" not in name:
print(
f"Error: repo name in JSON config must contain '/', got: {name!r}",
file=sys.stderr,
)
return 1
parsed_repos.append(repo_dict)
else:
if "/" not in r:
print(
f"Error: invalid repo format (expected owner/repo): {r!r}",
file=sys.stderr,
)
return 1
parsed_repos.append(r)

await jira.set_project_property(project_key, "forge.repos", parsed_repos)
print(f"[OK] forge.repos = {parsed_repos}")

# forge.default_repo
if args.default_repo:
Expand Down
18 changes: 12 additions & 6 deletions src/forge/integrations/github/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ async def create_pull_request(
body: str,
head: str,
base: str = "main",
draft: bool = False,
) -> dict[str, Any]:
"""Create a new pull request.

Expand All @@ -90,19 +91,24 @@ async def create_pull_request(
body: PR description.
head: Source branch name.
base: Target branch name.
draft: Whether the PR should be created as a draft.

Returns:
API response with PR details.
"""
client = await self._get_client()
payload = {
"title": title,
"body": body,
"head": head,
"base": base,
}
if draft:
payload["draft"] = True

response = await client.post(
f"/repos/{owner}/{repo}/pulls",
json={
"title": title,
"body": body,
"head": head,
"base": base,
},
json=payload,
)
response.raise_for_status()
data = response.json()
Expand Down
59 changes: 54 additions & 5 deletions src/forge/integrations/jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,14 +947,63 @@ async def get_project_repos(self, project_key: str) -> list[str]:
value = await self.get_project_property(project_key, "forge.repos")
if value is None:
raise MissingProjectConfig(f"forge.repos not set for project {project_key}")
if not isinstance(value, list) or any(
not isinstance(r, str) or "/" not in r for r in value
):
if not isinstance(value, list):
raise MissingProjectConfig(
f"forge.repos for project {project_key} is malformed: {value!r}"
)
logger.info(f"Project {project_key}: repos from Jira property: {value}")
return value

repos = []
for r in value:
if isinstance(r, str):
if "/" not in r:
raise MissingProjectConfig(
f"forge.repos for project {project_key} is malformed: {value!r}"
)
repos.append(r)
elif isinstance(r, dict):
name = r.get("name")
if not isinstance(name, str) or "/" not in name:
raise MissingProjectConfig(
f"forge.repos for project {project_key} is malformed: {value!r}"
)
repos.append(name)
else:
raise MissingProjectConfig(
f"forge.repos for project {project_key} is malformed: {value!r}"
)

logger.info(f"Project {project_key}: repos from Jira property: {repos}")
return repos

async def is_repo_draft(self, project_key: str, repo_name: str) -> bool:
"""Check if draft PRs are enabled for a given repository.

Args:
project_key: The Jira project key.
repo_name: Name of the repository (e.g. "owner/repo").

Returns:
True if draft is enabled, False otherwise.
"""
try:
value = await self.get_project_property(project_key, "forge.repos")
except Exception:
return False

if not isinstance(value, list):
return False

for r in value:
if isinstance(r, dict):
name = r.get("name")
if isinstance(name, str) and name.lower() == repo_name.lower():
# Check "draft" or "draft_pr"
draft = r.get("draft")
if draft is None:
draft = r.get("draft_pr")
return bool(draft)

return False

async def get_project_default_repo(self, project_key: str) -> str:
"""Fetch the forge.default_repo project property.
Expand Down
4 changes: 4 additions & 0 deletions src/forge/workflow/nodes/pr_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,17 @@ async def create_pull_request(state: WorkflowState) -> WorkflowState:

# Create PR from fork to upstream
# Head format: "fork_owner:branch_name"
project_key = ticket_key.split("-")[0] if "-" in ticket_key else ticket_key
is_draft = await jira.is_repo_draft(project_key, current_repo)

pr_data = await github.create_pull_request(
owner=owner,
repo=repo,
title=pr_title,
body=pr_body,
head=f"{fork_owner}:{branch_name}",
base="main",
draft=is_draft,
)

pr_url = pr_data.get("html_url", "")
Expand Down
1 change: 1 addition & 0 deletions tests/unit/workflow/nodes/test_code_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ async def test_sync_called_after_pr_creation(self):
mock_jira.get_issue = AsyncMock(return_value=MagicMock(summary="Test feature"))
mock_jira.add_comment = AsyncMock()
mock_jira.create_remote_link = AsyncMock()
mock_jira.is_repo_draft = AsyncMock(return_value=False)
mock_jira.close = AsyncMock()

mock_git = MagicMock()
Expand Down
Loading
Loading