Skip to content

Commit b296864

Browse files
authored
Merge pull request #16 from octokit-cr/github-app-auth
GitHub App Authentication
2 parents 01842a8 + 6867a9a commit b296864

31 files changed

+316
-162
lines changed
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require "../../src/octokit"
2+
require "./github_app_auth"
3+
4+
class AuthenticationError < Exception
5+
end
6+
7+
module GitHubAuthentication
8+
def self.login(client : Octokit::Client? = nil)
9+
if client
10+
puts "using pre-provided client"
11+
return client
12+
end
13+
14+
if ENV["GITHUB_APP_ID"]? && ENV["GITHUB_APP_INSTALLATION_ID"]? && ENV["GITHUB_APP_PRIVATE_KEY"]?
15+
puts "using github app authentication"
16+
return GitHubApp.new
17+
end
18+
19+
if token = ENV["GITHUB_TOKEN"]?
20+
puts "using github token authentication"
21+
octokit = Octokit.client(access_token: token)
22+
octokit.auto_paginate = true
23+
octokit.per_page = 100
24+
return octokit
25+
end
26+
27+
raise AuthenticationError.new("No valid GitHub authentication method was provided")
28+
end
29+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# This class provides a wrapper around the Octokit client for GitHub App authentication.
2+
# It handles token generation and refreshing, and delegates method calls to the Octokit client.
3+
# Helpful: https://github.com/octokit/handbook?tab=readme-ov-file#github-app-authentication-json-web-token
4+
#
5+
# Usage (examples):
6+
# github = GitHubApp.new
7+
# github.get "/meta"
8+
# github.get "/repos/<org>/<repo>"
9+
# github.user "grantbirki"
10+
11+
# Why? In some cases, you may not want to have a static long lived token like a GitHub PAT when authenticating...
12+
# Most importantly, this class will handle automatic token refreshing for you out-of-the-box. Simply provide the...
13+
# correct environment variables, call `GitHubApp.new`, and then use the returned object as you would an Octokit client.
14+
15+
require "../../src/octokit"
16+
require "jwt"
17+
require "openssl"
18+
require "json"
19+
20+
class GitHubApp
21+
TOKEN_EXPIRATION_TIME = 2700 # 45 minutes
22+
JWT_EXPIRATION_TIME = 600 # 10 minutes
23+
24+
@client : Octokit::Client
25+
@app_id : Int32
26+
@installation_id : Int32
27+
@app_key : String
28+
29+
def initialize
30+
@app_id = fetch_env_var("GITHUB_APP_ID").to_i
31+
@installation_id = fetch_env_var("GITHUB_APP_INSTALLATION_ID").to_i
32+
@app_key = fetch_env_var("GITHUB_APP_PRIVATE_KEY").gsub(/\\+n/, "\n")
33+
@token_refresh_time = Time.unix(0)
34+
@client = create_client
35+
end
36+
37+
private def fetch_env_var(key : String) : String
38+
ENV[key]? || raise "environment variable #{key} is not set"
39+
end
40+
41+
private def client
42+
if @client.nil? || token_expired?
43+
@client = create_client
44+
end
45+
@client
46+
end
47+
48+
private def jwt_token : String
49+
private_key = OpenSSL::PKey::RSA.new(@app_key)
50+
payload = {
51+
"iat" => Time.utc.to_unix - 60,
52+
"exp" => Time.utc.to_unix + JWT_EXPIRATION_TIME,
53+
"iss" => @app_id,
54+
}
55+
JWT.encode(payload, private_key.to_pem, JWT::Algorithm::RS256)
56+
end
57+
58+
private def create_client
59+
tmp_client = Octokit.client(bearer_token: jwt_token)
60+
response = tmp_client.create_app_installation_access_token(@installation_id, **{headers: {authorization: "Bearer #{tmp_client.bearer_token}"}})
61+
access_token = JSON.parse(response)["token"].to_s
62+
63+
client = Octokit.client(access_token: access_token)
64+
client.auto_paginate = true
65+
client.per_page = 100
66+
@token_refresh_time = Time.utc
67+
client
68+
end
69+
70+
private def token_expired? : Bool
71+
Time.utc.to_unix - @token_refresh_time.to_unix > TOKEN_EXPIRATION_TIME
72+
end
73+
74+
macro method_missing(call)
75+
{% if call.block %}
76+
client.{{call.name}}({{*call.args}}) do |{{call.block.args}}|
77+
{{call.block.body}}
78+
end
79+
{% else %}
80+
client.{{call.name}}({{*call.args}})
81+
{% end %}
82+
end
83+
84+
def respond_to_missing?(method_name : Symbol, include_private : Bool = false) : Bool
85+
client.respond_to?(method_name) || super
86+
end
87+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require "./auth"
2+
3+
github = GitHubAuthentication.login
4+
5+
github.add_comment("<owner>/<repo>", 123, "some comment here")

