# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-certificates.md
name: Check Certificates

# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
  create:
  push:
    paths:
      - '.github/workflows/check-certificates.ya?ml'
  pull_request:
    paths:
      - '.github/workflows/check-certificates.ya?ml'
  schedule:
    # Run every 10 hours.
    - cron: '0 */10 * * *'
  workflow_dispatch:
  repository_dispatch:

env:
  # Begin notifications when there are less than this many days remaining before expiration.
  EXPIRATION_WARNING_PERIOD: 30

jobs:
  run-determination:
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.determination.outputs.result }}
    steps:
      - name: Determine if the rest of the workflow should run
        id: determination
        run: |
          RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
          REPO_SLUG="arduino/arduino-ide"
          if [[
            (
              # Only run on branch creation when it is a release branch.
              # The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
              "${{ github.event_name }}" != "create" ||
              "${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
            ) &&
            (
              # Only run when the workflow will have access to the certificate secrets.
              # This could be done via a GitHub Actions workflow conditional, but makes more sense to do it here as well.
              (
                "${{ github.event_name }}" != "pull_request" &&
                "${{ github.repository }}" == "$REPO_SLUG"
              ) ||
              (
                "${{ github.event_name }}" == "pull_request" &&
                "${{ github.event.pull_request.head.repo.full_name }}" == "$REPO_SLUG"
              )
            )
          ]]; then
            # Run the other jobs.
            RESULT="true"
          else
            # There is no need to run the other jobs.
            RESULT="false"
          fi

          echo "result=$RESULT" >> $GITHUB_OUTPUT

  check-certificates:
    name: ${{ matrix.certificate.identifier }}
    needs: run-determination
    if: needs.run-determination.outputs.result == 'true'
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false

      matrix:
        certificate:
          # Additional certificate definitions can be added to this list.
          - identifier: macOS signing certificate # Text used to identify certificate in notifications.
            certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate.
            password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password.
            type: pkcs12
          - identifier: Windows signing certificate
            certificate-secret: INSTALLER_CERT_WINDOWS_CER
            # The password for the Windows certificate is not needed, because its not a container, but a single certificate.
            type: x509

    steps:
      - name: Set certificate path environment variable
        run: |
          # See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
          echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"

      - name: Decode certificate
        env:
          CERTIFICATE: ${{ secrets[matrix.certificate.certificate-secret] }}
        run: |
          echo "${{ env.CERTIFICATE }}" | base64 --decode > "${{ env.CERTIFICATE_PATH }}"

      - name: Verify certificate
        env:
          CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
        run: |
          (
            openssl ${{ matrix.certificate.type }} \
              -in "${{ env.CERTIFICATE_PATH }}" \
              -legacy \
              -noout \
              -passin env:CERTIFICATE_PASSWORD
          ) || (
            echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
            exit 1
          )

      - name: Slack notification of certificate verification failure
        if: failure()
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_MESSAGE: |
            :warning::warning::warning::warning:
            WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
            :warning::warning::warning::warning:
          SLACK_COLOR: danger
          MSG_MINIMAL: true
        uses: rtCamp/action-slack-notify@v2

      - name: Get days remaining before certificate expiration date
        env:
          CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
        id: get-days-before-expiration
        run: |
          if [[ ${{ matrix.certificate.type }} == "pkcs12" ]]; then
            EXPIRATION_DATE="$(
              (
                openssl pkcs12 \
                  -in "${{ env.CERTIFICATE_PATH }}" \
                  -clcerts \
                  -legacy \
                  -nodes \
                  -passin env:CERTIFICATE_PASSWORD
              ) | (
                openssl x509 \
                  -noout \
                  -enddate
              ) | (
                grep \
                  --max-count=1 \
                  --only-matching \
                  --perl-regexp \
                  'notAfter=(\K.*)'
              )
            )"
          elif [[ ${{ matrix.certificate.type }} == "x509" ]]; then
            EXPIRATION_DATE="$(
                (
                openssl x509 \
                    -in ${{ env.CERTIFICATE_PATH }} \
                    -noout \
                    -enddate
                ) | (
                grep \
                    --max-count=1 \
                    --only-matching \
                    --perl-regexp \
                    'notAfter=(\K.*)'
                )
            )"
          fi

          DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"

          # Display the expiration information in the log.
          echo "Certificate expiration date: $EXPIRATION_DATE"
          echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"

          echo "days=$DAYS_BEFORE_EXPIRATION" >> $GITHUB_OUTPUT

      - name: Check if expiration notification period has been reached
        id: check-expiration
        run: |
          if [[ ${{ steps.get-days-before-expiration.outputs.days }} -lt ${{ env.EXPIRATION_WARNING_PERIOD }} ]]; then
            echo "::error::${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!"
            exit 1
          fi

      - name: Slack notification of pending certificate expiration
        # Don't send spurious expiration notification if verification fails.
        if: failure() && steps.check-expiration.outcome == 'failure'
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_MESSAGE: |
            :warning::warning::warning::warning:
            WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
            :warning::warning::warning::warning:
          SLACK_COLOR: danger
          MSG_MINIMAL: true
        uses: rtCamp/action-slack-notify@v2
