diff --git a/.agents/docs/mcppls-0.0.17-release.md b/.agents/docs/mcppls-0.0.17-release.md index c4d52a8a..7b4f4709 100644 --- a/.agents/docs/mcppls-0.0.17-release.md +++ b/.agents/docs/mcppls-0.0.17-release.md @@ -358,3 +358,9 @@ U13 和 two-core 也走同样的方式:U13 每边 3 次(只跑 long 阶段 - termux、Zed(默认、推荐设置)、self-hosting 4 项、三个平台的 startup timing、最新版 mcpp 的 fixture 都通过。 这次 U13 能过,是因为窗口内发生了 trim;分组现象没有变,所以 Known issues 保持不变。 +- **第一次 Release 运行(`40022574908`)在四个平台的 payload job 上都失败**: + - 原因:GitHub 对 release 下载的 302 响应里,现在带一行 9243 字节的 `content-security-policy`;packaging 用的 tinyhttps 0.3 一行 header 超过 8192 字节就放弃(`Incomplete headers: line exceeds 8192 bytes`); + - PR CI 没有暴露:它的 payload 缓存里已经有这些包,不需要下载; + - 修复:PR #70,遇到这一种错误时改用 curl 下载,校验方式不变;server 不使用 tinyhttps,所以只影响发布流水线; + - 根本修复应在 tinyhttps(放宽行长限制),之后去掉这个回退; + - 这次失败的运行没有创建 tag,也没有创建 release。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 86bc2455..2947de3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,9 @@ what an idle or a crashed clangd costs you, and typing imports with autosave on. is not available with Clang at all (Clang has no coroutines on 32-bit x86 MSVC, so the MSVC STL's `std` module cannot be built), and such a project is read with the semantic kit (docs/20-projects.md). A conformance fixture that needs a newer mcpp than CI's pinned one declares it (`requires-mcpp`). +- Packaging downloads a GitHub release asset with curl when its HTTP client (tinyhttps 0.3) cannot read GitHub's + answer: since 2026-10 the redirect carries a 9.2 KB header line, over the client's 8 KB limit. The download is + verified against the lock the same way. This is the release pipeline only; the server makes no such downloads. - **A change can be measured without being built again.** A manual nightly can test the payloads a pull request's CI already built (`payload-run`), run only the ux rounds (`scope`), run their edits stage several times in new server sessions (`ux-repeat`), and run a released server beside them on the same machines (`ux-baseline`), compared by diff --git a/modules/pack/src/fetch.cpp b/modules/pack/src/fetch.cpp index 78d62c47..8fa62a91 100644 --- a/modules/pack/src/fetch.cpp +++ b/modules/pack/src/fetch.cpp @@ -10,7 +10,9 @@ import mcppls.base.error; import mcppls.base.log; import mcppls.base.path; import mcppls.base.sha256; +import mcppls.platform.env; import mcppls.platform.fs; +import mcppls.platform.process; namespace mcppls::pack::fetch { namespace fs = mcppls::platform::fs; @@ -30,6 +32,27 @@ mcpplibs::tinyhttps::HttpClientConfig client_config() { std::string partial_name(std::string_view target) { return std::string { target } + ".partial"; } +// GitHub's answer to a release download -- a redirect to its asset host -- carries a content-security-policy +// line of 9.2 KB (measured 2026-10-24); tinyhttps 0.3 gives up on any header line over 8 KB. curl, on every CI +// runner and developer machine, takes such a download instead, verified the same way. Remove when tinyhttps +// takes longer lines. +bool header_line_too_long(std::string_view error) { return error.contains("line exceeds"); } + +// The download with curl, into `partial`; empty when it worked, else why not. +std::string download_with_curl(const std::string& url, const std::string& partial) { + const auto curl = platform::env::find_executable("curl"); + if (!curl) return "curl is not on PATH"; + platform::SpawnOptions spawn; + spawn.program = *curl; + spawn.arguments = { "--fail", "--silent", "--show-error", "--location", "--connect-timeout", "30", "--output", partial, url }; + spawn.pipeError = true; + const auto ran = platform::run(std::move(spawn), std::chrono::minutes { 30 }); + if (!ran) return ran.error().message; + if (ran->timedOut) return "curl did not finish in time"; + if (ran->exitCode != 0) return std::format("curl exited {}: {}", ran->exitCode, platform::last_lines(ran->error, 3)); + return {}; +} + void remove_quietly(const std::string& path) { std::error_code ignored; std::filesystem::remove(path, ignored); @@ -75,16 +98,29 @@ base::Result get(const Entry& entry, std::string_view cacheDir, int remove_quietly(partial); const auto answer = client.download_to_file(entry.url, std::filesystem::path { partial }); - if (!answer.ok()) { - lastError = answer.error.empty() ? std::format("HTTP {}", answer.statusCode) : answer.error; + std::uint64_t written { static_cast(answer.bytesWritten) }; + std::string failed; + if (!answer.ok()) failed = answer.error.empty() ? std::format("HTTP {}", answer.statusCode) : answer.error; + if (!failed.empty() && header_line_too_long(failed)) { + if (!quiet) base::log::info("fetch: {} ({}); downloading it with curl", entry.url, failed); + remove_quietly(partial); + if (const auto why = download_with_curl(entry.url, partial); why.empty()) { + failed.clear(); + written = fs::stamp(partial) ? fs::stamp(partial)->size : 0; + } else { + failed = std::format("{}; with curl: {}", failed, why); + } + } + if (!failed.empty()) { + lastError = failed; } else if (auto got = digest_of(partial); !got) { lastError = got.error().message; } else if (*got != entry.sha256) { lastError = std::format("sha256 mismatch for {}: the lock says {}, the download is {}", entry.file, entry.sha256, *got); - } else if (entry.size && static_cast(answer.bytesWritten) != *entry.size) { + } else if (entry.size && written != *entry.size) { lastError = std::format("size mismatch for {}: the lock says {} bytes, the download is {}", - entry.file, *entry.size, answer.bytesWritten); + entry.file, *entry.size, written); } else { std::error_code failed; std::filesystem::rename(partial, target, failed);