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
5 changes: 5 additions & 0 deletions .changeset/git-cli-auth-header.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/git-cli-server-url.md
Original file line number Diff line number Diff line change
@@ -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.
86 changes: 68 additions & 18 deletions src/git.ts → src/github.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
};

const push = async (branch: string, options: GitOptions) => {
Expand Down Expand Up @@ -46,17 +50,51 @@ const checkIfClean = async (options: GitOptions): Promise<boolean> => {
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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this just be called mode instead? it affects more than just commits

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rename it here but I wouldn't like to touch the public option in this PR - and those 2 are the same thing so keeping the name consistent makes it easier to reason about. That said, I agree it's not a particularly accurate name


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<string, string> {
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]"`], {
Expand All @@ -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<string, string>,
});
}

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
*
Expand All @@ -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,
Expand All @@ -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<string, string>,
});
}
}
26 changes: 7 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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") {
Expand All @@ -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");
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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"),
Expand Down
12 changes: 4 additions & 8 deletions src/publish/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(
Expand All @@ -39,9 +37,7 @@ async function main() {

const result = await runPublish({
script,
githubToken,
git,
octokit,
github,
createGithubReleases,
cwd,
fromPackDir,
Expand Down
43 changes: 16 additions & 27 deletions src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand All @@ -21,7 +20,6 @@ vi.mock("@actions/github", () => ({
graphql: mockedGraphql,
}),
}));
vi.mock("./git.ts");
vi.mock("@changesets/ghcommit/git");

let mockedGithubMethods = {
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -126,9 +131,7 @@ describe("version", () => {
);

await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
github: createGithub(cwd),
cwd,
});

Expand Down Expand Up @@ -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",
});
Expand Down Expand Up @@ -197,9 +198,7 @@ describe("version", () => {
);

await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
github: createGithub(cwd),
cwd,
});

Expand Down Expand Up @@ -232,9 +231,7 @@ describe("version", () => {
);

await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
github: createGithub(cwd),
cwd,
});

Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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",
});
Expand Down Expand Up @@ -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",
});
Expand Down
Loading