Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions .github/workflows/registry-webhook-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
name: πŸ”„ Terraform Registry Webhook Sync

# Audits the Terraform Registry webhook on every matching repo in one or more
# orgs and ensures it fires on tag/branch creation+deletion (not just push),
# so new module tags auto-sync to registry.terraform.io without a manual resync.
#
# Root cause this fixes: the registry installs a webhook subscribed to "push"
# only. Tag ingestion keys off the "create" event, so new tags never reach the
# registry and require a manual "Resync Module" click. Adding create+delete to
# the hook makes sync automatic and self-healing for newly published repos.

on:
workflow_call:
inputs:
orgs:
description: 'Comma-separated GitHub orgs to audit. e.g. "clouddrove,org2,org3"'
required: true
type: string
repo_filter:
description: 'Only repos whose name matches this regex are touched.'
required: false
type: string
default: '^terraform-'
required_events:
description: 'Comma-separated webhook events the registry hook must have.'
required: false
type: string
default: 'push,create,delete'
dry_run:
description: 'When true, report drift only; do not modify any webhook.'
required: false
type: boolean
default: false
secrets:
GH_ADMIN_TOKEN:
description: 'PAT with admin:repo_hook + repo (read) scope across the orgs.'
required: true

workflow_dispatch:
inputs:
orgs:
description: 'Comma-separated GitHub orgs to audit.'
required: true
type: string
repo_filter:
description: 'Repo name regex filter.'
required: false
type: string
default: '^terraform-'
required_events:
description: 'Webhook events the registry hook must have.'
required: false
type: string
default: 'push,create,delete'
dry_run:
description: 'Report drift only; do not modify.'
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
sync-webhooks:
name: πŸ” Audit & fix registry webhooks
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_ADMIN_TOKEN || github.token }}
ORGS: ${{ inputs.orgs }}
REPO_FILTER: ${{ inputs.repo_filter }}
REQUIRED_EVENTS: ${{ inputs.required_events }}
DRY_RUN: ${{ inputs.dry_run }}
steps:
- name: πŸ”„ Reconcile webhooks
shell: bash
run: |
set -euo pipefail

# jq array of events the registry hook must contain.
mapfile -t WANT < <(echo "$REQUIRED_EVENTS" | tr ',' '\n' | sed 's/ //g' | grep -v '^$')
WANT_JSON=$(printf '%s\n' "${WANT[@]}" | jq -R . | jq -sc .)
echo "Required events: $WANT_JSON | dry_run=$DRY_RUN"

changed=0; ok=0; missing=0; total=0
summary=""

IFS=',' read -ra ORG_LIST <<< "$ORGS"
for org in "${ORG_LIST[@]}"; do
org=$(echo "$org" | xargs) # trim
[ -z "$org" ] && continue
echo "::group::Org $org"

# All repos in the org matching the filter (paginated).
repos=$(gh api "orgs/$org/repos" --paginate \
--jq ".[] | select(.name|test(\"$REPO_FILTER\")) | .name")

for repo in $repos; do
total=$((total+1))
# Find the registry's webhook on this repo, if any.
hook=$(gh api "repos/$org/$repo/hooks" \
--jq '.[] | select(.config.url // "" | test("registry.terraform.io")) | {id, events}' \
2>/dev/null | jq -sc '.[0] // empty') || hook=""

if [ -z "$hook" ]; then
missing=$((missing+1))
summary+="⚠️ $org/$repo: no registry webhook (publish module on registry first)\n"
continue
fi

id=$(echo "$hook" | jq -r '.id')
have=$(echo "$hook" | jq -c '.events')
# Drift = any required event not already present.
drift=$(jq -nc --argjson have "$have" --argjson want "$WANT_JSON" \
'$want - $have')

if [ "$drift" = "[]" ]; then
ok=$((ok+1))
continue
fi

if [ "$DRY_RUN" = "true" ]; then
summary+="πŸ”Έ $org/$repo: hook $id missing $drift (would set $WANT_JSON)\n"
changed=$((changed+1))
continue
fi

# PATCH: set the hook's events to the full required set.
args=(); for e in "${WANT[@]}"; do args+=(-f "events[]=$e"); done
gh api -X PATCH "repos/$org/$repo/hooks/$id" "${args[@]}" >/dev/null
summary+="βœ… $org/$repo: hook $id updated -> $WANT_JSON (was missing $drift)\n"
changed=$((changed+1))
done
echo "::endgroup::"
done

echo "## Registry webhook sync" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- Repos scanned: $total" >> "$GITHUB_STEP_SUMMARY"
echo "- Already correct: $ok" >> "$GITHUB_STEP_SUMMARY"
echo "- Changed${DRY_RUN:+ (dry-run)}: $changed" >> "$GITHUB_STEP_SUMMARY"
echo "- No registry hook: $missing" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo -e "$summary" >> "$GITHUB_STEP_SUMMARY"
echo -e "$summary"
1 change: 1 addition & 0 deletions WORKFLOW_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Complete index of all available workflows organized by category and use case.
| [yml-lint.yml](./.github/workflows/yml-lint.yml) | YAML linting | Code quality |
| [yml-lint-internal.yml](./.github/workflows/yml-lint-internal.yml) | Internal YAML linting | Internal checks |
| [readme.yml](./.github/workflows/readme.yml) | Generate README | Documentation |
| [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 |
| [infracost.yml](./.github/workflows/infracost.yml) | Cost estimation | Cost management |
| [sst_workflow.yml](./.github/workflows/sst_workflow.yml) | SST deployment | Serverless |

Expand Down
54 changes: 54 additions & 0 deletions docs/registry-webhook-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## [Terraform Registry Webhook Sync](https://github.com/clouddrove/github-shared-workflows/blob/master/.github/workflows/registry-webhook-sync.yml)

Keeps the **public Terraform Registry** in sync with new module tags automatically β€” no more manual *Resync Module* clicks.

### The problem

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.

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.

### Inputs

| Name | Required | Default | Description |
|------|----------|---------|-------------|
| `orgs` | yes | – | Comma-separated GitHub orgs to audit, e.g. `clouddrove,org2`. |
| `repo_filter` | no | `^terraform-` | Only repos whose name matches this regex are touched. |
| `required_events` | no | `push,create,delete` | Events the registry hook must contain. |
| `dry_run` | no | `false` | Report drift only; make no changes. |

### Secrets

| Name | Required | Description |
|------|----------|-------------|
| `GH_ADMIN_TOKEN` | yes | PAT with `admin:repo_hook` + `repo` (read) scope across the orgs. Fine-grained: Webhooks read/write + Metadata read. |

### Example β€” scheduled self-healing caller

Drop this in an ops/infra repo (a reusable workflow cannot own the `schedule` trigger itself):

```yaml
name: Registry webhook sync
on:
schedule:
- cron: '0 6 * * 1' # every Monday 06:00 UTC
workflow_dispatch:
inputs:
dry_run:
type: boolean
default: false
jobs:
sync:
uses: clouddrove/github-shared-workflows/.github/workflows/registry-webhook-sync.yml@v2
with:
orgs: 'clouddrove,org2,org3,org4,org5'
dry_run: ${{ inputs.dry_run || false }}
secrets:
GH_ADMIN_TOKEN: ${{ secrets.REGISTRY_HOOK_PAT }}
```

### Notes

- Repos with **no** registry webhook are reported (`⚠️`) but not modified β€” publish the module on the registry first, then this workflow maintains the hook.
- Run with `dry_run: true` (via `workflow_dispatch`) first to preview drift; the job summary lists every repo and the events it would set.
- 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.
Loading