Skip to content

Notify workflow

Runs in the source repo, after an image has been built and pushed. It builds a client_payload describing the image and the commit that produced it, reads .github/deploy_targets.yaml, and sends one repository_dispatch event per target — each authenticated with a fresh installation token scoped to that single repository.

Every target is attempted independently and reported in the job's step summary, so one bad target never blocks the others.

Caller example

jobs:
  notify:
    needs: [shortsha, build_test_push]
    if: ${{ github.repository == 'ioos/buoy_retriever' && github.event_name != 'pull_request' }}
    uses: gulfofmaine/odp-releaser/.github/workflows/notify.yml@<sha>
    permissions:
      contents: read
      pull-requests: read
    with:
      image_name: ghcr.io/ioos/buoy_retriever_hohonu
      tag: ${{ needs.shortsha.outputs.shortsha }}
      digest: ${{ needs.build_test_push.outputs.image_digest }}
      # environment: production                           # optional gate
      # deploy_targets_path: .github/deploy_targets.yaml  # optional
      # verbosity: 1                                       # optional, default
    secrets:
      dispatch_app_id: ${{ secrets.DISPATCH_APP_ID }}
      dispatch_app_private_key: ${{ secrets.DISPATCH_APP_PRIVATE_KEY }}
      # dispatch_apps: ${{ secrets.DISPATCH_APPS }}        # optional multi-org

Note the explicit secrets: block — notify.yml does not support secrets: inherit, since it only ever needs the dispatch credentials named above.

Add an if: to constrain the job to the right repo, branch, and event so forks and unrelated pushes don't dispatch anything — see Security notes.

The protected environment gate

Setting with: environment: runs the job under that GitHub environment, so any protection rules configured there (required reviewers, wait timers, branch restrictions) apply before a single dispatch is sent. Leave it empty to skip gating entirely.

Reference

Notify deploy targets

uses: gulfofmaine/odp-releaser/.github/workflows/notify.yml@<sha-or-tag>
permissions:
  contents: read
  pull-requests: read
with:
  image_name: (1)
  tag: (2)
  digest: (3)
  1. Full image name exactly as your deployment manifests reference it. For Docker Hub this is owner/name (no docker.io/ prefix); other registries must include the registry host (e.g. ghcr.io/owner/my-service). No tag or digest.

  2. Tag the image was published under.

  3. Digest (sha256:...) of the published image. Must be a bare digest -- a value still carrying a repo@ prefix (e.g. from docker inspect's RepoDigests) is rejected.

Inputs: ¤

Name Description Default
image_name ¤

Full image name exactly as your deployment manifests reference it. For Docker Hub this is owner/name (no docker.io/ prefix); other registries must include the registry host (e.g. ghcr.io/owner/my-service). No tag or digest.

tag ¤

Tag the image was published under.

digest ¤

Digest (sha256:...) of the published image. Must be a bare digest -- a value still carrying a repo@ prefix (e.g. from docker inspect's RepoDigests) is rejected.

environment ¤

Optional GitHub environment used to gate the dispatch behind protection rules. Leave empty for no gating.

deploy_targets_path ¤

Path to the deploy-targets YAML file in the calling repo.

.github/deploy_targets.yaml
verbosity ¤

CLI verbosity: 0=warning, 1=info (default), 2 or more=debug. Maps to the CLI's -v/-vv/-vvv flags (capped at 3).

1
Testing aids
dry_run ¤

Resolve dispatch credentials for every target but send no dispatch events. Testing aid; used by this repo's own e2e CI.

false
event_name ¤

Override the GitHub event name used to build the client payload (testing aid). Empty (the default) uses the run's real event. Only push, release, and workflow_dispatch are supported by the payload builder; workflow_dispatch is the simplest override as it needs no event file, token, or pull request lookup.

Secrets: ¤

Name Description
dispatch_app_id ¤

App ID of the default GitHub App used to dispatch to deploy repos. Used for any target owner that dispatch_apps does not cover, so it is required unless dispatch_apps covers every target.

dispatch_app_private_key ¤

Private key of the default dispatch GitHub App. Required whenever dispatch_app_id is set.

dispatch_apps ¤

Optional JSON object mapping owner -> {app_id, private_key} for dispatching across multiple organizations.

Outputs: ¤

Name Description
results ¤

JSON array of per-target dispatch results, each {owner, repo, event_type, ok, detail}.

target_count ¤

Number of deploy targets that were attempted.

Source of gulfofmaine/odp-releaser/.github/workflows/notify.yml@<sha-or-tag>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# Reusable workflow: notify deploy targets of a newly published image.
#
# This workflow is meant to be called cross-repo by an image-building repo
# after it has pushed a container image. It runs the `odp-releaser notify`
# CLI, which reads the calling repo's deploy-targets file and dispatches
# repository_dispatch events (via a GitHub App) to each configured deploy
# repository so they can bump the image to the new tag/digest.
#
# Minimal caller example:
#
#   jobs:
#     notify:
#       uses: gulfofmaine/odp-releaser/.github/workflows/notify.yml@<ref>
#       with:
#         image_name: ghcr.io/gulfofmaine/my-service
#         tag: ${{ github.ref_name }}
#         digest: ${{ steps.build.outputs.digest }}
#         # environment: production                           # optional gate
#         # deploy_targets_path: .github/deploy_targets.yaml  # optional
#         # verbosity: 1                                       # optional, default
#       secrets:
#         dispatch_app_id: ${{ secrets.DISPATCH_APP_ID }}
#         dispatch_app_private_key: ${{ secrets.DISPATCH_APP_PRIVATE_KEY }}
#         # dispatch_apps: ${{ secrets.DISPATCH_APPS }}        # optional multi-org

name: Notify deploy targets

on:
  workflow_call:
    inputs:
      image_name:
        description: >-
          Full image name exactly as your deployment manifests reference it. For
          Docker Hub this is owner/name (no docker.io/ prefix); other registries
          must include the registry host (e.g. ghcr.io/owner/my-service). No tag
          or digest.
        required: true
        type: string
      tag:
        description: Tag the image was published under.
        required: true
        type: string
      digest:
        description: >-
          Digest (sha256:...) of the published image. Must be a bare digest -- a
          value still carrying a repo@ prefix (e.g. from docker inspect's
          RepoDigests) is rejected.
        required: true
        type: string
      environment:
        description: >-
          Optional GitHub environment used to gate the dispatch behind
          protection rules. Leave empty for no gating.
        required: false
        type: string
        default: ""
      deploy_targets_path:
        description: Path to the deploy-targets YAML file in the calling repo.
        required: false
        type: string
        default: .github/deploy_targets.yaml
      verbosity:
        description: >-
          CLI verbosity: 0=warning, 1=info (default), 2 or more=debug. Maps to
          the CLI's -v/-vv/-vvv flags (capped at 3).
        required: false
        type: number
        default: 1
      dry_run: # group: Testing aids
        description: >-
          Resolve dispatch credentials for every target but send no dispatch
          events. Testing aid; used by this repo's own e2e CI.
        required: false
        type: boolean
        default: false
      event_name: # group: Testing aids
        description: >-
          Override the GitHub event name used to build the client payload
          (testing aid). Empty (the default) uses the run's real event. Only
          push, release, and workflow_dispatch are supported by the payload
          builder; workflow_dispatch is the simplest override as it needs no
          event file, token, or pull request lookup.
        required: false
        type: string
        default: ""
    outputs:
      results:
        description: >-
          JSON array of per-target dispatch results, each {owner, repo,
          event_type, ok, detail}.
        value: ${{ jobs.notify.outputs.results }}
      target_count:
        description: Number of deploy targets that were attempted.
        value: ${{ jobs.notify.outputs.target_count }}
    secrets:
      dispatch_app_id:
        description: >-
          App ID of the default GitHub App used to dispatch to deploy repos.
          Used for any target owner that dispatch_apps does not cover, so it is
          required unless dispatch_apps covers every target.
        required: false
      dispatch_app_private_key:
        description:
          Private key of the default dispatch GitHub App. Required whenever
          dispatch_app_id is set.
        required: false
      dispatch_apps:
        description: >-
          Optional JSON object mapping owner -> {app_id, private_key} for
          dispatching across multiple organizations.
        required: false

permissions: {}

jobs:
  notify:
    name: Notify deploy targets
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    permissions:
      contents: read
      pull-requests: read # odp-releaser reads PR metadata for the dispatch payload
    outputs:
      results: ${{ steps.notify.outputs.results }}
      target_count: ${{ steps.notify.outputs.target_count }}
    steps:
      - name: Checkout calling repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Install ODP Releaser
        uses: $/.github/actions/install
        with:
          cache_suffix: odp-releaser-${{ job.workflow_sha }}

      - name: Notify deploy targets
        id: notify
        env:
          IMAGE_NAME: ${{ inputs.image_name }}
          TAG: ${{ inputs.tag }}
          DIGEST: ${{ inputs.digest }}
          DEPLOY_TARGETS_PATH: ${{ inputs.deploy_targets_path }}
          GITHUB_TOKEN: ${{ github.token }}
          DISPATCH_APP_ID: ${{ secrets.dispatch_app_id }}
          DISPATCH_APP_PRIVATE_KEY: ${{ secrets.dispatch_app_private_key }}
          DISPATCH_APPS: ${{ secrets.dispatch_apps }}
          VERBOSITY: ${{ inputs.verbosity }}
          DRY_RUN: ${{ inputs.dry_run }}
          EVENT_NAME_OVERRIDE: ${{ inputs.event_name }}
        run: |
          case "$VERBOSITY" in
            0) FLAGS=() ;;
            1) FLAGS=(-v) ;;
            2) FLAGS=(-vv) ;;
            *) FLAGS=(-vvv) ;;
          esac
          NOTIFY_ARGS=()
          if [ "$DRY_RUN" = "true" ]; then
            NOTIFY_ARGS+=(--dry-run)
          fi
          if [ -n "$EVENT_NAME_OVERRIDE" ]; then
            NOTIFY_ARGS+=(--github-event-name "$EVENT_NAME_OVERRIDE")
          fi
          odp-releaser "${FLAGS[@]}" notify "$IMAGE_NAME" "$TAG" "$DIGEST" \
            "${NOTIFY_ARGS[@]}"

Where the dispatch credentials come from, and how to request them from a deploy org, is covered in GitHub App.