script/acceptance

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
#!/bin/bash
22

3-
# COLORS
4-
OFF='\033[0m'
5-
RED='\033[0;31m'
6-
GREEN='\033[0;32m'
7-
BLUE='\033[0;34m'
8-
PURPLE='\033[0;35m'
3+
source script/setup-env $@
94

105
# set the working directory to the root of the project
116
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"

script/all

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
#!/bin/bash
22

3-
# COLORS
4-
OFF='\033[0m'
5-
RED='\033[0;31m'
6-
GREEN='\033[0;32m'
7-
BLUE='\033[0;34m'
8-
PURPLE='\033[0;35m'
3+
source script/setup-env $@
94

105
# runs all formatting scripts and tests at once
116

12-
# set the working directory to the root of the project
13-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
14-
157
set -e
168

179
script/format

script/bootstrap

+5-21
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,7 @@
22

33
set -e
44

5-
# COLORS
6-
OFF='\033[0m'
7-
RED='\033[0;31m'
8-
GREEN='\033[0;32m'
9-
BLUE='\033[0;34m'
10-
PURPLE='\033[0;35m'
11-
YELLOW='\033[0;33m'
12-
13-
# set the working directory to the root of the project
14-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
15-
VENDOR_DIR="$DIR/vendor"
16-
SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
17-
SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
18-
19-
if [[ "$@" == *"--ci"* ]]; then
20-
source script/ci-env
21-
fi
5+
source script/setup-env $@
226

237
crystal_path_var="$(crystal env CRYSTAL_PATH)"
248
# if the crystal_path_var does not contain 'vendor/shards/install' then we need to add it to the CRYSTAL_PATH
@@ -49,9 +33,9 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then
4933
elif [[ "$OSTYPE" == "darwin"* ]]; then
5034
os="mac"
5135

52-
# if CRYSTAL_OPTS is not set to `--link-flags=-Wl,-ld_classic` then print a warning
53-
if [[ -z "$CRYSTAL_OPTS" || "$CRYSTAL_OPTS" != "--link-flags=-Wl,-ld_classic" ]]; then
54-
compatability_warning="⚠️ ${YELLOW}Warning${OFF}: crystal may have issues on macOS due to -> https://github.com/crystal-lang/crystal/issues/13846. If you have issues, please consider exporting the following env vars in your terminal -> https://github.com/GrantBirki/dotfiles/blob/d57db3e4167aa5ae7766c5e544f38ead111f040c/dotfiles/.bashrc#L199"
36+
# if CRYSTAL_OPTS is not set to `--link-flags=-Wl` then print a warning
37+
if [[ -z "$CRYSTAL_OPTS" || "$CRYSTAL_OPTS" != "--link-flags=-Wl" ]]; then
38+
compatability_warning="⚠️ ${YELLOW}Warning${OFF}: please consider exporting the following env vars in your terminal -> https://github.com/GrantBirki/dotfiles/blob/42526c0004cd7562883e5019db8e462e8f307e6a/dotfiles/.bashrc#L201"
5539
fi
5640

5741
elif [[ "$OSTYPE" == "cygwin" ]]; then
@@ -88,7 +72,7 @@ if [[ "$@" == *"--ci"* ]]; then
8872
fi
8973

