Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/docs/mcppls-0.0.17-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 40 additions & 4 deletions modules/pack/src/fetch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -75,16 +98,29 @@ base::Result<std::string> 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<std::uint64_t>(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<std::uint64_t>(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);
Expand Down
Loading