Avoid putting the github token in the global .netrc file (#670)

* Avoid putting the github token in the global `.netrc` file

* fix wording
This commit is contained in:
Mateusz Burzyński
2026-06-24 10:07:11 +00:00
committed by GitHub
parent 0490518932
commit 5a8b9b721b
8 changed files with 136 additions and 106 deletions
+5
View File
@@ -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
View File
@@ -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.
+68 -18
View File
@@ -1,11 +1,15 @@
import { Buffer } from "node:buffer";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { exec, getExecOutput } from "@actions/exec"; import { exec, getExecOutput } from "@actions/exec";
import * as github from "@actions/github"; import { context } from "@actions/github";
import { commitChangesFromRepo } from "@changesets/ghcommit/git"; 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 = { type GitOptions = {
cwd: string; cwd: string;
env?: Record<string, string>;
}; };
const push = async (branch: string, options: GitOptions) => { const push = async (branch: string, options: GitOptions) => {
@@ -46,17 +50,51 @@ const checkIfClean = async (options: GitOptions): Promise<boolean> => {
return !stdout.length; return !stdout.length;
}; };
export class Git { export class GitHub {
readonly octokit: Octokit | null; readonly #githubToken: string;
readonly octokit: Octokit;
readonly cwd: string; readonly cwd: string;
readonly commitMode: CommitMode;
constructor(args: { octokit?: Octokit; cwd: string }) { constructor(options: {
this.octokit = args.octokit ?? null; githubToken: string;
this.cwd = args.cwd; cwd: string;
commitMode?: CommitMode;
}) {
this.#githubToken = options.githubToken;
this.cwd = options.cwd;
this.commitMode = options.commitMode ?? "git-cli";
this.octokit = setupOctokit(options.githubToken);
}
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() { async setupUser() {
if (this.octokit) { if (this.commitMode === "github-api") {
return; return;
} }
await exec("git", ["config", "user.name", `"github-actions[bot]"`], { await exec("git", ["config", "user.name", `"github-actions[bot]"`], {
@@ -76,32 +114,38 @@ export class Git {
} }
async pushTag(tag: string) { async pushTag(tag: string) {
if (this.octokit) { if (this.commitMode === "github-api") {
return this.octokit.rest.git return this.octokit.rest.git
.createRef({ .createRef({
...github.context.repo, ...context.repo,
ref: `refs/tags/${tag}`, ref: `refs/tags/${tag}`,
sha: github.context.sha, sha: context.sha,
}) })
.catch((err) => { .catch((err) => {
// Assuming tag was manually pushed in custom publish script // Assuming tag was manually pushed in custom publish script
core.warning(`Failed to create tag ${tag}: ${err.message}`); 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) { async prepareBranch(branch: string) {
if (this.octokit) { if (this.commitMode === "github-api") {
// Preparing a new local branch is not necessary when using the API // Preparing a new local branch is not necessary when using the API
return; return;
} }
await switchToMaybeExistingBranch(branch, { cwd: this.cwd }); 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 }) { async pushChanges({ branch, message }: { branch: string; message: string }) {
if (this.octokit) { if (this.commitMode === "github-api") {
/** /**
* Only add files form the current working directory * Only add files form the current working directory
* *
@@ -111,11 +155,11 @@ export class Git {
const addFromDirectory = this.cwd; const addFromDirectory = this.cwd;
return commitChangesFromRepo({ return commitChangesFromRepo({
octokit: this.octokit, octokit: this.octokit,
...github.context.repo, ...context.repo,
branch, branch,
message, message,
base: { base: {
commit: github.context.sha, commit: context.sha,
}, },
cwd: this.cwd, cwd: this.cwd,
force: true, force: true,
@@ -124,6 +168,12 @@ export class Git {
if (!(await checkIfClean({ cwd: this.cwd }))) { if (!(await checkIfClean({ cwd: this.cwd }))) {
await commitAll(message, { 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>,
});
} }
} }
+7 -19
View File
@@ -1,7 +1,6 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { Git } from "./git.ts"; import { GitHub } from "./github.ts";
import { setupOctokit } from "./octokit.ts";
import readChangesetState from "./readChangesetState.ts"; import readChangesetState from "./readChangesetState.ts";
import { runPublish, runVersion } from "./run.ts"; import { runPublish, runVersion } from "./run.ts";
import { fileExists, getOptionalInput } from "./utils.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 // If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd(); const cwd = process.cwd();
const octokit = setupOctokit(githubToken);
const commitMode = getOptionalInput("commitMode") ?? "git-cli"; const commitMode = getOptionalInput("commitMode") ?? "git-cli";
const prDraft = getOptionalInput("prDraft"); const prDraft = getOptionalInput("prDraft");
if (commitMode !== "git-cli" && commitMode !== "github-api") { if (commitMode !== "git-cli" && commitMode !== "github-api") {
@@ -30,24 +28,19 @@ import { fileExists, getOptionalInput } from "./utils.ts";
core.setFailed(`Invalid prDraft: ${prDraft}`); core.setFailed(`Invalid prDraft: ${prDraft}`);
return; return;
} }
const git = new Git({ const github = new GitHub({
octokit: commitMode === "github-api" ? octokit : undefined,
cwd, cwd,
githubToken,
commitMode,
}); });
let setupGitUser = core.getBooleanInput("setupGitUser"); let setupGitUser = core.getBooleanInput("setupGitUser");
if (setupGitUser) { if (setupGitUser) {
core.info("setting git user"); 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 { changesets } = await readChangesetState(cwd);
let publishScript = core.getInput("publish"); let publishScript = core.getInput("publish");
@@ -119,9 +112,7 @@ import { fileExists, getOptionalInput } from "./utils.ts";
const result = await runPublish({ const result = await runPublish({
script: publishScript, script: publishScript,
githubToken, github,
git,
octokit,
createGithubReleases: core.getBooleanInput("createGithubReleases"), createGithubReleases: core.getBooleanInput("createGithubReleases"),
cwd, cwd,
}); });
@@ -152,12 +143,9 @@ import { fileExists, getOptionalInput } from "./utils.ts";
core.info("All changesets are empty; not creating PR"); core.info("All changesets are empty; not creating PR");
return; return;
case hasChangesets: { case hasChangesets: {
const octokit = setupOctokit(githubToken);
const { pullRequestNumber } = await runVersion({ const { pullRequestNumber } = await runVersion({
script: getOptionalInput("version"), script: getOptionalInput("version"),
githubToken, github,
git,
octokit,
cwd, cwd,
prTitle: getOptionalInput("title"), prTitle: getOptionalInput("title"),
commitMessage: getOptionalInput("commit"), commitMessage: getOptionalInput("commit"),
+4 -8
View File
@@ -1,8 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { Git } from "../git.ts"; import { GitHub } from "../github.ts";
import { setupOctokit } from "../octokit.ts";
import { runPublish } from "../run.ts"; import { runPublish } from "../run.ts";
import { import {
downloadArtifact, downloadArtifact,
@@ -25,9 +24,8 @@ async function main() {
// If the user needs to change the cwd, set `working-directory` in the step instead // If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd(); const cwd = process.cwd();
const octokit = setupOctokit(githubToken); // NOTE: Always use API mode here as publish does not need a commit-mode.
// NOTE: Always pass octokit here as publish does not need a commit-mode const github = new GitHub({ cwd, githubToken, commitMode: "github-api" });
const git = new Git({ octokit, cwd });
const fromPackDir = packDirArtifactId const fromPackDir = packDirArtifactId
? await downloadArtifact( ? await downloadArtifact(
@@ -39,9 +37,7 @@ async function main() {
const result = await runPublish({ const result = await runPublish({
script, script,
githubToken, github,
git,
octokit,
createGithubReleases, createGithubReleases,
cwd, cwd,
fromPackDir, fromPackDir,
+16 -27
View File
@@ -3,8 +3,7 @@ import type { Changeset } from "@changesets/types";
import writeChangeset from "@changesets/write"; import writeChangeset from "@changesets/write";
import { createFixture } from "fs-fixture"; import { createFixture } from "fs-fixture";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { Git } from "./git.ts"; import { GitHub } from "./github.ts";
import { setupOctokit } from "./octokit.ts";
import { runVersion } from "./run.ts"; import { runVersion } from "./run.ts";
vi.mock("@actions/github", () => ({ vi.mock("@actions/github", () => ({
@@ -21,7 +20,6 @@ vi.mock("@actions/github", () => ({
graphql: mockedGraphql, graphql: mockedGraphql,
}), }),
})); }));
vi.mock("./git.ts");
vi.mock("@changesets/ghcommit/git"); vi.mock("@changesets/ghcommit/git");
let mockedGithubMethods = { let mockedGithubMethods = {
@@ -91,6 +89,13 @@ const writeChangesets = (changesets: Changeset[], cwd: string) => {
return Promise.all(changesets.map((commit) => writeChangeset(commit, cwd))); return Promise.all(changesets.map((commit) => writeChangeset(commit, cwd)));
}; };
const createGithub = (cwd: string) =>
new GitHub({
cwd,
githubToken: "@@GITHUB_TOKEN",
commitMode: "github-api",
});
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@@ -126,9 +131,7 @@ describe("version", () => {
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
}); });
@@ -161,9 +164,7 @@ describe("version", () => {
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
prDraft: "create", prDraft: "create",
}); });
@@ -197,9 +198,7 @@ describe("version", () => {
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
}); });
@@ -232,9 +231,7 @@ describe("version", () => {
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
}); });
@@ -287,9 +284,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
prBodyMaxCharacters: 1000, prBodyMaxCharacters: 1000,
}); });
@@ -346,9 +341,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
prBodyMaxCharacters: 500, prBodyMaxCharacters: 500,
}); });
@@ -383,9 +376,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
prDraft: "create", prDraft: "create",
}); });
@@ -417,9 +408,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
); );
await runVersion({ await runVersion({
octokit: setupOctokit("@@GITHUB_TOKEN"), github: createGithub(cwd),
githubToken: "@@GITHUB_TOKEN",
git: new Git({ cwd }),
cwd, cwd,
prDraft: "always", prDraft: "always",
}); });
+22 -25
View File
@@ -7,10 +7,10 @@ import {
type ExecOptions, type ExecOptions,
type ExecOutput, type ExecOutput,
} from "@actions/exec"; } from "@actions/exec";
import * as github from "@actions/github"; import { context } from "@actions/github";
import type { PreState } from "@changesets/types"; import type { PreState } from "@changesets/types";
import { type Package, getPackages } from "@manypkg/get-packages"; 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 type { Octokit } from "./octokit.ts";
import readChangesetState from "./readChangesetState.ts"; import readChangesetState from "./readChangesetState.ts";
import { import {
@@ -57,17 +57,15 @@ const createRelease = async (
tag_name: tagName, tag_name: tagName,
body: changelogEntry.content, body: changelogEntry.content,
prerelease: pkg.packageJson.version.includes("-"), prerelease: pkg.packageJson.version.includes("-"),
...github.context.repo, ...context.repo,
}); });
}; };
type PublishOptions = { type PublishOptions = {
script?: string; script?: string;
fromPackDir?: string; fromPackDir?: string;
githubToken: string;
octokit: Octokit;
createGithubReleases: boolean; createGithubReleases: boolean;
git: Git; github: GitHub;
cwd: string; cwd: string;
}; };
@@ -87,17 +85,16 @@ type PublishResult =
export async function runPublish({ export async function runPublish({
script, script,
fromPackDir, fromPackDir,
githubToken, github,
git,
octokit,
createGithubReleases, createGithubReleases,
cwd, cwd,
}: PublishOptions): Promise<PublishResult> { }: PublishOptions): Promise<PublishResult> {
const { octokit } = github;
let changesetPublishOutput: ExecOutput; let changesetPublishOutput: ExecOutput;
const execOptions: ExecOptions = { const execOptions: ExecOptions = {
cwd, cwd,
ignoreReturnCode: true, ignoreReturnCode: true,
env: { ...process.env, GITHUB_TOKEN: githubToken }, env: { ...process.env, GITHUB_TOKEN: github.getToken() },
}; };
if (script) { if (script) {
@@ -144,7 +141,7 @@ export async function runPublish({
await Promise.all( await Promise.all(
releasedPackages.map(async (pkg) => { releasedPackages.map(async (pkg) => {
const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`; const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`;
await git.pushTag(tagName); await github.pushTag(tagName);
await createRelease(octokit, { pkg, tagName }); await createRelease(octokit, { pkg, tagName });
}), }),
); );
@@ -166,7 +163,7 @@ export async function runPublish({
releasedPackages.push(pkg); releasedPackages.push(pkg);
if (createGithubReleases) { if (createGithubReleases) {
const tagName = `v${pkg.packageJson.version}`; const tagName = `v${pkg.packageJson.version}`;
await git.pushTag(tagName); await github.pushTag(tagName);
await createRelease(octokit, { pkg, tagName }); await createRelease(octokit, { pkg, tagName });
} }
break; break;
@@ -259,9 +256,7 @@ export async function getVersionPrBody({
type VersionOptions = { type VersionOptions = {
script?: string; script?: string;
githubToken: string; github: GitHub;
git: Git;
octokit: Octokit;
cwd?: string; cwd?: string;
prTitle?: string; prTitle?: string;
commitMessage?: string; commitMessage?: string;
@@ -277,26 +272,25 @@ type RunVersionResult = {
export async function runVersion({ export async function runVersion({
script, script,
githubToken, github,
git,
octokit,
cwd = process.cwd(), cwd = process.cwd(),
prTitle = "Version Packages", prTitle = "Version Packages",
commitMessage = "Version Packages", commitMessage = "Version Packages",
hasPublishScript = false, hasPublishScript = false,
prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE, prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE,
branch = github.context.ref.replace("refs/heads/", ""), branch = context.ref.replace("refs/heads/", ""),
prDraft, prDraft,
}: VersionOptions): Promise<RunVersionResult> { }: VersionOptions): Promise<RunVersionResult> {
const { octokit } = github;
let versionBranch = `changeset-release/${branch}`; let versionBranch = `changeset-release/${branch}`;
let { preState } = await readChangesetState(cwd); let { preState } = await readChangesetState(cwd);
await git.prepareBranch(versionBranch); await github.prepareBranch(versionBranch);
let versionsByDirectory = await getVersionsByDirectory(cwd); let versionsByDirectory = await getVersionsByDirectory(cwd);
const env = { ...process.env, GITHUB_TOKEN: githubToken }; const env = { ...process.env, GITHUB_TOKEN: github.getToken() };
if (script) { if (script) {
await exec(script, undefined, { cwd, env }); await exec(script, undefined, { cwd, env });
@@ -335,9 +329,9 @@ export async function runVersion({
* which GitHub will then react to by closing the PRs) * which GitHub will then react to by closing the PRs)
*/ */
const existingPullRequests = await octokit.rest.pulls.list({ const existingPullRequests = await octokit.rest.pulls.list({
...github.context.repo, ...context.repo,
state: "open", state: "open",
head: `${github.context.repo.owner}:${versionBranch}`, head: `${context.repo.owner}:${versionBranch}`,
base: branch, base: branch,
}); });
core.info( 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) const changedPackagesInfo = (await changedPackagesInfoPromises)
.filter((x) => x) .filter((x) => x)
@@ -370,7 +367,7 @@ export async function runVersion({
title: finalPrTitle, title: finalPrTitle,
body: prBody, body: prBody,
draft: prDraft !== undefined, draft: prDraft !== undefined,
...github.context.repo, ...context.repo,
}); });
return { return {
+9 -9
View File
@@ -1,6 +1,5 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { Git } from "../git.ts"; import { GitHub } from "../github.ts";
import { setupOctokit } from "../octokit.ts";
import { runVersion } from "../run.ts"; import { runVersion } from "../run.ts";
import { getOptionalInput, getRequiredInput } from "../utils.ts"; import { getOptionalInput, getRequiredInput } from "../utils.ts";
@@ -24,26 +23,27 @@ async function main() {
if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") { if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") {
throw new Error(`Invalid pr-draft input: ${prDraft}`); 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 // If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd(); const cwd = process.cwd();
const octokit = setupOctokit(githubToken); const github = new GitHub({
const git = new Git({
octokit: commitMode === "github-api" ? octokit : undefined,
cwd, cwd,
githubToken,
commitMode,
}); });
if (setupGitUser) { if (setupGitUser) {
core.info("setting git user"); core.info("setting git user");
await git.setupUser(); await github.setupUser();
} }
const { pullRequestNumber } = await runVersion({ const { pullRequestNumber } = await runVersion({
script, script,
githubToken, github,
git,
octokit,
cwd, cwd,
prTitle, prTitle,
commitMessage, commitMessage,