Use GitHub API for pushes by default and require explicit github-token when using custom tokens - #692
Conversation
🦋 Changeset detectedLatest commit: cb7d7f0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| ).toEqual(remote.requests.map(() => [getAuthorization(actionToken)])); | ||
| }, 15_000); | ||
|
|
||
| it("uses the push URL when it differs from the fetch URL", async () => { |
There was a problem hiding this comment.
do we need to test for this? is it a case we handle explicitly?
There was a problem hiding this comment.
we handle it here:
Lines 109 to 121 in 99664fb
| // GIT_CONFIG_COUNT/KEY_n/VALUE_n add command-scoped config. Preserve any | ||
| // existing entries and append ours. `http.extraHeader` is multi-valued, so | ||
| // merely adding our Authorization header would make Git send both tokens. | ||
| // An empty value resets the list; the following value adds only our token. | ||
| // | ||
| // In v1, `github-token` lived in ~/.netrc. When checkout had already | ||
| // supplied Authorization through an extraheader, that header took | ||
| // precedence and ~/.netrc was effectively a fallback. These entries | ||
| // intentionally make `github-token` win for pushes. |
There was a problem hiding this comment.
i think a full description of how we override the token and why we do it that way should be written somewhere else, the current doc comments are both duplicated and split up so they are hard to read.
There was a problem hiding this comment.
I added a short explainer on why we are doing this stuff at the top of this function but kept comments relevant to specific code lines close to them (while improving them a little bit). I feel it's easier to follow the steps performed here this way
| async function runGitHttpBackend( | ||
| cwd: string, | ||
| request: IncomingMessage, | ||
| response: ServerResponse, | ||
| ) { | ||
| const requestUrl = new URL( | ||
| request.url ?? "/", | ||
| `http://${request.headers.host ?? "localhost"}`, | ||
| ); | ||
| const env: NodeJS.ProcessEnv = { | ||
| ...process.env, | ||
| CONTENT_LENGTH: request.headers["content-length"] ?? "0", | ||
| GATEWAY_INTERFACE: "CGI/1.1", | ||
| GIT_HTTP_EXPORT_ALL: "1", | ||
| GIT_PROJECT_ROOT: cwd, | ||
| PATH_INFO: decodeURIComponent(requestUrl.pathname), | ||
| QUERY_STRING: requestUrl.search.slice(1), | ||
| REMOTE_ADDR: request.socket.remoteAddress ?? "", | ||
| REQUEST_METHOD: request.method ?? "GET", | ||
| SERVER_PROTOCOL: `HTTP/${request.httpVersion}`, | ||
| }; | ||
| if (request.headers["content-type"] !== undefined) { | ||
| env.CONTENT_TYPE = request.headers["content-type"]; | ||
| } | ||
|
|
||
| const backend = spawn("git", ["http-backend"], { | ||
| env, | ||
| stdio: ["pipe", "pipe", "pipe"], | ||
| }); | ||
| request.pipe(backend.stdin); | ||
|
|
||
| const stdout: Buffer[] = []; | ||
| const stderr: Buffer[] = []; | ||
| backend.stdout.on("data", (chunk: Buffer) => stdout.push(chunk)); | ||
| backend.stderr.on("data", (chunk: Buffer) => stderr.push(chunk)); | ||
|
|
||
| const exitCode = await new Promise<number | null>((resolve, reject) => { | ||
| backend.on("error", reject); | ||
| backend.on("close", resolve); | ||
| }); | ||
| if (exitCode !== 0) { | ||
| throw new Error( | ||
| `git http-backend exited with ${exitCode}: ${Buffer.concat(stderr).toString("utf8")}`, | ||
| ); | ||
| } | ||
|
|
||
| const output = Buffer.concat(stdout); | ||
| let separator = Buffer.from("\r\n\r\n"); | ||
| let headerEnd = output.indexOf(separator); | ||
| if (headerEnd === -1) { | ||
| separator = Buffer.from("\n\n"); | ||
| headerEnd = output.indexOf(separator); | ||
| } | ||
| if (headerEnd === -1) { | ||
| throw new Error("git http-backend returned an invalid CGI response"); | ||
| } | ||
|
|
||
| let status = 200; | ||
| const headers = output.subarray(0, headerEnd).toString("utf8").split(/\r?\n/); | ||
| for (const header of headers) { | ||
| const separatorIndex = header.indexOf(":"); | ||
| if (separatorIndex === -1) continue; | ||
|
|
||
| const name = header.slice(0, separatorIndex); | ||
| const value = header.slice(separatorIndex + 1).trim(); | ||
| if (name.toLowerCase() === "status") { | ||
| status = Number.parseInt(value, 10); | ||
| } else { | ||
| response.setHeader(name, value); | ||
| } | ||
| } | ||
|
|
||
| response.writeHead(status); | ||
| response.end(output.subarray(headerEnd + separator.length)); | ||
| } | ||
|
|
||
| async function listen(server: http.Server) { | ||
| const waiter = Promise.withResolvers(); | ||
|
|
||
| server.on("listening", waiter.resolve); | ||
| server.on("error", waiter.reject); | ||
|
|
||
| server.listen(0); | ||
|
|
||
| try { | ||
| await waiter.promise; | ||
| return server; | ||
| } finally { | ||
| server.off("listening", waiter.resolve); | ||
| server.off("error", waiter.reject); | ||
| } | ||
| } | ||
|
|
||
| async function createGitHttpServer(cwd: string) { | ||
| const requests: RecordedRequest[] = []; | ||
| const server = http.createServer((request, response) => { | ||
| const recordedRequest = recordRequest(request); | ||
| requests.push(recordedRequest); | ||
|
|
||
| void runGitHttpBackend(cwd, request, response).catch((error: unknown) => { | ||
| response.destroy( | ||
| Error.isError(error) | ||
| ? error | ||
| : new Error("Server error", { cause: error }), | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| await listen(server); | ||
| const address = server.address(); | ||
| assert( | ||
| !!address && typeof address !== "string", | ||
| "Failed to get server address", | ||
| ); | ||
|
|
||
| return { | ||
| origin: `http://127.0.0.1:${address.port}`, | ||
| requests, | ||
| async [Symbol.asyncDispose]() { | ||
| await new Promise<void>((resolve, reject) => { | ||
| server.close((error) => { | ||
| if (error) { | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve(); | ||
| }); | ||
| }); | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
this should probably be in its own file and split up a bit.
i would also prefer if we use srvx so we can use Request/Response instead of node's http server
There was a problem hiding this comment.
IMO node's createServer isn't bad and we shouldn't try to avoid it. Its API is not the most modern, but we don't need to risk running a server framework that might have its own issues, plus Request-Response-based servers also have their own limitations which don't exist in createServer.
There was a problem hiding this comment.
i just prefer Requst/Response over node's api 😜
srvx is the one used by h3/nuxt (soon?), so i think it'd be fine for our use case in tests.
like i wrote before, i'm getting overwhelmed by the test setups we're creating so anything to simplify them would be a win in my eyes
There was a problem hiding this comment.
Hm, I get this is a little bit hard to follow - but at the same time it's not something we need to care about deeply as this is test-only glue code. What matters is in the test bodies (which admittedly are kinda long right now). I added some code comments here to make it easier to follow.
I think it would be good to split up this file in the future - but given the test files are now in the same directory as the source files and this is a "single-package" repo, I don't want to restructure this too much right now.
There was a problem hiding this comment.
i think these test files are getting a bit overwhelming... my brain struggles massively to read them
There was a problem hiding this comment.
I moved some shared stuff into in-file utils. I hope this will help with the readability of those tests
github-token wins for git-cli pushesgithub-token when using custom tokens
This should fix auth headers when the action gets used with
actions/checkout's defaultpersist-credentials: truerelated to sveltejs/kit#16248