Skip to content

Commit dba5868

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 dba5868

9 files changed

Lines changed: 775 additions & 0 deletions

File tree

.github/workflows/sdk-sync.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: SDK Sync
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * 0'
6+
workflow_dispatch:
7+
push:
8+
branches:
9+
- sdk-nrf
10+
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
15+
jobs:
16+
sdk-sync:
17+
runs-on: ubuntu-24.04
18+
env:
19+
SDK_SYNC_PAT: ${{ secrets.SDK_SYNC_PAT }}
20+
GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }}
21+
steps:
22+
- name: Checkout the code
23+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
24+
with:
25+
fetch-depth: 0
26+
persist-credentials: false
27+
28+
- name: Set state file path
29+
run: echo "STATE_FILE=${RUNNER_TEMP}/sdk_sync_state.json" >> "$GITHUB_ENV"
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
33+
with:
34+
python-version: '3.12'
35+
cache: pip
36+
cache-dependency-path: scripts/sdk_sync/requirements.txt
37+
38+
- name: Install dependencies
39+
run: pip install -r scripts/sdk_sync/requirements.txt
40+
41+
- name: Configure git credentials
42+
run: |
43+
git remote set-url origin "https://x-access-token:${SDK_SYNC_PAT}@github.com/${{ github.repository }}.git"
44+
git config --global user.email "sdk-sync-bot@users.noreply.github.com"
45+
git config --global user.name "sdk-sync-bot"
46+
47+
- name: Rebase sdk-nrf onto main
48+
run: python scripts/sdk_sync/rebase_branch.py
49+
50+
- name: Prepare test PR branch
51+
run: python scripts/sdk_sync/prepare_pr.py --state-file "${STATE_FILE}"
52+
53+
- name: Collect upstream revision report
54+
run: python scripts/sdk_sync/chip_sync_report.py --state-file "${STATE_FILE}"
55+
56+
- name: Update west.yml on PR branch
57+
run: |
58+
git checkout sdk-sync/test
59+
python scripts/sdk_sync/update_manifest.py --state-file "${STATE_FILE}" --branch sdk-sync/test
60+
61+
- name: Post sync summary comment
62+
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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
github_get,
24+
github_get_paginated,
25+
github_headers,
26+
github_token,
27+
read_json,
28+
short_sha,
29+
within_last_days,
30+
)
31+
32+
import requests
33+
34+
NRF_REPO = "nrfconnect/sdk-nrf"
35+
CHIP_REPO = "nrfconnect/sdk-connectedhomeip"
36+
37+
38+
def recent_pull_requests(*, repo: str, days: int) -> list[dict]:
39+
pulls = github_get_paginated(
40+
f"/repos/{repo}/pulls",
41+
params={"state": "all", "sort": "updated", "direction": "desc", "per_page": 100},
42+
)
43+
recent = []
44+
for pull in pulls:
45+
if within_last_days(pull.get("updated_at"), days) or within_last_days(pull.get("merged_at"), days):
46+
recent.append(pull)
47+
return recent
48+
49+
50+
def render_pr_list(pulls: list[dict]) -> str:
51+
if not pulls:
52+
return "_No pull requests updated in the last 7 days._\n"
53+
lines = []
54+
for pull in pulls:
55+
number = pull["number"]
56+
title = pull["title"].replace("\n", " ")
57+
url = pull["html_url"]
58+
state = pull.get("state", "unknown")
59+
lines.append(f"- [#{number} {title}]({url}) ({state})")
60+
return "\n".join(lines) + "\n"
61+
62+
63+
def render_commit_list(commits: list[dict]) -> str:
64+
if not commits:
65+
return "_None._\n"
66+
lines = []
67+
for commit in commits:
68+
lines.append(f"- [{commit['subject']}]({commit['html_url']}) (`{short_sha(commit['sha'])}`)")
69+
return "\n".join(lines) + "\n"
70+
71+
72+
def build_report_body(*, state: SyncState, days: int) -> str:
73+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
74+
chip = state.chip_sync
75+
nrf_prs = recent_pull_requests(repo=NRF_REPO, days=days)
76+
chip_prs = recent_pull_requests(repo=CHIP_REPO, days=days)
77+
78+
body = f"""{REPORT_MARKER}
79+
## Weekly SDK sync report
80+
81+
Generated: {now}
82+
83+
### Manifest revisions under test
84+
85+
| Project | Revision |
86+
|---------|----------|
87+
| `sdk-nrf` (`nrf`) | `{state.nrf_sha}` |
88+
| `sdk-connectedhomeip` (`matter`, `sdk-nrf` branch) | `{state.matter_sha}` |
89+
90+
<details>
91+
<summary>sdk-nrf pull requests (last {days} days)</summary>
92+
93+
{render_pr_list(nrf_prs)}
94+
</details>
95+
96+
<details>
97+
<summary>sdk-connectedhomeip pull requests (last {days} days)</summary>
98+
99+
{render_pr_list(chip_prs)}
100+
</details>
101+
102+
<details>
103+
<summary>sdk-connectedhomeip commits above merge-base ({len(chip.commits_above_anchor)})</summary>
104+
105+
{render_commit_list([c.__dict__ for c in chip.commits_above_anchor])}
106+
</details>
107+
108+
<details>
109+
<summary>Commits missing {FROM_NRF_MARKER} ({len(chip.commits_missing_from_nrf)})</summary>
110+
111+
{render_commit_list([c.__dict__ for c in chip.commits_missing_from_nrf])}
112+
</details>
113+
"""
114+
return body
115+
116+
117+
def find_existing_comment(*, repo: str, pr_number: int) -> int | None:
118+
comments = github_get_paginated(f"/repos/{repo}/issues/{pr_number}/comments")
119+
for comment in comments:
120+
if REPORT_MARKER in comment.get("body", ""):
121+
return int(comment["id"])
122+
return None
123+
124+
125+
def upsert_pr_comment(*, repo: str, pr_number: int, body: str) -> None:
126+
existing_id = find_existing_comment(repo=repo, pr_number=pr_number)
127+
headers = github_headers()
128+
if existing_id is not None:
129+
response = requests.patch(
130+
f"https://api.github.com/repos/{repo}/issues/comments/{existing_id}",
131+
headers=headers,
132+
json={"body": body},
133+
timeout=60,
134+
)
135+
action = "Updated"
136+
else:
137+
response = requests.post(
138+
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
139+
headers=headers,
140+
json={"body": body},
141+
timeout=60,
142+
)
143+
action = "Posted"
144+
if response.status_code >= 400:
145+
sys.exit(f"Failed to comment on PR #{pr_number}: {response.status_code} {response.text}")
146+
print(f"{action} report comment on PR #{pr_number}")
147+
148+
149+
def main() -> None:
150+
parser = argparse.ArgumentParser(description=__doc__)
151+
parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "nrfconnect/ncs-matter"))
152+
parser.add_argument("--state-file", type=Path, required=True)
153+
parser.add_argument("--days", type=int, default=7)
154+
args = parser.parse_args()
155+
156+
_ = github_token()
157+
raw = read_json(args.state_file)
158+
if "pr_number" not in raw:
159+
sys.exit("state file is missing pr_number; run prepare_pr.py first")
160+
161+
state = SyncState.from_dict(raw)
162+
body = build_report_body(state=state, days=args.days)
163+
upsert_pr_comment(repo=args.repo, pr_number=state.pr_number, body=body)
164+
165+
166+
if __name__ == "__main__":
167+
main()
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
github_get,
21+
read_json,
22+
write_json,
23+
)
24+
25+
CHIP_REPO = "nrfconnect/sdk-connectedhomeip"
26+
NRF_REPO = "nrfconnect/sdk-nrf"
27+
28+
29+
def branch_tip(*, repo: str, branch: str) -> str:
30+
ref = github_get(f"/repos/{repo}/git/ref/heads/{branch}")
31+
return ref["object"]["sha"]
32+
33+
34+
def merge_base(*, repo: str, base: str, head: str) -> str:
35+
comparison = github_get(f"/repos/{repo}/compare/{base}...{head}")
36+
return comparison["merge_base_commit"]["sha"]
37+
38+
39+
def commits_on_branch(*, repo: str, branch: str, since_sha: str) -> list[CommitInfo]:
40+
comparison = github_get(f"/repos/{repo}/compare/{since_sha}...{branch}")
41+
commits: list[CommitInfo] = []
42+
for commit in comparison.get("commits", []):
43+
subject = commit["commit"]["message"].splitlines()[0]
44+
commits.append(
45+
CommitInfo(
46+
sha=commit["sha"],
47+
subject=subject,
48+
html_url=commit["html_url"],
49+
has_from_nrf=FROM_NRF_MARKER in subject,
50+
)
51+
)
52+
return commits
53+
54+
55+
def build_chip_sync_report(*, chip_repo: str = CHIP_REPO) -> ChipSyncReport:
56+
master_sha = branch_tip(repo=chip_repo, branch="master")
57+
sdk_nrf_sha = branch_tip(repo=chip_repo, branch="sdk-nrf")
58+
anchor_sha = merge_base(repo=chip_repo, base="master", head="sdk-nrf")
59+
above_anchor = commits_on_branch(repo=chip_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: str = NRF_REPO, branch: str = "main") -> str:
71+
return branch_tip(repo=nrf_repo, branch=branch)
72+
73+
74+
def main() -> None:
75+
parser = argparse.ArgumentParser(description=__doc__)
76+
parser.add_argument("--chip-repo", default=CHIP_REPO)
77+
parser.add_argument("--nrf-repo", default=NRF_REPO)
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_report = build_chip_sync_report(chip_repo=args.chip_repo)
83+
nrf_sha = nrf_main_branch_sha(nrf_repo=args.nrf_repo, branch=args.nrf_branch)
84+
85+
if args.state_file.exists():
86+
state = read_json(args.state_file)
87+
else:
88+
state = {}
89+
90+
state["chip_sync"] = chip_report.to_dict()
91+
state["matter_sha"] = chip_report.sdk_nrf_branch_sha
92+
state["nrf_sha"] = nrf_sha
93+
write_json(args.state_file, state)
94+
95+
print(f"sdk-connectedhomeip master: {chip_report.master_sha}")
96+
print(f"sdk-connectedhomeip sdk-nrf: {chip_report.sdk_nrf_branch_sha}")
97+
print(f"merge-base anchor: {chip_report.merge_base_sha}")
98+
print(f"commits above anchor: {len(chip_report.commits_above_anchor)}")
99+
print(f"commits missing {FROM_NRF_MARKER}: {len(chip_report.commits_missing_from_nrf)}")
100+
print(f"sdk-nrf {args.nrf_branch}: {nrf_sha}")
101+
102+
103+
if __name__ == "__main__":
104+
main()

0 commit comments

Comments
 (0)