Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 89 additions & 46 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
name: CI

on:
release:
types: [published]
pull_request:
branches:
- '**'
paths-ignore:
- '**.md'
merge_group:
push:
tags:
- '*'
branches:
- develop
Comment thread
coderabbitai[bot] marked this conversation as resolved.
paths-ignore:
- '**.md'

concurrency:
# merge_group runs group by the merge-group ref so a recreated group cancels the stale run;
# push/release runs group by run_id on purpose (never cancel a deploy in progress)
# push (incl. tag) runs group by run_id on purpose (never cancel a deploy in progress)
group: ${{ github.workflow }}-${{ github.head_ref || (github.event_name == 'merge_group' && github.ref) || github.run_id }}
cancel-in-progress: true

Expand Down Expand Up @@ -189,7 +189,7 @@ jobs:
name: 🚀 Notify external services - draft
runs-on: ubuntu-24.04-arm
needs: [release-versions]
if: github.event_name == 'release'
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -199,30 +199,22 @@ jobs:
ref: ${{ github.ref }}

- name: Register release on cloud as Draft
if: github.event_name == 'release'
if: startsWith(github.ref, 'refs/tags/')
env:
UPDATE_TOKEN: ${{ secrets.UPDATE_TOKEN }}
run: |
REPO_VERSION=$(node -p "require('./package.json').version")

if [[ '${{ github.event_name }}' = 'release' ]]; then
GIT_TAG="${GITHUB_REF#*tags/}"
GIT_BRANCH=""
ARTIFACT_NAME="${REPO_VERSION}"
RC_VERSION=$GIT_TAG
GIT_TAG="${GITHUB_REF#*tags/}"
GIT_BRANCH=""
ARTIFACT_NAME="${REPO_VERSION}"
RC_VERSION=$GIT_TAG

if [[ '${{ needs.release-versions.outputs.release }}' = 'release-candidate' ]]; then
RC_RELEASE=candidate
elif [[ '${{ needs.release-versions.outputs.release }}' = 'latest' ]]; then
RC_RELEASE=stable
fi
else
GIT_TAG=""
GIT_BRANCH="${GITHUB_REF#*heads/}"
ARTIFACT_NAME="${REPO_VERSION}.$GITHUB_SHA"
RC_VERSION="${REPO_VERSION}"
RC_RELEASE=develop
fi;
if [[ '${{ needs.release-versions.outputs.release }}' = 'release-candidate' ]]; then
RC_RELEASE=candidate
elif [[ '${{ needs.release-versions.outputs.release }}' = 'latest' ]]; then
RC_RELEASE=stable
fi

curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \
"{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\",\"compatibleMongoVersions\": [\"8.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \
Expand Down Expand Up @@ -328,7 +320,7 @@ jobs:
- production
- coverage
exclude:
- type: ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'production' || '' }}
- type: ${{ (!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/develop') && 'production' || '' }}

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -360,7 +352,7 @@ jobs:
[rocketchat],
]
type:
- ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}
- ${{ (!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}
- fips
exclude:
- arch: arm64
Expand All @@ -384,7 +376,7 @@ jobs:
if: github.actor != 'dependabot[bot]'
env:
# add suffix for the extra images with coverage if building for production
DOCKER_TAG_SUFFIX_ROCKETCHAT: ${{ matrix.type == 'coverage' && (github.event_name == 'release' || github.ref == 'refs/heads/develop') && '-cov' || '' }}
DOCKER_TAG_SUFFIX_ROCKETCHAT: ${{ matrix.type == 'coverage' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop') && '-cov' || '' }}
with:
CR_USER: ${{ secrets.CR_USER }}
CR_PAT: ${{ secrets.CR_PAT }}
Expand All @@ -394,7 +386,7 @@ jobs:
arch: ${{ matrix.arch }}
service: ${{ matrix.service[0] }}
type: ${{ matrix.type }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop' }}

- name: Image ${{ matrix.service[1] || '"skipped"' }}
uses: ./.github/actions/build-docker
Expand All @@ -410,7 +402,7 @@ jobs:
arch: ${{ matrix.arch }}
service: ${{ matrix.service[1] }}
type: ${{ matrix.type }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop' }}
setup-docker: false

- name: Image ${{ matrix.service[2] || '"skipped"' }}
Expand All @@ -427,7 +419,7 @@ jobs:
arch: ${{ matrix.arch }}
service: ${{ matrix.service[2] }}
type: ${{ matrix.type }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop' }}
setup-docker: false

- name: Image ${{ matrix.service[3] || '"skipped"' }}
Expand All @@ -444,7 +436,7 @@ jobs:
arch: ${{ matrix.arch }}
service: ${{ matrix.service[3] }}
type: ${{ matrix.type }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }}
publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop' }}
setup-docker: false

build-gh-docker-publish:
Expand All @@ -458,7 +450,7 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop')
with:
sparse-checkout: |
docker-compose-ci.yml
Expand All @@ -467,23 +459,23 @@ jobs:
ref: ${{ github.ref }}

- name: Login to GitHub Container Registry
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop')
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ secrets.CR_USER }}
password: ${{ secrets.CR_PAT }}

- name: Download manifests
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop')
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: manifests-*
path: /tmp/manifests
merge-multiple: true

- name: Create and push multi-arch manifests
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop')
run: |
set -o xtrace
shopt -s nullglob
Expand Down Expand Up @@ -571,7 +563,6 @@ jobs:
run: |
./actionlint -color


# gating `checks` also skips every e2e test job (they all `need` it);
# test-storybook and test-unit don't, so they carry the same gate below
checks:
Expand Down Expand Up @@ -659,7 +650,7 @@ jobs:
deno-version: ${{ needs.release-versions.outputs.deno-version }}
lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }}
gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }}
retries: ${{ (github.event_name == 'release' || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }}
retries: ${{ (startsWith(github.ref, 'refs/tags/') || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }}
secrets:
CR_USER: ${{ secrets.CR_USER }}
CR_PAT: ${{ secrets.CR_PAT }}
Expand Down Expand Up @@ -750,7 +741,7 @@ jobs:
deno-version: ${{ needs.release-versions.outputs.deno-version }}
lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }}
gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }}
retries: ${{ (github.event_name == 'release' || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }}
retries: ${{ (startsWith(github.ref, 'refs/tags/') || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }}
secrets:
CR_USER: ${{ secrets.CR_USER }}
CR_PAT: ${{ secrets.CR_PAT }}
Expand Down Expand Up @@ -821,7 +812,7 @@ jobs:
deno-version: ${{ needs.release-versions.outputs.deno-version }}
lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }}
gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }}
retries: ${{ (github.event_name == 'release' || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }}
retries: ${{ (startsWith(github.ref, 'refs/tags/') || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }}
secrets:
CR_USER: ${{ secrets.CR_USER }}
CR_PAT: ${{ secrets.CR_PAT }}
Expand Down Expand Up @@ -860,7 +851,7 @@ jobs:
run: yarn build

- name: Login to GitHub Container Registry
if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]'
if: (github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
Expand All @@ -875,15 +866,15 @@ jobs:
# Download Docker images from build artifacts
- name: Download Docker images
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop'
if: github.event.pull_request.head.repo.full_name != github.repository && !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/develop'
with:
pattern: 'docker-image-rocketchat-amd64-coverage'
path: /tmp/docker-images
merge-multiple: true

# Load Docker images
- name: Load Docker images
if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop'
if: github.event.pull_request.head.repo.full_name != github.repository && !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/develop'
shell: bash
run: |
set -o xtrace
Expand Down Expand Up @@ -985,7 +976,23 @@ jobs:
tests-done:
name: ✅ Tests Done
runs-on: ubuntu-24.04-arm
needs: [test-guard, checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-api-livechat, test-api-livechat-ee, test-api-apps-deno-ee, test-api-fips, test-api-livechat-fips, test-ui-fips, test-federation-matrix]
needs:
[
test-guard,
checks,
test-unit,
test-api,
test-ui,
test-api-ee,
test-ui-ee,
test-api-livechat,
test-api-livechat-ee,
test-api-apps-deno-ee,
test-api-fips,
test-api-livechat-fips,
test-ui-fips,
test-federation-matrix,
]
if: always() && needs.test-guard.outputs.skip-tests != 'true'
steps:
- name: Test finish aggregation
Expand Down Expand Up @@ -1047,7 +1054,7 @@ jobs:
deploy:
name: 🚀 Publish build assets
runs-on: ubuntu-24.04-arm
if: github.event_name == 'release' || github.ref == 'refs/heads/develop'
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop'
needs: [build-gh-docker-publish, release-versions]

steps:
Expand All @@ -1073,7 +1080,7 @@ jobs:
run: |
REPO_VERSION=$(node -p "require('./package.json').version")

if [[ "${{ github.event_name }}" = 'release' ]]; then
if [[ "${{ startsWith(github.ref, 'refs/tags/') }}" = 'true' ]]; then
ARTIFACT_NAME="${REPO_VERSION}"
else
ARTIFACT_NAME="${REPO_VERSION}.$GITHUB_SHA"
Expand Down Expand Up @@ -1128,7 +1135,7 @@ jobs:
password: ${{ secrets.CR_PAT }}

- name: Download manifests
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop')
if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/develop')
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: manifests-*
Expand Down Expand Up @@ -1245,7 +1252,7 @@ jobs:
run: |
REPO_VERSION=$(node -p "require('./package.json').version")

if [[ '${{ github.event_name }}' = 'release' ]]; then
if [[ '${{ startsWith(github.ref, 'refs/tags/') }}' = 'true' ]]; then
GIT_TAG="${GITHUB_REF#*tags/}"
GIT_BRANCH=""
ARTIFACT_NAME="${REPO_VERSION}"
Expand All @@ -1271,10 +1278,46 @@ jobs:
# Makes build fail if the release isn't there
curl --fail "https://releases.rocket.chat/$RC_VERSION/info"

publish-github-release:
name: 🚀 Publish GitHub Release
runs-on: ubuntu-24.04-arm
if: startsWith(github.ref, 'refs/tags/')
needs: [docker-image-publish, release-versions]
permissions:
contents: write
steps:
# the release is created as a draft by the release-action and is only published
# here, after all artifacts are available, so users never see a release
# they can't install yet
- name: Remove draft from release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

# drafts are not addressable by tag (GET /releases/tags/{tag} returns 404),
# so find the release id by listing; the draft was created right before the
# tag push, so it's always within the most recent releases
RELEASE_ID=$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
--jq "[.[] | select(.tag_name == \"${GITHUB_REF_NAME}\")][0].id // empty")

if [[ -z "$RELEASE_ID" ]]; then
echo "No release found for tag ${GITHUB_REF_NAME}"
exit 1
fi

MAKE_LATEST=false
if [[ '${{ needs.release-versions.outputs.release }}' == 'latest' && '${{ needs.release-versions.outputs.latest-release }}' == "$GITHUB_REF_NAME" ]]; then
MAKE_LATEST=true
fi

gh api -X PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
-F draft=false -f make_latest="$MAKE_LATEST"
Comment on lines +1309 to +1315

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
sed -n '1130,1335p' .github/workflows/ci.yml | cat -n | sed 's/^/LINE /'

echo "== release-versions output definitions =="
rg -n "release-versions|outputs:\s*$|release:|latest-release|GH_REF_NAME|GITHUB_REF_NAME|MAKE_LATEST|publish" .github/workflows/ci.yml

echo "== focused snippets around candidates =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/ci.yml')
lines=p.read_text().splitlines()
ranges=[(1140,1190),(1260,1335)]
for a,b in ranges:
    print(f"\n--- lines {a}-{b} ---")
    for i in range(a,b+1):
        print(f"{i}: {lines[i-1]}")
PY

echo "== deterministic Bash single-quote interpolation illustration =="
python3 - <<'PY'
tags=["latest","latest'; printf 'INJECTED'; echo; #","latest\",\"latest'"]
for tag in tags:
    # The template expands GitHub Actions expression literally; assume outputs.latest-release becomes this tag.
    line = f"if [[ 'latest' == 'latest' && '{tag}' == \"main\" ]]; then ..."
    print(f"tag={tag!r}")
    print(f"generated_line={line}")
    try:
        exec(line)
        print("parsed_and_executed=no_syntax_error")
    except Exception as e:
        print(f"runtime_or_parse_{type(e).__name__.lower()}={e}")
PY

Repository: RocketChat/Rocket.Chat

Length of output: 27422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== release-versions candidate excerpt =="
sed -n '1,150p' .github/workflows/ci.yml | cat -n

echo "== repo refs to release-versions/latest outputs =="
rg -n "steps\.latest|GITHUB_OUTPUT|latest-release|needs\.release-versions\.outputs\.latest-release|needs\.release-versions\.outputs\.release" .github/workflows/ci.yml

echo "== deterministic Bash interpolation simulation for single-quoted injection =="
python3- <<'PY'
from pathlib import Path
workflow = Path(".github/workflows/ci.yml").read_text().splitlines()
template = workflow[1168]
print("template:", template)
for tag in ["latest", "latest'; printf 'INJECTED'>&2; echo \"; main", "latest\"latest'1", "latest' && [[ \"a\" == a ]]; then printf 'INJECTED' >&2; then :; "]:
    line = template.replace("${{ needs.release-versions.outputs.latest-release }}", tag)
    print("tag:", repr(tag))
    print("line:", line)
    try:
        exec(line)
        print("exec_result:", "success")
    except Exception as e:
        print("exec_exception:", type(e).__name__, str(e).splitlines()[0])
PY

Repository: RocketChat/Rocket.Chat

Length of output: 9205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deterministic Bash single-quote parsing probe =="
python3 <<'PY'
import subprocess, sys, textwrap

templates = [
    "if [[ '${latest}' == \"main\" ]]; then echo 1; fi",
    "if [[ '${latest}' == \"main\" ]]; then printf 'INJECTED' >&2; fi",
]
for t in templates:
    for latest in ["1.0.0", "latest'", "latest'; printf 'INJECTED' >&2; echo"]:
        code = t.replace("${latest}", latest)
        print(f"latest={latest!r}")
        print(f"code={code!r}")
        try:
            out = subprocess.run(
                ["bash", "-c", code],
                text=True,
                input="",
                stderr=subprocess.PIPE,
                timeout=5,
            )
            print(f"stdout={out.stdout!r} stderr={out.stderr!r} returncode={out.returncode}")
        except Exception as e:
            print(f"exception={type(e).__name__}: {e}")
        print("---")
PY

echo "== deterministic Bash source-string comparison quote-split example =="
bash -c 'latest="latest'\''"; if [[ '"'"''"'"'${latest}'"'"'"' == "main" ]]; then printf "matched\n"; fi' || true

echo "== local run with controlled output is unavailable: inspect fixed assignment pattern =="
sed -n '1155,1174p;1303,1311p' .github/workflows/ci.yml

Repository: RocketChat/Rocket.Chat

Length of output: 1594


Injection (CWE-78): Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Reachability: Internal

Do not embed latest-release directly in the Bash comparison.

The release tag flow is pushed to *, and latest-release reads the newest ls-remote tag. A tag name can contain a single quote, so using it inside ${{ ... }} can change the command source when that tag becomes the selected latest tag. Assign release and latest-release to environment variables first, then compare quoted shell variables in both the Docker publish job and the GitHub release marking step.

🧰 Tools
🪛 zizmor (1.29.0)

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

(template-injection)


[info] 1310-1310: 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/ci.yml around lines 1309 - 1315, Update both the Docker
publish job and GitHub release marking step to pass the release and
latest-release outputs through environment variables before entering Bash. In
the comparison around MAKE_LATEST, compare the quoted shell variables rather
than embedding `${{ needs.release-versions.outputs.latest-release }}` or the
release output directly, preserving the existing latest-tag selection behavior.

Source: Linters/SAST tools


docs-update:
name: Update Version Durability

if: github.event_name == 'release'
if: startsWith(github.ref, 'refs/tags/')
needs:
- docker-image-publish

Expand Down
Loading
Loading