Skip to content

Commit c2b0843

Browse files
ci: Synchronize with sdk-nrf
This commit is dedicated to checking synchronization between ncs-matter and the newest sdk-nrf and sdk-connectedhomeip. Signed-off-by: Arkadiusz Balys <arkadiusz.balys@nordicsemi.no>
1 parent cce83b9 commit c2b0843

9 files changed

Lines changed: 827 additions & 0 deletions

File tree

.github/workflows/sdk-sync.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: SDK Sync
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * 0'
6+
workflow_dispatch:
7+
inputs:
8+
nrf_repo:
9+
description: 'sdk-nrf repository (owner/name)'
10+
required: false
11+
default: 'ArekBalysNordic/sdk-nrf'
12+
chip_repo:
13+
description: 'sdk-connectedhomeip repository (owner/name)'
14+
required: false
15+
default: 'ArekBalysNordic/sdk-connectedhomeip'
16+
push:
17+
branches:
18+
- sdk-nrf
19+
20+
permissions:
21+
contents: write
22+
pull-requests: write
23+
24+
jobs:
25+
sdk-sync:
26+
runs-on: ubuntu-24.04
27+
env:
28+
SDK_SYNC_PAT: ${{ secrets.SDK_SYNC_PAT }}
29+
GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }}
30+
SDK_SYNC_NRF_REPO: ${{ inputs.nrf_repo || 'ArekBalysNordic/sdk-nrf' }}
31+
SDK_SYNC_CHIP_REPO: ${{ inputs.chip_repo || 'ArekBalysNordic/sdk-connectedhomeip' }}
32+
steps:
33+
- name: Checkout the code
34+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
35+
with:
36+
fetch-depth: 0
37+
persist-credentials: false
38+
39+
- name: Set state file path
40+
run: echo "STATE_FILE=${RUNNER_TEMP}/sdk_sync_state.json" >> "$GITHUB_ENV"
41+
42+
- name: Set up Python
43+
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
44+
with:
45+
python-version: '3.12'
46+
cache: pip
47+
cache-dependency-path: scripts/sdk_sync/requirements.txt
48+
49+
- name: Install dependencies
50+
run: pip install -r scripts/sdk_sync/requirements.txt
51+
52+
- name: Configure git credentials
53+
run: |
54+
git remote set-url origin "https://x-access-token:${SDK_SYNC_PAT}@github.com/${{ github.repository }}.git"
55+
git config --global user.email "sdk-sync-bot@users.noreply.github.com"
56+
git config --global user.name "sdk-sync-bot"
57+
58+
- name: Rebase sdk-nrf onto main
59+
run: python scripts/sdk_sync/rebase_branch.py
60+
61+
- name: Prepare test PR branch
62+
run: python scripts/sdk_sync/prepare_pr.py --state-file "${STATE_FILE}"
63+
64+
- name: Collect upstream revision report
65+
run: python scripts/sdk_sync/chip_sync_report.py --state-file "${STATE_FILE}"
66+
67+
- name: Update west.yml on PR branch
68+
run: |
69+
git checkout sdk-sync/test
70+
python scripts/sdk_sync/update_manifest.py --state-file "${STATE_FILE}" --branch sdk-sync/test
71+
72+
- name: Post sync summary comment
73+
run: python scripts/sdk_sync/build_report.py --state-file "${STATE_FILE}"

