Skip to content

ci(deploy): gate production deploy to release publication, not every main push - #1398

Merged
LucasSantana-Dev merged 3 commits into
mainfrom
ci/deploy-on-release-only
Jun 14, 2026
Merged

LucasSantana-Dev merged 3 commits into
mainfrom
ci/deploy-on-release-only

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Jun 14, 2026 •

Copy link
Copy Markdown
Owner

Production currently ships on every push to main — docker-publish builds images on each main commit and Deploy to Homelab chains off it via workflow_run, so every merge auto-deploys to prod (and they race — see #1397). This makes shipping accidental rather than deliberate.

Change

Gate production deploys to GitHub Release publication:

  • Trigger: swap workflow_run (Build & Push Docker Images / main) → release: types: [published]. workflow_dispatch (manual deploy + rollback) is unchanged.
  • SHA resolution: add one resolve_sha step as the single source of truth for the shipped commit. On a release it dereferences the annotated tag to its commit SHA (github.sha is unreliable on release events). Rollback and workflow_dispatch paths are preserved.
  • Consistency: every downstream step (image wait, webhook, Sentry create/finalize, version validation, homelab wait) now reads that one resolved SHA, so the Sentry release, the deployed :<sha> image, and the validated commit status can never disagree (they were independently re-derived in 6 places before).

What does NOT change

  • docker-publish.yml still builds :<sha> + :latest on every main push, so the image for a release commit already exists when the release is cut. Building artifacts is decoupled from shipping them.
  • The Production environment protection (wait timer) still applies — now on release deploys.
  • Rollback via workflow_dispatch rollback_sha is untouched.

Shipping flow after this

merge to main → images built (no deploy) → cut release vX (push annotated tag → Release workflow publishes it) → Deploy to Homelab fires for the tagged commit.

Known limitation

If a release tag points at a commit that never built a :<sha> image (e.g. a docs-only HEAD that docker-publish path-filtered out), the deploy falls to the existing :latest path. In practice release tags sit on code/package.json commits that always build. Flagging for review rather than over-engineering.

Verification

  • actionlint: 0 new findings vs main baseline (7 pre-existing SC2016 info notes on intentional single-quoted jq/node -e, identical on both).
  • YAML parses; no workflow_run / stray github.sha references remain outside the intended workflow_dispatch path.
  • Pre-commit type-check (shared/bot/backend/frontend) passed.

@cubic-dev-ai please review.


Summary by cubic

Gate production deploys to GitHub Release publication instead of every push to main. Adds SHA validation and env-based propagation to prevent injection, keeping Sentry, image tags, and commit status in sync, and improves Docker build lookup for older tags.

  • Refactors
    • Trigger changed: workflow_run → release: [published]; workflow_dispatch (manual/rollback) stays.
    • Added resolve_sha to pick the shipped commit once; on release, dereferences the annotated tag to its commit SHA.
    • Validates the resolved SHA (7–40 lowercase hex) and passes it via env; all steps read it to prevent template-injection via rollback_sha.
    • Docker wait now finds the build by exact head_sha via the Actions API (not a recent window), so releases for older tags don’t fail lookup.
    • Downstream steps (image wait, webhook, Sentry create/finalize, version validation, homelab wait) use the one SHA.
    • docker-publish.yml still builds :<sha> and :latest on main; building artifacts is decoupled from shipping them.

Written for commit 0b4902a. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Chores
    • Updated deployment workflow to trigger on release publication instead of Docker build completion.
    • Improved deployment SHA resolution for better consistency across rollbacks, releases, and manual deployments.

Production was shipping on every push to main: docker-publish built
images on each main commit and Deploy to Homelab chained off it via
workflow_run, so every merge auto-deployed (and raced — see #1397).

Gate deploys to deliberate release publication instead:
- swap the workflow_run(main) trigger for release: types: [published];
  workflow_dispatch (manual + rollback) is kept unchanged
- add one resolve_sha step as the single source of truth for the
  shipped commit. On a release it derefs the (annotated) tag to its
  commit SHA — github.sha is unreliable on release events. Rollback and
  workflow_dispatch paths are preserved
- every downstream step (image wait, webhook, Sentry create/finalize,
  version validation, homelab wait) now reads that one resolved SHA, so
  the Sentry release, deployed :<sha> image, and validated status can
  never disagree

docker-publish still builds :<sha>/:latest on main push, so the image
for a release commit already exists when the release is cut. Building
artifacts is decoupled from shipping them.
@vercel

vercel Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Ready Ready Preview, Comment Jun 14, 2026 1:19am

Request Review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The production deploy workflow trigger is changed from workflow_run (Docker build completion) to release publication. A new resolve_sha step centralizes commit SHA derivation across rollback, release, and dispatch entry points. All downstream steps—Docker polling, Sentry, webhook, version validation, and homelab wait—are rewired to consume the single resolved SHA.

Changes

Deploy trigger and SHA resolution refactor

Layer / File(s) Summary
Trigger and job gate condition
.github/workflows/deploy.yml
Production deploy trigger switches from workflow_run (Docker build completion on main) to release with types: [published]; the deploy job if condition adds github.event_name == 'release' to the allowed event set.
Centralized resolve_sha step
.github/workflows/deploy.yml
A new resolve_sha step (lines 61–105) computes the authoritative deploy commit SHA: uses inputs.rollback_sha for rollbacks, dereferences the released tag to its commit SHA via gh api for release events, and falls back to github.sha for workflow_dispatch; fails the workflow if no SHA is resolved; exports the result as steps.resolve_sha.outputs.sha.
Downstream steps rewired to resolved SHA
.github/workflows/deploy.yml
Docker image readiness polling, Sentry release creation, deploy webhook DEPLOY_SHA, deployed version validation, and homelab deploy completion waiting all replace their previous per-event or rollback-branching SHA logic with a direct reference to steps.resolve_sha.outputs.sha.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LucasSantana-Dev/Lucky#437: Modifies the same deploy webhook firing section in .github/workflows/deploy.yml and introduces Docker image availability waiting keyed to the commit SHA, directly overlapping with the webhook and Docker polling changes in this PR.

Suggested labels

ci, size/m

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: gating production deployment to release publication instead of every main push, which directly addresses the core objective of preventing auto-deployment on every merge.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/deploy-on-release-only

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.

@github-actions

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

133-141: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

docker_wait can falsely fail valid release deploys because run lookup is capped at 10.

With release-triggered shipping, the tagged commit can be older than the latest 10 docker-publish runs. The current query then misses the real run and Line 178 can abort a valid deploy.

Suggested lookup fix (filter by commit instead of scanning last 10)
-                    run_info=$(gh run list \
-                      --repo "${{ github.repository }}" \
-                      --workflow=docker-publish.yml \
-                      --json headSha,status,conclusion,databaseId \
-                      --limit 10 \
-                      | jq -r --arg sha "$commit_sha" \
-                        '.[] | select(.headSha == $sha) | "\(.databaseId) \(.status) \(.conclusion)"' \
-                      | head -1)
+                    run_info=$(gh run list \
+                      --repo "${{ github.repository }}" \
+                      --workflow=docker-publish.yml \
+                      --commit "$commit_sha" \
+                      --json status,conclusion,databaseId \
+                      --limit 1 \
+                      --jq '.[] | "\(.databaseId) \(.status) \(.conclusion)"' \
+                      | head -1)

Also applies to: 148-181

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 133 - 141, The docker_wait
function uses a gh run list query with --limit 10 that only scans the last 10
Docker publish runs, which can miss older tagged commits in release deployments
and cause false failures. Modify the gh run list command to either remove the
arbitrary limit or add a filter to the query itself to specifically target the
commit SHA instead of relying on scanning only recent runs. This issue appears
in the docker_wait logic around the gh run list invocation and potentially in
subsequent similar queries elsewhere in the workflow file, so apply the same fix
approach to all locations where gh run list queries are made without proper
commit-based filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy.yml:
- Around line 61-105: The `deploy_sha` variable resolved in the resolve_sha step
is derived from user-controlled input (inputs.rollback_sha on workflow_dispatch)
and is currently exported without strict validation, then inlined directly into
multiple downstream shell scripts at lines 121, 237, 443, and 685. This creates
a template-injection vulnerability. Add strict validation of deploy_sha in the
resolve_sha step to ensure it matches the expected git commit SHA format (using
a regex pattern to validate it contains only hexadecimal characters of the
correct length), and then update all downstream usages at those four locations
to safely reference the value via environment variable assignment with proper
shell quoting rather than direct template expansion into command strings.

---

Outside diff comments:
In @.github/workflows/deploy.yml:
- Around line 133-141: The docker_wait function uses a gh run list query with
--limit 10 that only scans the last 10 Docker publish runs, which can miss older
tagged commits in release deployments and cause false failures. Modify the gh
run list command to either remove the arbitrary limit or add a filter to the
query itself to specifically target the commit SHA instead of relying on
scanning only recent runs. This issue appears in the docker_wait logic around
the gh run list invocation and potentially in subsequent similar queries
elsewhere in the workflow file, so apply the same fix approach to all locations
where gh run list queries are made without proper commit-based filtering.
🪄 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: CHILL

Plan: Pro

Run ID: d5943b77-e450-46a6-860a-9ba35ed9fa77

📥 Commits

Reviewing files that changed from the base of the PR and between ff5e0b6 and 2f89ef6.

📒 Files selected for processing (1)
  • .github/workflows/deploy.yml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Test — bot
  • GitHub Check: Checks
  • GitHub Check: Test — shared
  • GitHub Check: Test — backend
  • GitHub Check: Test — frontend
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: quality / SAST (CodeQL) (javascript-typescript)
  • GitHub Check: quality / Lint (lint)
  • GitHub Check: Build — backend
🧰 Additional context used
🪛 zizmor (1.25.2)
.github/workflows/deploy.yml

[info] 121-121: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 237-237: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 443-443: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 685-685: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🔇 Additional comments (1)
.github/workflows/deploy.yml (1)

16-17: LGTM!

Also applies to: 35-37

Comment thread .github/workflows/deploy.yml
Address CodeRabbit Major: rollback_sha is user-controlled on
workflow_dispatch and flowed into resolve_sha.outputs.sha, which was
then inlined as ${{ }} into run: scripts — a template-injection path.

- resolve_sha now rejects anything but a 7-40 char lowercase hex SHA
  before exporting (covers short rollback SHA through full commit SHA)
- the four steps that still inlined the output (docker_wait, Sentry
  create/finalize, homelab wait) now read it from env, matching the
  webhook/validate steps and the workflow's pre-existing injection-safe
  pattern

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 1 file

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/deploy.yml Outdated
Comment thread .github/workflows/deploy.yml Outdated

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

Fixed in ffe13e1 (@cubic-dev-ai fyi): resolve_sha now rejects anything but a 7–40 char lowercase hex SHA before exporting, and the four steps that still inlined the output (docker_wait, Sentry create/finalize, homelab wait) now read it from env — matching the webhook/validate steps and the workflow's pre-existing injection-safe pattern. Used 7–40 rather than strict 40 so short rollback SHAs still work.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 14, 2026
@cubic-dev-ai

cubic-dev-ai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Fixed in ffe13e1 (@cubic-dev-ai fyi): resolve_sha now rejects anything but a 7–40 char lowercase hex SHA before exporting, and the four steps that still inlined the output (docker_wait, Sentry create/finalize, homelab wait) now read it from env — matching the webhook/validate steps and the workflow's pre-existing injection-safe pattern. Used 7–40 rather than strict 40 so short rollback SHAs still work.

@LucasSantana-Dev Looks good — thanks for the flag.

The main things I was going to flag from the first diff are addressed:

  • Injection guard on rollback_sha: the grep -Eq '^[0-9a-f]{7,40}$' after trimming/stripping means a user-supplied rollback_sha containing shell metacharacters, newlines, or path traversal gets caught before it reaches $GITHUB_OUTPUT or any run: script. The 7–40 leniency makes sense — short SHAs are valid for rollback and git resolves them fine.

  • No more inlined ${{ }} in run: blocks for SHA values: docker_wait, Sentry create/finalize, and homelab wait all read $RESOLVED_SHA / $EXPECTED_SHA from env: instead, matching the webhook and validate steps that already did this. Keeps the template-injection surface at zero.

  • Consolidated SHA resolution: one resolve_sha step, all consumers read its output. The logic for annotated tag deref vs workflow_dispatch vs rollback is clear and doesn't duplicate the branching.

No new concerns from the updated diff — this looks ready to ship.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

Address cubic P2: docker_wait found the build via `gh run list --limit
10`, which was always safe under workflow_run coupling (deploy fired
right after the build). With release-triggered deploys a release can be
cut for an older tag whose docker-publish run has fallen out of the
10-run window, so the capped lookup would miss it and falsely abort.

Query the build by exact head_sha via the Actions API instead — immune
to how many builds ran since the tagged commit. Same id/status/
conclusion output, same not-yet-queued retry and fallback paths.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

@cubic-dev-ai P2 fixed in 0b4902a: docker_wait now looks the build up by exact head_sha via the Actions API (workflows/docker-publish.yml/runs?head_sha=…) instead of scanning the 10 most recent runs — so a release published for an older tag still finds its build regardless of how many builds ran since. Same downstream id/status/conclusion handling and not-yet-queued retry.

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 14, 2026

Copy link
Copy Markdown

@cubic-dev-ai P2 fixed in 0b4902a: docker_wait now looks the build up by exact head_sha via the Actions API (workflows/docker-publish.yml/runs?head_sha=…) instead of scanning the 10 most recent runs — so a release published for an older tag still finds its build regardless of how many builds ran since. Same downstream id/status/conclusion handling and not-yet-queued retry.

@LucasSantana-Dev That looks right to me.

Switching from gh run list --limit 10 | jq 'select(.headSha == $sha)' to a direct gh api .../runs?head_sha=... call is the right approach here — it's no longer a sampling problem. A release cut for a commit that built days (or weeks) ago won't be missed because the API query is index-backed on head_sha rather than scanning a capped window.

The rest of the logic lines up too:

  • Same id/status/conclusion extraction via --jq
  • Same not-yet-queued detection loop with detect_max/detect_interval
  • Same fallback to :latest if no matching build is found
  • Rollback still short-circuits before this check (reads docker_rebuilt=false and exits early)

One minor thing I noticed — the previous gh run list approach used databaseId as the field name, and the new API response uses id. Both end up as the first field in the space-separated run_info string, and downstream code only reads it via $run_id=$(echo "$run_info" | awk '{print $1}'), so the rename is functionally transparent. No issue there.

No concerns from the updated diff.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Requires human review: Modifies CI/CD deployment trigger from every main push to release publications; adds SHA resolution logic and validation.

Re-trigger cubic

@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit 4eb43d4 into main Jun 14, 2026
42 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the ci/deploy-on-release-only branch June 14, 2026 01:23

This branch was successfully deployed

1 active deployment
Preview — 0b4902a2 Deployed Jun 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant