diff --git a/.github/workflows/notify-consumers.yml b/.github/workflows/notify-consumers.yml new file mode 100644 index 0000000..7ca85fc --- /dev/null +++ b/.github/workflows/notify-consumers.yml @@ -0,0 +1,107 @@ +name: Notify Consumers + +# workflow_run fires on EVERY completion of Main (any branch, any outcome), +# so this workflow only actually runs for: +# - a successful Main build on `main` (the commit-tagged image was just +# published, e.g. a PR merged to main) -> triggered_by=merge, or +# - a manual workflow_dispatch (re-trigger a stuck/skipped pin update) +# -> triggered_by=manual +# A failed Main run, or a successful run on a PR/feature branch, is +# skipped: no image was published for consumers to pin to. + +# To manually re-trigger a consumer pin update, run: +# `gh workflow run "Notify Consumers" --ref main` + +on: # yamllint disable-line rule:truthy + workflow_run: + workflows: ["Main"] + types: [completed] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha || github.sha }} + cancel-in-progress: true + +# Default permissions for all jobs +permissions: {} + +jobs: + determine-inputs: + name: "Determine trigger inputs" + runs-on: ubuntu-24.04 + permissions: + actions: read + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') + outputs: + commit: ${{ steps.inputs.outputs.commit }} + triggered_by: ${{ steps.inputs.outputs.triggered_by }} + run_url: ${{ steps.inputs.outputs.run_url }} + steps: + - name: Determine trigger inputs + id: inputs + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "commit=${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "triggered_by=manual" >> "$GITHUB_OUTPUT" + else + echo "commit=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" + echo "triggered_by=merge" >> "$GITHUB_OUTPUT" + fi + echo "run_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_OUTPUT" + + # A manual dispatch can be run against any ref at any time, including + # before Main has built and published an image for that commit. + - name: Verify image was published for this commit + env: + GH_TOKEN: ${{ github.token }} + COMMIT: ${{ steps.inputs.outputs.commit }} + run: | + conclusion=$(gh api "repos/${{ github.repository }}/actions/runs?head_sha=$COMMIT&status=completed" \ + --jq '[.workflow_runs[] | select(.name == "Main")] | sort_by(.run_started_at) | last | .conclusion // "none"') + if [ "$conclusion" != "success" ]; then + echo "::error::No successful Main run found for commit $COMMIT (found: $conclusion). The image for this commit may not be published yet." + exit 1 + fi + + notify: + name: "Notify ${{ matrix.repo }}" + needs: determine-inputs + runs-on: ubuntu-24.04 + permissions: + id-token: write + strategy: + fail-fast: false + matrix: + include: + # To add a new consumer: add a { repo, policy } entry below, add a + # matching self. dd-octo-sts policy in the consumer repo, + # get a cross-repo dd-octo-sts grant provisioned for images-rb to + # dispatch into it, and make sure the consumer repo actually has a + # workflow listening for the images-updated repository_dispatch + # event. + - repo: DataDog/dd-trace-rb + policy: images-rb.notify-dd-trace-rb + steps: + - name: Get GitHub Token via dd-octo-sts + id: generate-token + uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 + with: + scope: ${{ matrix.repo }} + policy: ${{ matrix.policy }} + + # There appers to be a race condition; wait it out a bit + # See: https://datadoghq.atlassian.net/browse/APMLP-1305 + - name: Wait for token propagation + run: sleep 30 + + - name: Dispatch to ${{ matrix.repo }} + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + run: | + gh api "repos/${{ matrix.repo }}/dispatches" \ + -f event_type=images-updated \ + -f "client_payload[commit]=${{ needs.determine-inputs.outputs.commit }}" \ + -f "client_payload[source_run_url]=${{ needs.determine-inputs.outputs.run_url }}" \ + -f "client_payload[triggered_by]=${{ needs.determine-inputs.outputs.triggered_by }}"