scripts/sdk_sync/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Scripts for weekly sdk-nrf / sdk-connectedhomeip synchronization."""

scripts/sdk_sync/build_report.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Build and post the sdk-sync summary comment on the test PR."""
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import os
10+
import sys
11+
from datetime import datetime, timezone
12+
from pathlib import Path
13+
14+
SCRIPT_DIR = Path(__file__).resolve().parent
15+
if str(SCRIPT_DIR) not in sys.path:
16+
sys.path.insert(0, str(SCRIPT_DIR))
17+
18+
from common import ( # noqa: E402
19+
FROM_NRF_MARKER,
20+
REPORT_MARKER,
21+
ChipSyncReport,
22+
SyncState,
23+
chip_repo,
24+
github_get,
25+
github_get_paginated,
26+
github_headers,
27+
github_token,
28+
nrf_repo,
29+
read_json,
30+
short_sha,
31+
within_last_days,
32+
)
33+
34+
import requests
35+
36+
37+
def recent_pull_requests(*, repo: str, days: int) -> list[dict]:
38+
pulls = github_get_paginated(
39+
f"/repos/{repo}/pulls",
40+
params={"state": "all", "sort": "updated", "direction": "desc", "per_page": 100},
41+
)
42+
recent = []
43+
for pull in pulls:
44+
if within_last_days(pull.get("updated_at"), days) or within_last_days(pull.get("merged_at"), days):
45+
recent.append(pull)
46+
return recent
47+
48+
49+
def render_pr_list(pulls: list[dict]) -> str:
50+
if not pulls:
51+
return "_No pull requests updated in the last 7 days._\n"
52+
lines = []
53+
for pull in pulls:
54+
number = pull["number"]
55+
title = pull["title"].replace("\n", " ")
56+
url = pull["html_url"]
57+
state = pull.get("state", "unknown")
58+
lines.append(f"- [#{number} {title}]({url}) ({state})")
59+
return "\n".join(lines) + "\n"
60+
61+
62+
def render_commit_list(commits: list[dict]) -> str:
63+
if not commits:
64+
return "_None._\n"
65+
lines = []
66+
for commit in commits:
67+
lines.append(f"- [{commit['subject']}]({commit['html_url']}) (`{short_sha(commit['sha'])}`)")
68+
return "\n".join(lines) + "\n"
69+
70+
71+
def build_report_body(*, state: SyncState, days: int, nrf_repo_name: str, chip_repo_name: str) -> str:
72+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
73+
chip = state.chip_sync
74+
nrf_prs = recent_pull_requests(repo=nrf_repo_name, days=days)
75+
chip_prs = recent_pull_requests(repo=chip_repo_name, days=days)
76+
77+
body = f"""{REPORT_MARKER}
78+
## Weekly SDK sync report
79+
80+
Generated: {now}
81+
82+
### Manifest revisions under test
83+
84+
| Project | Revision |
85+
|---------|----------|
86+
| `sdk-nrf` (`nrf`) | `{state.nrf_sha}` |
87+
| `sdk-connectedhomeip` (`matter`, `sdk-nrf` branch) | `{state.matter_sha}` |
88+
89+
<details>
90+
<summary>sdk-nrf pull requests (last {days} days)</summary>
91+
92+
{render_pr_list(nrf_prs)}
93+
</details>
94+
95+
<details>
96+
<summary>sdk-connectedhomeip pull requests (last {days} days)</summary>
97+
98+
{render_pr_list(chip_prs)}
99+
</details>
100+
101+
<details>
102+
<summary>sdk-connectedhomeip commits above merge-base ({len(chip.commits_above_anchor)})</summary>
103+
104+
{render_commit_list([c.__dict__ for c in chip.commits_above_anchor])}
105+
</details>
106+
107+
<details>
108+
<summary>Commits missing {FROM_NRF_MARKER} ({len(chip.commits_missing_from_nrf)})</summary>
109+
110+
{render_commit_list([c.__dict__ for c in chip.commits_missing_from_nrf])}
111+
</details>
112+
"""
113+
return body
114+
115+
116+
def find_existing_comment(*, repo: str, pr_number: int) -> int | None:
117+
comments = github_get_paginated(f"/repos/{repo}/issues/{pr_number}/comments")
118+
for comment in comments:
119+
if REPORT_MARKER in comment.get("body", ""):
120+
return int(comment["id"])
121+
return None
122+
123+
124+
def upsert_pr_comment(*, repo: str, pr_number: int, body: str) -> None:
125+
existing_id = find_existing_comment(repo=repo, pr_number=pr_number)
126+
headers = github_headers()
127+
if existing_id is not None:
128+
response = requests.patch(
129+
f"https://api.github.com/repos/{repo}/issues/comments/{existing_id}",
130+
headers=headers,
131+
json={"body": body},
132+
timeout=60,
133+
)
134+
action = "Updated"
135+
else:
136+
response = requests.post(
137+
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
138+
headers=headers,
139+
json={"body": body},
140+
timeout=60,
141+
)
142+
action = "Posted"
143+
if response.status_code >= 400:
144+
sys.exit(f"Failed to comment on PR #{pr_number}: {response.status_code} {response.text}")
145+
print(f"{action} report comment on PR #{pr_number}")
146+
147+
148+
def main() -> None:
149+
parser = argparse.ArgumentParser(description=__doc__)
150+
parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "nrfconnect/ncs-matter"))
151+
parser.add_argument("--nrf-repo", default=None)
152+
parser.add_argument("--chip-repo", default=None)
153+
parser.add_argument("--state-file", type=Path, required=True)
154+
parser.add_argument("--days", type=int, default=7)
155+
args = parser.parse_args()
156+
157+
_ = github_token()
158+
raw = read_json(args.state_file)
159+
if "pr_number" not in raw:
160+
sys.exit("state file is missing pr_number; run prepare_pr.py first")
161+
162+
nrf_repo_name = args.nrf_repo or nrf_repo()
163+
chip_repo_name = args.chip_repo or chip_repo()
164+
state = SyncState.from_dict(raw)
165+
body = build_report_body(
166+
state=state,
167+
days=args.days,
168+
nrf_repo_name=nrf_repo_name,
169+
chip_repo_name=chip_repo_name,
170+
)
171+
upsert_pr_comment(repo=args.repo, pr_number=state.pr_number, body=body)
172+
173+
174+
if __name__ == "__main__":
175+
main()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Compare sdk-connectedhomeip master and sdk-nrf branches."""
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import sys
10+
from pathlib import Path
11+
12+
SCRIPT_DIR = Path(__file__).resolve().parent
13+
if str(SCRIPT_DIR) not in sys.path:
14+
sys.path.insert(0, str(SCRIPT_DIR))
15+
16+
from common import ( # noqa: E402
17+
FROM_NRF_MARKER,
18+
ChipSyncReport,
19+
CommitInfo,
20+
chip_repo,
21+
github_get,
22+
nrf_repo,
23+
read_json,
24+
write_json,
25+
)
26+
27+
28+
def branch_tip(*, repo: str, branch: str) -> str:
29+
ref = github_get(f"/repos/{repo}/git/ref/heads/{branch}")
30+
return ref["object"]["sha"]
31+
32+
33+
def merge_base(*, repo: str, base: str, head: str) -> str:
34+
comparison = github_get(f"/repos/{repo}/compare/{base}...{head}")
35+
return comparison["merge_base_commit"]["sha"]
36+
37+
38+
def commits_on_branch(*, repo: str, branch: str, since_sha: str) -> list[CommitInfo]:
39+
comparison = github_get(f"/repos/{repo}/compare/{since_sha}...{branch}")
40+
commits: list[CommitInfo] = []
41+
for commit in comparison.get("commits", []):
42+
subject = commit["commit"]["message"].splitlines()[0]
43+
commits.append(
44+
CommitInfo(
45+
sha=commit["sha"],
46+
subject=subject,
47+
html_url=commit["html_url"],
48+
has_from_nrf=FROM_NRF_MARKER in subject,
49+
)
50+
)
51+
return commits
52+
53+
54+
def build_chip_sync_report(*, chip_repo_name: str | None = None) -> ChipSyncReport:
55+
repo = chip_repo_name or chip_repo()
56+
master_sha = branch_tip(repo=repo, branch="master")
57+
sdk_nrf_sha = branch_tip(repo=repo, branch="sdk-nrf")
58+
anchor_sha = merge_base(repo=repo, base="master", head="sdk-nrf")
59+
above_anchor = commits_on_branch(repo=repo, branch="sdk-nrf", since_sha=anchor_sha)
60+
missing = [commit for commit in above_anchor if not commit.has_from_nrf]
61+
return ChipSyncReport(
62+
master_sha=master_sha,
63+
sdk_nrf_branch_sha=sdk_nrf_sha,
64+
merge_base_sha=anchor_sha,
65+
commits_above_anchor=above_anchor,
66+
commits_missing_from_nrf=missing,
67+
)
68+
69+
70+
def nrf_main_branch_sha(*, nrf_repo_name: str | None = None, branch: str = "main") -> str:
71+
return branch_tip(repo=nrf_repo_name or nrf_repo(), branch=branch)
72+
73+
74+
def main() -> None:
75+
parser = argparse.ArgumentParser(description=__doc__)
76+
parser.add_argument("--chip-repo", default=None)
77+
parser.add_argument("--nrf-repo", default=None)
78+
parser.add_argument("--nrf-branch", default="main")
79+
parser.add_argument("--state-file", type=Path, required=True)
80+
args = parser.parse_args()
81+
82+
chip_repo_name = args.chip_repo or chip_repo()
83+
nrf_repo_name = args.nrf_repo or nrf_repo()
84+
chip_report = build_chip_sync_report(chip_repo_name=chip_repo_name)
85+
nrf_sha = nrf_main_branch_sha(nrf_repo_name=nrf_repo_name, branch=args.nrf_branch)
86+
87+
if args.state_file.exists():
88+
state = read_json(args.state_file)
89+
else:
90+
state = {}
91+
92+
state["chip_sync"] = chip_report.to_dict()
93+
state["matter_sha"] = chip_report.sdk_nrf_branch_sha
94+
state["nrf_sha"] = nrf_sha
95+
write_json(args.state_file, state)
96+
97+
print(f"sdk-connectedhomeip master: {chip_report.master_sha}")
98+
print(f"sdk-connectedhomeip sdk-nrf: {chip_report.sdk_nrf_branch_sha}")
99+
print(f"merge-base anchor: {chip_report.merge_base_sha}")
100+
print(f"commits above anchor: {len(chip_report.commits_above_anchor)}")
101+
print(f"commits missing {FROM_NRF_MARKER}: {len(chip_report.commits_missing_from_nrf)}")
102+
print(f"sdk-nrf {args.nrf_branch}: {nrf_sha}")
103+
104+
105+
if __name__ == "__main__":
106+
main()

0 commit comments

Comments
 (0)