snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
105
.github/actions/check-e2e-test-only/action.yml
vendored
Normal file
105
.github/actions/check-e2e-test-only/action.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: Check E2E Test Only
|
||||
description: Check if PR contains only E2E test changes and determine the appropriate docker image tag
|
||||
|
||||
inputs:
|
||||
base_sha:
|
||||
description: Base commit SHA (PR base)
|
||||
required: false
|
||||
head_sha:
|
||||
description: Head commit SHA (PR head)
|
||||
required: false
|
||||
pr_number:
|
||||
description: PR number (used to fetch SHAs via API if base_sha/head_sha not provided)
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
e2e_test_only:
|
||||
description: Whether the PR contains only E2E test changes (true/false)
|
||||
value: ${{ steps.check.outputs.e2e_test_only }}
|
||||
image_tag:
|
||||
description: Docker image tag to use (base branch ref for E2E-only, short SHA for mixed)
|
||||
value: ${{ steps.check.outputs.image_tag }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/check-e2e-test-only
|
||||
id: check
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
INPUT_BASE_SHA: ${{ inputs.base_sha }}
|
||||
INPUT_HEAD_SHA: ${{ inputs.head_sha }}
|
||||
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
# Resolve SHAs and base branch from PR number if not provided
|
||||
BASE_REF=""
|
||||
if [ -z "$INPUT_BASE_SHA" ] || [ -z "$INPUT_HEAD_SHA" ]; then
|
||||
if [ -z "$INPUT_PR_NUMBER" ]; then
|
||||
echo "::error::Either base_sha/head_sha or pr_number must be provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Resolving SHAs from PR #${INPUT_PR_NUMBER}"
|
||||
PR_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${INPUT_PR_NUMBER}")
|
||||
INPUT_BASE_SHA=$(echo "$PR_DATA" | jq -r '.base.sha')
|
||||
INPUT_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha')
|
||||
BASE_REF=$(echo "$PR_DATA" | jq -r '.base.ref')
|
||||
|
||||
if [ -z "$INPUT_BASE_SHA" ] || [ "$INPUT_BASE_SHA" = "null" ] || \
|
||||
[ -z "$INPUT_HEAD_SHA" ] || [ "$INPUT_HEAD_SHA" = "null" ]; then
|
||||
echo "::error::Could not resolve SHAs for PR #${INPUT_PR_NUMBER}"
|
||||
exit 1
|
||||
fi
|
||||
elif [ -n "$INPUT_PR_NUMBER" ]; then
|
||||
# SHAs provided but we still need the base branch ref
|
||||
BASE_REF=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${INPUT_PR_NUMBER}" --jq '.base.ref')
|
||||
fi
|
||||
|
||||
# Default to master if base ref could not be determined
|
||||
if [ -z "$BASE_REF" ] || [ "$BASE_REF" = "null" ]; then
|
||||
BASE_REF="master"
|
||||
fi
|
||||
echo "PR base branch: ${BASE_REF}"
|
||||
|
||||
SHORT_SHA="${INPUT_HEAD_SHA::7}"
|
||||
|
||||
# Get changed files - try git first, fall back to API
|
||||
CHANGED_FILES=$(git diff --name-only "$INPUT_BASE_SHA"..."$INPUT_HEAD_SHA" 2>/dev/null || \
|
||||
gh api "repos/${GITHUB_REPOSITORY}/pulls/${INPUT_PR_NUMBER}/files" --jq '.[].filename' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "::warning::Could not determine changed files, assuming not E2E-only"
|
||||
echo "e2e_test_only=false" >> $GITHUB_OUTPUT
|
||||
echo "image_tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
# Check if all files are E2E-related
|
||||
E2E_TEST_ONLY="true"
|
||||
while IFS= read -r file; do
|
||||
[ -z "$file" ] && continue
|
||||
if [[ ! "$file" =~ ^e2e-tests/ ]] && \
|
||||
[[ ! "$file" =~ ^\.github/workflows/e2e- ]] && \
|
||||
[[ ! "$file" =~ ^\.github/actions/ ]]; then
|
||||
echo "Non-E2E file found: $file"
|
||||
E2E_TEST_ONLY="false"
|
||||
break
|
||||
fi
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
echo "E2E test only: ${E2E_TEST_ONLY}"
|
||||
|
||||
# Set outputs
|
||||
echo "e2e_test_only=${E2E_TEST_ONLY}" >> $GITHUB_OUTPUT
|
||||
if [ "$E2E_TEST_ONLY" = "true" ] && \
|
||||
{ [ "$BASE_REF" = "master" ] || [[ "$BASE_REF" =~ ^release-[0-9]+\.[0-9]+$ ]]; }; then
|
||||
echo "image_tag=${BASE_REF}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "image_tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
66
.github/actions/load-buildenv/action.yml
vendored
Normal file
66
.github/actions/load-buildenv/action.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Load build environment
|
||||
description: >
|
||||
Loads build-server images uploaded by setup-buildenv into the local Docker
|
||||
daemon, and resolves the image name from the tag. Use in jobs that run the
|
||||
container directly (test jobs). For jobs that only need a shell inside the
|
||||
container, use run-in-buildenv instead.
|
||||
|
||||
inputs:
|
||||
build-server-tag:
|
||||
description: "Build image tag, from server/build/buildenv-tag.sh (e.g. go1.26.3-node24.11.1)"
|
||||
required: true
|
||||
fips-enabled:
|
||||
description: "Set to 'true' to resolve the FIPS build image"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
outputs:
|
||||
image:
|
||||
description: "Fully-qualified build image name (e.g. mattermost/mattermost-build-server:go1.26.3-node24.11.1)."
|
||||
value: ${{ steps.resolve.outputs.image }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set constants
|
||||
shell: bash
|
||||
run: |
|
||||
echo "BUILDENV_ARTIFACT=buildenv-image" >> "${GITHUB_ENV}"
|
||||
echo "BUILDENV_FIPS_ARTIFACT=buildenv-fips-image" >> "${GITHUB_ENV}"
|
||||
echo "BUILDENV_TMPDIR=/tmp/buildenv-save" >> "${GITHUB_ENV}"
|
||||
- name: Download buildenv artifact
|
||||
id: download
|
||||
if: ${{ inputs.fips-enabled != 'true' }}
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: ${{ env.BUILDENV_ARTIFACT }}
|
||||
path: ${{ env.BUILDENV_TMPDIR }}/
|
||||
- name: Load buildenv from artifact
|
||||
if: ${{ inputs.fips-enabled != 'true' && steps.download.outcome == 'success' }}
|
||||
shell: bash
|
||||
run: docker load -i "${BUILDENV_TMPDIR}/image.tar.gz"
|
||||
- name: Download buildenv-fips artifact
|
||||
id: download-fips
|
||||
if: ${{ inputs.fips-enabled == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: ${{ env.BUILDENV_FIPS_ARTIFACT }}
|
||||
path: ${{ env.BUILDENV_TMPDIR }}/
|
||||
- name: Load buildenv-fips from artifact
|
||||
if: ${{ inputs.fips-enabled == 'true' && steps.download-fips.outcome == 'success' }}
|
||||
shell: bash
|
||||
run: docker load -i "${BUILDENV_TMPDIR}/image-fips.tar.gz"
|
||||
- name: Resolve image name
|
||||
id: resolve
|
||||
shell: bash
|
||||
env:
|
||||
FIPS: ${{ inputs.fips-enabled }}
|
||||
TAG: ${{ inputs.build-server-tag }}
|
||||
run: |
|
||||
if [[ "${FIPS}" == 'true' ]]; then
|
||||
echo "image=mattermost/mattermost-build-server-fips:${TAG}" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "image=mattermost/mattermost-build-server:${TAG}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
46
.github/actions/pack-cypress-deps/action.yml
vendored
Normal file
46
.github/actions/pack-cypress-deps/action.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: "Pack Cypress deps artifact"
|
||||
description: >
|
||||
The producing half of the artifact transport for the Cypress deps: tars the
|
||||
node_modules and Cypress binary just installed by prep-deps and uploads them
|
||||
as a run-scoped artifact for this run's workers to unpack.
|
||||
|
||||
This is the fallback path, reached only when restore-cypress-deps found the
|
||||
cache key cold. It is not a cache — the artifact is scoped to one run, costs
|
||||
the shared Actions cache nothing, and is retained for a day. It must produce
|
||||
the payload restore-cypress-deps would otherwise have restored, since the
|
||||
workers accept either.
|
||||
|
||||
inputs:
|
||||
fips:
|
||||
description: >
|
||||
Whether this is a FIPS-edition run. e2e-tests-ci.yml invokes the cypress
|
||||
template twice per workflow run — once per edition — and artifact names are
|
||||
run-scoped, so the two invocations would otherwise collide. Must match the
|
||||
unpack step.
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
# -C so the archives hold paths relative to their extraction root, letting
|
||||
# unpack extract straight into the workspace and HOME.
|
||||
- name: ci/pack-cypress-deps
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tar -czf "${RUNNER_TEMP}/cypress-node-modules.tgz" -C "${GITHUB_WORKSPACE}" e2e-tests/cypress/node_modules
|
||||
tar -czf "${RUNNER_TEMP}/cypress-binary.tgz" -C "${HOME}" .cache/Cypress
|
||||
- name: ci/upload-cypress-deps
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: e2e-cypress-deps${{ inputs.fips == 'true' && '-fips' || '' }}
|
||||
path: |
|
||||
${{ runner.temp }}/cypress-node-modules.tgz
|
||||
${{ runner.temp }}/cypress-binary.tgz
|
||||
# Only ever consumed by workers in the same run.
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
# The tarballs are already gzipped; re-compressing them in the zip
|
||||
# wrapper costs CPU for no gain.
|
||||
compression-level: 0
|
||||
51
.github/actions/restore-cypress-deps/action.yml
vendored
Normal file
51
.github/actions/restore-cypress-deps/action.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: "Restore Cypress deps cache"
|
||||
description: >
|
||||
The Actions-cache transport for the Cypress deps — node_modules plus the
|
||||
Cypress binary — keyed on the cypress lockfile.
|
||||
|
||||
This is the fast path, taken whenever the key is warm: prep-deps asks whether
|
||||
it exists with lookup-only, and the workers do the real restore. Read-only, as
|
||||
e2e-ci-cache-warm-cypress-deps.yml is the only writer of the key, so PR and
|
||||
merge runs never write to the Actions cache.
|
||||
|
||||
When the key is cold the template falls back to the artifact transport instead
|
||||
— pack-cypress-deps into unpack-cypress-deps — which must land the same
|
||||
payload in the same paths, so the two transports are interchangeable.
|
||||
|
||||
inputs:
|
||||
lookup-only:
|
||||
description: "Probe whether the key exists without downloading the entry."
|
||||
required: false
|
||||
default: "false"
|
||||
fail-on-cache-miss:
|
||||
description: "Fail the step when the exact key is absent."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: "true on an exact-key hit"
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
key:
|
||||
description: "The computed cache key"
|
||||
value: ${{ steps.key.outputs.key }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: ci/compute-cypress-deps-key
|
||||
id: key
|
||||
shell: bash
|
||||
run: echo "key=e2e-cypress-deps-${{ runner.os }}-${{ hashFiles('e2e-tests/cypress/package-lock.json') }}" >> "${GITHUB_OUTPUT}"
|
||||
- name: ci/restore-cypress-deps
|
||||
id: restore
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
# No restore-keys: an older node_modules is not a usable substitute for
|
||||
# this lockfile, and a partial hit would silently diverge from `npm ci`.
|
||||
with:
|
||||
path: |
|
||||
e2e-tests/cypress/node_modules
|
||||
~/.cache/Cypress
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
lookup-only: ${{ inputs.lookup-only }}
|
||||
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
|
||||
13
.github/actions/restore-e2e-npm-cache/action.yml
vendored
Normal file
13
.github/actions/restore-e2e-npm-cache/action.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "Restore E2E npm registry cache"
|
||||
description: "Restore ~/.npm for cypress/playwright/api jobs (keyed on all E2E lockfiles)"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: ci/restore-npm-cache
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: e2e-npm-registry-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('e2e-tests/cypress/package-lock.json', 'e2e-tests/playwright/package-lock.json', 'api/package-lock.json') }}
|
||||
restore-keys: |
|
||||
e2e-npm-registry-${{ runner.os }}-${{ runner.arch }}-
|
||||
49
.github/actions/restore-go-cache/action.yml
vendored
Normal file
49
.github/actions/restore-go-cache/action.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Restore Go Cache
|
||||
description: >
|
||||
Read-only restore of the Go module cache, build cache, and CI tool binaries
|
||||
warmed by server-ci-cache-warm. Module/build caches live under RUNNER_TEMP
|
||||
(outside the checkout); tool binaries in server/bin.
|
||||
|
||||
outputs:
|
||||
gomodcache:
|
||||
description: "Go module cache path, for GOMODCACHE"
|
||||
value: ${{ steps.paths.outputs.gomodcache }}
|
||||
gocache:
|
||||
description: "Go build cache path, for GOCACHE"
|
||||
value: ${{ steps.paths.outputs.gocache }}
|
||||
gobin:
|
||||
description: "CI tool binary path, for GOBIN"
|
||||
value: ${{ steps.paths.outputs.gobin }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Resolve Go cache paths
|
||||
id: paths
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "gomodcache=${RUNNER_TEMP}/go-cache/mod"
|
||||
echo "gocache=${RUNNER_TEMP}/go-cache/build"
|
||||
echo "gobin=${GITHUB_WORKSPACE}/server/bin"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
- name: Create Go cache directories
|
||||
shell: bash
|
||||
env:
|
||||
GOMODCACHE: ${{ steps.paths.outputs.gomodcache }}
|
||||
GOCACHE: ${{ steps.paths.outputs.gocache }}
|
||||
GOBIN: ${{ steps.paths.outputs.gobin }}
|
||||
run: mkdir -p "${GOMODCACHE}" "${GOCACHE}" "${GOBIN}"
|
||||
- name: Restore Go module and build cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ runner.temp }}/go-cache/mod
|
||||
${{ runner.temp }}/go-cache/build
|
||||
server/bin
|
||||
key: gomod-v1-${{ hashFiles('server/.go-version') }}-${{ hashFiles('**/go.mod', '**/go.sum') }}
|
||||
# On a go.mod change, prefix-match the newest cache for this Go version
|
||||
# and download only the delta. The Go-version segment is never dropped:
|
||||
# build cache and tool binaries can't be reused across toolchains.
|
||||
restore-keys: |
|
||||
gomod-v1-${{ hashFiles('server/.go-version') }}-
|
||||
77
.github/actions/run-in-buildenv/action.yml
vendored
Normal file
77
.github/actions/run-in-buildenv/action.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Run in build environment
|
||||
description: >
|
||||
Runs a script inside the build-server container. Use for build and check
|
||||
jobs that don't need direct control over the docker run invocation.
|
||||
|
||||
Note the special handling of HEAD_REF, REF_NAME, and RUN_ID. Since the
|
||||
container is not otherwise given access to the runner's environment, we
|
||||
re-export github.head_ref, github.ref_name, and github.run_id as a
|
||||
special case for use in safely building up BUILD_NUMBER. In general,
|
||||
don't extend this: if your use case requires more customization, use
|
||||
load-buildenv and invoke docker run yourself.
|
||||
|
||||
inputs:
|
||||
run:
|
||||
description: "Bash script to execute inside the container"
|
||||
required: true
|
||||
build-server-tag:
|
||||
description: "Build image tag. Defaults to server/build/buildenv-tag.sh."
|
||||
required: false
|
||||
default: ""
|
||||
fips-enabled:
|
||||
description: "Set to 'true' to use the FIPS build image"
|
||||
required: false
|
||||
default: "false"
|
||||
working-directory:
|
||||
description: "Working directory inside the container (absolute path under /mattermost/)"
|
||||
required: false
|
||||
default: "/mattermost/server"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Read build image tag
|
||||
id: tag
|
||||
shell: bash
|
||||
env:
|
||||
TAG_INPUT: ${{ inputs.build-server-tag }}
|
||||
run: |
|
||||
if [[ -n "${TAG_INPUT}" ]]; then
|
||||
tag="${TAG_INPUT}"
|
||||
else
|
||||
tag=$(./server/build/buildenv-tag.sh)
|
||||
fi
|
||||
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
|
||||
- uses: ./.github/actions/load-buildenv
|
||||
id: buildenv
|
||||
with:
|
||||
build-server-tag: ${{ steps.tag.outputs.tag }}
|
||||
fips-enabled: ${{ inputs.fips-enabled }}
|
||||
- name: Run in buildenv
|
||||
shell: bash
|
||||
env:
|
||||
CMD: ${{ inputs.run }}
|
||||
IMAGE: ${{ steps.buildenv.outputs.image }}
|
||||
WORKING_DIR: ${{ inputs.working-directory }}
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
SCRIPT=$(mktemp)
|
||||
printf 'git config --global --add safe.directory /mattermost\n' > "${SCRIPT}"
|
||||
printf '%s\n' "${CMD}" >> "${SCRIPT}"
|
||||
# Reuse the Go caches warmed by server-ci-cache-warm and restored by
|
||||
# restore-go-cache (under RUNNER_TEMP, outside the checkout).
|
||||
docker run --rm \
|
||||
--network host \
|
||||
-e HEAD_REF -e REF_NAME -e RUN_ID \
|
||||
-e GOMODCACHE=/gocache/mod \
|
||||
-e GOCACHE=/gocache/build \
|
||||
-e GOBIN=/mattermost/server/bin \
|
||||
-v "${RUNNER_TEMP}/go-cache:/gocache" \
|
||||
-v "$PWD:/mattermost" \
|
||||
-v "${SCRIPT}:/run-script.sh" \
|
||||
-w "${WORKING_DIR}" \
|
||||
"${IMAGE}" \
|
||||
bash -eo pipefail /run-script.sh
|
||||
rm -f "${SCRIPT}"
|
||||
44
.github/actions/runner-prep-openldap/action.yml
vendored
Normal file
44
.github/actions/runner-prep-openldap/action.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Runner prep for openldap
|
||||
description: |
|
||||
Disable the Ubuntu 24.04 AppArmor user-namespace restriction and ensure
|
||||
docker-compose >= 2.36.0 before starting any stack that includes openldap.
|
||||
|
||||
Background: Ubuntu 24.04 sets kernel.apparmor_restrict_unprivileged_userns=1
|
||||
by default. The osixia/openldap:1.4.0 init scripts rely on unprivileged
|
||||
user namespaces; blocking them causes an immediate exit(1) with no useful
|
||||
stderr ("dependency failed to start: container mmserver-openldap-1 exited (1)").
|
||||
The container's own security_opt: apparmor:unconfined is insufficient — it
|
||||
only unconfines the slapd process, not the entrypoint. The fix must be at
|
||||
the host-kernel level.
|
||||
|
||||
docker-compose 2.35.1 (shipped on some ubuntu-24.04 runner images) also has
|
||||
a known `up` regression that surfaces as spurious dependency-failed errors
|
||||
under load. We upgrade to >= 2.36.0 when needed.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Disable AppArmor user-namespace restriction and ensure docker-compose >= 2.36.0
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Before: docker compose version"
|
||||
docker compose version || true
|
||||
|
||||
# Disable the AppArmor user-namespace restriction. Idempotent;
|
||||
# safe if the key doesn't exist (older kernel).
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
|
||||
# If docker-compose is older than 2.36.0, install a newer one to
|
||||
# the user's cli-plugins dir (takes precedence over the system copy).
|
||||
CURRENT=$(docker compose version --short 2>/dev/null || echo "0.0.0")
|
||||
NEED="2.36.0"
|
||||
if [ "$(printf '%s\n' "$NEED" "$CURRENT" | sort -V | head -n1)" != "$NEED" ]; then
|
||||
echo "Upgrading docker-compose from ${CURRENT} to 2.39.1"
|
||||
mkdir -p "$HOME/.docker/cli-plugins"
|
||||
curl -SL -o "$HOME/.docker/cli-plugins/docker-compose" \
|
||||
"https://github.com/docker/compose/releases/download/v2.39.1/docker-compose-linux-x86_64"
|
||||
chmod +x "$HOME/.docker/cli-plugins/docker-compose"
|
||||
fi
|
||||
|
||||
echo "After: docker compose version"
|
||||
docker compose version
|
||||
18
.github/actions/save-junit-report-tms/.env.example
vendored
Normal file
18
.github/actions/save-junit-report-tms/.env.example
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Example environment file for local testing
|
||||
# Copy this to .env and fill in your values
|
||||
# Note: GitHub Actions inputs use INPUT_ prefix with hyphens kept in the name
|
||||
|
||||
# Required inputs
|
||||
INPUT_REPORT-PATH=./example-report/report.xml
|
||||
INPUT_ZEPHYR-API-KEY=your-api-key-here
|
||||
INPUT_BUILD-IMAGE=mattermostdevelopment/mattermost-enterprise-edition:1234567
|
||||
|
||||
# GitHub environment variables (used to generate build-number internally)
|
||||
GITHUB_HEAD_REF=feature-branch
|
||||
GITHUB_REF_NAME=master
|
||||
GITHUB_RUN_ID=12345678
|
||||
GITHUB_REPOSITORY=mattermost/mattermost
|
||||
|
||||
# Optional inputs with defaults
|
||||
INPUT_ZEPHYR-FOLDER-ID=27504432
|
||||
INPUT_JIRA-PROJECT-KEY=MM
|
||||
4
.github/actions/save-junit-report-tms/.gitignore
vendored
Normal file
4
.github/actions/save-junit-report-tms/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
package-lock.json
|
||||
|
||||
.example-report
|
||||
77
.github/actions/save-junit-report-tms/README.md
vendored
Normal file
77
.github/actions/save-junit-report-tms/README.md
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
# Save JUnit Test Report to TMS Action
|
||||
|
||||
GitHub Action to save JUnit test reports to Zephyr Scale Test Management System.
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
- name: Save JUnit test report to Zephyr
|
||||
uses: ./.github/actions/save-junit-report-tms
|
||||
with:
|
||||
report-path: ./test-reports/report.xml
|
||||
zephyr-api-key: ${{ secrets.ZEPHYR_API_KEY }}
|
||||
build-image: ${{ env.BUILD_IMAGE }}
|
||||
zephyr-folder-id: '27504432' # Optional, defaults to 27504432
|
||||
jira-project-key: 'MM' # Optional, defaults to MM
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
|-------|-------------|----------|---------|
|
||||
| `report-path` | Path to the XML test report file (from artifact) | Yes | - |
|
||||
| `zephyr-api-key` | Zephyr Scale API key | Yes | - |
|
||||
| `build-image` | Docker build image used for testing | Yes | - |
|
||||
| `zephyr-folder-id` | Zephyr Scale folder ID | No | `27504432` |
|
||||
| `jira-project-key` | Jira project key | No | `MM` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
|--------|-------------|
|
||||
| `test-cycle` | The created test cycle key in Zephyr Scale |
|
||||
| `test-keys-execution-count` | Total number of test executions (including duplicates) |
|
||||
| `test-keys-unique-count` | Number of unique test keys successfully saved to Zephyr |
|
||||
| `junit-total-tests` | Total number of tests in the JUnit XML report |
|
||||
| `junit-total-passed` | Number of passed tests in the JUnit XML report |
|
||||
| `junit-total-failed` | Number of failed tests in the JUnit XML report |
|
||||
| `junit-pass-rate` | Pass rate percentage from the JUnit XML report |
|
||||
| `junit-duration-seconds` | Total test duration in seconds from the JUnit XML report |
|
||||
|
||||
## Local Development
|
||||
|
||||
1. Copy `.env.example` to `.env` and fill in your values
|
||||
2. Run `npm install` to install dependencies
|
||||
3. Run `npm run pretter` to format code
|
||||
4. Run `npm test` to run unit tests
|
||||
5. Run `npm run local-action` to test locally
|
||||
6. Run `npm run build` to build for production
|
||||
|
||||
### Submitting Code Changes
|
||||
|
||||
**IMPORTANT**: When submitting code changes, you must run the following checks locally as there are no CI jobs for this action:
|
||||
|
||||
1. Run `npm run prettier` to format your code
|
||||
2. Run `npm test` to ensure all tests pass
|
||||
3. Run `npm run build` to compile your changes
|
||||
4. Include the updated `dist/` folder in your commit
|
||||
|
||||
GitHub Actions runs the compiled code from the `dist/` folder, not the source TypeScript files. If you don't include the built files, your changes won't be reflected in the action.
|
||||
|
||||
## Report Format
|
||||
|
||||
The action expects a JUnit XML format report with test case names containing Zephyr test keys in the format `{PROJECT_KEY}-T{NUMBER}` (e.g., `MM-T1234`, `FOO-T5678`).
|
||||
|
||||
The test key pattern is automatically determined by the `jira-project-key` input (defaults to `MM`).
|
||||
|
||||
Example:
|
||||
```xml
|
||||
<testsuites tests="10" failures="2" errors="0" time="45.2">
|
||||
<testsuite name="mmctl tests" tests="10" failures="2" time="45.2" timestamp="2024-01-01T00:00:00Z">
|
||||
<testcase name="MM-T1234 - Test user creation" time="2.5"/>
|
||||
<testcase name="MM-T1235 - Test user login" time="3.2">
|
||||
<failure message="Login failed"/>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
```
|
||||
46
.github/actions/save-junit-report-tms/action.yaml
vendored
Normal file
46
.github/actions/save-junit-report-tms/action.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Save JUnit Test Report to TMS
|
||||
description: Save JUnit test report to Zephyr Scale Test Management System
|
||||
author: Mattermost
|
||||
|
||||
# Define your inputs here.
|
||||
inputs:
|
||||
report-path:
|
||||
description: Path to the XML test report file (from artifact)
|
||||
required: true
|
||||
zephyr-api-key:
|
||||
description: Zephyr Scale API key
|
||||
required: true
|
||||
build-image:
|
||||
description: Docker build image used for testing
|
||||
required: true
|
||||
zephyr-folder-id:
|
||||
description: Zephyr Scale folder ID
|
||||
required: false
|
||||
default: '27504432'
|
||||
jira-project-key:
|
||||
description: Jira project key
|
||||
required: false
|
||||
default: 'MM'
|
||||
|
||||
# Define your outputs here.
|
||||
outputs:
|
||||
test-cycle:
|
||||
description: The created test cycle key in Zephyr Scale
|
||||
test-keys-execution-count:
|
||||
description: Total number of test executions (including duplicates)
|
||||
test-keys-unique-count:
|
||||
description: Number of unique test keys successfully saved to Zephyr
|
||||
junit-total-tests:
|
||||
description: Total number of tests in the JUnit XML report
|
||||
junit-total-passed:
|
||||
description: Number of passed tests in the JUnit XML report
|
||||
junit-total-failed:
|
||||
description: Number of failed tests in the JUnit XML report
|
||||
junit-pass-rate:
|
||||
description: Pass rate percentage from the JUnit XML report
|
||||
junit-duration-seconds:
|
||||
description: Total test duration in seconds from the JUnit XML report
|
||||
|
||||
runs:
|
||||
using: node24
|
||||
main: dist/index.js
|
||||
14
.github/actions/save-junit-report-tms/jest.config.js
vendored
Normal file
14
.github/actions/save-junit-report-tms/jest.config.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/index.ts',
|
||||
],
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
verbose: true,
|
||||
// Suppress console output from code during tests
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
};
|
||||
19
.github/actions/save-junit-report-tms/jest.setup.js
vendored
Normal file
19
.github/actions/save-junit-report-tms/jest.setup.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// Mock @actions/core to suppress console output during tests
|
||||
jest.mock('@actions/core', () => ({
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
startGroup: jest.fn(),
|
||||
endGroup: jest.fn(),
|
||||
setOutput: jest.fn(),
|
||||
setFailed: jest.fn(),
|
||||
getInput: jest.fn(),
|
||||
summary: {
|
||||
addHeading: jest.fn().mockReturnThis(),
|
||||
addTable: jest.fn().mockReturnThis(),
|
||||
addLink: jest.fn().mockReturnThis(),
|
||||
addRaw: jest.fn().mockReturnThis(),
|
||||
write: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}));
|
||||
26
.github/actions/save-junit-report-tms/package.json
vendored
Normal file
26
.github/actions/save-junit-report-tms/package.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "save-junit-report-tms",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prettier": "npx prettier --write \"src/**/*.ts\"",
|
||||
"local-action": "local-action . src/main.ts .env",
|
||||
"test": "jest --verbose",
|
||||
"test:watch": "jest --watch --verbose",
|
||||
"test:silent": "jest --silent"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"fast-xml-parser": "5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@github/local-action": "6.0.2",
|
||||
"@types/jest": "30.0.0",
|
||||
"jest": "30.2.0",
|
||||
"ts-jest": "29.4.5",
|
||||
"tsup": "8.5.0",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
14
.github/actions/save-junit-report-tms/src/__tests__/fixtures/report-basic.xml
vendored
Normal file
14
.github/actions/save-junit-report-tms/src/__tests__/fixtures/report-basic.xml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites tests="5" failures="1" errors="0" skipped="1" time="10.5">
|
||||
<testsuite name="Test Suite 1" tests="5" failures="1" errors="0" skipped="1" time="10.5" timestamp="2024-01-15T10:00:00Z">
|
||||
<testcase name="MM-T1001 User can login" classname="auth" time="2.5"/>
|
||||
<testcase name="MM-T1002 User can logout" classname="auth" time="1.5"/>
|
||||
<testcase name="MM-T1003 User can reset password" classname="auth" time="3.0">
|
||||
<failure message="Password reset failed">Expected password to be reset but it wasn't</failure>
|
||||
</testcase>
|
||||
<testcase name="MM-T1004 User can view profile" classname="profile" time="2.0">
|
||||
<skipped message="Test skipped"/>
|
||||
</testcase>
|
||||
<testcase name="MM-T1005 User can update profile" classname="profile" time="1.5"/>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
15
.github/actions/save-junit-report-tms/src/__tests__/fixtures/report-duplicate-keys.xml
vendored
Normal file
15
.github/actions/save-junit-report-tms/src/__tests__/fixtures/report-duplicate-keys.xml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites tests="6" failures="2" errors="0" skipped="0" time="15.0">
|
||||
<testsuite name="Duplicate Key Tests" tests="6" failures="2" errors="0" skipped="0" time="15.0" timestamp="2024-01-15T11:00:00Z">
|
||||
<testcase name="MM-T4001 Test with retry - attempt 1" classname="retry" time="2.0">
|
||||
<failure message="First attempt failed"/>
|
||||
</testcase>
|
||||
<testcase name="MM-T4001 Test with retry - attempt 2" classname="retry" time="2.5"/>
|
||||
<testcase name="MM-T4002 Another test - run 1" classname="retry" time="3.0"/>
|
||||
<testcase name="MM-T4002 Another test - run 2" classname="retry" time="3.5">
|
||||
<failure message="Second run failed"/>
|
||||
</testcase>
|
||||
<testcase name="MM-T4003 Single test" classname="single" time="2.0"/>
|
||||
<testcase name="Test without MM key" classname="nokey" time="2.0"/>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
25
.github/actions/save-junit-report-tms/src/__tests__/fixtures/report-multiple-suites.xml
vendored
Normal file
25
.github/actions/save-junit-report-tms/src/__tests__/fixtures/report-multiple-suites.xml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites tests="10" failures="2" errors="1" skipped="1" time="25.75">
|
||||
<testsuite name="Authentication Tests" tests="5" failures="1" errors="0" skipped="0" time="12.5" timestamp="2024-01-15T10:00:00Z">
|
||||
<testcase name="MM-T2001 Admin can login" classname="auth" time="2.5"/>
|
||||
<testcase name="MM-T2002 Guest can login" classname="auth" time="2.0"/>
|
||||
<testcase name="MM-T2003 Invalid credentials rejected" classname="auth" time="3.5">
|
||||
<failure message="Test assertion failed">Expected rejection but got acceptance</failure>
|
||||
</testcase>
|
||||
<testcase name="MM-T2004 Session expires correctly" classname="auth" time="2.5"/>
|
||||
<testcase name="MM-T2005 Token refresh works" classname="auth" time="2.0"/>
|
||||
</testsuite>
|
||||
<testsuite name="Channel Tests" tests="5" failures="1" errors="1" skipped="1" time="13.25" timestamp="2024-01-15T10:05:00Z">
|
||||
<testcase name="MM-T3001 Create channel" classname="channel" time="1.5"/>
|
||||
<testcase name="MM-T3002 Delete channel" classname="channel" time="2.0">
|
||||
<failure message="Channel not deleted">Expected channel to be deleted</failure>
|
||||
</testcase>
|
||||
<testcase name="MM-T3003 Archive channel" classname="channel" time="1.75">
|
||||
<error message="Unexpected error">NullPointerException at line 45</error>
|
||||
</testcase>
|
||||
<testcase name="MM-T3004 Restore channel" classname="channel" time="3.0">
|
||||
<skipped message="Feature not ready"/>
|
||||
</testcase>
|
||||
<testcase name="MM-T3005 Rename channel" classname="channel" time="5.0"/>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
114
.github/actions/save-junit-report-tms/src/__tests__/helpers.test.ts
vendored
Normal file
114
.github/actions/save-junit-report-tms/src/__tests__/helpers.test.ts
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
import { sortTestExecutions } from "../main";
|
||||
import type { TestExecution } from "../types";
|
||||
|
||||
describe("sortTestExecutions", () => {
|
||||
it("should sort by status first (Pass, Fail, Not Executed)", () => {
|
||||
const executions: TestExecution[] = [
|
||||
{
|
||||
testCaseKey: "MM-T1003",
|
||||
statusName: "Fail",
|
||||
executionTime: 3.0,
|
||||
comment: "Test 3",
|
||||
},
|
||||
{
|
||||
testCaseKey: "MM-T1001",
|
||||
statusName: "Pass",
|
||||
executionTime: 1.0,
|
||||
comment: "Test 1",
|
||||
},
|
||||
{
|
||||
testCaseKey: "MM-T1004",
|
||||
statusName: "Not Executed",
|
||||
executionTime: 4.0,
|
||||
comment: "Test 4",
|
||||
},
|
||||
{
|
||||
testCaseKey: "MM-T1002",
|
||||
statusName: "Pass",
|
||||
executionTime: 2.0,
|
||||
comment: "Test 2",
|
||||
},
|
||||
];
|
||||
|
||||
const sorted = sortTestExecutions(executions);
|
||||
|
||||
// All Pass should come first
|
||||
expect(sorted[0].statusName).toBe("Pass");
|
||||
expect(sorted[1].statusName).toBe("Pass");
|
||||
// Then Fail
|
||||
expect(sorted[2].statusName).toBe("Fail");
|
||||
// Then Not Executed
|
||||
expect(sorted[3].statusName).toBe("Not Executed");
|
||||
});
|
||||
|
||||
it("should sort by test key within same status", () => {
|
||||
const executions: TestExecution[] = [
|
||||
{
|
||||
testCaseKey: "MM-T1003",
|
||||
statusName: "Pass",
|
||||
executionTime: 3.0,
|
||||
comment: "Test 3",
|
||||
},
|
||||
{
|
||||
testCaseKey: "MM-T1001",
|
||||
statusName: "Pass",
|
||||
executionTime: 1.0,
|
||||
comment: "Test 1",
|
||||
},
|
||||
{
|
||||
testCaseKey: "MM-T1002",
|
||||
statusName: "Pass",
|
||||
executionTime: 2.0,
|
||||
comment: "Test 2",
|
||||
},
|
||||
];
|
||||
|
||||
const sorted = sortTestExecutions(executions);
|
||||
|
||||
expect(sorted[0].testCaseKey).toBe("MM-T1001");
|
||||
expect(sorted[1].testCaseKey).toBe("MM-T1002");
|
||||
expect(sorted[2].testCaseKey).toBe("MM-T1003");
|
||||
});
|
||||
|
||||
it("should not mutate the original array", () => {
|
||||
const executions: TestExecution[] = [
|
||||
{
|
||||
testCaseKey: "MM-T1002",
|
||||
statusName: "Fail",
|
||||
executionTime: 2.0,
|
||||
comment: "Test 2",
|
||||
},
|
||||
{
|
||||
testCaseKey: "MM-T1001",
|
||||
statusName: "Pass",
|
||||
executionTime: 1.0,
|
||||
comment: "Test 1",
|
||||
},
|
||||
];
|
||||
|
||||
const sorted = sortTestExecutions(executions);
|
||||
|
||||
// Original array should remain unchanged
|
||||
expect(executions[0].testCaseKey).toBe("MM-T1002");
|
||||
expect(sorted[0].testCaseKey).toBe("MM-T1001");
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const sorted = sortTestExecutions([]);
|
||||
expect(sorted).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle single item", () => {
|
||||
const executions: TestExecution[] = [
|
||||
{
|
||||
testCaseKey: "MM-T1001",
|
||||
statusName: "Pass",
|
||||
executionTime: 1.0,
|
||||
comment: "Test 1",
|
||||
},
|
||||
];
|
||||
|
||||
const sorted = sortTestExecutions(executions);
|
||||
expect(sorted).toEqual(executions);
|
||||
});
|
||||
});
|
||||
285
.github/actions/save-junit-report-tms/src/__tests__/main.test.ts
vendored
Normal file
285
.github/actions/save-junit-report-tms/src/__tests__/main.test.ts
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
import * as path from "path";
|
||||
import { getTestData } from "../main";
|
||||
|
||||
describe("getTestData", () => {
|
||||
const config = {
|
||||
projectKey: "MM",
|
||||
zephyrFolderId: 27504432,
|
||||
branch: "feature-branch",
|
||||
buildImage:
|
||||
"mattermostdevelopment/mattermost-enterprise-edition:1234567",
|
||||
buildNumber: "feature-branch-12345678",
|
||||
githubRunUrl:
|
||||
"https://github.com/mattermost/mattermost/actions/runs/12345678",
|
||||
};
|
||||
|
||||
describe("Basic JUnit report parsing", () => {
|
||||
it("should parse a basic JUnit report correctly", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-basic.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Check test cycle metadata
|
||||
expect(result.testCycle.projectKey).toBe("MM");
|
||||
expect(result.testCycle.name).toBe(
|
||||
"mmctl: E2E Tests with feature-branch, mattermostdevelopment/mattermost-enterprise-edition:1234567, feature-branch-12345678",
|
||||
);
|
||||
expect(result.testCycle.statusName).toBe("Done");
|
||||
expect(result.testCycle.folderId).toBe(27504432);
|
||||
expect(result.testCycle.description).toContain("Test Summary:");
|
||||
expect(result.testCycle.description).toContain(
|
||||
"github.com/mattermost/mattermost/actions/runs",
|
||||
);
|
||||
|
||||
// Check JUnit stats
|
||||
expect(result.junitStats.totalTests).toBe(5);
|
||||
expect(result.junitStats.totalFailures).toBe(1);
|
||||
expect(result.junitStats.totalErrors).toBe(0);
|
||||
expect(result.junitStats.totalSkipped).toBe(1);
|
||||
expect(result.junitStats.totalPassed).toBe(4);
|
||||
expect(result.junitStats.totalTime).toBe(10.5);
|
||||
expect(result.junitStats.passRate).toBe("80.0");
|
||||
|
||||
// Check test key stats
|
||||
expect(result.testKeyStats.totalOccurrences).toBe(5);
|
||||
expect(result.testKeyStats.uniqueCount).toBe(5);
|
||||
expect(result.testKeyStats.passedCount).toBe(3);
|
||||
expect(result.testKeyStats.failedCount).toBe(1);
|
||||
expect(result.testKeyStats.skippedCount).toBe(1);
|
||||
expect(result.testKeyStats.failedKeys).toEqual(["MM-T1003"]);
|
||||
expect(result.testKeyStats.skippedKeys).toEqual(["MM-T1004"]);
|
||||
|
||||
// Check test executions
|
||||
expect(result.testExecutions).toHaveLength(5);
|
||||
expect(result.testExecutions[0].testCaseKey).toBe("MM-T1001");
|
||||
expect(result.testExecutions[0].statusName).toBe("Pass");
|
||||
expect(result.testExecutions[2].testCaseKey).toBe("MM-T1003");
|
||||
expect(result.testExecutions[2].statusName).toBe("Fail");
|
||||
expect(result.testExecutions[3].testCaseKey).toBe("MM-T1004");
|
||||
expect(result.testExecutions[3].statusName).toBe("Not Executed");
|
||||
});
|
||||
|
||||
it("should handle timestamps and set planned dates", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-basic.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
expect(result.testCycle.plannedStartDate).toBeDefined();
|
||||
expect(result.testCycle.plannedEndDate).toBeDefined();
|
||||
|
||||
const startDate = new Date(result.testCycle.plannedStartDate!);
|
||||
const endDate = new Date(result.testCycle.plannedEndDate!);
|
||||
|
||||
expect(startDate.getTime()).toBeLessThanOrEqual(endDate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Multiple test suites", () => {
|
||||
it("should aggregate stats from multiple testsuites", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-multiple-suites.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Aggregated stats
|
||||
expect(result.junitStats.totalTests).toBe(10);
|
||||
expect(result.junitStats.totalFailures).toBe(2);
|
||||
expect(result.junitStats.totalErrors).toBe(1);
|
||||
expect(result.junitStats.totalSkipped).toBe(1);
|
||||
expect(result.junitStats.totalTime).toBe(25.75);
|
||||
|
||||
// Test executions extracted
|
||||
expect(result.testExecutions).toHaveLength(10);
|
||||
expect(result.testKeyStats.uniqueCount).toBe(10);
|
||||
});
|
||||
|
||||
it("should handle latest timestamp from multiple suites", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-multiple-suites.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
expect(result.testCycle.plannedStartDate).toBeDefined();
|
||||
expect(result.testCycle.plannedEndDate).toBeDefined();
|
||||
|
||||
const startDate = new Date(result.testCycle.plannedStartDate!);
|
||||
const endDate = new Date(result.testCycle.plannedEndDate!);
|
||||
|
||||
// End date should be after start date
|
||||
expect(endDate.getTime()).toBeGreaterThan(startDate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Duplicate test keys", () => {
|
||||
it("should count test key occurrences separately from unique keys", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-duplicate-keys.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Total test executions created (including duplicates)
|
||||
expect(result.testExecutions).toHaveLength(5); // 5 tests with MM-T keys
|
||||
|
||||
// Test key statistics
|
||||
expect(result.testKeyStats.totalOccurrences).toBe(5);
|
||||
expect(result.testKeyStats.uniqueCount).toBe(3); // MM-T4001, MM-T4002, MM-T4003
|
||||
|
||||
// Status tracking for unique keys
|
||||
// MM-T4001: has both Pass and Fail
|
||||
// MM-T4002: has both Pass and Fail
|
||||
// MM-T4003: has only Pass
|
||||
expect(result.testKeyStats.passedCount).toBe(3); // MM-T4001, MM-T4002, MM-T4003
|
||||
expect(result.testKeyStats.failedCount).toBe(2); // MM-T4001, MM-T4002
|
||||
expect(result.testKeyStats.skippedCount).toBe(0);
|
||||
});
|
||||
|
||||
it("should create separate test executions for each occurrence", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-duplicate-keys.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Find MM-T4001 executions
|
||||
const t4001Executions = result.testExecutions.filter(
|
||||
(e) => e.testCaseKey === "MM-T4001",
|
||||
);
|
||||
expect(t4001Executions).toHaveLength(2);
|
||||
|
||||
// One should be Fail, one should be Pass
|
||||
const statuses = t4001Executions.map((e) => e.statusName).sort();
|
||||
expect(statuses).toEqual(["Fail", "Pass"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test execution data", () => {
|
||||
it("should return executions with correct status values", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-duplicate-keys.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Verify we have both Pass and Fail statuses
|
||||
const statuses = result.testExecutions.map((e) => e.statusName);
|
||||
expect(statuses).toContain("Pass");
|
||||
expect(statuses).toContain("Fail");
|
||||
|
||||
// Verify each execution has required fields
|
||||
result.testExecutions.forEach((execution) => {
|
||||
expect(execution.testCaseKey).toBeTruthy();
|
||||
expect(execution.statusName).toBeTruthy();
|
||||
expect(execution.comment).toBeTruthy();
|
||||
expect(typeof execution.executionTime).toBe("number");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test cycle description", () => {
|
||||
it("should include test summary in description", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-basic.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
expect(result.testCycle.description).toContain("Test Summary:");
|
||||
expect(result.testCycle.description).toContain("4 passed");
|
||||
expect(result.testCycle.description).toContain("1 failed");
|
||||
expect(result.testCycle.description).toContain("80.0% pass rate");
|
||||
expect(result.testCycle.description).toContain("10.5s duration");
|
||||
});
|
||||
|
||||
it("should include build details in description", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-basic.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
expect(result.testCycle.description).toContain(
|
||||
"branch: feature-branch",
|
||||
);
|
||||
expect(result.testCycle.description).toContain(
|
||||
"build image: mattermostdevelopment/mattermost-enterprise-edition:1234567",
|
||||
);
|
||||
expect(result.testCycle.description).toContain(
|
||||
"build number: feature-branch-12345678",
|
||||
);
|
||||
});
|
||||
|
||||
it("should include GitHub run URL", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-basic.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
expect(result.testCycle.description).toContain(
|
||||
"https://github.com/mattermost/mattermost/actions/runs/12345678",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle reports with no test keys", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-duplicate-keys.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Report has 6 total tests, but only 5 have MM-T keys
|
||||
expect(result.junitStats.totalTests).toBe(6);
|
||||
expect(result.testExecutions).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("should calculate 0% pass rate when all tests fail", async () => {
|
||||
// Using report-multiple-suites which has failures
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"report-multiple-suites.xml",
|
||||
);
|
||||
const result = await getTestData(reportPath, config);
|
||||
|
||||
// Pass rate should be less than 100%
|
||||
expect(parseFloat(result.junitStats.passRate)).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("should throw error for non-existent file", async () => {
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"non-existent.xml",
|
||||
);
|
||||
|
||||
await expect(getTestData(reportPath, config)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should throw error for invalid XML", async () => {
|
||||
// This would require creating an invalid XML fixture
|
||||
// Skipping for now, but you could add one
|
||||
});
|
||||
});
|
||||
});
|
||||
3
.github/actions/save-junit-report-tms/src/index.ts
vendored
Normal file
3
.github/actions/save-junit-report-tms/src/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { run } from "./main";
|
||||
|
||||
run();
|
||||
563
.github/actions/save-junit-report-tms/src/main.ts
vendored
Normal file
563
.github/actions/save-junit-report-tms/src/main.ts
vendored
Normal file
@@ -0,0 +1,563 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as fs from "fs/promises";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import type {
|
||||
TestExecution,
|
||||
TestCycle,
|
||||
TestData,
|
||||
ZephyrApiClient,
|
||||
} from "./types";
|
||||
|
||||
const zephyrCloudApiUrl = "https://api.zephyrscale.smartbear.com/v2";
|
||||
|
||||
function newTestExecution(
|
||||
testCaseKey: string,
|
||||
statusName: string,
|
||||
executionTime: number,
|
||||
comment: string,
|
||||
): TestExecution {
|
||||
return {
|
||||
testCaseKey,
|
||||
statusName, // Pass or Fail
|
||||
executionTime,
|
||||
comment,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getTestData(
|
||||
junitFile: string,
|
||||
config: {
|
||||
projectKey: string;
|
||||
zephyrFolderId: number;
|
||||
branch: string;
|
||||
buildImage: string;
|
||||
buildNumber: string;
|
||||
githubRunUrl: string;
|
||||
},
|
||||
): Promise<TestData> {
|
||||
const testCycle: TestCycle = {
|
||||
projectKey: config.projectKey,
|
||||
name: `mmctl: E2E Tests with ${config.branch}, ${config.buildImage}, ${config.buildNumber}`,
|
||||
description: "",
|
||||
statusName: "Done",
|
||||
folderId: config.zephyrFolderId,
|
||||
};
|
||||
|
||||
// Create dynamic regex based on project key (e.g., MM-T1234, FOO-T5678)
|
||||
const reTestKey = new RegExp(`${config.projectKey}-T\\d+`);
|
||||
const testExecutions: TestExecution[] = [];
|
||||
|
||||
const data = await fs.readFile(junitFile, "utf8");
|
||||
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "@_",
|
||||
});
|
||||
|
||||
const result = parser.parse(data);
|
||||
|
||||
// Parse test results and collect test data
|
||||
let totalTests = 0;
|
||||
let totalFailures = 0;
|
||||
let totalErrors = 0;
|
||||
let totalSkipped = 0;
|
||||
let totalTime = 0;
|
||||
let earliestTimestamp: Date | null = null;
|
||||
let latestTimestamp: Date | null = null;
|
||||
|
||||
// Track test keys by status
|
||||
const testKeysPassed = new Set<string>();
|
||||
const testKeysFailed = new Set<string>();
|
||||
const testKeysSkipped = new Set<string>();
|
||||
let totalTestKeyOccurrences = 0;
|
||||
|
||||
if (result?.testsuites?.testsuite) {
|
||||
const testsuites = Array.isArray(result.testsuites.testsuite)
|
||||
? result.testsuites.testsuite
|
||||
: [result.testsuites.testsuite];
|
||||
|
||||
for (const testsuite of testsuites) {
|
||||
const tests = parseInt(testsuite["@_tests"] || "0", 10);
|
||||
const failures = parseInt(testsuite["@_failures"] || "0", 10);
|
||||
const errors = parseInt(testsuite["@_errors"] || "0", 10);
|
||||
const skipped = parseInt(testsuite["@_skipped"] || "0", 10);
|
||||
const time = parseFloat(testsuite["@_time"] || "0");
|
||||
|
||||
totalTests += tests;
|
||||
totalFailures += failures;
|
||||
totalErrors += errors;
|
||||
totalSkipped += skipped;
|
||||
totalTime += time;
|
||||
|
||||
// Extract timestamp if available
|
||||
const timestamp = testsuite["@_timestamp"];
|
||||
if (timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
if (!isNaN(date.getTime())) {
|
||||
if (!earliestTimestamp || date < earliestTimestamp) {
|
||||
earliestTimestamp = date;
|
||||
}
|
||||
if (!latestTimestamp || date > latestTimestamp) {
|
||||
latestTimestamp = date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (testsuite?.testcase) {
|
||||
const testcases = Array.isArray(testsuite.testcase)
|
||||
? testsuite.testcase
|
||||
: [testsuite.testcase];
|
||||
|
||||
for (const testcase of testcases) {
|
||||
const testName = testcase["@_name"];
|
||||
const testTime = testcase["@_time"] || 0;
|
||||
const hasFailure = testcase.failure !== undefined;
|
||||
const hasSkipped = testcase.skipped !== undefined;
|
||||
|
||||
if (testName) {
|
||||
const match = testName.match(reTestKey);
|
||||
|
||||
if (match !== null) {
|
||||
const testKey = match[0];
|
||||
totalTestKeyOccurrences++;
|
||||
testCycle.description += `* ${testKey} - ${testTime}s\n`;
|
||||
|
||||
let statusName: string;
|
||||
if (hasSkipped) {
|
||||
statusName = "Not Executed";
|
||||
testKeysSkipped.add(testKey);
|
||||
} else if (hasFailure) {
|
||||
statusName = "Fail";
|
||||
testKeysFailed.add(testKey);
|
||||
} else {
|
||||
statusName = "Pass";
|
||||
testKeysPassed.add(testKey);
|
||||
}
|
||||
|
||||
testExecutions.push(
|
||||
newTestExecution(
|
||||
testKey,
|
||||
statusName,
|
||||
parseFloat(testTime),
|
||||
testName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log detailed summary
|
||||
core.startGroup("JUnit report summary");
|
||||
core.info(` - Total tests: ${totalTests}`);
|
||||
core.info(` - Failures: ${totalFailures}`);
|
||||
core.info(` - Errors: ${totalErrors}`);
|
||||
core.info(` - Skipped: ${totalSkipped}`);
|
||||
const timeInMinutes = (totalTime / 60).toFixed(1);
|
||||
core.info(` - Duration: ${totalTime.toFixed(1)}s (~${timeInMinutes}m)`);
|
||||
core.endGroup();
|
||||
|
||||
core.startGroup("Extracted MM-T test cases");
|
||||
const uniqueTestKeys = new Set([
|
||||
...testKeysPassed,
|
||||
...testKeysFailed,
|
||||
...testKeysSkipped,
|
||||
]);
|
||||
core.info(` - Total test key occurrences: ${totalTestKeyOccurrences}`);
|
||||
core.info(` - Unique test keys: ${uniqueTestKeys.size}`);
|
||||
core.info(` - Passed: ${testKeysPassed.size} test keys`);
|
||||
if (testKeysFailed.size > 0) {
|
||||
core.info(
|
||||
` - Failed: ${testKeysFailed.size} test keys (${Array.from(testKeysFailed).join(", ")})`,
|
||||
);
|
||||
} else {
|
||||
core.info(` - Failed: ${testKeysFailed.size} test keys`);
|
||||
}
|
||||
if (testKeysSkipped.size > 0) {
|
||||
core.info(
|
||||
` - Skipped: ${testKeysSkipped.size} test keys (${Array.from(testKeysSkipped).join(", ")})`,
|
||||
);
|
||||
} else {
|
||||
core.info(` - Skipped: ${testKeysSkipped.size} test keys`);
|
||||
}
|
||||
core.endGroup();
|
||||
|
||||
// Build the description with summary and link
|
||||
const passedTests = totalTests - totalFailures;
|
||||
const passRate =
|
||||
totalTests > 0 ? ((passedTests / totalTests) * 100).toFixed(1) : "0";
|
||||
|
||||
testCycle.description = `Test Summary: `;
|
||||
testCycle.description += `${passedTests} passed | `;
|
||||
testCycle.description += `${totalFailures} failed | `;
|
||||
testCycle.description += `${passRate}% pass rate | `;
|
||||
testCycle.description += `${totalTime.toFixed(1)}s duration | `;
|
||||
|
||||
testCycle.description += `branch: ${config.branch} | `;
|
||||
testCycle.description += `build image: ${config.buildImage} | `;
|
||||
testCycle.description += `build number: ${config.buildNumber} | `;
|
||||
|
||||
testCycle.description += `${config.githubRunUrl}`;
|
||||
|
||||
// Calculate and set planned start and end dates
|
||||
if (earliestTimestamp) {
|
||||
// Use the earliest timestamp from the report as the start date
|
||||
testCycle.plannedStartDate = earliestTimestamp.toISOString();
|
||||
|
||||
// Calculate end date: if we have a latest timestamp, use it
|
||||
// Otherwise, calculate from start + total duration
|
||||
if (latestTimestamp && latestTimestamp > earliestTimestamp) {
|
||||
testCycle.plannedEndDate = latestTimestamp.toISOString();
|
||||
} else {
|
||||
// Add total duration (in seconds) to start time
|
||||
const endDate = new Date(
|
||||
earliestTimestamp.getTime() + totalTime * 1000,
|
||||
);
|
||||
testCycle.plannedEndDate = endDate.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
testCycle,
|
||||
testExecutions,
|
||||
junitStats: {
|
||||
totalTests,
|
||||
totalFailures,
|
||||
totalErrors,
|
||||
totalSkipped,
|
||||
totalPassed: passedTests,
|
||||
passRate,
|
||||
totalTime,
|
||||
},
|
||||
testKeyStats: {
|
||||
totalOccurrences: totalTestKeyOccurrences,
|
||||
uniqueCount: uniqueTestKeys.size,
|
||||
passedCount: testKeysPassed.size,
|
||||
failedCount: testKeysFailed.size,
|
||||
skippedCount: testKeysSkipped.size,
|
||||
failedKeys: Array.from(testKeysFailed),
|
||||
skippedKeys: Array.from(testKeysSkipped),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Sort test executions by status (Pass, Fail, Not Executed), then by test key
|
||||
export function sortTestExecutions(
|
||||
executions: TestExecution[],
|
||||
): TestExecution[] {
|
||||
const statusOrder: Record<string, number> = {
|
||||
Pass: 1,
|
||||
Fail: 2,
|
||||
"Not Executed": 3,
|
||||
};
|
||||
|
||||
return [...executions].sort((a, b) => {
|
||||
// First, sort by status
|
||||
const statusA = statusOrder[a.statusName] || 999;
|
||||
const statusB = statusOrder[b.statusName] || 999;
|
||||
const statusComparison = statusA - statusB;
|
||||
if (statusComparison !== 0) {
|
||||
return statusComparison;
|
||||
}
|
||||
|
||||
// Then, sort by test key
|
||||
return a.testCaseKey.localeCompare(b.testCaseKey);
|
||||
});
|
||||
}
|
||||
|
||||
// Write GitHub Actions summary using core.summary API
|
||||
export async function writeGitHubSummary(
|
||||
testCycle: TestCycle,
|
||||
junitStats: TestData["junitStats"],
|
||||
testKeyStats: TestData["testKeyStats"],
|
||||
successCount: number,
|
||||
failureCount: number,
|
||||
uniqueSavedTestKeys: Set<string>,
|
||||
uniqueFailedTestKeys: Set<string>,
|
||||
projectKey: string,
|
||||
testCycleKey: string,
|
||||
): Promise<void> {
|
||||
const timeInMinutes = (junitStats.totalTime / 60).toFixed(1);
|
||||
const zephyrUrl = `https://mattermost.atlassian.net/projects/${projectKey}?selectedItem=com.atlassian.plugins.atlassian-connect-plugin:com.kanoah.test-manager__main-project-page#!/v2/testCycle/${testCycleKey}`;
|
||||
|
||||
const summary = core.summary
|
||||
.addHeading("mmctl: E2E Test Report", 2)
|
||||
.addHeading("JUnit report summary", 3)
|
||||
.addTable([
|
||||
["Total tests", `${junitStats.totalTests}`],
|
||||
["Passed", `${junitStats.totalPassed}`],
|
||||
["Failed", `${junitStats.totalFailures}`],
|
||||
["Skipped", `${junitStats.totalSkipped}`],
|
||||
["Error", `${junitStats.totalErrors}`],
|
||||
[
|
||||
"Duration",
|
||||
`${junitStats.totalTime.toFixed(1)}s (~${timeInMinutes}m)`,
|
||||
],
|
||||
])
|
||||
.addHeading("Extracted MM-T test cases", 3)
|
||||
.addTable([
|
||||
["Total tests found", `${testKeyStats.totalOccurrences}`],
|
||||
["Unique test keys", `${testKeyStats.uniqueCount}`],
|
||||
["Passed", `${testKeyStats.passedCount} test keys`],
|
||||
["Failed", `${testKeyStats.failedCount} test keys`],
|
||||
["Skipped", `${testKeyStats.skippedCount} test keys`],
|
||||
])
|
||||
.addHeading("Zephyr Scale Results", 3)
|
||||
.addTable([
|
||||
["Test cycle key", `${testCycleKey}`],
|
||||
["Test cycle name", `${testCycle.name}`],
|
||||
[
|
||||
"Successfully saved",
|
||||
`${successCount} executions (${uniqueSavedTestKeys.size} unique test keys)`,
|
||||
],
|
||||
|
||||
...(failureCount === 0
|
||||
? []
|
||||
: [
|
||||
[
|
||||
"Failed on saving",
|
||||
`${failureCount} executions (${uniqueFailedTestKeys.size} unique test keys)`,
|
||||
],
|
||||
]),
|
||||
])
|
||||
.addLink("View in Zephyr", zephyrUrl);
|
||||
|
||||
await summary.write();
|
||||
}
|
||||
|
||||
// Create Zephyr API client implementation
|
||||
export function createZephyrApiClient(apiKey: string): ZephyrApiClient {
|
||||
return {
|
||||
async createTestCycle(testCycle: TestCycle): Promise<{ key: string }> {
|
||||
const response = await fetch(`${zephyrCloudApiUrl}/testcycles`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(testCycle),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorDetails = await response.json();
|
||||
throw new Error(
|
||||
`Failed to create test cycle: ${JSON.stringify(errorDetails)} (Status: ${response.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
async saveTestExecution(
|
||||
testExecution: TestExecution,
|
||||
retries = 3,
|
||||
): Promise<void> {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${zephyrCloudApiUrl}/testexecutions`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type":
|
||||
"application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(testExecution),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(
|
||||
`HTTP ${response.status}: ${errorBody}`,
|
||||
);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
core.info(
|
||||
`Saved test execution: ${testExecution.testCaseKey} (${testExecution.statusName}) - Response: ${JSON.stringify(responseData)}`,
|
||||
);
|
||||
return; // Success
|
||||
} catch (error) {
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
core.warning(
|
||||
`Error saving test execution for ${testExecution.testCaseKey} (attempt ${attempt}/${retries}): ${errorMsg}`,
|
||||
);
|
||||
|
||||
if (attempt === retries) {
|
||||
throw new Error(
|
||||
`Failed after ${retries} attempts: ${errorMsg}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Wait before retry (exponential backoff)
|
||||
const delay = 1000 * attempt;
|
||||
core.info(
|
||||
`Retrying ${testExecution.testCaseKey} in ${delay}ms...`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
// GitHub environment variables
|
||||
const branch =
|
||||
process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || "unknown";
|
||||
const githubRepository = process.env.GITHUB_REPOSITORY || "";
|
||||
const githubRunId = process.env.GITHUB_RUN_ID || "";
|
||||
const githubRunUrl =
|
||||
githubRepository && githubRunId
|
||||
? `https://github.com/${githubRepository}/actions/runs/${githubRunId}`
|
||||
: "";
|
||||
|
||||
// Generate build number from GitHub environment variables
|
||||
const buildNumber = `${branch}-${githubRunId}`;
|
||||
|
||||
// Required inputs
|
||||
const reportPath = core.getInput("report-path", { required: true });
|
||||
const buildImage = core.getInput("build-image", { required: true });
|
||||
const zephyrApiKey = core.getInput("zephyr-api-key", { required: true });
|
||||
|
||||
// Optional inputs with defaults
|
||||
const zephyrFolderId = parseInt(
|
||||
core.getInput("zephyr-folder-id") || "27504432",
|
||||
10,
|
||||
);
|
||||
const projectKey = core.getInput("jira-project-key") || "MM";
|
||||
|
||||
// Validate required fields
|
||||
if (!reportPath) {
|
||||
throw new Error("report-path is required");
|
||||
}
|
||||
if (!buildImage) {
|
||||
throw new Error("build-image is required");
|
||||
}
|
||||
if (!zephyrApiKey) {
|
||||
throw new Error("zephyr-api-key is required");
|
||||
}
|
||||
|
||||
core.info(`Reading report file from: ${reportPath}`);
|
||||
core.info(` - Branch: ${branch}`);
|
||||
core.info(` - Build Image: ${buildImage}`);
|
||||
core.info(` - Build Number: ${buildNumber}`);
|
||||
|
||||
const { testCycle, testExecutions, junitStats, testKeyStats } =
|
||||
await getTestData(reportPath, {
|
||||
projectKey,
|
||||
zephyrFolderId,
|
||||
branch,
|
||||
buildImage,
|
||||
buildNumber,
|
||||
githubRunUrl,
|
||||
});
|
||||
|
||||
const client = createZephyrApiClient(zephyrApiKey);
|
||||
|
||||
core.startGroup("Creating test cycle and saving test executions in Zephyr");
|
||||
|
||||
// Create test cycle
|
||||
const createdTestCycle = await client.createTestCycle(testCycle);
|
||||
core.info(`Created test cycle: ${createdTestCycle.key}`);
|
||||
|
||||
// Sort and save test executions
|
||||
const sortedExecutions = sortTestExecutions(testExecutions);
|
||||
|
||||
const promises = sortedExecutions.map((testExecution) => {
|
||||
// Add project key and test cycle key
|
||||
testExecution.projectKey = projectKey;
|
||||
testExecution.testCycleKey = createdTestCycle.key;
|
||||
|
||||
return client
|
||||
.saveTestExecution(testExecution)
|
||||
.then(() => ({
|
||||
success: true,
|
||||
testCaseKey: testExecution.testCaseKey,
|
||||
}))
|
||||
.catch((error) => ({
|
||||
success: false,
|
||||
testCaseKey: testExecution.testCaseKey,
|
||||
error: error.message,
|
||||
}));
|
||||
});
|
||||
const results = await Promise.all(promises);
|
||||
core.endGroup();
|
||||
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
const savedTestKeys: string[] = [];
|
||||
const failedTestKeys: string[] = [];
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
savedTestKeys.push(result.testCaseKey);
|
||||
} else {
|
||||
failureCount++;
|
||||
failedTestKeys.push(result.testCaseKey);
|
||||
const error = "error" in result ? result.error : "Unknown error";
|
||||
core.warning(
|
||||
`Test execution failed for ${result.testCaseKey}: ${error}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate unique test keys
|
||||
const uniqueSavedTestKeys = new Set(savedTestKeys);
|
||||
const uniqueFailedTestKeys = new Set(failedTestKeys);
|
||||
|
||||
// Create GitHub Actions summary (only if running in GitHub Actions environment)
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
await writeGitHubSummary(
|
||||
testCycle,
|
||||
junitStats,
|
||||
testKeyStats,
|
||||
successCount,
|
||||
failureCount,
|
||||
uniqueSavedTestKeys,
|
||||
uniqueFailedTestKeys,
|
||||
projectKey,
|
||||
createdTestCycle.key,
|
||||
);
|
||||
}
|
||||
|
||||
core.startGroup("Zephyr Scale Results");
|
||||
core.info(`Test cycle key: ${createdTestCycle.key}`);
|
||||
core.info(`Test cycle name: ${testCycle.name}`);
|
||||
core.info(
|
||||
`Successfully saved: ${successCount} executions (${uniqueSavedTestKeys.size} unique test keys)`,
|
||||
);
|
||||
if (failureCount > 0) {
|
||||
core.info(
|
||||
`Failed to save: ${failureCount} executions (${uniqueFailedTestKeys.size} unique test keys)`,
|
||||
);
|
||||
}
|
||||
core.info(
|
||||
`View in Zephyr: https://mattermost.atlassian.net/projects/${projectKey}?selectedItem=com.atlassian.plugins.atlassian-connect-plugin:com.kanoah.test-manager__main-project-page#!/v2/testCycle/${createdTestCycle.key}`,
|
||||
);
|
||||
|
||||
if (failedTestKeys.length > 0) {
|
||||
core.info(
|
||||
`Failed test keys (${failedTestKeys.length} total, ${uniqueFailedTestKeys.size} unique): ${failedTestKeys.join(", ")}`,
|
||||
);
|
||||
}
|
||||
core.endGroup();
|
||||
|
||||
// JUnit summary outputs
|
||||
core.setOutput("junit-total-tests", junitStats.totalTests);
|
||||
core.setOutput("junit-total-passed", junitStats.totalPassed);
|
||||
core.setOutput("junit-total-failed", junitStats.totalFailures);
|
||||
core.setOutput("junit-pass-rate", junitStats.passRate);
|
||||
core.setOutput("junit-duration-seconds", junitStats.totalTime.toFixed(1));
|
||||
|
||||
// Zephyr results outputs
|
||||
core.setOutput("test-cycle", createdTestCycle.key);
|
||||
core.setOutput("test-keys-execution-count", testExecutions.length);
|
||||
core.setOutput("test-keys-unique-count", uniqueSavedTestKeys.size);
|
||||
}
|
||||
50
.github/actions/save-junit-report-tms/src/types.ts
vendored
Normal file
50
.github/actions/save-junit-report-tms/src/types.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
export interface TestExecution {
|
||||
testCaseKey: string;
|
||||
statusName: string;
|
||||
executionTime: number;
|
||||
comment: string;
|
||||
projectKey?: string;
|
||||
testCycleKey?: string;
|
||||
}
|
||||
|
||||
export interface TestCycle {
|
||||
projectKey: string;
|
||||
name: string;
|
||||
description: string;
|
||||
statusName: string;
|
||||
folderId: number;
|
||||
plannedStartDate?: string;
|
||||
plannedEndDate?: string;
|
||||
customFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface TestData {
|
||||
testCycle: TestCycle;
|
||||
testExecutions: TestExecution[];
|
||||
junitStats: {
|
||||
totalTests: number;
|
||||
totalFailures: number;
|
||||
totalErrors: number;
|
||||
totalSkipped: number;
|
||||
totalPassed: number;
|
||||
passRate: string;
|
||||
totalTime: number;
|
||||
};
|
||||
testKeyStats: {
|
||||
totalOccurrences: number;
|
||||
uniqueCount: number;
|
||||
passedCount: number;
|
||||
failedCount: number;
|
||||
skippedCount: number;
|
||||
failedKeys: string[];
|
||||
skippedKeys: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ZephyrApiClient {
|
||||
createTestCycle(testCycle: TestCycle): Promise<{ key: string }>;
|
||||
saveTestExecution(
|
||||
testExecution: TestExecution,
|
||||
retries?: number,
|
||||
): Promise<void>;
|
||||
}
|
||||
13
.github/actions/save-junit-report-tms/tsconfig.json
vendored
Normal file
13
.github/actions/save-junit-report-tms/tsconfig.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
},
|
||||
"exclude": ["node_modules", "../../../node_modules"]
|
||||
}
|
||||
12
.github/actions/save-junit-report-tms/tsup.config.ts
vendored
Normal file
12
.github/actions/save-junit-report-tms/tsup.config.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs'],
|
||||
outDir: 'dist',
|
||||
clean: true,
|
||||
noExternal: [/.*/], // Bundle all dependencies
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
target: 'node24',
|
||||
});
|
||||
115
.github/actions/setup-buildenv/action.yml
vendored
Normal file
115
.github/actions/setup-buildenv/action.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: Setup build environment
|
||||
description: >
|
||||
Builds the mattermost-build-server images if they don't yet exist on Docker
|
||||
Hub (e.g. during a Go or Node version bump), and uploads them as artifacts
|
||||
for downstream jobs. Run once per workflow before any build or test jobs.
|
||||
|
||||
inputs:
|
||||
build-server-tag:
|
||||
description: "Build image tag, from server/build/buildenv-tag.sh (e.g. go1.26.3-node24.11.1)"
|
||||
required: true
|
||||
dockerfile:
|
||||
description: "Path to Dockerfile.buildenv (relative to workspace root)"
|
||||
required: false
|
||||
default: "server/build/Dockerfile.buildenv"
|
||||
dockerfile-fips:
|
||||
description: "Path to Dockerfile.buildenv-fips (relative to workspace root)"
|
||||
required: false
|
||||
default: "server/build/Dockerfile.buildenv-fips"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set constants
|
||||
id: constants
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "image=mattermost/mattermost-build-server"
|
||||
echo "artifact=buildenv-image"
|
||||
echo "fips-image=mattermost/mattermost-build-server-fips"
|
||||
echo "fips-artifact=buildenv-fips-image"
|
||||
echo "tmpdir=/tmp/buildenv-save"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
# ── Regular ────────────────────────────────────────────────────────
|
||||
- name: Resolve regular image
|
||||
id: resolve
|
||||
shell: bash
|
||||
env:
|
||||
BUILDENV_IMAGE: ${{ steps.constants.outputs.image }}
|
||||
BUILDENV_TAG: ${{ inputs.build-server-tag }}
|
||||
run: |
|
||||
docker manifest inspect "${BUILDENV_IMAGE}:${BUILDENV_TAG}" > /dev/null 2>&1 || \
|
||||
echo "needs-build=true" >> "${GITHUB_OUTPUT}"
|
||||
- name: Build regular image
|
||||
if: ${{ steps.resolve.outputs.needs-build == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
BUILDENV_IMAGE: ${{ steps.constants.outputs.image }}
|
||||
BUILDENV_TAG: ${{ inputs.build-server-tag }}
|
||||
BUILDENV_DOCKERFILE: ${{ inputs.dockerfile }}
|
||||
run: docker build -t "${BUILDENV_IMAGE}:${BUILDENV_TAG}" - < "${BUILDENV_DOCKERFILE}"
|
||||
- name: Save and upload regular image
|
||||
if: ${{ steps.resolve.outputs.needs-build == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
BUILDENV_IMAGE: ${{ steps.constants.outputs.image }}
|
||||
BUILDENV_TAG: ${{ inputs.build-server-tag }}
|
||||
BUILDENV_TMPDIR: ${{ steps.constants.outputs.tmpdir }}
|
||||
run: |
|
||||
mkdir -p "${BUILDENV_TMPDIR}"
|
||||
docker save "${BUILDENV_IMAGE}:${BUILDENV_TAG}" | gzip > "${BUILDENV_TMPDIR}/image.tar.gz"
|
||||
- name: Upload regular image artifact
|
||||
if: ${{ steps.resolve.outputs.needs-build == 'true' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ steps.constants.outputs.artifact }}
|
||||
path: ${{ steps.constants.outputs.tmpdir }}/image.tar.gz
|
||||
retention-days: 1
|
||||
# ── FIPS (skipped on fork PRs) ─────────────────────────────────────
|
||||
- name: Docker Hub login
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||
- name: Setup Chainctl
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
|
||||
with:
|
||||
identity: ${{ env.CHAINCTL_IDENTITY }}
|
||||
- name: Resolve FIPS image
|
||||
id: resolve-fips
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
shell: bash
|
||||
env:
|
||||
BUILDENV_FIPS_IMAGE: ${{ steps.constants.outputs.fips-image }}
|
||||
BUILDENV_TAG: ${{ inputs.build-server-tag }}
|
||||
run: |
|
||||
docker manifest inspect "${BUILDENV_FIPS_IMAGE}:${BUILDENV_TAG}" > /dev/null 2>&1 || \
|
||||
echo "needs-build=true" >> "${GITHUB_OUTPUT}"
|
||||
- name: Build FIPS image
|
||||
if: ${{ steps.resolve-fips.outputs.needs-build == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
BUILDENV_FIPS_IMAGE: ${{ steps.constants.outputs.fips-image }}
|
||||
BUILDENV_TAG: ${{ inputs.build-server-tag }}
|
||||
BUILDENV_FIPS_DOCKERFILE: ${{ inputs.dockerfile-fips }}
|
||||
run: docker build -t "${BUILDENV_FIPS_IMAGE}:${BUILDENV_TAG}" - < "${BUILDENV_FIPS_DOCKERFILE}"
|
||||
- name: Save and upload FIPS image
|
||||
if: ${{ steps.resolve-fips.outputs.needs-build == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
BUILDENV_FIPS_IMAGE: ${{ steps.constants.outputs.fips-image }}
|
||||
BUILDENV_TAG: ${{ inputs.build-server-tag }}
|
||||
BUILDENV_TMPDIR: ${{ steps.constants.outputs.tmpdir }}
|
||||
run: |
|
||||
mkdir -p "${BUILDENV_TMPDIR}"
|
||||
docker save "${BUILDENV_FIPS_IMAGE}:${BUILDENV_TAG}" | gzip > "${BUILDENV_TMPDIR}/image-fips.tar.gz"
|
||||
- name: Upload FIPS image artifact
|
||||
if: ${{ steps.resolve-fips.outputs.needs-build == 'true' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ steps.constants.outputs.fips-artifact }}
|
||||
path: ${{ steps.constants.outputs.tmpdir }}/image-fips.tar.gz
|
||||
retention-days: 1
|
||||
32
.github/actions/unpack-cypress-deps/action.yml
vendored
Normal file
32
.github/actions/unpack-cypress-deps/action.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: "Unpack Cypress deps artifact"
|
||||
description: >
|
||||
The consuming half of the artifact transport for the Cypress deps: downloads
|
||||
this run's artifact from pack-cypress-deps and extracts the node_modules and
|
||||
Cypress binary into place.
|
||||
|
||||
Runs on a worker only when restore-cypress-deps found the cache key cold, and
|
||||
must leave the same paths populated that a cache restore would have, so the
|
||||
steps that follow cannot tell which transport ran.
|
||||
|
||||
inputs:
|
||||
fips:
|
||||
description: >
|
||||
Whether this is a FIPS-edition run. Must match the pack step so the correct
|
||||
artifact is downloaded.
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: ci/download-cypress-deps
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: e2e-cypress-deps${{ inputs.fips == 'true' && '-fips' || '' }}
|
||||
path: ${{ runner.temp }}
|
||||
- name: ci/unpack-cypress-deps
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tar -xzf "${RUNNER_TEMP}/cypress-node-modules.tgz" -C "${GITHUB_WORKSPACE}"
|
||||
tar -xzf "${RUNNER_TEMP}/cypress-binary.tgz" -C "${HOME}"
|
||||
67
.github/actions/webapp-setup/action.yml
vendored
Normal file
67
.github/actions/webapp-setup/action.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: "Web app setup"
|
||||
description: "Set up NPM and dependencies"
|
||||
|
||||
inputs:
|
||||
read-only:
|
||||
description: "Restore from cache without saving"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
- name: ci/cache-node-modules
|
||||
if: inputs.read-only != 'true'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
id: cache-node-modules-rw
|
||||
with:
|
||||
path: |
|
||||
webapp/node_modules
|
||||
webapp/channels/node_modules
|
||||
webapp/platform/client/node_modules
|
||||
webapp/platform/components/node_modules
|
||||
webapp/platform/shared/node_modules
|
||||
webapp/platform/types/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ hashFiles('webapp/package-lock.json') }}
|
||||
# fall back to most recent cache for this OS if package-lock.json changed
|
||||
restore-keys: |
|
||||
node-modules-${{ runner.os }}-
|
||||
- name: ci/restore-node-modules
|
||||
if: inputs.read-only == 'true'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
id: cache-node-modules-ro
|
||||
with:
|
||||
path: |
|
||||
webapp/node_modules
|
||||
webapp/channels/node_modules
|
||||
webapp/platform/client/node_modules
|
||||
webapp/platform/components/node_modules
|
||||
webapp/platform/shared/node_modules
|
||||
webapp/platform/types/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ hashFiles('webapp/package-lock.json') }}
|
||||
# fall back to most recent cache for this OS if package-lock.json changed
|
||||
restore-keys: |
|
||||
node-modules-${{ runner.os }}-
|
||||
- name: ci/resolve-cache-hit
|
||||
id: cache-hit
|
||||
shell: bash
|
||||
run: |
|
||||
echo "cache-hit=${{ steps.cache-node-modules-rw.outputs.cache-hit || steps.cache-node-modules-ro.outputs.cache-hit }}" >> $GITHUB_OUTPUT
|
||||
- name: ci/get-node-modules
|
||||
if: steps.cache-hit.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
working-directory: webapp
|
||||
run: |
|
||||
make node_modules
|
||||
- name: ci/build-platform-packages
|
||||
# These are built automatically when dependencies are installed, but they aren't cached properly, so we need to
|
||||
# manually build them when the cache is hit. They aren't worth caching because they have too many dependencies.
|
||||
if: steps.cache-hit.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
working-directory: webapp
|
||||
run: |
|
||||
npm run postinstall
|
||||
Reference in New Issue
Block a user