Migrate CI from Travis to GitHub Actions (main branch)
Scope of this issue
This issue covers exactly one branch of this plugin: main.
All work in the PR you open from this issue must target the main branch and only main. Other active branches of this plugin are tracked in separate issues — do not touch them here.
Plugin context (use this to render the workflow file correctly):
- Repository default branch:
main
- All active branches of this plugin:
main
- This issue's target branch:
main (default branch? yes)
Primary goal
Every test currently run by .travis.yml on the main branch MUST also run in GitHub Actions after this change. That is the single criterion for success. Read the .travis.yml on main carefully (including any matrix.include, env matrices, and jobs.include entries) and make sure every combination has an equivalent GHA job. Do not silently drop matrix entries.
Context
The logstash-plugins/.ci repository now hosts shared reusable GitHub Actions workflows that replace the old Travis-based setup. Two pilot plugins have already been migrated and are the canonical examples to follow:
The shared reusable workflows live here and are what your new workflow files must call. The canonical branch is 1.x — always use @1.x as the ref:
logstash-plugins/.ci/.github/workflows/unit-tests.yml@1.x — unit tests
logstash-plugins/.ci/.github/workflows/integration-tests.yml@1.x — integration tests
logstash-plugins/.ci/.github/workflows/secure-integration-tests.yml@1.x — secure integration tests
logstash-plugins/.ci/.github/workflows/performance.yml@1.x — performance tests
Read the source of those reusable workflows here so you know what inputs they accept and what matrices they already define: https://github.com/logstash-plugins/.ci/tree/1.x/.github/workflows. The reusable workflows already handle the elastic-stack-version / snapshot matrix — do not redefine that matrix in this repo; just call the reusable workflow.
The reference POC issue and PR (filter-grok, unit-only case):
What to do
1. Inspect .travis.yml (follow every import:)
Determine which test types this plugin actually runs — do not guess, do not assume. The file system is the source of truth.
Many plugin .travis.yml files contain nothing but an import: directive pointing at a shared config in logstash-plugins/.ci, for example:
import:
- logstash-plugins/.ci:travis/travis.yml@1.x
When you see import:, you must fetch the imported file and read it (and recursively any files it imports). The imported file is what defines the matrix — the local .travis.yml by itself tells you nothing. Use the shared .ci repo on disk if it's available, or fetch via https://raw.githubusercontent.com/logstash-plugins/.ci/<ref>/<path>.
Only after you have the fully-resolved Travis matrix in front of you, map each test type to a GHA workflow file you need to add:
| Travis signal (in the fully-resolved config) |
Add this workflow file |
| Base matrix runs unit tests (always true) |
.github/workflows/unit-tests.yml calling unit-tests.yml |
INTEGRATION=true in any matrix env |
.github/workflows/integration-tests.yml calling integration-tests.yml |
SECURE_INTEGRATION=true in any matrix env |
.github/workflows/secure-integration-tests.yml calling secure-integration-tests.yml |
HAS_PERFORMANCE_TESTS=1 or a _performance job entry |
.github/workflows/performance.yml calling performance.yml |
If a test type does not appear in the resolved Travis matrix, do not add a workflow for it. Adding workflows that the plugin never ran under Travis is a failure, not a feature.
Only add the workflow files for test types this plugin actually runs. If .travis.yml does not exercise integration tests, do not add an integration workflow.
"Unit tests only" does not mean "no external services." A plugin can run only unit tests yet still depend on a backing service (redis, rabbitmq, kafka, …) that Travis started on the host via services: / addons: / before_install:. Those daemons vanish when .travis.yml is deleted and are not provided by the reusable harness — see section 2d. Classifying a plugin as "unit-only" does not let you skip that reconciliation.
2. Add the consumer workflow file(s)
Schedule durability — read this first. A GitHub Actions schedule: trigger only fires from the workflow file on the default branch (main), and the scheduled run only exercises the default branch's code unless we explicitly tell it otherwise. A schedule: block on a non-default branch's workflow file is silently dead. To get nightly coverage of every active branch, the default-branch workflow file uses a scheduled matrix job that iterates over every active branch and passes ref: ${{ matrix.branch }} to the reusable workflow.
Symmetric file across branches. The same workflow file is committed to every active branch — identical content, no per-branch divergence. The matrix-cron job is dormant on non-default branches (GitHub ignores their schedule: block) but the file stays in sync so future maintenance is straightforward. Do not strip the schedule: block from non-default-branch copies of the file.
Use exactly this YAML for the unit-tests consumer workflow. The same file goes on every active branch — do not change it based on whether this issue's target branch is the default:
name: Unit Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Daily run that fans out across every active branch via the matrix
# job below. GitHub only fires `schedule:` from the workflow file on
# the default branch (main); copies of this file on
# non-default branches keep the block for symmetry but it is dormant
# there.
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
tests:
if: github.event_name != 'schedule'
uses: logstash-plugins/.ci/.github/workflows/unit-tests.yml@1.x
# Forward org/repo secrets (e.g. SLACK_BOT_TOKEN used by the reusable's
# failure-notification step). The reusable does not declare a
# workflow_call.secrets block, so `inherit` is required.
secrets: inherit
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
with:
timeout-minutes: 60
scheduled:
if: github.event_name == 'schedule'
strategy:
fail-fast: false
matrix:
branch: [main]
uses: logstash-plugins/.ci/.github/workflows/unit-tests.yml@1.x
secrets: inherit
with:
ref: ${{ matrix.branch }}
timeout-minutes: 60
For integration / secure integration / performance workflows you add, copy the same shape and swap name and the uses: target (integration-tests.yml@1.x, secure-integration-tests.yml@1.x, performance.yml@1.x) plus any inputs the reusable workflow accepts. The on-block, jobs structure, and matrix logic stay identical.
secrets: inherit is mandatory on every caller job. Each reusable workflow has a failure-notification step that reads secrets.SLACK_BOT_TOKEN. The reusables do not declare a workflow_call.secrets: block, so the only way the token reaches them is for the caller to forward all secrets with secrets: inherit. Add secrets: inherit as a sibling of uses: on both the tests job and the scheduled job, in every workflow file you add (unit, integration, secure-integration, performance). Omitting it means the token is empty inside the reusable and failure notifications silently never fire. (This is also why the PR must come from a branch in the repo, not a fork — fork-based PRs cannot inherit secrets; see Branch / PR requirements below.)
Note on the ref input to reusable workflows. The scheduled matrix job passes ref: ${{ matrix.branch }} to the reusable workflow so it checks out the right branch. The reusable workflows on logstash-plugins/.ci@1.x already accept this ref input (default '' — no-op for non-scheduled callers).
timeout-minutes should reflect the plugin's actual needs — start with what .travis.yml uses, or 60 if unspecified.
2a. Per-plugin matrix overrides (read .travis.yml for these)
The reusable workflows ship a default ELASTIC_STACK_VERSION matrix and assume DISTRIBUTION=default. Most plugins should accept those defaults. But if this plugin's fully-resolved .travis.yml deviates from the defaults, you must mirror the deviation into the consumer workflow via the new with: inputs on the reusable.
The relevant reusable inputs are:
elastic-stack-versions: a JSON-encoded array of stack version strings, e.g. '["8.current"]'. Empty string = use the reusable's built-in default matrix. Setting it narrows (and on integration / secure-integration also strips the snapshot-only include: entries for 9.next and main — you get a strict cartesian product of your versions × [snapshot:false, snapshot:true] with no SSL variants on secure-integration).
distribution: scalar string, 'default' or 'oss'. Maps to the DISTRIBUTION env var consumed by docker-setup.sh (which selects the OSS image suffix when set to oss).
Apply these inputs based on what .travis.yml says:
Signal in fully-resolved .travis.yml |
Action |
jobs.exclude: removes one or more ELASTIC_STACK_VERSION values from the matrix |
Pass elastic-stack-versions: '[...]' listing only the versions that remain after the exclude |
jobs.include: lists a narrow, hand-picked set of ELASTIC_STACK_VERSION values (rather than relying on the inherited matrix) |
Pass elastic-stack-versions: '[...]' listing exactly those values |
Any env: entry sets DISTRIBUTION=oss (e.g. filter-geoip) |
Pass distribution: 'oss' on every consumer workflow you add |
| Plugin uses the inherited matrix with no exclusions, distribution overrides, or hand-picked include list |
Do not pass elastic-stack-versions or distribution — leave them at default |
Maintenance-branch heuristic for unit tests. Travis tolerates broken jobs on maintenance branches because nobody looks; GHA's status checks do not. Apply the same elastic-stack-versions narrowing to unit-tests when both of these hold:
- This issue's branch is not the repo's default branch (i.e. the "default branch?" answer at the top of this issue is
no), AND
- The fully-resolved
.travis.yml has jobs.include: entries that hand-pick a narrow set of ELASTIC_STACK_VERSION values for integration and/or secure-integration jobs, without a parallel jobs.exclude: narrowing the inherited unit-test matrix.
A non-default branch whose maintainers only re-declared integration jobs against, say, 8.current is implicitly an N.x maintenance branch. Its source may not load against the JRuby/Ruby bundled with stack versions outside that set (e.g., Rufus::Scheduler::CronLine::Fixnum blowing up on Ruby ≥ 2.4 / JRuby 9.4+). The inherited unit matrix from travis/matrix.yml will surface those incompatibilities even though they're pre-existing source debt, not anything you introduced.
When this heuristic fires, pass the same narrowed elastic-stack-versions to all workflows you add (unit + integration + secure-integration), and note in the PR description: "Maintenance-branch narrowing: unit-tests narrowed to match integration-tests because main is a non-default branch with hand-picked integration versions. Travis on main has been failing unit-tests against the broader inherited matrix; GHA mirrors only the versions the branch actually supports."
Example: a plugin whose .travis.yml only runs 8.current and sets DISTRIBUTION=oss. The full workflow shape (matrix-cron with tests + scheduled jobs) stays exactly as shown in section 2 — you only add the new keys under each with: block, identical on both jobs:
with:
elastic-stack-versions: '["8.current"]'
distribution: 'oss'
# plus ref: ${{ matrix.branch }} on the scheduled job, as section 2 shows
The override values must be identical between the tests and scheduled callers — the scheduled run is meant to exercise the same matrix as PR runs, just on a different branch.
2b. Modernize or delete the plugin's vendored .ci/ scripts
The setup composite action (from logstash-plugins/.ci/setup@1.x) has a "Bootstrap CI assets" step that downloads the shared .ci@1.x tarball and extracts *Dockerfile*, *docker*, *.sh, and *logstash-versions* into the plugin's local .ci/ directory. The extraction uses tar's "skip existing files" flag, so the bootstrap only fills in files the plugin does not already vendor — it never overwrites a vendored script.
This means: if the plugin vendors a stale .ci/docker-run.sh (or any other vendored helper), the bootstrap step won't fix it, and the reusable workflow's bash .ci/docker-run.sh step runs the stale version. That has bitten us before:
- The GitHub-hosted Ubuntu runners ship Docker Compose v2 only. The v1
docker-compose (hyphenated) binary is not installed. Vendored scripts that call docker-compose … fail with exit code 127.
For each vendored script under .ci/ (typical names: docker-run.sh, setup.sh, logstash-run.sh, Dockerfile*, docker-compose*.yml), decide between delete and modernize:
Delete the vendored file if its only deviations from the corresponding shared file in https://github.com/logstash-plugins/.ci/tree/1.x are stylistic or stale (e.g., it just uses the old docker-compose v1 syntax). Once deleted, the bootstrap step will provide the modern shared version at runtime. Verify by diff-ing against the shared file before deleting.
Modernize the vendored file if it contains plugin-specific behavior the shared file lacks (e.g., the script brings up a custom set of services, sets plugin-specific env vars, or runs extra steps). In that case, just fix the obsolete invocations:
| Stale (v1, fails on GHA) |
Modern (v2, works on GHA) |
docker-compose up … |
docker compose up … |
docker-compose -f … up … |
docker compose -f … up … |
docker-compose down |
docker compose down |
docker-compose exec … |
docker compose exec … |
Bulk modernization:
grep -rl 'docker-compose' .ci/ 2>/dev/null | while read f; do
sed -i 's/docker-compose /docker compose /g' "$f"
done
Do not rename the .ci/docker-compose.yml / .ci/docker-compose.override.yml files themselves — Compose v2 still reads those filenames. Only the CLI invocation changes.
Verify after the change:
grep -rn 'docker-compose ' .ci/ || echo "clean"
Note which scripts you deleted vs. modernized in the PR description.
2c. Reconcile .travis.yml env vars consumed by vendored .ci/ scripts
Deleting .travis.yml also deletes every environment variable it defined. Travis injected env.global, env.matrix, and jobs.include[].env values into the shell before running the .ci/ scripts. In GitHub Actions those variables do not exist unless the reusable workflow, setup/action.yml@1.x, or your consumer workflow sets them. A vendored .ci/ script that reads a now-undefined variable gets an empty string — no error, just a silent misconfiguration that usually surfaces as an auth/connection failure deep in an integration run.
This is a per-plugin decision the reusable workflows deliberately do not make for you: how the plugin's own harness authenticates and configures its services is plugin-specific behavior. You must account for it here.
Do not stop at the one famous variable. A single Travis env: line usually sets several variables at once (e.g. SECURE_INTEGRATION=true INTEGRATION=true ELASTIC_PASSWORD=password ELASTIC_SECURITY_ENABLED=true ELASTIC_STACK_VERSION=8.current SNAPSHOT=true). You must account for every variable on every env line — fixing only the well-known ELASTIC_PASSWORD and missing a sibling like ELASTIC_SECURITY_ENABLED on the same line will still leave the matrix red. Enumerate them mechanically; do not eyeball.
Do this audit:
-
From the fully-resolved .travis.yml, list every variable set in env.global, env.matrix, matrix.include[].env, and jobs.include[].env. Build the complete set — split each space-delimited env line into individual NAME=VALUE pairs and collect the union of names across all jobs.
-
For each variable, check whether a vendored .ci/ script (or Dockerfile* / docker-compose*.yml) actually consumes it:
grep -rnE '\$\{?VARNAME\b' .ci/ 2>/dev/null
-
Classify each consumed variable:
| Class |
Examples |
Action |
Re-derived by the reusable / setup@1.x |
INTEGRATION, SECURE_INTEGRATION, DISTRIBUTION, ELASTIC_STACK_VERSION, snapshot flags |
Nothing — already handled by sections 1 / 2a. |
Consumed by a vendored .ci/ script but not set anywhere in GHA |
ELASTIC_PASSWORD, ELASTIC_SECURITY_ENABLED, custom *_HOST / *_PORT / credential vars for a plugin-specific service |
You must resolve it (see below). Leaving it blank is a silent regression. |
After you have decided how to handle each variable, verify none were missed — every variable that a .ci/ file references must now have a defined value in the GHA path:
# every ${VAR} referenced by the harness...
grep -rhoE '\$\{?[A-Z_][A-Z0-9_]*' .ci/ | tr -d '${' | sort -u
# ...must be either provided by the reusable/setup, or given a default in the harness. Nothing left blank.
For each variable in the second class, choose one:
-
Preferred — modernize the harness so it no longer depends on the Travis-injected var. This is what logstash-output-elasticsearch did: its .ci/docker-compose.override.yml no longer references ELASTIC_PASSWORD, so nothing external has to supply it. If the vendored harness can carry its own known value, that's the cleanest parity.
-
Otherwise — set the variable explicitly in the consumer workflow. Add it under the tests and scheduled jobs (identical on both) so the vendored script sees the same value Travis gave it:
with:
# ...existing inputs...
env:
ELASTIC_PASSWORD: password
(Only if the reusable workflow forwards env: to the harness; otherwise set it inside the vendored script itself as part of modernizing it.)
Known secure-integration example — ELASTIC_PASSWORD and ELASTIC_SECURITY_ENABLED (they travel together). Secure-integration plugins typically had a Travis job with env ... ELASTIC_PASSWORD=password ELASTIC_SECURITY_ENABLED=true .... Both feed the vendored harness and both must be handled — fixing one and forgetting the other is the classic partial migration:
-
ELASTIC_PASSWORD: .ci/docker-compose.override.yml / logstash-run.sh authenticate as -u elastic:$ELASTIC_PASSWORD. Blank → empty-password auth → readiness fails. Give it a default: ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-password}.
-
ELASTIC_SECURITY_ENABLED: .ci/docker-compose.override.yml maps it to xpack.security.enabled=${ELASTIC_SECURITY_ENABLED:-false}. Blank → security disabled → Elasticsearch serves plain HTTP on 9200, but the harness health-check curls https://elasticsearch:9200 (TLS), the handshake never connects, the readiness loop exhausts, and the job exits 1. Tie it to the secure flag rather than hard-coding true — plain integration must keep it off:
# in .ci/docker-compose.override.yml, for both the elasticsearch and logstash services
- ELASTIC_SECURITY_ENABLED=${ELASTIC_SECURITY_ENABLED:-${SECURE_INTEGRATION:-false}}
# and where the ES service maps it:
- xpack.security.enabled=${ELASTIC_SECURITY_ENABLED:-${SECURE_INTEGRATION:-false}}
The setup@1.x action sets SECURE_INTEGRATION=true only for secure-integration runs, so this default enables TLS exactly when it should and leaves plain integration on HTTP.
Do not delete .travis.yml (section 4) until you have accounted for every variable it set — run the verification grep above and confirm nothing the harness references is left undefined.
Record in the PR description the full list of env vars from .travis.yml, and for each one how you handled it (re-derived by reusable / defaulted in harness / set in workflow / eliminated by modernizing).
2d. Reconcile Travis-provided backing services (services:, addons:, before_install/before_script daemons)
A plugin's tests — even ones that run in the unit matrix — may depend on a live backing service (redis, rabbitmq, kafka, mongodb, memcached, …) that Travis started on the host, not inside the plugin's docker-compose. Travis provided these three ways, all of which vanish when .travis.yml is deleted:
services: — Travis's built-in service list, e.g. services: [redis-server, rabbitmq, docker].
addons: — host packages/daemons, e.g. addons.apt.packages: [redis-server], or addons.hosts.
before_install: / before_script: — shell that installs or starts a daemon, e.g. sudo service redis-server start --bind 0.0.0.0.
The shared reusable workflows run your tests inside a docker-compose network and do not install or start any of these. A test that connects to a hardcoded 127.0.0.1:<port> / localhost:<port> will get ECONNREFUSED for every example — a silent regression even though the resolved Travis matrix "only" ran unit tests.
Detect it:
-
In the fully-resolved .travis.yml, list every entry under services:, addons: (especially apt.packages that are daemons), and any daemon-starting command in before_install: / before_script:.
-
Confirm the tests actually reach that service — grep the specs for a hardcoded host/port:
grep -rnE '127\.0\.0\.1|localhost|:6379|:5672|:9092|:27017' spec/
-
Watch for the network_mode: host trap: a vendored .ci/docker-compose.override.yml that sets network_mode: host on the logstash service is a tell-tale sign the harness expects a daemon on the runner host (exactly what Travis's before_install started). Under GHA there is no such daemon, so host networking reaches nothing and every spec fails with a connection-refused error.
Fix it — reconstruct the service inside the harness so the specs' host/port still resolve, without touching plugin source or specs. The cleanest, spec-preserving approach is to add the missing service to .ci/docker-compose.override.yml. If the existing service uses network_mode: host, give the new service network_mode: host too so it binds the same localhost:<port> the specs expect:
# .ci/docker-compose.override.yml — input-redis worked example
services:
logstash:
network_mode: host
redis:
image: redis:7
network_mode: host # binds host 0.0.0.0:6379, mirroring Travis's `service redis-server start`
(If the harness is on a bridge network instead, add the service normally and — only if the specs read the host from an env var — point that var at the compose service name. Do not edit specs to change a hardcoded host; keep the service reachable at the address the specs already use.)
Record in the PR description every Travis services: / addons: / before_install daemon and how you reconstructed it in the GHA harness.
3. Update README.md status badges (on main)
This is required. Replace any Travis badge with the equivalent GitHub Actions badges — one per workflow file you added. Add ?branch=main to each badge URL so the badge reflects the status of this branch specifically.
Replace lines that look like:
[](https://travis-ci.com/logstash-plugins/logstash-filter-split)
With one badge per added workflow, e.g.:
[](https://github.com/logstash-plugins/logstash-filter-split/actions/workflows/unit-tests.yml)
[](https://github.com/logstash-plugins/logstash-filter-split/actions/workflows/integration-tests.yml)
[](https://github.com/logstash-plugins/logstash-filter-split/actions/workflows/secure-integration-tests.yml)
Reference: logstash-plugins/logstash-output-elasticsearch#1257.
If the README has no Travis badge at all, add the GHA badge(s) near the top of the file (above the description, matching the convention used in other plugins).
4. Delete .travis.yml
Remove .travis.yml from the repository as part of this PR. Also remove any other Travis-only files if present (e.g. .travis/). The whole point of this migration is to retire Travis for this plugin.
Before deleting, walk through .travis.yml one more time and convince yourself that every matrix entry, env var, and job is covered by the GHA workflows you added. Include that mapping in the PR description (see below).
Explicitly out of scope
- Do not modify plugin source code, specs, or
Gemfile. If you believe a code change is required, stop and call it out in the PR description instead of making the change.
- Do not change the default branch, repo settings, or any other workflow files beyond what is described above.
Branch / PR requirements
The PR must be raised from a branch in the logstash-plugins/logstash-filter-split repository itself, not from a fork. GitHub Actions secrets and reusable-workflow access are not granted to fork-based PRs, so workflows will not run if the PR comes from a fork. Push your working branch directly to logstash-plugins/logstash-filter-split and open the PR from there.
The PR base branch must be main. This issue was assigned with agentAssignment.baseRef = main, so your working branch should already be cut from main. When opening the PR, pass --base explicitly to be safe.
The PR title must be exactly Migrate CI from Travis to GitHub Actions (main branch) — matching this issue's title. Do not invent a different title; consistent titles across (plugin × branch) PRs make the migration trackable. Example:
gh pr create --base main --head <your-branch> \
--title "Migrate CI from Travis to GitHub Actions (main branch)" \
--body "..."
After opening the PR, verify the base branch is correct: gh pr view <number> --json baseRefName must report main. If it shows anything else, fix it with gh pr edit <number> --base main before requesting review.
PR description must include
- Confirmation that the PR targets the
main branch, and a link back to this issue.
- A table mapping each
.travis.yml matrix entry / env combination to the corresponding GHA job(s), demonstrating full parity on main.
- The list of new workflow files added.
- The README badge changes.
- Confirmation that
.travis.yml (and any other Travis files) has been deleted on main.
Migrate CI from Travis to GitHub Actions (
mainbranch)Scope of this issue
This issue covers exactly one branch of this plugin:
main.All work in the PR you open from this issue must target the
mainbranch and onlymain. Other active branches of this plugin are tracked in separate issues — do not touch them here.Plugin context (use this to render the workflow file correctly):
mainmainmain(default branch? yes)Primary goal
Every test currently run by
.travis.ymlon themainbranch MUST also run in GitHub Actions after this change. That is the single criterion for success. Read the.travis.ymlonmaincarefully (including anymatrix.include,envmatrices, andjobs.includeentries) and make sure every combination has an equivalent GHA job. Do not silently drop matrix entries.Context
The
logstash-plugins/.cirepository now hosts shared reusable GitHub Actions workflows that replace the old Travis-based setup. Two pilot plugins have already been migrated and are the canonical examples to follow:logstash-integration-kafkalogstash-output-elasticsearchThe shared reusable workflows live here and are what your new workflow files must call. The canonical branch is
1.x— always use@1.xas the ref:logstash-plugins/.ci/.github/workflows/unit-tests.yml@1.x— unit testslogstash-plugins/.ci/.github/workflows/integration-tests.yml@1.x— integration testslogstash-plugins/.ci/.github/workflows/secure-integration-tests.yml@1.x— secure integration testslogstash-plugins/.ci/.github/workflows/performance.yml@1.x— performance testsRead the source of those reusable workflows here so you know what inputs they accept and what matrices they already define: https://github.com/logstash-plugins/.ci/tree/1.x/.github/workflows. The reusable workflows already handle the elastic-stack-version / snapshot matrix — do not redefine that matrix in this repo; just call the reusable workflow.
The reference POC issue and PR (filter-grok, unit-only case):
What to do
1. Inspect
.travis.yml(follow everyimport:)Determine which test types this plugin actually runs — do not guess, do not assume. The file system is the source of truth.
Many plugin
.travis.ymlfiles contain nothing but animport:directive pointing at a shared config inlogstash-plugins/.ci, for example:When you see
import:, you must fetch the imported file and read it (and recursively any files it imports). The imported file is what defines the matrix — the local.travis.ymlby itself tells you nothing. Use the shared.cirepo on disk if it's available, or fetch viahttps://raw.githubusercontent.com/logstash-plugins/.ci/<ref>/<path>.Only after you have the fully-resolved Travis matrix in front of you, map each test type to a GHA workflow file you need to add:
.github/workflows/unit-tests.ymlcallingunit-tests.ymlINTEGRATION=truein any matrix env.github/workflows/integration-tests.ymlcallingintegration-tests.ymlSECURE_INTEGRATION=truein any matrix env.github/workflows/secure-integration-tests.ymlcallingsecure-integration-tests.ymlHAS_PERFORMANCE_TESTS=1or a_performancejob entry.github/workflows/performance.ymlcallingperformance.ymlIf a test type does not appear in the resolved Travis matrix, do not add a workflow for it. Adding workflows that the plugin never ran under Travis is a failure, not a feature.
Only add the workflow files for test types this plugin actually runs. If
.travis.ymldoes not exercise integration tests, do not add an integration workflow."Unit tests only" does not mean "no external services." A plugin can run only unit tests yet still depend on a backing service (redis, rabbitmq, kafka, …) that Travis started on the host via
services:/addons:/before_install:. Those daemons vanish when.travis.ymlis deleted and are not provided by the reusable harness — see section 2d. Classifying a plugin as "unit-only" does not let you skip that reconciliation.2. Add the consumer workflow file(s)
Schedule durability — read this first. A GitHub Actions
schedule:trigger only fires from the workflow file on the default branch (main), and the scheduled run only exercises the default branch's code unless we explicitly tell it otherwise. Aschedule:block on a non-default branch's workflow file is silently dead. To get nightly coverage of every active branch, the default-branch workflow file uses a scheduled matrix job that iterates over every active branch and passesref: ${{ matrix.branch }}to the reusable workflow.Symmetric file across branches. The same workflow file is committed to every active branch — identical content, no per-branch divergence. The matrix-cron job is dormant on non-default branches (GitHub ignores their
schedule:block) but the file stays in sync so future maintenance is straightforward. Do not strip theschedule:block from non-default-branch copies of the file.Use exactly this YAML for the unit-tests consumer workflow. The same file goes on every active branch — do not change it based on whether this issue's target branch is the default:
For integration / secure integration / performance workflows you add, copy the same shape and swap
nameand theuses:target (integration-tests.yml@1.x,secure-integration-tests.yml@1.x,performance.yml@1.x) plus any inputs the reusable workflow accepts. The on-block, jobs structure, and matrix logic stay identical.secrets: inheritis mandatory on every caller job. Each reusable workflow has a failure-notification step that readssecrets.SLACK_BOT_TOKEN. The reusables do not declare aworkflow_call.secrets:block, so the only way the token reaches them is for the caller to forward all secrets withsecrets: inherit. Addsecrets: inheritas a sibling ofuses:on both thetestsjob and thescheduledjob, in every workflow file you add (unit, integration, secure-integration, performance). Omitting it means the token is empty inside the reusable and failure notifications silently never fire. (This is also why the PR must come from a branch in the repo, not a fork — fork-based PRs cannot inherit secrets; see Branch / PR requirements below.)Note on the
refinput to reusable workflows. The scheduled matrix job passesref: ${{ matrix.branch }}to the reusable workflow so it checks out the right branch. The reusable workflows onlogstash-plugins/.ci@1.xalready accept thisrefinput (default''— no-op for non-scheduled callers).timeout-minutesshould reflect the plugin's actual needs — start with what.travis.ymluses, or 60 if unspecified.2a. Per-plugin matrix overrides (read
.travis.ymlfor these)The reusable workflows ship a default ELASTIC_STACK_VERSION matrix and assume
DISTRIBUTION=default. Most plugins should accept those defaults. But if this plugin's fully-resolved.travis.ymldeviates from the defaults, you must mirror the deviation into the consumer workflow via the newwith:inputs on the reusable.The relevant reusable inputs are:
elastic-stack-versions: a JSON-encoded array of stack version strings, e.g.'["8.current"]'. Empty string = use the reusable's built-in default matrix. Setting it narrows (and on integration / secure-integration also strips the snapshot-onlyinclude:entries for 9.next and main — you get a strict cartesian product of your versions ×[snapshot:false, snapshot:true]with no SSL variants on secure-integration).distribution: scalar string,'default'or'oss'. Maps to theDISTRIBUTIONenv var consumed bydocker-setup.sh(which selects the OSS image suffix when set tooss).Apply these inputs based on what
.travis.ymlsays:.travis.ymljobs.exclude:removes one or moreELASTIC_STACK_VERSIONvalues from the matrixelastic-stack-versions: '[...]'listing only the versions that remain after the excludejobs.include:lists a narrow, hand-picked set ofELASTIC_STACK_VERSIONvalues (rather than relying on the inherited matrix)elastic-stack-versions: '[...]'listing exactly those valuesenv:entry setsDISTRIBUTION=oss(e.g. filter-geoip)distribution: 'oss'on every consumer workflow you addelastic-stack-versionsordistribution— leave them at defaultMaintenance-branch heuristic for unit tests. Travis tolerates broken jobs on maintenance branches because nobody looks; GHA's status checks do not. Apply the same
elastic-stack-versionsnarrowing to unit-tests when both of these hold:no), AND.travis.ymlhasjobs.include:entries that hand-pick a narrow set ofELASTIC_STACK_VERSIONvalues for integration and/or secure-integration jobs, without a paralleljobs.exclude:narrowing the inherited unit-test matrix.A non-default branch whose maintainers only re-declared integration jobs against, say,
8.currentis implicitly an N.x maintenance branch. Its source may not load against the JRuby/Ruby bundled with stack versions outside that set (e.g.,Rufus::Scheduler::CronLine::Fixnumblowing up on Ruby ≥ 2.4 / JRuby 9.4+). The inherited unit matrix fromtravis/matrix.ymlwill surface those incompatibilities even though they're pre-existing source debt, not anything you introduced.When this heuristic fires, pass the same narrowed
elastic-stack-versionsto all workflows you add (unit + integration + secure-integration), and note in the PR description: "Maintenance-branch narrowing: unit-tests narrowed to match integration-tests becausemainis a non-default branch with hand-picked integration versions. Travis onmainhas been failing unit-tests against the broader inherited matrix; GHA mirrors only the versions the branch actually supports."Example: a plugin whose
.travis.ymlonly runs8.currentand setsDISTRIBUTION=oss. The full workflow shape (matrix-cron withtests+scheduledjobs) stays exactly as shown in section 2 — you only add the new keys under eachwith:block, identical on both jobs:The override values must be identical between the
testsandscheduledcallers — the scheduled run is meant to exercise the same matrix as PR runs, just on a different branch.2b. Modernize or delete the plugin's vendored
.ci/scriptsThe
setupcomposite action (fromlogstash-plugins/.ci/setup@1.x) has a "Bootstrap CI assets" step that downloads the shared.ci@1.xtarball and extracts*Dockerfile*,*docker*,*.sh, and*logstash-versions*into the plugin's local.ci/directory. The extraction uses tar's "skip existing files" flag, so the bootstrap only fills in files the plugin does not already vendor — it never overwrites a vendored script.This means: if the plugin vendors a stale
.ci/docker-run.sh(or any other vendored helper), the bootstrap step won't fix it, and the reusable workflow'sbash .ci/docker-run.shstep runs the stale version. That has bitten us before:docker-compose(hyphenated) binary is not installed. Vendored scripts that calldocker-compose …fail withexit code 127.For each vendored script under
.ci/(typical names:docker-run.sh,setup.sh,logstash-run.sh,Dockerfile*,docker-compose*.yml), decide between delete and modernize:Delete the vendored file if its only deviations from the corresponding shared file in https://github.com/logstash-plugins/.ci/tree/1.x are stylistic or stale (e.g., it just uses the old
docker-composev1 syntax). Once deleted, the bootstrap step will provide the modern shared version at runtime. Verify bydiff-ing against the shared file before deleting.Modernize the vendored file if it contains plugin-specific behavior the shared file lacks (e.g., the script brings up a custom set of services, sets plugin-specific env vars, or runs extra steps). In that case, just fix the obsolete invocations:
docker-compose up …docker compose up …docker-compose -f … up …docker compose -f … up …docker-compose downdocker compose downdocker-compose exec …docker compose exec …Bulk modernization:
Do not rename the
.ci/docker-compose.yml/.ci/docker-compose.override.ymlfiles themselves — Compose v2 still reads those filenames. Only the CLI invocation changes.Verify after the change:
Note which scripts you deleted vs. modernized in the PR description.
2c. Reconcile
.travis.ymlenv vars consumed by vendored.ci/scriptsDeleting
.travis.ymlalso deletes every environment variable it defined. Travis injectedenv.global,env.matrix, andjobs.include[].envvalues into the shell before running the.ci/scripts. In GitHub Actions those variables do not exist unless the reusable workflow,setup/action.yml@1.x, or your consumer workflow sets them. A vendored.ci/script that reads a now-undefined variable gets an empty string — no error, just a silent misconfiguration that usually surfaces as an auth/connection failure deep in an integration run.This is a per-plugin decision the reusable workflows deliberately do not make for you: how the plugin's own harness authenticates and configures its services is plugin-specific behavior. You must account for it here.
Do not stop at the one famous variable. A single Travis
env:line usually sets several variables at once (e.g.SECURE_INTEGRATION=true INTEGRATION=true ELASTIC_PASSWORD=password ELASTIC_SECURITY_ENABLED=true ELASTIC_STACK_VERSION=8.current SNAPSHOT=true). You must account for every variable on every env line — fixing only the well-knownELASTIC_PASSWORDand missing a sibling likeELASTIC_SECURITY_ENABLEDon the same line will still leave the matrix red. Enumerate them mechanically; do not eyeball.Do this audit:
From the fully-resolved
.travis.yml, list every variable set inenv.global,env.matrix,matrix.include[].env, andjobs.include[].env. Build the complete set — split each space-delimited env line into individualNAME=VALUEpairs and collect the union of names across all jobs.For each variable, check whether a vendored
.ci/script (orDockerfile*/docker-compose*.yml) actually consumes it:Classify each consumed variable:
setup@1.xINTEGRATION,SECURE_INTEGRATION,DISTRIBUTION,ELASTIC_STACK_VERSION, snapshot flags.ci/script but not set anywhere in GHAELASTIC_PASSWORD,ELASTIC_SECURITY_ENABLED, custom*_HOST/*_PORT/ credential vars for a plugin-specific serviceAfter you have decided how to handle each variable, verify none were missed — every variable that a
.ci/file references must now have a defined value in the GHA path:For each variable in the second class, choose one:
Preferred — modernize the harness so it no longer depends on the Travis-injected var. This is what
logstash-output-elasticsearchdid: its.ci/docker-compose.override.ymlno longer referencesELASTIC_PASSWORD, so nothing external has to supply it. If the vendored harness can carry its own known value, that's the cleanest parity.Otherwise — set the variable explicitly in the consumer workflow. Add it under the
testsandscheduledjobs (identical on both) so the vendored script sees the same value Travis gave it:(Only if the reusable workflow forwards
env:to the harness; otherwise set it inside the vendored script itself as part of modernizing it.)Known secure-integration example —
ELASTIC_PASSWORDandELASTIC_SECURITY_ENABLED(they travel together). Secure-integration plugins typically had a Travis job with env... ELASTIC_PASSWORD=password ELASTIC_SECURITY_ENABLED=true .... Both feed the vendored harness and both must be handled — fixing one and forgetting the other is the classic partial migration:ELASTIC_PASSWORD:.ci/docker-compose.override.yml/logstash-run.shauthenticate as-u elastic:$ELASTIC_PASSWORD. Blank → empty-password auth → readiness fails. Give it a default:ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-password}.ELASTIC_SECURITY_ENABLED:.ci/docker-compose.override.ymlmaps it toxpack.security.enabled=${ELASTIC_SECURITY_ENABLED:-false}. Blank → security disabled → Elasticsearch serves plain HTTP on 9200, but the harness health-check curlshttps://elasticsearch:9200(TLS), the handshake never connects, the readiness loop exhausts, and the job exits 1. Tie it to the secure flag rather than hard-codingtrue— plain integration must keep it off:The
setup@1.xaction setsSECURE_INTEGRATION=trueonly for secure-integration runs, so this default enables TLS exactly when it should and leaves plain integration on HTTP.Do not delete
.travis.yml(section 4) until you have accounted for every variable it set — run the verification grep above and confirm nothing the harness references is left undefined.Record in the PR description the full list of env vars from
.travis.yml, and for each one how you handled it (re-derived by reusable / defaulted in harness / set in workflow / eliminated by modernizing).2d. Reconcile Travis-provided backing services (
services:,addons:,before_install/before_scriptdaemons)A plugin's tests — even ones that run in the unit matrix — may depend on a live backing service (redis, rabbitmq, kafka, mongodb, memcached, …) that Travis started on the host, not inside the plugin's
docker-compose. Travis provided these three ways, all of which vanish when.travis.ymlis deleted:services:— Travis's built-in service list, e.g.services: [redis-server, rabbitmq, docker].addons:— host packages/daemons, e.g.addons.apt.packages: [redis-server], oraddons.hosts.before_install:/before_script:— shell that installs or starts a daemon, e.g.sudo service redis-server start --bind 0.0.0.0.The shared reusable workflows run your tests inside a docker-compose network and do not install or start any of these. A test that connects to a hardcoded
127.0.0.1:<port>/localhost:<port>will getECONNREFUSEDfor every example — a silent regression even though the resolved Travis matrix "only" ran unit tests.Detect it:
In the fully-resolved
.travis.yml, list every entry underservices:,addons:(especiallyapt.packagesthat are daemons), and any daemon-starting command inbefore_install:/before_script:.Confirm the tests actually reach that service — grep the specs for a hardcoded host/port:
grep -rnE '127\.0\.0\.1|localhost|:6379|:5672|:9092|:27017' spec/Watch for the
network_mode: hosttrap: a vendored.ci/docker-compose.override.ymlthat setsnetwork_mode: hoston thelogstashservice is a tell-tale sign the harness expects a daemon on the runner host (exactly what Travis'sbefore_installstarted). Under GHA there is no such daemon, so host networking reaches nothing and every spec fails with a connection-refused error.Fix it — reconstruct the service inside the harness so the specs' host/port still resolve, without touching plugin source or specs. The cleanest, spec-preserving approach is to add the missing service to
.ci/docker-compose.override.yml. If the existing service usesnetwork_mode: host, give the new servicenetwork_mode: hosttoo so it binds the samelocalhost:<port>the specs expect:(If the harness is on a bridge network instead, add the service normally and — only if the specs read the host from an env var — point that var at the compose service name. Do not edit specs to change a hardcoded host; keep the service reachable at the address the specs already use.)
Record in the PR description every Travis
services:/addons:/before_installdaemon and how you reconstructed it in the GHA harness.3. Update
README.mdstatus badges (onmain)This is required. Replace any Travis badge with the equivalent GitHub Actions badges — one per workflow file you added. Add
?branch=mainto each badge URL so the badge reflects the status of this branch specifically.Replace lines that look like:
With one badge per added workflow, e.g.:
Reference: logstash-plugins/logstash-output-elasticsearch#1257.
If the README has no Travis badge at all, add the GHA badge(s) near the top of the file (above the description, matching the convention used in other plugins).
4. Delete
.travis.ymlRemove
.travis.ymlfrom the repository as part of this PR. Also remove any other Travis-only files if present (e.g..travis/). The whole point of this migration is to retire Travis for this plugin.Before deleting, walk through
.travis.ymlone more time and convince yourself that every matrix entry, env var, and job is covered by the GHA workflows you added. Include that mapping in the PR description (see below).Explicitly out of scope
Gemfile. If you believe a code change is required, stop and call it out in the PR description instead of making the change.Branch / PR requirements
The PR must be raised from a branch in the
logstash-plugins/logstash-filter-splitrepository itself, not from a fork. GitHub Actions secrets and reusable-workflow access are not granted to fork-based PRs, so workflows will not run if the PR comes from a fork. Push your working branch directly tologstash-plugins/logstash-filter-splitand open the PR from there.The PR base branch must be
main. This issue was assigned withagentAssignment.baseRef = main, so your working branch should already be cut frommain. When opening the PR, pass--baseexplicitly to be safe.The PR title must be exactly
Migrate CI from Travis to GitHub Actions (main branch)— matching this issue's title. Do not invent a different title; consistent titles across (plugin × branch) PRs make the migration trackable. Example:After opening the PR, verify the base branch is correct:
gh pr view <number> --json baseRefNamemust reportmain. If it shows anything else, fix it withgh pr edit <number> --base mainbefore requesting review.PR description must include
mainbranch, and a link back to this issue..travis.ymlmatrix entry / env combination to the corresponding GHA job(s), demonstrating full parity onmain..travis.yml(and any other Travis files) has been deleted onmain.