Skip to content

Commit 6671200

Browse files
committed
Initial commit
0 parents  commit 6671200

File tree

6 files changed

+312
-0
lines changed

6 files changed

+312
-0
lines changed

Dockerfile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM alpine:3.10
2+
3+
RUN apk add bash curl git gnupg jq
4+
5+
COPY README.md entrypoint.sh /
6+
7+
COPY lib.sh /
8+
RUN chmod +x /lib.sh
9+
COPY gpg-wrapper /
10+
RUN chmod +x /gpg-wrapper
11+
12+
13+
ENTRYPOINT ["/entrypoint.sh"]

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# gha-commit-and-push
2+
3+
Github action to commit and push changes in a checkout to a specified branch.
4+
5+
## Usage
6+
7+
For the full list of inputs and outputs see [action.yml](action.yml).
8+
9+
Basic example:
10+
11+
```yaml
12+
on: pull_request
13+
steps:
14+
- uses: magicstack/gha-commit-and-push@master
15+
with:
16+
target_branch: gh-pages
17+
workdir: docs/gh-pages
18+
commit_message: Automatic documentation update
19+
```

action.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: 'Commit and push changes in a checkout.'
2+
author: 'MagicStack Inc.'
3+
inputs:
4+
target_branch:
5+
description: The name of the target branch to push to.
6+
required: true
7+
workdir:
8+
description: Directory containing the repository.
9+
commit_message:
10+
description: The message in the commit.
11+
required: true
12+
github_username:
13+
description: The name of the Github user to use for the commit.
14+
required: true
15+
github_token:
16+
description: Github API token.
17+
required: true
18+
gpg_key:
19+
required: false
20+
description: GPG private key used to sign the commit.
21+
gpg_key_id:
22+
required: false
23+
description: |
24+
The id of a key used to sign the commit. Specific subkeys
25+
must end with '!'. If not specified,
26+
the master imported key would be used instead.
27+
ssh_key:
28+
description: SSH private key used to authenticate with Github.
29+
required: true
30+
runs:
31+
using: 'docker'
32+
image: 'Dockerfile'

entrypoint.sh

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
set -Eeo pipefail
4+
5+
. /lib.sh
6+
7+
check_event_env
8+
check_input github_token commit_message target_branch
9+
10+
gpg_key_id=$(import_gpg_key "${INPUT_GPG_KEY}" "${INPUT_GPG_KEY_ID}")
11+
import_ssh_key "${INPUT_SSH_KEY}"
12+
configure_git "${gpg_key_id}"
13+
14+
branch="${INPUT_TARGET_BRANCH}"
15+
message="${INPUT_COMMIT_MESSAGE}"
16+
workdir="${INPUT_WORKDIR:-.}"
17+
18+
cd "${workdir}"
19+
20+
if git diff --quiet --exit-code; then
21+
echo "No changes."
22+
exit 0
23+
fi
24+
25+
git add .
26+
git commit -m "${message}"
27+
git push --follow-tags origin HEAD:"${branch}"

gpg-wrapper

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
exec /usr/bin/gpg --batch --no-tty "$@"

