diff --git a/.changeset/promote-image-ecr-cosign-refs.md b/.changeset/promote-image-ecr-cosign-refs.md new file mode 100644 index 000000000..84ff955a5 --- /dev/null +++ b/.changeset/promote-image-ecr-cosign-refs.md @@ -0,0 +1,56 @@ +--- +"promote-image-ecr": minor +--- + +Stop passing skopeo transport URLs to cosign. + +The action copies with `cosign copy`, but the script still built image references +in skopeo's form, `docker:///:`. cosign parses references +with go-containerregistry, which reads everything before the first `/` as the +registry: it took `docker:` as the host and tried to reach `https://docker/v2/`, +so every promotion failed on DNS before a registry was contacted. + + Error: Get "https://docker/v2/": dial tcp: lookup docker on 127.0.0.53:53: server misbehaving + +This affected both the single-image and the images-matrix paths, and every +released version, so no promotion this action ran has ever succeeded. + +The rest is the remainder of the same unfinished skopeo-to-cosign migration. + +`copy-signatures` is removed. It was documented as a switch to skopeo, but no +skopeo path exists and the script never read the value, so setting it to `false` +did not change what the action did. `cosign copy` always carries signatures and +attestations. No caller in the org passes it. + +`SKOPEO_ARGS` and the `SRC_PASS`/`DST_PASS` environment variables are removed +from the copy step. `SKOPEO_ARGS` was populated from `inputs.skopeo-additional-args`, +an input this action does not declare, so it always expanded to empty. The two +passwords were what skopeo's `--creds` flags needed; cosign authenticates from +the Docker config that `amazon-ecr-login` writes, and nothing read them. They no +longer enter the step environment. + +Three fixes to the images-matrix path, which was unusable beyond the reference +bug above: + +- The loop read from a pipe, so it ran in a subshell. Its `IMAGE_COUNT` was + discarded and the run always reported `All 0 images copied successfully!`, + and the failure `exit 1` left the subshell rather than the script. It now + reads from a process substitution. +- `sed -i "4i\"` is rejected by GNU sed as `i` with no text after it, so the + summary write failed once copying had succeeded. The total is now inserted by + rewriting the file. The heading it writes was `** Total Images Promoted:**`, + which Markdown does not render as bold; the stray space is gone. +- `action.yaml` described the `images` objects with camelCase keys + (`sourceRepository`), while the script and the README both use snake_case + (`source_repository`). Following the documented casing produced nulls. The + description now matches. + +The `promoted-images` output returns the promotions it promised. It read +`/tmp/promotion-results/promoted-images.json`, a file the script never writes; +results land in `promotion-results.json`. The output was `[]` on every run, +including successful ones. It now reads the `.promotions` array. + +The README's examples were not runnable: they passed underscore input names +(`source_role_arn`) that the action does not declare, all of which are +kebab-case, and pointed `uses:` at a local `./.github/actions/promote-image` +path that does not exist in a consuming repo. diff --git a/actions/promote-image-ecr/README.md b/actions/promote-image-ecr/README.md index aa6ba8c5a..06c2b82cc 100644 --- a/actions/promote-image-ecr/README.md +++ b/actions/promote-image-ecr/README.md @@ -70,21 +70,19 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v4 - - name: Promote image - uses: ./.github/actions/promote-image + uses: smartcontractkit/.github/actions/promote-image-ecr@promote-image-ecr/0.5.0 with: - source_aws_region: eu-west-1 - destination_aws_region: us-east-1 - source_role_arn: arn:aws:iam::111111111111:role/github-actions-ecr-read - destination_role_arn: arn:aws:iam::222222222222:role/github-actions-ecr-write - source_registry: 111111111111.dkr.ecr.eu-west-1.amazonaws.com - destination_registry: 222222222222.dkr.ecr.us-east-1.amazonaws.com - source_repository: my-app - destination_repository: my-app - source_tag: v1.0.0 - destination_tag: v1.0.0 + source-aws-region: eu-west-1 + destination-aws-region: us-east-1 + source-role-arn: arn:aws:iam::111111111111:role/github-actions-ecr-read + destination-role-arn: arn:aws:iam::222222222222:role/github-actions-ecr-write + source-registry: 111111111111.dkr.ecr.eu-west-1.amazonaws.com + destination-registry: 222222222222.dkr.ecr.us-east-1.amazonaws.com + source-repository: my-app + destination-repository: my-app + source-tag: v1.0.0 + destination-tag: v1.0.0 ``` ### Multiple Images Promotion (Matrix) @@ -101,17 +99,15 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v4 - - name: Promote multiple images - uses: ./.github/actions/promote-image + uses: smartcontractkit/.github/actions/promote-image-ecr@promote-image-ecr/0.5.0 with: - source_aws_region: eu-west-1 - destination_aws_region: us-east-1 - source_role_arn: arn:aws:iam::111111111111:role/github-actions-ecr-read - destination_role_arn: arn:aws:iam::222222222222:role/github-actions-ecr-write - source_registry: 111111111111.dkr.ecr.eu-west-1.amazonaws.com - destination_registry: 222222222222.dkr.ecr.us-east-1.amazonaws.com + source-aws-region: eu-west-1 + destination-aws-region: us-east-1 + source-role-arn: arn:aws:iam::111111111111:role/github-actions-ecr-read + destination-role-arn: arn:aws:iam::222222222222:role/github-actions-ecr-write + source-registry: 111111111111.dkr.ecr.eu-west-1.amazonaws.com + destination-registry: 222222222222.dkr.ecr.us-east-1.amazonaws.com images: | [ { @@ -157,21 +153,19 @@ jobs: - { repo: "app2", tag: "v2.0.0" } - { repo: "service-x", tag: "sha-abc123" } steps: - - uses: actions/checkout@v4 - - name: Promote ${{ matrix.image.repo }} - uses: ./.github/actions/promote-image + uses: smartcontractkit/.github/actions/promote-image-ecr@promote-image-ecr/0.5.0 with: - source_aws_region: eu-west-1 - destination_aws_region: us-east-1 - source_role_arn: arn:aws:iam::111111111111:role/github-actions-ecr-read - destination_role_arn: arn:aws:iam::222222222222:role/github-actions-ecr-write - source_registry: 111111111111.dkr.ecr.eu-west-1.amazonaws.com - destination_registry: 222222222222.dkr.ecr.us-east-1.amazonaws.com - source_repository: ${{ matrix.image.repo }} - destination_repository: ${{ matrix.image.repo }} - source_tag: ${{ matrix.image.tag }} - destination_tag: ${{ matrix.image.tag }} + source-aws-region: eu-west-1 + destination-aws-region: us-east-1 + source-role-arn: arn:aws:iam::111111111111:role/github-actions-ecr-read + destination-role-arn: arn:aws:iam::222222222222:role/github-actions-ecr-write + source-registry: 111111111111.dkr.ecr.eu-west-1.amazonaws.com + destination-registry: 222222222222.dkr.ecr.us-east-1.amazonaws.com + source-repository: ${{ matrix.image.repo }} + destination-repository: ${{ matrix.image.repo }} + source-tag: ${{ matrix.image.tag }} + destination-tag: ${{ matrix.image.tag }} ``` ## Copy Tool diff --git a/actions/promote-image-ecr/action.yaml b/actions/promote-image-ecr/action.yaml index 16298d7ad..9ddea8680 100644 --- a/actions/promote-image-ecr/action.yaml +++ b/actions/promote-image-ecr/action.yaml @@ -1,6 +1,6 @@ name: Promote image between ECR registries description: | - Promote an image from one ECR registry to another, using cosign (default) or skopeo. + Promote an image from one ECR registry to another, using cosign. The action will assume the provided IAM roles to access the source and destination registries. inputs: @@ -55,17 +55,11 @@ inputs: required: false images: description: | - JSON array of images to promote. Each object should have: sourceRepository, destinationRepository, sourceTag, destinationTag. - Example: [{"sourceRepository":"app1","destinationRepository":"app1","sourceTag":"v1.0","destinationTag":"v1.0"}] - If provided, this takes precedence over individual sourceRepository/destinationRepository/sourceTag/destinationTag inputs. + JSON array of images to promote. Each object should have: source_repository, destination_repository, source_tag, destination_tag. + Example: [{"source_repository":"app1","destination_repository":"app1","source_tag":"v1.0","destination_tag":"v1.0"}] + If provided, this takes precedence over the individual source-repository/destination-repository/source-tag/destination-tag inputs. required: false default: "" - copy-signatures: - description: - Use cosign to copy images (includes signatures and attestations). Set to - 'false' to use skopeo instead. - required: false - default: "true" outputs: promoted-images: @@ -112,13 +106,9 @@ runs: SOURCE_TAG: ${{ inputs.source-tag }} DESTINATION_TAG: ${{ inputs.destination-tag }} IMAGES_JSON: ${{ inputs.images }} - SKOPEO_ARGS: ${{ inputs.skopeo-additional-args }} SOURCE_AWS_REGION: ${{ inputs.source-aws-region || inputs.aws-region }} DESTINATION_AWS_REGION: ${{ inputs.destination-aws-region || inputs.aws-region }} - SRC_PASS: ${{ steps.src.outputs.password }} - DST_PASS: ${{ steps.dst.outputs.password }} - COPY_SIGNATURES: ${{ inputs.copy-signatures }} ACTION_PATH: ${{ github.action_path }} run: | "${ACTION_PATH}/scripts/promote-images.sh" @@ -144,8 +134,8 @@ runs: if: always() shell: bash run: | - if [ -f /tmp/promotion-results/promoted-images.json ]; then - echo "promoted-images=$(cat /tmp/promotion-results/promoted-images.json)" >> $GITHUB_OUTPUT + if [ -f /tmp/promotion-results/promotion-results.json ]; then + echo "promoted-images=$(jq -c '.promotions' /tmp/promotion-results/promotion-results.json)" >> $GITHUB_OUTPUT else echo "promoted-images=[]" >> $GITHUB_OUTPUT fi diff --git a/actions/promote-image-ecr/scripts/promote-images.sh b/actions/promote-image-ecr/scripts/promote-images.sh index fc76653b7..7ba9958bb 100755 --- a/actions/promote-image-ecr/scripts/promote-images.sh +++ b/actions/promote-image-ecr/scripts/promote-images.sh @@ -7,11 +7,13 @@ mkdir -p /tmp/promotion-results RESULTS_FILE="/tmp/promotion-results/promotion-summary.md" RESULTS_JSON="/tmp/promotion-results/promotion-results.json" -echo "# Image Promotion Results" > "$RESULTS_FILE" -echo "" >> "$RESULTS_FILE" -echo "**Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> "$RESULTS_FILE" -echo "**Copy Tool:** cosign" >> "$RESULTS_FILE" -echo "" >> "$RESULTS_FILE" +{ + echo "# Image Promotion Results" + echo "" + echo "**Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "**Copy Tool:** cosign" + echo "" +} > "$RESULTS_FILE" # Initialize JSON results echo '{"promotions": []}' > "$RESULTS_JSON" @@ -41,21 +43,23 @@ write_markdown_result() { else emoji="❌" fi - echo "### $emoji ${repo}" >> "$RESULTS_FILE" - echo "" >> "$RESULTS_FILE" - echo "- **Source:** \`${repo}:${src_tag}\`" >> "$RESULTS_FILE" - echo "- **Destination:** \`${dst_repo}:${dst_tag}\`" >> "$RESULTS_FILE" - if [[ -n "$src_region" ]]; then - echo "- **Source Region:** \`${src_region}\`" >> "$RESULTS_FILE" - fi - if [[ -n "$dst_region" ]]; then - echo "- **Destination Region:** \`${dst_region}\`" >> "$RESULTS_FILE" - fi - if [[ "$status" == "success" ]]; then - echo "- **Duration:** ${duration}s" >> "$RESULTS_FILE" - fi - echo "- **Status:** ${status^}" >> "$RESULTS_FILE" - echo "" >> "$RESULTS_FILE" + { + echo "### $emoji ${repo}" + echo "" + echo "- **Source:** \`${repo}:${src_tag}\`" + echo "- **Destination:** \`${dst_repo}:${dst_tag}\`" + if [[ -n "$src_region" ]]; then + echo "- **Source Region:** \`${src_region}\`" + fi + if [[ -n "$dst_region" ]]; then + echo "- **Destination Region:** \`${dst_region}\`" + fi + if [[ "$status" == "success" ]]; then + echo "- **Duration:** ${duration}s" + fi + echo "- **Status:** ${status^}" + echo "" + } >> "$RESULTS_FILE" } # Function to append promotion result to JSON @@ -88,19 +92,29 @@ write_promotion_json() { if [[ -n "$IMAGES_JSON" ]]; then # Process multiple images echo "Processing multiple images from matrix..." - echo "" >> "$RESULTS_FILE" - echo "## Promoted Images" >> "$RESULTS_FILE" - echo "" >> "$RESULTS_FILE" + { + echo "" + echo "## Promoted Images" + echo "" + } >> "$RESULTS_FILE" + + if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"$IMAGES_JSON"; then + echo "::error::images input must be a JSON array" + exit 1 + fi + # Read from a process substitution, not a pipe: a piped `while` runs in a + # subshell, so IMAGE_COUNT below would be discarded and the failure `exit 1` + # would leave the subshell rather than the script. IMAGE_COUNT=0 - echo "$IMAGES_JSON" | jq -c '.[]' | while read -r image; do + while read -r image; do SRC_REPO=$(echo "$image" | jq -r '.source_repository') DST_REPO=$(echo "$image" | jq -r '.destination_repository') SRC_TAG=$(echo "$image" | jq -r '.source_tag') DST_TAG=$(echo "$image" | jq -r '.destination_tag') - SRC="docker://${SOURCE_REGISTRY}/${SRC_REPO}:${SRC_TAG}" - DST="docker://${DESTINATION_REGISTRY}/${DST_REPO}:${DST_TAG}" + SRC="${SOURCE_REGISTRY}/${SRC_REPO}:${SRC_TAG}" + DST="${DESTINATION_REGISTRY}/${DST_REPO}:${DST_TAG}" echo "-> Copying ${SRC} to ${DST}" @@ -127,17 +141,22 @@ if [[ -n "$IMAGES_JSON" ]]; then exit 1 fi - done + done < <(jq -c '.[]' <<<"$IMAGES_JSON") echo "----------------------------------------" echo "All ${IMAGE_COUNT} images copied successfully!" - # Add summary at the top - sed -i "3i\\** Total Images Promoted:** ${IMAGE_COUNT}" "$RESULTS_FILE" - sed -i "4i\\" "$RESULTS_FILE" + # Add the total just under the title block. Done by rewriting the file rather + # than with `sed -i "4i\\"`, which GNU sed rejects: `i` with nothing after it. + { + head -n 2 "$RESULTS_FILE" + echo "**Total Images Promoted:** ${IMAGE_COUNT}" + echo "" + tail -n +3 "$RESULTS_FILE" + } > "${RESULTS_FILE}.tmp" && mv "${RESULTS_FILE}.tmp" "$RESULTS_FILE" else # Process single image - SRC="docker://${SOURCE_REGISTRY}/${SOURCE_REPOSITORY}:${SOURCE_TAG}" - DST="docker://${DESTINATION_REGISTRY}/${DESTINATION_REPOSITORY}:${DESTINATION_TAG}" + SRC="${SOURCE_REGISTRY}/${SOURCE_REPOSITORY}:${SOURCE_TAG}" + DST="${DESTINATION_REGISTRY}/${DESTINATION_REPOSITORY}:${DESTINATION_TAG}" echo "-> Copying ${SRC} to ${DST}"