Skip to content

fix(gantt): render bars for end dates that do not strictly match the dateFormat #21684

fix(gantt): render bars for end dates that do not strictly match the dateFormat

fix(gantt): render bars for end dates that do not strictly match the dateFormat #21684

Workflow file for this run

name: E2E
on:
push:
branches:
- develop
- master
- release/**
pull_request:
merge_group:
workflow_dispatch:
inputs: &workflow_inputs
group_argos_images:
description: Composite screenshots into batched sheets before Argos upload
type: boolean
default: true
spec_pattern:
description: Cypress spec glob (empty = full suite)
type: string
default: ''
workflow_call:
inputs: *workflow_inputs
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
env:
# For PRs and MergeQueues, the target commit is used, and for push events to non-develop branches, github.event.previous is used if available. Otherwise, 'develop' is used.
targetHash: >-
${{
github.event_name == 'workflow_dispatch' && 'develop' ||
github.event.pull_request.base.sha ||
github.event.merge_group.base_sha ||
(
(
(github.event_name == 'push' && github.ref == 'refs/heads/develop') ||
github.event.before == '0000000000000000000000000000000000000000'
) && 'develop'
) ||
github.event.before
}}
RUN_VISUAL_TEST: >-
${{
github.repository == 'mermaid-js/mermaid' && (
github.event_name == 'workflow_dispatch' ||
(github.event_name != 'pull_request' || !startsWith(github.head_ref, 'renovate/'))
)
}}
# Automatic runs always batch; manual runs can upload per-screenshot instead.
# `inputs.group_argos_images` is `null` for non-manual runs, so treat it like `true`.
GROUP_ARGOS_IMAGES: >-
${{ format('{0}', inputs.group_argos_images) == 'false' && 'false' || 'true' }}
# On a push to the baseline branch (develop), upload BOTH the grouped sheets
# and the individual per-test screenshots in a single Argos build, so the
# default build's baseline is a superset. Scoped grouped PRs and manual
# per-screenshot runs then both diff cleanly against it (each marked --subset).
ARGOS_UPLOAD_BOTH: >-
${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
jobs:
cache:
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-20.16.0-chrome-127.0.6533.88-1-ff-128.0.3-edge-127.0.2651.74-1
options: --user 1001
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ./cypress/snapshots
key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
# If a snapshot for a given Hash is not found, we checkout that commit, run the tests and cache the snapshots.
- name: Switch to base branch
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ env.targetHash }}
- name: Install dependencies
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
# just perform install
runTests: false
- name: Calculate bundle size
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true'}}
run: |
pnpm run build:viz
mkdir -p cypress/snapshots/stats/base
mv stats cypress/snapshots/stats/base
# Detect which diagram(s) the PR touches and determine the minimal set of
# Cypress specs to run.
detect-scope:
runs-on: ubuntu-latest
outputs:
spec_pattern: ${{ steps.scope.outputs.spec_pattern }}
matrix: ${{ steps.scope.outputs.matrix }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Need enough history to diff against the base commit.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
- name: Detect e2e scope
id: scope
run: |
if [ "$SPEC" != 'null' ]; then
SPEC=$(node -e '
// `SPEC` should be a JSON string, so unwrap it
const assert = require("node:assert");
const parsed = JSON.parse(process.env.SPEC);
assert.strictEqual(typeof parsed, "string", "SPEC must be a JSON string");
console.log(parsed);
')
if [ -n "$SPEC" ]; then
echo "spec_pattern=${SPEC}" >> "$GITHUB_OUTPUT"
echo "matrix=[1]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Manual run — scoped to: ${SPEC}"
else
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Manual run — full suite."
fi
exit 0
fi
# Feature flag off → always use full matrix
if [ "${E2E_SCOPE_BY_DIAGRAM}" != "true" ]; then
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Feature flag disabled — full suite."
exit 0
fi
BASE="${{ env.targetHash }}"
# For direct pushes to the main development branch, always run the
# full suite (no PR diff to scope against).
if [ "$BASE" = "develop" ] || [ "$BASE" = "master" ]; then
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Direct push to ${BASE} — full suite."
exit 0
fi
# Fetch the target branch so we can find the true merge-base.
git fetch origin develop --depth=1 2>/dev/null || true
MERGE_BASE=$(git merge-base HEAD origin/develop 2>/dev/null || echo "")
if [ -z "$MERGE_BASE" ]; then
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Could not determine merge-base — full suite."
exit 0
fi
echo "[detect-scope] merge-base: ${MERGE_BASE}"
# Get changed files and run the detection script.
SPEC=$(git diff --name-only "$MERGE_BASE" HEAD 2>/dev/null \
| node scripts/e2e-diagram-scope.mjs \
|| echo "")
# Check and set required no of containers to run
if [ "$SPEC" = "SKIP" ]; then
echo "spec_pattern=SKIP" >> "$GITHUB_OUTPUT"
echo "matrix=[1]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Only docs/ignorable files changed — e2e can be skipped."
elif [ -n "$SPEC" ]; then
echo "spec_pattern=${SPEC}" >> "$GITHUB_OUTPUT"
echo "matrix=[1]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Scoped to: ${SPEC}"
else
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Cannot scope — full suite."
fi
echo "[detect-scope] matrix output: $(grep '^matrix=' "$GITHUB_OUTPUT" | tail -1)"
env:
# Toggle diagram-scoped e2e: set this repository variable to 'true'
# (GitHub → Settings → Secrets and variables → Actions → Variables)
E2E_SCOPE_BY_DIAGRAM: "${{ vars.E2E_SCOPE_BY_DIAGRAM == 'true' }}"
# Either `null` (no inputs), or a JSON string to the spec_pattern.
# Empty JSON string (i.e. `""`) means run the full suite.
SPEC: ${{ toJSON( inputs.spec_pattern ) }}
e2e:
# Skip the entire e2e job when only docs/ignorable files changed.
if: needs.detect-scope.outputs.spec_pattern != 'SKIP'
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-20.16.0-chrome-127.0.6533.88-1-ff-128.0.3-edge-127.0.2651.74-1
options: --user 1001
needs: [cache, detect-scope]
strategy:
fail-fast: false
matrix:
containers: ${{ fromJSON(needs.detect-scope.outputs.matrix) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# uses version from "packageManager" field in package.json
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
# These cached snapshots are downloaded, providing the reference snapshots.
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ./cypress/snapshots
key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
- name: Install dependencies
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
runTests: false
- name: Output size diff
if: ${{ matrix.containers == 1 }}
run: |
pnpm run build:viz
mv stats cypress/snapshots/stats/head
echo '## Bundle size difference' >> "$GITHUB_STEP_SUMMARY"
echo '' >> "$GITHUB_STEP_SUMMARY"
npx tsx scripts/size.ts >> "$GITHUB_STEP_SUMMARY"
# Rebuild the served bundle with inline source maps so the Cypress run can
# collect native V8 coverage and map it back to source (no instrumentation).
# Gated to runs whose coverage is uploaded.
- name: Build coverage bundle for e2e coverage
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
run: pnpm build:coverage
# Install NPM dependencies, cache them correctly
# and run all Cypress tests (or a scoped subset when detect-scope fires).
- name: Cypress run
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
id: cypress
with:
install: false
start: pnpm run dev:coverage
wait-on: 'http://localhost:9000'
browser: chrome
# When detect-scope produced a pattern, run only those specs.
spec: ${{ needs.detect-scope.outputs.spec_pattern }}
# Disable recording if we don't have an API key
# e.g. if this action was run from a fork
record: ${{ env.RUN_VISUAL_TEST == 'true' && secrets.CYPRESS_RECORD_KEY != '' }}
env:
CYPRESS_COMMIT: ${{ github.sha }}
CYPRESS_RECORD_KEY: ${{ env.RUN_VISUAL_TEST == 'true' && secrets.CYPRESS_RECORD_KEY || ''}}
SPLIT: ${{ strategy.job-total }}
SPLIT_INDEX: ${{ strategy.job-index }}
SPLIT_FILE: 'cypress/timings.json'
VITEST_COVERAGE: true
# Stash each shard's lcov; the host-based `codecov-upload` job uploads them.
- name: Upload e2e coverage artifact
if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: e2e-coverage-${{ matrix.containers }}
path: coverage/cypress/lcov.info
if-no-files-found: ignore
retention-days: 1
- name: Upload raw screenshots for Argos batching
if: ${{ env.RUN_VISUAL_TEST == 'true' && !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-${{ matrix.containers }}
path: cypress/screenshots
if-no-files-found: ignore
retention-days: 1
# Upload coverage from the host, where curl + gpg are available; the
# cypress/browsers container lacks them and can't install them (non-root).
codecov-upload:
needs: e2e
if: ${{ !cancelled() && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download e2e coverage artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: e2e-coverage-*
path: coverage-artifacts
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
with:
directory: coverage-artifacts
flags: e2e
name: mermaid-codecov
fail_ci_if_error: false
verbose: true
token: 6845cc80-77ee-4e17-85a1-026cd95e0766
# Batch all per-container screenshots into folder-wise composite sheets and
# upload them to Argos in a single build. PR / merge-queue runs upload only the
# sheets; a push to develop additionally uploads the individual screenshots so
# the baseline is a superset (see ARGOS_UPLOAD_BOTH).
#
# Only runs when the e2e matrix fully succeeded: a failed/partial e2e run would
# otherwise upload an incomplete or wrong set of screenshots — and on develop
# that set becomes the baseline. `needs.e2e.result == 'success'` requires every
# shard to pass (the matrix is fail-fast: false, so its result is failure if any
# shard failed).
argos-batch:
if: >-
${{
github.repository == 'mermaid-js/mermaid' &&
(github.event_name == 'workflow_dispatch' ||
(github.event_name != 'pull_request' || !startsWith(github.head_ref, 'renovate/'))) &&
needs.detect-scope.outputs.spec_pattern != 'SKIP' &&
needs.e2e.result == 'success'
}}
needs: [detect-scope, e2e]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download screenshot artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: argos-screenshots-*
merge-multiple: true
path: cypress/screenshots
# cypress-split gives each shard a different spec subset, so Cypress names
# screenshots relative to that shard's spec common-ancestor (the rendering/
# prefix is kept or stripped depending on the mix). Re-root every screenshot
# under its spec's true cypress/integration path so names are deterministic
# across runs — otherwise the same diagram churns the Argos baseline.
- name: Canonicalize screenshot paths
run: pnpm run argos:canonicalize
- name: Build composite sheets
if: env.GROUP_ARGOS_IMAGES == 'true'
run: pnpm run argos:batch
# Baseline branch (develop): merge the grouped sheets and the individual
# screenshots into one tree and upload them as a SINGLE Argos build. The CLI
# uploads one directory and names each screenshot relative to it, so merging
# preserves both naming schemes — sheet names (<group>/<group>-NNN.png) and
# individual names (<group>/<spec>.spec.js/<name>.png) never collide and match
# exactly what grouped PR / manual runs upload. The default build thus becomes
# a superset baseline both modes can diff against (each marked --subset).
# No --subset here: a develop push runs the full suite and uploads both
# representations, so this is the complete, authoritative baseline.
#
# Sent as a single Argos build, but split into K parallel uploads sharing one
# nonce. Argos's PUT /builds/{buildId} finalize body inlines metadata for every
# screenshot, so uploading the whole superset in one request exceeds the API
# body limit ("request entity too large", HTTP 413). Each parallel index PUTs
# only its slice; --files globs stay relative to the same root so names are
# preserved, and Argos combines the indices into one build once all K report.
- name: Upload images to Argos
env:
ARGOS_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
mkdir -p cypress/argos-baseline
if [ "$ARGOS_UPLOAD_BOTH" = 'true' ]; then
cp -r cypress/argos-sheets/. cypress/argos-baseline/
cp -r cypress/screenshots/. cypress/argos-baseline/
elif [ "$GROUP_ARGOS_IMAGES" = 'true' ]; then
# PR / merge-queue / manual grouped runs: upload only the composite sheets.
cp -r cypress/argos-sheets/. cypress/argos-baseline/
export ARGOS_SUBSET=true
else
# Manual per-screenshot runs (workflow_dispatch with group_argos_images=false).
cp -r cypress/screenshots/. cypress/argos-baseline/
export ARGOS_SUBSET=true
fi
# Round-robin the leaf directories (those that directly hold images)
# into K buckets so each upload's finalize body carries only ~1/K of the
# suite. Bucketing by top-level folder instead put all of rendering/
# (~60% of the suite) into one bucket whose finalize body exceeded
# Argos's limit (HTTP 413) regardless of K; balancing at the leaf level
# keeps the biggest bucket to ~1/K.
mapfile -t groups < <(
cd cypress/argos-baseline &&
find . -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) |
sed 's#/[^/]*$##' | sort -u | sed 's#^\./##'
)
K=5
for i in $(seq 1 "$K"); do
globs=()
for j in "${!groups[@]}"; do
# Glob each leaf dir non-recursively (`/*` not `/**`) so the buckets
# stay a disjoint partition, and match only images: a `/**` glob would
# also pick up the `<name>.png.argos.json` metadata sidecars, which
# Argos treats as screenshots and whose doubled `<sidecar>.argos.json`
# path overflows the 255-byte filename limit (ENAMETOOLONG, surfaced
# as "Failed to read metadata").
if [ $(( j % K + 1 )) -eq "$i" ]; then globs+=( "${groups[$j]}/*.{png,jpg,jpeg}" ); fi
done
[ ${#globs[@]} -eq 0 ] && continue
npx argos upload cypress/argos-baseline \
--files "${globs[@]}" \
--parallel --parallel-total "$K" --parallel-index "$i"
done
e2e-required:
if: always()
needs: [detect-scope, e2e]
runs-on: ubuntu-latest
steps:
- name: Verify e2e outcome
env:
E2E_RESULT: ${{ needs.e2e.result }}
SCOPE_RESULT: ${{ needs.detect-scope.result }}
SPEC_PATTERN: ${{ needs.detect-scope.outputs.spec_pattern }}
run: |
echo "detect-scope result: ${SCOPE_RESULT}"
echo "detect-scope spec_pattern: ${SPEC_PATTERN}"
echo "e2e result: ${E2E_RESULT}"
case "${E2E_RESULT}" in
success|skipped)
echo "OK — required check satisfied."
;;
*)
echo "FAIL — e2e matrix did not complete successfully."
exit 1
;;
esac