fix(ci): make build.yml GA build fire on release tags under protected main - #764
Conversation
… check The GA image build was guarded on github.event.base_ref == 'refs/heads/main'. That field is empty for a tag pushed to a branch-protected main: protected main only receives PR-merge commits (server-side), never client branch pushes, so GitHub records no branch association and base_ref comes through empty. The guard therefore skipped every job silently — a green run that built nothing. It worked for v2.0.0 only because main was still unprotected then. Replace it with a verify-tag gate that the three build jobs depend on: - fires for release tags (v*) but not RC tags (build_rc.yml owns -rc.) - verifies the tagged commit is reachable from origin/main via merge-base, failing LOUD (red) instead of skipping silently if a tag is off-main First release affected: v2.0.1.
📝 WalkthroughWalkthroughThe release workflow adds GA tag ancestry verification against ChangesRelease tag validation
Estimated code review effort: 3 (Moderate) | ~15 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/build.yml:
- Around line 20-22: Update the job conditions for build-and-push-image,
build-and-push-image-ray, and build-and-push-image-admin-ui to allow
workflow_dispatch runs to proceed even when verify-tag is skipped, while still
requiring verify-tag success for other events.
- Around line 32-35: Update the workflow step containing the git merge-base
check to pass github.ref_name through the step’s env configuration, then
reference the quoted shell variable in both echo messages instead of
interpolating the GitHub expression directly in run. Keep github.sha
interpolation unchanged unless similarly required.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 75d94916-d6a5-4cdd-9562-f10ab1481200
📒 Files selected for processing (1)
.github/workflows/build.yml
| verify-tag: | ||
| runs-on: ubuntu-latest | ||
| if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc.') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow files =="
git ls-files | grep -E '(^|/)\.github/workflows/.*\.ya?ml$' || true
echo
echo "== build.yml relevant lines =="
if [ -f .github/workflows/build.yml ]; then
nl -ba .github/workflows/build.yml | sed -n '1,180p'
fi
echo
echo "== github ref/event_name docs relevant local references =="
rg -n "workflow_dispatch|github\.ref|always\(\)|needs\.[a-z-]+\.result|runs-on|verify-tag|depends|needs:" .github/workflows README.md 2>/dev/null || trueRepository: linagora/openrag
Length of output: 561
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== build.yml relevant lines =="
python3 - <<'PY'
from pathlib import Path
p=Path(".github/workflows/build.yml")
for i,line in enumerate(p.read_text().splitlines(),1):
if 1 <= i <= 180:
print(f"{i:4}\t{line}")
PY
echo
echo "== event_name/ref refs =="
rg -n "event_name|github\.ref|workflow_dispatch|verify-tag|needs:|always\(\)" .github/workflows/build.yml .github/workflows 2>/dev/null || trueRepository: linagora/openrag
Length of output: 8846
🌐 Web query:
GitHub Actions skipped job downstream jobs needs dependency job skipped docs
💡 Result:
In GitHub Actions, if a job is skipped or fails, all downstream jobs that depend on it (via the needs keyword) are automatically skipped [1][2][3]. This behavior propagates through the dependency chain [1][4]. To force a downstream job to run even if a dependency is skipped or fails, you must explicitly override this default behavior using the always conditional expression in the downstream job's if property [1][2][3]. Key Implementation Details: 1. Use always: Adding if: ${{ always }} ensures the job is evaluated even when preceding jobs are skipped [1][2]. 2. Manage Dependency Results: When using always, the downstream job will run regardless of the outcome of its dependencies. If you need to verify the status of specific dependencies (e.g., to ensure they either succeeded or were skipped but did not fail), you should combine always with checks on the result property [5][6][7]: downstream-job: needs: [upstream-job] if: ${{ always && (needs.upstream-job.result == 'success' || needs.upstream-job.result == 'skipped') }} runs-on: ubuntu-latest steps: - run: echo "This job runs even if upstream-job was skipped." 3. Context Considerations: By default, if no if condition is provided, GitHub Actions applies an implicit success condition [8]. Using always removes this implicit check, which is why it is often necessary to manually check for 'failure' or 'cancelled' states if you want to avoid running the job in those specific scenarios [6][7]. Warning: A skipped job is still reported as "Success" in the overall workflow status [9][10]. If you use a skipped job as a dependency for a required check in a pull request, that check will be considered successful, which might bypass intended gating logic [9][10].
Citations:
- 1: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-jobs
- 2: https://docs.github.com/actions/using-jobs/using-jobs-in-a-workflow
- 3: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
- 4: Clarify expressions and provide examples for workflows with skipped jobs github/docs#28762
- 5: https://stackoverflow.com/questions/76750973/how-to-execute-a-job-that-needs-a-job-that-was-skipped
- 6: https://stackoverflow.com/questions/69354003/github-action-job-fire-when-previous-job-skipped
- 7: https://stackoverflow.com/questions/68945610/how-to-put-conditional-job-in-need-of-another-job-in-github-action
- 8: Jobs skipped when NEEDS job ran successfully actions/runner#2205
- 9: How can I make a job with `needs` to require dependents to succeed also when skipped? actions/runner#2419
- 10: https://docs.github.com/actions/using-jobs/using-conditions-to-control-job-execution
🌐 Web query:
GitHub Actions workflow_dispatch branches tags refs branch push github.ref
💡 Result:
The workflow_dispatch event in GitHub Actions allows for manual triggering of workflows. A critical requirement for this trigger is that the workflow file must exist on the repository's default branch [1][2][3]. Regarding branches, tags, and github.ref: 1. Workflow File Location: While the workflow file itself must reside on the default branch to enable the trigger, the workflow can be executed against other branches or tags [2]. 2. Selecting Refs: When triggering the workflow manually via the GitHub UI, GitHub CLI (e.g., gh workflow run --ref <branch/tag>), or the REST API, you can specify the target branch or tag [2]. 3. github.ref Context: When the workflow runs, the github.ref context variable is set to the fully-formed reference (e.g., refs/heads/<branch_name> or refs/tags/<tag_name>) of the branch or tag that was selected for the manual run [1][4][3]. 4. No Branch/Tag Filtering: Unlike the push event, the workflow_dispatch event trigger does not support branches or tags filtering keywords (like on.push.branches or on.push.tags) in the workflow YAML configuration. The event is inherently manual and defined by the selection made at the time of execution [1][5][6].
Citations:
- 1: https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows
- 2: https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow?tool=webui
- 3: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows
- 4: https://docs.github.com/en/actions/reference/workflows-and-actions/variables
- 5: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- 6: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
Preserve the workflow_dispatch path.
A normal manual dispatch runs from a branch ref, so verify-tag is skipped and every job with needs: verify-tag is then skipped. Add an explicit dispatch condition, such as if: github.event_name == 'workflow_dispatch' || needs.verify-tag.result == 'success', to build-and-push-image, build-and-push-image-ray, and build-and-push-image-admin-ui.
Also applies to: 39-40, 96-97, 144-145
🤖 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/build.yml around lines 20 - 22, Update the job conditions
for build-and-push-image, build-and-push-image-ray, and
build-and-push-image-admin-ui to allow workflow_dispatch runs to proceed even
when verify-tag is skipped, while still requiring verify-tag success for other
events.
Source: Learnings
| if git merge-base --is-ancestor "${{ github.sha }}" FETCH_HEAD; then | ||
| echo "OK: ${{ github.ref_name }} (${{ github.sha }}) is on main" | ||
| else | ||
| echo "::error::Tag ${{ github.ref_name }} is not on main — refusing to build GA images." |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow excerpt =="
if [ -f .github/workflows/build.yml ]; then
nl -ba .github/workflows/build.yml | sed -n '1,90p'
else
echo "missing .github/workflows/build.yml"
fi
echo
echo "== changed files summary =="
git diff --stat || true
echo
echo "== workflow metadata/name refs tags =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/build.yml')
if p.exists():
text=p.read_text()
for i,line in enumerate(text.splitlines(),1):
if 'github.ref_name' in line or 'github.sha' in line or 'match:' in line:
print(f"{i}: {line}")
PYRepository: linagora/openrag
Length of output: 217
🌐 Web query:
GitHub Actions untrusted inputs run with context values script injection documentation
💡 Result:
GitHub Actions script injection occurs when untrusted context values—such as those from pull request titles, body text, or issue labels—are directly interpolated into workflow shell scripts using the ${{ }} syntax [1][2][3]. Because these expressions are evaluated and expanded into the script body before execution, an attacker can supply malicious characters (like quotes or semicolons) to break out of the intended command and execute arbitrary code on the runner [1][4][3]. To mitigate these risks, follow these authoritative security practices: 1. Use Intermediate Environment Variables (Recommended for Inline Scripts) Instead of placing context expressions directly into a run command, map the untrusted value to an environment variable [5][6]. The runner then treats this as data rather than executable code [5][3]. Example: env: USER_INPUT: ${{ github.event.issue.title }} run: | echo "$USER_INPUT" 2. Use Actions Instead of Inline Scripts The most secure approach is to use a pre-built or custom JavaScript action [5][7]. Actions receive inputs as arguments, which prevents them from being interpreted as shell commands [5][7]. 3. Implement Least Privilege Ensure your workflows operate with the minimum necessary permissions [4][6]. Limit the GITHUB_TOKEN permissions to prevent an attacker from gaining excessive access to your repository even if an injection occurs [4][6]. 4. Enable Security Scanning Enable code scanning, such as GitHub's CodeQL, to automatically identify unsafe use of untrusted inputs in your workflows [2][4]. Treat all data from the github context (e.g., github.event.pull_request.body, github.event.issue.title) as potentially untrusted input, even if they seem benign [1][3][8]. Avoid the use of ${{ }} syntax inside run blocks for any user-controlled values [2][4]. [1][5][2][4][3][9][6][7][8]
Citations:
- 1: https://docs.github.com/en/actions/concepts/security/script-injections
- 2: https://github.blog/security/supply-chain-security/four-tips-to-keep-your-github-actions-workflows-secure/
- 3: https://securitylab.github.com/resources/github-actions-untrusted-input/
- 4: https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
- 5: https://docs.github.com/en/actions/reference/security/secure-use
- 6: https://codeql.github.com/codeql-query-help/actions/actions-code-injection-critical/
- 7: https://docs.github.com/en/enterprise-server@2.22/actions/security-guides/security-hardening-for-github-actions
- 8: https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/GitHub_Actions_Security_Cheat_Sheet.md
- 9: https://github.com/github/docs/blob/main/content/actions/concepts/security/script-injections.md
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow excerpt =="
if [ -f .github/workflows/build.yml ]; then
cat -n .github/workflows/build.yml | sed -n '1,120p'
else
echo "missing .github/workflows/build.yml"
fi
echo
echo "== GitHub context interpolations and triggers =="
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/build.yml')
if not p.exists():
raise SystemExit
text = p.read_text()
for name in ['on:', 'schedule:', 'pull_request:', 'pull_request_target:', 'push:', 'workflow_dispatch:', 'match:', 'github.ref_name', 'github.sha']:
lines = [f"{i}: {line}" for i, line in enumerate(text.splitlines(), 1) if name in line]
print(f"-- {name} --")
for line in lines:
print(line)
if not any(x in text for x in ['github.ref_name', 'github.sha']):
print("-- no github ref/sha occurrences --")
PY
echo
echo "== deterministic shell expansion behavior for quoted tag names =="
python3 - <<'PY'
import subprocess, tempfile, os
examples = [
'git merge-base --is-ancestor "v1.0;malicious"'
'git merge-base --is-ancestor "$TAG_NAME"'
]
for src in examples:
print(src)
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, 'script.sh')
content = f"{src}\\nTAG_NAME=\\'v1.0;echo INJECTED\\'\\nprintenv TAG_NAME\\n"
with open(path, 'wb') as f:
# emulate POSIX sh: first run expands, second run sets after expansion
if src.endswith('"v1.0;malicious"'):
expanded = 'git merge-base --is-ancestor "v1.0;malicious"'
to_write = expanded + '\\nTAG_NAME=\\'v1.0;echo INJECTED\\'\\nprintenv TAG_NAME\\n'
elif src.endswith('"$TAG_NAME"'):
expanded = 'git merge-base --is-ancestor "$TAG_NAME"'
to_write = expanded + '\\nTAG_NAME=\\"v1.0;echo INJECTED\\"\\nprintenv TAG_NAME\\n'
subprocess.run(['sh', path], cwd=d, text=True, capture_output=True)
PYRepository: linagora/openrag
Length of output: 5665
Avoid interpolating github.ref_name directly into run.
This workflow is triggered on push: tags, so repository-controlled tag names are expanded inside the shell script before execution. Pass the tag name via env and quote the shell variable to avoid script-injection risk.
Proposed fix
+ env:
+ TAG_NAME: ${{ github.ref_name }}
+ TAG_SHA: ${{ github.sha }}
run: |
git fetch --no-tags origin main
- if git merge-base --is-ancestor "${{ github.sha }}" FETCH_HEAD; then
- echo "OK: ${{ github.ref_name }} (${{ github.sha }}) is on main"
+ if git merge-base --is-ancestor "$TAG_SHA" FETCH_HEAD; then
+ echo "OK: $TAG_NAME ($TAG_SHA) is on main"
else
- echo "::error::Tag ${{ github.ref_name }} is not on main — refusing to build GA images."
+ echo "::error::Tag $TAG_NAME is not on main — refusing to build GA images."
exit 1📝 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.
| if git merge-base --is-ancestor "${{ github.sha }}" FETCH_HEAD; then | |
| echo "OK: ${{ github.ref_name }} (${{ github.sha }}) is on main" | |
| else | |
| echo "::error::Tag ${{ github.ref_name }} is not on main — refusing to build GA images." | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| TAG_SHA: ${{ github.sha }} | |
| run: | | |
| git fetch --no-tags origin main | |
| if git merge-base --is-ancestor "$TAG_SHA" FETCH_HEAD; then | |
| echo "OK: $TAG_NAME ($TAG_SHA) is on main" | |
| else | |
| echo "::error::Tag $TAG_NAME is not on main — refusing to build GA images." | |
| exit 1 |
🧰 Tools
🪛 zizmor (1.26.1)
[error] 33-33: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 35-35: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 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/build.yml around lines 32 - 35, Update the workflow step
containing the git merge-base check to pass github.ref_name through the step’s
env configuration, then reference the quoted shell variable in both echo
messages instead of interpolating the GitHub expression directly in run. Keep
github.sha interpolation unchanged unless similarly required.
Source: Linters/SAST tools
…lation Review findings on the verify-tag gate added in linagora#764 (raised by @hedhoud and CodeRabbit/zizmor), all three confirmed against the merged workflow: 1. Tag format was too loose. The guard only rejected '-rc.', so v2.0.1-rc1, v2.0.1-beta, vfoo etc. passed and would publish GA images and move :latest. The rc1 case is the sharp one: build_rc.yml triggers on 'v*-rc.*' which requires the dot, so a one-character typo matched neither workflow's intent. Now validated against ^v[0-9]+\.[0-9]+\.[0-9]+$ and failed loud. 2. Template injection. ${{ github.ref_name }} expanded into the run body before the shell ran, and git permits ; $ ` " | & in ref names — arbitrary code execution in a job that holds packages:write and Docker Hub credentials. Tag name and SHA now passed via env: and referenced as shell variables. 3. Checkout persisted credentials, inconsistent with build_rc.yml which already sets persist-credentials: false on all three checkouts (95fd86f). The repo is public, so the origin/main fetch still works without them. Behavior: vX.Y.Z on main builds; vX.Y.Z-rc.N skips to build_rc.yml; malformed or prerelease tags and off-main tags now fail loudly instead of publishing.
The v2.0.1 tag push triggered
build.ymlbut all three image-build jobs skipped — a green run that built nothing.Cause
The jobs were guarded on
github.event.base_ref == 'refs/heads/main'. That field is empty for a tag pushed to a branch-protectedmain: a protectedmainonly receives PR-merge commits (created server-side), never client branch pushes, so GitHub records no branch association andbase_refarrives empty. The guard skipped everything, silently.It worked for v2.0.0 only because
mainwas still unprotected then (direct client pushes established the association). v2.0.1 is the first release since branch protection went on (2026-07-09), so it is the first to hit this.Fix
Add a
verify-taggate that the three build jobsneeds::v*) but not RC tags (build_rc.ymlowns-rc.)origin/main(merge-base --is-ancestor), failing loud (red) instead of skipping silently if a tag is ever off-mainTrigger matrix after this change:
vX.Y.Zon mainvX.Y.Zoff mainvX.Y.Z-rc.Nbuild_rc.yml)workflow_dispatchUnblocks the v2.0.1 release. Must also back-merge to
developso future releases inherit the fix.Summary by CodeRabbit