Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce an option to use the GitHub API to commit changes, for GPG #391

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions .changeset/green-dogs-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@changesets/action": minor
---

Introduce a new input commitUsingApi that allows pushing tags and commits
using the GitHub API instead of the git CLI.

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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
"main": "dist/index.js",
"license": "MIT",
"devDependencies": {
"@changesets/changelog-github": "^0.4.2",
"@changesets/cli": "^2.20.0",
"@changesets/write": "^0.1.6",
"@vercel/ncc": "^0.36.1",
"fixturez": "^1.1.0",
"prettier": "^2.0.5",
"typescript": "^5.0.4",
"@babel/core": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@babel/preset-typescript": "^7.13.0",
"@changesets/changelog-github": "^0.4.2",
"@changesets/cli": "^2.20.0",
"@changesets/write": "^0.1.6",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^29.5.1",
"@types/node": "^20.11.17",
"@types/semver": "^7.5.0",
"@vercel/ncc": "^0.36.1",
"babel-jest": "^29.5.0",
"fixturez": "^1.1.0",
"husky": "^3.0.3",
"jest": "^29.5.0"
"jest": "^29.5.0",
"prettier": "^2.0.5",
"typescript": "^5.0.4"
},
"scripts": {
"build": "ncc build src/index.ts -o dist --transpile-only --minify",
Expand All @@ -41,6 +41,7 @@
"@changesets/read": "^0.5.3",
"@manypkg/get-packages": "^1.1.3",
"@octokit/plugin-throttling": "^5.2.1",
"@s0/ghcommit": "^1.2.1",
"fs-extra": "^8.1.0",
"mdast-util-to-string": "^1.0.6",
"remark-parse": "^7.0.1",
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -88,6 +90,7 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined;
script: publishScript,
githubToken,
createGithubReleases: core.getBooleanInput("createGithubReleases"),
commitUsingApi
});

if (result.published) {
Expand All @@ -109,6 +112,7 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined;
prTitle: getOptionalInput("title"),
commitMessage: getOptionalInput("commit"),
hasPublishScript,
commitUsingApi,
branch: getOptionalInput("branch"),
});

Expand Down
1 change: 1 addition & 0 deletions src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jest.mock("@actions/github/lib/utils", () => ({
getOctokitOptions: jest.fn(),
}));
jest.mock("./gitUtils");
jest.mock("@s0/ghcommit/git");

let mockedGithubMethods = {
pulls: {
Expand Down
88 changes: 68 additions & 20 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as gitUtils from "./gitUtils";
import readChangesetState from "./readChangesetState";
import resolveFrom from "resolve-from";
import { throttling } from "@octokit/plugin-throttling";
import { commitChangesFromRepo } from "@s0/ghcommit/git";

// GitHub Issues/PRs messages have a max size limit on the
// message body payload.
Expand Down Expand Up @@ -100,6 +101,7 @@ type PublishOptions = {
script: string;
githubToken: string;
createGithubReleases: boolean;
commitUsingApi: boolean;
cwd?: string;
};

Expand All @@ -118,6 +120,7 @@ export async function runPublish({
script,
githubToken,
createGithubReleases,
commitUsingApi,
cwd = process.cwd(),
}: PublishOptions): Promise<PublishResult> {
const octokit = setupOctokit(githubToken);
Expand All @@ -130,7 +133,9 @@ export async function runPublish({
{ cwd }
);

await gitUtils.pushTags();
if (!commitUsingApi) {
await gitUtils.pushTags();
}

let { packages, tool } = await getPackages(cwd);
let releasedPackages: Package[] = [];
Expand All @@ -157,12 +162,24 @@ export async function runPublish({

if (createGithubReleases) {
await Promise.all(
releasedPackages.map((pkg) =>
createRelease(octokit, {
pkg,
tagName: `${pkg.packageJson.name}@${pkg.packageJson.version}`,
})
)
releasedPackages.map(async (pkg) => {
const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`;
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 });
})
);
}
} else {
Expand All @@ -181,10 +198,22 @@ export async function runPublish({
if (match) {
releasedPackages.push(pkg);
if (createGithubReleases) {
await createRelease(octokit, {
pkg,
tagName: `v${pkg.packageJson.version}`,
});
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;
}
Expand Down Expand Up @@ -299,6 +328,7 @@ type VersionOptions = {
commitMessage?: string;
hasPublishScript?: boolean;
prBodyMaxCharacters?: number;
commitUsingApi: boolean;
branch?: string;
};

Expand All @@ -314,6 +344,7 @@ export async function runVersion({
commitMessage = "Version Packages",
hasPublishScript = false,
prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE,
commitUsingApi,
branch,
}: VersionOptions): Promise<RunVersionResult> {
const octokit = setupOctokit(githubToken);
Expand All @@ -324,8 +355,10 @@ export async function runVersion({

let { preState } = await readChangesetState(cwd);

await gitUtils.switchToMaybeExistingBranch(versionBranch);
await gitUtils.reset(github.context.sha);
if (!commitUsingApi) {
await gitUtils.switchToMaybeExistingBranch(versionBranch);
await gitUtils.reset(github.context.sha);
}

let versionsByDirectory = await getVersionsByDirectory(cwd);

Expand Down Expand Up @@ -367,16 +400,31 @@ export async function runVersion({
);

const finalPrTitle = `${prTitle}${!!preState ? ` (${preState.tag})` : ""}`;
const finalCommitMessage = `${commitMessage}${
!!preState ? ` (${preState.tag})` : ""
}`;

// project with `commit: true` setting could have already committed files
if (!(await gitUtils.checkIfClean())) {
const finalCommitMessage = `${commitMessage}${
!!preState ? ` (${preState.tag})` : ""
}`;
await gitUtils.commitAll(finalCommitMessage);
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);
}
}

await gitUtils.push(versionBranch, { force: true });
if (!commitUsingApi) {
await gitUtils.push(versionBranch, { force: true });
}

let existingPullRequests = await existingPullRequestsPromise;
core.info(JSON.stringify(existingPullRequests.data, null, 2));
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */

/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "nodenext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
Expand Down
Loading