Skip to content

Update Muted tests

Update Muted tests #90

name: Update Muted tests
on:
schedule:
- cron: "0 * * * *" # Every hour at :00 UTC
workflow_dispatch:
inputs:
branches:
description: 'Optional: comma-separated branches instead of stable_tests_branches.json. Empty = use stable_tests_branches.json from WORKFLOW_CHECKOUT_REF.'
required: false
default: ''
build_types:
description: 'Comma-separated presets or "all" (default). Cannot request presets outside .github/config/mute_config.json (default + per-branch overrides).'
required: false
default: all
type: string
env:
# workflow_dispatch: branch selected in UI. schedule: repository default branch.
WORKFLOW_CHECKOUT_REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.repository.default_branch }}
BRANCHES_CONFIG_PATH: .github/config/stable_tests_branches.json
MUTE_BUILD_TYPES_CONFIG_PATH: .github/config/mute_config.json
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BRANCH_PREFIX: update-muted-ya
REVIEWERS: '["ci"]'
LABELS: "mute-unmute,not-for-changelog,ok-to-test,automerge"
jobs:
setup:
runs-on: [ self-hosted, auto-provisioned, build-preset-analytic-node]
outputs:
matrix_include: ${{ steps.set-mute-matrix.outputs.matrix_include }}
steps:
- name: Checkout branch list and mute files
uses: actions/checkout@v5
with:
ref: ${{ env.WORKFLOW_CHECKOUT_REF }}
path: config-repo
sparse-checkout: |
.github/config/
.github/scripts/tests/mute/mute_helper.py
.github/scripts/tests/mute/mute_utils.py
sparse-checkout-cone-mode: false
- id: set-mute-matrix
env:
INPUT_BRANCHES: ${{ github.event.inputs.branches || '' }}
INPUT_BUILD_TYPES: ${{ github.event.inputs.build_types || '' }}
run: |
set -euo pipefail
python3 config-repo/.github/scripts/tests/mute/mute_helper.py matrix \
--branches-file "config-repo/${{ env.BRANCHES_CONFIG_PATH }}" \
--build-types-config "config-repo/${{ env.MUTE_BUILD_TYPES_CONFIG_PATH }}"
collect-testowners-and-sync:
runs-on: [ self-hosted, auto-provisioned, build-preset-analytic-node]
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ env.WORKFLOW_CHECKOUT_REF }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ydb[yc] codeowners requests
- name: Setup ydb access
uses: ./.github/actions/setup_ci_ydb_service_account_key_file_credentials
with:
ci_ydb_service_account_key_file_credentials: ${{ secrets.CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS }}
ydb_qa_config: ${{ vars.YDB_QA_CONFIG }}
- name: Collect testowners
run: python3 .github/scripts/analytics/upload_testowners.py
# ``mute/manual_unmute.py sync`` selects CLOSED+COMPLETED candidates from YDB ``issues``;
# refresh that table here so fast-unmute does not wait for collect_analytics_fast (~30 min).
- name: Export GitHub issues to YDB
env:
GITHUB_TOKEN: ${{ secrets.YDBOT_TOKEN }}
run: python3 .github/scripts/analytics/export_issues_to_ydb.py
- name: Sync manual fast-unmute state
env:
GITHUB_TOKEN: ${{ secrets.YDBOT_TOKEN }}
run: python3 .github/scripts/tests/mute/manual_unmute.py sync
update-muted-tests:
needs: [setup, collect-testowners-and-sync]
runs-on: [ self-hosted, auto-provisioned, build-preset-analytic-node]
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.setup.outputs.matrix_include) }}
steps:
- name: Checkout workflow branch
uses: actions/checkout@v5
with:
token: ${{ secrets.YDBOT_TOKEN }}
ref: ${{ env.WORKFLOW_CHECKOUT_REF }}
fetch-depth: 0
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ydb[yc] PyGithub pandas numpy requests
- name: Setup ydb access
uses: ./.github/actions/setup_ci_ydb_service_account_key_file_credentials
with:
ci_ydb_service_account_key_file_credentials: ${{ secrets.CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS }}
ydb_qa_config: ${{ vars.YDB_QA_CONFIG }}
- name: Prepare base mute file
run: |
set -euo pipefail
echo "BASE_BRANCH=${{ matrix.BASE_BRANCH }}" >> "$GITHUB_ENV"
echo "BUILD_TYPE=${{ matrix.BUILD_TYPE }}" >> "$GITHUB_ENV"
echo "PR_BRANCH=${{ env.PR_BRANCH_PREFIX }}_${{ matrix.BASE_BRANCH }}_${{ matrix.BUILD_TYPE }}" >> "$GITHUB_ENV"
git fetch origin "${{ matrix.BASE_BRANCH }}:refs/remotes/origin/${{ matrix.BASE_BRANCH }}"
git show "origin/${{ matrix.BASE_BRANCH }}:${{ matrix.MUTED_YA_RELATIVE }}" > base_muted_ya.txt
- name: Collect test history data
run: python3 .github/scripts/analytics/flaky_tests_history.py --branch=${{ env.BASE_BRANCH }} --build_type=${{ env.BUILD_TYPE }}
- name: Update muted tests in DB
run: python3 .github/scripts/tests/mute/get_muted_tests.py upload_muted_tests --branch=${{ env.BASE_BRANCH }} --build_type=${{ env.BUILD_TYPE }} --muted_ya_file=base_muted_ya.txt
- name: Update test monitor
run: python3 .github/scripts/analytics/tests_monitor.py --branch=${{ env.BASE_BRANCH }} --build_type=${{ env.BUILD_TYPE }}
- name: Sync fast-unmute grace for re-muted tests
run: |
.github/scripts/tests/mute/create_new_muted_ya.py sync_fast_unmute_grace \
--branch=${{ env.BASE_BRANCH }} \
--build-type=${{ env.BUILD_TYPE }}
- name: Generate new muted_ya.txt
run: |
.github/scripts/tests/mute/create_new_muted_ya.py update_muted_ya \
--branch=${{ env.BASE_BRANCH }} \
--muted_ya_file=base_muted_ya.txt \
--build-type=${{ env.BUILD_TYPE }}
- name: Check if changes exist
id: changes_check
run: |
# Compare base_muted_ya.txt with newly generated file
if git diff --no-index --quiet base_muted_ya.txt mute_update/new_muted_ya.txt; then
echo "No changes detected in muted_ya.txt"
echo "changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected in muted_ya.txt"
echo "changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup git config
if: steps.changes_check.outputs.changes == 'true'
run: |
git config user.name YDBot
git config user.email ydbot@ydb.tech
- name: Create PR branch and apply changes
if: steps.changes_check.outputs.changes == 'true'
run: |
set -euo pipefail
# Путь к mute-файлу заранее в матрице (тот же dedicated_relative, что в mute_update_workflow_matrix).
OUT='${{ matrix.MUTED_YA_RELATIVE }}'
git checkout -B ${{ env.PR_BRANCH }} origin/${{ env.BASE_BRANCH }}
mkdir -p "$(dirname "$OUT")"
cp mute_update/new_muted_ya.txt "$OUT"
git add "$OUT"
git commit -m "Update muted_ya for ${{ env.BUILD_TYPE }} ${{ env.BASE_BRANCH }}"
echo "✓ PR branch ${{ env.PR_BRANCH }} created with changes ($OUT)"
- name: Collect PR description
if: steps.changes_check.outputs.changes == 'true'
run: |
python3 - <<'PY'
from pathlib import Path
from urllib.parse import quote
base_branch = "${{ env.BASE_BRANCH }}"
build_type = "${{ env.BUILD_TYPE }}"
max_tests = 50
def read_lines(path):
p = Path(path)
if not p.exists() or p.stat().st_size == 0:
return []
return p.read_text(encoding="utf-8").splitlines()
def append_debug_section(parts, title, lines, count_override=None):
if not lines:
return
count = count_override if count_override is not None else len(lines)
parts.append(f"**{title}: {count}**\n\n")
parts.append("```\n")
parts.append("\n".join(lines))
parts.append("\n```\n\n")
to_delete = read_lines("mute_update/to_delete.txt")
to_mute = read_lines("mute_update/to_mute.txt")
to_unmute = read_lines("mute_update/to_unmute.txt")
to_delete_debug = read_lines("mute_update/to_delete_debug.txt")
to_mute_debug = read_lines("mute_update/to_mute_debug.txt")
to_unmute_debug = read_lines("mute_update/to_unmute_debug.txt")
body = [f"# Muted tests update for {base_branch} (build: {build_type})\n\n"]
append_debug_section(
body,
"Removed from mute",
to_delete_debug or to_delete,
count_override=len(to_delete),
)
append_debug_section(
body,
"Muted flaky",
to_mute_debug or to_mute,
count_override=len(to_mute),
)
if to_mute:
url = (
"https://datalens.yandex.cloud/34xnbsom67hcq-ydb-autotests-test-history-link"
f"?branch={quote(base_branch, safe='')}"
f"&build_type={quote(build_type, safe='')}"
)
for test_name in to_mute[:max_tests]:
url += f"&full_name={quote(test_name.replace(' ', '/'), safe='')}"
body.append(f"[View history of muted flaky tests on Dashboard]({url})\n")
append_debug_section(
body,
"Unmuted stable",
to_unmute_debug or to_unmute,
count_override=len(to_unmute),
)
Path("pr_body_content.txt").write_text("".join(body), encoding="utf-8")
PY
echo "PR_BODY_PATH=pr_body_content.txt" >> "$GITHUB_ENV"
- name: Upload all generated files as artifacts
if: steps.changes_check.outputs.changes == 'true'
uses: actions/upload-artifact@v6
with:
name: muted-tests-all-files-${{ env.BASE_BRANCH }}-${{ env.BUILD_TYPE }}
path: |
mute_update/
retention-days: 7
- name: Push PR branch
if: steps.changes_check.outputs.changes == 'true'
run: |
# Retry logic for transient GitHub API timeouts
MAX_RETRIES=5
RETRY_DELAY=10
ATTEMPT=1
while [ $ATTEMPT -le $MAX_RETRIES ]; do
echo "Attempt $ATTEMPT of $MAX_RETRIES to push branch ${{ env.PR_BRANCH }}"
if git push origin ${{ env.PR_BRANCH }} --force; then
echo "✓ Successfully pushed branch ${{ env.PR_BRANCH }}"
exit 0
else
EXIT_CODE=$?
echo "⚠️ Push failed with exit code $EXIT_CODE"
if [ $ATTEMPT -lt $MAX_RETRIES ]; then
echo "Waiting ${RETRY_DELAY} seconds before retry..."
sleep $RETRY_DELAY
RETRY_DELAY=$((RETRY_DELAY * 2)) # Exponential backoff
else
echo "❌ Failed to push after $MAX_RETRIES attempts"
exit $EXIT_CODE
fi
fi
ATTEMPT=$((ATTEMPT + 1))
done
- name: Create or update PR
if: steps.changes_check.outputs.changes == 'true'
id: create_or_update_pr
env:
GITHUB_TOKEN: ${{ secrets.YDBOT_TOKEN }}
run: |
# Возвращаемся на WORKFLOW_CHECKOUT_REF для доступа к скриптам
git checkout "origin/${{ env.WORKFLOW_CHECKOUT_REF }}"
python .github/scripts/create_or_update_pr.py create_or_update \
--base_branch="${{ env.BASE_BRANCH }}" \
--branch_for_pr="${{ env.PR_BRANCH }}" \
--title="Update muted_ya (${{ env.BUILD_TYPE }}) in ${{ env.BASE_BRANCH }}" \
--body="${{ env.PR_BODY_PATH }}"
- name: Prepare PR comment
if: steps.changes_check.outputs.changes == 'true'
id: comment_pr
run: |
# Читаем содержимое PR body
PR_BODY_CONTENT=$(cat pr_body_content.txt)
WORKFLOW_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
COMPLETE_BODY=$(cat << EOF
Collected in workflow [#${{ github.run_number }}](${WORKFLOW_URL}) for ${{ env.BASE_BRANCH }} (build: ${{ env.BUILD_TYPE }})
${PR_BODY_CONTENT}
EOF
)
echo "$COMPLETE_BODY" > pr_comment_final.txt
echo "PR_COMMENT_FILE=pr_comment_final.txt" >> $GITHUB_OUTPUT
- name: Add comment, reviewers and labels
if: steps.changes_check.outputs.changes == 'true'
uses: actions/github-script@v8
env:
LABELS: ${{ env.LABELS }}
REVIEWERS: ${{ env.REVIEWERS }}
with:
github-token: ${{ secrets.YDBOT_TOKEN }}
script: |
const fs = require('fs');
// Читаем комментарий из файла
const commentFile = '${{ steps.comment_pr.outputs.PR_COMMENT_FILE }}';
const commentBody = fs.readFileSync(commentFile, 'utf8');
const MAX_COMMENT_LENGTH = 65000;
let finalCommentBody = commentBody;
if (commentBody.length > MAX_COMMENT_LENGTH) {
console.log(`Comment is too large (${commentBody.length} chars), creating gist fallback`);
try {
const gist = await github.rest.gists.create({
public: false,
description: `Muted tests update details for PR #${{ steps.create_or_update_pr.outputs.pr_number }}`,
files: {
[`muted-tests-update-${{ env.BASE_BRANCH }}-${{ env.BUILD_TYPE }}.md`]: {
content: commentBody
}
}
});
finalCommentBody = [
`Collected in workflow [#${{ github.run_number }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for ${{ env.BASE_BRANCH }} (build: ${{ env.BUILD_TYPE }})`,
'',
`The full comment body is too large for GitHub issue comments (${commentBody.length.toLocaleString()} chars).`,
`Full details: ${gist.data.html_url}`
].join('\n');
} catch (error) {
console.log(`Failed to create gist fallback: ${error.message}`);
const suffix = '\n\n[Comment truncated due to GitHub limit]';
finalCommentBody = commentBody.slice(0, MAX_COMMENT_LENGTH - suffix.length) + suffix;
}
}
// Добавляем комментарий
await github.rest.issues.createComment({
issue_number: ${{ steps.create_or_update_pr.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: finalCommentBody
});
// Добавляем лейблы
const labelsToAdd = process.env.LABELS.split(',').map(label => label.trim()).filter(Boolean);
await github.rest.issues.addLabels({
...context.repo,
issue_number: ${{ steps.create_or_update_pr.outputs.pr_number }},
labels: labelsToAdd
});
// Добавляем ревьюеров
const reviewers = JSON.parse(process.env.REVIEWERS);
await github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ steps.create_or_update_pr.outputs.pr_number }},
team_reviewers: reviewers
});
- name: Enable auto-merge (squash)
if: steps.changes_check.outputs.changes == 'true'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.YDBOT_TOKEN }}
script: |
const pr = ${{ steps.create_or_update_pr.outputs.pr_number }};
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr
});
const mutation = `
mutation EnableAutoMerge {
enablePullRequestAutoMerge(input: {
pullRequestId: "${pullRequest.node_id}",
mergeMethod: SQUASH
}) {
clientMutationId
}
}
`;
try {
await github.graphql(mutation);
console.log("Auto-merge with squash successfully enabled");
} catch (error) {
console.error("Failed to enable auto-merge with squash:", error);
}