Skip to content

Commit 6297df6

Browse files
committed
feat: initial version
1 parent fe4977a commit 6297df6

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

action.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Create or Update Pull Request
2+
description: "A GitHub Action to create or update a pull request based on local changes"
3+
inputs:
4+
title:
5+
description: "Pull Request title"
6+
required: true
7+
default: "Update from 'Create or Update Request' action"
8+
body:
9+
description: "Pull Request body"
10+
required: true
11+
default: |
12+
This pull request has been created by the ["Create or Update Pull Request" action](https://github.com/gr2m/create-or-update-pull-request-action#readme).
13+
14+
You can set a custom pull request title, body, branch and commit messages, see [Usage](https://github.com/gr2m/create-or-update-pull-request-action#usage).
15+
branch:
16+
description: "Branch name"
17+
required: true
18+
default: create-or-update-pull-request-action
19+
commit-message:
20+
description: Commit message
21+
required: true
22+
default: Update from 'Create or Update Request' action
23+
author:
24+
description: Commit author name & email address. Must be in the form "Name <[email protected]>"
25+
required: true
26+
default: "Create or Update Pull Request Action <[email protected]>"
27+
runs:
28+
using: "node12"
29+
main: "dist/index.js"

index.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
const assert = require("assert");
2+
const { inspect } = require("util");
3+
4+
const { command } = require("execa");
5+
const core = require("@actions/core");
6+
const { request } = require("@octokit/request");
7+
8+
main();
9+
10+
async function main() {
11+
if (!process.env.GITHUB_TOKEN) {
12+
core.setFailed(
13+
`GITHUB_TOKEN is not configured. Make sure you made it available to your action
14+
15+
uses: gr2m/create-or-update-pull-request-with-local-changes-action@master
16+
env:
17+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}`
18+
);
19+
return;
20+
}
21+
22+
if (!process.env.GITHUB_REPOSITORY) {
23+
core.setFailed(
24+
'GITHUB_REPOSITORY missing, must be set to "<repo owner>/<repo name>"'
25+
);
26+
return;
27+
}
28+
if (!process.env.GITHUB_REF) {
29+
core.setFailed(
30+
"GITHUB_REF missing, must be set to the repository's default branch"
31+
);
32+
return;
33+
}
34+
35+
try {
36+
const inputs = {
37+
title: core.getInput("title"),
38+
body: core.getInput("body"),
39+
branch: core.getInput("branch"),
40+
commitMessage: core.getInput("commit-message"),
41+
author: core.getInput("author")
42+
};
43+
44+
core.debug(`Inputs: ${inspect(inputs)}`);
45+
46+
const { hasChanges, hasUncommitedChanges } = await getLocalChanges();
47+
48+
if (!hasChanges) {
49+
core.info("No local changes");
50+
process.exit(0); // there is currently no neutral exit code
51+
}
52+
53+
if (hasUncommitedChanges) {
54+
core.debug(`Uncommited changes found`);
55+
56+
const gitUser = await getGitUser();
57+
if (gitUser) {
58+
core.debug(`Git User already configured as: ${inspect(gitUser)}`);
59+
} else {
60+
const matches = inputs.author.match(/^([^<]+)\s*<([^>]+)>$/);
61+
assert(
62+
matches,
63+
`The "author" input "${inputs.author}" does conform to the "Name <[email protected]>" format`
64+
);
65+
const [, name, email] = matches;
66+
67+
await setGitUser({
68+
name,
69+
email
70+
});
71+
}
72+
73+
core.debug(`Comitting local changes`);
74+
await command("git add .", { shell: true });
75+
await command(
76+
`git commit -a -m "${inputs.commitMessage}" --author "${inputs.author}"`,
77+
{ shell: true }
78+
);
79+
} else {
80+
core.debug(`No uncommited changes found`);
81+
}
82+
83+
core.debug(`Try to fetch and checkout remote branch`);
84+
const remoteBranchExists = await checkOutRemoteBranch(inputs.branch);
85+
86+
core.debug(`Pushing local changes`);
87+
const { stdout: pushStdOut, stderr: pushStdErr } = await command(
88+
`git push -f https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/${process.env.GITHUB_REPOSITORY}.git HEAD:refs/heads/${inputs.branch}`,
89+
{ shell: true }
90+
);
91+
92+
// no idea why the `git push` output goes into stderr. Checking in both just in case.
93+
if (remoteBranchExists) {
94+
core.info(`Existing pull request for "${inputs.branch}" updated`);
95+
return;
96+
}
97+
98+
core.debug(`Creating pull request`);
99+
const {
100+
data: { html_url }
101+
} = await request(`POST /repos/${process.env.GITHUB_REPOSITORY}/pulls`, {
102+
headers: {
103+
authorization: `token ${process.env.GITHUB_TOKEN}`
104+
},
105+
title: inputs.title,
106+
body: inputs.body,
107+
head: inputs.branch,
108+
base: process.env.GITHUB_REF.substr("refs/heads/".length)
109+
});
110+
111+
core.info(`Pull request created: ${html_url}`);
112+
} catch (error) {
113+
core.debug(inspect(error));
114+
core.setFailed(error.message);
115+
}
116+
}
117+
118+
async function getLocalChanges() {
119+
const { stdout } = await command("git status", { shell: true });
120+
121+
if (/Your branch is up to date/.test(stdout)) {
122+
return;
123+
}
124+
125+
const hasCommits = /Your branch is ahead/.test(stdout);
126+
const hasUncommitedChanges = /(Changes to be committed|Changes not staged|Untracked files)/.test(
127+
stdout
128+
);
129+
130+
return {
131+
hasCommits,
132+
hasUncommitedChanges,
133+
hasChanges: hasCommits || hasUncommitedChanges
134+
};
135+
}
136+
137+
async function getGitUser() {
138+
try {
139+
const { stdout: name } = await command("git config --get user.name", {
140+
shell: true
141+
});
142+
const { stdout: email } = await command("git config --get user.email", {
143+
shell: true
144+
});
145+
146+
return {
147+
name,
148+
email
149+
};
150+
} catch (error) {
151+
return;
152+
}
153+
}
154+
155+
async function setGitUser({ name, email }) {
156+
core.debug(`Configuring user.name as "${name}"`);
157+
await command(`git config --global user.name "${name}"`, { shell: true });
158+
159+
core.debug(`Configuring user.email as "${email}"`);
160+
await command(`git config --global user.email "${email}"`, { shell: true });
161+
}
162+
163+
async function checkOutRemoteBranch(branch) {
164+
try {
165+
await command(
166+
`git fetch https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/${process.env.GITHUB_REPOSITORY}.git ${branch}:${branch}`,
167+
{ shell: true }
168+
);
169+
await command(`git checkout ${branch}`, { shell: true });
170+
core.info(`Remote branch "${branch}" checked out locally.`);
171+
await command(`git rebase -Xtheirs -`, { shell: true });
172+
return true;
173+
} catch (error) {
174+
core.info(`Branch "${branch}" does not yet exist on remote.`);
175+
return false;
176+
}
177+
}

0 commit comments

Comments
 (0)