af-cicd 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
af_cicd-0.1.4/PKG-INFO ADDED
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: af-cicd
3
+ Version: 0.1.4
4
+ Summary: shared cicd tooling
5
+ License: MIT
6
+ Author: hello
7
+ Author-email: hello@allfly.io
8
+ Requires-Python: >=3.13,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: boto3 (>=1.35.5,<2.0.0)
14
+ Requires-Dist: inquirer (>=3.3.0,<4.0.0)
15
+ Requires-Dist: slack-sdk (>=3.34.0,<4.0.0)
16
+ Requires-Dist: typer[slim] (>=0.20.0,<0.21.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # tools-cicd
20
+ share cicd scripts
21
+
22
+ # Running tool locally
23
+
24
+ ## Example:
25
+ ```shell
26
+ AWS_PROFILE=dev-admin-terraform poetry run cicd --help
27
+ ```
28
+
29
+ # Release Procedures
30
+ - This project uses "trunk" based development off of `master`.
31
+ - PRs are allowed but always folded back to `master`.
32
+ - For now, releasing is manual, no automation.
33
+ - **To release**:
34
+ - Ensure all your features are ready on `master`
35
+ - *You should not have any staged changes!*
36
+ - In a terminal run `make version v={major|minor|patch}`
37
+ - **Do not include a number! Poetry will auto-rev the value accordingly**
38
+ - That's it
39
+ - This will create a new tag in Git ready for use
40
+
41
+
42
+ # Multi-Repository Release Coordination
43
+
44
+ The `quest-v2` project includes a coordinated release script that allows you to trigger deployments across all related Quest v2 repositories simultaneously, rather than having to manually trigger each repository's release workflow individually.
45
+
46
+ ### Prerequisites
47
+
48
+ 1. **GitHub CLI**: Install the GitHub CLI tool
49
+ ```bash
50
+ # macOS
51
+ brew install gh
52
+
53
+ # Ubuntu/Debian
54
+ sudo apt install gh
55
+
56
+ # Windows
57
+ winget install GitHub.cli
58
+ ```
59
+
60
+ 2. **Authentication**: Authenticate with GitHub
61
+ ```bash
62
+ gh auth login
63
+ ```
64
+
65
+ 3. **Script Setup**: Navigate to the quest-v2 directory and make the script executable
66
+ ```bash
67
+ cd quest-v2
68
+ chmod +x release-all.sh
69
+ ```
70
+
71
+ ### Usage
72
+
73
+ From the `quest-v2` directory, the script supports promoting from `develop` to `qa`:
74
+
75
+ ```bash
76
+ # Promote to QA
77
+ ./release-all.sh
78
+
79
+ # Show help and options
80
+ ./release-all.sh --help
81
+ ```
82
+
83
+ ### What it does
84
+
85
+ The script will:
86
+ 1. Trigger the "Release" workflow on all three Quest v2 repositories:
87
+ - `allfly-quest-ui`
88
+ - `allfly-quest-core-api`
89
+ - `gds-api`
90
+ 2. Promote code from `develop` branch to `qa`
91
+ 3. Provide real-time status updates and links to monitor progress
92
+ 4. Display a summary of successful and failed releases
93
+
94
+ ### Monitoring
95
+
96
+ After triggering the releases, you can monitor progress through:
97
+ - The provided GitHub Actions URLs in the script output
98
+ - GitHub's Actions tab in each repository
99
+ - Your configured Slack notifications (if enabled)
100
+
101
+ > **Note**: The script requires appropriate permissions to trigger workflows on all three Quest v2 repositories. Ensure your GitHub account has the necessary access rights.
@@ -0,0 +1,83 @@
1
+ # tools-cicd
2
+ share cicd scripts
3
+
4
+ # Running tool locally
5
+
6
+ ## Example:
7
+ ```shell
8
+ AWS_PROFILE=dev-admin-terraform poetry run cicd --help
9
+ ```
10
+
11
+ # Release Procedures
12
+ - This project uses "trunk" based development off of `master`.
13
+ - PRs are allowed but always folded back to `master`.
14
+ - For now, releasing is manual, no automation.
15
+ - **To release**:
16
+ - Ensure all your features are ready on `master`
17
+ - *You should not have any staged changes!*
18
+ - In a terminal run `make version v={major|minor|patch}`
19
+ - **Do not include a number! Poetry will auto-rev the value accordingly**
20
+ - That's it
21
+ - This will create a new tag in Git ready for use
22
+
23
+
24
+ # Multi-Repository Release Coordination
25
+
26
+ The `quest-v2` project includes a coordinated release script that allows you to trigger deployments across all related Quest v2 repositories simultaneously, rather than having to manually trigger each repository's release workflow individually.
27
+
28
+ ### Prerequisites
29
+
30
+ 1. **GitHub CLI**: Install the GitHub CLI tool
31
+ ```bash
32
+ # macOS
33
+ brew install gh
34
+
35
+ # Ubuntu/Debian
36
+ sudo apt install gh
37
+
38
+ # Windows
39
+ winget install GitHub.cli
40
+ ```
41
+
42
+ 2. **Authentication**: Authenticate with GitHub
43
+ ```bash
44
+ gh auth login
45
+ ```
46
+
47
+ 3. **Script Setup**: Navigate to the quest-v2 directory and make the script executable
48
+ ```bash
49
+ cd quest-v2
50
+ chmod +x release-all.sh
51
+ ```
52
+
53
+ ### Usage
54
+
55
+ From the `quest-v2` directory, the script supports promoting from `develop` to `qa`:
56
+
57
+ ```bash
58
+ # Promote to QA
59
+ ./release-all.sh
60
+
61
+ # Show help and options
62
+ ./release-all.sh --help
63
+ ```
64
+
65
+ ### What it does
66
+
67
+ The script will:
68
+ 1. Trigger the "Release" workflow on all three Quest v2 repositories:
69
+ - `allfly-quest-ui`
70
+ - `allfly-quest-core-api`
71
+ - `gds-api`
72
+ 2. Promote code from `develop` branch to `qa`
73
+ 3. Provide real-time status updates and links to monitor progress
74
+ 4. Display a summary of successful and failed releases
75
+
76
+ ### Monitoring
77
+
78
+ After triggering the releases, you can monitor progress through:
79
+ - The provided GitHub Actions URLs in the script output
80
+ - GitHub's Actions tab in each repository
81
+ - Your configured Slack notifications (if enabled)
82
+
83
+ > **Note**: The script requires appropriate permissions to trigger workflows on all three Quest v2 repositories. Ensure your GitHub account has the necessary access rights.
File without changes
File without changes
@@ -0,0 +1,36 @@
1
+ import os
2
+
3
+ import boto3
4
+
5
+ from allfly.utils import get_exit_code, run_shell_command, stdout
6
+
7
+
8
+ class AWSCore:
9
+
10
+ @staticmethod
11
+ def get_current_user_region():
12
+ current_session = boto3.session.Session()
13
+ current_region = (
14
+ current_session.region_name if current_session.region_name else "us-east-1"
15
+ )
16
+ stdout(f"Current region: {current_region}")
17
+ return current_region
18
+
19
+ @staticmethod
20
+ def is_aws_profile_is_active(profile: str):
21
+ return get_exit_code(f'aws s3 ls --profile {profile}') == 0
22
+
23
+ @staticmethod
24
+ def setup_aws_creds(cicd: bool = False):
25
+ """
26
+ In CI/CD environments, there are no AWS profiles, so we don't need to set the AWS_PROFILE environment variable
27
+ """
28
+ aws_profile = os.environ.get('AWS_PROFILE', None)
29
+ stdout(f"AWS_PROFILE is set to: {aws_profile}")
30
+ if aws_profile and not cicd:
31
+ # If the AWS profile is not active, re-authenticate with AWS
32
+ if not AWSCore.is_aws_profile_is_active(aws_profile):
33
+ stdout(
34
+ f'AWS profile {aws_profile} is not active. Re-authenticating with AWS...'
35
+ )
36
+ run_shell_command(f'aws sso login --profile {aws_profile}')
@@ -0,0 +1,110 @@
1
+ import boto3
2
+ from botocore.client import BaseClient
3
+
4
+ from allfly.utils import stdout, stderr
5
+
6
+
7
+ class AwsEcr:
8
+ ecr: BaseClient
9
+
10
+ def __init__(self):
11
+ self.ecr = boto3.client("ecr")
12
+
13
+ @staticmethod
14
+ def build_ecr_image_url(app_name: str, image_tag: str, ecr_account_id: str) -> str:
15
+ return f"{ecr_account_id}.dkr.ecr.us-east-1.amazonaws.com/{app_name}:{image_tag}"
16
+
17
+ def promote_ecr_docker_tag(
18
+ self, app_name: str, ecr_account_id: str, source_branch_tag: str, target_branch_tag: str
19
+ ) -> str:
20
+ """
21
+ Re-tag an ECR image without having to pull and push.
22
+ Based on https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-retag.html
23
+
24
+ return the full image URL of the container to be used in the next environment
25
+ """
26
+ stdout("Re-tagging image...")
27
+
28
+ # First, we need to get all associated images for the source (from) environment
29
+ associated_version_tag = self.get_next_image_tag(
30
+ ecr_account_id=ecr_account_id,
31
+ app_name=app_name,
32
+ source_branch_tag=source_branch_tag
33
+ )
34
+
35
+ # Now, we need to get the imageManifest; unfortunately, another call
36
+ get_image_response = self.ecr.batch_get_image(
37
+ registryId=ecr_account_id,
38
+ repositoryName=app_name,
39
+ imageIds=[{"imageTag": associated_version_tag}],
40
+ )
41
+ images = get_image_response.get("images")
42
+ if not images:
43
+ raise Exception(
44
+ f"Failed to get image, could not find image with tag {source_branch_tag}"
45
+ )
46
+
47
+ image = images[0]
48
+ image_manifest = image.get("imageManifest")
49
+
50
+ # Now, we re-tag for AWS (to) environment
51
+ stdout(f"Re-tagging {associated_version_tag} now to {target_branch_tag}")
52
+ try:
53
+ self.ecr.put_image(
54
+ registryId=ecr_account_id,
55
+ repositoryName=app_name,
56
+ imageManifest=image_manifest,
57
+ imageTag=target_branch_tag,
58
+ )
59
+ except self.ecr.exceptions.ImageAlreadyExistsException as e:
60
+ stderr(f"Image tag already {target_branch_tag} exists with that manifest, that's okay.")
61
+
62
+ return self.build_ecr_image_url(
63
+ app_name=app_name,
64
+ image_tag=associated_version_tag,
65
+ ecr_account_id=ecr_account_id,
66
+ )
67
+
68
+ def get_next_image_tag(self,
69
+ ecr_account_id: str,
70
+ app_name: str,
71
+ source_branch_tag: str):
72
+ """
73
+ the purpose of this function is to get the image tag that is associated with the previous environment
74
+
75
+ for example: from develop to qa, one might see
76
+ found associated tag develop
77
+ found associated tag v.2025.01.13.212928.18
78
+
79
+ v.2025.01.13.212928.18 would be returned to promote to qa
80
+ """
81
+ describe_image_response = self.ecr.describe_images(
82
+ registryId=ecr_account_id,
83
+ repositoryName=app_name,
84
+ imageIds=[{"imageTag": source_branch_tag}],
85
+ )
86
+
87
+ # To do things right, we need to get the v. version associated with the previous environment so it's passed
88
+ # on to the next environment
89
+ stdout(f"looking for versioned image associated to tag {source_branch_tag}")
90
+ image_details = describe_image_response.get("imageDetails")
91
+ if not image_details:
92
+ raise Exception(
93
+ f"Failed to describe image for {source_branch_tag}; no images returned"
94
+ )
95
+
96
+ aws_image = image_details[0]
97
+
98
+ associated_version_tag = None
99
+ image_tags = aws_image.get("imageTags")
100
+ for tag in image_tags:
101
+ stdout(f"found associated tag {tag}")
102
+ if tag.startswith("v."):
103
+ associated_version_tag = tag
104
+
105
+ if not associated_version_tag:
106
+ raise Exception(
107
+ f"Could not find associated version tag for {source_branch_tag}! Something is wrong."
108
+ )
109
+
110
+ return associated_version_tag
@@ -0,0 +1,80 @@
1
+ from typing_extensions import deprecated
2
+
3
+ import boto3
4
+ from allfly.aws.ssm import AwsSSM
5
+ from allfly.utils import run_shell_command, stdout, exit_with_error
6
+
7
+
8
+ class AwsEcs:
9
+ ssm: AwsSSM
10
+
11
+ def __init__(self):
12
+ self.ssm = AwsSSM()
13
+
14
+ @deprecated("here are backup in case TF based deployments don't pan out")
15
+ def deploy_image_tag_to_ecs_app(self, app_name: str, tag: str):
16
+ service_id = self.ssm.get_ssm_param_value(f"/infra/services/{app_name}/ecs/cluster_id")
17
+ cluster_id = self.ssm.get_ssm_param_value(f"/infra/services/{app_name}/ecs/service_id")
18
+ stdout(f"Deploying to ecs {cluster_id} {service_id} {tag}")
19
+ run_shell_command(
20
+ f'ecs deploy {cluster_id} {service_id} --tag {tag}'
21
+ )
22
+
23
+ def run_pre_deploy_task(self, app_name: str, task_family: str, new_image_url: str) -> None:
24
+ """
25
+ Run a one-off ECS Fargate task before the main service deployment.
26
+
27
+ Runs the latest revision of task_family — the targeted terraform apply
28
+ in Phase 1 already registered a new revision with the correct image.
29
+ Clones network configuration from the running service so no extra
30
+ SSM params are needed.
31
+
32
+ Raises a hard exit if the task fails to start or exits non-zero.
33
+ """
34
+ cluster_id = self.ssm.get_ssm_param_value(f"/infra/services/{app_name}/ecs/cluster_id")
35
+ service_name = self.ssm.get_ssm_param_value(f"/infra/services/{app_name}/ecs/service_id")
36
+
37
+ client = boto3.client("ecs")
38
+
39
+ # Clone network config from the live service — keeps subnets/SGs consistent
40
+ services_response = client.describe_services(cluster=cluster_id, services=[service_name])
41
+ services = services_response.get("services", [])
42
+ if not services:
43
+ exit_with_error(f"Could not find ECS service '{service_name}' in cluster '{cluster_id}'")
44
+ network_config = services[0]["networkConfiguration"]
45
+
46
+ stdout(f"Running pre-deploy task '{task_family}' with image {new_image_url}")
47
+
48
+ run_response = client.run_task(
49
+ cluster=cluster_id,
50
+ taskDefinition=task_family,
51
+ launchType="FARGATE",
52
+ networkConfiguration=network_config,
53
+ )
54
+
55
+ failures = run_response.get("failures", [])
56
+ tasks = run_response.get("tasks", [])
57
+ if failures or not tasks:
58
+ exit_with_error(f"Failed to start pre-deploy task '{task_family}': {failures}")
59
+
60
+ task_arn = tasks[0]["taskArn"]
61
+ stdout(f"Pre-deploy task started: {task_arn} — waiting for completion...")
62
+
63
+ waiter = client.get_waiter("tasks_stopped")
64
+ waiter.wait(
65
+ cluster=cluster_id,
66
+ tasks=[task_arn],
67
+ WaiterConfig={"Delay": 10, "MaxAttempts": 60}, # up to 10 minutes
68
+ )
69
+
70
+ describe_response = client.describe_tasks(cluster=cluster_id, tasks=[task_arn])
71
+ container = describe_response["tasks"][0]["containers"][0]
72
+ exit_code = container.get("exitCode")
73
+
74
+ if exit_code != 0:
75
+ exit_with_error(
76
+ f"Pre-deploy task '{task_family}' failed with exit code {exit_code}. "
77
+ f"Halting deployment."
78
+ )
79
+
80
+ stdout(f"Pre-deploy task '{task_family}' completed successfully.")
@@ -0,0 +1,18 @@
1
+ import boto3
2
+ from botocore.client import BaseClient
3
+
4
+ from allfly.utils import stdout
5
+
6
+
7
+ class AwsLambda:
8
+ client: BaseClient
9
+
10
+ def __init__(self):
11
+ self.client = boto3.client('lambda')
12
+
13
+ def update_lambda_image_with_image_url(self, app_name: str, image_url: str):
14
+ stdout("Deploying Lambda function...")
15
+ self.client.update_function_code(
16
+ FunctionName=f"{app_name}", ImageUri=image_url, Publish=True
17
+ )
18
+ stdout("Lambda function deployed successfully.")
@@ -0,0 +1,19 @@
1
+ import boto3
2
+ from botocore.client import BaseClient
3
+
4
+ from allfly.utils import exit_with_error
5
+
6
+
7
+ class AwsSSM:
8
+ client: BaseClient
9
+
10
+ def __init__(self):
11
+ self.client = boto3.client('ssm')
12
+
13
+ def get_ssm_param_value(self, param_name: str, is_encrypted=False) -> str | None:
14
+
15
+ try:
16
+ response = self.client.get_parameter(Name=param_name, WithDecryption=is_encrypted)
17
+ return response['Parameter']['Value']
18
+ except self.client.exceptions.ParameterNotFound:
19
+ exit_with_error(f"Could not find ssm param value {param_name}")