From 2bc7b5ea275c68c51000f8ad87ff8875b3a0af8c Mon Sep 17 00:00:00 2001 From: stephamie7 <1223696150@qq.com> Date: Thu, 2 Jul 2026 10:07:54 +0800 Subject: [PATCH 1/2] ci: fix autotest artifact download redirect --- .../dispatch-autotest-windows-upgrade.yml | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dispatch-autotest-windows-upgrade.yml b/.github/workflows/dispatch-autotest-windows-upgrade.yml index b9c08c6e..bb16f371 100644 --- a/.github/workflows/dispatch-autotest-windows-upgrade.yml +++ b/.github/workflows/dispatch-autotest-windows-upgrade.yml @@ -199,9 +199,31 @@ jobs: with urllib.request.urlopen(request, timeout=30) as response: return json.loads(response.read().decode("utf-8")) + class NoRedirectHandler(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + def download(url, destination): request = urllib.request.Request(url, headers=headers) - with urllib.request.urlopen(request, timeout=300) as response: + opener = urllib.request.build_opener(NoRedirectHandler) + try: + opener.open(request, timeout=30) + except urllib.error.HTTPError as error: + if error.code not in (301, 302, 303, 307, 308): + raise + redirect_url = error.headers.get("Location") + if not redirect_url: + raise RuntimeError("Artifact download redirect did not include Location header") from error + else: + raise RuntimeError("Artifact download did not return the expected redirect") + + # GitHub returns a short-lived signed URL for the artifact archive. + # Do not forward the GitHub PAT to that storage endpoint. + redirect_request = urllib.request.Request( + redirect_url, + headers={"Accept": "application/octet-stream"}, + ) + with urllib.request.urlopen(redirect_request, timeout=300) as response: with open(destination, "wb") as f: f.write(response.read()) From f1d5d212d057e1b7af52ce93ee94a1e0c0bc10d4 Mon Sep 17 00:00:00 2001 From: stephamie7 <1223696150@qq.com> Date: Thu, 2 Jul 2026 10:30:40 +0800 Subject: [PATCH 2/2] ci: match autotest artifact by exact name --- .../dispatch-autotest-windows-upgrade.yml | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dispatch-autotest-windows-upgrade.yml b/.github/workflows/dispatch-autotest-windows-upgrade.yml index bb16f371..711b1398 100644 --- a/.github/workflows/dispatch-autotest-windows-upgrade.yml +++ b/.github/workflows/dispatch-autotest-windows-upgrade.yml @@ -53,6 +53,7 @@ jobs: GITHUB_SHA_VALUE: ${{ github.sha }} GITHUB_REF_NAME_VALUE: ${{ github.ref_name }} GITHUB_RUN_ID_VALUE: ${{ github.run_id }} + GITHUB_RUN_ATTEMPT_VALUE: ${{ github.run_attempt }} run: | set -euo pipefail @@ -78,11 +79,15 @@ jobs: exit 1 fi + artifact_time="$(printf "%02d" "${GITHUB_RUN_ATTEMPT_VALUE:-1}")" + artifact_name="flocks-windows-upgrade-${release_tag}-time${artifact_time}" + { echo "release_tag=$release_tag" echo "release_url=$release_url" echo "rollback_version=${INPUT_ROLLBACK_VERSION:-}" echo "run_force_fallback=${INPUT_RUN_FORCE_FALLBACK:-false}" + echo "artifact_name=$artifact_name" } >> "$GITHUB_OUTPUT" { @@ -95,7 +100,7 @@ jobs: echo "- Source sha: ${GITHUB_SHA_VALUE}" echo "- Rollback version: ${INPUT_ROLLBACK_VERSION:-}" echo "- Run force fallback: ${INPUT_RUN_FORCE_FALLBACK:-false}" - echo "- Expected autotest artifact prefix: flocks-windows-upgrade-${release_tag}-time" + echo "- Expected autotest artifact: ${artifact_name}" echo "- Autotest workflow: https://github.com/AgentFlocks/flocks_autotest/actions/workflows/flocks-release-dispatch.yml" } >> "$GITHUB_STEP_SUMMARY" @@ -105,6 +110,7 @@ jobs: RELEASE_URL: ${{ steps.payload.outputs.release_url }} ROLLBACK_VERSION: ${{ steps.payload.outputs.rollback_version }} RUN_FORCE_FALLBACK: ${{ steps.payload.outputs.run_force_fallback }} + ARTIFACT_NAME: ${{ steps.payload.outputs.artifact_name }} SOURCE_REPOSITORY: ${{ github.repository }} SOURCE_RUN_ID: ${{ github.run_id }} SOURCE_SHA: ${{ github.sha }} @@ -126,6 +132,7 @@ jobs: "source_run_id": os.environ["SOURCE_RUN_ID"], "source_sha": os.environ["SOURCE_SHA"], "source_ref": os.environ["SOURCE_REF"], + "artifact_name": os.environ["ARTIFACT_NAME"], "rollback_version": os.environ.get("ROLLBACK_VERSION", ""), "run_force_fallback": os.environ.get("RUN_FORCE_FALLBACK", "false"), }, @@ -158,7 +165,7 @@ jobs: timeout-minutes: 210 env: GH_TOKEN: ${{ secrets.AUTOTEST_DISPATCH_TOKEN }} - RELEASE_TAG: ${{ steps.payload.outputs.release_tag }} + EXPECTED_ARTIFACT_NAME: ${{ steps.payload.outputs.artifact_name }} SOURCE_RUN_ID: ${{ github.run_id }} AUTOTEST_REPOSITORY: AgentFlocks/flocks_autotest AUTOTEST_WORKFLOW_FILE: flocks-release-dispatch.yml @@ -178,7 +185,7 @@ jobs: api_root = "https://api.github.com" token = os.environ["GH_TOKEN"] - release_tag = os.environ["RELEASE_TAG"] + expected_artifact_name = os.environ["EXPECTED_ARTIFACT_NAME"] source_run_id = os.environ["SOURCE_RUN_ID"] autotest_repo = os.environ["AUTOTEST_REPOSITORY"] workflow_file = os.environ["AUTOTEST_WORKFLOW_FILE"] @@ -242,7 +249,6 @@ jobs: f"{api_root}/repos/{autotest_repo}/actions/workflows/" f"{workflow_ref}/runs?event=repository_dispatch&per_page=30" ) - artifact_prefix = f"flocks-windows-upgrade-{release_tag}-time" deadline = time.time() + wait_timeout matched_run = None @@ -268,7 +274,7 @@ jobs: "", f"- Status: timed out waiting for workflow run", f"- Source run id: {source_run_id}", - f"- Expected artifact prefix: {artifact_prefix}", + f"- Expected artifact: {expected_artifact_name}", ] ) raise SystemExit("Timed out waiting for flocks_autotest workflow run") @@ -297,19 +303,37 @@ jobs: "", f"- Status: timed out waiting for completion", f"- Run: {run_html_url}", - f"- Expected artifact prefix: {artifact_prefix}", + f"- Expected artifact: {expected_artifact_name}", ] ) raise SystemExit("Timed out waiting for flocks_autotest completion") conclusion = matched_run.get("conclusion") or "unknown" artifacts_url = f"{api_root}/repos/{autotest_repo}/actions/runs/{run_id}/artifacts?per_page=100" - artifacts = api_json(artifacts_url).get("artifacts", []) - matching_artifacts = [ - artifact - for artifact in artifacts - if not artifact.get("expired") and (artifact.get("name") or "").startswith(artifact_prefix) - ] + artifact_lookup_deadline = time.time() + 120 + artifacts = [] + available_artifact_names = [] + matching_artifacts = [] + while True: + artifacts = api_json(artifacts_url).get("artifacts", []) + available_artifact_names = [ + artifact.get("name") or "" + for artifact in artifacts + if not artifact.get("expired") and artifact.get("name") + ] + matching_artifacts = [ + artifact + for artifact in artifacts + if not artifact.get("expired") and (artifact.get("name") or "") == expected_artifact_name + ] + if matching_artifacts or time.time() >= artifact_lookup_deadline: + break + available_text = ", ".join(available_artifact_names) if available_artifact_names else "(none)" + print( + f"Waiting for artifact {expected_artifact_name}. Available artifacts: {available_text}", + flush=True, + ) + time.sleep(10) matching_artifacts.sort(key=lambda artifact: artifact.get("created_at") or "", reverse=True) artifact_name = "" @@ -341,13 +365,15 @@ jobs: "", f"- Run: {run_html_url}", f"- Conclusion: {conclusion}", - f"- Expected artifact prefix: {artifact_prefix}", + f"- Expected artifact: {expected_artifact_name}", ] if artifact_name: summary_lines.append(f"- Artifact: [{artifact_name}]({artifact_html_url})") summary_lines.append("- Artifact copy: uploaded to this flocks workflow run") else: summary_lines.append("- Artifact: not found") + available_text = ", ".join(available_artifact_names) if available_artifact_names else "(none)" + summary_lines.append(f"- Available artifacts: {available_text}") write_summary(summary_lines) if conclusion != "success":