9074
# install the shards
91-
SHARDS_CACHE_PATH="$SHARDS_CACHE_PATH" SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards install --local --frozen $ci_flags $@
75+
shards install --local --frozen $ci_flags $@
9276

9377
# shards install often wipes out our custom shards sha256 file so we need to recompute it if they are gone
9478
script/compute-dep-shas

script/build

+2-13
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,7 @@
22

33
set -e
44

5-
# COLORS
6-
OFF='\033[0m'
7-
RED='\033[0;31m'
8-
GREEN='\033[0;32m'
9-
BLUE='\033[0;34m'
10-
PURPLE='\033[0;35m'
11-
12-
# set the working directory to the root of the project
13-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
14-
VENDOR_DIR="$DIR/vendor"
15-
SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
16-
SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
5+
source script/setup-env $@
176

187
if [[ "$CI" == "true" ]]; then
198
source script/ci-env
@@ -27,5 +16,5 @@ if [[ "$@" == *"--production"* ]] || [[ "$CRYSTAL_ENV" == "production" ]]; then
2716
fi
2817

2918
echo -e "🔨 ${BLUE}building in ${PURPLE}release${BLUE} mode${OFF}"
30-
SHARDS_CACHE_PATH="$SHARDS_CACHE_PATH" SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards build --production --release --progress --debug --error-trace
19+
shards build --production --release --progress --debug --error-trace
3120
echo -e "📦 ${GREEN}build complete${OFF}"

script/compute-dep-shas

+3-12
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
#!/bin/bash
22

3-
# set the working directory to the root of the project
4-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
5-
VENDOR_DIR="$DIR/vendor"
6-
SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
7-
SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
8-
SHARDS_CACHED="$VENDOR_DIR/shards/cache"
3+
source script/setup-env $@
94

10-
SHARD_SHA_FILE=".shard.vendor.cache.sha256"
11-
12-
file="vendor/shards/install/.shards.info"
13-
14-
if [ -f "$VENDOR_DIR/shards/install/.shards.info" ]; then
5+
if [ -f "$VENDOR_SHARDS_INFO_FILE" ]; then
156

167
# Use yq to parse the file and extract shard names and versions
17-
shards=$(yq eval '.shards | to_entries | .[] | "\(.key)|\(.value.git)|\(.value.version)"' $file)
8+
shards=$(yq eval '.shards | to_entries | .[] | "\(.key)|\(.value.git)|\(.value.version)"' $VENDOR_SHARDS_INFO_FILE)
189

1910
# Loop over each shard
2011
echo "$shards" | while IFS= read -r shard; do

script/format

+1-13
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,7 @@
22

33
set -e
44

5-
# COLORS
6-
OFF='\033[0m'
7-
RED='\033[0;31m'
8-
GREEN='\033[0;32m'
9-
BLUE='\033[0;34m'
10-
PURPLE='\033[0;35m'
11-
12-
# set the working directory to the root of the project
13-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
14-
15-
if [[ "$CI" == "true" ]]; then
16-
source script/ci-env
17-
fi
5+
source script/setup-env $@
186

197
echo -e "🧹 ${BLUE}formatting ${PURPLE}crystal${BLUE} files...${OFF}"
208

script/lint

+1-13
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,7 @@
22

33
set -e
44

5-
# COLORS
6-
OFF='\033[0m'
7-
RED='\033[0;31m'
8-
GREEN='\033[0;32m'
9-
BLUE='\033[0;34m'
10-
PURPLE='\033[0;35m'
11-
12-
# set the working directory to the root of the project
13-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
14-
15-
if [[ "$CI" == "true" ]]; then
16-
source script/ci-env
17-
fi
5+
source script/setup-env $@
186

197
echo -e "🖌️ ${BLUE}linting ${PURPLE}crystal${BLUE} files..."
208

script/postinstall

+2-13
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
#!/bin/bash
22

3-
# set the working directory to the root of the project
4-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
5-
VENDOR_DIR="$DIR/vendor"
6-
SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
7-
8-
LINUX_VENDOR_DIR="$VENDOR_DIR/linux_x86_64/bin"
9-
DARWIN_VENDOR_DIR_X64="$VENDOR_DIR/darwin_x86_64/bin"
10-
DARWIN_VENDOR_DIR_ARM64="$VENDOR_DIR/darwin_arm64/bin"
11-
12-
if [[ "$CI" == "true" ]]; then
13-
source script/ci-env
14-
fi
3+
source script/setup-env $@
154

