Skip to content
Open
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
215 changes: 215 additions & 0 deletions .github/workflows/sync-terraform-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Keeps packages/docs-site/src/generated/terraform-docs.json in sync with the latest stable
# Cloudflare Terraform provider.
#
# Security model:
# - `generate` handles untrusted third-party sources. It has read-only permissions, no secrets and
# no persisted git credentials. It clones the provider and the cloudflare-go modules its go.mod
# requires as plain git data at release tags, and runs the offline generator. The generator
# doesn't download or execute anything, and it refuses symlinks and paths outside each checkout.
# Only the generated JSON leaves the job, as an artifact.
# - `pull-request` never sees third-party sources. It commits only the generated file to a bot
# branch, opens or updates a PR, and merges it only once every check on it has passed. The
# generated file is data that the site renders as escaped text, and the recorded commit SHAs
# keep every merged update traceable to its sources.
# - Third-party actions are pinned to commit SHAs. Inputs reach shell scripts only through `env`
# and are validated.
name: Sync Terraform docs

on:
schedule:
- cron: '17 6 * * *' # every day at 06:17 UTC
workflow_dispatch:
inputs:
provider_version:
description: Provider version (x.y.z). Defaults to the latest stable version on the Terraform registry.
required: false
type: string

permissions: {}

concurrency:
group: sync-terraform-docs
cancel-in-progress: false

env:
OUTPUT: packages/docs-site/src/generated/terraform-docs.json

