From 82f5d444ee1179143fd5bc133dd27355812a77f0 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 15:05:41 +0100 Subject: [PATCH 01/19] feat: extract package upload helper --- package-and-upload-artifact/action.yml | 67 +-------- .../package_and_upload.sh | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+), 66 deletions(-) create mode 100755 package-and-upload-artifact/package_and_upload.sh diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index ceae7cc2..1f790274 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -62,72 +62,7 @@ runs: SOURCE_TYPE: ${{ inputs.source-type }} SOURCE_LOCATION: ${{ inputs.source-location }} TAG: ${{ inputs.tag }} - run: | - tag="$TAG" - echo "$AWSCREDS" > /tmp/awscreds - export AWS_CONFIG_FILE="/tmp/awscreds" - - if [ "$SOURCE_TYPE" = "folder" ]; then - (cd "$SOURCE_LOCATION" && zip -r ../archive.zip . ;) - SOURCE_LOCATION="archive.zip" - SOURCE_TYPE="file" - fi - - if [ "$SOURCE_TYPE" = "file" ]; then - file_extension="$(echo "$SOURCE_LOCATION" | sed -n 's/^.*\.\(.*\)$/\1/p')" - tag="$tag.$file_extension" - fi - - echo "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]' | while read -r item; do ( - if [ "$SOURCE_TYPE" = "file" ]; then - environment="$(echo "$item" | jq -e -r .key)" - account_id="$(echo "$item" | jq -e -r .value.accountId)" - bucket_name="$(echo "$item" | jq -e -r .value.artifactBucketName)" - - echo "Uploading $SOURCE_LOCATION to S3 with key $tag in $environment" - - export AWS_PROFILE="$environment" - aws s3 cp "$SOURCE_LOCATION" "s3://$bucket_name/$tag" - elif [ "$SOURCE_TYPE" = "docker-image" ]; then - environment="$(echo "$item" | jq -e -r .key)" - account_id="$(echo "$item" | jq -e -r .value.accountId)" - ecr_repository_name="$(echo "$item" | jq -e -r .value.artifactEcrRepositoryName)" - default_region="$(echo "$item" | jq -e -r .value.defaultRegion)" - - export AWS_PROFILE="$environment" - login_password="$(aws ecr get-login-password --region "$default_region")" - echo "::add-mask::$login_password" - - ecr_repository_uri="$account_id.dkr.ecr.$default_region.amazonaws.com" - image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" - - echo "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" - echo "Tagging image with image tag: $image_tag" - docker tag "$SOURCE_LOCATION" "$ecr_repository_uri/$ecr_repository_name:$tag" - echo "Pushing image with tag: $image_tag" - docker push "$image_tag" - else - echo "Unrecognized source type '$SOURCE_TYPE' - skipping" >&2 - fi - ); done - - rm /tmp/awscreds - - # $GITHUB_WORKFLOW_REF looks like this: //.github/workflows/@ - workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" - workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" - - echo "tag=$tag" >> "$GITHUB_OUTPUT" - cat <> "$GITHUB_STEP_SUMMARY" - Built and uploaded artifact with tag: - \`\`\` - $tag - \`\`\` - - --- - - _To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ - EOF + run: package-and-upload-artifact/package_and_upload.sh - name: Store artifact tag in commit status shell: bash --noprofile --norc -euo pipefail {0} env: diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh new file mode 100755 index 00000000..fd4979b2 --- /dev/null +++ b/package-and-upload-artifact/package_and_upload.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${AWSCREDS:?Missing AWSCREDS}" +: "${CONFIG:?Missing CONFIG}" +: "${SOURCE_TYPE:?Missing SOURCE_TYPE}" +: "${SOURCE_LOCATION:?Missing SOURCE_LOCATION}" +: "${TAG:?Missing TAG}" +: "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" +: "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" +: "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" +: "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" + +source_type="$SOURCE_TYPE" +source_location="$SOURCE_LOCATION" +tag="$TAG" + +aws_config_file="$(mktemp)" + +cleanup() { + rm -f "$aws_config_file" +} +trap cleanup EXIT + +write_aws_credentials() { + printf '%s\n' "$AWSCREDS" >"$aws_config_file" + export AWS_CONFIG_FILE="$aws_config_file" +} + +package_folder_source() { + (cd "$source_location" && zip -r ../archive.zip .) + source_location="archive.zip" + source_type="file" +} + +append_extension_to_tag() { + local extension + extension="$(printf '%s\n' "$source_location" | sed -n 's/^.*\.\(.*\)$/\1/p')" + if [[ -n "$extension" ]]; then + tag="$tag.$extension" + fi +} + +prepare_source() { + if [[ "$source_type" == "folder" ]]; then + package_folder_source + fi + + if [[ "$source_type" == "file" ]]; then + append_extension_to_tag + fi +} + +upload_file_to_s3() { + local item="$1" + local environment bucket_name + + environment="$(printf '%s' "$item" | jq -e -r .key)" + bucket_name="$(printf '%s' "$item" | jq -e -r .value.artifactBucketName)" + + printf 'Uploading %s to S3 with key %s in %s\n' "$source_location" "$tag" "$environment" + + export AWS_PROFILE="$environment" + aws s3 cp "$source_location" "s3://$bucket_name/$tag" +} + +push_image_to_ecr() { + local item="$1" + local environment account_id ecr_repository_name default_region + + environment="$(printf '%s' "$item" | jq -e -r .key)" + account_id="$(printf '%s' "$item" | jq -e -r .value.accountId)" + ecr_repository_name="$(printf '%s' "$item" | jq -e -r .value.artifactEcrRepositoryName)" + default_region="$(printf '%s' "$item" | jq -e -r .value.defaultRegion)" + + export AWS_PROFILE="$environment" + login_password="$(aws ecr get-login-password --region "$default_region")" + printf '::add-mask::%s\n' "$login_password" + + ecr_repository_uri="$account_id.dkr.ecr.$default_region.amazonaws.com" + image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" + + printf '%s\n' "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" + printf 'Tagging image with image tag: %s\n' "$image_tag" + docker tag "$source_location" "$image_tag" + printf 'Pushing image with tag: %s\n' "$image_tag" + docker push "$image_tag" +} + +process_environment() { + local item="$1" + + case "$source_type" in + file) + upload_file_to_s3 "$item" + ;; + docker-image) + push_image_to_ecr "$item" + ;; + *) + printf 'Unrecognized source type %s - skipping\n' "$source_type" >&2 + ;; + esac +} + +iterate_environments() { + printf '%s' "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]' +} + +emit_summary() { + local workflow_filename workflow_dispatch_url + workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" + workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" + + printf 'tag=%s\n' "$tag" >>"$GITHUB_OUTPUT" + cat <>"$GITHUB_STEP_SUMMARY" +Built and uploaded artifact with tag: +\`\`\` +$tag +\`\`\` + +--- + +_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ +EOF +} + +main() { + write_aws_credentials + prepare_source + + iterate_environments | while read -r item; do + process_environment "$item" + done + + emit_summary +} + +main "$@" From 685e6e49a643319dac8ea019b5642ae2238c6d60 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 15:11:49 +0100 Subject: [PATCH 02/19] refactor: clarify helper flow --- .../package_and_upload.sh | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index fd4979b2..8bacada1 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -22,7 +22,7 @@ cleanup() { } trap cleanup EXIT -write_aws_credentials() { +configure_aws_credentials() { printf '%s\n' "$AWSCREDS" >"$aws_config_file" export AWS_CONFIG_FILE="$aws_config_file" } @@ -41,7 +41,7 @@ append_extension_to_tag() { fi } -prepare_source() { +package_artifact() { if [[ "$source_type" == "folder" ]]; then package_folder_source fi @@ -107,7 +107,13 @@ iterate_environments() { printf '%s' "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]' } -emit_summary() { +upload_artifact_to_environments() { + iterate_environments | while read -r item; do + process_environment "$item" + done +} + +summarize_upload() { local workflow_filename workflow_dispatch_url workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" @@ -126,14 +132,10 @@ EOF } main() { - write_aws_credentials - prepare_source - - iterate_environments | while read -r item; do - process_environment "$item" - done - - emit_summary + configure_aws_credentials + package_artifact + upload_artifact_to_environments + summarize_upload } main "$@" From e93ae8c1011c400bee89990eefe61d99bd4f3286 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 15:17:01 +0100 Subject: [PATCH 03/19] refactor: adopt bash bible style --- .../package_and_upload.sh | 94 ++++++++++++------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index 8bacada1..a424a6fd 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -euo pipefail +IFS=$'\n\t' : "${AWSCREDS:?Missing AWSCREDS}" : "${CONFIG:?Missing CONFIG}" @@ -16,24 +17,42 @@ source_location="$SOURCE_LOCATION" tag="$TAG" aws_config_file="$(mktemp)" +package_tmp_dir="" cleanup() { rm -f "$aws_config_file" + if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then + rm -rf "$package_tmp_dir" + fi } trap cleanup EXIT -configure_aws_credentials() { +log_info() { + printf '[INFO] %s\n' "$*" +} + +log_error() { + printf '[ERROR] %s\n' "$*" >&2 +} + +die() { + log_error "$1" + exit 1 +} + +configure_aws_profiles() { printf '%s\n' "$AWSCREDS" >"$aws_config_file" export AWS_CONFIG_FILE="$aws_config_file" } -package_folder_source() { - (cd "$source_location" && zip -r ../archive.zip .) - source_location="archive.zip" +package_folder_source_into_archive() { + package_tmp_dir="$(mktemp -d)" + (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) + source_location="$package_tmp_dir/archive.zip" source_type="file" } -append_extension_to_tag() { +append_file_extension_suffix() { local extension extension="$(printf '%s\n' "$source_location" | sed -n 's/^.*\.\(.*\)$/\1/p')" if [[ -n "$extension" ]]; then @@ -41,32 +60,43 @@ append_extension_to_tag() { fi } -package_artifact() { - if [[ "$source_type" == "folder" ]]; then - package_folder_source - fi - - if [[ "$source_type" == "file" ]]; then - append_extension_to_tag - fi +prepare_artifact_payload() { + case "$source_type" in + folder) + log_info "Packaging folder artifact" + package_folder_source_into_archive + append_file_extension_suffix + ;; + file) + log_info "Preparing file artifact" + append_file_extension_suffix + ;; + docker-image) + log_info "Preparing docker image artifact" + ;; + *) + die "Unsupported source type: $source_type" + ;; + esac } -upload_file_to_s3() { +upload_file_artifact() { local item="$1" local environment bucket_name environment="$(printf '%s' "$item" | jq -e -r .key)" bucket_name="$(printf '%s' "$item" | jq -e -r .value.artifactBucketName)" - printf 'Uploading %s to S3 with key %s in %s\n' "$source_location" "$tag" "$environment" + log_info "Uploading $source_location to S3 as $tag in $environment" export AWS_PROFILE="$environment" aws s3 cp "$source_location" "s3://$bucket_name/$tag" } -push_image_to_ecr() { +upload_image_artifact() { local item="$1" local environment account_id ecr_repository_name default_region + local login_password ecr_repository_uri image_tag environment="$(printf '%s' "$item" | jq -e -r .key)" account_id="$(printf '%s' "$item" | jq -e -r .value.accountId)" @@ -81,39 +111,39 @@ push_image_to_ecr() { image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" printf '%s\n' "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" - printf 'Tagging image with image tag: %s\n' "$image_tag" + log_info "Tagging image with image tag: $image_tag" docker tag "$source_location" "$image_tag" - printf 'Pushing image with tag: %s\n' "$image_tag" + log_info "Pushing image with tag: $image_tag" docker push "$image_tag" } -process_environment() { +publish_artifact_to_environment() { local item="$1" case "$source_type" in file) - upload_file_to_s3 "$item" + upload_file_artifact "$item" ;; docker-image) - push_image_to_ecr "$item" + upload_image_artifact "$item" ;; *) - printf 'Unrecognized source type %s - skipping\n' "$source_type" >&2 + log_error "Unrecognized source type $source_type - skipping" ;; esac } -iterate_environments() { +deployment_environments() { printf '%s' "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]' } -upload_artifact_to_environments() { - iterate_environments | while read -r item; do - process_environment "$item" - done +publish_artifact_to_environments() { + while read -r item; do + publish_artifact_to_environment "$item" + done < <(deployment_environments) } -summarize_upload() { +summarize_publication() { local workflow_filename workflow_dispatch_url workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" @@ -132,10 +162,10 @@ EOF } main() { - configure_aws_credentials - package_artifact - upload_artifact_to_environments - summarize_upload + configure_aws_profiles + prepare_artifact_payload + publish_artifact_to_environments + summarize_publication } main "$@" From 2c6c1266b29a00dcab56fac7c427a6e80eb2c9aa Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 15:20:26 +0100 Subject: [PATCH 04/19] refactor: inline packaging flow --- .../package_and_upload.sh | 81 ++++++++----------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index a424a6fd..22642642 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -60,26 +60,6 @@ append_file_extension_suffix() { fi } -prepare_artifact_payload() { - case "$source_type" in - folder) - log_info "Packaging folder artifact" - package_folder_source_into_archive - append_file_extension_suffix - ;; - file) - log_info "Preparing file artifact" - append_file_extension_suffix - ;; - docker-image) - log_info "Preparing docker image artifact" - ;; - *) - die "Unsupported source type: $source_type" - ;; - esac -} - upload_file_artifact() { local item="$1" local environment bucket_name @@ -117,32 +97,6 @@ upload_image_artifact() { docker push "$image_tag" } -publish_artifact_to_environment() { - local item="$1" - - case "$source_type" in - file) - upload_file_artifact "$item" - ;; - docker-image) - upload_image_artifact "$item" - ;; - *) - log_error "Unrecognized source type $source_type - skipping" - ;; - esac -} - -deployment_environments() { - printf '%s' "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]' -} - -publish_artifact_to_environments() { - while read -r item; do - publish_artifact_to_environment "$item" - done < <(deployment_environments) -} - summarize_publication() { local workflow_filename workflow_dispatch_url workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" @@ -163,8 +117,39 @@ EOF main() { configure_aws_profiles - prepare_artifact_payload - publish_artifact_to_environments + + case "$source_type" in + folder) + log_info "Packaging folder artifact" + package_folder_source_into_archive + append_file_extension_suffix + ;; + file) + log_info "Preparing file artifact" + append_file_extension_suffix + ;; + docker-image) + log_info "Preparing docker image artifact" + ;; + *) + die "Unsupported source type: $source_type" + ;; + esac + + while read -r item; do + case "$source_type" in + file) + upload_file_artifact "$item" + ;; + docker-image) + upload_image_artifact "$item" + ;; + *) + log_error "Unrecognized source type $source_type - skipping" + ;; + esac + done < <(printf '%s' "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]') + summarize_publication } From fbcdb9a694354eb6e4692a4d53cab81be3267a9e Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 15:27:59 +0100 Subject: [PATCH 05/19] refactor: simplify environment loop --- .../package_and_upload.sh | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index 22642642..9def5df1 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -60,12 +60,21 @@ append_file_extension_suffix() { fi } +environment_defined() { + local environment="$1" + printf '%s' "$CONFIG" | jq -e ".${environment} != null" >/dev/null 2>&1 +} + +environment_value() { + local environment="$1" key="$2" + printf '%s' "$CONFIG" | jq -e -r ".${environment}.${key}" +} + upload_file_artifact() { - local item="$1" - local environment bucket_name + local environment="$1" + local bucket_name - environment="$(printf '%s' "$item" | jq -e -r .key)" - bucket_name="$(printf '%s' "$item" | jq -e -r .value.artifactBucketName)" + bucket_name="$(environment_value "$environment" artifactBucketName)" log_info "Uploading $source_location to S3 as $tag in $environment" @@ -74,14 +83,13 @@ upload_file_artifact() { } upload_image_artifact() { - local item="$1" - local environment account_id ecr_repository_name default_region + local environment="$1" + local account_id ecr_repository_name default_region local login_password ecr_repository_uri image_tag - environment="$(printf '%s' "$item" | jq -e -r .key)" - account_id="$(printf '%s' "$item" | jq -e -r .value.accountId)" - ecr_repository_name="$(printf '%s' "$item" | jq -e -r .value.artifactEcrRepositoryName)" - default_region="$(printf '%s' "$item" | jq -e -r .value.defaultRegion)" + account_id="$(environment_value "$environment" accountId)" + ecr_repository_name="$(environment_value "$environment" artifactEcrRepositoryName)" + default_region="$(environment_value "$environment" defaultRegion)" export AWS_PROFILE="$environment" login_password="$(aws ecr get-login-password --region "$default_region")" @@ -136,19 +144,23 @@ main() { ;; esac - while read -r item; do + for environment in dev prod; do + if ! environment_defined "$environment"; then + continue + fi + case "$source_type" in file) - upload_file_artifact "$item" + upload_file_artifact "$environment" ;; docker-image) - upload_image_artifact "$item" + upload_image_artifact "$environment" ;; *) log_error "Unrecognized source type $source_type - skipping" ;; esac - done < <(printf '%s' "$CONFIG" | jq -c '{dev,prod} | to_entries | .[]') + done summarize_publication } From 0b427e7d68feb9352c4724ab104ea63884fd00f8 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:01:57 +0100 Subject: [PATCH 06/19] chore: rename summary helper --- package-and-upload-artifact/package_and_upload.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index 9def5df1..ecabe5c8 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -105,7 +105,7 @@ upload_image_artifact() { docker push "$image_tag" } -summarize_publication() { +write_github_summary() { local workflow_filename workflow_dispatch_url workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" @@ -144,6 +144,7 @@ main() { ;; esac + # TODO: iterate environments dynamically from CONFIG instead of hardcoding dev/prod. for environment in dev prod; do if ! environment_defined "$environment"; then continue @@ -162,7 +163,7 @@ main() { esac done - summarize_publication + write_github_summary } main "$@" From 1b8806874a513a82dacf00672eac05745a8a426e Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:06:12 +0100 Subject: [PATCH 07/19] docs: explain artifact packaging flow --- package-and-upload-artifact/README.md | 3 ++- package-and-upload-artifact/action.yml | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package-and-upload-artifact/README.md b/package-and-upload-artifact/README.md index 6377c053..492aa010 100644 --- a/package-and-upload-artifact/README.md +++ b/package-and-upload-artifact/README.md @@ -3,7 +3,8 @@ # Package and upload artifact -Packages and uploads an artifact +Packages Docker images or files/folders per environment using data from a JSON config: docker sources are pushed to ECR for later Terraform-driven ECS deploys, while file/folder sources are zipped if needed and uploaded to S3 for a Lambda + CloudFront deployment flow. Each environment entry (e.g. dev/prod) in the config carries its AWS account, bucket, role, and repository metadata so the action can iterate over every defined target. + ## Usage diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index 1f790274..4f4f53a4 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -1,5 +1,11 @@ name: "Package and upload artifact" -description: Packages and uploads an artifact +description: > + Packages Docker images or files/folders per environment using data from a JSON + config: docker sources are pushed to ECR for later Terraform-driven ECS deploys, + while file/folder sources are zipped if needed and uploaded to S3 for a Lambda + + CloudFront deployment flow. Each environment entry (e.g. dev/prod) in the config + carries its AWS account, bucket, role, and repository metadata so the action can + iterate over every defined target. inputs: config: description: "JSON-encoded config (.gp.cicd.json)" From 33d1efe9cca6eb2049b26de971c94eb595048b94 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:08:46 +0100 Subject: [PATCH 08/19] docs: tighten packaging description --- package-and-upload-artifact/README.md | 2 +- package-and-upload-artifact/action.yml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package-and-upload-artifact/README.md b/package-and-upload-artifact/README.md index 492aa010..c9b8c966 100644 --- a/package-and-upload-artifact/README.md +++ b/package-and-upload-artifact/README.md @@ -3,7 +3,7 @@ # Package and upload artifact -Packages Docker images or files/folders per environment using data from a JSON config: docker sources are pushed to ECR for later Terraform-driven ECS deploys, while file/folder sources are zipped if needed and uploaded to S3 for a Lambda + CloudFront deployment flow. Each environment entry (e.g. dev/prod) in the config carries its AWS account, bucket, role, and repository metadata so the action can iterate over every defined target. +Packages Docker images or files for every environment listed in the JSON config, using the AWS account, bucket, role, and repository data defined there. Docker sources are retagged and pushed to ECR so Terraform can deploy them to ECS. Files or folders are zipped if needed, copied to S3, and later published through the Terraform → Lambda → CloudFront flow. ## Usage diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index 4f4f53a4..3862c13a 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -1,11 +1,10 @@ name: "Package and upload artifact" description: > - Packages Docker images or files/folders per environment using data from a JSON - config: docker sources are pushed to ECR for later Terraform-driven ECS deploys, - while file/folder sources are zipped if needed and uploaded to S3 for a Lambda + - CloudFront deployment flow. Each environment entry (e.g. dev/prod) in the config - carries its AWS account, bucket, role, and repository metadata so the action can - iterate over every defined target. + Packages Docker images or files for every environment listed in the JSON config, + using the AWS account, bucket, role, and repository data defined there. Docker + sources are retagged and pushed to ECR so Terraform can deploy them to ECS. Files + or folders are zipped if needed, copied to S3, and later published through the + Terraform → Lambda → CloudFront flow. inputs: config: description: "JSON-encoded config (.gp.cicd.json)" From 023614d76e727d03a461ac8e4f8e24931e671a69 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:17:50 +0100 Subject: [PATCH 09/19] docs: describe helper functions --- .../package_and_upload.sh | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index ecabe5c8..41567be1 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -19,6 +19,7 @@ tag="$TAG" aws_config_file="$(mktemp)" package_tmp_dir="" +# Remove temporary AWS config and archive before exiting. cleanup() { rm -f "$aws_config_file" if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then @@ -27,24 +28,29 @@ cleanup() { } trap cleanup EXIT +# Print informational message with a consistent prefix. log_info() { printf '[INFO] %s\n' "$*" } +# Print errors to stderr with a consistent prefix. log_error() { printf '[ERROR] %s\n' "$*" >&2 } +# Exit with an error message. die() { log_error "$1" exit 1 } +# Write inline AWS credentials to a temp config file used by AWS CLI. configure_aws_profiles() { printf '%s\n' "$AWSCREDS" >"$aws_config_file" export AWS_CONFIG_FILE="$aws_config_file" } +# Zip a folder source into a temporary archive so it can be uploaded as a file. package_folder_source_into_archive() { package_tmp_dir="$(mktemp -d)" (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) @@ -52,6 +58,7 @@ package_folder_source_into_archive() { source_type="file" } +# Append file extension to the tag so consumers can infer artifact type. append_file_extension_suffix() { local extension extension="$(printf '%s\n' "$source_location" | sed -n 's/^.*\.\(.*\)$/\1/p')" @@ -60,16 +67,19 @@ append_file_extension_suffix() { fi } +# Return success when the config contains the target environment key. environment_defined() { local environment="$1" printf '%s' "$CONFIG" | jq -e ".${environment} != null" >/dev/null 2>&1 } +# Extract a single value from the config for the given environment. environment_value() { local environment="$1" key="$2" printf '%s' "$CONFIG" | jq -e -r ".${environment}.${key}" } +# Upload a prepared file artifact to the environment-specific S3 bucket. upload_file_artifact() { local environment="$1" local bucket_name @@ -82,6 +92,7 @@ upload_file_artifact() { aws s3 cp "$source_location" "s3://$bucket_name/$tag" } +# Push a Docker image artifact to the environment-specific ECR repository. upload_image_artifact() { local environment="$1" local account_id ecr_repository_name default_region @@ -105,6 +116,7 @@ upload_image_artifact() { docker push "$image_tag" } +# Write the resulting tag to the GitHub Action summary and outputs. write_github_summary() { local workflow_filename workflow_dispatch_url workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" @@ -126,30 +138,33 @@ EOF main() { configure_aws_profiles + # Package once before iterating environments so every target reuses the same artifact. case "$source_type" in folder) - log_info "Packaging folder artifact" + log_info "Packaging folder artifact for upload to S3" package_folder_source_into_archive - append_file_extension_suffix ;; - file) - log_info "Preparing file artifact" - append_file_extension_suffix - ;; - docker-image) - log_info "Preparing docker image artifact" + file | docker-image) ;; *) die "Unsupported source type: $source_type" ;; esac + if [[ "$source_type" == "file" ]]; then + log_info "Preparing file artifact for upload to S3" + append_file_extension_suffix + elif [[ "$source_type" == "docker-image" ]]; then + log_info "Preparing Docker image artifact for push to ECR" + fi + # TODO: iterate environments dynamically from CONFIG instead of hardcoding dev/prod. for environment in dev prod; do if ! environment_defined "$environment"; then continue fi + # Upload separately per environment, reusing the prepared artifact above. case "$source_type" in file) upload_file_artifact "$environment" @@ -157,9 +172,6 @@ main() { docker-image) upload_image_artifact "$environment" ;; - *) - log_error "Unrecognized source type $source_type - skipping" - ;; esac done From 19eb1d2d6f7e44357b692b6dbcf55fedcad2ebda Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:25:18 +0100 Subject: [PATCH 10/19] chore: ok --- package-and-upload-artifact/package_and_upload.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index 41567be1..27b4f098 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -50,8 +50,8 @@ configure_aws_profiles() { export AWS_CONFIG_FILE="$aws_config_file" } -# Zip a folder source into a temporary archive so it can be uploaded as a file. -package_folder_source_into_archive() { +# Zip a folder source into a temporary archive so it can be uploaded as a single file. +archive_folder_source() { package_tmp_dir="$(mktemp -d)" (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) source_location="$package_tmp_dir/archive.zip" @@ -138,13 +138,13 @@ EOF main() { configure_aws_profiles - # Package once before iterating environments so every target reuses the same artifact. case "$source_type" in folder) - log_info "Packaging folder artifact for upload to S3" - package_folder_source_into_archive + log_info "Compressing folder artifact into archive for upload" + archive_folder_source ;; file | docker-image) + # Explicitly list the supported types so we don't fall through to the error arm below. ;; *) die "Unsupported source type: $source_type" @@ -154,8 +154,6 @@ main() { if [[ "$source_type" == "file" ]]; then log_info "Preparing file artifact for upload to S3" append_file_extension_suffix - elif [[ "$source_type" == "docker-image" ]]; then - log_info "Preparing Docker image artifact for push to ECR" fi # TODO: iterate environments dynamically from CONFIG instead of hardcoding dev/prod. From 779bfb255f46d3844e6764a1ddfaa43218e233f4 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:32:25 +0100 Subject: [PATCH 11/19] chore: no need for sed --- package-and-upload-artifact/package_and_upload.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index 27b4f098..e12c0caf 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -59,9 +59,12 @@ archive_folder_source() { } # Append file extension to the tag so consumers can infer artifact type. +# Use shell parameter expansion to derive the extension efficiently. append_file_extension_suffix() { - local extension - extension="$(printf '%s\n' "$source_location" | sed -n 's/^.*\.\(.*\)$/\1/p')" + local extension="" + if [[ "$source_location" == *.* ]]; then + extension="${source_location##*.}" + fi if [[ -n "$extension" ]]; then tag="$tag.$extension" fi From 7eaac2dda2814bbdeafc6f1dd03bb98b78485cd4 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 17:35:23 +0100 Subject: [PATCH 12/19] chore: shfmt --- .../package_and_upload.sh | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh index e12c0caf..c0e0a406 100755 --- a/package-and-upload-artifact/package_and_upload.sh +++ b/package-and-upload-artifact/package_and_upload.sh @@ -142,16 +142,16 @@ main() { configure_aws_profiles case "$source_type" in - folder) - log_info "Compressing folder artifact into archive for upload" - archive_folder_source - ;; - file | docker-image) - # Explicitly list the supported types so we don't fall through to the error arm below. - ;; - *) - die "Unsupported source type: $source_type" - ;; + folder) + log_info "Compressing folder artifact into archive for upload" + archive_folder_source + ;; + file | docker-image) + # Explicitly list the supported types so we don't fall through to the error arm below. + ;; + *) + die "Unsupported source type: $source_type" + ;; esac if [[ "$source_type" == "file" ]]; then @@ -167,12 +167,12 @@ main() { # Upload separately per environment, reusing the prepared artifact above. case "$source_type" in - file) - upload_file_artifact "$environment" - ;; - docker-image) - upload_image_artifact "$environment" - ;; + file) + upload_file_artifact "$environment" + ;; + docker-image) + upload_image_artifact "$environment" + ;; esac done From f78fc3be4561c3c879b129c88776da9fc4dc7111 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 22:33:28 +0100 Subject: [PATCH 13/19] feat: separate scripts - maybe revert here --- package-and-upload-artifact/action.yml | 37 +++- package-and-upload-artifact/lib/common.sh | 60 ++++++ .../package_and_upload.sh | 182 ------------------ .../scripts/push-ecr-image.sh | 60 ++++++ .../scripts/upload-s3-artifact.sh | 83 ++++++++ 5 files changed, 237 insertions(+), 185 deletions(-) create mode 100644 package-and-upload-artifact/lib/common.sh delete mode 100755 package-and-upload-artifact/package_and_upload.sh create mode 100644 package-and-upload-artifact/scripts/push-ecr-image.sh create mode 100644 package-and-upload-artifact/scripts/upload-s3-artifact.sh diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index 3862c13a..ff63c0d4 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -47,9 +47,33 @@ runs: output-env-credentials: false output-credentials: true - - name: Package and upload artifact + - name: Upload artifact to S3 + if: ${{ inputs.source-type == 'file' || inputs.source-type == 'folder' }} shell: bash --noprofile --norc -euo pipefail {0} - id: upload + id: upload-s3-artifact + env: + AWSCREDS: | + [profile dev] + aws_access_key_id=${{ steps.aws-credentials-dev.outputs.aws-access-key-id }} + aws_secret_access_key=${{ steps.aws-credentials-dev.outputs.aws-secret-access-key }} + aws_session_token=${{ steps.aws-credentials-dev.outputs.aws-session-token }} + + [profile prod] + aws_access_key_id=${{ steps.aws-credentials-prod.outputs.aws-access-key-id }} + aws_secret_access_key=${{ steps.aws-credentials-prod.outputs.aws-secret-access-key }} + aws_session_token=${{ steps.aws-credentials-prod.outputs.aws-session-token }} + TZ: "Europe/Oslo" + CONFIG: ${{ inputs.config }} + PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" + SOURCE_TYPE: ${{ inputs.source-type }} + SOURCE_LOCATION: ${{ inputs.source-location }} + TAG: ${{ inputs.tag }} + run: package-and-upload-artifact/scripts/upload-s3-artifact.sh + + - name: Push Docker image to ECR + if: ${{ inputs.source-type == 'docker-image' }} + shell: bash --noprofile --norc -euo pipefail {0} + id: push-ecr-image env: AWSCREDS: | [profile dev] @@ -67,7 +91,14 @@ runs: SOURCE_TYPE: ${{ inputs.source-type }} SOURCE_LOCATION: ${{ inputs.source-location }} TAG: ${{ inputs.tag }} - run: package-and-upload-artifact/package_and_upload.sh + run: package-and-upload-artifact/scripts/push-ecr-image.sh + + - name: Record artifact tag + shell: bash --noprofile --norc -euo pipefail {0} + id: upload + run: | + : "${ARTIFACT_TAG:?Missing ARTIFACT_TAG}" + printf 'tag=%s\n' "$ARTIFACT_TAG" >>"$GITHUB_OUTPUT" - name: Store artifact tag in commit status shell: bash --noprofile --norc -euo pipefail {0} env: diff --git a/package-and-upload-artifact/lib/common.sh b/package-and-upload-artifact/lib/common.sh new file mode 100644 index 00000000..a920eae9 --- /dev/null +++ b/package-and-upload-artifact/lib/common.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Common helpers shared between artifact scripts. + +log_info() { + printf '[INFO] %s\n' "$*" +} + +log_error() { + printf '[ERROR] %s\n' "$*" >&2 +} + +die() { + log_error "$1" + exit 1 +} + +configure_aws() { + : "${AWSCREDS:?Missing AWSCREDS}" + aws_config_file="$(mktemp)" + printf '%s\n' "$AWSCREDS" >"$aws_config_file" + export AWS_CONFIG_FILE="$aws_config_file" +} + +environment_defined() { + : "${CONFIG:?Missing CONFIG}" + local environment="$1" + printf '%s' "$CONFIG" | jq -e ".${environment} != null" >/dev/null 2>&1 +} + +environment_value() { + : "${CONFIG:?Missing CONFIG}" + local environment="$1" key="$2" + printf '%s' "$CONFIG" | jq -e -r ".${environment}.${key}" +} + +write_github_summary() { + local final_tag="$1" + : "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" + : "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" + : "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" + : "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" + : "${GITHUB_ENV:?Missing GITHUB_ENV}" + + local workflow_filename workflow_dispatch_url + workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" + workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" + + printf 'tag=%s\n' "$final_tag" >>"$GITHUB_OUTPUT" + printf 'ARTIFACT_TAG=%s\n' "$final_tag" >>"$GITHUB_ENV" + cat <>"$GITHUB_STEP_SUMMARY" +Built and uploaded artifact with tag: +\`\`\` +$final_tag +\`\`\` + +--- + +_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ +EOF +} diff --git a/package-and-upload-artifact/package_and_upload.sh b/package-and-upload-artifact/package_and_upload.sh deleted file mode 100755 index c0e0a406..00000000 --- a/package-and-upload-artifact/package_and_upload.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' - -: "${AWSCREDS:?Missing AWSCREDS}" -: "${CONFIG:?Missing CONFIG}" -: "${SOURCE_TYPE:?Missing SOURCE_TYPE}" -: "${SOURCE_LOCATION:?Missing SOURCE_LOCATION}" -: "${TAG:?Missing TAG}" -: "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" -: "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" -: "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" -: "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" - -source_type="$SOURCE_TYPE" -source_location="$SOURCE_LOCATION" -tag="$TAG" - -aws_config_file="$(mktemp)" -package_tmp_dir="" - -# Remove temporary AWS config and archive before exiting. -cleanup() { - rm -f "$aws_config_file" - if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then - rm -rf "$package_tmp_dir" - fi -} -trap cleanup EXIT - -# Print informational message with a consistent prefix. -log_info() { - printf '[INFO] %s\n' "$*" -} - -# Print errors to stderr with a consistent prefix. -log_error() { - printf '[ERROR] %s\n' "$*" >&2 -} - -# Exit with an error message. -die() { - log_error "$1" - exit 1 -} - -# Write inline AWS credentials to a temp config file used by AWS CLI. -configure_aws_profiles() { - printf '%s\n' "$AWSCREDS" >"$aws_config_file" - export AWS_CONFIG_FILE="$aws_config_file" -} - -# Zip a folder source into a temporary archive so it can be uploaded as a single file. -archive_folder_source() { - package_tmp_dir="$(mktemp -d)" - (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) - source_location="$package_tmp_dir/archive.zip" - source_type="file" -} - -# Append file extension to the tag so consumers can infer artifact type. -# Use shell parameter expansion to derive the extension efficiently. -append_file_extension_suffix() { - local extension="" - if [[ "$source_location" == *.* ]]; then - extension="${source_location##*.}" - fi - if [[ -n "$extension" ]]; then - tag="$tag.$extension" - fi -} - -# Return success when the config contains the target environment key. -environment_defined() { - local environment="$1" - printf '%s' "$CONFIG" | jq -e ".${environment} != null" >/dev/null 2>&1 -} - -# Extract a single value from the config for the given environment. -environment_value() { - local environment="$1" key="$2" - printf '%s' "$CONFIG" | jq -e -r ".${environment}.${key}" -} - -# Upload a prepared file artifact to the environment-specific S3 bucket. -upload_file_artifact() { - local environment="$1" - local bucket_name - - bucket_name="$(environment_value "$environment" artifactBucketName)" - - log_info "Uploading $source_location to S3 as $tag in $environment" - - export AWS_PROFILE="$environment" - aws s3 cp "$source_location" "s3://$bucket_name/$tag" -} - -# Push a Docker image artifact to the environment-specific ECR repository. -upload_image_artifact() { - local environment="$1" - local account_id ecr_repository_name default_region - local login_password ecr_repository_uri image_tag - - account_id="$(environment_value "$environment" accountId)" - ecr_repository_name="$(environment_value "$environment" artifactEcrRepositoryName)" - default_region="$(environment_value "$environment" defaultRegion)" - - export AWS_PROFILE="$environment" - login_password="$(aws ecr get-login-password --region "$default_region")" - printf '::add-mask::%s\n' "$login_password" - - ecr_repository_uri="$account_id.dkr.ecr.$default_region.amazonaws.com" - image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" - - printf '%s\n' "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" - log_info "Tagging image with image tag: $image_tag" - docker tag "$source_location" "$image_tag" - log_info "Pushing image with tag: $image_tag" - docker push "$image_tag" -} - -# Write the resulting tag to the GitHub Action summary and outputs. -write_github_summary() { - local workflow_filename workflow_dispatch_url - workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" - workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" - - printf 'tag=%s\n' "$tag" >>"$GITHUB_OUTPUT" - cat <>"$GITHUB_STEP_SUMMARY" -Built and uploaded artifact with tag: -\`\`\` -$tag -\`\`\` - ---- - -_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ -EOF -} - -main() { - configure_aws_profiles - - case "$source_type" in - folder) - log_info "Compressing folder artifact into archive for upload" - archive_folder_source - ;; - file | docker-image) - # Explicitly list the supported types so we don't fall through to the error arm below. - ;; - *) - die "Unsupported source type: $source_type" - ;; - esac - - if [[ "$source_type" == "file" ]]; then - log_info "Preparing file artifact for upload to S3" - append_file_extension_suffix - fi - - # TODO: iterate environments dynamically from CONFIG instead of hardcoding dev/prod. - for environment in dev prod; do - if ! environment_defined "$environment"; then - continue - fi - - # Upload separately per environment, reusing the prepared artifact above. - case "$source_type" in - file) - upload_file_artifact "$environment" - ;; - docker-image) - upload_image_artifact "$environment" - ;; - esac - done - - write_github_summary -} - -main "$@" diff --git a/package-and-upload-artifact/scripts/push-ecr-image.sh b/package-and-upload-artifact/scripts/push-ecr-image.sh new file mode 100644 index 00000000..227d6b0e --- /dev/null +++ b/package-and-upload-artifact/scripts/push-ecr-image.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +: "${CONFIG:?Missing CONFIG}" +: "${SOURCE_TYPE:?Missing SOURCE_TYPE}" +: "${SOURCE_LOCATION:?Missing SOURCE_LOCATION}" +: "${TAG:?Missing TAG}" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +source "$repo_root/lib/common.sh" + +source_type="$SOURCE_TYPE" +source_location="$SOURCE_LOCATION" +tag="$TAG" + +cleanup() { + if [[ -n "${aws_config_file:-}" ]]; then + rm -f "$aws_config_file" + fi +} +trap cleanup EXIT + +configure_aws + +if [[ "$source_type" != "docker-image" ]]; then + die "push-ecr-image.sh only supports docker-image sources (received $source_type)" +fi + +upload_image_artifact() { + local environment="$1" + local account_id ecr_repository_name default_region + local login_password ecr_repository_uri image_tag + + account_id="$(environment_value "$environment" accountId)" + ecr_repository_name="$(environment_value "$environment" artifactEcrRepositoryName)" + default_region="$(environment_value "$environment" defaultRegion)" + + export AWS_PROFILE="$environment" + login_password="$(aws ecr get-login-password --region "$default_region")" + printf '::add-mask::%s\n' "$login_password" + + ecr_repository_uri="$account_id.dkr.ecr.$default_region.amazonaws.com" + image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" + + printf '%s\n' "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" + log_info "Tagging image with image tag: $image_tag" + docker tag "$source_location" "$image_tag" + log_info "Pushing image with tag: $image_tag" + docker push "$image_tag" +} + +for environment in dev prod; do + if environment_defined "$environment"; then + upload_image_artifact "$environment" + fi +done + +write_github_summary "$tag" diff --git a/package-and-upload-artifact/scripts/upload-s3-artifact.sh b/package-and-upload-artifact/scripts/upload-s3-artifact.sh new file mode 100644 index 00000000..c0fb0bf7 --- /dev/null +++ b/package-and-upload-artifact/scripts/upload-s3-artifact.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +: "${CONFIG:?Missing CONFIG}" +: "${SOURCE_TYPE:?Missing SOURCE_TYPE}" +: "${SOURCE_LOCATION:?Missing SOURCE_LOCATION}" +: "${TAG:?Missing TAG}" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +source "$repo_root/lib/common.sh" + +source_type="$SOURCE_TYPE" +source_location="$SOURCE_LOCATION" +tag="$TAG" +package_tmp_dir="" + +cleanup() { + if [[ -n "${aws_config_file:-}" ]]; then + rm -f "$aws_config_file" + fi + if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then + rm -rf "$package_tmp_dir" + fi +} +trap cleanup EXIT + +configure_aws + +archive_folder_source() { + package_tmp_dir="$(mktemp -d)" + (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) + source_location="$package_tmp_dir/archive.zip" + source_type="file" +} + +append_file_extension_suffix() { + local extension="" + if [[ "$source_location" == *.* ]]; then + extension="${source_location##*.}" + fi + if [[ -n "$extension" ]]; then + tag="$tag.$extension" + fi +} + +upload_file_artifact() { + local environment="$1" + local bucket_name + + bucket_name="$(environment_value "$environment" artifactBucketName)" + + log_info "Uploading $source_location to S3 as $tag in $environment" + + export AWS_PROFILE="$environment" + aws s3 cp "$source_location" "s3://$bucket_name/$tag" +} + +case "$source_type" in +folder) + log_info "Compressing folder artifact into archive for upload" + archive_folder_source + ;; +file) ;; +docker-image) + die "upload-s3-artifact.sh only supports file or folder sources" + ;; +*) + die "Unsupported source type: $source_type" + ;; +esac + +log_info "Preparing file artifact for upload to S3" +append_file_extension_suffix + +for environment in dev prod; do + if environment_defined "$environment"; then + upload_file_artifact "$environment" + fi +done + +write_github_summary "$tag" From 7ad26d3a5e86abd3802df69c68cb6888b3017be1 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Tue, 18 Nov 2025 22:35:26 +0100 Subject: [PATCH 14/19] fix: use action path --- package-and-upload-artifact/action.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index ff63c0d4..9084bc7b 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -52,6 +52,7 @@ runs: shell: bash --noprofile --norc -euo pipefail {0} id: upload-s3-artifact env: + ACTION_PATH: ${{ github.action_path }} AWSCREDS: | [profile dev] aws_access_key_id=${{ steps.aws-credentials-dev.outputs.aws-access-key-id }} @@ -68,13 +69,14 @@ runs: SOURCE_TYPE: ${{ inputs.source-type }} SOURCE_LOCATION: ${{ inputs.source-location }} TAG: ${{ inputs.tag }} - run: package-and-upload-artifact/scripts/upload-s3-artifact.sh + run: "$ACTION_PATH/scripts/upload-s3-artifact.sh" - name: Push Docker image to ECR if: ${{ inputs.source-type == 'docker-image' }} shell: bash --noprofile --norc -euo pipefail {0} id: push-ecr-image env: + ACTION_PATH: ${{ github.action_path }} AWSCREDS: | [profile dev] aws_access_key_id=${{ steps.aws-credentials-dev.outputs.aws-access-key-id }} @@ -91,7 +93,7 @@ runs: SOURCE_TYPE: ${{ inputs.source-type }} SOURCE_LOCATION: ${{ inputs.source-location }} TAG: ${{ inputs.tag }} - run: package-and-upload-artifact/scripts/push-ecr-image.sh + run: "$ACTION_PATH/scripts/push-ecr-image.sh" - name: Record artifact tag shell: bash --noprofile --norc -euo pipefail {0} From aef20fd9c8849fbb3c7326830977937484b0c07f Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Wed, 19 Nov 2025 10:23:17 +0100 Subject: [PATCH 15/19] feat: oidc with curl --- package-and-upload-artifact/action.yml | 40 ------------ package-and-upload-artifact/lib/common.sh | 61 +++++++++++++++++-- .../scripts/push-ecr-image.sh | 31 ++++++---- .../scripts/upload-s3-artifact.sh | 27 +++++--- 4 files changed, 92 insertions(+), 67 deletions(-) diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index 9084bc7b..f161a763 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -27,42 +27,12 @@ outputs: runs: using: composite steps: - - name: Configure AWS credentials in dev - if: ${{ fromJSON(inputs.config).dev != null }} - id: aws-credentials-dev - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0 - with: - aws-region: ${{ fromJSON(inputs.config).dev.defaultRegion }} - role-to-assume: ${{ fromJSON(inputs.config).dev.artifactRoleArn }} - output-env-credentials: false - output-credentials: true - - - name: Configure AWS credentials in prod - if: ${{ fromJSON(inputs.config).prod != null }} - id: aws-credentials-prod - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0 - with: - aws-region: ${{ fromJSON(inputs.config).prod.defaultRegion }} - role-to-assume: ${{ fromJSON(inputs.config).prod.artifactRoleArn }} - output-env-credentials: false - output-credentials: true - - name: Upload artifact to S3 if: ${{ inputs.source-type == 'file' || inputs.source-type == 'folder' }} shell: bash --noprofile --norc -euo pipefail {0} id: upload-s3-artifact env: ACTION_PATH: ${{ github.action_path }} - AWSCREDS: | - [profile dev] - aws_access_key_id=${{ steps.aws-credentials-dev.outputs.aws-access-key-id }} - aws_secret_access_key=${{ steps.aws-credentials-dev.outputs.aws-secret-access-key }} - aws_session_token=${{ steps.aws-credentials-dev.outputs.aws-session-token }} - - [profile prod] - aws_access_key_id=${{ steps.aws-credentials-prod.outputs.aws-access-key-id }} - aws_secret_access_key=${{ steps.aws-credentials-prod.outputs.aws-secret-access-key }} - aws_session_token=${{ steps.aws-credentials-prod.outputs.aws-session-token }} TZ: "Europe/Oslo" CONFIG: ${{ inputs.config }} PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" @@ -77,16 +47,6 @@ runs: id: push-ecr-image env: ACTION_PATH: ${{ github.action_path }} - AWSCREDS: | - [profile dev] - aws_access_key_id=${{ steps.aws-credentials-dev.outputs.aws-access-key-id }} - aws_secret_access_key=${{ steps.aws-credentials-dev.outputs.aws-secret-access-key }} - aws_session_token=${{ steps.aws-credentials-dev.outputs.aws-session-token }} - - [profile prod] - aws_access_key_id=${{ steps.aws-credentials-prod.outputs.aws-access-key-id }} - aws_secret_access_key=${{ steps.aws-credentials-prod.outputs.aws-secret-access-key }} - aws_session_token=${{ steps.aws-credentials-prod.outputs.aws-session-token }} TZ: "Europe/Oslo" CONFIG: ${{ inputs.config }} PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" diff --git a/package-and-upload-artifact/lib/common.sh b/package-and-upload-artifact/lib/common.sh index a920eae9..35ac4fc3 100644 --- a/package-and-upload-artifact/lib/common.sh +++ b/package-and-upload-artifact/lib/common.sh @@ -14,11 +14,62 @@ die() { exit 1 } -configure_aws() { - : "${AWSCREDS:?Missing AWSCREDS}" - aws_config_file="$(mktemp)" - printf '%s\n' "$AWSCREDS" >"$aws_config_file" - export AWS_CONFIG_FILE="$aws_config_file" +require_cmd() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" +} + +authenticate_via_oidc() { + local role_arn="$1" + local aws_region="${2:-eu-north-1}" + local session_name + session_name="GitHubAction-$(date +%s)" + + : "${ACTIONS_ID_TOKEN_REQUEST_URL:?Missing ACTIONS_ID_TOKEN_REQUEST_URL}" + : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?Missing ACTIONS_ID_TOKEN_REQUEST_TOKEN}" + + require_cmd curl + require_cmd jq + require_cmd aws + + log_info "Authenticating to $role_arn via OIDC..." + + local oidc_response oidc_token + if ! oidc_response="$(curl -sSLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" \ + -H "User-Agent: actions/oidc-client" \ + -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN")"; then + die "Failed to reach GitHub OIDC endpoint" + fi + + oidc_token="$(printf '%s' "$oidc_response" | jq -r '.value')" + if [[ -z "$oidc_token" || "$oidc_token" == "null" ]]; then + die "Failed to obtain OIDC token from GitHub. Is 'permissions: id-token: write' set?" + fi + + local credentials_json + if ! credentials_json="$(aws sts assume-role-with-web-identity \ + --role-arn "$role_arn" \ + --role-session-name "$session_name" \ + --web-identity-token "$oidc_token" \ + --duration-seconds 900 \ + --region "$aws_region" \ + --output json)"; then + die "Failed to assume role $role_arn with web identity" + fi + + AWS_ACCESS_KEY_ID="$(printf '%s' "$credentials_json" | jq -r '.Credentials.AccessKeyId')" + AWS_SECRET_ACCESS_KEY="$(printf '%s' "$credentials_json" | jq -r '.Credentials.SecretAccessKey')" + AWS_SESSION_TOKEN="$(printf '%s' "$credentials_json" | jq -r '.Credentials.SessionToken')" + + if [[ -z "$AWS_ACCESS_KEY_ID" || "$AWS_ACCESS_KEY_ID" == "null" ]]; then + die "Failed to parse AWS credentials from STS response" + fi + + export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN +} + +clear_credentials() { + unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE AWS_REGION AWS_DEFAULT_REGION } environment_defined() { diff --git a/package-and-upload-artifact/scripts/push-ecr-image.sh b/package-and-upload-artifact/scripts/push-ecr-image.sh index 227d6b0e..32c7a48c 100644 --- a/package-and-upload-artifact/scripts/push-ecr-image.sh +++ b/package-and-upload-artifact/scripts/push-ecr-image.sh @@ -15,15 +15,6 @@ source_type="$SOURCE_TYPE" source_location="$SOURCE_LOCATION" tag="$TAG" -cleanup() { - if [[ -n "${aws_config_file:-}" ]]; then - rm -f "$aws_config_file" - fi -} -trap cleanup EXIT - -configure_aws - if [[ "$source_type" != "docker-image" ]]; then die "push-ecr-image.sh only supports docker-image sources (received $source_type)" fi @@ -37,7 +28,6 @@ upload_image_artifact() { ecr_repository_name="$(environment_value "$environment" artifactEcrRepositoryName)" default_region="$(environment_value "$environment" defaultRegion)" - export AWS_PROFILE="$environment" login_password="$(aws ecr get-login-password --region "$default_region")" printf '::add-mask::%s\n' "$login_password" @@ -51,10 +41,25 @@ upload_image_artifact() { docker push "$image_tag" } -for environment in dev prod; do - if environment_defined "$environment"; then - upload_image_artifact "$environment" +environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" +if [[ -z "$environments" ]]; then + die "No environments with artifactRoleArn defined in config" +fi + +for environment in $environments; do + if ! environment_defined "$environment"; then + continue fi + + role_arn="$(environment_value "$environment" artifactRoleArn)" + default_region="$(environment_value "$environment" defaultRegion)" + + authenticate_via_oidc "$role_arn" "$default_region" + export AWS_REGION="$default_region" + export AWS_DEFAULT_REGION="$default_region" + + upload_image_artifact "$environment" + clear_credentials done write_github_summary "$tag" diff --git a/package-and-upload-artifact/scripts/upload-s3-artifact.sh b/package-and-upload-artifact/scripts/upload-s3-artifact.sh index c0fb0bf7..b20cbf45 100644 --- a/package-and-upload-artifact/scripts/upload-s3-artifact.sh +++ b/package-and-upload-artifact/scripts/upload-s3-artifact.sh @@ -17,17 +17,12 @@ tag="$TAG" package_tmp_dir="" cleanup() { - if [[ -n "${aws_config_file:-}" ]]; then - rm -f "$aws_config_file" - fi if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then rm -rf "$package_tmp_dir" fi } trap cleanup EXIT -configure_aws - archive_folder_source() { package_tmp_dir="$(mktemp -d)" (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) @@ -53,7 +48,6 @@ upload_file_artifact() { log_info "Uploading $source_location to S3 as $tag in $environment" - export AWS_PROFILE="$environment" aws s3 cp "$source_location" "s3://$bucket_name/$tag" } @@ -74,10 +68,25 @@ esac log_info "Preparing file artifact for upload to S3" append_file_extension_suffix -for environment in dev prod; do - if environment_defined "$environment"; then - upload_file_artifact "$environment" +environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" +if [[ -z "$environments" ]]; then + die "No environments with artifactRoleArn defined in config" +fi + +for environment in $environments; do + if ! environment_defined "$environment"; then + continue fi + + role_arn="$(environment_value "$environment" artifactRoleArn)" + default_region="$(environment_value "$environment" defaultRegion)" + + authenticate_via_oidc "$role_arn" "$default_region" + export AWS_REGION="$default_region" + export AWS_DEFAULT_REGION="$default_region" + + upload_file_artifact "$environment" + clear_credentials done write_github_summary "$tag" From bf5a66730e554ae0b47645df492a9141d674fdd0 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Wed, 19 Nov 2025 10:26:09 +0100 Subject: [PATCH 16/19] feat: no need to know about docker here --- package-and-upload-artifact/scripts/upload-s3-artifact.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/package-and-upload-artifact/scripts/upload-s3-artifact.sh b/package-and-upload-artifact/scripts/upload-s3-artifact.sh index b20cbf45..60997534 100644 --- a/package-and-upload-artifact/scripts/upload-s3-artifact.sh +++ b/package-and-upload-artifact/scripts/upload-s3-artifact.sh @@ -57,9 +57,6 @@ folder) archive_folder_source ;; file) ;; -docker-image) - die "upload-s3-artifact.sh only supports file or folder sources" - ;; *) die "Unsupported source type: $source_type" ;; From 8fb31fe88c3314c9beb49ba7381ba2f55e67d1f6 Mon Sep 17 00:00:00 2001 From: "Anders K. Pettersen" Date: Wed, 19 Nov 2025 10:34:43 +0100 Subject: [PATCH 17/19] fix: various bugs and improvements --- package-and-upload-artifact/lib/common.sh | 9 +++++---- package-and-upload-artifact/scripts/push-ecr-image.sh | 1 + .../scripts/upload-s3-artifact.sh | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package-and-upload-artifact/lib/common.sh b/package-and-upload-artifact/lib/common.sh index 35ac4fc3..23059199 100644 --- a/package-and-upload-artifact/lib/common.sh +++ b/package-and-upload-artifact/lib/common.sh @@ -19,6 +19,9 @@ require_cmd() { command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" } +require_cmd jq +require_cmd aws + authenticate_via_oidc() { local role_arn="$1" local aws_region="${2:-eu-north-1}" @@ -29,8 +32,6 @@ authenticate_via_oidc() { : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?Missing ACTIONS_ID_TOKEN_REQUEST_TOKEN}" require_cmd curl - require_cmd jq - require_cmd aws log_info "Authenticating to $role_arn via OIDC..." @@ -75,13 +76,13 @@ clear_credentials() { environment_defined() { : "${CONFIG:?Missing CONFIG}" local environment="$1" - printf '%s' "$CONFIG" | jq -e ".${environment} != null" >/dev/null 2>&1 + printf '%s' "$CONFIG" | jq -e --arg env "$environment" '.[$env] != null' >/dev/null 2>&1 } environment_value() { : "${CONFIG:?Missing CONFIG}" local environment="$1" key="$2" - printf '%s' "$CONFIG" | jq -e -r ".${environment}.${key}" + printf '%s' "$CONFIG" | jq -e -r --arg env "$environment" --arg k "$key" '.[$env][$k]' } write_github_summary() { diff --git a/package-and-upload-artifact/scripts/push-ecr-image.sh b/package-and-upload-artifact/scripts/push-ecr-image.sh index 32c7a48c..153f8816 100644 --- a/package-and-upload-artifact/scripts/push-ecr-image.sh +++ b/package-and-upload-artifact/scripts/push-ecr-image.sh @@ -39,6 +39,7 @@ upload_image_artifact() { docker tag "$source_location" "$image_tag" log_info "Pushing image with tag: $image_tag" docker push "$image_tag" + docker logout "$ecr_repository_uri" } environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" diff --git a/package-and-upload-artifact/scripts/upload-s3-artifact.sh b/package-and-upload-artifact/scripts/upload-s3-artifact.sh index 60997534..147631d7 100644 --- a/package-and-upload-artifact/scripts/upload-s3-artifact.sh +++ b/package-and-upload-artifact/scripts/upload-s3-artifact.sh @@ -35,7 +35,7 @@ append_file_extension_suffix() { if [[ "$source_location" == *.* ]]; then extension="${source_location##*.}" fi - if [[ -n "$extension" ]]; then + if [[ -n "$extension" && "$tag" != *."$extension" ]]; then tag="$tag.$extension" fi } @@ -76,7 +76,7 @@ for environment in $environments; do fi role_arn="$(environment_value "$environment" artifactRoleArn)" - default_region="$(environment_value "$environment" defaultRegion)" + default_region="$(environment_value "$environment" defaultRegion)" || die "Missing defaultRegion for $environment" authenticate_via_oidc "$role_arn" "$default_region" export AWS_REGION="$default_region" From 5342b534b4cb37a4757b61e080be785358a33be4 Mon Sep 17 00:00:00 2001 From: Yngvar Kristiansen <562343+yngvark@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:57:02 +0100 Subject: [PATCH 18/19] experiment with split --- configure-aws-credentials/action.yml | 22 ++++ configure-aws-credentials/lib/common.sh | 86 +++++++++++++ .../scripts/configure.sh | 25 ++++ docker-push/action.yml | 57 +++++++++ docker-push/scripts/push-image.sh | 84 +++++++++++++ package-and-upload-artifact/action.yml | 2 +- package-and-upload-to-s3/action.yml | 62 ++++++++++ .../scripts/upload-to-s3.sh | 113 ++++++++++++++++++ 8 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 configure-aws-credentials/action.yml create mode 100644 configure-aws-credentials/lib/common.sh create mode 100644 configure-aws-credentials/scripts/configure.sh create mode 100644 docker-push/action.yml create mode 100644 docker-push/scripts/push-image.sh create mode 100644 package-and-upload-to-s3/action.yml create mode 100644 package-and-upload-to-s3/scripts/upload-to-s3.sh diff --git a/configure-aws-credentials/action.yml b/configure-aws-credentials/action.yml new file mode 100644 index 00000000..195e482d --- /dev/null +++ b/configure-aws-credentials/action.yml @@ -0,0 +1,22 @@ +name: "Configure AWS Credentials" +description: > + Authenticates to AWS via OIDC for a specific environment defined in the config JSON. + Sets AWS credential environment variables for subsequent steps. +inputs: + config: + description: "JSON-encoded config (.gp.cicd.json)" + required: true + environment: + description: "The environment name (e.g., 'dev', 'prod')" + required: true + +runs: + using: composite + steps: + - name: Configure AWS credentials + shell: bash --noprofile --norc -euo pipefail {0} + env: + ACTION_PATH: ${{ github.action_path }} + CONFIG: ${{ inputs.config }} + ENVIRONMENT: ${{ inputs.environment }} + run: "$ACTION_PATH/scripts/configure.sh" diff --git a/configure-aws-credentials/lib/common.sh b/configure-aws-credentials/lib/common.sh new file mode 100644 index 00000000..b13666f8 --- /dev/null +++ b/configure-aws-credentials/lib/common.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Common helpers shared between artifact scripts. + +log_info() { + printf '[INFO] %s\n' "$*" +} + +log_error() { + printf '[ERROR] %s\n' "$*" >&2 +} + +die() { + log_error "$1" + exit 1 +} + +require_cmd() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" +} + +require_cmd jq +require_cmd aws + +authenticate_via_oidc() { + local role_arn="$1" + local aws_region="${2:-eu-north-1}" + local session_name + session_name="GitHubAction-$(date +%s)" + + : "${ACTIONS_ID_TOKEN_REQUEST_URL:?Missing ACTIONS_ID_TOKEN_REQUEST_URL}" + : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?Missing ACTIONS_ID_TOKEN_REQUEST_TOKEN}" + + require_cmd curl + + log_info "Authenticating to $role_arn via OIDC..." + + local oidc_response oidc_token + if ! oidc_response="$(curl -sSLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" \ + -H "User-Agent: actions/oidc-client" \ + -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN")"; then + die "Failed to reach GitHub OIDC endpoint" + fi + + oidc_token="$(printf '%s' "$oidc_response" | jq -r '.value')" + if [[ -z "$oidc_token" || "$oidc_token" == "null" ]]; then + die "Failed to obtain OIDC token from GitHub. Is 'permissions: id-token: write' set?" + fi + + local credentials_json + if ! credentials_json="$(aws sts assume-role-with-web-identity \ + --role-arn "$role_arn" \ + --role-session-name "$session_name" \ + --web-identity-token "$oidc_token" \ + --duration-seconds 900 \ + --region "$aws_region" \ + --output json)"; then + die "Failed to assume role $role_arn with web identity" + fi + + AWS_ACCESS_KEY_ID="$(printf '%s' "$credentials_json" | jq -r '.Credentials.AccessKeyId')" + AWS_SECRET_ACCESS_KEY="$(printf '%s' "$credentials_json" | jq -r '.Credentials.SecretAccessKey')" + AWS_SESSION_TOKEN="$(printf '%s' "$credentials_json" | jq -r '.Credentials.SessionToken')" + + if [[ -z "$AWS_ACCESS_KEY_ID" || "$AWS_ACCESS_KEY_ID" == "null" ]]; then + die "Failed to parse AWS credentials from STS response" + fi + + export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN +} + +clear_credentials() { + unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE AWS_REGION AWS_DEFAULT_REGION +} + +environment_defined() { + : "${CONFIG:?Missing CONFIG}" + local environment="$1" + printf '%s' "$CONFIG" | jq -e --arg env "$environment" '.[$env] != null' >/dev/null 2>&1 +} + +environment_value() { + : "${CONFIG:?Missing CONFIG}" + local environment="$1" key="$2" + printf '%s' "$CONFIG" | jq -e -r --arg env "$environment" --arg k "$key" '.[$env][$k]' +} diff --git a/configure-aws-credentials/scripts/configure.sh b/configure-aws-credentials/scripts/configure.sh new file mode 100644 index 00000000..892d6aeb --- /dev/null +++ b/configure-aws-credentials/scripts/configure.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +: "${CONFIG:?Missing CONFIG}" +: "${ENVIRONMENT:?Missing ENVIRONMENT}" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +source "$repo_root/lib/common.sh" + +environment="$ENVIRONMENT" + +if ! environment_defined "$environment"; then + die "Environment '$environment' not defined in config" +fi + +role_arn="$(environment_value "$environment" artifactRoleArn)" +default_region="$(environment_value "$environment" defaultRegion)" || die "Missing defaultRegion for $environment" + +authenticate_via_oidc "$role_arn" "$default_region" +export AWS_REGION="$default_region" +export AWS_DEFAULT_REGION="$default_region" + +log_info "AWS credentials configured for environment: $environment" diff --git a/docker-push/action.yml b/docker-push/action.yml new file mode 100644 index 00000000..faa61ac7 --- /dev/null +++ b/docker-push/action.yml @@ -0,0 +1,57 @@ +name: "Push Docker image to ECR" +description: > + Pushes a Docker image to ECR for every environment listed in the JSON config, + using the AWS account, role, and repository data defined there. The Docker + image is retagged and pushed to ECR so Terraform can deploy it to ECS. +inputs: + config: + description: "JSON-encoded config (.gp.cicd.json)" + required: true + tag: + description: "The main tag to apply to the Docker image" + required: true + image-id: + description: "Docker image ID from docker build (e.g., from steps.docker-build.outputs.imageid)" + required: true + +outputs: + result: + description: The final tag value + value: ${{ steps.record-tag.outputs.tag }} + +runs: + using: composite + steps: + - name: Push Docker image to ECR + shell: bash --noprofile --norc -euo pipefail {0} + id: push-ecr-image + env: + ACTION_PATH: ${{ github.action_path }} + TZ: "Europe/Oslo" + CONFIG: ${{ inputs.config }} + PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" + IMAGE_ID: ${{ inputs.image-id }} + TAG: ${{ inputs.tag }} + run: "$ACTION_PATH/scripts/push-image.sh" + + - name: Record artifact tag + shell: bash --noprofile --norc -euo pipefail {0} + id: record-tag + run: | + : "${ARTIFACT_TAG:?Missing ARTIFACT_TAG}" + printf 'tag=%s\n' "$ARTIFACT_TAG" >>"$GITHUB_OUTPUT" + + - name: Store artifact tag in commit status + shell: bash --noprofile --norc -euo pipefail {0} + env: + COMMIT_SHA: ${{ github.sha }} + GH_TOKEN: ${{ github.token }} + DESCRIPTION: ${{ steps.record-tag.outputs.tag }} + WORKFLOW: ${{ github.workflow }} + TARGET_URL: "${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + run: | + gh api "repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \ + -f state="success" \ + -f context="$WORKFLOW / artifact-tag" \ + -f target_url="$TARGET_URL" \ + -f description="$DESCRIPTION" diff --git a/docker-push/scripts/push-image.sh b/docker-push/scripts/push-image.sh new file mode 100644 index 00000000..08f32681 --- /dev/null +++ b/docker-push/scripts/push-image.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +: "${CONFIG:?Missing CONFIG}" +: "${IMAGE_ID:?Missing IMAGE_ID}" +: "${TAG:?Missing TAG}" +: "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" +: "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" +: "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" +: "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" +: "${GITHUB_ENV:?Missing GITHUB_ENV}" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +action_dir="$(cd "$script_dir/.." && pwd)" + +# Import common functions from configure-aws-credentials action +configure_action_dir="$(cd "$action_dir/../configure-aws-credentials" && pwd)" +source "$configure_action_dir/lib/common.sh" + +require_cmd docker + +image_id="$IMAGE_ID" +tag="$TAG" + +push_image_to_ecr() { + local environment="$1" + local account_id ecr_repository_name default_region + local login_password ecr_repository_uri image_tag + + account_id="$(environment_value "$environment" accountId)" + ecr_repository_name="$(environment_value "$environment" artifactEcrRepositoryName)" + default_region="$(environment_value "$environment" defaultRegion)" + + login_password="$(aws ecr get-login-password --region "$default_region")" + printf '::add-mask::%s\n' "$login_password" + + ecr_repository_uri="$account_id.dkr.ecr.$default_region.amazonaws.com" + image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" + + printf '%s\n' "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" + log_info "Tagging image with image tag: $image_tag" + docker tag "$image_id" "$image_tag" + log_info "Pushing image with tag: $image_tag" + docker push "$image_tag" + docker logout "$ecr_repository_uri" +} + +environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" +if [[ -z "$environments" ]]; then + die "No environments with artifactRoleArn defined in config" +fi + +for environment in $environments; do + if ! environment_defined "$environment"; then + continue + fi + + role_arn="$(environment_value "$environment" artifactRoleArn)" + default_region="$(environment_value "$environment" defaultRegion)" + + authenticate_via_oidc "$role_arn" "$default_region" + export AWS_REGION="$default_region" + export AWS_DEFAULT_REGION="$default_region" + + push_image_to_ecr "$environment" + clear_credentials +done + +# Write summary and outputs +workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" +workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" + +printf 'ARTIFACT_TAG=%s\n' "$tag" >>"$GITHUB_ENV" +cat <>"$GITHUB_STEP_SUMMARY" +Built and uploaded Docker image with tag: +\`\`\` +$tag +\`\`\` + +--- + +_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ +EOF diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index f161a763..b820755c 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -46,7 +46,7 @@ runs: shell: bash --noprofile --norc -euo pipefail {0} id: push-ecr-image env: - ACTION_PATH: ${{ github.action_path }} + ACTION_PATH: ${{ github.action_path 1}} TZ: "Europe/Oslo" CONFIG: ${{ inputs.config }} PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" diff --git a/package-and-upload-to-s3/action.yml b/package-and-upload-to-s3/action.yml new file mode 100644 index 00000000..88e74b33 --- /dev/null +++ b/package-and-upload-to-s3/action.yml @@ -0,0 +1,62 @@ +name: "Package and upload to S3" +description: > + Packages files or folders for every environment listed in the JSON config, + using the AWS account, bucket, and role data defined there. Files or folders + are zipped if needed, copied to S3, and later published through the + Terraform → Lambda → CloudFront flow. +inputs: + config: + description: "JSON-encoded config (.gp.cicd.json)" + required: true + tag: + description: "The main tag to apply to the artifact" + required: true + source-location: + description: "A file or folder path" + required: true + source-type: + description: "file | folder" + required: true + +outputs: + result: + description: The final tag value + value: ${{ steps.record-tag.outputs.tag }} + +runs: + using: composite + steps: + - name: Upload artifact to S3 + shell: bash --noprofile --norc -euo pipefail {0} + id: upload-s3-artifact + env: + ACTION_PATH: ${{ github.action_path }} + TZ: "Europe/Oslo" + CONFIG: ${{ inputs.config }} + PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" + SOURCE_TYPE: ${{ inputs.source-type }} + SOURCE_LOCATION: ${{ inputs.source-location }} + TAG: ${{ inputs.tag }} + run: "$ACTION_PATH/scripts/upload-to-s3.sh" + + - name: Record artifact tag + shell: bash --noprofile --norc -euo pipefail {0} + id: record-tag + run: | + : "${ARTIFACT_TAG:?Missing ARTIFACT_TAG}" + printf 'tag=%s\n' "$ARTIFACT_TAG" >>"$GITHUB_OUTPUT" + + - name: Store artifact tag in commit status + shell: bash --noprofile --norc -euo pipefail {0} + env: + COMMIT_SHA: ${{ github.sha }} + GH_TOKEN: ${{ github.token }} + DESCRIPTION: ${{ steps.record-tag.outputs.tag }} + WORKFLOW: ${{ github.workflow }} + TARGET_URL: "${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + run: | + gh api "repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \ + -f state="success" \ + -f context="$WORKFLOW / artifact-tag" \ + -f target_url="$TARGET_URL" \ + -f description="$DESCRIPTION" diff --git a/package-and-upload-to-s3/scripts/upload-to-s3.sh b/package-and-upload-to-s3/scripts/upload-to-s3.sh new file mode 100644 index 00000000..b37caca6 --- /dev/null +++ b/package-and-upload-to-s3/scripts/upload-to-s3.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +: "${CONFIG:?Missing CONFIG}" +: "${SOURCE_TYPE:?Missing SOURCE_TYPE}" +: "${SOURCE_LOCATION:?Missing SOURCE_LOCATION}" +: "${TAG:?Missing TAG}" +: "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" +: "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" +: "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" +: "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" +: "${GITHUB_ENV:?Missing GITHUB_ENV}" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +action_dir="$(cd "$script_dir/.." && pwd)" + +# Import common functions from configure-aws-credentials action +configure_action_dir="$(cd "$action_dir/../configure-aws-credentials" && pwd)" +source "$configure_action_dir/lib/common.sh" + +require_cmd zip + +source_type="$SOURCE_TYPE" +source_location="$SOURCE_LOCATION" +tag="$TAG" +package_tmp_dir="" + +cleanup() { + if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then + rm -rf "$package_tmp_dir" + fi +} +trap cleanup EXIT + +archive_folder_source() { + package_tmp_dir="$(mktemp -d)" + (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) + source_location="$package_tmp_dir/archive.zip" + source_type="file" +} + +append_file_extension_suffix() { + local extension="" + if [[ "$source_location" == *.* ]]; then + extension="${source_location##*.}" + fi + if [[ -n "$extension" && "$tag" != *."$extension" ]]; then + tag="$tag.$extension" + fi +} + +upload_file_to_s3() { + local environment="$1" + local bucket_name + + bucket_name="$(environment_value "$environment" artifactBucketName)" + + log_info "Uploading $source_location to S3 as $tag in $environment" + + aws s3 cp "$source_location" "s3://$bucket_name/$tag" +} + +case "$source_type" in +folder) + log_info "Compressing folder artifact into archive for upload" + archive_folder_source + ;; +file) ;; +*) + die "Unsupported source type: $source_type" + ;; +esac + +log_info "Preparing file artifact for upload to S3" +append_file_extension_suffix + +environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" +if [[ -z "$environments" ]]; then + die "No environments with artifactRoleArn defined in config" +fi + +for environment in $environments; do + if ! environment_defined "$environment"; then + continue + fi + + role_arn="$(environment_value "$environment" artifactRoleArn)" + default_region="$(environment_value "$environment" defaultRegion)" || die "Missing defaultRegion for $environment" + + authenticate_via_oidc "$role_arn" "$default_region" + export AWS_REGION="$default_region" + export AWS_DEFAULT_REGION="$default_region" + + upload_file_to_s3 "$environment" + clear_credentials +done + +# Write summary and outputs +workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" +workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" + +printf 'ARTIFACT_TAG=%s\n' "$tag" >>"$GITHUB_ENV" +cat <>"$GITHUB_STEP_SUMMARY" +Built and uploaded artifact to S3 with tag: +\`\`\` +$tag +\`\`\` + +--- + +_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ +EOF From 178b5db0bcfeea3c262419efa4440ad99e40a773 Mon Sep 17 00:00:00 2001 From: Yngvar Kristiansen <562343+yngvark@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:48:46 +0100 Subject: [PATCH 19/19] Revert "experiment with split" This reverts commit 5342b534b4cb37a4757b61e080be785358a33be4. --- configure-aws-credentials/action.yml | 22 ---- configure-aws-credentials/lib/common.sh | 86 ------------- .../scripts/configure.sh | 25 ---- docker-push/action.yml | 57 --------- docker-push/scripts/push-image.sh | 84 ------------- package-and-upload-artifact/action.yml | 2 +- package-and-upload-to-s3/action.yml | 62 ---------- .../scripts/upload-to-s3.sh | 113 ------------------ 8 files changed, 1 insertion(+), 450 deletions(-) delete mode 100644 configure-aws-credentials/action.yml delete mode 100644 configure-aws-credentials/lib/common.sh delete mode 100644 configure-aws-credentials/scripts/configure.sh delete mode 100644 docker-push/action.yml delete mode 100644 docker-push/scripts/push-image.sh delete mode 100644 package-and-upload-to-s3/action.yml delete mode 100644 package-and-upload-to-s3/scripts/upload-to-s3.sh diff --git a/configure-aws-credentials/action.yml b/configure-aws-credentials/action.yml deleted file mode 100644 index 195e482d..00000000 --- a/configure-aws-credentials/action.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Configure AWS Credentials" -description: > - Authenticates to AWS via OIDC for a specific environment defined in the config JSON. - Sets AWS credential environment variables for subsequent steps. -inputs: - config: - description: "JSON-encoded config (.gp.cicd.json)" - required: true - environment: - description: "The environment name (e.g., 'dev', 'prod')" - required: true - -runs: - using: composite - steps: - - name: Configure AWS credentials - shell: bash --noprofile --norc -euo pipefail {0} - env: - ACTION_PATH: ${{ github.action_path }} - CONFIG: ${{ inputs.config }} - ENVIRONMENT: ${{ inputs.environment }} - run: "$ACTION_PATH/scripts/configure.sh" diff --git a/configure-aws-credentials/lib/common.sh b/configure-aws-credentials/lib/common.sh deleted file mode 100644 index b13666f8..00000000 --- a/configure-aws-credentials/lib/common.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# Common helpers shared between artifact scripts. - -log_info() { - printf '[INFO] %s\n' "$*" -} - -log_error() { - printf '[ERROR] %s\n' "$*" >&2 -} - -die() { - log_error "$1" - exit 1 -} - -require_cmd() { - local cmd="$1" - command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" -} - -require_cmd jq -require_cmd aws - -authenticate_via_oidc() { - local role_arn="$1" - local aws_region="${2:-eu-north-1}" - local session_name - session_name="GitHubAction-$(date +%s)" - - : "${ACTIONS_ID_TOKEN_REQUEST_URL:?Missing ACTIONS_ID_TOKEN_REQUEST_URL}" - : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?Missing ACTIONS_ID_TOKEN_REQUEST_TOKEN}" - - require_cmd curl - - log_info "Authenticating to $role_arn via OIDC..." - - local oidc_response oidc_token - if ! oidc_response="$(curl -sSLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" \ - -H "User-Agent: actions/oidc-client" \ - -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN")"; then - die "Failed to reach GitHub OIDC endpoint" - fi - - oidc_token="$(printf '%s' "$oidc_response" | jq -r '.value')" - if [[ -z "$oidc_token" || "$oidc_token" == "null" ]]; then - die "Failed to obtain OIDC token from GitHub. Is 'permissions: id-token: write' set?" - fi - - local credentials_json - if ! credentials_json="$(aws sts assume-role-with-web-identity \ - --role-arn "$role_arn" \ - --role-session-name "$session_name" \ - --web-identity-token "$oidc_token" \ - --duration-seconds 900 \ - --region "$aws_region" \ - --output json)"; then - die "Failed to assume role $role_arn with web identity" - fi - - AWS_ACCESS_KEY_ID="$(printf '%s' "$credentials_json" | jq -r '.Credentials.AccessKeyId')" - AWS_SECRET_ACCESS_KEY="$(printf '%s' "$credentials_json" | jq -r '.Credentials.SecretAccessKey')" - AWS_SESSION_TOKEN="$(printf '%s' "$credentials_json" | jq -r '.Credentials.SessionToken')" - - if [[ -z "$AWS_ACCESS_KEY_ID" || "$AWS_ACCESS_KEY_ID" == "null" ]]; then - die "Failed to parse AWS credentials from STS response" - fi - - export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN -} - -clear_credentials() { - unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE AWS_REGION AWS_DEFAULT_REGION -} - -environment_defined() { - : "${CONFIG:?Missing CONFIG}" - local environment="$1" - printf '%s' "$CONFIG" | jq -e --arg env "$environment" '.[$env] != null' >/dev/null 2>&1 -} - -environment_value() { - : "${CONFIG:?Missing CONFIG}" - local environment="$1" key="$2" - printf '%s' "$CONFIG" | jq -e -r --arg env "$environment" --arg k "$key" '.[$env][$k]' -} diff --git a/configure-aws-credentials/scripts/configure.sh b/configure-aws-credentials/scripts/configure.sh deleted file mode 100644 index 892d6aeb..00000000 --- a/configure-aws-credentials/scripts/configure.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' - -: "${CONFIG:?Missing CONFIG}" -: "${ENVIRONMENT:?Missing ENVIRONMENT}" - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd "$script_dir/.." && pwd)" -source "$repo_root/lib/common.sh" - -environment="$ENVIRONMENT" - -if ! environment_defined "$environment"; then - die "Environment '$environment' not defined in config" -fi - -role_arn="$(environment_value "$environment" artifactRoleArn)" -default_region="$(environment_value "$environment" defaultRegion)" || die "Missing defaultRegion for $environment" - -authenticate_via_oidc "$role_arn" "$default_region" -export AWS_REGION="$default_region" -export AWS_DEFAULT_REGION="$default_region" - -log_info "AWS credentials configured for environment: $environment" diff --git a/docker-push/action.yml b/docker-push/action.yml deleted file mode 100644 index faa61ac7..00000000 --- a/docker-push/action.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: "Push Docker image to ECR" -description: > - Pushes a Docker image to ECR for every environment listed in the JSON config, - using the AWS account, role, and repository data defined there. The Docker - image is retagged and pushed to ECR so Terraform can deploy it to ECS. -inputs: - config: - description: "JSON-encoded config (.gp.cicd.json)" - required: true - tag: - description: "The main tag to apply to the Docker image" - required: true - image-id: - description: "Docker image ID from docker build (e.g., from steps.docker-build.outputs.imageid)" - required: true - -outputs: - result: - description: The final tag value - value: ${{ steps.record-tag.outputs.tag }} - -runs: - using: composite - steps: - - name: Push Docker image to ECR - shell: bash --noprofile --norc -euo pipefail {0} - id: push-ecr-image - env: - ACTION_PATH: ${{ github.action_path }} - TZ: "Europe/Oslo" - CONFIG: ${{ inputs.config }} - PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" - IMAGE_ID: ${{ inputs.image-id }} - TAG: ${{ inputs.tag }} - run: "$ACTION_PATH/scripts/push-image.sh" - - - name: Record artifact tag - shell: bash --noprofile --norc -euo pipefail {0} - id: record-tag - run: | - : "${ARTIFACT_TAG:?Missing ARTIFACT_TAG}" - printf 'tag=%s\n' "$ARTIFACT_TAG" >>"$GITHUB_OUTPUT" - - - name: Store artifact tag in commit status - shell: bash --noprofile --norc -euo pipefail {0} - env: - COMMIT_SHA: ${{ github.sha }} - GH_TOKEN: ${{ github.token }} - DESCRIPTION: ${{ steps.record-tag.outputs.tag }} - WORKFLOW: ${{ github.workflow }} - TARGET_URL: "${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - run: | - gh api "repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \ - -f state="success" \ - -f context="$WORKFLOW / artifact-tag" \ - -f target_url="$TARGET_URL" \ - -f description="$DESCRIPTION" diff --git a/docker-push/scripts/push-image.sh b/docker-push/scripts/push-image.sh deleted file mode 100644 index 08f32681..00000000 --- a/docker-push/scripts/push-image.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' - -: "${CONFIG:?Missing CONFIG}" -: "${IMAGE_ID:?Missing IMAGE_ID}" -: "${TAG:?Missing TAG}" -: "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" -: "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" -: "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" -: "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" -: "${GITHUB_ENV:?Missing GITHUB_ENV}" - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -action_dir="$(cd "$script_dir/.." && pwd)" - -# Import common functions from configure-aws-credentials action -configure_action_dir="$(cd "$action_dir/../configure-aws-credentials" && pwd)" -source "$configure_action_dir/lib/common.sh" - -require_cmd docker - -image_id="$IMAGE_ID" -tag="$TAG" - -push_image_to_ecr() { - local environment="$1" - local account_id ecr_repository_name default_region - local login_password ecr_repository_uri image_tag - - account_id="$(environment_value "$environment" accountId)" - ecr_repository_name="$(environment_value "$environment" artifactEcrRepositoryName)" - default_region="$(environment_value "$environment" defaultRegion)" - - login_password="$(aws ecr get-login-password --region "$default_region")" - printf '::add-mask::%s\n' "$login_password" - - ecr_repository_uri="$account_id.dkr.ecr.$default_region.amazonaws.com" - image_tag="$ecr_repository_uri/$ecr_repository_name:$tag" - - printf '%s\n' "$login_password" | docker login --username AWS --password-stdin "$ecr_repository_uri" - log_info "Tagging image with image tag: $image_tag" - docker tag "$image_id" "$image_tag" - log_info "Pushing image with tag: $image_tag" - docker push "$image_tag" - docker logout "$ecr_repository_uri" -} - -environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" -if [[ -z "$environments" ]]; then - die "No environments with artifactRoleArn defined in config" -fi - -for environment in $environments; do - if ! environment_defined "$environment"; then - continue - fi - - role_arn="$(environment_value "$environment" artifactRoleArn)" - default_region="$(environment_value "$environment" defaultRegion)" - - authenticate_via_oidc "$role_arn" "$default_region" - export AWS_REGION="$default_region" - export AWS_DEFAULT_REGION="$default_region" - - push_image_to_ecr "$environment" - clear_credentials -done - -# Write summary and outputs -workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" -workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" - -printf 'ARTIFACT_TAG=%s\n' "$tag" >>"$GITHUB_ENV" -cat <>"$GITHUB_STEP_SUMMARY" -Built and uploaded Docker image with tag: -\`\`\` -$tag -\`\`\` - ---- - -_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ -EOF diff --git a/package-and-upload-artifact/action.yml b/package-and-upload-artifact/action.yml index b820755c..f161a763 100644 --- a/package-and-upload-artifact/action.yml +++ b/package-and-upload-artifact/action.yml @@ -46,7 +46,7 @@ runs: shell: bash --noprofile --norc -euo pipefail {0} id: push-ecr-image env: - ACTION_PATH: ${{ github.action_path 1}} + ACTION_PATH: ${{ github.action_path }} TZ: "Europe/Oslo" CONFIG: ${{ inputs.config }} PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" diff --git a/package-and-upload-to-s3/action.yml b/package-and-upload-to-s3/action.yml deleted file mode 100644 index 88e74b33..00000000 --- a/package-and-upload-to-s3/action.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: "Package and upload to S3" -description: > - Packages files or folders for every environment listed in the JSON config, - using the AWS account, bucket, and role data defined there. Files or folders - are zipped if needed, copied to S3, and later published through the - Terraform → Lambda → CloudFront flow. -inputs: - config: - description: "JSON-encoded config (.gp.cicd.json)" - required: true - tag: - description: "The main tag to apply to the artifact" - required: true - source-location: - description: "A file or folder path" - required: true - source-type: - description: "file | folder" - required: true - -outputs: - result: - description: The final tag value - value: ${{ steps.record-tag.outputs.tag }} - -runs: - using: composite - steps: - - name: Upload artifact to S3 - shell: bash --noprofile --norc -euo pipefail {0} - id: upload-s3-artifact - env: - ACTION_PATH: ${{ github.action_path }} - TZ: "Europe/Oslo" - CONFIG: ${{ inputs.config }} - PARTIAL_WORKFLOW_DISPATCH_URL: "${{ github.server_url}}/${{ github.repository }}/actions/workflows" - SOURCE_TYPE: ${{ inputs.source-type }} - SOURCE_LOCATION: ${{ inputs.source-location }} - TAG: ${{ inputs.tag }} - run: "$ACTION_PATH/scripts/upload-to-s3.sh" - - - name: Record artifact tag - shell: bash --noprofile --norc -euo pipefail {0} - id: record-tag - run: | - : "${ARTIFACT_TAG:?Missing ARTIFACT_TAG}" - printf 'tag=%s\n' "$ARTIFACT_TAG" >>"$GITHUB_OUTPUT" - - - name: Store artifact tag in commit status - shell: bash --noprofile --norc -euo pipefail {0} - env: - COMMIT_SHA: ${{ github.sha }} - GH_TOKEN: ${{ github.token }} - DESCRIPTION: ${{ steps.record-tag.outputs.tag }} - WORKFLOW: ${{ github.workflow }} - TARGET_URL: "${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - run: | - gh api "repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \ - -f state="success" \ - -f context="$WORKFLOW / artifact-tag" \ - -f target_url="$TARGET_URL" \ - -f description="$DESCRIPTION" diff --git a/package-and-upload-to-s3/scripts/upload-to-s3.sh b/package-and-upload-to-s3/scripts/upload-to-s3.sh deleted file mode 100644 index b37caca6..00000000 --- a/package-and-upload-to-s3/scripts/upload-to-s3.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' - -: "${CONFIG:?Missing CONFIG}" -: "${SOURCE_TYPE:?Missing SOURCE_TYPE}" -: "${SOURCE_LOCATION:?Missing SOURCE_LOCATION}" -: "${TAG:?Missing TAG}" -: "${PARTIAL_WORKFLOW_DISPATCH_URL:?Missing PARTIAL_WORKFLOW_DISPATCH_URL}" -: "${GITHUB_WORKFLOW_REF:?Missing GITHUB_WORKFLOW_REF}" -: "${GITHUB_OUTPUT:?Missing GITHUB_OUTPUT}" -: "${GITHUB_STEP_SUMMARY:?Missing GITHUB_STEP_SUMMARY}" -: "${GITHUB_ENV:?Missing GITHUB_ENV}" - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -action_dir="$(cd "$script_dir/.." && pwd)" - -# Import common functions from configure-aws-credentials action -configure_action_dir="$(cd "$action_dir/../configure-aws-credentials" && pwd)" -source "$configure_action_dir/lib/common.sh" - -require_cmd zip - -source_type="$SOURCE_TYPE" -source_location="$SOURCE_LOCATION" -tag="$TAG" -package_tmp_dir="" - -cleanup() { - if [[ -n "$package_tmp_dir" && -d "$package_tmp_dir" ]]; then - rm -rf "$package_tmp_dir" - fi -} -trap cleanup EXIT - -archive_folder_source() { - package_tmp_dir="$(mktemp -d)" - (cd "$source_location" && zip -r "$package_tmp_dir/archive.zip" .) - source_location="$package_tmp_dir/archive.zip" - source_type="file" -} - -append_file_extension_suffix() { - local extension="" - if [[ "$source_location" == *.* ]]; then - extension="${source_location##*.}" - fi - if [[ -n "$extension" && "$tag" != *."$extension" ]]; then - tag="$tag.$extension" - fi -} - -upload_file_to_s3() { - local environment="$1" - local bucket_name - - bucket_name="$(environment_value "$environment" artifactBucketName)" - - log_info "Uploading $source_location to S3 as $tag in $environment" - - aws s3 cp "$source_location" "s3://$bucket_name/$tag" -} - -case "$source_type" in -folder) - log_info "Compressing folder artifact into archive for upload" - archive_folder_source - ;; -file) ;; -*) - die "Unsupported source type: $source_type" - ;; -esac - -log_info "Preparing file artifact for upload to S3" -append_file_extension_suffix - -environments="$(printf '%s' "$CONFIG" | jq -r 'to_entries[] | select(.value.artifactRoleArn != null) | .key')" -if [[ -z "$environments" ]]; then - die "No environments with artifactRoleArn defined in config" -fi - -for environment in $environments; do - if ! environment_defined "$environment"; then - continue - fi - - role_arn="$(environment_value "$environment" artifactRoleArn)" - default_region="$(environment_value "$environment" defaultRegion)" || die "Missing defaultRegion for $environment" - - authenticate_via_oidc "$role_arn" "$default_region" - export AWS_REGION="$default_region" - export AWS_DEFAULT_REGION="$default_region" - - upload_file_to_s3 "$environment" - clear_credentials -done - -# Write summary and outputs -workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")" -workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename" - -printf 'ARTIFACT_TAG=%s\n' "$tag" >>"$GITHUB_ENV" -cat <>"$GITHUB_STEP_SUMMARY" -Built and uploaded artifact to S3 with tag: -\`\`\` -$tag -\`\`\` - ---- - -_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_ -EOF