Skip to content

Commit f6ef7e5

Browse files
clouddrove-cianmolnagpalhahirwar-cd
authored
feat(registry-webhook-sync): Introducing Terraform Registry webhook sync workflow 🚀 (#400)
Co-authored-by: Anmol Nagpal <ianmolnagpal@gmail.com> Co-authored-by: Himanshu Ahirwar <himanshu.ahirwar@clouddrove.com>
1 parent 5a15692 commit f6ef7e5

3 files changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
name: 🔄 Terraform Registry Webhook Sync
3+
4+
# Audits the Terraform Registry webhook on every matching repo in one or more
5+
# orgs and ensures it fires on tag/branch creation+deletion (not just push),
6+
# so new module tags auto-sync to registry.terraform.io without a manual resync.
7+
#
8+
# Root cause this fixes: the registry installs a webhook subscribed to "push"
9+
# only. Tag ingestion keys off the "create" event, so new tags never reach the
10+
# registry and require a manual "Resync Module" click. Adding create+delete to
11+
# the hook makes sync automatic and self-healing for newly published repos.
12+
13+
on:
14+
workflow_call:
15+
inputs:
16+
orgs:
17+
description: 'Comma-separated GitHub orgs to audit. e.g. "clouddrove,org2,org3"'
18+
required: true
19+
type: string
20+
repo_filter:
21+
description: 'Only repos whose name matches this regex are touched.'
22+
required: false
23+
type: string
24+
default: '^terraform-'
25+
required_events:
26+
description: 'Comma-separated webhook events the registry hook must have.'
27+
required: false
28+
type: string
29+
default: 'push,create,delete'
30+
dry_run:
31+
description: 'When true, report drift only; do not modify any webhook.'
32+
required: false
33+
type: boolean
34+
default: false
35+
secrets:
36+
GH_ADMIN_TOKEN:
37+
description: 'PAT with admin:repo_hook + repo (read) scope across the orgs.'
38+
required: true
39+
40+
workflow_dispatch:
41+
inputs:
42+
orgs:
43+
description: 'Comma-separated GitHub orgs to audit.'
44+
required: true
45+
type: string
46+
repo_filter:
47+
description: 'Repo name regex filter.'
48+
required: false
49+
type: string
50+
default: '^terraform-'
51+
required_events:
52+
description: 'Webhook events the registry hook must have.'
53+
required: false
54+
type: string
55+
default: 'push,create,delete'
56+
dry_run:
57+
description: 'Report drift only; do not modify.'
58+
required: false
59+
type: boolean
60+
default: false
61+
62+
permissions:
63+
contents: read
64+
65+
jobs:
66+
sync-webhooks:
67+
name: 🔍 Audit & fix registry webhooks
68+
runs-on: ubuntu-latest
69+
env:
70+
GH_TOKEN: ${{ secrets.GH_ADMIN_TOKEN || github.token }}
71+
ORGS: ${{ inputs.orgs }}
72+
REPO_FILTER: ${{ inputs.repo_filter }}
73+
REQUIRED_EVENTS: ${{ inputs.required_events }}
74+
DRY_RUN: ${{ inputs.dry_run }}
75+
steps:
76+
- name: 🔄 Reconcile webhooks
77+
shell: bash
78+
run: |
79+
set -euo pipefail
80+
81+
# jq array of events the registry hook must contain.
82+
mapfile -t WANT < <(echo "$REQUIRED_EVENTS" | tr ',' '\n' | sed 's/ //g' | grep -v '^$')
83+
WANT_JSON=$(printf '%s\n' "${WANT[@]}" | jq -R . | jq -sc .)
84+
echo "Required events: $WANT_JSON | dry_run=$DRY_RUN"
85+
86+
changed=0; ok=0; missing=0; total=0
87+
summary=""
88+
89+
IFS=',' read -ra ORG_LIST <<< "$ORGS"
90+
for org in "${ORG_LIST[@]}"; do
91+
org=$(echo "$org" | xargs) # trim
92+
[ -z "$org" ] && continue
93+
echo "::group::Org $org"
94+
95+
# All repos in the org matching the filter (paginated).
96+
repos=$(gh api "orgs/$org/repos" --paginate \
97+
--jq ".[] | select(.name|test(\"$REPO_FILTER\")) | .name")
98+
99+
for repo in $repos; do
100+
total=$((total+1))
101+
# Find the registry's webhook on this repo, if any.
102+
hook=$(gh api "repos/$org/$repo/hooks" \
103+
--jq '.[] | select(.config.url // "" | test("registry.terraform.io")) | {id, events}' \
104+
2>/dev/null | jq -sc '.[0] // empty') || hook=""
105+
106+
if [ -z "$hook" ]; then
107+
missing=$((missing+1))
108+
summary+="⚠️ $org/$repo: no registry webhook (publish module on registry first)\n"
109+
continue
110+
fi
111+
112+
id=$(echo "$hook" | jq -r '.id')
113+
have=$(echo "$hook" | jq -c '.events')
114+
# Drift = any required event not already present.
115+
drift=$(jq -nc --argjson have "$have" --argjson want "$WANT_JSON" \
116+
'$want - $have')
117+
118+
if [ "$drift" = "[]" ]; then
119+
ok=$((ok+1))
120+
continue
121+
fi
122+
123+
if [ "$DRY_RUN" = "true" ]; then
124+
summary+="🔸 $org/$repo: hook $id missing $drift (would set $WANT_JSON)\n"
125+
changed=$((changed+1))
126+
continue
127+
fi
128+
129+
# PATCH: set the hook's events to the full required set.
130+
args=(); for e in "${WANT[@]}"; do args+=(-f "events[]=$e"); done
131+
gh api -X PATCH "repos/$org/$repo/hooks/$id" "${args[@]}" >/dev/null
132+
summary+="✅ $org/$repo: hook $id updated -> $WANT_JSON (was missing $drift)\n"
133+
changed=$((changed+1))
134+
done
135+
echo "::endgroup::"
136+
done
137+
138+
echo "## Registry webhook sync" >> "$GITHUB_STEP_SUMMARY"
139+
echo "" >> "$GITHUB_STEP_SUMMARY"
140+
echo "- Repos scanned: $total" >> "$GITHUB_STEP_SUMMARY"
141+
echo "- Already correct: $ok" >> "$GITHUB_STEP_SUMMARY"
142+
echo "- Changed${DRY_RUN:+ (dry-run)}: $changed" >> "$GITHUB_STEP_SUMMARY"
143+
echo "- No registry hook: $missing" >> "$GITHUB_STEP_SUMMARY"
144+
echo "" >> "$GITHUB_STEP_SUMMARY"
145+
echo -e "$summary" >> "$GITHUB_STEP_SUMMARY"
146+
echo -e "$summary"

WORKFLOW_CATALOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Complete index of all available workflows organized by category and use case.
111111
| [yml-lint.yml](./.github/workflows/yml-lint.yml) | YAML linting | Code quality |
112112
| [yml-lint-internal.yml](./.github/workflows/yml-lint-internal.yml) | Internal YAML linting | Internal checks |
113113
| [readme.yml](./.github/workflows/readme.yml) | Generate README | Documentation |
114+
| [registry-webhook-sync.yml](./.github/workflows/registry-webhook-sync.yml) | Fix registry webhooks (push→push,create,delete) so new module tags auto-sync | Registry maintenance |
114115
| [infracost.yml](./.github/workflows/infracost.yml) | Cost estimation | Cost management |
115116
| [sst_workflow.yml](./.github/workflows/sst_workflow.yml) | SST deployment | Serverless |
116117

docs/registry-webhook-sync.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## [Terraform Registry Webhook Sync](https://github.com/clouddrove/github-shared-workflows/blob/master/.github/workflows/registry-webhook-sync.yml)
2+
3+
Keeps the **public Terraform Registry** in sync with new module tags automatically — no more manual *Resync Module* clicks.
4+
5+
### The problem
6+
7+
When the registry publishes a module it installs a GitHub webhook on the repo. By default that hook fires on the **`push`** event only. Registry tag ingestion keys off the **`create`** event, so new tags are never reported and the version only appears after a manual *Resync Module* in the UI.
8+
9+
This workflow audits the registry webhook on every matching repo across one or more orgs and ensures it carries the `push,create,delete` events. New repos self-heal on the next scheduled run.
10+
11+
### Inputs
12+
13+
| Name | Required | Default | Description |
14+
|------|----------|---------|-------------|
15+
| `orgs` | yes || Comma-separated GitHub orgs to audit, e.g. `clouddrove,org2`. |
16+
| `repo_filter` | no | `^terraform-` | Only repos whose name matches this regex are touched. |
17+
| `required_events` | no | `push,create,delete` | Events the registry hook must contain. |
18+
| `dry_run` | no | `false` | Report drift only; make no changes. |
19+
20+
### Secrets
21+
22+
| Name | Required | Description |
23+
|------|----------|-------------|
24+
| `GH_ADMIN_TOKEN` | yes | PAT with `admin:repo_hook` + `repo` (read) scope across the orgs. Fine-grained: Webhooks read/write + Metadata read. |
25+
26+
### Example — scheduled self-healing caller
27+
28+
Drop this in an ops/infra repo (a reusable workflow cannot own the `schedule` trigger itself):
29+
30+
```yaml
31+
name: Registry webhook sync
32+
on:
33+
schedule:
34+
- cron: '0 6 * * 1' # every Monday 06:00 UTC
35+
workflow_dispatch:
36+
inputs:
37+
dry_run:
38+
type: boolean
39+
default: false
40+
jobs:
41+
sync:
42+
uses: clouddrove/github-shared-workflows/.github/workflows/registry-webhook-sync.yml@v2
43+
with:
44+
orgs: 'clouddrove,org2,org3,org4,org5'
45+
dry_run: ${{ inputs.dry_run || false }}
46+
secrets:
47+
GH_ADMIN_TOKEN: ${{ secrets.REGISTRY_HOOK_PAT }}
48+
```
49+
50+
### Notes
51+
52+
- Repos with **no** registry webhook are reported (`⚠️`) but not modified — publish the module on the registry first, then this workflow maintains the hook.
53+
- Run with `dry_run: true` (via `workflow_dispatch`) first to preview drift; the job summary lists every repo and the events it would set.
54+
- This is the supported fix. The registry has **no** public API to force a resync, so fixing the webhook is the only durable, automatable path.

0 commit comments

Comments
 (0)