jobs:
generate:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
changed: ${{ steps.versions.outputs.changed }}
current: ${{ steps.versions.outputs.current }}
target: ${{ steps.versions.outputs.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Resolve versions
id: versions
env:
INPUT_PROVIDER: ${{ inputs.provider_version }}
run: |
set -euo pipefail
current="$(jq -r '.source.provider.version // empty' "$OUTPUT")"
# The registry, not GitHub tags: a tag can exist days before the release is published.
target="${INPUT_PROVIDER:-$(
curl -fsSL --proto '=https' https://registry.terraform.io/v1/providers/cloudflare/cloudflare/versions \
| jq -r '.versions[].version' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1
)}"
if ! [[ "$target" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid provider version: $target"; exit 1
fi
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "target=$target" >> "$GITHUB_OUTPUT"
echo "changed=$([ "$current" != "$target" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "::notice::Terraform provider $current -> $target"

- name: Fetch pinned sources (data only)
if: steps.versions.outputs.changed == 'true'
env:
TARGET: ${{ steps.versions.outputs.target }}
run: |
set -euo pipefail
sources="$RUNNER_TEMP/terraform-sources"
mkdir -p "$sources"
clone() { # <repository> <tag> <directory>
git -c advice.detachedHead=false -c core.symlinks=false \
clone --quiet --depth 1 --no-tags --branch "$2" "https://github.com/cloudflare/$1" "$3"
echo "::notice::$1 $2 = $(git -C "$3" rev-parse HEAD)"
}
clone terraform-provider-cloudflare "v$TARGET" "$sources/provider"
args=(--provider-dir "$sources/provider")
while read -r module version; do
[[ "$module" =~ ^github\.com/cloudflare/cloudflare-go/v[0-9]+$ ]] || { echo "::error::Unexpected module $module"; exit 1; }
[[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Unsupported version $module $version"; exit 1; }
clone cloudflare-go "$version" "$sources/${module##*/}"
args+=(--sdk-dir "$module=$sources/${module##*/}")
done < <(grep -E '^\s*github\.com/cloudflare/cloudflare-go/v[0-9]+ ' "$sources/provider/go.mod" | awk '{print $1, $2}')
printf '%s\n' "${args[@]}" > "$RUNNER_TEMP/generator-args"

- uses: ./.github/actions/setup-workspace
if: steps.versions.outputs.changed == 'true'

- name: Generate
if: steps.versions.outputs.changed == 'true'
run: |
set -euo pipefail
mapfile -t args < "$RUNNER_TEMP/generator-args"
pnpm --filter docs-site generate:terraform-docs "${args[@]}"

- name: Test
if: steps.versions.outputs.changed == 'true'
run: pnpm --filter docs-site test

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: steps.versions.outputs.changed == 'true'
with:
name: terraform-docs
path: ${{ env.OUTPUT }}
if-no-files-found: error
retention-days: 7

pull-request:
needs: generate
if: needs.generate.outputs.changed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: terraform-docs
path: ${{ runner.temp }}/terraform-docs

- name: Apply artifact
run: |
set -euo pipefail
jq -e '.format == 2' "$RUNNER_TEMP/terraform-docs/terraform-docs.json" > /dev/null
cp "$RUNNER_TEMP/terraform-docs/terraform-docs.json" "$OUTPUT"

- name: Open or update pull request
id: pr
env:
# PRs opened with GITHUB_TOKEN don't trigger CI; set DOCS_SYNC_TOKEN (GitHub App or
# fine-grained PAT limited to this repository) so checks run on the bot PR.
GH_TOKEN: ${{ secrets.DOCS_SYNC_TOKEN || github.token }}
BASE: ${{ github.event.repository.default_branch }}
BRANCH: bot/sync-terraform-docs
CURRENT: ${{ needs.generate.outputs.current }}
TARGET: ${{ needs.generate.outputs.target }}
run: |
set -euo pipefail
if git diff --quiet -- "$OUTPUT"; then
echo "::notice::$OUTPUT is already up to date"
exit 0
fi

# Only the generated file is committed; the branch is rebuilt from the base every run.
gh auth setup-git
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git switch --create "$BRANCH"
git add -- "$OUTPUT"
git commit --quiet --message "chore(docs): sync Terraform docs to provider v$TARGET"
git push --force --quiet origin "$BRANCH"

title="chore(docs): sync Terraform docs (provider v$TARGET)"
body="$(cat <<EOF
Automated by \`.github/workflows/sync-terraform-docs.yml\`.

- Provider: \`$CURRENT\` → \`$TARGET\` ([release notes](https://github.com/cloudflare/terraform-provider-cloudflare/releases/tag/v$TARGET))
- Source commits are recorded under \`source\` in the generated file.
- All provider SDK calls resolved; docs-site tests passed.
EOF
)"
number="$(gh pr list --head "$BRANCH" --base "$BASE" --state open --json number --jq '.[0].number // empty')"
if [ -n "$number" ]; then
gh pr edit "$number" --title "$title" --body "$body"
echo "::notice::Updated pull request #$number"
else
url="$(gh pr create --head "$BRANCH" --base "$BASE" --title "$title" --body "$body")"
number="${url##*/}"
echo "::notice::Opened pull request #$number"
fi
echo "number=$number" >> "$GITHUB_OUTPUT"
echo "head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

# Merges only when every check on the PR's head commit has passed. Nothing is merged if any
# check fails, if no checks ever appear (e.g. the PR was opened with GITHUB_TOKEN, which doesn't
# trigger CI), or if the branch moved after the checks ran.
- name: Merge when all checks pass
if: steps.pr.outputs.number != ''
timeout-minutes: 50
env:
GH_TOKEN: ${{ secrets.DOCS_SYNC_TOKEN || github.token }}
NUMBER: ${{ steps.pr.outputs.number }}
HEAD_SHA: ${{ steps.pr.outputs.head }}
run: |
set -euo pipefail
checks() { gh pr view "$NUMBER" --json statusCheckRollup --jq '.statusCheckRollup | length'; }
count=0
for _ in $(seq 1 20); do
count="$(checks)"
[ "$count" -gt 0 ] && break
sleep 15
done
if [ "$count" -eq 0 ]; then
echo "::warning::No checks reported on #$NUMBER; leaving it open for manual review"
exit 0
fi
sleep 30 # let workflows that register later attach their checks too

# Exits non-zero on the first failing check, which fails this job and leaves the PR open.
gh pr checks "$NUMBER" --watch --fail-fast --interval 30
gh pr merge "$NUMBER" --squash --match-head-commit "$HEAD_SHA"
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf",
"ignorePatterns": ["**/_generated/**", "dist", ".wrangler"]
"ignorePatterns": ["**/_generated/**", "packages/docs-site/src/generated", "dist", ".wrangler"]
}
33 changes: 33 additions & 0 deletions packages/docs-site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,39 @@ and method metadata defines each public product, resource hierarchy, and method
route. OpenAPI tags remain internal ownership metadata and do not appear in
SDK-backed URLs.

### Terraform

The Terraform target is built from `src/generated/terraform-docs.json`. That file is generated
from local checkouts of the released Cloudflare Terraform provider and the cloudflare-go modules
it imports:

```sh
git clone --depth 1 --branch v5.27.0 https://github.com/cloudflare/terraform-provider-cloudflare /tmp/tf/provider
git clone --depth 1 --branch v7.12.0 https://github.com/cloudflare/cloudflare-go /tmp/tf/sdk-v7 # versions from the provider's go.mod
git clone --depth 1 --branch v6.10.0 https://github.com/cloudflare/cloudflare-go /tmp/tf/sdk-v6

pnpm generate:terraform-docs --provider-dir /tmp/tf/provider \
--sdk-dir github.com/cloudflare/cloudflare-go/v7=/tmp/tf/sdk-v7 \
--sdk-dir github.com/cloudflare/cloudflare-go/v6=/tmp/tf/sdk-v6 # add --check to verify instead
```

The generator is offline and read-only. It doesn't download, spawn or execute anything (a test
enforces this). It reads checkouts through `scripts/source-tree.ts`, which refuses symbolic links
and paths outside each checkout. It reads:

- `docs/{resources,data-sources}/*.md` (tfplugindocs output, the same content as the Terraform
Registry) for descriptions and Required / Optional / Read-only attributes;
- `examples/` for HCL and import syntax;
- `internal/services/*` client calls plus cloudflare-go `api.md` to link each declaration to the
API operations it calls (create, read, update, delete, import);
- `internal/version.go` and `.git/HEAD` to record the versions and commits used.

The parsers are strict. Unexpected Markdown fails with `file:line`, and so does an SDK call that
can't be resolved. `.github/workflows/sync-terraform-docs.yml` runs daily and opens a PR when a new
provider release is published. Untrusted sources are only handled in a job with read-only
permissions and no secrets. `src/terraform-extension.ts` attaches declarations to OpenAPI
operations and warns during builds about declarations that match no operation.

## Project ownership

- `src/content.config.ts` selects OpenAPI sources, discovers SDK products and ownership sections, configures snapshots and execution targets, and registers the canonical, route-neutral API collection.
Expand Down
3 changes: 2 additions & 1 deletion packages/docs-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"build": "astro build",
"preview": "astro preview",
"check": "astro check",
"test": "node --experimental-strip-types --test src/*.test.ts"
"generate:terraform-docs": "node --experimental-strip-types scripts/generate-terraform-docs.ts",
"test": "node --experimental-strip-types --test src/*.test.ts scripts/*.test.mjs"
},
"dependencies": {
"@astrojs/cloudflare": "^14.2.5",
Expand Down
Loading
Loading