From 893ba16eb15f436c0f29fa125b17858767f08f4e Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Sat, 2 Nov 2024 11:27:25 +0000 Subject: [PATCH] Make using GitHub API Optional Change this to a minor version bump, with a new feature that allows for using the GitHub API to create tags and commits. --- .changeset/green-dogs-change.md | 12 ++-- .changeset/ninety-poems-explode.md | 5 -- .changeset/thick-jars-chew.md | 5 -- README.md | 1 + action.yml | 7 ++ src/gitUtils.ts | 51 ++++++++++++++- src/index.ts | 4 ++ src/run.ts | 100 ++++++++++++++++++----------- 8 files changed, 131 insertions(+), 54 deletions(-) delete mode 100644 .changeset/ninety-poems-explode.md delete mode 100644 .changeset/thick-jars-chew.md diff --git a/.changeset/green-dogs-change.md b/.changeset/green-dogs-change.md index 05a0e5d6..4820bebb 100644 --- a/.changeset/green-dogs-change.md +++ b/.changeset/green-dogs-change.md @@ -1,10 +1,10 @@ --- -"@changesets/action": major +"@changesets/action": minor --- -Start using GitHub API to push tags and commits to repos +Introduce a new input commitUsingApi that allows pushing tags and commits +using the GitHub API instead of the git CLI. -Rather than use local git commands to push changes to GitHub, -this action now uses the GitHub API directly, -which means that all tags and commits will be attributed to the user whose -GITHUB_TOKEN is used, and signed. +When used, this means means that all tags and commits will be attributed +to the user whose GITHUB_TOKEN is used, +and also signed using GitHub's internal GPG key. diff --git a/.changeset/ninety-poems-explode.md b/.changeset/ninety-poems-explode.md deleted file mode 100644 index b5d592ff..00000000 --- a/.changeset/ninety-poems-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@changesets/action": patch ---- - -Update to latest version of ghcommit diff --git a/.changeset/thick-jars-chew.md b/.changeset/thick-jars-chew.md deleted file mode 100644 index f5a3f095..00000000 --- a/.changeset/thick-jars-chew.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@changesets/action": minor ---- - -Handle custom publish commands more gracefully diff --git a/README.md b/README.md index cc7f703b..c531e98d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This action for [Changesets](https://github.com/atlassian/changesets) creates a - title - The pull request title. Default to `Version Packages` - setupGitUser - Sets up the git user for commits as `"github-actions[bot]"`. Default to `true` - createGithubReleases - A boolean value to indicate whether to create Github releases after `publish` or not. Default to `true` +- commitUsingApi - A boolean value to indicate whether to use the GitHub API to push changes or not, so changes are GPG-signed. Default to `false` - cwd - Changes node's `process.cwd()` if the project is not located on the root. Default to `process.cwd()` ### Outputs diff --git a/action.yml b/action.yml index 86b97909..d067082e 100644 --- a/action.yml +++ b/action.yml @@ -28,6 +28,13 @@ inputs: description: "A boolean value to indicate whether to create Github releases after `publish` or not" required: false default: true + commitUsingApi: + description: > + A boolean value to indicate whether to push changes via Github API or not, + this will mean all commits and tags are signed using GitHub's GPG key, + and attributed to the user or app who owns the GITHUB_TOKEN + required: false + default: false branch: description: Sets the branch in which the action will run. Default to `github.ref_name` if not provided required: false diff --git a/src/gitUtils.ts b/src/gitUtils.ts index 16d9bf21..b15151ab 100644 --- a/src/gitUtils.ts +++ b/src/gitUtils.ts @@ -11,4 +11,53 @@ export const setupUser = async () => { "user.email", `"github-actions[bot]@users.noreply.github.com"`, ]); -}; \ No newline at end of file +}; + +export const pullBranch = async (branch: string) => { + await exec("git", ["pull", "origin", branch]); +}; + +export const push = async ( + branch: string, + { force }: { force?: boolean } = {} +) => { + await exec( + "git", + ["push", "origin", `HEAD:${branch}`, force && "--force"].filter( + Boolean as any + ) + ); +}; + +export const pushTags = async () => { + await exec("git", ["push", "origin", "--tags"]); +}; + +export const switchToMaybeExistingBranch = async (branch: string) => { + let { stderr } = await getExecOutput("git", ["checkout", branch], { + ignoreReturnCode: true, + }); + let isCreatingBranch = !stderr + .toString() + .includes(`Switched to a new branch '${branch}'`); + if (isCreatingBranch) { + await exec("git", ["checkout", "-b", branch]); + } +}; + +export const reset = async ( + pathSpec: string, + mode: "hard" | "soft" | "mixed" = "hard" +) => { + await exec("git", ["reset", `--${mode}`, pathSpec]); +}; + +export const commitAll = async (message: string) => { + await exec("git", ["add", "."]); + await exec("git", ["commit", "-m", message]); +}; + +export const checkIfClean = async (): Promise => { + const { stdout } = await getExecOutput("git", ["status", "--porcelain"]); + return !stdout.length; +}; diff --git a/src/index.ts b/src/index.ts index 194204dc..8b85e450 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,8 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined; await gitUtils.setupUser(); } + const commitUsingApi = core.getBooleanInput("commitUsingApi"); + core.info("setting GitHub credentials"); await fs.writeFile( `${process.env.HOME}/.netrc`, @@ -88,6 +90,7 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined; script: publishScript, githubToken, createGithubReleases: core.getBooleanInput("createGithubReleases"), + commitUsingApi }); if (result.published) { @@ -109,6 +112,7 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined; prTitle: getOptionalInput("title"), commitMessage: getOptionalInput("commit"), hasPublishScript, + commitUsingApi, branch: getOptionalInput("branch"), }); diff --git a/src/run.ts b/src/run.ts index 25b35c93..c1ea9efd 100644 --- a/src/run.ts +++ b/src/run.ts @@ -101,6 +101,7 @@ type PublishOptions = { script: string; githubToken: string; createGithubReleases: boolean; + commitUsingApi: boolean; cwd?: string; }; @@ -119,6 +120,7 @@ export async function runPublish({ script, githubToken, createGithubReleases, + commitUsingApi, cwd = process.cwd(), }: PublishOptions): Promise { const octokit = setupOctokit(githubToken); @@ -131,6 +133,10 @@ export async function runPublish({ { cwd } ); + if (!commitUsingApi) { + await gitUtils.pushTags(); + } + let { packages, tool } = await getPackages(cwd); let releasedPackages: Package[] = []; @@ -158,21 +164,21 @@ export async function runPublish({ await Promise.all( releasedPackages.map(async (pkg) => { const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`; - // Tag will only be created locally, - // Create it using the GitHub API so it's signed. - await octokit.rest.git - .createRef({ - ...github.context.repo, - ref: `refs/tags/${tagName}`, - sha: github.context.sha, - }) - .catch((err) => { - // Assuming tag was manually pushed in custom publish script - core.warning(`Failed to create tag ${tagName}: ${err.message}`); - }); - if (createGithubReleases) { - await createRelease(octokit, { pkg, tagName }); + if (commitUsingApi) { + // Tag will usually only be created locally, + // Create it using the GitHub API so it's signed. + await octokit.rest.git + .createRef({ + ...github.context.repo, + ref: `refs/tags/${tagName}`, + sha: github.context.sha, + }) + .catch((err) => { + // Assuming tag was manually pushed in custom publish script + core.warning(`Failed to create tag ${tagName}: ${err.message}`); + }); } + await createRelease(octokit, { pkg, tagName }); }) ); } @@ -191,20 +197,22 @@ export async function runPublish({ if (match) { releasedPackages.push(pkg); - const tagName = `v${pkg.packageJson.version}`; - // Tag will only be created locally, - // Create it using the GitHub API so it's signed. - await octokit.rest.git - .createRef({ - ...github.context.repo, - ref: `refs/tags/${tagName}`, - sha: github.context.sha, - }) - .catch((err) => { - // Assuming tag was manually pushed in custom publish script - core.warning(`Failed to create tag ${tagName}: ${err.message}`); - }); if (createGithubReleases) { + const tagName = `v${pkg.packageJson.version}`; + if (commitUsingApi) { + // Tag will only be created locally, + // Create it using the GitHub API so it's signed. + await octokit.rest.git + .createRef({ + ...github.context.repo, + ref: `refs/tags/${tagName}`, + sha: github.context.sha, + }) + .catch((err) => { + // Assuming tag was manually pushed in custom publish script + core.warning(`Failed to create tag ${tagName}: ${err.message}`); + }); + } await createRelease(octokit, { pkg, tagName }); } break; @@ -320,6 +328,7 @@ type VersionOptions = { commitMessage?: string; hasPublishScript?: boolean; prBodyMaxCharacters?: number; + commitUsingApi: boolean; branch?: string; }; @@ -335,6 +344,7 @@ export async function runVersion({ commitMessage = "Version Packages", hasPublishScript = false, prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE, + commitUsingApi, branch, }: VersionOptions): Promise { const octokit = setupOctokit(githubToken); @@ -345,6 +355,11 @@ export async function runVersion({ let { preState } = await readChangesetState(cwd); + if (!commitUsingApi) { + await gitUtils.switchToMaybeExistingBranch(versionBranch); + await gitUtils.reset(github.context.sha); + } + let versionsByDirectory = await getVersionsByDirectory(cwd); if (script) { @@ -389,16 +404,27 @@ export async function runVersion({ !!preState ? ` (${preState.tag})` : "" }`; - await commitChangesFromRepo({ - octokit, - ...github.context.repo, - branch: versionBranch, - message: finalCommitMessage, - base: { - commit: github.context.sha, - }, - force: true, - }); + if (commitUsingApi) { + await commitChangesFromRepo({ + octokit, + ...github.context.repo, + branch: versionBranch, + message: finalCommitMessage, + base: { + commit: github.context.sha, + }, + force: true, + }); + } else { + // project with `commit: true` setting could have already committed files + if (!(await gitUtils.checkIfClean())) { + await gitUtils.commitAll(finalCommitMessage); + } + } + + if (!commitUsingApi) { + await gitUtils.push(versionBranch, { force: true }); + } let existingPullRequests = await existingPullRequestsPromise; core.info(JSON.stringify(existingPullRequests.data, null, 2));