Skip to content

Commit da87d8b

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 da87d8b

9 files changed

Lines changed: 872 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: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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, timedelta, 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_commits(*, repo: str, branch: str, days: int) -> list[dict]:
38+
since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
39+
commits = github_get_paginated(
40+
f"/repos/{repo}/commits",
41+
params={"sha": branch, "since": since, "per_page": 100},
42+
)
43+
recent: list[dict] = []
44+
for commit in commits:
45+
subject = commit["commit"]["message"].splitlines()[0]
46+
recent.append(
47+
{
48+
"sha": commit["sha"],
49+
"subject": subject,
50+
"html_url": commit["html_url"],
51+
}
52+
)
53+
return recent
54+
55+
56+
def recent_pull_requests(*, repo: str, days: int) -> list[dict]:
57+
pulls = github_get_paginated(
58+
f"/repos/{repo}/pulls",
59+
params={"state": "all", "sort": "updated", "direction": "desc", "per_page": 100},
60+
)
61+
recent = []
62+
for pull in pulls:
63+
if within_last_days(pull.get("updated_at"), days) or within_last_days(pull.get("merged_at"), days):
64+
recent.append(pull)
65+
return recent
66+
67+
68+
def render_pr_list(pulls: list[dict]) -> str:
69+
if not pulls:
70+
return "_No pull requests updated in the last 7 days._\n"
71+
lines = []
72+
for pull in pulls:
73+
number = pull["number"]
74+
title = pull["title"].replace("\n", " ")
75+
url = pull["html_url"]
76+
state = pull.get("state", "unknown")
77+
lines.append(f"- [#{number} {title}]({url}) ({state})")
78+
return "\n".join(lines) + "\n"
79+
80+
81+
def render_commit_list(commits: list[dict], *, empty_message: str = "_None._") -> str:
82+
if not commits:
83+
return f"{empty_message}\n"
84+
lines = []
85+
for commit in commits:
86+
lines.append(f"- [{commit['subject']}]({commit['html_url']}) (`{short_sha(commit['sha'])}`)")
87+
return "\n".join(lines) + "\n"
88+
89+
90+
def build_report_body(
91+
*,
92+
state: SyncState,
93+
days: int,
94+
nrf_repo_name: str,
95+
chip_repo_name: str,
96+
nrf_branch: str = "main",
97+
chip_branch: str = "sdk-nrf",
98+
) -> str:
99+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
100+
chip = state.chip_sync
101+
nrf_commits = recent_commits(repo=nrf_repo_name, branch=nrf_branch, days=days)
102+
chip_commits = recent_commits(repo=chip_repo_name, branch=chip_branch, days=days)
103+
nrf_prs = recent_pull_requests(repo=nrf_repo_name, days=days)
104+
chip_prs = recent_pull_requests(repo=chip_repo_name, days=days)
105+
106+
body = f"""{REPORT_MARKER}
107+
## Weekly SDK sync report
108+
109+
Generated: {now}
110+
111+
### Manifest revisions under test
112+
113+
| Project | Revision |
114+
|---------|----------|
115+
| `sdk-nrf` (`nrf`, `{nrf_branch}`) | `{state.nrf_sha}` |
116+
| `sdk-connectedhomeip` (`matter`, `{chip_branch}`) | `{state.matter_sha}` |
117+
118+
<details>
119+
<summary>sdk-nrf commits on `{nrf_branch}` (last {days} days, {len(nrf_commits)})</summary>
120+
121+
{render_commit_list(nrf_commits, empty_message=f"_No commits on `{nrf_branch}` in the last {days} days._")}
122+
</details>
123+
124+
<details>
125+
<summary>sdk-connectedhomeip commits on `{chip_branch}` (last {days} days, {len(chip_commits)})</summary>
126+
127+
{render_commit_list(chip_commits, empty_message=f"_No commits on `{chip_branch}` in the last {days} days._")}
128+
</details>
129+
130+
<details>
131+
<summary>sdk-nrf pull requests (last {days} days, {len(nrf_prs)})</summary>
132+
133+
{render_pr_list(nrf_prs)}
134+
</details>
135+
136+
<details>
137+
<summary>sdk-connectedhomeip pull requests (last {days} days, {len(chip_prs)})</summary>
138+
139+
{render_pr_list(chip_prs)}
140+
</details>
141+
142+
<details>
143+
<summary>sdk-connectedhomeip commits above merge-base ({len(chip.commits_above_anchor)})</summary>
144+
145+
{render_commit_list([c.__dict__ for c in chip.commits_above_anchor])}
146+
</details>
147+
148+
<details>
149+
<summary>Commits missing {FROM_NRF_MARKER} ({len(chip.commits_missing_from_nrf)})</summary>
150+
151+
{render_commit_list([c.__dict__ for c in chip.commits_missing_from_nrf])}
152+
</details>
153+
"""
154+
return body
155+
156+
157+
def find_existing_comment(*, repo: str, pr_number: int) -> int | None:
158+
comments = github_get_paginated(f"/repos/{repo}/issues/{pr_number}/comments")
159+
for comment in comments:
160+
if REPORT_MARKER in comment.get("body", ""):
161+
return int(comment["id"])
162+
return None
163+
164+
165+
def upsert_pr_comment(*, repo: str, pr_number: int, body: str) -> None:
166+
existing_id = find_existing_comment(repo=repo, pr_number=pr_number)
167+
headers = github_headers()
168+
if existing_id is not None:
169+
response = requests.patch(
170+
f"https://api.github.com/repos/{repo}/issues/comments/{existing_id}",
171+
headers=headers,
172+
json={"body": body},
173+
timeout=60,
174+
)
175+
action = "Updated"
176+
else:
177+
response = requests.post(
178+
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
179+
headers=headers,
180+
json={"body": body},
181+
timeout=60,
182+
)
183+
action = "Posted"
184+
if response.status_code >= 400:
185+
sys.exit(f"Failed to comment on PR #{pr_number}: {response.status_code} {response.text}")
186+
print(f"{action} report comment on PR #{pr_number}")
187+
188+
189+
def main() -> None:
190+
parser = argparse.ArgumentParser(description=__doc__)
191+
parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "nrfconnect/ncs-matter"))
192+
parser.add_argument("--nrf-repo", default=None)
193+
parser.add_argument("--chip-repo", default=None)
194+
parser.add_argument("--nrf-branch", default=os.environ.get("SDK_SYNC_NRF_BRANCH", "main"))
195+
parser.add_argument("--chip-branch", default=os.environ.get("SDK_SYNC_CHIP_BRANCH", "sdk-nrf"))
196+
parser.add_argument("--state-file", type=Path, required=True)
197+
parser.add_argument("--days", type=int, default=7)
198+
args = parser.parse_args()
199+
200+
_ = github_token()
201+
raw = read_json(args.state_file)
202+
if "pr_number" not in raw:
203+
sys.exit("state file is missing pr_number; run prepare_pr.py first")
204+
205+
nrf_repo_name = args.nrf_repo or nrf_repo()
206+
chip_repo_name = args.chip_repo or chip_repo()
207+
state = SyncState.from_dict(raw)
208+
body = build_report_body(
209+
state=state,
210+
days=args.days,
211+
nrf_repo_name=nrf_repo_name,
212+
chip_repo_name=chip_repo_name,
213+
nrf_branch=args.nrf_branch,
214+
chip_branch=args.chip_branch,
215+
)
216+
upsert_pr_comment(repo=args.repo, pr_number=state.pr_number, body=body)
217+
218+
219+
if __name__ == "__main__":
220+
main()

0 commit comments

Comments
 (0)