lib.sh

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/bin/bash
2+
3+
4+
die() {
5+
echo ::error::${@}
6+
exit 1
7+
}
8+
9+
# Check if given required input variables
10+
# are specified, and abort if not.
11+
check_input() {
12+
local varnames="${@}"
13+
local input_varname
14+
local env_varname
15+
16+
if [ -n "${varnames}" ]; then
17+
for input_varname in ${varnames}; do
18+
env_varname="INPUT_${input_varname^^}"
19+
if [ -z "${!env_varname}" ]; then
20+
die "The required '${input_varname}' input variable is not specified."
21+
fi
22+
done
23+
fi
24+
}
25+
26+
# Check if given required environment variables
27+
# are specified, and abort if not.
28+
check_event_env() {
29+
local env_varname
30+
31+
for env_varname in GITHUB_REF GITHUB_EVENT_PATH; do
32+
if [ -z "${env_varname}" ]; then
33+
die "The required '${env_varname}' env variable is not specified."
34+
fi
35+
done
36+
}
37+
38+
# Make a GET request to Github API v3
39+
# Args:
40+
# $1: The API path to request, e.g "/user"
41+
# Environment:
42+
# INPUT_GITHUB_TOKEN: GitHub token with appropriate scope
43+
get() {
44+
curl -sSL -X GET \
45+
-H "Authorization: token ${INPUT_GITHUB_TOKEN}" \
46+
-H "Accept: application/vnd.github.v3+json" \
47+
-H "Accept: application/vnd.github.antiope-preview+json" \
48+
"https://api.github.com${1}"
49+
}
50+
51+
# Make a POST request to Github API v3
52+
# Args:
53+
# $1: The API path to request, e.g "/user"
54+
# Stdin:
55+
# POST request body.
56+
# Environment:
57+
# INPUT_GITHUB_TOKEN: GitHub token with appropriate scope
58+
# Returns:
59+
# HTTP response with numeric status on the last line.
60+
post() {
61+
curl -sSL -v -w "%{http_code}" \
62+
-H "Authorization: token ${INPUT_GITHUB_TOKEN}" \
63+
-H "Accept: application/vnd.github.v3+json" \
64+
-H "Accept: application/vnd.github.antiope-preview+json" \
65+
-d @- \
66+
"https://api.github.com${1}"
67+
}
68+
69+
_prepare_graphql_query() {
70+
# Fold newlines and escape the quotes.
71+
echo "${1}" \
72+
| sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g' \
73+
| sed -e 's/"/\\"/g'
74+
}
75+
76+
# Make a request to Github GraphQL API
77+
# Args:
78+
# $1: GraphQL query.
79+
gql() {
80+
local auth_header="Authorization: bearer ${INPUT_GITHUB_TOKEN}"
81+
echo {\"query\": \"$(_prepare_graphql_query "${1}")\"} \
82+
| curl -sSL -H "${auth_header}" -d @- "https://api.github.com/graphql"
83+
}
84+
85+
# jq wrapper with raw output by default.
86+
jqr() {
87+
jq --raw-output "${@}"
88+
}
89+
90+
# Run a jq operation on Github event JSON
91+
# Args:
92+
# $@: Arguments to jq.
93+
jqevent() {
94+
jqr "${@}" "${GITHUB_EVENT_PATH}"
95+
}
96+
97+
# Wrapper around GPG to make it behave in non-interactive mode.
98+
# Args:
99+
# $@: Arguments to gpg.
100+
gpgb() {
101+
/gpg-wrapper --batch --no-tty "${@}"
102+
}
103+
104+
# Check if the specified user is a maintainer of a given repository.
105+
# Args:
106+
# $1: Github username.
107+
# $2: Organization name.
108+
# $3: Repository name.
109+
is_maintainer() {
110+
local perm
111+
local request
112+
local user="${1}"
113+
local org="${2}"
114+
local repo="${3}"
115+
116+
read -r -d '' request <<EOF
117+
{
118+
organization(login: "${org}") {
119+
teams(first: 100, userLogins: ["${user}"]) {
120+
edges {
121+
node {
122+
name
123+
repositories(first: 100, query: "${repo}") {
124+
edges {
125+
node {
126+
name
127+
}
128+
permission
129+
}
130+
}
131+
}
132+
}
133+
}
134+
}
135+
}
136+
EOF
137+
perm=$(gql "${request}" \
138+
| jqr ".data.organization.teams.edges[]?
139+
| .node.repositories.edges[]
140+
| select(.node.name == \"${repo}\")
141+
| .permission")
142+
143+
[[ "${perm}" = *MAINTAIN* || "${perm}" = *ADMIN* ]]
144+
}
145+
146+
# Import the given GPG private key into the local keychain.
147+
# Args:
148+
# $1: GPG private key (suitable for gpg --import)
149+
# $2: Optional key id to use for signatures. If not specified,
150+
# and the input key contains only one signing subkey, that subkey
151+
# is used.
152+
# Returns:
153+
# The id of the signing key.
154+
import_gpg_key() {
155+
local gpg_key="${1}"
156+
local gpg_key_id="${2}"
157+
158+
if [ -n "${gpg_key}" ]; then
159+
if [ -z "${gpg_key_id}" ]; then
160+
gpg_key_id=$(echo "${gpg_key}" \
161+
| gpgb --import --import-options show-only --with-colons \
162+
| grep '^sec:' \
163+
| cut -f 5 -d':')
164+
165+
if [[ $(echo "${gpg_key_id}" | wc -l) -gt 1 ]]; then
166+
die "Multiple keys found in INPUT_GPG_KEY, please specify " \
167+
"the key id via INPUT_GPG_KEY_ID".
168+
fi
169+
fi
170+
171+
echo "${gpg_key}" \
172+
| gpgb --import
173+
fi
174+
175+
echo "${gpg_key_id}"
176+
}
177+
178+
# Import the given SSH private key for the current user.
179+
# Args:
180+
# $1: SSH private key.
181+
import_ssh_key() {
182+
local ssh_key="${1}"
183+
184+
if [ -n "${ssh_key}" ]; then
185+
mkdir -p "${HOME}/.ssh"
186+
echo "${ssh_key}" > "${HOME}/.ssh/id_rsa"
187+
chmod 600 "${HOME}/.ssh/id_rsa"
188+
fi
189+
}
190+
191+
# Configure git using the information about the current Github user.
192+
# Args:
193+
# $1: Optional GPG key id to use for git object signing.
194+
# Environment:
195+
# INPUT_GITHUB_TOKEN: the token of a Github user to use for git operations.
196+
configure_git() {
197+
local gpg_key_id="${1}"
198+
local user=$(get /user)
199+
local login=$(echo "${user}" | jqr .login)
200+
local username=$(echo "${user}" | jqr .name)
201+
local email=$(echo "${user}" | jqr .email)
202+
203+
git config --global user.name "${username}"
204+
git config --global user.email "${email}"
205+
git config --global gpg.program "/gpg-wrapper"
206+
git config --global credential.helper "store --file=${HOME}/.git-credentials"
207+
git config --global "credential.https://github.com.username" "${login}"
208+
209+
echo "https://${login}:${INPUT_GITHUB_TOKEN}@github.com" \
210+
>> "${HOME}/.git-credentials"
211+
212+
chmod 600 "${HOME}/.git-credentials"
213+
214+
if [ -n "${gpg_key_id}" ]; then
215+
git config --global commit.gpgsign true
216+
git config --global user.signingkey "${gpg_key_id}"
217+
fi
218+
}

0 commit comments

Comments
 (0)