diff --git a/.github/workflows/drift-report.yml b/.github/workflows/drift-report.yml index abb3625b..0af55194 100644 --- a/.github/workflows/drift-report.yml +++ b/.github/workflows/drift-report.yml @@ -3,9 +3,21 @@ # # Diffs the endpoint families of LaunchDarkly's public OpenAPI spec against the # resources registered in the provider, using the curated mapping at -# scripts/driftreport/mapping.yaml. On drift, posts to a private Slack channel +# scripts/driftreport/mapping.yaml. On drift, posts to a PRIVATE Slack channel # via the SLACK_DRIFT_WEBHOOK_URL secret. This repo is PUBLIC, so internal -# drift tracking must not use GitHub issues. +# drift tracking must not use GitHub issues, artifacts, or step-summary detail. +# +# NOTIFICATION MODEL (one Slack message becomes several): +# 1. a summary message (drift counts per category), then +# 2. one message per NEW endpoint family, each carrying the family's paths and +# a one-click link + CLI to dispatch the stage-2 scaffold workflow for it, +# 3. one message per partial family with UNCLAIMED operations (informational — +# stage 2 only scaffolds NEW resources, so these need a manual PR). +# Messages are POSTed individually to a SINGLE generic Slack Workflow Builder +# webhook trigger. That trigger must declare exactly two Text variables — +# `title` and `body` — and a "Send a message" step into the private channel. +# (`body` is Slack mrkdwn: links render.) The stage-3 verify +# workflow reuses the same trigger for its "ready for review" message. name: API drift report on: @@ -42,7 +54,10 @@ jobs: id: drift run: | set +e - /tmp/driftreport -out drift-report.md + # JSON (not md): the notify step builds Slack messages from the + # structured report. The file stays on the ephemeral runner and is + # never uploaded or printed — it carries the internal roadmap. + /tmp/driftreport -format json -out drift-report.json code=$? set -e if [ "$code" -ne 0 ] && [ "$code" -ne 2 ]; then @@ -51,46 +66,100 @@ jobs: fi drift=$([ "$code" -eq 2 ] && echo true || echo false) echo "drift=$drift" >> "$GITHUB_OUTPUT" - # PUBLIC-REPO SAFETY: this run's logs/summary are world-readable, so the - # report body must NOT land here. Summary states only whether drift was - # found; the detail goes to the private Slack channel. drift-report.md - # stays on the ephemeral runner (never uploaded as an artifact). + # PUBLIC-REPO SAFETY: this run's logs/summary are world-readable, so + # only the yes/no fact lands here. Detail goes to the private channel. if [ "$drift" = "true" ]; then - echo "Drift detected — full report sent to the private Slack channel." >> "$GITHUB_STEP_SUMMARY" + echo "Drift detected — detail sent to the private Slack channel." >> "$GITHUB_STEP_SUMMARY" else echo "No drift detected." >> "$GITHUB_STEP_SUMMARY" fi - # Build the Slack payload with jq so the report markdown is correctly - # JSON-escaped (quotes/newlines/backticks can't break the payload). - # Truncated to stay under Slack's ~4000-char webhook-variable limit; - # full detail is one re-dispatch away. - - name: Build Slack payload - id: slack_payload - if: steps.drift.outputs.drift == 'true' - run: | - limit=3500 - report="$(head -c "$limit" drift-report.md)" - if [ "$(wc -c < drift-report.md)" -gt "$limit" ]; then - report="${report}"$'\n\n…(truncated — re-dispatch the workflow for the full report)' - fi - payload="$(jq -nc \ - --arg message "API coverage drift detected — the provider lags behind the public API. Review and decide whether to scaffold." \ - --arg repo "$GITHUB_REPOSITORY" \ - --arg run_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ - --arg report "$report" \ - '{message: $message, repo: $repo, run_url: $run_url, report: $report}')" - { - printf '%s\n' 'payload<> "$GITHUB_OUTPUT" - # Notify a private Slack channel on drift. The Workflow Builder trigger - # behind SLACK_DRIFT_WEBHOOK_URL should declare these Text variables: - # message, repo, run_url, report. + # Post one summary + one-per-family message to the private Slack channel. + # Each POST carries {title, body}; the Workflow Builder trigger renders + # them. PUBLIC-REPO SAFETY: family names / paths / operationIds are the + # internal roadmap — they are passed only to curl, NEVER echoed to the log. - name: Notify Slack on drift if: steps.drift.outputs.drift == 'true' - uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0 - with: - webhook: ${{ secrets.SLACK_DRIFT_WEBHOOK_URL }} - webhook-type: webhook-trigger - payload: ${{ steps.slack_payload.outputs.payload }} + shell: bash + env: + WEBHOOK: ${{ secrets.SLACK_DRIFT_WEBHOOK_URL }} + REPO: ${{ github.repository }} + SERVER: ${{ github.server_url }} + RUN_ID: ${{ github.run_id }} + run: | + JSON=drift-report.json + RUN_URL="$SERVER/$REPO/actions/runs/$RUN_ID" + ACTIONS_URL="$SERVER/$REPO/actions/workflows/scaffold-resource.yml" + LIMIT=3500 + sent=0 + + # post_slack <body>: POST one message to the Workflow Builder + # trigger. Truncates body to Slack's webhook-variable limit. Never + # echoes title/body (roadmap) to the world-readable log. + post_slack() { + local title="$1" body="$2" payload + if [ "${#body}" -gt "$LIMIT" ]; then + body="${body:0:$LIMIT}"$'\n\n…(truncated — re-dispatch for full detail)' + fi + payload="$(jq -nc --arg title "$title" --arg body "$body" '{title: $title, body: $body}')" + curl -sS -f --retry 3 --retry-connrefused --retry-delay 2 \ + -X POST -H 'Content-Type: application/json' -d "$payload" "$WEBHOOK" >/dev/null + sent=$((sent + 1)) + } + + # 1) Summary — one tally line (zero categories dropped) + scaffoldable + # families + run link. The title carries the headline, so the body + # doesn't restate it. + summary_body="$(jq -r --arg run "$RUN_URL" ' + def lbl($n; $one; $many): if $n > 0 then "\($n) \(if $n == 1 then $one else $many end)" else empty end; + ([ lbl(.new_families | length; "new family"; "new families"), + lbl(.scaffoldable_resources | length; "scaffoldable resource"; "scaffoldable resources"), + lbl(.unclaimed_operations | length; "unclaimed op"; "unclaimed ops"), + lbl(.stale_families | length; "stale family"; "stale families"), + lbl(.stale_operations | length; "stale op claim"; "stale op claims"), + lbl(.unmapped_resources | length; "unmapped resource"; "unmapped resources"), + lbl(.registered_candidates | length; "miscurated candidate"; "miscurated candidates") + ] | join(" · ")) + + (([.new_families[].tag] + [.scaffoldable_resources[].name]) as $s + | if ($s | length) > 0 then "\nScaffoldable: " + ($s | join(", ")) else "" end) + + "\n<\($run)|drift run>" + ' "$JSON")" + post_slack "🔎 API coverage drift" "$summary_body" + + # 2) One message per NEW family — scaffoldable via stage 2. Paths inline, + # scaffold link + CLI on one line. + while IFS= read -r fam; do + [ -n "$fam" ] || continue + tag="$(printf '%s' "$fam" | jq -r '.tag')" + paths="$(printf '%s' "$fam" | jq -r '[.paths[] | "`" + . + "`"] | join(" · ")')" + # shellcheck disable=SC2016 # backticks are literal Slack mrkdwn, not command subst + body="$(printf 'No provider resource yet. Paths: %s\n<%s|▶ Scaffold it> · `gh workflow run scaffold-resource.yml -f family='"'"'%s'"'"'`' \ + "$paths" "$ACTIONS_URL" "$tag")" + post_slack "🆕 New API family: $tag" "$body" + done < <(jq -c '.new_families[]' "$JSON") + + # 2b) One message per curated NET-NEW resource inside a partial family — + # scaffoldable via stage 2, scoped to the resource name + its operations + # (the family's existing resources are untouched). + while IFS= read -r sr; do + [ -n "$sr" ] || continue + tag="$(printf '%s' "$sr" | jq -r '.tag')" + name="$(printf '%s' "$sr" | jq -r '.name')" + opids="$(printf '%s' "$sr" | jq -r '[.operations[].operation_id] | join(",")')" + oplist="$(printf '%s' "$sr" | jq -r '[.operations[] | "• `\(.method) \(.path)`"] | join("\n")')" + # shellcheck disable=SC2016 # backticks are literal Slack mrkdwn, not command subst + body="$(printf 'Net-new resource in the `%s` family (existing resources untouched):\n%s\n<%s|▶ Scaffold it> · `gh workflow run scaffold-resource.yml -f family='"'"'%s'"'"' -f resource_name='"'"'%s'"'"' -f operations='"'"'%s'"'"'`' \ + "$tag" "$oplist" "$ACTIONS_URL" "$tag" "$name" "$opids")" + post_slack "🆕 New resource: $name" "$body" + done < <(jq -c '.scaffoldable_resources[]' "$JSON") + + # 3) One message per partial family with UNCLAIMED ops — informational + # (stage 2 only scaffolds NEW resources, so no dispatch link). + while IFS= read -r tag; do + [ -n "$tag" ] || continue + ops="$(jq -r --arg t "$tag" '.unclaimed_operations[] | select(.tag == $t) | "• `\(.method) \(.path)` (`\(.operation_id)`)"' "$JSON")" + # shellcheck disable=SC2016 # backticks are literal Slack mrkdwn, not command subst + body="$(printf 'Modeled, but lagging these ops — extend the existing resource via a manual PR:\n%s' "$ops")" + post_slack "⚠️ Coverage gap: $tag" "$body" + done < <(jq -r '[.unclaimed_operations[].tag] | unique | .[]' "$JSON") + + echo "Posted $sent Slack message(s)." diff --git a/.github/workflows/scaffold-resource.yml b/.github/workflows/scaffold-resource.yml index 6ce9c8d9..63354dd4 100644 --- a/.github/workflows/scaffold-resource.yml +++ b/.github/workflows/scaffold-resource.yml @@ -23,6 +23,14 @@ on: description: "Endpoint family tag from the drift report (e.g. AgentControl)" required: true type: string + resource_name: + description: "Optional: scaffold a single NET-NEW resource with this name (a scaffoldable_resources entry inside a partial family). Blank = scaffold the whole family." + required: false + type: string + operations: + description: "Optional: comma-separated operationIds the net-new resource should cover. Used with resource_name." + required: false + type: string notes: description: "Optional extra instructions for the scaffolding agent" required: false @@ -34,7 +42,7 @@ permissions: jobs: scaffold: - name: Scaffold ${{ inputs.family }} + name: Scaffold ${{ inputs.resource_name || inputs.family }} runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -43,6 +51,14 @@ jobs: run: | echo "::error::Scaffolding agent is disabled (repository variable SCAFFOLD_AGENT_ENABLED != 'true')." exit 1 + - name: Validate scoped inputs + # Scoped mode (resource_name set) needs the operationId list — otherwise + # the agent enters SCOPED MODE with no operations and the whole family + # slice, an ambiguous instruction. Fail fast instead. + if: inputs.resource_name != '' && inputs.operations == '' + run: | + echo "::error::'operations' is required when 'resource_name' is set (scoped mode needs the comma-separated operationId list)." + exit 1 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: preview-v3 # scaffolds always target the v3 line @@ -66,12 +82,11 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} prompt: | - Scaffold a new resource for the LaunchDarkly Terraform provider covering - the "${{ inputs.family }}" API endpoint family. + ${{ inputs.resource_name && format('SCOPED MODE — implement exactly ONE net-new resource named `{0}` for the LaunchDarkly Terraform provider, covering ONLY these operationIds: {1}. It belongs to the partial "{2}" API family; do NOT modify, regenerate, or touch the family''s EXISTING resources, and do not model operations outside that list.', inputs.resource_name, inputs.operations, inputs.family) || format('Scaffold a new resource for the LaunchDarkly Terraform provider covering the "{0}" API endpoint family.', inputs.family) }} Inputs: - - Endpoint summary for the family: ./family-slice.json (repo root). - This is input context only — read it, but do NOT commit it. + - Endpoint summary for the family: ./family-slice.json (repo root). This + is the WHOLE family for context — read it, but do NOT commit it.${{ inputs.resource_name && format(' Model ONLY the operations listed above ({0}); ignore the rest of the family.', inputs.operations) || '' }} - Full OpenAPI spec (for schemas): https://app.launchdarkly.com/api/v2/openapi.json - Read .claude/skills/terraform-provider-add-resource/SKILL.md and its references/patterns.md (both vendored into this repo), and follow that @@ -86,15 +101,14 @@ jobs: Additional notes from the operator: ${{ inputs.notes }} Deliverable: - 1. Create a branch named scaffold/${{ inputs.family }} (lowercased, - non-alphanumerics replaced with '-') off preview-v3. - 2. Implement the resource (and data source if the family has GET-by-key), + 1. Create a branch named scaffold/${{ inputs.resource_name || inputs.family }} + (lowercased, non-alphanumerics replaced with '-') off preview-v3. + 2. Implement the resource (and data source if it has GET-by-key), unit-testable helpers, acceptance tests, docs templates, and provider - registration. Update scripts/driftreport/mapping.yaml to mark the family - covered. + registration. ${{ inputs.resource_name && format('In scripts/driftreport/mapping.yaml, move these operations out of the "{0}" family''s new_resource_candidates into a new entry under its resources (the family stays partial).', inputs.family) || 'Update scripts/driftreport/mapping.yaml to mark the family covered.' }} 3. Run go build ./... and make fmt; fix what they surface. 4. Commit, push the branch, and open a DRAFT pull request against - preview-v3 titled "feat: scaffold ${{ inputs.family }} resource + preview-v3 titled "feat: scaffold ${{ inputs.resource_name || inputs.family }} resource (autogen stage 2)". The PR body must state it is agent-scaffolded and needs human review per stage 3 of the autogen pipeline. Never push to preview-v3 directly and never merge anything. diff --git a/.github/workflows/verify-scaffold.yml b/.github/workflows/verify-scaffold.yml new file mode 100644 index 00000000..374c55ca --- /dev/null +++ b/.github/workflows/verify-scaffold.yml @@ -0,0 +1,306 @@ +# Stage 3 of the autogen pipeline (see .claude/plans/AUTOGEN_PIPELINE.md): +# verify a stage-2 scaffolded draft PR against a REAL LaunchDarkly account +# before a human finishes it. Picks up the draft PR, builds the provider from +# its branch, points terraform at the local build via a dev override, then runs +# an agent that reviews the code and exercises the new resource with real +# `terraform plan`/`apply`, fixing functionality/tests until apply is clean. +# +# The applied example resources are RETAINED (no `terraform destroy`) so a human +# reviewer can inspect them in the LD account. PROJECT-scoped resources live in a +# project keyed `tf-verify-pr<N>` that the pre-clean step deletes at the start of +# each run, so at most one set lingers per PR. ACCOUNT-scoped resources have no +# project to pre-clean, so the agent names them per-run (`tf-verify-pr<N>-run<R>`) +# to avoid colliding with a retained resource from a previous run; those +# accumulate and need manual cleanup. Result posts back as a PR comment AND a +# private-Slack "ready for review" message (same generic title/body webhook as +# the drift report — SLACK_DRIFT_WEBHOOK_URL). +# +# This file lives on `main` only so GitHub registers the workflow_dispatch +# trigger. It checks out the PR's own branch (always a same-repo scaffold/* +# branch off the v3 line), so no ref pin is needed here. +# +# Credentials: the LD access token comes from the same AWS-OIDC -> SSM path the +# acceptance matrix uses (a prod REST token); api_host is forced to prod. +# Kill switch: repository variable VERIFY_AGENT_ENABLED must be "true". +name: Verify scaffolded resource against LaunchDarkly + +on: + workflow_dispatch: + inputs: + pr_number: + description: "Number of the stage-2 draft PR to verify (branch must be scaffold/*)" + required: true + type: string + notes: + description: "Optional extra instructions for the verification agent" + required: false + type: string + +permissions: + contents: write # agent pushes fixes to the PR branch + pull-requests: write # agent posts a review comment + id-token: write # AWS OIDC to read the LD token from SSM + +jobs: + verify: + name: Verify PR #${{ inputs.pr_number }} + runs-on: ubuntu-latest + timeout-minutes: 90 + # Serialize runs per PR so two dispatches don't clobber each other's apply + # / branch push / the shared tf-verify-pr<N> project. Queue, don't cancel. + concurrency: + group: verify-scaffold-pr-${{ inputs.pr_number }} + cancel-in-progress: false + env: + # Pin the install dir so the workflow's `go install` and the agent's later + # rebuilds land in the SAME place the dev override points at — no GOPATH + # drift between subshells (cf. memory feedback_dev_override_gopath). + GOBIN: ${{ github.workspace }}/.gobin + steps: + - name: Check kill switch + if: vars.VERIFY_AGENT_ENABLED != 'true' + run: | + echo "::error::Verification agent is disabled (repository variable VERIFY_AGENT_ENABLED != 'true')." + exit 1 + + # Resolve the PR to its head branch and guard the inputs: same-repo only + # (fork branches must not run with our OIDC/secrets) and scaffold/* only + # (this workflow is for stage-2 output, not arbitrary PRs). + - name: Resolve PR + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ inputs.pr_number }} + REPO: ${{ github.repository }} + run: | + if ! printf '%s' "$PR" | grep -Eq '^[0-9]+$'; then + echo "::error::pr_number must be a positive integer (got '$PR')." + exit 1 + fi + meta="$(gh pr view "$PR" --repo "$REPO" --json headRefName,isCrossRepository,baseRefName,title,url,state)" + cross="$(printf '%s' "$meta" | jq -r '.isCrossRepository')" + state="$(printf '%s' "$meta" | jq -r '.state')" + # Strip CR/newlines: these become step outputs via `key=value` lines, and + # a newline in the free-text PR title (a JSON string can contain one) could + # otherwise inject a second head_branch=… line that wins the duplicate-key + # race and bypasses the scaffold/* guard below. + head="$(printf '%s' "$meta" | jq -r '.headRefName' | tr -d '\r\n')" + url="$(printf '%s' "$meta" | jq -r '.url' | tr -d '\r\n')" + title="$(printf '%s' "$meta" | jq -r '.title' | tr -d '\r\n')" + if [ "$cross" = "true" ]; then + echo "::error::PR #$PR is from a fork; verification runs real applies with our credentials and only supports same-repo PRs." + exit 1 + fi + if [ "$state" != "OPEN" ]; then + echo "::error::PR #$PR is not open (state=$state)." + exit 1 + fi + case "$head" in + scaffold/*) : ;; + *) + echo "::error::PR #$PR head branch '$head' is not a scaffold/* branch." + exit 1 + ;; + esac + { + echo "head_branch=$head" + echo "pr_url=$url" + echo "pr_title=$title" + } >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ steps.pr.outputs.head_branch }} + fetch-depth: 0 + # Keep credentials so the agent can push fixes back to the branch. + persist-credentials: true + + - uses: aws-actions/configure-aws-credentials@f24d7193d98baebaeacc7e2227925dd47cc267f5 # v4.2.0 + with: + audience: https://github.com/launchdarkly + aws-region: us-east-1 + role-to-assume: arn:aws:iam::061661829416:role/github-actions-terraform-provider-launchdarkly + role-session-name: GitHubActionsVerify_run-${{ github.run_id }} + - id: get-launchdarkly-access-token + uses: dkershner6/aws-ssm-getparameters-action@4fcb4872421f387a6c43058473acc1b22443fe13 # v2.0.3 + with: + parameterPairs: | + /global/services/github/terraform-provider/launchdarkly-access-token = LAUNCHDARKLY_ACCESS_TOKEN + - name: Mask the LD token + run: echo "::add-mask::$LAUNCHDARKLY_ACCESS_TOKEN" + + # setup-go AFTER checkout so go-version-file reads the PR branch's go.mod + # (the v3 line pins a newer Go than main). + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: "go.mod" + cache: true + - run: go mod download + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_wrapper: false + + # Build the provider from the PR branch and point terraform at it via a + # dev override, so `terraform plan`/`apply` exercise THIS code with no + # `terraform init` and no registry download. + - name: Build provider and configure dev override + run: | + # `go install` (not `make build`) so a cosmetic gofmt issue — caught + # separately by the PR's own CI — can't block verification. GOBIN (job + # env) fixes the install dir; the dev override points at the same dir. + mkdir -p "$GOBIN" + go install . + echo "Provider installed to $GOBIN" + cat > "$HOME/.terraformrc" <<EOF + provider_installation { + dev_overrides { + "launchdarkly/launchdarkly" = "$GOBIN" + } + direct {} + } + EOF + + # Deterministic prereq cleanup (NOT delegated to the agent): delete any + # leftover project from a previous run of this PR so `apply` starts clean. + # Doing this here — not in the agent — means the agent does not need + # Bash(curl:*) with the live admin token (removes an exfiltration path). + - name: Pre-clean prior verification project + env: + LD_TOKEN: ${{ env.LAUNCHDARKLY_ACCESS_TOKEN }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + code="$(curl -sS -X DELETE \ + -H "Authorization: $LD_TOKEN" \ + "https://app.launchdarkly.com/api/v2/projects/tf-verify-pr$PR_NUMBER" \ + -o /dev/null -w '%{http_code}' || true)" + # 200/204 = deleted, 404 = nothing to clean; anything else is non-fatal + # (the apply will surface a real problem). Don't print bodies. + echo "pre-clean DELETE tf-verify-pr$PR_NUMBER -> HTTP $code" + + - name: Run verification agent + uses: anthropics/claude-code-action@0f97b95b6536c26e5f6bd90faec370d41695beca # v1 + env: + # LAUNCHDARKLY_ACCESS_TOKEN is already in the job process env (the SSM + # action exported it via GITHUB_ENV, masked) and is inherited by the + # agent's shell — same pattern test.yml uses for `make testacc`. Force + # api_host to prod (the token is a prod REST token; the shell default + # may be staging). The provider treats access_token as sensitive. + LAUNCHDARKLY_API_HOST: https://app.launchdarkly.com + TF_IN_AUTOMATION: "1" + PR_NUMBER: ${{ inputs.pr_number }} + PR_URL: ${{ steps.pr.outputs.pr_url }} + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + prompt: | + You are stage 3 (verification) of the LaunchDarkly Terraform provider + autogen pipeline. A stage-2 agent scaffolded a NEW resource on the + currently checked-out branch and opened draft PR #${{ inputs.pr_number }} + (${{ steps.pr.outputs.pr_title }}). Verify it against a REAL LaunchDarkly + account before a human finishes it. + + Environment already prepared for you: + - The provider is built from this branch and installed to $GOBIN. + - ~/.terraformrc has a dev_override for "launchdarkly/launchdarkly" pointing + there, so `terraform plan`/`apply` run THIS build with NO `terraform init`. + - Real LD credentials are in the environment: LAUNCHDARKLY_ACCESS_TOKEN and + LAUNCHDARKLY_API_HOST=https://app.launchdarkly.com. NEVER print the token. + - `terraform` is on PATH. + - A previous `tf-verify-pr${{ inputs.pr_number }}` project (if any) was already + deleted by a workflow step, so the account is clean for a project-scoped apply. + + Do this, in order: + + 1. REVIEW the scaffolded code on this branch. Run `git fetch origin preview-v3` + first, then `git diff origin/preview-v3...HEAD` to see only the scaffold, + for correctness against CLAUDE.md and the vendored playbook at + .claude/skills/terraform-provider-add-resource/ (SKILL.md + references/). + Note real defects (schema/CRUD/helper/test/docs issues), not style nits. + + 2. EXERCISE the resource. Write a minimal terraform config under a temp dir + (e.g. "$RUNNER_TEMP/verify") with a `provider "launchdarkly" {}` block + (it reads creds from the environment), a prerequisite project keyed + `tf-verify-pr${{ inputs.pr_number }}` (use this key so the live resources are + identifiable and the workflow can pre-clean it next run), and the new resource + (plus its data source if one was scaffolded). If the new resource is + account-scoped rather than project-scoped, there is no project for the + workflow to pre-clean, so name its key/name per-run as + `tf-verify-pr${{ inputs.pr_number }}-run${{ github.run_number }}` to avoid + colliding with a resource retained by a previous run. Then: + terraform -chdir="$RUNNER_TEMP/verify" plan -input=false + terraform -chdir="$RUNNER_TEMP/verify" apply -input=false -auto-approve + A second `plan` afterwards MUST be empty (no perpetual diff). Do NOT commit + the scratch config or any terraform.tfstate*. + + 3. FIX as needed. If plan/apply or the empty-diff check fails, fix the resource + implementation/tests/docs on the branch, re-run `go install .` (the dev + override auto-picks up the rebuilt binary), and retry. Where practical, also run the + resource's acceptance test: + TF_ACC=1 TF_ACC_TERRAFORM_PATH="$(command -v terraform)" \ + go test -run <TestAccName> ./launchdarkly/... -v -timeout 30m + Run `make fmt` and `go build ./...` before committing. + + 4. RETAIN the resources. Do NOT run `terraform destroy`. Leave the applied + example resources in the account for human review. + + 5. COMMIT any fixes to this branch (clear conventional-commit message; touch only + files relevant to the fix — never the scratch config or state) and push. + + 6. REPORT. Post a PR comment on #${{ inputs.pr_number }}. This repo is PUBLIC, so + keep the comment to: the verification verdict (did plan/apply succeed? was the + re-plan clean? which tests ran?), any code fixes you made, and the retained + project key `tf-verify-pr${{ inputs.pr_number }}`. Do NOT restate API endpoint + paths / operationIds / roadmap detail beyond what the diff already shows, and + note the LD console link is in the private Slack thread. Use: + gh pr comment ${{ inputs.pr_number }} --body-file <file> + + 7. Write a machine-readable result to `verify-result.json` at the repo root (do NOT + commit it) with exactly these fields: + {"status":"pass"|"fail","project_key":"tf-verify-pr${{ inputs.pr_number }}", + "ld_url":"https://app.launchdarkly.com/projects/tf-verify-pr${{ inputs.pr_number }}", + "summary":"<one or two sentences: what you verified and any fixes>"} + status is "pass" only if apply succeeded AND the re-plan was clean. + + Operator notes: ${{ inputs.notes }} + # No Bash(curl:*): prereq cleanup is a workflow step, so the agent never + # needs raw HTTP with the live admin token (closes an exfil path). git is + # scoped to the subcommands the flow needs (no `git config` token read). + claude_args: | + --allowedTools "Read,Edit,Write,Glob,Grep,Bash(go:*),Bash(make:*),Bash(git fetch:*),Bash(git diff:*),Bash(git log:*),Bash(git status:*),Bash(git add:*),Bash(git commit:*),Bash(git push:*),Bash(gh pr comment:*),Bash(gh pr view:*),Bash(gofmt:*),Bash(terraform:*),WebFetch(domain:app.launchdarkly.com)" + + # Deterministic, log-safe Slack notification. Reads the agent's result file + # if present; otherwise reports the run errored (no internal detail leaks). + - name: Notify Slack + # Only notify once we got past the input guards (so a disabled kill + # switch or a rejected PR doesn't ping the channel). Runs whether the + # agent step passed or failed. + if: always() && steps.pr.outcome == 'success' + shell: bash + env: + WEBHOOK: ${{ secrets.SLACK_DRIFT_WEBHOOK_URL }} + PR_URL: ${{ steps.pr.outputs.pr_url }} + PR_NUMBER: ${{ inputs.pr_number }} + REPO: ${{ github.repository }} + SERVER: ${{ github.server_url }} + RUN_ID: ${{ github.run_id }} + run: | + RUN_URL="$SERVER/$REPO/actions/runs/$RUN_ID" + if [ -f verify-result.json ] && jq -e . verify-result.json >/dev/null 2>&1; then + status="$(jq -r '.status // "unknown"' verify-result.json)" + ld_url="$(jq -r '.ld_url // ""' verify-result.json)" + summary="$(jq -r '.summary // ""' verify-result.json)" + project="$(jq -r '.project_key // ""' verify-result.json)" + if [ "$status" = "pass" ]; then icon="✅"; else icon="⚠️"; fi + title="$icon Scaffold PR #$PR_NUMBER verified ($status)" + # shellcheck disable=SC2016 # backticks are literal Slack mrkdwn, not command subst + body="$(printf '%s\nRetained in `%s`: <%s|open in LaunchDarkly> · <%s|PR #%s>' \ + "$summary" "$project" "$ld_url" "$PR_URL" "$PR_NUMBER")" + else + title="❌ Scaffold PR #$PR_NUMBER verification did not complete" + body="$(printf 'Stage-3 verification produced no result file — the run likely errored. See the <%s|workflow run> and <%s|PR #%s>.' \ + "$RUN_URL" "$PR_URL" "$PR_NUMBER")" + fi + payload="$(jq -nc --arg title "$title" --arg body "$body" '{title: $title, body: $body}')" + curl -sS -f --retry 3 --retry-connrefused --retry-delay 2 \ + -X POST -H 'Content-Type: application/json' -d "$payload" "$WEBHOOK" >/dev/null + echo "Posted verification result to Slack." diff --git a/launchdarkly/integration_configs_generated.go b/launchdarkly/integration_configs_generated.go index 3227b008..f1276fc0 100644 --- a/launchdarkly/integration_configs_generated.go +++ b/launchdarkly/integration_configs_generated.go @@ -395,6 +395,14 @@ var SUBSCRIPTION_CONFIGURATION_FIELDS = map[string]IntegrationConfig{ IsSecret: false, Type: "string", }, + "url": { + AllowedValues: []string{}, + DefaultValue: "", + Description: "A link to the user's installation page on vercel.com", + IsOptional: true, + IsSecret: false, + Type: "string", + }, }, }