diff --git a/.github/actions/flaky-filter/action.yml b/.github/actions/flaky-filter/action.yml new file mode 100644 index 0000000..c4b1f18 --- /dev/null +++ b/.github/actions/flaky-filter/action.yml @@ -0,0 +1,134 @@ +name: Filter flaky tests from a test list +description: | + Fetch active flaky marks for the current branch from the REPO/BRANCH-keyed + flaky DB and filter them out of a caller-provided test-id list, emitting the + path to a filtered TESTFILE the runner can consume. + + The caller enumerates its own tests (that step is product-specific, e.g. + `LIST=1 make pytest`) and passes the list as `tests`. This action only does + the generic fetch + filter, so it stays framework/product agnostic. + + Fail-open: emits an empty `testfile` when there are no active marks, when the + DB is unset/unreachable, or on any error — callers treat empty as "run the + full suite". When marks exist and they filter out *every* enumerated test, the + `all-quarantined` output is `true` so the caller can choose to skip the shard + entirely (an empty TESTFILE alone can't express "run nothing"). + + Container-safe: the DB CLI is fetched into the (mounted) workspace via checkout, + since github.action_path is not mounted in container jobs. + +inputs: + redis-url: + description: "Redis connection URL (rediss://...). No marks fetched if empty." + required: true + tests: + description: "Path to the enumerated test-id list (one RLTest id per line)." + required: true + output: + description: "Path to write the filtered TESTFILE." + required: true + branch: + description: "Branch to fetch marks for. Defaults to the PR / merge-queue target." + required: false + default: "" + ci-common-ref: + description: "Ref of redisearch-ci-common to fetch the DB CLI from (match your pin)." + required: false + default: v1 + +outputs: + testfile: + description: "Filtered TESTFILE path; empty if no marks or any step failed." + value: ${{ steps.filter.outputs.testfile }} + all-quarantined: + description: > + 'true' when active marks filtered out every enumerated test (the filtered + set is empty). Callers can skip the test step entirely in that case. + 'false' otherwise (no marks, partial filter, or any error). + value: ${{ steps.filter.outputs.all-quarantined }} + +runs: + using: composite + steps: + - name: Resolve target branch + shell: bash -l -eo pipefail {0} + env: + OVERRIDE: ${{ inputs.branch }} + MERGE_GROUP_BASE: ${{ github.event.merge_group.base_ref }} + BASE_REF: ${{ github.base_ref }} + REF_NAME: ${{ github.ref_name }} + run: | + if [ -n "$OVERRIDE" ]; then + branch="$OVERRIDE" + elif [ -n "$MERGE_GROUP_BASE" ]; then + branch="${MERGE_GROUP_BASE#refs/heads/}" + elif [ -n "$BASE_REF" ]; then + branch="$BASE_REF" + else + branch="$REF_NAME" + fi + echo "Resolved flaky target branch: $branch" + echo "FLAKY_BRANCH=$branch" >> "$GITHUB_ENV" + + - name: Fetch shared flaky DB CLI + uses: actions/checkout@v6 + with: + repository: redislabsdev/redisearch-ci-common + ref: ${{ inputs.ci-common-ref }} + path: .redisearch-ci-common + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install dependencies + shell: bash -l -eo pipefail {0} + run: python3 -m pip install 'redis>=6,<7' + + - name: Fetch marks and filter + id: filter + shell: bash -l -eo pipefail {0} + env: + REDIS_URL: ${{ inputs.redis-url }} + REPO: ${{ github.repository }} + BRANCH: ${{ env.FLAKY_BRANCH }} + TESTS: ${{ inputs.tests }} + OUTPUT: ${{ inputs.output }} + run: | + cli="$GITHUB_WORKSPACE/.redisearch-ci-common/scripts/ci_common/flaky_db.py" + marks="$RUNNER_TEMP/flaky_marks.txt" + # Fail-open: a DB outage must not break the test pipeline. + python3 "$cli" fetch --output-json "$RUNNER_TEMP/flaky_marks.json" --output-list "$marks" || \ + echo "::warning::flaky fetch failed; running the full suite" + : > "$OUTPUT" + all_quarantined=false + if [ -s "$marks" ]; then + if python3 "$cli" filter --tests "$TESTS" --marks "$marks" --output "$OUTPUT"; then + if [ ! -s "$OUTPUT" ]; then + # Output empty: distinguish "every valid test was quarantined" from + # "the input had no valid test ids at all". Re-filter with no marks + # (same code path, so the same id-validation applies) to get the set + # of tests that *would* run; only call it all-quarantined if that + # baseline was non-empty. + empty_marks="$RUNNER_TEMP/flaky_empty_marks.txt"; : > "$empty_marks" + baseline="$RUNNER_TEMP/flaky_baseline.txt" + python3 "$cli" filter --tests "$TESTS" --marks "$empty_marks" --output "$baseline" || : > "$baseline" + if [ -s "$baseline" ]; then + all_quarantined=true + echo "::notice::All enumerated tests are quarantined for this shard." + fi + fi + else + : > "$OUTPUT" # filter error ⇒ fail-open (full suite) + fi + else + echo "No active flaky marks — running the full suite" + fi + if [ -s "$OUTPUT" ]; then + echo "testfile=$OUTPUT" >> "$GITHUB_OUTPUT" + else + echo "testfile=" >> "$GITHUB_OUTPUT" + fi + echo "all-quarantined=$all_quarantined" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/flaky-record-results/action.yml b/.github/actions/flaky-record-results/action.yml index fc39690..c367d1b 100644 --- a/.github/actions/flaky-record-results/action.yml +++ b/.github/actions/flaky-record-results/action.yml @@ -9,9 +9,11 @@ description: | This action never fails the job: it warns on connection errors so a flaky-DB outage cannot break the test pipeline. The calling step should set - `if: always()` so failures are recorded even when tests fail. The DB CLI - (scripts/ci_common/flaky_db.py) is bundled in this repo, so the action is - self-contained. + `if: always()` so failures are recorded even when tests fail. + + Container-safe: the DB CLI is fetched into the (mounted) workspace via a + checkout, since `github.action_path` points at the runner host and is not + mounted inside container jobs. inputs: redis-url: @@ -33,6 +35,12 @@ inputs: description: "Short label for this job/shard, recorded with each result" required: false default: "" + ci-common-ref: + description: > + Ref of redisearch-ci-common to fetch the DB CLI from. Set this to the same + ref you pinned this action to (e.g. v1). + required: false + default: v1 runs: using: composite @@ -59,6 +67,15 @@ runs: echo "Resolved flaky target branch: $branch" echo "FLAKY_BRANCH=$branch" >> "$GITHUB_ENV" + - name: Fetch shared flaky DB CLI + # Into the workspace (mounted in container jobs), not action_path (host). + uses: actions/checkout@v6 + with: + repository: redislabsdev/redisearch-ci-common + ref: ${{ inputs.ci-common-ref }} + path: .redisearch-ci-common + persist-credentials: false + - name: Set up Python uses: actions/setup-python@v6 with: @@ -66,7 +83,7 @@ runs: - name: Install dependencies shell: bash -l -eo pipefail {0} - run: pip install 'redis>=6,<7' + run: python3 -m pip install 'redis>=6,<7' - name: Record results shell: bash -l -eo pipefail {0} @@ -80,6 +97,4 @@ runs: JOB_NAME: ${{ inputs.job-name != '' && inputs.job-name || github.job }} FAILED_FILE: ${{ inputs.failed-file }} PASSED_FILE: ${{ inputs.passed-file }} - # flaky_db.py is bundled in this repo; reference it relative to the action - # (action_path -> .github/actions/flaky-record-results, so go up 3 to root). - run: python3 "${{ github.action_path }}/../../../scripts/ci_common/flaky_db.py" record + run: python3 "$GITHUB_WORKSPACE/.redisearch-ci-common/scripts/ci_common/flaky_db.py" record diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..fc94772 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,40 @@ +name: Stale issues and PRs (reusable) + +# Reusable wrapper around actions/stale with sensible defaults. Opt-in: only +# repos that track work in GitHub issues/PRs (e.g. the public repo) need call it +# via a thin `on: schedule` caller. Tunables are overridable via inputs. + +on: + workflow_call: + inputs: + days-before-stale: + type: number + default: 60 + days-before-close: + type: number + default: -1 + operations-per-run: + type: number + default: 1000 + runs-on: + type: string + default: ubuntu-latest + +jobs: + stale: + runs-on: ${{ inputs.runs-on }} + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v10 + with: + days-before-stale: ${{ inputs.days-before-stale }} + days-before-close: ${{ inputs.days-before-close }} + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for ${{ inputs.days-before-stale }} days with no activity." + close-issue-message: "This issue was closed because it has been inactive for ${{ inputs.days-before-close }} days since being marked as stale." + stale-pr-label: "stale" + stale-pr-message: "This pull request is stale because it has been open for ${{ inputs.days-before-stale }} days with no activity." + close-pr-message: "This pull request was closed because it has been inactive for ${{ inputs.days-before-close }} days since being marked as stale." + operations-per-run: ${{ inputs.operations-per-run }} diff --git a/README.md b/README.md index 53401bb..b6cd339 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ plumbing. | `pr-size-label` reusable workflow | [`.github/workflows/pr-size-label.yml`](.github/workflows/pr-size-label.yml) | Label a PR by diff size. Add a thin `on: pull_request` caller. | | `spellcheck` reusable workflow | [`.github/workflows/spellcheck.yml`](.github/workflows/spellcheck.yml) | codespell over a PR's changed files. The repo supplies its own `.codespell/` config. | | `link-check` reusable workflow | [`.github/workflows/link-check.yml`](.github/workflows/link-check.yml) | Validate Markdown links/anchors. Self-contained — bundles `scripts/ci_common/check_links.py`. | +| `stale` reusable workflow | [`.github/workflows/stale.yml`](.github/workflows/stale.yml) | Mark/close stale issues & PRs (actions/stale). Opt-in — for repos that track work in GitHub issues. | ### Flaky-test DB @@ -46,15 +47,20 @@ RLTest suites can quarantine known-flaky tests consistently. | `flaky-mark` reusable workflow | [`.github/workflows/flaky-mark.yml`](.github/workflows/flaky-mark.yml) | Add a flaky mark (test id, reason, Jira key, expiry). Add a thin `on: workflow_dispatch` caller. | | `flaky-unmark` reusable workflow | [`.github/workflows/flaky-unmark.yml`](.github/workflows/flaky-unmark.yml) | Remove a flaky mark. | | `flaky_db.py` CLI | [`scripts/ci_common/flaky_db.py`](scripts/ci_common/flaky_db.py) | `mark`/`unmark`/`fetch`/`filter`/`record`. No-op when `REDIS_URL` is unset (keeps fork-PR CI green). | +| `flaky-filter` composite action | [`.github/actions/flaky-filter`](.github/actions/flaky-filter/action.yml) | Fetch marks and filter them out of a caller-provided test list → filtered TESTFILE. Caller enumerates its own tests (product-specific); empty output = run full suite. Also emits `all-quarantined` so a caller can skip a fully-quarantined shard. | | `flaky-record-results` composite action | [`.github/actions/flaky-record-results`](.github/actions/flaky-record-results/action.yml) | Record an RLTest run's failed/passed test ids to the DB from inside a test job (`if: always()`). Never fails the job. | > Unlike the units above (which are test-framework agnostic), the flaky tooling > is **RLTest-shaped**: it expects test ids in RLTest's > `:[variant]` form — which is what the consuming suites -> emit. Marks only take effect once the consuming repo's **test pipeline** calls -> `flaky_db.py fetch`/`filter` to skip marked tests (and `flaky-record-results` -> to log per-run results); that wiring is product-specific and lives in each -> repo's own test workflow — intentionally **not** here. +> emit. The pieces — mark/unmark, `flaky-filter` (skip marked tests), and +> `flaky-record-results` (log per-run results) — cover the whole pipeline, so a +> consuming repo needs no local `flaky_db.py`. Only the *test enumeration* (e.g. +> `LIST=1 make pytest`) stays product-specific in the caller. +> +> **Container-safe:** the two composite actions fetch the DB CLI into the mounted +> workspace via a checkout (not `github.action_path`, which isn't mounted inside +> container jobs). Pass `ci-common-ref` matching the ref you pinned the action to. ### Security posture (baked into `codex-run`)