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
3333jobs :
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