165
mkdir -p "$DIR/bin"
176

@@ -55,7 +44,7 @@ if [[ ! "$@" == *"--production"* ]]; then
5544
# ensure the ameba binary is built and available in the bin directory
5645
AMEBA_UP_TO_DATE=false
5746
# first, check the version of the ameba binary in the lock file
58-
AMEBA_VERSION=$(SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards list | grep ameba | awk '{print $3}' | tr -d '()')
47+
AMEBA_VERSION=$(shards list | grep ameba | awk '{print $3}' | tr -d '()')
5948

6049
# if the bin/ameba binary exists, check if it is the correct version
6150
if [ -f "$DIR/bin/ameba" ]; then

script/preinstall

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
set -e
44

5-
# set the working directory to the root of the project
6-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
5+
source script/setup-env $@
76

87
mkdir -p "$DIR/bin"

script/server

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
set -e
44

5-
# set the working directory to the root of the project
6-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
5+
source script/setup-env $@
76

8-
SHARDS_CACHE_PATH="$DIR/.cache/shards" shards run --release --debug --error-trace
7+
shards run --release --debug --error-trace -- $@

script/setup-env

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
export OFF='\033[0m'
6+
export RED='\033[0;31m'
7+
export GREEN='\033[0;32m'
8+
export BLUE='\033[0;34m'
9+
export PURPLE='\033[0;35m'
10+
11+
# set the working directory to the root of the project
12+
export DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
13+
14+
export CRYSTAL_VERSION=$(cat "$DIR/.crystal-version")
15+
16+
# set common variables
17+
export VENDOR_DIR="$DIR/vendor"
18+
export SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
19+
export SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
20+
export SHARDS_CACHED="$VENDOR_DIR/shards/cache"
21+
export SHARD_SHA_FILE=".shard.vendor.cache.sha256"
22+
export VENDOR_SHARDS_INFO_FILE="vendor/shards/install/.shards.info"
23+
24+
# common vendor dirs for binaries
25+
export LINUX_VENDOR_DIR="$VENDOR_DIR/linux_x86_64/bin"
26+
export DARWIN_VENDOR_DIR_X64="$VENDOR_DIR/darwin_x86_64/bin"
27+
export DARWIN_VENDOR_DIR_ARM64="$VENDOR_DIR/darwin_arm64/bin"
28+
29+
# if --production was provided, always ensure CRYSTAL_ENV is set to production
30+
if [[ "$@" == *"--production"* ]]; then
31+
export CRYSTAL_ENV="production"
32+
fi
33+
34+
if [[ "$@" == *"--production"* ]] || [[ "$CRYSTAL_ENV" == "production" ]] || [[ "$CI" == "true" ]]; then
35+
crystal_path_var="$(crystal env CRYSTAL_PATH)"
36+
37+
# if the crystal_path_var does not contain 'vendor/shards/install' then we need to add it to the CRYSTAL_PATH
38+
if [[ "$crystal_path_var" != *"vendor/shards/install"* ]]; then
39+
echo "setting CRYSTAL_PATH to include vendored shards - reason: --production flag passed or CRYSTAL_ENV is set to production or CI is true"
40+
export CRYSTAL_PATH="vendor/shards/install:$(crystal env CRYSTAL_PATH)"
41+
fi
42+
fi
43+
44+
if [[ "$OSTYPE" == "darwin"* && "$CI" == "true" ]]; then
45+
# only set the CRYSTAL_OPTS if it is not set or if it does not contain "-Wl"
46+
if [[ -z "$CRYSTAL_OPTS" || "$CRYSTAL_OPTS" != *"-Wl"* ]]; then
47+
echo "setting custom macos CRYSTAL_OPTS for CI"
48+
export CRYSTAL_OPTS="--link-flags=-Wl"
49+
fi
50+
fi

0 commit comments

Comments
 (0)