diff --git a/Dockerfile b/Dockerfile index 19bba186..edf07776 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,9 @@ RUN apk --no-cache add \ gpg \ gpg-agent \ jq \ - openssh-client + openssh-client \ + openssl \ + curl RUN git config --global user.email "git@localhost" RUN git config --global user.name "git" diff --git a/assets/check b/assets/check index 3eb43b4c..9df2bc72 100755 --- a/assets/check +++ b/assets/check @@ -54,6 +54,8 @@ fi configure_git_global "${git_config_payload}" +setup_github_app_credentials + destination=$TMPDIR/git-resource-repo-cache # Optimization when last commit only is checked and skip ci is disabled diff --git a/assets/common.sh b/assets/common.sh index d2fa1d13..b0cc49c8 100644 --- a/assets/common.sh +++ b/assets/common.sh @@ -260,3 +260,39 @@ load_git_crypt_key() { cat $git_crypt_tmp_key_path | tr ' ' '\n' | base64 -d > $GIT_CRYPT_KEY_PATH fi } + +setup_github_app_credentials() { + github_app_id=$(jq -r '.source.github_app_id // ""' <<< "$payload") + github_app_private_key=$(jq -r '.source.github_app_private_key // ""' <<< "$payload") + github_app_installation_id=$(jq -r '.source.github_app_installation_id // ""' <<< "$payload") + + if [ -n "$github_app_id" ] && [ -n "$github_app_private_key" ] && [ -n "$github_app_installation_id" ]; then + # Generate JWT for GitHub App + now=$(date +%s) + iat=$((now - 60)) + exp=$((now + 600)) # 10 minutes expiration + jwt_header=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\r\n') + jwt_payload=$(echo -n "{\"iat\":$iat,\"exp\":$exp,\"iss\":\"$github_app_id\"}" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\r\n') + jwt_signature=$(echo -n "$jwt_header.$jwt_payload" | openssl dgst -sha256 -sign <(echo -n "$github_app_private_key") | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\r\n') + jwt_token="$jwt_header.$jwt_payload.$jwt_signature" + + # Get installation access token + access_token=$(curl -s -X POST -H "Authorization: Bearer $jwt_token" -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/app/installations/$github_app_installation_id/access_tokens" | jq -r '.token') + + if [ -z "$access_token" ]; then + echo "Failed to generate GitHub App access token" + exit 1 + fi + + # Configure Git to use the token for the repository host + git config --global credential.helper "!f() { echo \"username=x-access-token\"; echo \"password=$access_token\"; }; f" + git config --global credential.https://github.com.username "x-access-token" + git config --global credential.https://github.com.password "$access_token" + + # Add this block to switch to HTTPS URI if it's an SSH URI + if [[ $uri == git@github.com:* ]]; then + uri="https://github.com/${uri#git@github.com:}" + fi + fi +} diff --git a/assets/in b/assets/in index 59c29a9b..090741f2 100755 --- a/assets/in +++ b/assets/in @@ -76,6 +76,8 @@ if [ -z "$uri" ]; then exit 1 fi +setup_github_app_credentials + branchflag="" if [ -n "$branch" ]; then branchflag="--branch $branch" diff --git a/assets/out b/assets/out index 5d3b9d67..9bcdf0ca 100755 --- a/assets/out +++ b/assets/out @@ -57,6 +57,8 @@ if [ -z "$uri" ]; then exit 1 fi +setup_github_app_credentials + if [ -z "$branch" ] && [ "$only_tag" != "true" ] && [ -z "$override_branch" ]; then echo "invalid payload (missing branch)" exit 1 diff --git a/assets/source_schema.json b/assets/source_schema.json index a1ae86d2..b0b2cb72 100644 --- a/assets/source_schema.json +++ b/assets/source_schema.json @@ -25,5 +25,8 @@ "https_tunnel": "", "commit_filter": "", "version_depth": "", - "search_remote_refs": "" + "search_remote_refs": "", + "github_app_id": "", + "github_app_installation_id": "", + "github_app_private_key": "" } diff --git a/test/all.sh b/test/all.sh index 43a34736..987e48f6 100755 --- a/test/all.sh +++ b/test/all.sh @@ -8,5 +8,6 @@ $(dirname $0)/common.sh $(dirname $0)/get.sh $(dirname $0)/put.sh $(dirname $0)/lfs.sh +$(dirname $0)/github_app.sh echo -e '\e[32mall tests passed!\e[0m' diff --git a/test/github_app.sh b/test/github_app.sh new file mode 100755 index 00000000..07540e53 --- /dev/null +++ b/test/github_app.sh @@ -0,0 +1,186 @@ +#!/bin/bash + +set -e + +source "$(dirname "$0")/helpers.sh" +if [ -d /opt/resource ]; then + source /opt/resource/common.sh +else + source "$(dirname "$0")/../assets/common.sh" +fi + +# Test case 1: setup_github_app_credentials without GitHub App credentials +it_skips_setup_when_github_app_credentials_not_provided() { + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git" + } + }' + export uri="https://github.com/test/repo.git" + + # This should not error and should not modify git config + setup_github_app_credentials + + # Verify that git config was not modified with credential helper + ! git config --global credential.helper | grep -q "x-access-token" || true +} + +# Test case 2: setup_github_app_credentials with incomplete credentials (missing private key) +it_skips_setup_when_github_app_credentials_incomplete() { + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git", + "github_app_id": "123456" + } + }' + export uri="https://github.com/test/repo.git" + + # This should not error and should not modify git config + setup_github_app_credentials + + # Verify that git config was not modified + ! git config --global credential.helper | grep -q "x-access-token" || true +} + +# Test case 3: setup_github_app_credentials extracts credentials correctly +it_extracts_github_app_credentials() { + local test_key="test-private-key-content" + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git", + "github_app_id": "123456", + "github_app_private_key": "'"$test_key"'", + "github_app_installation_id": "789012" + } + }' + export uri="https://github.com/test/repo.git" + + # Extract the values to verify they are extracted correctly + github_app_id=$(jq -r '.source.github_app_id // ""' <<< "$payload") + github_app_private_key=$(jq -r '.source.github_app_private_key // ""' <<< "$payload") + github_app_installation_id=$(jq -r '.source.github_app_installation_id // ""' <<< "$payload") + + test "$github_app_id" = "123456" + test "$github_app_installation_id" = "789012" + test "$github_app_private_key" = "$test_key" +} + +# Test case 4: setup_github_app_credentials converts SSH URI to HTTPS +it_converts_ssh_uri_to_https() { + export uri="git@github.com:test/repo.git" + + # Simulate the URI conversion logic + if [[ $uri == git@github.com:* ]]; then + uri="https://github.com/${uri#git@github.com:}" + fi + + test "$uri" = "https://github.com/test/repo.git" +} + +# Test case 5: setup_github_app_credentials with empty github_app_id +it_skips_when_github_app_id_is_empty() { + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git", + "github_app_id": "", + "github_app_private_key": "test-key", + "github_app_installation_id": "789012" + } + }' + export uri="https://github.com/test/repo.git" + + # This should not error + setup_github_app_credentials +} + +# Test case 6: setup_github_app_credentials with empty github_app_private_key +it_skips_when_github_app_private_key_is_empty() { + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git", + "github_app_id": "123456", + "github_app_private_key": "", + "github_app_installation_id": "789012" + } + }' + export uri="https://github.com/test/repo.git" + + # This should not error + setup_github_app_credentials +} + +# Test case 7: setup_github_app_credentials with empty github_app_installation_id +it_skips_when_github_app_installation_id_is_empty() { + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git", + "github_app_id": "123456", + "github_app_private_key": "test-key", + "github_app_installation_id": "" + } + }' + export uri="https://github.com/test/repo.git" + + # This should not error + setup_github_app_credentials +} + +# Test case 8: Verify that the function handles JSON parsing correctly +it_handles_missing_json_fields() { + export payload='{ + "source": { + "uri": "https://github.com/test/repo.git" + } + }' + export uri="https://github.com/test/repo.git" + + # Extract values when fields are missing + github_app_id=$(jq -r '.source.github_app_id // ""' <<< "$payload") + github_app_private_key=$(jq -r '.source.github_app_private_key // ""' <<< "$payload") + github_app_installation_id=$(jq -r '.source.github_app_installation_id // ""' <<< "$payload") + + # Should return empty strings when fields are missing + test -z "$github_app_id" + test -z "$github_app_private_key" + test -z "$github_app_installation_id" +} + +# Test case 9: Verify SSH URI patterns are converted correctly +it_converts_various_ssh_uri_formats() { + # Test standard SSH format + uri="git@github.com:user/repo.git" + if [[ $uri == git@github.com:* ]]; then + uri="https://github.com/${uri#git@github.com:}" + fi + test "$uri" = "https://github.com/user/repo.git" + + # Test without .git extension + uri="git@github.com:user/repo" + if [[ $uri == git@github.com:* ]]; then + uri="https://github.com/${uri#git@github.com:}" + fi + test "$uri" = "https://github.com/user/repo" +} + +# Test case 10: Verify HTTPS URIs are not modified +it_does_not_modify_https_uris() { + uri="https://github.com/test/repo.git" + + # Should not modify HTTPS URIs + if [[ $uri == git@github.com:* ]]; then + uri="https://github.com/${uri#git@github.com:}" + fi + + test "$uri" = "https://github.com/test/repo.git" +} + +run it_skips_setup_when_github_app_credentials_not_provided +run it_skips_setup_when_github_app_credentials_incomplete +run it_extracts_github_app_credentials +run it_converts_ssh_uri_to_https +run it_skips_when_github_app_id_is_empty +run it_skips_when_github_app_private_key_is_empty +run it_skips_when_github_app_installation_id_is_empty +run it_handles_missing_json_fields +run it_converts_various_ssh_uri_formats +run it_does_not_modify_https_uris