Skip to content

Commit 82cbe67

Browse files
feat: handle multiple source paths per source repo
1 parent f3cd5c8 commit 82cbe67

1 file changed

Lines changed: 151 additions & 41 deletions

File tree

.github/workflows/syncweaver-update-source.yml

Lines changed: 151 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ on:
55
workflow_dispatch:
66
inputs:
77
source_path:
8-
description: Tracked source path in the host repository.
8+
description: Optional tracked source path override for manual runs; if omitted, source_path is resolved from the lockfile.
99
required: false
1010
type: string
1111
repo_url:
12-
description: Optional source repository URL used to resolve source_path from lockfile.
12+
description: Optional source repository identifier (URL or OWNER/REPO) used to resolve source_path from lockfile; for repository_dispatch this defaults to client_payload.source_repository.
1313
required: false
1414
type: string
1515
lockfile:
@@ -31,18 +31,158 @@ permissions:
3131
pull-requests: write
3232

3333
jobs:
34-
update-source:
34+
resolve-source-paths:
3535
runs-on: ubuntu-latest
3636
outputs:
37-
source_url: ${{ steps.update_source.outputs.source_url }}
38-
ref: ${{ steps.update_source.outputs.ref }}
39-
remote_subdir: ${{ steps.update_source.outputs.remote_subdir }}
37+
source_paths: ${{ steps.resolve_source_paths.outputs.source_paths }}
38+
source_count: ${{ steps.resolve_source_paths.outputs.source_count }}
39+
lockfile: ${{ env.LOCKFILE }}
40+
ref: ${{ env.REF }}
41+
remote_subdir: ${{ env.REMOTE_SUBDIR }}
42+
repo_url: ${{ env.REPO_URL }}
4043
env:
44+
# workflow_dispatch may provide source_path directly; repository_dispatch usually
45+
# omits it so the workflow resolves source_path from lockfile + source_repository.
4146
SOURCE_PATH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.source_path || github.event.client_payload.source_path || '' }}
47+
# Prefer explicit repo_url, then fall back to source_repository from dispatch payload.
4248
REPO_URL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.repo_url || github.event.client_payload.repo_url || github.event.client_payload.source_repository || '' }}
4349
LOCKFILE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.lockfile || github.event.client_payload.lockfile || '.syncweaver-lock.json' }}
4450
REF: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.event.client_payload.ref || '' }}
4551
REMOTE_SUBDIR: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.remote_subdir || github.event.client_payload.remote_subdir || '' }}
52+
steps:
53+
- name: Checkout repository
54+
uses: actions/checkout@v6
55+
56+
- name: Resolve source paths
57+
id: resolve_source_paths
58+
shell: python
59+
env:
60+
SOURCE_PATH_INPUT: ${{ env.SOURCE_PATH }}
61+
REPO_URL_INPUT: ${{ env.REPO_URL }}
62+
LOCKFILE_PATH: ${{ env.LOCKFILE }}
63+
run: |
64+
import json
65+
import os
66+
import pathlib
67+
68+
def normalize_remote_url(url: str) -> str:
69+
stripped = url.strip()
70+
normalized = stripped
71+
if stripped.startswith("git@") and ":" in stripped:
72+
host_part, path_part = stripped.split(":", 1)
73+
host = host_part.split("@", 1)[1]
74+
path = path_part.removesuffix(".git")
75+
normalized = f"https://{host}/{path}"
76+
elif stripped.startswith("http://") or stripped.startswith("https://"):
77+
normalized = stripped.removesuffix(".git")
78+
elif "/" in stripped and "@" not in stripped and "://" not in stripped:
79+
parts = [part for part in stripped.split("/") if part]
80+
if len(parts) == 2:
81+
normalized = f"https://github.com/{parts[0]}/{parts[1]}"
82+
elif len(parts) >= 3 and "." in parts[0]:
83+
normalized = f"https://{'/'.join(parts)}"
84+
normalized = normalized.removesuffix(".git")
85+
return normalized
86+
87+
def upgrade_legacy_lockfile_shape(lock_data: dict) -> dict:
88+
upgraded = lock_data
89+
has_sources = isinstance(upgraded.get("sources"), dict)
90+
has_legacy_repos = isinstance(upgraded.get("repos"), dict)
91+
if has_legacy_repos and not has_sources:
92+
sources: dict[str, dict] = {}
93+
legacy_repos = upgraded.get("repos", {})
94+
for repo_url, repo_entry in legacy_repos.items():
95+
legacy_sources = repo_entry.get("sources", {})
96+
for source_path, source_entry in legacy_sources.items():
97+
upgraded_entry = dict(source_entry)
98+
if "branch" in upgraded_entry and "ref" not in upgraded_entry:
99+
upgraded_entry["ref"] = upgraded_entry.pop("branch")
100+
upgraded_entry["repo_url"] = repo_url
101+
existing_entry = sources.get(source_path)
102+
if existing_entry and existing_entry != upgraded_entry:
103+
raise ValueError(
104+
"Legacy lockfile has conflicting tracked entries for "
105+
f"source path: {source_path}"
106+
)
107+
sources[source_path] = upgraded_entry
108+
upgraded["sources"] = sources
109+
upgraded.pop("repos", None)
110+
return upgraded
111+
112+
source_path_input = os.environ["SOURCE_PATH_INPUT"].strip()
113+
repo_url_input = os.environ["REPO_URL_INPUT"].strip()
114+
lockfile_path = pathlib.Path(os.environ["LOCKFILE_PATH"])
115+
output_path = pathlib.Path(os.environ["GITHUB_OUTPUT"])
116+
117+
if source_path_input:
118+
resolved_source_paths = [source_path_input]
119+
else:
120+
if not lockfile_path.exists():
121+
raise SystemExit(
122+
"Error: lockfile does not exist and source_path was not provided: "
123+
f"{lockfile_path}"
124+
)
125+
126+
lock_data = json.loads(lockfile_path.read_text())
127+
lock_data = upgrade_legacy_lockfile_shape(lock_data)
128+
sources = lock_data.get("sources", {})
129+
has_sources = isinstance(sources, dict) and bool(sources)
130+
if not has_sources:
131+
raise SystemExit(
132+
"Error: no tracked sources found in lockfile and source_path was not provided"
133+
)
134+
135+
if repo_url_input:
136+
normalized_repo_url = normalize_remote_url(repo_url_input)
137+
matching_source_paths: list[str] = []
138+
for path_key, source_entry in sources.items():
139+
entry_repo_url = str(source_entry.get("repo_url", "")).strip()
140+
if entry_repo_url:
141+
normalized_entry_url = normalize_remote_url(entry_repo_url)
142+
if normalized_entry_url == normalized_repo_url:
143+
matching_source_paths.append(str(path_key))
144+
145+
if not matching_source_paths:
146+
raise SystemExit(
147+
"Error: source_path was not provided and no tracked sources matched "
148+
f"repo_url: {repo_url_input}"
149+
)
150+
151+
resolved_source_paths = sorted(matching_source_paths)
152+
else:
153+
source_paths = sorted(str(path) for path in sources.keys())
154+
if len(source_paths) != 1:
155+
source_paths_csv = ", ".join(source_paths)
156+
raise SystemExit(
157+
"Error: source_path was not provided and lockfile tracks multiple sources. "
158+
"Please provide source_path explicitly or set repo_url. "
159+
f"Tracked sources: {source_paths_csv}"
160+
)
161+
resolved_source_paths = source_paths
162+
163+
with output_path.open("a", encoding="utf-8") as fh:
164+
fh.write(f"source_paths={json.dumps(resolved_source_paths)}\n")
165+
fh.write(f"source_count={len(resolved_source_paths)}\n")
166+
167+
- name: Print resolved source paths
168+
shell: bash
169+
run: |
170+
set -euo pipefail
171+
echo "source_count=${{ steps.resolve_source_paths.outputs.source_count }}"
172+
echo "source_paths=${{ steps.resolve_source_paths.outputs.source_paths }}"
173+
174+
update-source:
175+
runs-on: ubuntu-latest
176+
needs: resolve-source-paths
177+
strategy:
178+
fail-fast: false
179+
matrix:
180+
source_path: ${{ fromJson(needs.resolve-source-paths.outputs.source_paths) }}
181+
env:
182+
LOCKFILE: ${{ needs.resolve-source-paths.outputs.lockfile }}
183+
REF: ${{ needs.resolve-source-paths.outputs.ref }}
184+
REMOTE_SUBDIR: ${{ needs.resolve-source-paths.outputs.remote_subdir }}
185+
REPO_URL: ${{ needs.resolve-source-paths.outputs.repo_url }}
46186
steps:
47187
- name: Generate CCBR-bot token
48188
id: ccbr_bot
@@ -67,41 +207,11 @@ jobs:
67207
python -m pip install --upgrade pip
68208
python -m pip install "git+https://github.com/CCBR/syncweaver.git@main"
69209
70-
- name: Resolve source path
71-
id: resolve_source_path
72-
shell: python
73-
env:
74-
SOURCE_PATH_INPUT: ${{ env.SOURCE_PATH }}
75-
REPO_URL_INPUT: ${{ env.REPO_URL }}
76-
LOCKFILE_PATH: ${{ env.LOCKFILE }}
77-
run: |
78-
import os
79-
import pathlib
80-
81-
from syncweaver.lockfile import resolve_source_path_from_lockfile
82-
83-
source_path_input = os.environ["SOURCE_PATH_INPUT"].strip()
84-
repo_url_input = os.environ["REPO_URL_INPUT"].strip()
85-
lockfile_path = pathlib.Path(os.environ["LOCKFILE_PATH"])
86-
output_path = pathlib.Path(os.environ["GITHUB_OUTPUT"])
87-
88-
try:
89-
resolved_source_path = resolve_source_path_from_lockfile(
90-
lockfile=lockfile_path,
91-
source_path=source_path_input,
92-
repo_url=repo_url_input,
93-
)
94-
except ValueError as exc:
95-
raise SystemExit(f"Error: {exc}") from exc
96-
97-
with output_path.open("a", encoding="utf-8") as fh:
98-
fh.write(f"source_path={resolved_source_path}\n")
99-
100210
- name: Run update-source composite action
101211
id: update_source
102212
uses: CCBR/syncweaver/actions/update-source@latest
103213
with:
104-
source_path: ${{ steps.resolve_source_path.outputs.source_path }}
214+
source_path: ${{ matrix.source_path }}
105215
lockfile: ${{ env.LOCKFILE }}
106216
ref: ${{ env.REF }}
107217
remote_subdir: ${{ env.REMOTE_SUBDIR }}
@@ -110,15 +220,15 @@ jobs:
110220
uses: peter-evans/create-pull-request@v7
111221
with:
112222
token: ${{ steps.ccbr_bot.outputs.token }}
113-
branch: syncweaver/update-source/${{ steps.resolve_source_path.outputs.source_path }}
223+
branch: syncweaver/update-source/${{ matrix.source_path }}
114224
delete-branch: true
115-
commit-message: "chore(syncweaver): update ${{ steps.resolve_source_path.outputs.source_path }} from ${{ steps.update_source.outputs.source_repo }}@${{ steps.update_source.outputs.ref }}"
116-
title: "chore(syncweaver): update ${{ steps.resolve_source_path.outputs.source_path }} from ${{ steps.update_source.outputs.source_repo }}@${{ steps.update_source.outputs.ref }}"
225+
commit-message: "chore(syncweaver): update ${{ matrix.source_path }} from ${{ steps.update_source.outputs.source_repo }}@${{ steps.update_source.outputs.ref }}"
226+
title: "chore(syncweaver): update ${{ matrix.source_path }} from ${{ steps.update_source.outputs.source_repo }}@${{ steps.update_source.outputs.ref }}"
117227
body: |
118228
Automated source update from the update-source workflow.
119229
120230
Inputs used:
121-
- source_path: ${{ steps.resolve_source_path.outputs.source_path }}
231+
- source_path: ${{ matrix.source_path }}
122232
- repo_url: ${{ env.REPO_URL }}
123233
- lockfile: ${{ env.LOCKFILE }}
124234
- ref: ${{ env.REF }}

0 commit comments

Comments
 (0)