Skip to content

feat: release infra + prune orphans on apply (closes #3)#4

Merged
intel352 merged 7 commits into
mainfrom
chore/release-infra
May 20, 2026
Merged

feat: release infra + prune orphans on apply (closes #3)#4
intel352 merged 7 commits into
mainfrom
chore/release-infra

Conversation

@intel352
Copy link
Copy Markdown
Contributor

@intel352 intel352 commented May 20, 2026

Summary

Adds GoReleaser v2 + GitHub Actions release workflow so tagged versions (v*) build cross-platform binaries (linux/darwin × amd64/arm64) and publish them to GitHub Releases.

Patterns + RELEASES_TOKEN requirement mirror workflow-plugin-digitalocean's release infra. RELEASES_TOKEN is already set at the GoCodeAlone org level.

Files

File Purpose
.github/workflows/release.yml Fires on v* tags; fail-fasts when RELEASES_TOKEN is unset; runs goreleaser; publishes the draft release. Git insteadOf rewrite is scoped to github.com/GoCodeAlone/ so the token never leaks to other hosts.
.goreleaser.yaml linux/darwin × amd64/arm64 build matrix; tar.gz archives; checksums; in-archive plugin.json rewritten with the tag version (version field only — current plugin.json has no asset URL fields to rewrite).
.gitignore Keeps superpowers hook state out of git.

Test plan

  • .goreleaser.yaml build path resolves to the cmd dir (exists).
  • Tag v0.1.0 + push → release workflow runs end-to-end.
  • Verify SHAs in published checksums.txt match downloaded artifacts.

🤖 Generated with Claude Code

GoReleaser v2 builds linux/darwin × amd64/arm64 binaries +
tar.gz archives + checksums; release.yml fires on `v*` tags.
plugin.json is rewritten with the tag version + asset URLs
during goreleaser's before-hook so the in-archive plugin.json
matches the published download URLs.

Requires the repo to have a `RELEASES_TOKEN` secret set (PAT or
fine-grained token with `contents:read` on the GoCodeAlone org
for private dep resolution). Without it, the goreleaser step
will fail at the git-config-insteadOf line — that's the signal
to add the secret.

After tagging v0.1.0 and the release publishes, register the
plugin in workflow-registry with the published asset SHAs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GoReleaser v2 configuration and a GitHub Actions workflow to build and publish cross-platform release artifacts whenever a v* tag is pushed, producing linux/darwin binaries for amd64/arm64 and attaching archives + checksums to GitHub Releases.

Changes:

  • Introduces .goreleaser.yaml with a build matrix, tar.gz archives, checksums, and a pre-build hook to stamp plugin.json with the release version.
  • Adds .github/workflows/release.yml to run GoReleaser on tag push and then publish the draft release.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
.goreleaser.yaml Defines GoReleaser v2 build/archive/release behavior and pre-release plugin.json rewriting.
.github/workflows/release.yml Adds a tag-triggered release pipeline that runs GoReleaser and publishes the GitHub Release.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/release.yml Outdated
- 'v*'
permissions:
contents: write
id-token: write
Comment thread .github/workflows/release.yml Outdated
Comment on lines +13 to +16
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
Comment thread .goreleaser.yaml Outdated
before:
hooks:
- "sh -c \"rm -rf .release && mkdir -p .release && cp plugin.json .release/plugin.json && sed -i.bak 's/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"{{ .Version }}\\\"/' .release/plugin.json && rm -f .release/plugin.json.bak\""
- "sh -c \"sed -i.bak 's|/releases/download/v[^/]*/|/releases/download/{{ .Tag }}/|g' .release/plugin.json && rm -f .release/plugin.json.bak\""
upsertRecords now deletes records that exist upstream but are
not in the desired config, completing the convergence loop.
Previously orphans persisted indefinitely; the README
documented the gap as a "Limitations" entry.

Implementation:

- Removed the `len(desired) == 0 → early return` short-circuit
  so an explicit empty `records: []` flows into the prune step.
- Removed the `existingByKey[key] = append(existingByKey[key],
  *created)` post-create write that incorrectly re-introduced
  newly-created records into the leftover candidate set —
  it would trip the prune sweep below and delete the just-
  created record. Drop both the write and the unused `created`
  return value.
- After the upsert pass, iterate the still-populated
  existingByKey and call client.DeleteRecord(ctx, id) on every
  leftover. Error wrapping cites the orphan's
  type/name/id/domain so failures point at the offending row.

Stale Diff comment about "upsertRecords does not currently
prune the extras (separate follow-up; see Limitations)" rewritten
to match the new behavior. README "Limitations" entry removed;
the "No zone delete" caveat remains.

Tests:
- TestUpsertRecords_PrunesExtraRecords (apex stays, orphan
  deleted)
- TestUpsertRecords_EmptyDesiredDeletesAll (explicit `records:
  []` deletes everything)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intel352 intel352 changed the title chore: add release workflow + goreleaser config feat: release infra + prune orphans on apply (closes #3) May 20, 2026
@intel352 intel352 requested a review from Copilot May 20, 2026 19:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread internal/drivers/dns.go
Comment on lines +231 to +233
// An empty desired set means "drop everything" — fall through into
// the prune step rather than short-circuiting. Without this, an
// explicit `records: []` would still leave upstream records intact.
Comment thread internal/drivers/dns.go
Comment on lines +189 to +190
// surfaces the change to the operator; upsertRecords prunes
// them during apply.
Comment thread .github/workflows/release.yml Outdated
- 'v*'
permissions:
contents: write
id-token: write
- release.yml: drop unused `id-token: write` permission. No
  OIDC step in the workflow.
- .goreleaser.yaml: drop the second before-hook (URL rewrite
  in .release/plugin.json) — current plugin.json contains no
  releases/download URLs, so the hook was a no-op. The version-
  rewrite hook (first one) stays.
- declaredRecords: require config.records explicitly. Missing
  key now returns a typed error pointing the operator at
  `records: []` for the explicit-prune-everything case. Without
  this, a caller that forgot the records key would silently
  prune every upstream record on apply.
- Diff: stale comment about "may report drift the engine cannot
  satisfy" rewritten to match the new prune-on-apply behavior.

Tests updated for the records-required validation:
- TestDNSDriver_Create_Empty: now uses explicit `records: []`.
- TestDNSDriver_Diff_DomainChange_ForceReplace: same.
- TestDNSDriver_Update_DomainRenameRejected: same.
- New TestDNSDriver_Create_MissingRecordsKey_Rejected covers
  the typed-error path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread .goreleaser.yaml Outdated
Comment on lines +5 to +7
before:
hooks:
- "sh -c \"rm -rf .release && mkdir -p .release && cp plugin.json .release/plugin.json && sed -i.bak 's/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"{{ .Version }}\\\"/' .release/plugin.json && rm -f .release/plugin.json.bak\""
Comment thread .github/workflows/release.yml Outdated
Comment on lines +12 to +15
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
Comment thread .github/workflows/release.yml Outdated
Comment on lines +18 to +19
- name: Configure Git for private repos
run: git config --global url."https://x-access-token:${{ secrets.RELEASES_TOKEN }}@github.com/".insteadOf "https://github.com/"
- Git insteadOf rewrite now scoped to
  https://github.com/GoCodeAlone/ only. Previously the rewrite
  matched all https://github.com/ traffic, broadening where the
  RELEASES_TOKEN could be sent (e.g. transitive go module fetches
  from third-party orgs would have presented the token).
- Fail-fast when RELEASES_TOKEN is unset. Without this, the git
  config line would silently install a rewrite with empty
  credentials and the subsequent goreleaser fetch of public
  GoCodeAlone repos would fail with a confusing auth error.
- actions/checkout@v5 + actions/setup-go@v6 to match ci.yml's
  pins. Reduces drift between the two workflows.

(PR description rewritten separately on the GitHub side to drop
the inaccurate "asset URL rewrite" claim — the goreleaser hook
only rewrites the version field, since plugin.json has no
releases/download URLs to rewrite.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread internal/drivers/dns.go
Comment on lines 135 to 139
// Empty desired record set with no current records → in sync.
// Empty desired with leftover current records → drift (everything
// extra needs deletion). Note: upsertRecords today does NOT delete
// extras (it only adds/updates), so this Diff signal currently
// produces a NeedsUpdate the engine cannot fully satisfy. The
// alternative — silently letting extras persist — is worse: the
// declared spec never matches reality. Operators who want strict
// pruning need to either explicitly add `Delete` plumbing or
// document the gap; flagging it is the right Plan signal.
// extra needs deletion); upsertRecords prunes them during apply.
if len(desiredRecs) == 0 {
if len(currentRecs) == 0 {
Comment thread README.md
Comment on lines 75 to +80
## Limitations

- **No prune on apply**: `upsertRecords` only adds/updates. Records
that exist upstream but are not in the desired config are NOT
deleted on `apply`. `Diff` does flag them (so Plan reports drift),
but converging the actual record set requires manually deleting
the orphan records via Hover's UI or via a future explicit prune
path. Track follow-up via the project issue list.
- **No zone delete**: Hover exposes no API to drop a DNS zone.
Resource `Delete` is a no-op — the IaC state is cleared but
upstream records remain.
upstream records remain. Operators who want to drop the zone
must do so manually via Hover's UI.
- Diff: call declaredRecords() up front so config errors
  (e.g., missing required `records` key, wrong-type value)
  surface at Plan time for NEW resources too. Previously
  current==nil short-circuited with NeedsUpdate=true and the
  validation didn't run until Apply tried to read the config.
  Existing test updated to use explicit `records: []`; new
  TestDNSDriver_Diff_MissingRecordsKey_ErrorsAtPlanTime covers
  the validation-at-Plan-time guarantee.
- README Configuration section: spell out the
  declared-set-is-authoritative contract (upstream-only records
  are deleted; declared-only are created; differing are updated)
  and the "records: required; use empty list to wipe"
  invariant. Operators omitting the key would otherwise be
  surprised by the typed-error rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread .goreleaser.yaml
Comment on lines +5 to +8
before:
hooks:
- "sh -c \"rm -rf .release && mkdir -p .release && cp plugin.json .release/plugin.json && sed -i.bak 's/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"{{ .Version }}\\\"/' .release/plugin.json && rm -f .release/plugin.json.bak\""

Comment thread internal/drivers/dns.go
Comment on lines 118 to 120
if current == nil {
return &interfaces.DiffResult{NeedsUpdate: true}, nil
}
…lidate

- .goreleaser.yaml: switch the version rewrite from sed to jq.
  sed exits 0 even on no-substitution, which would have shipped
  a stale plugin.json if the file's formatting changed. Added a
  follow-up jq -e check that fails the hook with a clear message
  if .version didn't take.
- release.yml: add an `if: always()` cleanup step that unsets
  the credentialed insteadOf git config after goreleaser runs.
  Reduces the window during which the runner's global git config
  carries the RELEASES_TOKEN — only the goreleaser step (and the
  Publish step that follows) see it. Belt-and-braces: the
  insteadOf is already scoped to github.com/GoCodeAlone/ so the
  blast radius from leaving it set was already small.
- .gitignore: add .release/ and dist/ so local `goreleaser
  release --snapshot` runs don't leave untracked artifacts that
  could be accidentally committed.
- Diff: validate config.domain up front via domainFromSpec, same
  pattern as the round-7 declaredRecords validation. Now both
  missing/empty domain AND missing/wrong-type records surface
  at Plan time for brand-new resources.

Test: TestDNSDriver_Diff_MissingDomain_ErrorsAtPlanTime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.

The "Unset credentialed git config" step previously interpolated
${{ secrets.RELEASES_TOKEN }} directly into the shell command
line. Copilot flagged this as widening the secret-exposure
surface vs. routing it through a per-step env mapping (which
GitHub's secret masking handles more reliably). Switch to the
env: pattern and read $RELEASES_TOKEN inside the shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.

@intel352 intel352 merged commit 8976ba6 into main May 20, 2026
8 checks passed
@intel352 intel352 deleted the chore/release-infra branch May 20, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants