Skip to content

ci(release): harden release publishing#273

Merged
SantiagoDePolonia merged 1 commit into
mainfrom
ci/harden-release-process
Apr 25, 2026
Merged

ci(release): harden release publishing#273
SantiagoDePolonia merged 1 commit into
mainfrom
ci/harden-release-process

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Apr 25, 2026

Summary

  • stage the multi-arch Docker image by digest before publishing final tags
  • promote semver Docker tags before publishing the GitHub release
  • assign latest only for stable release tags, not prereleases
  • scope release workflow permissions and add per-tag concurrency
  • stop tracking the generated integration model cache while keeping the cache directory with .gitkeep

Validation

  • ruby -e 'require "yaml"; YAML.load_file(".github/workflows/release.yml"); puts "workflow yaml ok"'
  • go run github.com/rhysd/actionlint/cmd/actionlint@latest .github/workflows/release.yml
  • git diff --check
  • pre-commit hooks during commit

Summary by CodeRabbit

  • Documentation

    • Clarified release sequencing to publish container digests first, then promote tags, and added guidance for post-release remediation if tag promotion fails.
  • Chores

    • Updated CI/CD to serialize releases and publish multi-arch images by digest before tagging/promoting.
    • Reduced workflow permissions for safer automation.
    • Adjusted repo ignore rules to keep a placeholder file while ignoring integration cache artifacts.

@mintlify
Copy link
Copy Markdown

mintlify Bot commented Apr 25, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Apr 25, 2026, 1:51 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

Release workflow restructured: docker builds and publishes multi-arch images by digest, docker-promote derives and creates semantic/latest tags by promoting the digest, and goreleaser runs after promotion with job-level contents: write. Top-level workflow permissions reduced to contents: read. Docs and .gitignore updated.

Changes

Cohort / File(s) Summary
Release Workflow
.github/workflows/release.yml
Reduced top-level permissions to contents: read, added concurrency per release-${{ github.ref_name }}, split Docker flow: docker job builds/pushes multi-arch image and outputs digest; new docker-promote job computes tags and promotes digest via docker buildx imagetools create; goreleaser moved to run after promote with job-level contents: write.
Repository Configuration
.gitignore
Ignore tests/integration/.cache/ while preserving tests/integration/.cache/.gitkeep.
Documentation
docs/DEVELOPMENT.md
Documented new digest-first Docker tagging flow and added post-failure reconciliation guidance when GitHub release publishing fails after Docker tag promotion.

Sequence Diagram(s)

sequenceDiagram
  participant GH as "GitHub Actions"
  participant DockerJob as "docker job\n(buildx push by digest)"
  participant Registry as "Container Registry"
  participant Promote as "docker-promote job"
  participant GoReleaser as "goreleaser job"

  GH->>DockerJob: start release workflow
  DockerJob->>Registry: build & push multi-arch image (by digest)
  Registry-->>DockerJob: return image digest
  DockerJob-->>GH: set job output `digest`
  GH->>GoReleaser: run after docker/promote (job-level `contents: write`)
  GoReleaser->>GH: publish GitHub release
  GH->>Promote: compute tags (semver / latest) and validate digest
  Promote->>Registry: `docker buildx imagetools create` (promote digest -> tags)
  Registry-->>Promote: confirm tag promotion
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

release:internal

Poem

🐰 I hopped through YAML, neat and spry,
Pushed a digest under moonlit sky,
Promoted tags with a careful thump,
GoReleaser chimed — then a joyful jump! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'ci(release): harden release publishing' clearly and concisely describes the main change: improving the release workflow's robustness. It accurately reflects the PR's core objective of restructuring the CI/CD release process.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/harden-release-process

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR hardens the release pipeline by (1) gating the GitHub release on a successful Docker image build/push rather than running them in parallel, (2) narrowing the workflow-level permission to contents: read and applying contents: write only to the goreleaser job, and (3) adding a per-tag concurrency group so duplicate tag pushes cannot race. The models.json integration cache is removed from git and a .gitkeep preserves the directory.

Confidence Score: 5/5

Safe to merge — changes are purely additive CI hardening with no logic regressions.

All changes are correct: job ordering, permission scoping, concurrency guard, and .gitignore cleanup are all well-formed and achieve the stated goals. No P0 or P1 findings.

No files require special attention.

Important Files Changed

Filename Overview
.github/workflows/release.yml Reordered jobs so Docker build/push precedes GitHub release publishing; scoped permissions to least privilege at both workflow and job level; added per-tag concurrency guard.
.gitignore Added glob to ignore generated integration cache files while preserving the directory via .gitkeep.
docs/DEVELOPMENT.md Added one-line note documenting that GitHub releases are gated on a successful Docker build/push.
tests/integration/.cache/.gitkeep Empty sentinel file added to preserve the cache directory in git without tracking its contents.
tests/integration/.cache/models.json Removed previously tracked auto-generated model cache file, now covered by the new .gitignore rule.

Sequence Diagram

sequenceDiagram
    participant GH as GitHub (tag push)
    participant D as docker job
    participant DHR as Docker Hub
    participant GR as goreleaser job
    participant GHR as GitHub Releases

    GH->>D: trigger on v* tag
    D->>DHR: build & push image (amd64/arm64/arm)
    DHR-->>D: success
    D-->>GR: needs: docker (gate)
    GR->>GHR: goreleaser release --clean
    GHR-->>GR: release published
Loading

Reviews (1): Last reviewed commit: "ci(release): harden release publishing" | Re-trigger Greptile

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/release.yml (1)

72-98: ⚠️ Potential issue | 🟠 Major

Orphan Docker image risk if goreleaser fails after a successful Docker push.

With goreleaser now needs: docker, the Docker image is pushed to Docker Hub (including the unconditional type=raw,value=latest tag set in the docker job) before the GitHub release is created. If GoReleaser fails for any reason (rate limits, transient network errors, GoReleaser config issues, missing id-token/packages perms for sbom/sign steps later, etc.), you’ll be left with a published image — including :latest — without a corresponding GitHub release, which contradicts the new docs guarantee that "GitHub releases are published only after the Docker image build and push succeed."

Consider one of:

  • Move the :latest tag promotion into the goreleaser job (or a third post-release job), so :latest is only updated once both succeed.
  • Add a third needs: [docker, goreleaser] cleanup/promotion job, or a failure()-conditional rollback step that retags/deletes the just-pushed tags on Docker Hub when goreleaser fails.
  • At minimum, document this partial-failure mode in docs/DEVELOPMENT.md so operators know to manually reconcile.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 72 - 98, The goreleaser job can
fail after the docker job has pushed images (including the unconditional latest
tag), leading to orphaned Docker images; update the workflow so tag promotion or
deletion happens only after goreleaser succeeds by either moving the ":latest"
promotion into the goreleaser job or adding a new job that needs: [docker,
goreleaser] to perform promotion/cleanup (or add a failure()-conditional
rollback step) — modify the existing goreleaser job (name: Publish GitHub
release / uses: goreleaser/goreleaser-action@v7) or add a new job that runs
post-release to retag/delete the just-pushed Docker tags, and/or document the
partial-failure behavior in docs/DEVELOPMENT.md so operators can reconcile
manually.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In @.github/workflows/release.yml:
- Around line 72-98: The goreleaser job can fail after the docker job has pushed
images (including the unconditional latest tag), leading to orphaned Docker
images; update the workflow so tag promotion or deletion happens only after
goreleaser succeeds by either moving the ":latest" promotion into the goreleaser
job or adding a new job that needs: [docker, goreleaser] to perform
promotion/cleanup (or add a failure()-conditional rollback step) — modify the
existing goreleaser job (name: Publish GitHub release / uses:
goreleaser/goreleaser-action@v7) or add a new job that runs post-release to
retag/delete the just-pushed Docker tags, and/or document the partial-failure
behavior in docs/DEVELOPMENT.md so operators can reconcile manually.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 509a558f-1521-4eab-b276-733b2cee32c7

📥 Commits

Reviewing files that changed from the base of the PR and between 13a2d07 and eeb9bf0.

