Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package-and-upload-artifact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

# Package and upload artifact

Packages and uploads an artifact
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

Expand Down
127 changes: 30 additions & 97 deletions package-and-upload-artifact/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: "Package and upload artifact"
description: Packages and uploads an artifact
description: >
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)"
Expand All @@ -22,112 +27,40 @@ 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: 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 }}
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: |
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
run: "$ACTION_PATH/scripts/upload-s3-artifact.sh"

# $GITHUB_WORKFLOW_REF looks like this: <org>/<repo>/.github/workflows/<workflow-filename>@<git-reference>
workflow_filename="$(basename "${GITHUB_WORKFLOW_REF%%@*}")"
workflow_dispatch_url="$PARTIAL_WORKFLOW_DISPATCH_URL/$workflow_filename"

echo "tag=$tag" >> "$GITHUB_OUTPUT"
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
Built and uploaded artifact with tag:
\`\`\`
$tag
\`\`\`

---
- 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 }}
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/push-ecr-image.sh"

_To manually deploy the artifact, copy the tag and pass it in through a [workflow dispatch]($workflow_dispatch_url)_
EOF
- 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:
Expand Down
112 changes: 112 additions & 0 deletions package-and-upload-artifact/lib/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/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]'
}

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 <<EOF >>"$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
}
66 changes: 66 additions & 0 deletions package-and-upload-artifact/scripts/push-ecr-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/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"

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)"

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"
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"

upload_image_artifact "$environment"
clear_credentials
done

write_github_summary "$tag"
Loading