Standalone end-to-end test repository for the semrel release tool
and its plugin ecosystem. This fills the gap tracked as "End-to-end
integration tests" (Issue #21)
in semrel/MVP-ROADMAP.md: semrel/scripts/local-demo.sh proves the concept
with one throwaway repo and two plugins; this repo generalizes that pattern
into a persistent, extensible suite covering every plugin category and
several core config features (conditions, prereleases, monorepos, version
ceilings, tag-exists handling).
Each scenarios/<NN-name>/run.sh:
- builds
semreland whichever plugin binaries it needs, straight from the sibling repos in the workspace root (../<plugin-repo>/cmd/plugin) -- no registry, no~/.semrel/plugins, no network install; - creates a fresh throwaway git repository under
.work/, seeds it with commits and fixture files; - runs a real
semrel release(or the plugin binary directly, see below) -- real tags, real file edits, real git pushes to local remotes; - asserts on the result (tags created/absent, file contents, changelog text, requests received by a local mock server, ...).
Run everything:
./scripts/run-all.shRun a subset (matches by directory-name prefix):
./scripts/run-all.sh 03 07Run one scenario directly while developing it:
bash scenarios/03-updater-go/run.shEnv vars:
SEMREL_E2E_REBUILD=1-- force rebuilding semrel + all plugin binaries (otherwise they're cached under.bin/between runs)SEMREL_E2E_WORKDIR=/some/path-- where scenario fixture repos are created (default:.work/, gitignored)
As of the last full run: 22 passed, 3 skipped (no external credentials available), 0 failed -- see "Known issues found by this suite" below for what it took to get there, and "Still not covered" for the 3 remaining skips and exactly what would close them.
Every scenario runs real code against real git repos -- no --dry-run. To
keep that safe without needing production credentials on every run,
scenarios split into two groups:
- Local-only (run by default, never touch anything outside
.work/). Providers/hooks that need "a remote" push to a localgit init --baredirectory instead of GitHub/GitLab/etc. Providers/hooks/publishers that need "an HTTP endpoint" hitscripts/mock-http-server/server.js, a tiny local Node server that logs what it received (provider-gitlab, provider- gitea, hook-slack, hook-teams, hook-matrix, hook-jira, publisher-generic- http all run for real against it). hook-email runs for real againstscripts/mock-smtp-server/server.js, a minimal local SMTP catcher. This covers every plugin category except the parts that are inherently external (a real GitHub/Bitbucket release, a real nfpm/oras toolchain and OCI registry). - External, opt-in (skipped unless you export credentials).
13-provider-external(github, bitbucket) and14-hooks-external(slack, as an extra confidence check beyond the local mock) talk to a real account and skip per-block unless you exportSEMREL_E2E_*credentials -- read the comment header in each file.20-publisher-ocineedsSEMREL_E2E_OCI_REFpointing at a real, already-trusted registry (see its header for why a local one can't substitute: the plugin hardcodesoras pushwith no--plain-http/--insecuresupport). 11-packager-nfpmruns for real oncenfpmis on PATH -- get it withgo install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest(no admin/Docker needed).
scripts/run-all.sh treats exit code 77 as skipped, distinct from a real
failure.
| Category | Scenario(s) | Notes |
|---|---|---|
| Condition | 01, 03, 06, 07, 15 | negative path (06) and all three CI-gate plugins, including their undocumented required token vars (07) |
| Provider | 01, 02, 08, 09, 13*, 15, 17 | git (local), gitlab+gitea (local mock), github+bitbucket (13, opt-in real) |
| Updater | 02, 03, 04, 05, 16, 23 | every updater plugin in the workspace, incl. two confirmed README/implementation mismatches (05) |
| Generator | 02, 08, 24 | changelog-md, changelog-html, release-notes, plus commit_changelog: false handoff to a generator plugin |
| Hook | 09, 14*, 18, 19 | gitplugin, slack/teams/matrix/jira (local mock), email (local SMTP), slack (14, opt-in real) |
| Publisher | 10, 20* | generic-http (local mock, real); oci (20, opt-in real -- needs a trusted registry, see below) |
| Packager | 11 | nfpm; real, needs nfpm on PATH (go install-able, see below) |
| Analyzer | 12 | best-effort smoke test, see scenario header -- not wired into semrel release yet |
| Core config | 15, 16, 21, 22, 23 | prerelease channels, workspace independent/lockstep, version_ceiling, tag_exists_strategy |
| Core CLI | 25 | lint, commitlint, doctor, config init/show/validate/set, migrate |
* opt-in / conditionally skipped -- see the scenario's header comment for exactly what it needs.
Everything below needs a credential or resource only you can provide -- these are the only 3 scenarios that skip in the last full run, and each tells you exactly what to export when you run it:
13-provider-external(github, bitbucket) -- needs a real account + a scratch repo you own. ExportSEMREL_E2E_GITHUB_TOKEN/_OWNER/_REPOand/orSEMREL_E2E_BITBUCKET_APP_PASSWORD/_USERNAME/_WORKSPACE/_REPO(see the file header for exact names).14-hooks-external(slack) -- an extra confidence check beyond the local-mock coverage in scenario 18. ExportSEMREL_E2E_SLACK_WEBHOOK_URL. (teams/matrix/jira/email don't have an external variant here at all -- only slack does; add one following its block as a template if you want the same extra check for the others.)20-publisher-oci-- needsoras(go install oras.land/oras/cmd/oras@latest) andSEMREL_E2E_OCI_REFpointing at a real, already-TLS-trusted registry (ghcr.io/..., a company registry, etc.) you're logged into viaoras login. A local plain-HTTP registry was tried and works as a real OCI server, but publisher-oci's ownoras pushcall has no--plain-http/--insecuresupport to talk to one -- see the scenario header.
provider-gitlab/provider-gitea (scenario 17) and hook-teams/hook- matrix/hook-jira/hook-email (scenarios 18/19) are exercised for real,
just against a local mock/SMTP catcher rather than a production instance --
that's a deliberate, permanent design choice (see "Real execution, safe by
default" above), not a gap to close.
Building and actually running every scenario (not just writing them) surfaced real bugs and README/implementation drift across the workspace. Each is documented in the relevant scenario's header comment with exact file/line references; summarized here:
semrel core (semrel/internal/cli/*.go):
semrel workspace release --config <relative path>is broken: package configs get double-joined against the wrong cwd afteros.Chdirand fail with "path not found". Always pass an absolute--configpath toworkspace release. (scenario 16)SEMREL_VERSIONpassed to pre-tag plugins is wrong for any workspace package with a non-trivialtagPrefix(e.g.packages/api@v): it's built from the full tag string instead of the bare version, even thoughSEMREL_TAG_PREFIXis exported separately for exactly this purpose. Every simple updater plugin writes the literal, un-stripped tag string into the version field. (scenario 16)ceiling_strategy: clampdoes not release atversion_ceilingas documented -- it steps down to the next-smaller bump size instead (a major bump retries ascurrent.Minor+1, a major-or-minor bump retries ascurrent.Patch+1), which can land well below the ceiling. (scenario 21)semrel workspace releasewithstrategy: lockstepmiscomputes the shared version: it scrapes"current_version":"(no space) out of--output json, which actually pretty-prints as"current_version": "(with a space) -- the match always fails, current version silently defaults to0.0.0, and the "shared next version" is wrong on every run. (scenario 23)
Plugins (README documents a different contract than the code implements):
updater-terraformignores any variable name and blind-regexes the firstversion = "x"line; there is no way to target a specificvariableblock. (scenario 05)updater-homebrewonly supportsSEMREL_PLUGIN_FILE(defaultFormula.rb) and aversion "x"line rewrite --FORMULA_FILE,URL_TEMPLATE, andSHA256from its README don't exist in the code. (scenario 05)updater-dockerdoesn't touchARG VERSION=at all; it rewrites the image tag on a Dockerfile'sFROMline instead, and with noimagearg it matches (and silently corrupts) the firstFROMline in the file -- including an unrelated base-image pin. (scenario 05)condition-github-actions/condition-gitlab-ci/condition-gitea-actionseach also require an auth-token env var (GITHUB_TOKEN/GH_TOKEN,CI_JOB_TOKEN/GITLAB_TOKEN,GITEA_TOKEN/CI_JOB_TOKEN) that isn't mentioned in their README's config table. (scenario 07)hook-gitplugin's README describes mirroring a release to a second repository viaSEMREL_PLUGIN_REPO; the actual code reads no such variable and instead re-tags/pushes in whatever directory semrel itself is running in -- which collides with the tag semrel's core already created unless given a differentSEMREL_PLUGIN_TAG_NAME. (scenario 09)hook-jira's actual required args areSEMREL_PLUGIN_BASE_URL/_EMAIL/_API_TOKEN/_PROJECT_KEY(Jira Cloud's email+API-token convention), not_TOKEN/_PROJECTas documented. (scenario 18)
None of these were fixed upstream as part of building this suite -- they're recorded here (and in each scenario's header) as findings for whoever owns those repos to triage.
Copy the closest existing scenarios/<NN-name>/run.sh, source scripts/common.sh, and use its helpers (build_semrel, build_plugin,
new_scenario_repo, commit_all, assert_*, start_mock_server,
start_mock_smtp_server, finish). Keep one scenario per run.sh file so
run-all.sh's prefix filter and pass/fail/skip summary stay meaningful.
semrel's own tests/e2e/ (per the roadmap) would test the core engine in
isolation. This repo instead exercises the full, real plugin ecosystem
end-to-end -- deliberately living next to (not inside) semrel/, since it
depends on every plugin repo as a sibling checkout and shouldn't force
semrel/'s own CI to check them all out.