Skip to content

Commit 567b539

Browse files
authored
fix: Allow github organization repo creation (#374)
* fix: Allow github organization repo creation * fix: validation script fixes
1 parent be6ce2f commit 567b539

10 files changed

Lines changed: 116 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### **Added**
1111

1212
### **Changed**
13+
- updated GitHub repo creation in `sagemaker-templates` module to support GitHub organizations
1314

1415
## v2.1.0
1516

examples/manifests/sagemaker-templates-modules-github.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ parameters:
55
- name: repository-type
66
value: GitHub
77
- name: repository-owner
8-
value: <github_account_owner>
8+
value: <github_account_owner> # Can be a personal account username or organization name
99
- name: access-token-secret-name
1010
value: github_token
1111
- name: aws-codeconnection-arn

modules/sagemaker/sagemaker-templates/README.md

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ The template contains an example CI/CD pipeline to deploy the model endpoints to
4343

4444
The template is based on basic multi-account template from [AWS Enterprise MLOps Framework](https://github.com/aws-samples/aws-enterprise-mlops-framework/blob/main/mlops-multi-account-cdk/mlops-sm-project-template/README.md#sagemaker-project-stack).
4545

46-
#### Third-party Code Repository Integration
47-
SageMaker templates support third party code repository (GitHub) integration along with default AWS CodeCommit. As part of integration, SageMaker templates will be able to manage (create, delete) repositories. As an example, if `sagemaker-templates-service-catalog` template configured to use GitHub as repository type then it would create code repository directly into GitHub account provided with manifest configuration. Repository will be named after SageMaker project name in AWS account `{sagemaker-project}-deploy`. For example, if SageMaker project name is `aiops-abalone-model` then GitHub repository would be created with name `aiops-abalone-model-deploy`.
46+
#### Third-party Code Repository Integration
47+
SageMaker templates support third party code repository (GitHub) integration along with default AWS CodeCommit. As part of integration, SageMaker templates will be able to manage (create, delete) repositories in both **personal GitHub accounts** and **GitHub organizations**. The template automatically detects whether the `repository-owner` parameter refers to an organization or personal account and uses the appropriate GitHub API endpoint.
48+
49+
As an example, if `sagemaker-templates-service-catalog` template configured to use GitHub as repository type then it would create code repository directly into the GitHub account or organization specified in manifest configuration. Repository will be named after SageMaker project name in AWS account `{sagemaker-project}-deploy`. For example, if SageMaker project name is `aiops-abalone-model` then GitHub repository would be created with name `aiops-abalone-model-deploy`.
4850

4951

5052
## Prerequesites:
@@ -53,9 +55,19 @@ SageMaker templates support third party code repository (GitHub) integration alo
5355
> [!IMPORTANT]
5456
> It is important to note AWS CodeCommit is no longer available to new customers. Existing customers of AWS CodeCommit can continue to use the service as normal.
5557
### GitHub repository integration
56-
- Target AWS account should contain AWS Secret Manager secret that contains GitHub personal access token with required permissions to manage repository. Refer guide [Creating a fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) in order to create access token.
58+
- Target AWS account should contain AWS Secret Manager secret that contains GitHub personal access token with required permissions to manage repositories. Refer guide [Creating a fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) in order to create access token.
59+
- **For personal accounts**: Token requires `repo` scope to create private repositories
60+
- **For organizations**: Token requires `repo` scope AND the user must be a member of the organization with permissions to create repositories. For organization SSO, ensure the token is authorized for SSO access.
5761
- Template also requires AWS CodeConnection created for GitHub provider in order to integrated GitHub repositories AWS CodeBuild and AWS CodePipeline. Refer guide [Create a connection to GitHub](https://docs.aws.amazon.com/dtconsole/latest/userguide/connections-create-github.html) in order to create connection with GitHub.
5862

63+
> [!IMPORTANT]
64+
> **For GitHub Organizations**: AWS CodeConnections have a **1:1 mapping** with GitHub organizations. Each organization requires:
65+
> - A separate GitHub App installation in that organization (must be installed by an organization owner)
66+
> - A dedicated CodeConnection ARN for that organization
67+
> - The `repository-owner` parameter must match the organization where the GitHub App is installed
68+
>
69+
> **Common Issue**: If your CodeConnection was created for a personal account (e.g., `username`) but you specify an organization name (e.g., `MyOrg`) in the `repository-owner` parameter, deployments will fail with "Repository not found" errors. Ensure the GitHub App is installed in the correct organization. See [How connections work with organizations](https://docs.aws.amazon.com/dtconsole/latest/userguide/welcome-connections-how-it-works-github-organizations.html) for details.
70+
5971
## Inputs and outputs:
6072

6173
### Required inputs:
@@ -65,7 +77,7 @@ SageMaker templates support third party code repository (GitHub) integration alo
6577

6678
### Common Optional Inputs:
6779
- `repository-type` - type of repository to be integrated with SageMaker template source code. Options: `CodeCommit` (default), `GitHub`, `GitHub Enterprise`
68-
- `repository-owner` - owner or organisation of project code repository (required for GitHub)
80+
- `repository-owner` - GitHub username (for personal accounts) or organization name (for organizations) where repositories will be created (required for GitHub)
6981
- `access-token-secret-name` - AWS Secret Manager secret name where access token is stored (default: `github_token`)
7082
- `aws-codeconnection-arn` - AWS CodeConnection ARN for repository provider
7183
- `sagemaker-domain-id` - SageMaker domain id
@@ -184,4 +196,57 @@ parameters:
184196
```
185197
### Sample manifest example for source repository options
186198
[sagemaker-templates-modules-github.yaml](/examples/manifests/sagemaker-templates-modules-github.yaml)
187-
[sagemaker-templates-modules-codecommit.yaml](/examples/manifests/sagemaker-templates-modules-codecommit.yaml)
199+
[sagemaker-templates-modules-codecommit.yaml](/examples/manifests/sagemaker-templates-modules-codecommit.yaml)
200+
201+
## Troubleshooting GitHub Integration
202+
203+
### Issue: "Repository not found" error during deployment
204+
205+
**Symptoms:**
206+
- Lambda logs show: `Repository created: https://github.com/username/repo-name.git`
207+
- Followed by: `remote: Repository not found` when trying to push
208+
- Repository was created under a personal account instead of the intended organization
209+
210+
**Root Cause:**
211+
This occurs when the `repository-owner` parameter specifies an organization name (e.g., `MyOrganization`), but either:
212+
1. The AWS CodeConnection was created for a personal account instead of the organization
213+
2. The GitHub App is not installed in the organization
214+
215+
**Solution:**
216+
1. Verify the CodeConnection is properly configured:
217+
- Go to AWS Developer Tools Console → Connections
218+
- Check which GitHub account/organization the connection is associated with
219+
- The connection must have the GitHub App installed in the target organization
220+
2. For organizations, ensure:
221+
- An organization owner installed the GitHub App in the organization
222+
- The CodeConnection ARN corresponds to that organization's installation
223+
- The `repository-owner` parameter matches the organization name exactly
224+
3. Clean up failed deployment:
225+
- Delete any repositories created under the wrong account
226+
- Delete or rollback the failed CloudFormation stack
227+
4. Redeploy with the correct CodeConnection ARN
228+
229+
### Issue: Permission denied when creating repositories in organization
230+
231+
**Symptoms:**
232+
- Lambda logs show HTTP 403 or 422 errors
233+
- Error message mentions permissions or organization access
234+
235+
**Solution:**
236+
1. Verify the GitHub Personal Access Token:
237+
- Token must have `repo` scope
238+
- User must be a member of the organization with repository creation permissions
239+
- For organizations with SSO: Token must be authorized for SSO access (check in GitHub Settings → Personal Access Tokens → Configure SSO)
240+
2. Verify organization settings:
241+
- Check organization member permissions in GitHub
242+
- Ensure the user has rights to create repositories
243+
244+
### Issue: CodePipeline or CodeBuild cannot access GitHub repository
245+
246+
**Symptoms:**
247+
- Repository is created successfully
248+
- CodePipeline or CodeBuild fails to clone/access the repository
249+
250+
**Solution:**
251+
1. Ensure the CodeConnection ARN is correct for the organization
252+
2. For organizations: Confirm the GitHub App has the necessary repository permissions in the organization settings

modules/sagemaker/sagemaker-templates/common/code_repo_construct.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,27 @@ def extract_zip_file(zip_file_path, extract_dir):
112112
print(f"Error extracting zip file: {str(e)}")
113113
raise e
114114
115+
def is_organization(github_owner, headers):
116+
# Check if github_owner is a GitHub organization (not a personal user account).
117+
# Returns True if it's an organization, False otherwise.
118+
print(f"Checking if '{github_owner}' is a GitHub organization...")
119+
req = urllib.request.Request(f'https://api.github.com/orgs/{github_owner}', headers=headers)
120+
try:
121+
with urllib.request.urlopen(req) as response:
122+
if response.getcode() == 200:
123+
print(f"'{github_owner}' is a GitHub organization")
124+
return True
125+
else:
126+
print(f"Unexpected status code {response.getcode()} when checking organization")
127+
return False
128+
except urllib.error.HTTPError as e:
129+
if e.code == 404:
130+
print(f"'{github_owner}' is not an organization (404 - not found)")
131+
return False
132+
else:
133+
print(f"Error checking if '{github_owner}' is an organization: {e.code} - {e.reason}")
134+
# For other errors (403, etc.), assume it might be an org and let the create call fail with proper error
135+
return True
115136
116137
def lambda_handler(event, context):
117138
print(f"Received event: {json.dumps(event)}")
@@ -142,19 +163,29 @@ def lambda_handler(event, context):
142163
'private': True
143164
}).encode('utf-8')
144165
145-
req = urllib.request.Request('https://api.github.com/user/repos', data=data, headers=headers, method='POST')
166+
# Determine if github_owner is an organization or personal user account
167+
if is_organization(github_owner, headers):
168+
# Use organization endpoint
169+
api_url = f'https://api.github.com/orgs/{github_owner}/repos'
170+
print(f"Creating repository under organization '{github_owner}' using {api_url}")
171+
else:
172+
# Use user endpoint
173+
api_url = 'https://api.github.com/user/repos'
174+
print(f"Creating repository under user account using {api_url}")
175+
176+
req = urllib.request.Request(api_url, data=data, headers=headers, method='POST')
146177
147178
try:
148179
with urllib.request.urlopen(req) as response:
149180
repo_data = json.loads(response.read().decode())
150181
repo_url = repo_data['clone_url']
182+
print(f"Repository created successfully: {repo_url}")
151183
except urllib.error.HTTPError as e:
152-
print(f"HTTP Error: {e.code} - {e.reason}")
153-
print(e.read().decode())
184+
print(f"HTTP Error creating repository: {e.code} - {e.reason}")
185+
error_body = e.read().decode()
186+
print(f"Error details: {error_body}")
154187
raise
155188
156-
print(f"Repository created: {repo_url}")
157-
158189
# Download entire S3 bucket
159190
s3 = boto3.client('s3')
160191
with tempfile.TemporaryDirectory() as tmp_dir:

modules/sagemaker/sagemaker-templates/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.ruff]
22
line-length = 120
3-
target-version = "py38"
3+
target-version = "py310"
44
extend-include = ["*.ipynb"]
55

66
exclude = [
@@ -24,7 +24,7 @@ select = ["F", "I", "E", "W"]
2424
fixable = ["ALL"]
2525

2626
[tool.mypy]
27-
python_version = "3.8"
27+
python_version = "3.10"
2828
strict = true
2929
ignore_missing_imports = true
3030
disallow_untyped_decorators = false

modules/sagemaker/sagemaker-templates/templates/finetune_llm_evaluation/pipeline_constructs/build_pipeline_construct.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,7 @@ def __init__(
228228
"sagemaker:ListTags",
229229
],
230230
resources=[
231-
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:pipeline/"
232-
f"{sagemaker_pipeline_name}",
231+
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:pipeline/{sagemaker_pipeline_name}",
233232
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:pipeline/"
234233
f"{sagemaker_pipeline_name}/execution/*",
235234
],

modules/sagemaker/sagemaker-templates/templates/finetune_llm_evaluation/seed_code/build_app/source_scripts/evaluate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def evaluate_model(args: Any) -> None:
7777
os.makedirs(args.model_dir, exist_ok=True)
7878
tar_file.extractall(args.model_dir)
7979

80-
logger.info(f"Decompressed Model assets: {os.listdir( args.model_dir )}")
80+
logger.info(f"Decompressed Model assets: {os.listdir(args.model_dir)}")
8181

8282
# Load test dataset
8383
test_dataset = load_from_disk(args.test_data_dir)
@@ -88,7 +88,7 @@ def evaluate_model(args: Any) -> None:
8888
# ensure that we do not have any trailing/leading whitespaces, this can lead to issues during inference
8989
test_data = test_dataset.map(lambda sample: {"prompt": sample["prompt"].strip()})
9090
logger.info("Loading test dataset")
91-
logger.info(f"Test dataset has {len( test_data )} samples")
91+
logger.info(f"Test dataset has {len(test_data)} samples")
9292

9393
model = AutoModelForCausalLM.from_pretrained(args.model_dir, device_map="auto", torch_dtype=torch.float16)
9494

modules/sagemaker/sagemaker-templates/templates/finetune_llm_evaluation/seed_code/build_app/source_scripts/train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def create_peft_model(model: Any, gradient_checkpointing: bool = True, bf16: boo
7171

7272
# get lora target modules
7373
modules = find_all_linear_names(model)
74-
logger.info(f"Found {len( modules )} modules to quantize: {modules}")
74+
logger.info(f"Found {len(modules)} modules to quantize: {modules}")
7575

7676
peft_config = LoraConfig(
7777
r=64,

modules/sagemaker/sagemaker-templates/templates/model_deploy/product_stack.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ def __init__(
111111
dev_region: str = Aws.REGION
112112

113113
model_package_arn = (
114-
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:model-package/"
115-
f"{model_package_group_name}/*"
114+
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:model-package/{model_package_group_name}/*"
116115
)
117116
model_package_group_arn = (
118117
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:model-package-group/"

modules/sagemaker/sagemaker-templates/templates/xgboost_abalone/pipeline_constructs/build_pipeline_construct.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,7 @@ def __init__(
262262
"sagemaker:ListTags",
263263
],
264264
resources=[
265-
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:pipeline/"
266-
f"{sagemaker_pipeline_name}",
265+
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:pipeline/{sagemaker_pipeline_name}",
267266
f"arn:{Aws.PARTITION}:sagemaker:{Aws.REGION}:{Aws.ACCOUNT_ID}:pipeline/"
268267
f"{sagemaker_pipeline_name}/execution/*",
269268
],

0 commit comments

Comments
 (0)