diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 912c79c..80e70e0 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -76,19 +76,41 @@ jobs: with: fetch-depth: 0 + - name: Resolve published driver release source + id: published_source + env: + DRIVER_RELEASE_TOKEN: ${{ secrets.DRIVER_RELEASE_TOKEN }} + shell: bash + run: | + set -euo pipefail + SOURCE_COMMIT="$(python3 tools/resolve-driver-release-source.py --repo Syngnat/GoNavi-DriverAgents --tag dev-latest)" + echo "source_commit=${SOURCE_COMMIT}" >> "$GITHUB_OUTPUT" + if [[ -n "$SOURCE_COMMIT" ]]; then + echo "🧭 Last published dev driver release source commit: $SOURCE_COMMIT" + else + echo "🧭 Unable to resolve published dev driver release source commit; fallback to push diff base" + fi + - name: Detect changed driver agents id: detect shell: bash run: | set -euo pipefail - BASE_REF="${{ github.event.before }}" - if [[ -z "$BASE_REF" || "$BASE_REF" =~ ^0+$ ]]; then - if BASE_REF="$(git rev-parse HEAD^ 2>/dev/null)"; then - : + BASE_REF="${{ steps.published_source.outputs.source_commit }}" + if [[ -n "$BASE_REF" ]]; then + if git rev-parse --verify "${BASE_REF}^{commit}" >/dev/null 2>&1 && git merge-base --is-ancestor "$BASE_REF" "$GITHUB_SHA"; then + echo "🧭 Using last published driver release source commit as detection base: $BASE_REF" else - BASE_REF="all" + echo "⚠️ Published driver release source commit is unavailable or not an ancestor of $GITHUB_SHA: $BASE_REF" + BASE_REF="" fi fi + + if [[ -z "$BASE_REF" ]]; then + echo "⚠️ Falling back to full driver rebuild because published driver release source commit is unavailable" + BASE_REF="all" + fi + echo "🧭 Final driver detection base: $BASE_REF" DRIVERS="$(bash ./tools/detect-changed-driver-agents.sh --base "$BASE_REF" --head "$GITHUB_SHA")" echo "drivers=${DRIVERS}" >> "$GITHUB_OUTPUT" if [ -n "$DRIVERS" ]; then diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 7396e24..bed8925 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -d0464f9da25e9356e61652e638c99ffe \ No newline at end of file +0295a42fd931778d85157816d79d29e5 \ No newline at end of file diff --git a/tools/detect-changed-driver-agents.test.sh b/tools/detect-changed-driver-agents.test.sh index 4380a7e..913c484 100755 --- a/tools/detect-changed-driver-agents.test.sh +++ b/tools/detect-changed-driver-agents.test.sh @@ -69,4 +69,49 @@ cp tools/detect-changed-driver-agents.sh "$tmpdir_script/tools/detect-changed-dr fi ) +tmpdir_compensation="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-detect-compensation.XXXXXX")" +git init -q "$tmpdir_compensation" +mkdir -p "$tmpdir_compensation/tools" "$tmpdir_compensation/internal/db" "$tmpdir_compensation/.github/workflows" +cp tools/detect-changed-driver-agents.sh "$tmpdir_compensation/tools/detect-changed-driver-agents.sh" +cat >"$tmpdir_compensation/internal/db/driver_agent_revisions_gen.go" <<'GOEOF' +package db + +func init() { + optionalDriverAgentRevisions = map[string]string{ + "clickhouse": "src-old-clickhouse", + } +} +GOEOF +cat >"$tmpdir_compensation/.github/workflows/dev-build.yml" <<'YAMLEOF' +name: Dev Build +YAMLEOF + +( + cd "$tmpdir_compensation" + git add . + git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m initial + published_base="$(git rev-parse HEAD)" + + perl -0pi -e 's/src-old-clickhouse/src-new-clickhouse/' internal/db/driver_agent_revisions_gen.go + git add internal/db/driver_agent_revisions_gen.go + git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m 'update clickhouse revision' + push_base="$(git rev-parse HEAD)" + + printf '\n# workflow fix\n' >> .github/workflows/dev-build.yml + git add .github/workflows/dev-build.yml + git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m 'fix workflow' + + actual_push_base="$(bash ./tools/detect-changed-driver-agents.sh --base "$push_base" --head HEAD)" + if [[ -n "$actual_push_base" ]]; then + echo "expected workflow-only fix commit to be empty when using push diff base, got: ${actual_push_base:-}" >&2 + exit 1 + fi + + actual_published_base="$(bash ./tools/detect-changed-driver-agents.sh --base "$published_base" --head HEAD)" + if [[ "$actual_published_base" != "clickhouse" ]]; then + echo "expected published release base to recover clickhouse rebuild, got: ${actual_published_base:-}" >&2 + exit 1 + fi +) + echo "detect-changed-driver-agents revision test passed" diff --git a/tools/resolve-driver-release-source.py b/tools/resolve-driver-release-source.py new file mode 100644 index 0000000..876f5c4 --- /dev/null +++ b/tools/resolve-driver-release-source.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import re +import sys +import urllib.error +import urllib.parse +import urllib.request + + +COMMIT_LINK_RE = re.compile(r"/commit/([0-9a-f]{40})(?:\b|/)") +FULL_SHA_RE = re.compile(r"\b([0-9a-f]{40})\b") + + +def github_headers(): + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "GoNavi-CI", + } + token = os.environ.get("DRIVER_RELEASE_TOKEN") or os.environ.get("GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def fetch_json(url): + request = urllib.request.Request(url, headers=github_headers()) + with urllib.request.urlopen(request, timeout=30) as response: + return json.loads(response.read().decode("utf-8")) + + +def load_release(repo, tag): + owner_repo = repo.strip() + if not owner_repo: + raise ValueError("repo is required") + + if tag == "latest": + url = f"https://api.github.com/repos/{owner_repo}/releases/latest" + else: + url = ( + f"https://api.github.com/repos/{owner_repo}/releases/tags/" + f"{urllib.parse.quote(tag, safe='')}" + ) + + try: + return fetch_json(url) + except urllib.error.HTTPError as exc: + if exc.code == 404: + print(f"warning: release {owner_repo}@{tag} not found", file=sys.stderr) + return None + print( + f"warning: failed to load release {owner_repo}@{tag}: HTTP {exc.code}", + file=sys.stderr, + ) + return None + except Exception as exc: # pragma: no cover - defensive logging path + print(f"warning: failed to load release {owner_repo}@{tag}: {exc}", file=sys.stderr) + return None + + +def extract_source_commit(release): + if not isinstance(release, dict): + return None + + body = str(release.get("body") or "") + for pattern in (COMMIT_LINK_RE, FULL_SHA_RE): + match = pattern.search(body) + if match: + return match.group(1) + + target_commitish = str(release.get("target_commitish") or "").strip() + if FULL_SHA_RE.fullmatch(target_commitish): + return target_commitish + + return None + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--repo", default="Syngnat/GoNavi-DriverAgents") + parser.add_argument("--tag", required=True, help="release tag name such as dev-latest or v1.0.0") + args = parser.parse_args() + + release = load_release(args.repo, args.tag) + if release is None: + return 0 + + source_commit = extract_source_commit(release) + if not source_commit: + print( + f"warning: release {args.repo}@{args.tag} does not expose source commit", + file=sys.stderr, + ) + return 0 + + print(source_commit) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/resolve-driver-release-source.test.py b/tools/resolve-driver-release-source.test.py new file mode 100644 index 0000000..856c5a0 --- /dev/null +++ b/tools/resolve-driver-release-source.test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import importlib.util +import pathlib +import unittest + + +MODULE_PATH = pathlib.Path(__file__).with_name("resolve-driver-release-source.py") +SPEC = importlib.util.spec_from_file_location("resolve_driver_release_source", MODULE_PATH) +MODULE = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(MODULE) + + +class ResolveDriverReleaseSourceTests(unittest.TestCase): + def test_extracts_commit_from_release_body_link(self): + commit = "a" * 40 + release = { + "body": ( + f"GoNavi dev driver-agent assets.\n\n" + f"**提交**: [`{commit}`](https://github.com/Syngnat/GoNavi/commit/{commit})" + ) + } + self.assertEqual(MODULE.extract_source_commit(release), commit) + + def test_extracts_commit_from_plain_body_sha(self): + commit = "b" * 40 + release = {"body": f"source commit: {commit}"} + self.assertEqual(MODULE.extract_source_commit(release), commit) + + def test_falls_back_to_full_sha_target_commitish(self): + commit = "c" * 40 + release = {"target_commitish": commit} + self.assertEqual(MODULE.extract_source_commit(release), commit) + + def test_ignores_branch_name_target_commitish(self): + release = {"body": "", "target_commitish": "main"} + self.assertIsNone(MODULE.extract_source_commit(release)) + + +if __name__ == "__main__": + unittest.main()