# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# This is the shared release workflow run by both `prod-release.yml` and `dry-run-release.yml'.
# A calling workflow will indicate whether it wants to run this with a prod run or a dry run.

# Allow only one release to run at a time
concurrency:
  group: release-smithy-rs-${{ inputs.dry_run }}
  cancel-in-progress: true

env:
  rust_version: 1.76.0

name: Release smithy-rs
on:
  workflow_call:
    inputs:
      commit_sha:
        description: The SHA of the git commit that you want to release.
        required: true
        type: string
      dry_run:
        description: When true, it only produces release artifacts, but will not cut a release tag in GitHub or publish to crates.io.
        required: true
        type: boolean
      skip_ci:
        description: Skip CI when executing a release
        required: true
        type: boolean
    secrets:
      RELEASE_AUTOMATION_BOT_PAT:
        required: true
      RELEASE_AUTOMATION_BOT_CRATESIO_TOKEN:
        required: true
      CANARY_GITHUB_ACTIONS_ROLE_ARN:
        required: true
      CANARY_STACK_CDK_OUTPUTS_BUCKET_NAME:
        required: true

jobs:
  check-actor-for-prod-run:
    name: Check actor for prod run
    if: inputs.dry_run == false
    runs-on: ubuntu-latest
    env:
      ACTOR: ${{ github.actor }}
    steps:
    - name: Check actor for prod run
      run: |
        set -e

        if [ "${ACTOR}" != "aws-sdk-rust-ci" ]; then
          echo "Error: The current actor is '${ACTOR}' but only 'aws-sdk-rust-ci' is allowed to run a prod release workflow."
          exit 1
        fi

        echo "The current actor is 'aws-sdk-rust-ci', continuing with the workflow."

  # We'll need to build a base image to work against if:
  # - a release was kicked off before the image build step triggered by a push to the release branch/main completed
  # - a dry-run release was kicked off against a feature branch to test automation changes
  # This job will be a no-op if an image had already been built.
  acquire-base-image:
    name: Acquire Base Image
    needs:
    - check-actor-for-prod-run
    # We need `always` here otherwise this job won't run if the previous job has been skipped
    # See https://samanpavel.medium.com/github-actions-conditional-job-execution-e6aa363d2867
    if: |
      always() &&
      (needs.check-actor-for-prod-run.result == 'success' || needs.check-actor-for-prod-run.result == 'skipped')
    runs-on: smithy_ubuntu-latest_16-core
    steps:
    - uses: actions/checkout@v4
      with:
        path: smithy-rs
        ref: ${{ inputs.commit_sha }}
        fetch-depth: 0
    - name: Acquire base image
      id: acquire
      run: ./smithy-rs/.github/scripts/acquire-build-image
    - name: Upload base image
      uses: actions/upload-artifact@v4
      with:
        name: smithy-rs-base-image
        path: smithy-rs-base-image
        retention-days: 1

  release-ci:
    name: Pre-release checks
    # We need `always` here otherwise this job won't run if the previous job has been skipped
    # See https://samanpavel.medium.com/github-actions-conditional-job-execution-e6aa363d2867
    if: always() && inputs.skip_ci == false
    needs:
    - acquire-base-image
    uses: ./.github/workflows/ci.yml
    with:
      run_sdk_examples: false
      git_ref: ${{ inputs.commit_sha }}
    secrets:
      CANARY_GITHUB_ACTIONS_ROLE_ARN: ${{ secrets.CANARY_GITHUB_ACTIONS_ROLE_ARN }}
      CANARY_STACK_CDK_OUTPUTS_BUCKET_NAME: ${{ secrets.CANARY_STACK_CDK_OUTPUTS_BUCKET_NAME }}

  check-semver-hazards:
    name: Check for semver hazards
    needs:
    - acquire-base-image
    # We need `always` here otherwise this job won't run if the previous job has been skipped
    # See https://samanpavel.medium.com/github-actions-conditional-job-execution-e6aa363d2867
    if: always()
    runs-on: smithy_ubuntu-latest_8-core
    steps:
    - uses: actions/checkout@v4
      with:
        path: smithy-rs
        ref: ${{ inputs.commit_sha }}
        fetch-depth: 0
    - uses: actions/checkout@v4
      with:
        repository: awslabs/aws-sdk-rust
        path: aws-sdk-rust
        fetch-depth: 0
    - name: Run check-semver-hazards
      uses: ./smithy-rs/.github/actions/docker-build
      with:
        action: check-semver-hazards

  get-or-create-release-branch:
    name: Get or create a release branch
    needs:
    - acquire-base-image
    - check-semver-hazards
    - release-ci
    # We need `always` here otherwise this job won't run if the previous job has been skipped
    # See https://samanpavel.medium.com/github-actions-conditional-job-execution-e6aa363d2867
    if: |
      always() &&
      needs.acquire-base-image.result == 'success' &&
      (needs.release-ci.result == 'success' || needs.release-ci.result == 'skipped')
    runs-on: ubuntu-latest
    outputs:
      release_branch: ${{ steps.branch-push.outputs.release_branch }}
      new_release_series: ${{ steps.branch-push.outputs.new_release_series }}
    steps:
    - uses: actions/checkout@v4
      with:
        ref: ${{ inputs.commit_sha }}
        token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
        fetch-depth: 0
    - name: Get or create release branch
      id: branch-push
      shell: bash
      env:
        DRY_RUN: ${{ inputs.dry_run }}
      run: |
        set -e

        ./.github/scripts/get-or-create-release-branch.sh output
        cat output > $GITHUB_OUTPUT

  release:
    name: Release
    needs:
    - get-or-create-release-branch
    # See https://github.com/actions/runner/issues/2205#issuecomment-1381988186 for an explanation as to why
    # we need this here _even though_ the job we depend on is never skipped.
    if: |
      always() &&
      !contains(needs.*.result, 'failure') &&
      !contains(needs.*.result, 'cancelled')
    runs-on: ubuntu-latest
    steps:
    - name: Install Rust
      uses: dtolnay/rust-toolchain@master
      with:
        toolchain: ${{ env.rust_version }}
    - name: Checkout smithy-rs
      uses: actions/checkout@v4
      with:
        ref: ${{ inputs.commit_sha }}
        path: smithy-rs
        token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
    - name: Generate release artifacts
      uses: ./smithy-rs/.github/actions/docker-build
      with:
        action: generate-smithy-rs-release
    - name: Download all artifacts
      uses: ./smithy-rs/.github/actions/download-all-artifacts
    - name: Push smithy-rs changes
      shell: bash
      working-directory: smithy-rs-release/smithy-rs
      id: push-changelog
      env:
        RELEASE_BRANCH_NAME: ${{ needs.get-or-create-release-branch.outputs.release_branch }}
        DRY_RUN: ${{ inputs.dry_run }}
      run: |
        if ! git diff-index --quiet HEAD; then
          echo "Pushing release commits..."
          # This will fail if we tried to release from a non-HEAD commit on the release branch.
          # The only scenario where we would try to release a non-HEAD commit from the release branch is
          # to retry a release action execution that failed due to a transient issue.
          # In that case, we expect the commit to be releasable as-is, i.e. the changelog should have already
          # been processed.
          git fetch --unshallow
          if [[ "${DRY_RUN}" == "true" ]]; then
            # During dry-runs, "git push" without "--force" can fail if smithy-rs-release-x.y.z-preview is behind
            # smithy-rs-release-x.y.z, but that does not matter much during dry-runs.
            git push --force origin "HEAD:refs/heads/${RELEASE_BRANCH_NAME}"
            git push --dry-run origin "HEAD:refs/heads/${RELEASE_BRANCH_NAME}"
          else
            echo "attempting to push $(git rev-parse HEAD) to $RELEASE_BRANCH_NAME $(git rev-parse $RELEASE_BRANCH_NAME)"
            git push origin "HEAD:refs/heads/${RELEASE_BRANCH_NAME}"
          fi
        fi
        echo "commit_sha=$(git rev-parse HEAD)" > $GITHUB_OUTPUT
    - name: Tag release
      uses: actions/github-script@v7
      with:
        github-token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
        script: |
          const createReleaseScript = require("./smithy-rs/.github/workflows/release-scripts/create-release.js");
          await createReleaseScript({
            github,
            isDryRun: ${{ inputs.dry_run }},
            releaseManifestPath: "smithy-rs-release/smithy-rs-release-manifest.json",
            releaseCommitish: "${{ steps.push-changelog.outputs.commit_sha }}"
          });
    - name: Publish to crates.io
      shell: bash
      working-directory: smithy-rs-release/crates-to-publish
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.RELEASE_AUTOMATION_BOT_CRATESIO_TOKEN }}
      run: |
        cargo install --path "$(realpath ../smithy-rs/tools/ci-build/publisher)"
        # Verify the publisher tool installed successfully
        publisher --version

        if [[ "${{ inputs.dry_run }}" == "true" ]]; then
          if [[ ! -f aws-smithy-types/Cargo.toml ]]; then
            echo "Crates to publish not found!"
            exit 1
          fi
          echo "Checking cargo auth token..."
          # This version has already been yanked. This command succeeds if we have a token with permission to yank the crate.
          cargo yank aws-sigv4 --version 0.55.0
        else
          publisher publish -y --location .
        fi

  open-backport-pull-request:
    name: Open backport pull request to merge the release branch back to main
    needs:
    - release
    # See https://github.com/actions/runner/issues/2205#issuecomment-1381988186 for details on the workaround
    if: inputs.dry_run == false && always() && needs.release.result == 'success'
    uses: ./.github/workflows/backport-pull-request.yml
    secrets:
      RELEASE_AUTOMATION_BOT_PAT: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
