diff --git a/.changeset/git-cli-auth-header.md b/.changeset/git-cli-auth-header.md new file mode 100644 index 00000000..e7fd6437 --- /dev/null +++ b/.changeset/git-cli-auth-header.md @@ -0,0 +1,5 @@ +--- +"@changesets/action": patch +--- + +Authenticate git CLI pushes with the configured GitHub token using Git extra headers instead of writing to a global `.netrc` file. diff --git a/.changeset/git-cli-server-url.md b/.changeset/git-cli-server-url.md new file mode 100644 index 00000000..bd04f862 --- /dev/null +++ b/.changeset/git-cli-server-url.md @@ -0,0 +1,5 @@ +--- +"@changesets/action": patch +--- + +Derive the Git server URL from the GitHub Actions context when configuring git CLI authentication to support GitHub Enterprise Server setups. diff --git a/src/git.ts b/src/github.ts similarity index 56% rename from src/git.ts rename to src/github.ts index ec2f3ac3..eee00019 100644 --- a/src/git.ts +++ b/src/github.ts @@ -1,11 +1,15 @@ +import { Buffer } from "node:buffer"; import * as core from "@actions/core"; import { exec, getExecOutput } from "@actions/exec"; -import * as github from "@actions/github"; +import { context } from "@actions/github"; import { commitChangesFromRepo } from "@changesets/ghcommit/git"; -import type { Octokit } from "./octokit.ts"; +import { setupOctokit, type Octokit } from "./octokit.ts"; + +export type CommitMode = "git-cli" | "github-api"; type GitOptions = { cwd: string; + env?: Record; }; const push = async (branch: string, options: GitOptions) => { @@ -46,17 +50,51 @@ const checkIfClean = async (options: GitOptions): Promise => { return !stdout.length; }; -export class Git { - readonly octokit: Octokit | null; +export class GitHub { + readonly #githubToken: string; + readonly octokit: Octokit; readonly cwd: string; + readonly commitMode: CommitMode; + + constructor(options: { + githubToken: string; + cwd: string; + commitMode?: CommitMode; + }) { + this.#githubToken = options.githubToken; + this.cwd = options.cwd; + this.commitMode = options.commitMode ?? "git-cli"; + this.octokit = setupOctokit(options.githubToken); + } - constructor(args: { octokit?: Octokit; cwd: string }) { - this.octokit = args.octokit ?? null; - this.cwd = args.cwd; + getToken() { + return this.#githubToken; + } + + #getCliAuthEnv(): Record { + const basic = Buffer.from(`x-access-token:${this.#githubToken}`).toString( + "base64", + ); + const serverUrl = ( + context.serverUrl ?? + process.env.GITHUB_SERVER_URL ?? + "https://github.com" + ).replace(/\/+$/, ""); + const gitConfigCount = Number(process.env.GIT_CONFIG_COUNT ?? 0); + if (!Number.isInteger(gitConfigCount) || gitConfigCount < 0) { + throw new Error( + `Invalid GIT_CONFIG_COUNT value: ${process.env.GIT_CONFIG_COUNT}`, + ); + } + return { + GIT_CONFIG_COUNT: String(gitConfigCount + 1), + [`GIT_CONFIG_KEY_${gitConfigCount}`]: `http.${serverUrl}/.extraheader`, + [`GIT_CONFIG_VALUE_${gitConfigCount}`]: `AUTHORIZATION: basic ${basic}`, + }; } async setupUser() { - if (this.octokit) { + if (this.commitMode === "github-api") { return; } await exec("git", ["config", "user.name", `"github-actions[bot]"`], { @@ -76,32 +114,38 @@ export class Git { } async pushTag(tag: string) { - if (this.octokit) { + if (this.commitMode === "github-api") { return this.octokit.rest.git .createRef({ - ...github.context.repo, + ...context.repo, ref: `refs/tags/${tag}`, - sha: github.context.sha, + sha: context.sha, }) .catch((err) => { // Assuming tag was manually pushed in custom publish script core.warning(`Failed to create tag ${tag}: ${err.message}`); }); } - await exec("git", ["push", "origin", tag], { cwd: this.cwd }); + await exec("git", ["push", "origin", tag], { + cwd: this.cwd, + env: { + ...process.env, + ...this.#getCliAuthEnv(), + } as Record, + }); } async prepareBranch(branch: string) { - if (this.octokit) { + if (this.commitMode === "github-api") { // Preparing a new local branch is not necessary when using the API return; } await switchToMaybeExistingBranch(branch, { cwd: this.cwd }); - await reset(github.context.sha, { cwd: this.cwd }); + await reset(context.sha, { cwd: this.cwd }); } async pushChanges({ branch, message }: { branch: string; message: string }) { - if (this.octokit) { + if (this.commitMode === "github-api") { /** * Only add files form the current working directory * @@ -111,11 +155,11 @@ export class Git { const addFromDirectory = this.cwd; return commitChangesFromRepo({ octokit: this.octokit, - ...github.context.repo, + ...context.repo, branch, message, base: { - commit: github.context.sha, + commit: context.sha, }, cwd: this.cwd, force: true, @@ -124,6 +168,12 @@ export class Git { if (!(await checkIfClean({ cwd: this.cwd }))) { await commitAll(message, { cwd: this.cwd }); } - await push(branch, { cwd: this.cwd }); + await push(branch, { + cwd: this.cwd, + env: { + ...process.env, + ...this.#getCliAuthEnv(), + } as Record, + }); } } diff --git a/src/index.ts b/src/index.ts index 99e5e49e..ef4b317e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import fs from "node:fs/promises"; import * as core from "@actions/core"; -import { Git } from "./git.ts"; -import { setupOctokit } from "./octokit.ts"; +import { GitHub } from "./github.ts"; import readChangesetState from "./readChangesetState.ts"; import { runPublish, runVersion } from "./run.ts"; import { fileExists, getOptionalInput } from "./utils.ts"; @@ -19,7 +18,6 @@ import { fileExists, getOptionalInput } from "./utils.ts"; // If the user needs to change the cwd, set `working-directory` in the step instead const cwd = process.cwd(); - const octokit = setupOctokit(githubToken); const commitMode = getOptionalInput("commitMode") ?? "git-cli"; const prDraft = getOptionalInput("prDraft"); if (commitMode !== "git-cli" && commitMode !== "github-api") { @@ -30,24 +28,19 @@ import { fileExists, getOptionalInput } from "./utils.ts"; core.setFailed(`Invalid prDraft: ${prDraft}`); return; } - const git = new Git({ - octokit: commitMode === "github-api" ? octokit : undefined, + const github = new GitHub({ cwd, + githubToken, + commitMode, }); let setupGitUser = core.getBooleanInput("setupGitUser"); if (setupGitUser) { core.info("setting git user"); - await git.setupUser(); + await github.setupUser(); } - core.info("setting GitHub credentials"); - await fs.writeFile( - `${process.env.HOME}/.netrc`, - `machine github.com\nlogin github-actions[bot]\npassword ${githubToken}`, - ); - let { changesets } = await readChangesetState(cwd); let publishScript = core.getInput("publish"); @@ -119,9 +112,7 @@ import { fileExists, getOptionalInput } from "./utils.ts"; const result = await runPublish({ script: publishScript, - githubToken, - git, - octokit, + github, createGithubReleases: core.getBooleanInput("createGithubReleases"), cwd, }); @@ -152,12 +143,9 @@ import { fileExists, getOptionalInput } from "./utils.ts"; core.info("All changesets are empty; not creating PR"); return; case hasChangesets: { - const octokit = setupOctokit(githubToken); const { pullRequestNumber } = await runVersion({ script: getOptionalInput("version"), - githubToken, - git, - octokit, + github, cwd, prTitle: getOptionalInput("title"), commitMessage: getOptionalInput("commit"), diff --git a/src/publish/index.ts b/src/publish/index.ts index d9653c0f..fd6ce2d2 100644 --- a/src/publish/index.ts +++ b/src/publish/index.ts @@ -1,8 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import * as core from "@actions/core"; -import { Git } from "../git.ts"; -import { setupOctokit } from "../octokit.ts"; +import { GitHub } from "../github.ts"; import { runPublish } from "../run.ts"; import { downloadArtifact, @@ -25,9 +24,8 @@ async function main() { // If the user needs to change the cwd, set `working-directory` in the step instead const cwd = process.cwd(); - const octokit = setupOctokit(githubToken); - // NOTE: Always pass octokit here as publish does not need a commit-mode - const git = new Git({ octokit, cwd }); + // NOTE: Always use API mode here as publish does not need a commit-mode. + const github = new GitHub({ cwd, githubToken, commitMode: "github-api" }); const fromPackDir = packDirArtifactId ? await downloadArtifact( @@ -39,9 +37,7 @@ async function main() { const result = await runPublish({ script, - githubToken, - git, - octokit, + github, createGithubReleases, cwd, fromPackDir, diff --git a/src/run.test.ts b/src/run.test.ts index ff1b268c..bb1bc681 100644 --- a/src/run.test.ts +++ b/src/run.test.ts @@ -3,8 +3,7 @@ import type { Changeset } from "@changesets/types"; import writeChangeset from "@changesets/write"; import { createFixture } from "fs-fixture"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { Git } from "./git.ts"; -import { setupOctokit } from "./octokit.ts"; +import { GitHub } from "./github.ts"; import { runVersion } from "./run.ts"; vi.mock("@actions/github", () => ({ @@ -21,7 +20,6 @@ vi.mock("@actions/github", () => ({ graphql: mockedGraphql, }), })); -vi.mock("./git.ts"); vi.mock("@changesets/ghcommit/git"); let mockedGithubMethods = { @@ -91,6 +89,13 @@ const writeChangesets = (changesets: Changeset[], cwd: string) => { return Promise.all(changesets.map((commit) => writeChangeset(commit, cwd))); }; +const createGithub = (cwd: string) => + new GitHub({ + cwd, + githubToken: "@@GITHUB_TOKEN", + commitMode: "github-api", + }); + beforeEach(() => { vi.clearAllMocks(); }); @@ -126,9 +131,7 @@ describe("version", () => { ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, }); @@ -161,9 +164,7 @@ describe("version", () => { ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, prDraft: "create", }); @@ -197,9 +198,7 @@ describe("version", () => { ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, }); @@ -232,9 +231,7 @@ describe("version", () => { ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, }); @@ -287,9 +284,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, prBodyMaxCharacters: 1000, }); @@ -346,9 +341,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, prBodyMaxCharacters: 500, }); @@ -383,9 +376,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, prDraft: "create", }); @@ -417,9 +408,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. ); await runVersion({ - octokit: setupOctokit("@@GITHUB_TOKEN"), - githubToken: "@@GITHUB_TOKEN", - git: new Git({ cwd }), + github: createGithub(cwd), cwd, prDraft: "always", }); diff --git a/src/run.ts b/src/run.ts index 599eeaaf..69fd1449 100644 --- a/src/run.ts +++ b/src/run.ts @@ -7,10 +7,10 @@ import { type ExecOptions, type ExecOutput, } from "@actions/exec"; -import * as github from "@actions/github"; +import { context } from "@actions/github"; import type { PreState } from "@changesets/types"; import { type Package, getPackages } from "@manypkg/get-packages"; -import { Git } from "./git.ts"; +import type { GitHub } from "./github.ts"; import type { Octokit } from "./octokit.ts"; import readChangesetState from "./readChangesetState.ts"; import { @@ -57,17 +57,15 @@ const createRelease = async ( tag_name: tagName, body: changelogEntry.content, prerelease: pkg.packageJson.version.includes("-"), - ...github.context.repo, + ...context.repo, }); }; type PublishOptions = { script?: string; fromPackDir?: string; - githubToken: string; - octokit: Octokit; createGithubReleases: boolean; - git: Git; + github: GitHub; cwd: string; }; @@ -87,17 +85,16 @@ type PublishResult = export async function runPublish({ script, fromPackDir, - githubToken, - git, - octokit, + github, createGithubReleases, cwd, }: PublishOptions): Promise { + const { octokit } = github; let changesetPublishOutput: ExecOutput; const execOptions: ExecOptions = { cwd, ignoreReturnCode: true, - env: { ...process.env, GITHUB_TOKEN: githubToken }, + env: { ...process.env, GITHUB_TOKEN: github.getToken() }, }; if (script) { @@ -144,7 +141,7 @@ export async function runPublish({ await Promise.all( releasedPackages.map(async (pkg) => { const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`; - await git.pushTag(tagName); + await github.pushTag(tagName); await createRelease(octokit, { pkg, tagName }); }), ); @@ -166,7 +163,7 @@ export async function runPublish({ releasedPackages.push(pkg); if (createGithubReleases) { const tagName = `v${pkg.packageJson.version}`; - await git.pushTag(tagName); + await github.pushTag(tagName); await createRelease(octokit, { pkg, tagName }); } break; @@ -259,9 +256,7 @@ export async function getVersionPrBody({ type VersionOptions = { script?: string; - githubToken: string; - git: Git; - octokit: Octokit; + github: GitHub; cwd?: string; prTitle?: string; commitMessage?: string; @@ -277,26 +272,25 @@ type RunVersionResult = { export async function runVersion({ script, - githubToken, - git, - octokit, + github, cwd = process.cwd(), prTitle = "Version Packages", commitMessage = "Version Packages", hasPublishScript = false, prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE, - branch = github.context.ref.replace("refs/heads/", ""), + branch = context.ref.replace("refs/heads/", ""), prDraft, }: VersionOptions): Promise { + const { octokit } = github; let versionBranch = `changeset-release/${branch}`; let { preState } = await readChangesetState(cwd); - await git.prepareBranch(versionBranch); + await github.prepareBranch(versionBranch); let versionsByDirectory = await getVersionsByDirectory(cwd); - const env = { ...process.env, GITHUB_TOKEN: githubToken }; + const env = { ...process.env, GITHUB_TOKEN: github.getToken() }; if (script) { await exec(script, undefined, { cwd, env }); @@ -335,9 +329,9 @@ export async function runVersion({ * which GitHub will then react to by closing the PRs) */ const existingPullRequests = await octokit.rest.pulls.list({ - ...github.context.repo, + ...context.repo, state: "open", - head: `${github.context.repo.owner}:${versionBranch}`, + head: `${context.repo.owner}:${versionBranch}`, base: branch, }); core.info( @@ -348,7 +342,10 @@ export async function runVersion({ )}`, ); - await git.pushChanges({ branch: versionBranch, message: finalCommitMessage }); + await github.pushChanges({ + branch: versionBranch, + message: finalCommitMessage, + }); const changedPackagesInfo = (await changedPackagesInfoPromises) .filter((x) => x) @@ -370,7 +367,7 @@ export async function runVersion({ title: finalPrTitle, body: prBody, draft: prDraft !== undefined, - ...github.context.repo, + ...context.repo, }); return { diff --git a/src/version/index.ts b/src/version/index.ts index 59f749ca..d9498e1f 100644 --- a/src/version/index.ts +++ b/src/version/index.ts @@ -1,6 +1,5 @@ import * as core from "@actions/core"; -import { Git } from "../git.ts"; -import { setupOctokit } from "../octokit.ts"; +import { GitHub } from "../github.ts"; import { runVersion } from "../run.ts"; import { getOptionalInput, getRequiredInput } from "../utils.ts"; @@ -24,26 +23,27 @@ async function main() { if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") { throw new Error(`Invalid pr-draft input: ${prDraft}`); } + if (commitMode !== "git-cli" && commitMode !== "github-api") { + throw new Error(`Invalid commit-mode input: ${commitMode}`); + } // If the user needs to change the cwd, set `working-directory` in the step instead const cwd = process.cwd(); - const octokit = setupOctokit(githubToken); - const git = new Git({ - octokit: commitMode === "github-api" ? octokit : undefined, + const github = new GitHub({ cwd, + githubToken, + commitMode, }); if (setupGitUser) { core.info("setting git user"); - await git.setupUser(); + await github.setupUser(); } const { pullRequestNumber } = await runVersion({ script, - githubToken, - git, - octokit, + github, cwd, prTitle, commitMessage,