⛔ Files ignored due to path filters (2)
  • tests/integration/.cache/.gitkeep is excluded by !**/.cache/**
  • tests/integration/.cache/models.json is excluded by !**/.cache/**
📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • .gitignore
  • docs/DEVELOPMENT.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 101-106: The workflow currently runs jobs in order docker →
goreleaser → docker-promote which can publish a GitHub release before Docker
tags are promoted; reorder job dependencies so docker-promote runs immediately
after docker and before goreleaser: update the docker-promote job's needs to
only include docker (remove goreleaser) and make goreleaser depend on
docker-promote (add docker-promote to goreleaser's needs), ensuring the sequence
becomes docker → docker-promote → goreleaser so tags are promoted before the
release is published.
- Around line 130-134: The raw `latest` tag rule (type=raw,value=latest)
currently applies to prerelease tags too; update that entry to include an enable
guard so it only runs for non-prereleases (i.e., when attributes.prerelease is
false). Locate the tags block and modify the type=raw,value=latest line to add
an enable condition such as using the docker/metadata-action prerelease
attribute (e.g., enable: ${{ !attributes.prerelease }} or enable: ${{
attributes.prerelease == false }}) so `latest` is not assigned for prerelease
tags.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4cb4c74c-0bbb-4dd0-a9fd-8e98789d43b5

📥 Commits

Reviewing files that changed from the base of the PR and between eeb9bf0 and bdfdecf.

⛔ Files ignored due to path filters (2)
  • tests/integration/.cache/.gitkeep is excluded by !**/.cache/**
  • tests/integration/.cache/models.json is excluded by !**/.cache/**
📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • .gitignore
  • docs/DEVELOPMENT.md

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 53-72: The meta step (id: meta, uses: docker/metadata-action@v6)
is computing tags that are never used by the build step (id: build, uses:
docker/build-push-action@v7); to stop generating unused tags add an explicit
empty/disabled tags input to the meta step (e.g. tags: '' or tags: | followed by
no entries) so the action only emits labels (steps.meta.outputs.labels) and
avoids producing a default :latest tag on pushes. Update the meta step inputs
accordingly and keep the build step using steps.meta.outputs.labels as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c031ec8-b56a-45e7-be8b-dd4adb2fdbc1

📥 Commits

Reviewing files that changed from the base of the PR and between bdfdecf and 6d83cb8.

⛔ Files ignored due to path filters (2)
  • tests/integration/.cache/.gitkeep is excluded by !**/.cache/**
  • tests/integration/.cache/models.json is excluded by !**/.cache/**
📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • .gitignore
  • docs/DEVELOPMENT.md

Comment on lines +53 to 72
- name: Extract Docker labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ steps.build_meta.outputs.image }}

- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ steps.build_meta.outputs.image }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ steps.build_meta.outputs.version }}
COMMIT=${{ steps.build_meta.outputs.commit }}
DATE=${{ steps.build_meta.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: meta step computes tags that are never used.

In the digest-only build, build-push-action doesn't consume steps.meta.outputs.tags (the outputs: line uses push-by-digest=true, and no tags: input is set). Only labels are consumed downstream. This is harmless but generates noise in the action output and a default :latest tag in the computed (unused) tag set on tag pushes. If you want to keep the step purely for labels, you can suppress tag generation explicitly:

♻️ Optional cleanup
       - name: Extract Docker labels
         id: meta
         uses: docker/metadata-action@v6
         with:
           images: ${{ steps.build_meta.outputs.image }}
+          tags: |
+            type=sha,enable=false
+          flavor: |
+            latest=false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Extract Docker labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ steps.build_meta.outputs.image }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ steps.build_meta.outputs.image }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ steps.build_meta.outputs.version }}
COMMIT=${{ steps.build_meta.outputs.commit }}
DATE=${{ steps.build_meta.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract Docker labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ steps.build_meta.outputs.image }}
tags: |
type=sha,enable=false
flavor: |
latest=false
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ steps.build_meta.outputs.image }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ steps.build_meta.outputs.version }}
COMMIT=${{ steps.build_meta.outputs.commit }}
DATE=${{ steps.build_meta.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 53 - 72, The meta step (id: meta,
uses: docker/metadata-action@v6) is computing tags that are never used by the
build step (id: build, uses: docker/build-push-action@v7); to stop generating
unused tags add an explicit empty/disabled tags input to the meta step (e.g.
tags: '' or tags: | followed by no entries) so the action only emits labels
(steps.meta.outputs.labels) and avoids producing a default :latest tag on
pushes. Update the meta step inputs accordingly and keep the build step using
steps.meta.outputs.labels as before.

@SantiagoDePolonia SantiagoDePolonia merged commit 701d4e2 into main Apr 25, 2026
20 checks passed
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