Plugin catalog
These pages are generated from the manifests at plugins/<name>/plugin.yaml at every doc build. The schemas you see here are the ones the control plane uses to validate with: blocks at apply time, so what’s written down matches what runs.
Each plugin’s uses: example pins a channel (@v1, @v2, …). Before relying on one, read Plugin versioning & pinning — in particular, @v1 tracks current content and is not a frozen major.
At a glance
base-image — jdk-base
build — dotnet, go, gradle, maven, node, php, python, ruby, rust
container — buildx, docker, docker-push, kaniko
deploy — ansible, argocd, aws-cli, cloudflare-pages, flyway, gcloud, gh-pages, goose, gravitee, helm, kubectl, kustomize, liquibase, netlify, ssh, terraform, vercel
lint — buf, golangci-lint
notifications — deploy-marker, discord, email, matrix, pr-comment, sentry-release, slack, teams
quality — ai-review, codecov, coveralls, lighthouse-ci, playwright, sonar
registry — artifactory, crates-publish, helm-push, maven-central-publish, nexus, npm-publish, pypi-publish, s3
release — check-pipeline-run, github-release, image-copy, release-notes, release-pr, semver-bump, tag
security — cosign, gitleaks, osv-scanner, semgrep, trivy
ai-review
quality — LLM-driven code review for PR pipelines. Sends the diff between the PR head and its base ref to Claude (Anthropic) or GPT (OpenAI), parses structured JSON findings, and optionally posts them as a GitHub/GitLab PR comment. Trunk-based teams use it as the “extra pair of eyes” stage in pr.yaml — in combination with Sonar (static analysis) and Gitleaks (secrets) it covers the dimensions tests + human review tend to miss (subtle nil paths, security smells, perf cliffs). Cost guardrails: diff is truncated at max-diff-bytes before the API call so a 500 KB monorepo PR doesn’t surprise the team with a $10 bill. Audit trail: full request + response + raw findings saved to .ai-review/ for artifacts.optional: [.ai-review]. Auth never lands on argv: API keys are read from env (ANTHROPIC_API_KEY / OPENAI_API_KEY), and SCM tokens (GITHUB_TOKEN / GITLAB_TOKEN) are read from env via the secrets: list.
Image: ghcr.io/klinux/gocdnext-plugin-ai-review:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
provider | yes | — | claude (Anthropic API) or openai (OpenAI Chat Completions). Picks the API endpoint + default model + which API key env var is consulted. |
model | no | — | Model id override. Defaults per provider: - claude → claude-sonnet-4-6 (good speed/cost balance for code review) - openai → gpt-4o-mini (cheap default; upgrade to gpt-4o for higher-quality reviews on large PRs) Bumping the model up to Opus/GPT-4o multiplies token cost by 5-15x; profile a sample PR before standardising. |
base-ref | no | origin/main | Git ref to diff against. Default origin/main matches the trunk-based contract (PRs branch off main, review = diff against main). The entrypoint runs git fetch origin --depth=200 first so a shallow agent clone resolves. |
head-ref | no | HEAD | Git ref the diff ends at. Default HEAD = the PR’s head SHA (which is what the agent already checked out on a PR run). |
mode | no | console | console (default) prints findings to stdout — useful in pipelines that surface logs to the reviewer. pr-comment additionally POSTs the findings as a Markdown comment on the PR. pr-comment requires pr-number: + repo: + scm-provider: + GITHUB_TOKEN / GITLAB_TOKEN env. |
pr-number | no | — | PR/MR id (GitHub PR number, GitLab MR iid). Required when mode=pr-comment. On PR runs gocdnext materialises this as CI_PULL_REQUEST_KEY server-side from the webhook payload (since v0.9.0), so the idiomatic value is ${CI_PULL_REQUEST_KEY} — no variables: plumbing needed. For manual reruns or non-PR triggers, hardcode the number. |
repo | no | — | Required when mode=pr-comment. GitHub: owner/repo. GitLab: numeric project id or URL-encoded namespace path (my-group%2Fmy-project). |
scm-provider | no | github | github (default) or gitlab. Picks the comment API endpoint and which auth header to use (Bearer vs PRIVATE-TOKEN). |
system-prompt | no | — | Override the default reviewer prompt. Empty (default) = built-in prompt focused on correctness, security, performance, maintainability. Override with project- specific guidance (e.g. “this is a kernel module — focus on memory safety”). The prompt MUST still ask for the JSON array shape the entrypoint parses (severity/file/ line/title/description/suggestion). |
temperature | no | 0 | Sampling temperature passed to the OpenAI API. “0” = deterministic — same diff → same findings. Higher values produce more varied (less reliable) reviews. Claude API doesn’t take this parameter on the messages endpoint and the entrypoint ignores it for provider=claude. |
max-tokens | no | 4096 | Cap on the model’s output tokens. 4096 fits ~20-30 findings comfortably. Bump to 8192 on monorepo PRs with many findings expected; over-sizing this just wastes output budget on the same set of findings. |
max-diff-bytes | no | 50000 | COST GUARDRAIL. Diff is truncated at this many bytes before the API call. Default 50000 (~12k input tokens) covers ~99% of feature PRs and caps cost at a few cents per review. Larger PRs are flagged as truncated in the output. To raise: bump this AND your model’s context window; the model’s pricing scales linearly with input. |
diff-exclude | no | — | Newline-separated :(exclude)<pathspec> git diff patterns. Default skips vendor/, node_modules/, *.lock, *.sum, dist/, build/ — the high-noise paths that dominate diffs without contributing review value. Override (not append) to scope the review tighter for niche cases. |
severity-threshold | no | warning | Filter findings below this severity from console output + PR comment. info shows everything; warning (default) surfaces warnings + errors; error shows only blocking issues. Audit trail (.ai-review/findings.json) always includes the full list regardless of threshold. |
fail-on-error | no | false | true makes the job exit non-zero when any error-severity finding survives the threshold filter — gates PR merge via the required-status-check mechanism. Default false = advisory mode (reviewer reads the findings, decides themselves). Adopt true once the team has calibrated what “error” means for your codebase (false positives become merge-blockers). |
Examples
console review (Claude, advisory)
Simplest shape: model reads the diff vs origin/main, prints findings to the job log. Reviewer reads the findings in gocdnext’s run-detail page. No PR comment, no fail-on- error — a softer rollout while the team calibrates.
ai-review: stage: review secrets: [ANTHROPIC_API_KEY] uses: ghcr.io/klinux/gocdnext-plugin-ai-review@v1 artifacts: optional: [.ai-review] with: provider: claudePR comment with Claude (GitHub)
Posts findings as a PR comment. Requires pr-number + repo + GITHUB_TOKEN. On PR-triggered runs gocdnext materialises CI_PULL_REQUEST_KEY from the webhook payload — use pr-number: ${CI_PULL_REQUEST_KEY} as shown below; no operator plumbing required.
ai-review: stage: review secrets: [ANTHROPIC_API_KEY, GITHUB_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-ai-review@v1 artifacts: optional: [.ai-review] with: provider: claude mode: pr-comment repo: my-org/my-app pr-number: ${CI_PULL_REQUEST_KEY}PR comment with OpenAI (gpt-4o for harder reviews)
Same shape against OpenAI’s API, upgrading to gpt-4o (more expensive, deeper reviews) for security-sensitive repos. Use the severity-threshold: knob to surface only meaningful findings.
ai-review: stage: review secrets: [OPENAI_API_KEY, GITHUB_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-ai-review@v1 artifacts: optional: [.ai-review] with: provider: openai model: gpt-4o mode: pr-comment repo: my-org/my-app pr-number: ${CI_PULL_REQUEST_KEY} severity-threshold: warningPR merge gate (fail on error-severity)
Hardest setting — any error-severity finding blocks the PR via gocdnext’s required-status-check integration. Pair with a calibrated severity threshold; until the team learns what the model flags as “error” this can cause false-positive merge-blocks.
ai-review: stage: review secrets: [ANTHROPIC_API_KEY, GITHUB_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-ai-review@v1 artifacts: optional: [.ai-review] with: provider: claude mode: pr-comment repo: my-org/my-app pr-number: ${CI_PULL_REQUEST_KEY} severity-threshold: error fail-on-error: "true"project-specific prompt + larger context
Override the default reviewer prompt to focus on a specific concern (here: a Go server). Bump max-diff-bytes + max-tokens for a monorepo PR that legitimately needs a longer context window — cost scales linearly with both so use sparingly.
ai-review: stage: review secrets: [ANTHROPIC_API_KEY] uses: ghcr.io/klinux/gocdnext-plugin-ai-review@v1 artifacts: optional: [.ai-review] with: provider: claude model: claude-opus-4-8 max-diff-bytes: "150000" max-tokens: "8192" system-prompt: | You are a senior Go reviewer for a high-throughput CI/CD server. Review the diff focusing on: - goroutine lifecycle (leaks, races, missing context cancellation propagation) - SQL injection via pgx string concatenation - Allocations in hot paths (scheduler dispatch, log streaming, webhook ingest) - Error wrapping with %w (never %v) Output the JSON array shape: severity / file / line / title / description / suggestion.tighter scope (only src/ + scanner code)
Override diff-exclude to ALSO skip docs + tests; useful for a project where the review value is concentrated in production code.
ai-review: stage: review secrets: [ANTHROPIC_API_KEY] uses: ghcr.io/klinux/gocdnext-plugin-ai-review@v1 artifacts: optional: [.ai-review] with: provider: claude diff-exclude: | :(exclude)**/vendor/** :(exclude)**/node_modules/** :(exclude)**/*.lock :(exclude)**/*.sum :(exclude)**/dist/** :(exclude)**/build/** :(exclude)docs/** :(exclude)**/*_test.goansible
deploy — Run Ansible playbooks against managed hosts. Useful for deploys to bare-metal, VMs, and anything outside Kubernetes where kubectl/helm don’t apply. SSH private key, become-password, and vault-password all come through secrets: — written to 0600 tempfiles and passed as file paths so nothing leaks via ps auxww or logs.
Image: ghcr.io/klinux/gocdnext-plugin-ansible:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
playbook | yes | — | Path (under the workspace) to the playbook, e.g. “deploy.yml” or “ops/provision.yml”. |
inventory | no | — | Path to the inventory file. Without this ansible uses its own fallback (/etc/ansible/hosts → empty), which rarely matches a CI container — almost every real usage sets this. |
extra-vars | no | — | -e VALUES. Either a raw “foo=bar baz=qux” string, or a path prefixed with @ (e.g. “@envs/prod.yml”) to load a YAML/JSON vars file. |
tags | no | — | Comma-separated -t tag filter. |
limit | no | — | -l host/pattern filter. |
ssh-user | no | — | Remote SSH user (—user). |
ssh-private-key | no | — | PEM key body. Populate via secrets: — the entrypoint writes it to a 0600 tempfile and passes —private-key instead of putting it on argv. |
become-password | no | — | sudo password for —become. Populate via secrets:; the entrypoint forwards it as ansible_become_password in the env (not argv). |
vault-password | no | — | ansible-vault password for encrypted files. Populate via secrets:; written to tempfile, passed via —vault-password-file. |
check | no | false | Set to “true” to run in —check (dry-run) mode. |
Examples
deploy to prod with SSH key + vault
Typical deploy: private key + vault password come from secrets, inventory points at the prod hosts file, extra vars sourced from a committed YAML.
secrets: - ANSIBLE_SSH_KEY - ANSIBLE_VAULT_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-ansible@v1with: playbook: deploy.yml inventory: inventory/prod extra-vars: "@envs/prod.yml" ssh-user: deploy ssh-private-key: ${{ ANSIBLE_SSH_KEY }} vault-password: ${{ ANSIBLE_VAULT_PASSWORD }}dry-run against staging
Sanity-check a playbook in —check mode before applying.
secrets: - ANSIBLE_SSH_KEYuses: ghcr.io/klinux/gocdnext-plugin-ansible@v1with: playbook: deploy.yml inventory: inventory/staging ssh-user: deploy ssh-private-key: ${{ ANSIBLE_SSH_KEY }} check: "true"argocd
deploy — Run any argocd subcommand against an Argo CD server. The typical CI/CD split: gocdnext owns build + push, Argo CD owns desired state + reconciliation. This plugin is the bridge — trigger a sync on the new commit, wait for health, or rollback when something regresses. Auth via API token (no kubeconfig needed; Argo CD does its own cluster auth internally).
Image: ghcr.io/klinux/gocdnext-plugin-argocd:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
server | yes | — | Argo CD API server URL (e.g. https://argocd.corp). Scheme stripped automatically — the CLI takes a host. |
auth_token | yes | — | Argo CD API token. Generate via argocd account generate-token --account <ci-bot>. Pipe through secrets: so the plaintext never touches the YAML or the run logs. |
command | yes | — | argocd subcommand + args, word-split. Common shapes: “app sync my-app” “app wait my-app —health —timeout 600” “app rollback my-app 3” “app diff my-app” “app set my-app -p image.tag=${CI_COMMIT_SHORT_SHA}“ |
insecure | no | false | ”true” → --insecure. Skips TLS verification — only for dev clusters with self-signed certs. |
grpc_web | no | false | ”true” → --grpc-web. Use when Argo CD sits behind a reverse proxy that doesn’t speak HTTP/2 to its backend (common with cloud LBs + ingress controllers). |
plugin_env | no | — | A single --plugin-env NAME=value passed to the command — for Argo apps backed by a config-management plugin (a Helm CMP, kustomize CMP, etc). The whole value is forwarded as ONE argument, so a multi-token Helm invocation survives intact: HELM_ARGS=--set global.image.tag=1.2.3 -f values-prod.yaml. This is why a CMP app set needs its own input — folding it into command: would word-split the spaces and Argo would see only the first token. Pairs with command: app set <app>; auto-sync apps redeploy on the parameter change, otherwise add a command: app sync <app> job after. |
Examples
sync after push
The vanilla CI → CD handoff. Pipeline builds + pushes the image; the deploy stage tells Argo CD to sync, which picks up the new tag from the manifest repo.
jobs: sync-prod: stage: deploy uses: ghcr.io/klinux/gocdnext-plugin-argocd@v1 with: server: https://argocd.corp auth_token: ${{ ARGOCD_TOKEN }} command: app sync api-prod grpc_web: "true" secrets: [ARGOCD_TOKEN]sync + wait for health
Two-step deploy that fails the job if the new revision doesn’t reach Healthy within 10 minutes — turns a noisy Argo dashboard alert into a CI red mark on the commit.
jobs: sync-prod: stage: deploy uses: ghcr.io/klinux/gocdnext-plugin-argocd@v1 with: server: https://argocd.corp auth_token: ${{ ARGOCD_TOKEN }} command: app sync api-prod secrets: [ARGOCD_TOKEN]
wait-healthy: stage: deploy needs: [sync-prod] uses: ghcr.io/klinux/gocdnext-plugin-argocd@v1 with: server: https://argocd.corp auth_token: ${{ ARGOCD_TOKEN }} command: app wait api-prod --health --timeout 600 secrets: [ARGOCD_TOKEN]parameterised image tag
Set the image tag on the Argo Application so the new commit deploys without touching the manifest repo. One subcommand per job — an app with an automated sync policy redeploys on the parameter change; otherwise add a command: app sync api-prod job after this one.
jobs: deploy: stage: deploy uses: ghcr.io/klinux/gocdnext-plugin-argocd@v1 with: server: https://argocd.corp auth_token: ${{ ARGOCD_TOKEN }} command: app set api-prod -p image.tag=${CI_COMMIT_SHORT_SHA} secrets: [ARGOCD_TOKEN]Helm config-management-plugin (HELM_ARGS)
Apps whose Argo source is a Helm CMP take their values via a --plugin-env HELM_ARGS=.... The value has spaces (--set + -f), so it can’t ride inside command: (it’d word-split) — pass it through the dedicated plugin_env: input, which forwards it as one argument. A deploy: marker records the version per environment.
jobs: deploy-prod: stage: deploy uses: ghcr.io/klinux/gocdnext-plugin-argocd@v1 with: server: ${{ ARGOCD_SERVER }} auth_token: ${{ ARGOCD_TOKEN }} grpc_web: "true" command: app set katools-production plugin_env: "HELM_ARGS=--set global.image.tag=1.${CI_RUN_COUNTER}.${CI_COMMIT_SHORT_SHA} -f ../../platform/configs/values-prod.yaml" deploy: environment: production version: ${CI_TAG_NAME} secrets: [ARGOCD_SERVER, ARGOCD_TOKEN]rollback after failure
Reactive rollback flow paired with an approval gate. The operator picks a known-good Argo history id and the plugin replays it.
jobs: approve-rollback: stage: rollback approval: approvers: [oncall] description: "Roll prod back to history #${HISTORY_ID}?"
rollback: stage: rollback needs: [approve-rollback] uses: ghcr.io/klinux/gocdnext-plugin-argocd@v1 with: server: https://argocd.corp auth_token: ${{ ARGOCD_TOKEN }} command: app rollback api-prod ${HISTORY_ID} secrets: [ARGOCD_TOKEN]artifactory
registry — Upload to + download from a JFrog Artifactory repository. Sibling of gocdnext/nexus — same action: upload \| download shape, Maven2 layout, version: LATEST support; the difference is the search endpoint (AQL vs. Nexus’s REST). Auth accepts Bearer token (Access/Identity tokens) OR basic user+password/API key — token wins when both are set.
Image: ghcr.io/klinux/gocdnext-plugin-artifactory:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
action | yes | — | upload | download |
url | yes | — | Artifactory base URL. Accepts both “https://mycorp.jfrog.io” (cloud) and “https://myart.corp/artifactory” (self-hosted); the script normalises the trailing “/artifactory” segment. |
repository | yes | — | Repository key. |
group-id | yes | — | Maven groupId (e.g. “com.acme.shared”). |
artifact-id | yes | — | Maven artifactId. |
extension | no | jar | Packaging extension. |
classifier | no | — | Maven classifier. |
version | no | — | action=download: exact version OR “LATEST” (AQL search by created-desc). |
dest | no | . | action=download: destination dir or full file path inside the workspace. |
file | no | — | action=upload: workspace-relative path of the artefact. |
upload-version | no | — | Version label used on action=upload. |
token | no | — | Bearer token (Access/Identity). Populate via secrets: — wins over username/password when both are set. |
username | no | — | Basic-auth user. Populate via secrets:. |
password | no | — | Basic-auth password / API key. Populate via secrets:. |
Examples
pull latest shared lib via Identity Token
Fetches the newest com.acme.shared:common-lib from the libs-release repo into lib/ using a JFrog Identity Token piped from secrets — cleaner than basic auth in 2025 deployments.
secrets: - ARTIFACTORY_TOKENuses: ghcr.io/klinux/gocdnext-plugin-artifactory@v1with: action: download url: https://mycorp.jfrog.io repository: libs-release group-id: com.acme.shared artifact-id: common-lib version: LATEST dest: lib/ token: ${{ ARTIFACTORY_TOKEN }}publish jar post-build
Upload target/my-service.jar under the commit SHA as the version label.
secrets: - ARTIFACTORY_TOKENuses: ghcr.io/klinux/gocdnext-plugin-artifactory@v1with: action: upload url: https://mycorp.jfrog.io repository: libs-snapshot group-id: com.acme.backend artifact-id: my-service upload-version: ${CI_COMMIT_SHORT_SHA} file: target/my-service.jar token: ${{ ARTIFACTORY_TOKEN }}aws-cli
deploy — Run AWS CLI v2 commands. Credentials come through the job’s secrets: list — AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ AWS_SESSION_TOKEN for STS). For OIDC federation the runner only has to export AWS_ROLE_ARN + AWS_WEB_IDENTITY_TOKEN_FILE; the CLI picks that up on its own.
Image: ghcr.io/klinux/gocdnext-plugin-aws-cli:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | aws subcommand + args, word-split. Examples: “s3 cp dist/ s3://bkt/ —recursive”, “ecr describe-repositories”, “ssm get-parameter —name /prod/db —with-decryption”. |
region | no | — | Sets AWS_DEFAULT_REGION so you don’t have to thread —region through every invocation. |
profile | no | — | —profile NAME. Rarely needed in CI. |
working-dir | no | . | Directory under the workspace to cd into before aws runs. |
Examples
sync a build to S3
Typical shape — creds via secrets, region set once, sync the build output to a bucket.
secrets: - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEYuses: ghcr.io/klinux/gocdnext-plugin-aws-cli@v1with: command: s3 sync dist/ s3://mycorp-releases/app/${CI_COMMIT_SHORT_SHA}/ region: us-east-1env: AWS_ACCESS_KEY_ID: ${{ AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ AWS_SECRET_ACCESS_KEY }}read an SSM parameter
Pull a secret/value from SSM Parameter Store into a shell var for a downstream step.
secrets: - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEYuses: ghcr.io/klinux/gocdnext-plugin-aws-cli@v1with: command: ssm get-parameter --name /prod/db/url --with-decryption --query Parameter.Value --output text region: us-east-1env: AWS_ACCESS_KEY_ID: ${{ AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ AWS_SECRET_ACCESS_KEY }}buf
lint — Run buf CLI commands — lint, breaking, generate, format. Wraps bufbuild/buf:1.47 with git + ca-certificates so breaking --against references work out of the box.
Image: ghcr.io/klinux/gocdnext-plugin-buf:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | buf subcommand + args, word-split. Examples: “lint”, “breaking —against .git#branch=main”, “generate”. |
working-dir | no | . | Directory under the workspace to cd into before running buf. |
Examples
lint protos
Lint contracts under proto/.
uses: ghcr.io/klinux/gocdnext-plugin-buf@v1with: working-dir: proto command: lintbreaking-change check vs main
Fail the build if proto changes break consumers on main. Runs from the repo root so .git is reachable.
uses: ghcr.io/klinux/gocdnext-plugin-buf@v1with: command: breaking --against .git#branch=mainbuildx
container — Multi-arch container build via docker buildx. Unlike gocdnext/docker (single-arch), this plugin sets up QEMU emulation and a dedicated buildx builder so one job produces a manifest list covering amd64 + arm64 (or any platforms you pick). Requires docker: true on the job. Pipe registry creds via secrets: — plaintext never touches the log.
Image: ghcr.io/klinux/gocdnext-plugin-buildx:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
image | yes | — | Target image repo without a tag, e.g. “ghcr.io/org/app”. |
tags | no | latest | Comma- or space-separated tag list. A single buildx pass produces all tags as one manifest list per tag. |
platforms | no | linux/amd64,linux/arm64 | Comma-separated target platforms. The default covers 95% of modern deployments; add “linux/arm/v7” or “linux/ppc64le” when targeting embedded / power. |
dockerfile | no | Dockerfile | Path to the Dockerfile, relative to the build context. |
context | no | . | Build context directory, relative to the workspace. |
build-args | no | — | KEY=VALUE list (comma- or newline-separated) forwarded as --build-arg. |
push | no | true | Set to “false” to skip the registry push. Multi-arch images can’t live in the legacy daemon store, so “false” builds both arches into the builder cache without a local image. |
registry | no | — | Registry hostname for login. Defaults to the prefix of image up to the first ”/”. |
username | no | — | Registry username. Empty = no login. |
password | no | — | Registry token/password. Populate via the job’s secrets: list so the plaintext stays out of the log. |
cache | no | — | Shorthand for layer caching. Pick one: - “registry” → reuses the destination registry as cache target (<image>:buildcache tag, mode=max). Auth flows through the same registry creds you already pass. - “inline” → embeds the cache metadata into the pushed manifest (no separate tag). Cheap but only works when push: true; ignores layers not in the final image. - “bucket” → reads GOCDNEXT_LAYER_CACHE_* env vars (set by the runner profile’s env: block) and builds a type=s3 cache spec automatically. Combined with the profile’s encrypted secrets: (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) it lets every job on the profile hit the same S3/GCS layer cache without any per-job credential plumbing. - "" (empty) → no automatic cache wiring; use cache-to / cache-from below if you want explicit control. Mutually exclusive with cache-to/cache-from — those win when both are set. |
cache-to | no | — | Full BuildKit --cache-to spec, passed verbatim. Use this for S3/GCS/Azure backends. Examples: type=s3,region=us-east-1,bucket=my-cache,name=org/app,mode=max type=gha,mode=max type=registry,ref=ghcr.io/org/app:cache,mode=max Pipe AWS/GCS credentials via secrets: so BuildKit can reach the bucket — the plugin doesn’t proxy the upload. |
cache-from | no | — | Full BuildKit --cache-from spec, passed verbatim. Usually the import-side mirror of cache-to. Examples: type=s3,region=us-east-1,bucket=my-cache,name=org/app type=registry,ref=ghcr.io/org/app:cache |
mirror | no | — | Pull-through mirror for docker.io base images, e.g. “mirror.gcr.io” or “registry.example.com/dockerhub” (path prefixes supported). FROM pulls resolve via the mirror with automatic fallback to Docker Hub when it misses or is down — the fix for Hub anonymous rate limits when the cluster egresses through shared NAT IPs. Only docker.io is mirrored; images referencing other registries are untouched. |
mirror-username | no | — | Username for an authenticated mirror. Optional — public mirrors need no login. |
mirror-password | no | — | Mirror token/password. Populate via the job’s secrets: so the plaintext stays out of the log. |
Examples
build behind a Docker Hub mirror
Route base-image pulls through an internal pull-through mirror — FROM lines keep referencing docker.io images, the builder resolves them via the mirror and falls back to the Hub on miss. Pair with cache: registry so warm rebuilds rarely pull bases at all.
secrets: [REGISTRY_USERNAME, REGISTRY_TOKEN]docker: trueuses: ghcr.io/klinux/gocdnext-plugin-buildx@v1with: image: registry.example.com/org/app tags: "${CI_COMMIT_SHORT_SHA}, latest" mirror: registry.example.com/dockerhub cache: registry username: ${{ REGISTRY_USERNAME }} password: ${{ REGISTRY_TOKEN }}amd64 + arm64 release
Typical release shape — one job, two architectures, pushed as a single manifest list. Creds come from secrets so the token never touches the log.
secrets: - GHCR_USERNAME - GHCR_TOKENdocker: trueuses: ghcr.io/klinux/gocdnext-plugin-buildx@v1with: image: ghcr.io/org/app tags: "latest, v${CI_COMMIT_SHORT_SHA}" platforms: linux/amd64,linux/arm64 username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}PR preview (build-only, no push)
Validate the Dockerfile still builds on both arches on a PR, without writing to the registry.
docker: trueuses: ghcr.io/klinux/gocdnext-plugin-buildx@v1with: image: ghcr.io/org/app tags: pr-${CI_PULL_REQUEST_KEY} push: "false"Layer cache via runner profile (zero per-job config)
Admin configures the runner profile once (env + encrypted secrets in /admin/profiles); every build that lands on that profile inherits the bucket coords + creds. The job just opts in with cache: bucket.
agent: profile: fast-buildsdocker: trueuses: ghcr.io/klinux/gocdnext-plugin-buildx@v1with: image: ghcr.io/org/app tags: latest cache: bucketregistry-as-cache shorthand
Zero-config option for projects that already push to a registry: BuildKit writes a <image>:buildcache tag and reads from it on the next run. No bucket or extra creds beyond what the push already requires.
secrets: - GHCR_USERNAME - GHCR_TOKENdocker: trueuses: ghcr.io/klinux/gocdnext-plugin-buildx@v1with: image: ghcr.io/org/app tags: latest username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }} cache: registryexplicit S3 cache spec
Escape hatch when you don’t want runner-profile env injection (e.g. project-specific bucket override). Pipe AWS creds via secrets: and write the full BuildKit cache-to / cache-from spec.
secrets: - GHCR_USERNAME - GHCR_TOKEN - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEYdocker: trueuses: ghcr.io/klinux/gocdnext-plugin-buildx@v1with: image: ghcr.io/org/app tags: latest username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }} cache-to: type=s3,region=us-east-1,bucket=my-cache,name=org/app,mode=max cache-from: type=s3,region=us-east-1,bucket=my-cache,name=org/appcheck-pipeline-run
release — Preflight check against the gocdnext REST API — confirms a target pipeline produced a terminal-success run matching the operator’s filter (tag or commit revision) before a downstream gate proceeds. Replaces the inline curl + jq script in trunk-based-release’s prod.yaml preflight job. Two modes: - single-shot (default): query once, fail loud if no match. For flows where the upstream run is GUARANTEED to be terminal by the time this fires (operator workflow: open prod.yaml after release/stage smoke went green). - poll: re-query every interval up to max-attempts. For flows where the prod deploy is queued right after the release pipeline and the upstream might still be running. Emits the matched run’s id / URL / revision / counter / finished_at to a shell-sourceable file AND to $GOCDNEXT_OUTPUT_FILE (v0.11+) so downstream steps can audit WHICH upstream run cleared the gate.
Image: ghcr.io/klinux/gocdnext-plugin-check-pipeline-run:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
api-url | yes | — | gocdnext base URL (no trailing slash), e.g. https://gocdnext.example.com. HTTPS recommended; http:// accepted for in-cluster / dev. Charset validated against shell metas + spaces. |
api-token | yes | — | gnk_... API token. Pipe via secrets: — the plugin writes it to a curl --config tempfile (0600 + EXIT/INT/TERM trap) so it NEVER lands on ps auxww. |
project | yes | — | Project slug to query (e.g. acme-org). The plugin reads GET /api/v1/projects/{slug}?runs=100 to scan recent runs. |
pipeline | yes | — | Pipeline name to match within the project’s recent runs. Same name shape gocdnext’s parser accepts (alphanumeric + dash + underscore). |
tag | no | — | Match runs whose cause_detail.tag_name equals this value — i.e. the run was triggered by a tag push (cause=tag) for this tag. EXACTLY ONE of tag: OR revision: must be set: blank is rejected (would match any recent success), and passing both is rejected (would silently AND the filters and confuse the operator). Charset follows Git refname rules (so release/v1.2.3 and v1.2.3+build.1 are accepted), not OCI image tag rules. |
revision | no | — | Match runs whose primary commit revision starts with this value. Prefix-match — pass ${CI_COMMIT_SHORT_SHA} directly. 7-40 hex chars. Mutually exclusive with tag:. |
expected-status | no | success | Comma-separated terminal statuses to accept. Default success. Whitespace around items is tolerated (success, canceled works). Use success,canceled for rare flows where a canceled run is OK (e.g. promote-to-prod considers a canceled “we did stage-smoke and cancelled the rest” run as valid). |
max-age | no | 7d | Reject matches whose finished_at is older than this. Format: <N>{m|h|d} (minutes/hours/days). Set to 0 to disable. Default 7 days — keeps the preflight from accepting a stale matching run that doesn’t reflect the current promotion intent. |
poll-attempts | no | 1 | Number of times to re-query the API when the first attempt finds no match. 1 (default) = single-shot. Higher values enable poll mode for queued-immediately-after-release flows. Capped at 60. |
poll-interval | no | 30 | Seconds between poll attempts. Capped at 600s. Ignored when poll-attempts=1. |
runs-limit | no | 100 | How many recent runs of the project the plugin scans per attempt. Default + cap = 100 because the server-side /api/v1/projects/{slug}?runs=N handler silently caps at 100 today; promising anything higher would create a false sense of coverage. If a legitimate match falls outside the last 100 runs of the project, tighten max-age: or use a narrower upstream pipeline name instead. |
output | no | .gocdnext/preflight.env | Workspace-relative output file (shell-sourceable). Always written on success. ALSO written to $GOCDNEXT_OUTPUT_FILE when the agent set it (v0.11+ native outputs); operators declare outputs: and reference values via ${{ needs.preflight.outputs.run_id }}. Workspace-relative with no .. traversal; absolute paths rejected. |
Examples
preflight prod deploy against the release pipeline
The canonical use: prod.yaml’s preflight job confirms the release pipeline produced a terminal-success run for the tag being promoted. Fails the deploy chain (gate stays red) when no matching run exists. Replaces the inline curl + jq + set -e in prod.yaml.
preflight: stage: preflight needs: [approve-prod] secrets: [GOCDNEXT_API_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-check-pipeline-run@v1 outputs: run_id: RUN_ID run_url: RUN_URL with: api-url: https://gocdnext.example.com api-token: ${{ GOCDNEXT_API_TOKEN }} project: my-org pipeline: release tag: ${TAG} expected-status: success max-age: 7d artifacts: paths: [".gocdnext/preflight.env"]native outputs (gocdnext v0.11+) — pass run_url to audit log
Combines outputs: with the post-deploy notification step so the audit message links the promoted-to-prod tag to the upstream release run that cleared it.
jobs: preflight: stage: preflight secrets: [GOCDNEXT_API_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-check-pipeline-run@v1 outputs: run_url: RUN_URL with: api-url: https://gocdnext.example.com api-token: ${{ GOCDNEXT_API_TOKEN }} project: my-org pipeline: release tag: ${TAG}
notify: stage: verify needs: [deploy-prod, preflight] uses: ghcr.io/klinux/gocdnext-plugin-slack@v1 secrets: [SLACK_WEBHOOK] with: webhook: ${{ SLACK_WEBHOOK }} channel: "#deploys" template: | :rocket: ${TAG} live in PROD upstream release run: ${{ needs.preflight.outputs.run_url }}poll mode — prod queued right after release
When prod.yaml is triggered immediately after release.yaml (rare; typical workflow waits for a human), the release run may still be running. poll-attempts=20 with 30s interval = up to 10 minutes of waiting before failing loud.
preflight: stage: preflight secrets: [GOCDNEXT_API_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-check-pipeline-run@v1 with: api-url: https://gocdnext.example.com api-token: ${{ GOCDNEXT_API_TOKEN }} project: my-org pipeline: release tag: ${TAG} poll-attempts: "20" poll-interval: "30"match by commit revision (no tag flow)
For teams that don’t tag every release. Pass the short or full SHA — the plugin prefix-matches against the run’s primary revision (lowest-uuid material, same rule as scheduler/civars.go::primaryRevision).
preflight: stage: preflight secrets: [GOCDNEXT_API_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-check-pipeline-run@v1 with: api-url: https://gocdnext.example.com api-token: ${{ GOCDNEXT_API_TOKEN }} project: my-org pipeline: main revision: ${CI_COMMIT_SHORT_SHA} max-age: "24h"cloudflare-pages
deploy — Deploy a built directory to Cloudflare Pages via wrangler. Token + account id ride env through the job’s secrets: (wrangler’s native contract), never argv. branch: maps non-production branches to Pages preview URLs.
Image: ghcr.io/klinux/gocdnext-plugin-cloudflare-pages:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir | yes | — | The built site directory. |
project-name | yes | — | The Cloudflare Pages project. |
branch | no | — | Deployment branch — non-production branches get preview URLs (e.g. pr-${CI_PULL_REQUEST_KEY}). |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
production deploy
secrets: [CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID]uses: ghcr.io/klinux/gocdnext-plugin-cloudflare-pages@v1with: dir: dist project-name: my-sitePR preview
secrets: [CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID]uses: ghcr.io/klinux/gocdnext-plugin-cloudflare-pages@v1with: dir: dist project-name: my-site branch: pr-${CI_PULL_REQUEST_KEY}codecov
quality — Upload a coverage report to Codecov via the official codecov-cli. Auth via a repo upload token (piped through secrets:) — works from private forks where the old auto-discover-token trick depended on GitHub Actions metadata. Flags split uploads per stage in the Codecov UI (unit / integration / e2e, …).
Image: ghcr.io/klinux/gocdnext-plugin-codecov:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
token | yes | — | Codecov repo upload token. Populate via secrets:. |
file | no | — | Explicit coverage file (cover.out, coverage.xml, lcov.info, …). Auto-detected at the working-dir root when omitted. |
flags | no | — | Comma-separated Codecov flags, e.g. “unit,integration” — each becomes a —flag arg so the Codecov UI can split coverage by stage. |
working-dir | no | . | Directory under the workspace to cd into first. |
slug | no | — | “owner/repo” override when the working-dir isn’t a git checkout (e.g. the coverage file came in as an uploaded artifact). |
url | no | — | Codecov Enterprise base URL. Defaults to the cloud. |
Examples
Go coverage from gotest
Upload cover.out produced by go test -cover -coverprofile. Pipe the token through secrets so it never touches the log.
secrets: - CODECOV_TOKENuses: ghcr.io/klinux/gocdnext-plugin-codecov@v1with: file: cover.out flags: unit token: ${{ CODECOV_TOKEN }}split unit + integration uploads
Two invocations in a single job — same commit, different flags. Codecov merges them per-commit in its UI.
secrets: - CODECOV_TOKENuses: ghcr.io/klinux/gocdnext-plugin-codecov@v1with: file: coverage/integration.lcov flags: integration token: ${{ CODECOV_TOKEN }}cosign
security — Sign, verify, and attest container images via cosign (Sigstore). Keyless signing (OIDC) is preferred — no long-lived key material to rotate — but a password-protected cosign.key on disk is supported for environments that can’t reach Fulcio/Rekor yet. image: should be a digest-pinned ref; signing a tag silently follows whichever image the tag resolves to at sign time.
Image: ghcr.io/klinux/gocdnext-plugin-cosign:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
image | yes | — | Digest-pinned image ref, e.g. “ghcr.io/org/app@sha256:abc…”. Tag refs mutate; cosign signs what the ref resolves to right now. |
action | no | sign | sign | verify | attest |
key | no | — | Path (under the workspace) to a cosign.key file. Mutually exclusive with key-content:. Empty = keyless signing/verification (Fulcio + OIDC). |
key-content | no | — | Inline cosign private-key bytes (the value of cosign.key itself). The entrypoint writes the content to a 0600 tempfile inside the plugin’s container and wipes it on exit via trap; the key never touches the artifact backend. Mutually exclusive with key: (file path). Typical use: key-content: ${{ COSIGN_PRIVATE_KEY }} with secrets: [COSIGN_PRIVATE_KEY] on the job, which keeps the bytes in the gocdnext log-mask layer the whole way through. |
key-password | no | — | Passphrase for the private key. Populate via secrets: so the plaintext stays out of the log. |
cert-identity | no | — | action=verify keyless: expected OIDC subject (e.g. the runner identity URL). |
cert-oidc-issuer | no | — | action=verify keyless: expected OIDC issuer URL (e.g. https://token.actions.githubusercontent.com). |
registry | no | — | Registry hostname for login. Defaults to the prefix of image up to the first ”/”. |
username | no | — | Registry username for push access (signatures upload). |
password | no | — | Registry token/password. Populate via the job’s secrets: list so the plaintext stays out of the log. |
predicate | no | — | action=attest: path to the in-toto predicate JSON (SBOM, SLSA provenance, etc). |
predicate-type | no | — | action=attest: predicate type. Common values: spdxjson, cyclonedx, slsaprovenance, custom. |
Examples
keyless sign (digest ref)
Preferred shape — no key material, signs the image the digest pins. Registry creds piped via secrets.
secrets: - GHCR_USERNAME - GHCR_TOKENuses: ghcr.io/klinux/gocdnext-plugin-cosign@v1with: image: ghcr.io/org/app@sha256:${IMAGE_SHA} username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}key-based sign with password
Old-school flow for environments without Sigstore access. The key file is checked into the workspace (usually via secrets-as-files) and the passphrase comes through secrets.
secrets: - COSIGN_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-cosign@v1with: image: ghcr.io/org/app@sha256:${IMAGE_SHA} key: cosign.key key-password: ${{ COSIGN_PASSWORD }}inline key content (no artifact persistence)
Pass the key bytes inline via secrets: — the entrypoint writes them to a 0600 tempfile and trap-cleans on exit. The key never persists in the artifact backend; with the v0.7.x Docker engine + cmd.Env env propagation, it also never lands on the docker CLI argv. Registry creds are required because cosign sign uploads the signature manifest, which on a private registry needs auth. Preferred shape for trunk-based release pipelines whose runners scan-after-publish + sign in the same window.
secrets: - COSIGN_PRIVATE_KEY - COSIGN_PASSWORD - GHCR_USERNAME - GHCR_TOKENuses: ghcr.io/klinux/gocdnext-plugin-cosign@v1with: image: ghcr.io/org/app:v1.2.3 key-content: ${{ COSIGN_PRIVATE_KEY }} key-password: ${{ COSIGN_PASSWORD }} registry: ghcr.io username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}attest an SBOM
Attach an SPDX SBOM as an attestation on the image.
uses: ghcr.io/klinux/gocdnext-plugin-cosign@v1with: image: ghcr.io/org/app@sha256:${IMAGE_SHA} action: attest predicate: sbom.spdx.json predicate-type: spdxjsoncoveralls
quality — Upload a coverage report to Coveralls. Accepts LCOV, JSON, simplecov, or Cobertura XML — the common interchange formats tools export to. Supports Coveralls’ parallel-build flow: flag each slice with parallel: "true" and make the last step call finalise: "true" to close the build.
Image: ghcr.io/klinux/gocdnext-plugin-coveralls:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
token | yes | — | Coveralls repo token. Populate via secrets:. |
file | no | coverage/lcov.info | Workspace-relative coverage file. The default matches the conventional Node/Jest output path. |
format | no | lcov | lcov | json | simplecov | cobertura |
working-dir | no | . | Directory under workspace to cd into first. |
parallel | no | false | Tag the upload as one parallel slice. Coveralls merges slices on the commit after a finalise call. |
finalise | no | false | Fire the “build done” webhook instead of an upload. Call once AFTER every parallel slice has uploaded. |
base-url | no | — | Coveralls Enterprise URL. Defaults to coveralls.io. |
Examples
Jest LCOV upload
Standard node path — Jest writes coverage/lcov.info by default and the plugin auto-picks it up.
secrets: - COVERALLS_TOKENuses: ghcr.io/klinux/gocdnext-plugin-coveralls@v1with: token: ${{ COVERALLS_TOKEN }}parallel unit + e2e then finalise
Three invocations across the pipeline — two parallel slices, one finalise step that closes the build.
# slice 1secrets: - COVERALLS_TOKENuses: ghcr.io/klinux/gocdnext-plugin-coveralls@v1with: file: coverage/unit.lcov parallel: "true" token: ${{ COVERALLS_TOKEN }}crates-publish
registry — Publish a crate to crates.io. The token rides CARGO_REGISTRY_TOKEN via the job’s secrets: — cargo reads it natively from env, never argv. crates.io versions are immutable AND cargo already answers “version exists” with a clean error, so reruns fail loud rather than silently re-tagging. dry-run packages + verifies the build without uploading (no token needed) — the PR preflight.
Image: ghcr.io/klinux/gocdnext-plugin-crates-publish:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir | no | . | Crate directory (where Cargo.toml lives). |
dry-run | no | false | cargo publish —dry-run: package + verify, no upload, no token. Run it on PR pipelines. |
allow-dirty | no | false | Pass —allow-dirty — needed when earlier jobs generated files into the workspace (build artifacts, version stamps). |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
publish on tag
secrets: [CARGO_REGISTRY_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-crates-publish@v1dry-run preflight on PRs
uses: ghcr.io/klinux/gocdnext-plugin-crates-publish@v1with: dry-run: "true"deploy-marker
notifications — Record a deploy event in the observability stack — Datadog (events API) or Grafana (annotations API) behind one provider: switch. Title/text default from the run context (tag or SHA + run id), so the minimal usage is provider + token. Tokens ride the job’s secrets: through a curl config file, never argv.
Image: ghcr.io/klinux/gocdnext-plugin-deploy-marker:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
provider | yes | — | datadog | grafana. |
title | no | — | Event title — default “deploy: |
text | no | — | Event body — default carries the version + run id. |
tags | no | — | Comma-separated tags, e.g. “env:prod,service:shop”. |
grafana-url | no | — | Grafana base URL (grafana provider; or GRAFANA_URL env). |
api-base | no | — | API base override (Datadog EU site, proxies, testing). |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
datadog marker after the prod deploy
jobs: deploy: stage: deploy uses: ghcr.io/klinux/gocdnext-plugin-helm@v1 with: { command: upgrade --install app ./chart }
mark: stage: deploy needs: [deploy] secrets: [DATADOG_API_KEY] uses: ghcr.io/klinux/gocdnext-plugin-deploy-marker@v1 with: provider: datadog tags: "env:prod,service:shop"grafana annotation
secrets: [GRAFANA_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-deploy-marker@v1with: provider: grafana grafana-url: https://grafana.example.com tags: "env:prod"discord
notifications — Post a message to a Discord channel webhook. Sibling of gocdnext/slack — same job contract, Discord-specific wire format. Default message is a concise “pipeline #N → status (sha)” built from CI_* env; override via content for richer copy using Discord markdown (bold, code, mentions).
Image: ghcr.io/klinux/gocdnext-plugin-discord:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
webhook | yes | — | Discord channel webhook URL. Pipe from the job’s secrets: list so the token-bearing URL stays out of logs. |
content | no | — | Message body. Supports Discord markdown. Defaults to a short status line built from CI_PIPELINE, CI_RUN_COUNTER, CI_PIPELINE_STATUS, CI_COMMIT_SHA. |
username | no | — | Override the bot display name for this message. Empty = whatever Discord’s webhook config chose at creation time. |
avatar | no | — | Override the bot avatar with an image URL. Empty = webhook config default. |
tts | no | false | ”true” sends as text-to-speech. Rarely useful in CI; kept for feature parity with Discord’s API. |
Examples
default status line
Minimal hit — uses defaults built from CI_* env.
secrets: - DISCORD_WEBHOOKuses: ghcr.io/klinux/gocdnext-plugin-discord@v1with: webhook: ${{ DISCORD_WEBHOOK }}custom copy + bot identity
Richer post with Discord mrkdwn + an override bot name for a release channel.
secrets: - DISCORD_WEBHOOKuses: ghcr.io/klinux/gocdnext-plugin-discord@v1with: webhook: ${{ DISCORD_WEBHOOK }} username: "release-bot" content: | **${CI_PIPELINE}** shipped ✅ Commit: `${CI_COMMIT_SHA}`docker
container — Build + push a container image. Pairs with docker: true on the job so the agent exposes a Docker daemon (socket mount on Shell/Docker engines, DinD sidecar on Kubernetes). Registry login is done per-run from username/password inputs — pipe them from secrets: so the plaintext never lands in a log.
Image: ghcr.io/klinux/gocdnext-plugin-docker:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
image | yes | — | Target image repo without a tag, e.g. “ghcr.io/org/app”. Tags come from the tags input. |
tags | no | latest | Comma- or space-separated tag list. “latest, v1” builds both in one pass so layers aren’t re-unpacked per tag. |
dockerfile | no | Dockerfile | Path to the Dockerfile, relative to the build context. |
context | no | . | Build context directory, relative to the workspace. |
build-args | no | — | KEY=VALUE list (comma- or newline-separated) forwarded as --build-arg to docker build. |
push | no | true | Set to “false” to build-only (useful on PR previews). |
registry | no | — | Registry hostname for login. Defaults to the hostname prefix of image up to the first ”/”. Irrelevant when username is empty (no login, anonymous push or build-only). |
username | no | — | Registry username. Empty = anonymous (no login). |
password | no | — | Registry token/password. Populate via the job’s secrets: list so the plaintext stays out of the log. |
Examples
build + push with registry auth
Typical “build + push to ghcr” shape with credentials piped from secrets — plaintext never touches the log.
secrets: - GHCR_USERNAME - GHCR_TOKENdocker: true # job-level flag; agent exposes a daemonuses: ghcr.io/klinux/gocdnext-plugin-docker@v1with: image: ghcr.io/org/app tags: "latest, ${CI_COMMIT_SHORT_SHA}" username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}PR preview (build only)
Build-only to validate Dockerfile on pull requests.
docker: trueuses: ghcr.io/klinux/gocdnext-plugin-docker@v1with: image: ghcr.io/org/app tags: pr-${CI_PULL_REQUEST_KEY} push: "false"docker-push
container — Login + retag + push a container image, without building it. The complementary half of gocdnext/docker — build once, then push to many registries / tags in parallel fan-out. Also useful for promotion flows (pull from staging registry, tag for prod, push to prod). Requires docker: true on the job.
Image: ghcr.io/klinux/gocdnext-plugin-docker-push:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
source | yes | — | Image ref already present in the daemon OR available in a remote registry (set pull: true if remote). Examples: “app:latest”, “ghcr.io/org/app@sha256:abc…”. |
target | yes | — | Target repo without a tag, e.g. “ghcr.io/org/app” or “myregistry.corp/app”. |
tags | no | latest | Comma- or space-separated tag list. Each tag triggers one docker tag + push pair. |
pull | no | false | Set to “true” to docker pull the source first (useful when source and target are in different registries). |
registry | no | — | Registry hostname for login. Defaults to the prefix of target up to the first ”/”. |
username | no | — | Registry username. Empty = anonymous (no login). |
password | no | — | Registry token/password. Populate via the job’s secrets: list so the plaintext stays out of the log. |
Examples
fan-out push to two registries
After a build step produces app:build-${CI_RUN_ID}, two parallel jobs push the same image to GHCR + to an internal registry. Each gets its own creds from secrets.
secrets: - GHCR_USERNAME - GHCR_TOKENdocker: trueuses: ghcr.io/klinux/gocdnext-plugin-docker-push@v1with: source: app:build-${CI_RUN_ID} target: ghcr.io/org/app tags: "latest, ${CI_COMMIT_SHORT_SHA}" username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}promote from staging registry to prod
Pull a digest-pinned image from a staging registry, tag for prod, push. Two sets of creds — staging pull and prod push — both via secrets.
secrets: - STAGING_PULL_TOKEN - PROD_PUSH_USERNAME - PROD_PUSH_TOKENdocker: trueuses: ghcr.io/klinux/gocdnext-plugin-docker-push@v1with: source: staging.corp/app@sha256:${STAGING_DIGEST} target: prod.corp/app tags: "stable, ${RELEASE_TAG}" pull: "true" username: ${{ PROD_PUSH_USERNAME }} password: ${{ PROD_PUSH_TOKEN }}dotnet
build — .NET build + test runner. Single image ships the 8.0 and 10.0 LTS SDKs side-by-side (jdk-base pattern); the dotnet muxer resolves a repo’s global.json natively, or pin via the sdk: input when the repo doesn’t carry one. Runs the provided dotnet subcommand and exits with its exit code.
Image: ghcr.io/klinux/gocdnext-plugin-dotnet:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | dotnet subcommand + args, word-split. Examples: “test -c Release”, “build”, “publish -c Release -o out”, “format —verify-no-changes”. |
working-dir | no | . | Directory under the workspace to cd into before running dotnet. Monorepos with a solution in a subdir set this. |
sdk | no | — | Pin the SDK major: “8” or “10”. Only for repos WITHOUT a global.json — the plugin resolves the exact installed version and writes a global.json with rollForward latestPatch. A repo that already ships global.json needs nothing (the muxer honours it); setting both fails loud (exit 2) so two pins can’t drift apart silently. The conflict check mirrors the muxer’s lookup — it walks up from working-dir to the filesystem root, so a repo-root global.json is detected even when working-dir points at a subdir. Empty = muxer default (newest installed SDK). |
Examples
test
Run the test suite in Release. Restore happens implicitly.
uses: ghcr.io/klinux/gocdnext-plugin-dotnet@v1with: command: test -c Releasepin SDK 8 (no global.json in repo)
Forces the 8.0 LTS SDK for a repo that hasn’t adopted global.json. Repos that ship global.json should NOT set this — the file wins and the input would exit 2. Pinning also keeps net8.0 ref-pack resolution fully local: under SDK 8 the bundled packs/ match exactly, while building net8.0 under the (default) SDK 10 requests a newer patch of the ref packs and fetches them from NuGet once into the cache — inherent .NET roll-forward behaviour, same as GHA’s setup-dotnet.
uses: ghcr.io/klinux/gocdnext-plugin-dotnet@v1with: command: test -c Release sdk: "8"publish + artifact
Publish the app and attach the output directory as an artifact for the deploy stage.
uses: ghcr.io/klinux/gocdnext-plugin-dotnet@v1with: command: publish src/Api -c Release -o outartifacts: [out/]cached test run
Tar + restore the NuGet package cache across runs. The plugin redirects NUGET_PACKAGES to .nuget-packages/ inside the workspace so a single paths: entry covers it. Key on the lock file when the repo uses central package locking; fall back to a branch key otherwise.
cache: - key: nuget-{{ hash "**/packages.lock.json" }} paths: [.nuget-packages]uses: ghcr.io/klinux/gocdnext-plugin-dotnet@v1with: command: test -c ReleaseTestcontainers for .NET (integration tests)
docker: true gives the job a Docker daemon. The plugin auto-exports TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE and TESTCONTAINERS_HOST_OVERRIDE only when the host socket is really mounted — in the Kubernetes DinD path Testcontainers for .NET auto-detects via DOCKER_HOST natively.
uses: ghcr.io/klinux/gocdnext-plugin-dotnet@v1docker: truecache: - key: nuget-${CI_COMMIT_BRANCH} paths: [.nuget-packages]with: command: test -c Release --filter Category=Integrationnotifications — Send an SMTP email — the most requested notifier after Slack. Useful for on-call flows that page via a shared mailbox, audit digests, and release-announce distribution lists. Uses Python stdlib smtplib so there’s no third-party notifier dep in the image. HTML is supported via format: html (a plain fallback is always included for Exchange / mobile clients).
Image: ghcr.io/klinux/gocdnext-plugin-email:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
host | yes | — | SMTP host, e.g. “smtp.sendgrid.net”. |
port | no | 587 | SMTP port. 587 for STARTTLS (default), 465 for implicit TLS, 25 for unauthenticated relay inside a corp net. |
username | no | — | Auth user. Empty = no login (open relay). Populate via secrets: for anything reachable from the public internet. |
password | no | — | Auth password / API key. Populate via secrets: — the runner masks it from the log. |
tls | no | starttls | starttls (port 587) | tls (port 465) | none |
from | yes | — | From header. Plain “ci@mycorp.com” or display-form “gocdnext CI ci@mycorp.com”. |
to | yes | — | Comma-separated recipients. |
cc | no | — | Comma-separated CC recipients. |
subject | yes | — | Subject line. |
body | yes | — | Message body. ${CI_*} env vars get expanded by the pipeline parser before this plugin runs, so embed run/commit metadata directly. |
format | no | plain | plain | html |
Examples
failure notification on prod
Typical “page the team on a red build” shape. when: failure runs the job only when an upstream step fails; SMTP creds come from secrets.
when: failuresecrets: - SMTP_USER - SMTP_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-email@v1with: host: smtp.sendgrid.net port: "587" tls: starttls username: ${{ SMTP_USER }} password: ${{ SMTP_PASSWORD }} from: "gocdnext CI <ci@mycorp.com>" to: "oncall@mycorp.com" subject: "[PROD] ${CI_PIPELINE_NAME} #${CI_RUN_COUNTER} failed" body: | Pipeline ${CI_PIPELINE_NAME} failed at ${CI_FINISHED_AT}. Commit: ${CI_COMMIT_SHA} Run: ${CI_RUN_URL}release-announce HTML
Richer HTML body to an announce list on release success.
when: successsecrets: - SMTP_USER - SMTP_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-email@v1with: host: smtp.sendgrid.net username: ${{ SMTP_USER }} password: ${{ SMTP_PASSWORD }} from: "Releases <ci@mycorp.com>" to: "eng-announce@mycorp.com" subject: "Released ${RELEASE_TAG}" format: html body: | <h2>${RELEASE_TAG} shipped</h2> <p>Commit: <code>${CI_COMMIT_SHA}</code></p> <p><a href="${CI_RUN_URL}">Run on gocdnext</a></p>flyway
deploy — Run Flyway schema migrations as a pipeline step. Connection material comes EXCLUSIVELY via the job’s secrets: list (FLYWAY_URL / FLYWAY_USER / FLYWAY_PASSWORD — Flyway reads its own env natively, values never touch argv or the persisted pipeline definition). On Postgres URLs (jdbc:postgresql:), lock hygiene is ON by default: lock_timeout 5s + statement_timeout 15min via initSql, so a DDL that can’t take its lock fails fast instead of queueing production traffic behind it — other databases skip the defaults (the injected initSql is Postgres syntax; use init-sql there). OSS edition: the PR-pipeline preflight is validate + info (dry-run is a Flyway Teams feature). repair is deliberately not exposed.
Image: ghcr.io/klinux/gocdnext-plugin-flyway:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | info | validate | migrate. Convention: validate (+ info) on PR pipelines as the cheap no-effect preflight; migrate only on the protected-branch pipeline, ideally behind an approval gate. repair rewrites the schema history table and belongs to a human with context — run it manually. |
locations | no | filesystem:./migrations | Flyway locations list (comma-separated). Relative paths resolve against working-dir. |
working-dir | no | . | Directory under the workspace to cd into first. |
lock-timeout | no | 5s | POSTGRES-ONLY (the default only applies when FLYWAY_URL is jdbc:postgresql:; setting it explicitly with another database fails loud — use init-sql with your engine’s equivalent there). Injects lock_timeout via Flyway initSql: a DDL that can’t take its lock within this window FAILS instead of queueing every query behind it — the failure mode that takes production down with a “tiny” ALTER TABLE. “0” disables. |
statement-timeout | no | 15min | POSTGRES-ONLY (same gating as lock-timeout). Injects statement_timeout via initSql — caps a runaway backfill or table rewrite. “0” disables. |
init-sql | no | — | Full override of the initSql session setup. When set, lock-timeout / statement-timeout inputs are ignored — you own the whole session preamble. |
Examples
validate on PR (no effect on the database)
The cheap preflight — parses every migration and checks checksums against the schema history. Pair with info in a second task if you want the pending-list in the log.
secrets: [FLYWAY_URL, FLYWAY_USER, FLYWAY_PASSWORD]uses: ghcr.io/klinux/gocdnext-plugin-flyway@v1with: command: validatemigrate gated by approval (deploy pipeline)
migrate runs ONLY after a human approves, and BEFORE the app deploy starts — expand/contract means schema N+1 must serve app N and N+1 (see the migrations concept page).
jobs: approve-migration: stage: migrate approval: approvers: [dba, platform-lead]
migrate: stage: migrate needs: [approve-migration] secrets: [FLYWAY_URL, FLYWAY_USER, FLYWAY_PASSWORD] uses: ghcr.io/klinux/gocdnext-plugin-flyway@v1 with: command: migrate
deploy-canary: stage: deploy needs: [migrate] uses: ghcr.io/klinux/gocdnext-plugin-helm@v1 with: command: upgrade --install app ./chart --set canary=truecustom migrations dir + relaxed statement timeout
A long-running backfill migration that legitimately needs more than 15 minutes — raise the cap explicitly instead of disabling it. Mutating, therefore gated like every apply.
jobs: approve-migration: stage: migrate approval: approvers: [dba, platform-lead]
migrate: stage: migrate needs: [approve-migration] secrets: [FLYWAY_URL, FLYWAY_USER, FLYWAY_PASSWORD] uses: ghcr.io/klinux/gocdnext-plugin-flyway@v1 with: command: migrate locations: filesystem:./db/migrations statement-timeout: "45min"gcloud
deploy — Run Google Cloud SDK (gcloud) commands. Two auth shapes: (1) service-account JSON key piped via secrets: — the entrypoint writes a 0600 tempfile and activates it; or (2) Workload Identity Federation, where the caller sets GOOGLE_APPLICATION_CREDENTIALS to a credential config file and leaves credentials-json empty.
Image: ghcr.io/klinux/gocdnext-plugin-gcloud:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | gcloud subcommand + args, word-split. Examples: “run deploy svc —image gcr.io/p/app:1 —region us-east1”, “container clusters get-credentials my-cluster —region us-east1”, “storage cp file.tgz gs://bucket/”. |
project | no | — | —project ID shortcut. Equivalent to passing —project to every subcommand, but less noise. |
credentials-json | no | — | Raw JSON body of a service-account key. Populate via secrets: so the plaintext never hits argv or logs. Leave empty when using Workload Identity Federation. |
working-dir | no | . | Directory under the workspace to cd into before gcloud runs. |
Examples
deploy to Cloud Run with a service-account key
Classic shape: JSON key via secrets, project set once, run deploy sends the image.
secrets: - GCP_SA_JSONuses: ghcr.io/klinux/gocdnext-plugin-gcloud@v1with: command: run deploy my-svc --image gcr.io/myproj/app:${CI_COMMIT_SHORT_SHA} --region us-east1 --platform managed project: myproj credentials-json: ${{ GCP_SA_JSON }}push container to Artifact Registry
Build + push via gcloud (lean alt to docker login when you only need a one-off push).
secrets: - GCP_SA_JSONuses: ghcr.io/klinux/gocdnext-plugin-gcloud@v1with: command: builds submit --tag us-east1-docker.pkg.dev/myproj/app/server:${CI_COMMIT_SHORT_SHA} project: myproj credentials-json: ${{ GCP_SA_JSON }}gh-pages
deploy — Publish a built directory to a Git branch (the GitHub Pages convention — works on any host). Each deploy is a fresh orphan commit force-pushed to the branch: pages branches are generated artifacts, and keeping a history of minified bundles only bloats every future clone. The remote defaults to the workspace clone’s origin with embedded credentials STRIPPED — pushes use the operator’s GIT_TOKEN from secrets: (the agent’s clone token is read-only/short-lived). Writes .nojekyll automatically; cname: writes a CNAME file.
Image: ghcr.io/klinux/gocdnext-plugin-gh-pages:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir | yes | — | The built site directory (dist/, build/, public/…). Fails loud when missing or empty. |
branch | no | gh-pages | Target branch. |
remote-url | no | — | Push remote (https). Default: derived from the workspace’s origin, credentials stripped. |
cname | no | — | Custom domain — written as the CNAME file. |
commit-message | no | — | Default “deploy: |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
docs site on push to main
Build job produces dist/; this publishes it. The pipeline’s material listens to push only — deploys never run from PRs.
jobs: build: stage: build uses: ghcr.io/klinux/gocdnext-plugin-node@v1 with: { command: "npm ci && npm run build" } artifacts: [dist/]
publish: stage: deploy needs_artifacts: - from_job: build paths: [dist/] secrets: [GIT_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-gh-pages@v1 with: dir: dist cname: docs.example.comgithub-release
release — Publish a GitHub release at the given tag. Wraps the gh CLI — operators pass inputs through with: without crafting curl calls. Auto-generates release notes from commit history when notes is empty and generate-notes stays true. Supports draft + pre-release flags, and uploads asset files the job produced (tarballs, binaries, etc.).
Image: ghcr.io/klinux/gocdnext-plugin-github-release:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
tag | yes | — | Git tag to release (e.g. “v1.2.3”). The tag must already exist at the revision you want released — the plugin doesn’t create the tag, pair with gocdnext/tag if you need that. |
repo | no | — | “owner/name”. Defaults to the workspace’s git remote — safe when the pipeline runs on the repo the release belongs to. |
title | no | — | Release title. Defaults to the tag name. |
notes | no | — | Body text for the release. Empty + generate-notes=true = gh auto-generates from commit history since the previous tag. Explicit notes win when set. |
generate-notes | no | true | When notes is empty and this is “true”, gh auto-generates release notes. Set “false” to create a release with an empty body. |
draft | no | false | ”true” creates the release as a draft — only repo admins see it until someone publishes it. |
prerelease | no | false | ”true” marks the release as a pre-release (alpha/beta). |
assets | no | — | Comma- or newline-separated list of workspace-relative paths to upload as release assets. Globs (bin/*.tar.gz) expand via gh. |
token | yes | — | GitHub token with repo scope. Pipe via the job’s secrets: list so the plaintext never hits the log — gh reads it via GH_TOKEN rather than a CLI flag so it stays out of argv. |
Examples
minimal release
Create a release at the tag just pushed; gh auto-generates notes from commits since the previous tag.
secrets: - GITHUB_TOKENuses: ghcr.io/klinux/gocdnext-plugin-github-release@v1with: tag: ${CI_GIT_TAG} token: ${{ GITHUB_TOKEN }}release with binaries attached
Ship a cross-compiled binary tree as release assets. Operators commonly pair this with a preceding go build step that wrote files into bin/.
secrets: - GITHUB_TOKENuses: ghcr.io/klinux/gocdnext-plugin-github-release@v1with: tag: ${CI_GIT_TAG} title: ${CI_GIT_TAG} assets: | bin/app-linux-amd64.tar.gz bin/app-linux-arm64.tar.gz bin/app-darwin-arm64.tar.gz token: ${{ GITHUB_TOKEN }}gitleaks
security — Secret scanner that catches accidentally-committed credentials, API keys, and tokens. Add at the front of a pipeline so leaks surface before they propagate to downstream jobs. Defaults to scanning the working tree (dir mode); git mode walks the full commit history for repo-lifetime audits.
Image: ghcr.io/klinux/gocdnext-plugin-gitleaks:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
path | no | . | Path under the workspace to scan. Defaults to the whole repo root; point at a subdir to narrow the scope on a monorepo. |
config | no | — | Path to a gitleaks.toml config (relative to the workspace). Empty = use the bundled default ruleset. |
format | no | json | Report format — json, sarif, or csv. |
report | no | — | Path under the workspace to write the report to. Pair with artifacts.optional: to attach the report to the run. |
scan-mode | no | dir | ”dir” scans the current working tree (fast, checks what’s checked out). “git” walks commit history (slower, catches secrets buried in old commits). |
exit-code | no | 1 | Exit code when leaks are found. Set to “0” for advisory- only scans that attach a report but don’t fail CI. |
verbose | no | true | When “true”, each finding is printed inline to the build log as it’s discovered (file:line, rule, redacted secret). Set “false” to silence inline output — the report file (if report: is set) still gets the full details. Default on so operators don’t have to download the JSON report to see what was flagged. |
redact | no | 75 | Percentage of each secret’s body to mask in the verbose inline output. 75 leaves prefix + suffix visible so the operator can identify the credential without leaving the key in plaintext in the build log stream. “0” disables masking (DANGEROUS — prints the secret); “100” fully masks. |
allowlist-paths | no | — | Comma- or whitespace-separated list of directory / path substrings to allowlist. Each entry becomes a .*<path>.* regex under [allowlist].paths in a runtime gitleaks config — substring match, so docs/ covers docs/, services/web/docs/, etc. Useful to skip known-safe directories (docs with example tokens, test fixtures with fake creds) WITHOUT committing a .gitleaks.toml to the repo. Composition: combines with config: via gitleaks’ [extend].path chain — the operator’s config rules stay active, our allowlist paths are additive. Without config:, the plugin synthesizes a runtime config that explicitly keeps [extend].useDefault = true so the built-in gitleaks ruleset isn’t accidentally disabled. Validation: charset restricted to [a-zA-Z0-9/_.-]. .. and absolute paths rejected at parse time. Regex meta- chars not supported here — use a real .gitleaks.toml via config: for those. SAFETY NOTE: every path you allowlist is a place secrets can hide undetected. Real-world incidents include “fake” API keys in README examples that turned out to be live, and staging credentials in test fixtures that became the attack vector. Prefer narrow targets (tests/fixtures/) over broad ones (tests/); review the list periodically. |
Examples
block PR on leak
Default mode — scan the working tree, fail on any leak.
uses: ghcr.io/klinux/gocdnext-plugin-gitleaks@v1advisory scan with report
Don’t fail the build; attach the JSON report so operators can review findings without blocking merges.
uses: ghcr.io/klinux/gocdnext-plugin-gitleaks@v1with: format: json report: gitleaks-report.json exit-code: "0"artifacts: optional: [gitleaks-report.json]with allowlist config
Point gitleaks at a project-managed .gitleaks.toml to whitelist test fixtures + example files. The TOML file lives in the repo so a local gitleaks detect --config .gitleaks.toml matches CI behaviour exactly.
uses: ghcr.io/klinux/gocdnext-plugin-gitleaks@v1with: config: .gitleaks.tomlskip docs + tests via inline allowlist
One-liner alternative to checking a .gitleaks.toml into the repo. Plugin synthesises a runtime config that extends gitleaks’ built-in ruleset and adds the operator’s paths to [allowlist].paths. Equivalent to maintaining the TOML manually, just keyed to the pipeline YAML instead of a tracked repo file. Combines with config: if both are set — runtime config chains the operator’s via [extend].path.
uses: ghcr.io/klinux/gocdnext-plugin-gitleaks@v1with: allowlist-paths: docs/, tests/, __tests__/fixtures/go
build — Run go toolchain commands — build, test, vet, mod, etc. Ships with go:1.25-alpine + git + make; operator provides the subcommand + args via command: and optionally changes working-dir. Exits with the go CLI’s exit code.
Image: ghcr.io/klinux/gocdnext-plugin-go:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | go subcommand + args, word-split. Examples: “build ./…”, “test -race ./…”, “test -cover -coverprofile=cover.out ./…”. |
working-dir | no | . | Directory under the workspace to cd into before running go. |
cgo | no | — | Override CGO toggle. Empty (default) leaves the toolchain default alone — alpine base ships gcc + musl-dev so CGO_ENABLED=1 unless GOOS forces it off. Set “false” for portable cross-compile or static release binaries. Set “true” to fail loud if the base image ever drops gcc (defensive — race-detector tests need cgo). |
Examples
test with race detector
Run the full test suite with -race (typical CI step).
uses: ghcr.io/klinux/gocdnext-plugin-go@v1with: command: test -race ./...build + coverage
Build then run coverage, attaching the report as artifact.
uses: ghcr.io/klinux/gocdnext-plugin-go@v1with: command: test -cover -coverprofile=cover.out ./...artifacts: optional: [cover.out]cached test run
Tar + restore the module + build caches across runs. Plugin redirects $GOMODCACHE and $GOCACHE to .go-mod/ and .go-cache/ inside the workspace so the cache: block captures both. Key is hash-of-go.sum so dependency bumps invalidate automatically — different branches sharing the same go.sum share the cache safely (contents are content- addressable, multi-version safe). Note: don’t pass -count=1 unless you want to bypass the test cache — go memoises pass/fail per test binary, and -count=1 defeats that memo.
cache: - key: go-{{ hash "go.sum" }} paths: - .go-mod - .go-cacheuses: ghcr.io/klinux/gocdnext-plugin-go@v1with: command: test -race ./...static binary for distroless / scratch
cgo: "false" + CGO_ENABLED=0 path gives a fully static binary you can drop into gcr.io/distroless/static or scratch. Trade-off: no race detector (needs cgo), no DNS via libc resolver (Go uses its own pure-Go resolver).
uses: ghcr.io/klinux/gocdnext-plugin-go@v1with: command: build -trimpath -ldflags="-s -w" -o dist/app ./cmd/app cgo: "false"variables: GOOS: linux GOARCH: amd64testcontainers-go (integration tests with sibling containers)
docker: true mounts the agent’s docker.sock + wires host.docker.internal so testcontainers-go can spin up sibling containers (postgres, redis, localstack, …). The plugin auto-exports TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE and TESTCONTAINERS_HOST_OVERRIDE ONLY when the socket is really present — in the Kubernetes DinD path (DOCKER_HOST=tcp://localhost:2375) Testcontainers-Go auto-detects via DOCKER_HOST without explicit overrides.
uses: ghcr.io/klinux/gocdnext-plugin-go@v1docker: truecache: - key: go-{{ hash "go.sum" }} paths: [.go-mod, .go-cache]with: command: test -race -tags=integration ./...golangci-lint
lint — Run golangci-lint against a Go module. Bundles the upstream v1.63 image; operators provide args + working-dir.
Image: ghcr.io/klinux/gocdnext-plugin-golangci-lint:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
args | no | ./... | Args appended after run. Example: “—timeout 5m ./…”. |
working-dir | no | . | Directory under the workspace to cd into before running. |
timeout | no | 5m | Value for —timeout. Default 5m. |
Examples
lint a single module
Lint the server module with the default 5m timeout.
uses: ghcr.io/klinux/gocdnext-plugin-golangci-lint@v1with: working-dir: serverlint multiple modules in one job
golangci-lint accepts multiple package patterns. The platform runs them serially in the same container — cheaper than three separate jobs when the lint is fast.
uses: ghcr.io/klinux/gocdnext-plugin-golangci-lint@v1with: args: ./server/... ./agent/... ./cli/...cached analyzer + Go module cache
golangci-lint compiles every package + runs every analyzer on a cold cache — the first run on a fresh container takes 5–10 minutes for a typical project. The plugin redirects GOMODCACHE, GOCACHE, and GOLANGCI_LINT_CACHE to workspace- local directories so the platform’s cache: block can tar all three. Warm runs drop to seconds. Hash-keyed on go.sum so a dependency bump invalidates automatically; different branches sharing the same go.sum share the cache safely because the contents are content-addressable.
golangci: stage: lint uses: ghcr.io/klinux/gocdnext-plugin-golangci-lint@v1 cache: - key: golangci-{{ hash "go.sum" }} paths: [.go-mod, .go-cache, .golangci-cache] with: args: ./...goose
deploy — Run goose schema migrations as a pipeline step — the Go-ecosystem counterpart of flyway/liquibase, and the tool gocdnext’s own schema is managed with. The DSN comes EXCLUSIVELY via the job’s secrets: list (GOOSE_DBSTRING — goose reads it natively; the value never touches argv or the persisted pipeline definition). down/redo/reset are deliberately not exposed: migrations are forward-only in pipelines, rollback is a corrective forward migration.
Image: ghcr.io/klinux/gocdnext-plugin-goose:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | status | validate | up. Convention: validate (parses every migration) + status on PR pipelines; up only on the protected-branch pipeline, ideally behind an approval gate. |
dir | no | ./migrations | Migrations directory, relative to working-dir. Fails loud when missing. |
driver | no | postgres | goose driver (postgres | mysql | sqlite3 | mssql | clickhouse). The DSN in GOOSE_DBSTRING must match. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
validate + status on PR
No effect on the database — validate parses every migration file, status shows the pending list in the job log. Two jobs because a plugin job runs one command.
jobs: validate-migrations: stage: test secrets: [GOOSE_DBSTRING] uses: ghcr.io/klinux/gocdnext-plugin-goose@v1 with: command: validate
migration-status: stage: test secrets: [GOOSE_DBSTRING] uses: ghcr.io/klinux/gocdnext-plugin-goose@v1 with: command: statusup gated, before the deploy
Schema first, deploy second — expand/contract means schema N+1 must serve app N and N+1 simultaneously during a canary (see the migrations concept page). The mutating command sits behind an approval gate, in a pipeline whose material listens to push only.
jobs: approve-migration: stage: migrate approval: approvers: [dba, platform-lead]
migrate: stage: migrate needs: [approve-migration] secrets: [GOOSE_DBSTRING] uses: ghcr.io/klinux/gocdnext-plugin-goose@v1 with: command: up dir: server/migrationsPostgres lock hygiene via DSN options
Inject lock_timeout / statement_timeout in the DSN itself (inside the secret value) so a DDL that can’t take its lock fails fast instead of queueing production traffic behind it. Mutating, therefore gated like every apply.
# GOOSE_DBSTRING secret value:# postgres://app:***@db:5432/app?options=-c%20lock_timeout%3D5s%20-c%20statement_timeout%3D15minjobs: approve-migration: stage: migrate approval: approvers: [dba, platform-lead]
migrate: stage: migrate needs: [approve-migration] secrets: [GOOSE_DBSTRING] uses: ghcr.io/klinux/gocdnext-plugin-goose@v1 with: command: upgradle
build — Run Gradle tasks — build, test, publish, etc. Prefers ./gradlew when the project ships a wrapper (Gradle’s best-practice path since the wrapper pins the exact version); falls back to the bundled gradle 8.10 otherwise. Daemon off by default — CI containers are ephemeral so the daemon rarely earns its keep. --build-cache / --parallel are OPT-IN (default off) so legacy projects don’t break silently; flip them on per-pipeline once you’ve verified the reactor is parallel-safe.
Image: ghcr.io/klinux/gocdnext-plugin-gradle:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | gradle tasks + args, word-split. Examples: “build”, “test —tests com.acme.AppTest”, “publish -Prelease=true”. |
working-dir | no | . | Directory under the workspace to cd into before running gradle. Multi-module monorepos with a nested build.gradle set this. |
gradle-opts | no | — | Extra JVM args exported as GRADLE_OPTS. Useful for “-Xmx4g” on big builds. Default JDK heap (~256 MB) is undersized for any non-trivial multi-module project. |
daemon | no | false | Set to “true” to run with —daemon instead of —no-daemon. Only worthwhile when the runner caches $GRADLE_USER_HOME across runs AND the job stays on the same agent — in isolated workspace mode (default v0.5.0+), each job is a fresh pod so the daemon never gets to see a second invocation. |
build-cache | no | — | Tri-state. Unset (default) passes NO flag, so the project’s org.gradle.caching property in gradle.properties decides — important because the plugin shouldn’t silently override a project that already opted in. “true” forces --build-cache; “false” forces --no-build-cache (useful on the release/deploy run when you want a fresh build). Pair with capturing .gradle-home/caches/build-cache-1/ in the platform’s cache: block. |
parallel | no | — | Tri-state. Unset (default) passes NO flag, respecting the project’s org.gradle.parallel in gradle.properties. “true” forces --parallel; “false” forces --no-parallel. Modern multi-module Gradle builds (8.x+) are almost always parallel-safe — use the flag to override the project’s property when you want CI to differ from local builds. |
configuration-cache | no | — | Tri-state. Unset (default) respects the project’s org.gradle.configuration-cache property. “true” forces --configuration-cache; “false” forces --no-configuration-cache. Memoises the configuration phase (build.gradle.kts parse + task graph) across runs; breaks on legacy plugins that haven’t migrated, so adopt incrementally. |
args | no | — | Generic extra args appended after command:. Use for one- off flags that don’t deserve a dedicated knob — --info, --stacktrace, -Pfoo=bar, etc. |
jdk | no | 21 | JDK version to run gradle (and the test forks) against. Accepts “11”, “17”, “21”, or “25”; default “21”. Backed by the shared jdk-base image so a single plugin tag covers every supported toolchain — apps stuck on JDK 11 don’t need a separate plugin variant. Typos fail loud at the entrypoint (jdk: "211" aborts with exit 2 instead of falling through to the default). |
Examples
test
Run the test task via the project’s wrapper. No opt-in perf flags — minimal safe shape for a first run.
uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: testparallel + build-cache (recommended for modern reactors)
Multi-module Gradle 8.x build with the two perf flags flipped on. Verify your reactor is parallel-safe before shipping this — typically true for builds that don’t mutate shared state across modules.
uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: build build-cache: "true" parallel: "true"publish with big heap
Publish a release with a 4 GB heap. Build-cache stays off so the published artefact is a fresh build, not a potentially-stale cache hit.
uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: publish -Prelease=true gradle-opts: "-Xmx4g"configuration-cache opt-in
Enable --configuration-cache for faster startup on builds whose plugins all support it. Pair with the local build-cache for fully memoised warm runs.
uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: check build-cache: "true" parallel: "true" configuration-cache: "true"cached build
Tar + restore the dep cache, wrapper JARs, AND the local build-cache across runs. Compact key hashes every Gradle build script (Groovy + KTS, any depth) + the wrapper props so a Gradle version bump invalidates the cache. EXTEND the key with more {{ hash "..." }} tokens for files whose changes should also invalidate — typical additions: gradle.properties (workers, JVM args, plugin flags), gradle/libs.versions.toml (version catalog), **/gradle-wrapper.properties (per-subproject wrappers). Drop tokens for files your project doesn’t have — a token whose glob matches zero files aborts the job. .gradle-home/caches/build-cache-1/ is where --build-cache writes task-output memo — capture it explicitly so warm runs hit it.
cache: - key: gradle-{{ hash "**/*.gradle*" }}-{{ hash "gradle/wrapper/gradle-wrapper.properties" }} paths: - .gradle-home/caches - .gradle-home/wrapper - .gradle-home/notificationsuses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: build build-cache: "true" parallel: "true"cached build with version catalog
Same shape as above, with gradle/libs.versions.toml added to the key — recommended for projects using the Gradle version catalog so a catalog edit invalidates the cache. Use this variant when your repo actually carries gradle/libs.versions.toml; omit the token for projects without it (the hash function aborts on zero matches).
cache: - key: gradle-{{ hash "**/*.gradle*" }}-{{ hash "gradle/libs.versions.toml" }}-{{ hash "gradle/wrapper/gradle-wrapper.properties" }} paths: - .gradle-home/caches - .gradle-home/wrapper - .gradle-home/notificationsuses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: build build-cache: "true" parallel: "true"testcontainers integration tests
docker: true mounts the agent’s docker.sock + wires host.docker.internal. The plugin auto-exports TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE and TESTCONTAINERS_HOST_OVERRIDE ONLY when the socket is really present — in the Kubernetes DinD path (DOCKER_HOST=tcp://localhost:2375) Testcontainers auto-detects via DOCKER_HOST without explicit overrides.
uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1docker: truecache: - key: gradle-{{ hash "**/*.gradle*" }}-{{ hash "gradle/wrapper/gradle-wrapper.properties" }} paths: [.gradle-home/caches, .gradle-home/wrapper]with: command: integrationTest gradle-opts: "-Xmx4g" build-cache: "true" parallel: "true"Android release bundle
Android library/app bundle pattern — needs a sizeable heap and benefits from the build cache. Signing creds come from secrets: so the keystore base64 stays out of the log.
secrets: [ANDROID_KEYSTORE_BASE64, ANDROID_KEY_PASSWORD]uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1cache: - key: gradle-{{ hash "**/*.gradle*" }}-{{ hash "gradle/wrapper/gradle-wrapper.properties" }} paths: [.gradle-home/caches, .gradle-home/wrapper]with: command: bundleRelease gradle-opts: "-Xmx6g" build-cache: "true"artifacts: paths: - "app/build/outputs/bundle/release/*.aab" - "app/build/outputs/mapping/release/mapping.txt"gravitee
deploy — Configure a Gravitee.io API definition through the graviteeio-cli (gio): merge a base values file with the per-API values, render the Jinja template, lint, then create-or-update the API on the Management API (idempotent by name) and optionally deploy it. The bearer token rides the environment, never argv, and is masked in logs.
Image: ghcr.io/klinux/gocdnext-plugin-gravitee:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
api_name | yes | — | API name in Gravitee. Used to look the API up (create vs update), offered to the template as ${API_NAME}, and shown in the log line. Must not contain quotes or newlines. If more than one API shares the name the plugin refuses to guess (disambiguate in Gravitee). |
url | yes | — | Gravitee Management API base URL (GIO_APIM_URL). Must be https:// with a host and no userinfo — the bearer token rides this connection and is never sent over cleartext http. |
token | yes | — | Management API bearer token. Reference a secret — token: ${{ GRAVITEE_TOKEN }} — so it’s resolved and masked; the plugin passes it to gio via the environment, never on argv. |
path | no | . | The API config folder (gio’s “def-path”): holds the per-API values file plus the settings/ and templates/ folders gio expects. Defaults to the workspace root. |
values_file | no | api.yml | Per-API values file (relative to path), overlaid on defaults. |
envsubst_vars | no | — | Extra environment variables (space/comma separated) allowed to be substituted into the rendered config alongside ${API_NAME}. Substitution is ALLOWLIST-only: a ${VAR} not named here (or the always-allowed API_NAME) is left literal, so a job secret in the environment can never leak into the payload sent to Gravitee. List only NON-secret vars (e.g. GO_PIPELINE_GROUP_NAME). Credential-looking names (that are, or end in, TOKEN/SECRET/PASSWORD/KEY/CREDENTIAL) are rejected. Each must be a valid env var name. |
defaults | no | — | Base values file shared across APIs — a workspace path OR an https:// URL (auto-detected; http:// is refused). For a URL, pin a tag/ref for reproducibility and pass config_token if the host is private. Omit to use values_file as-is. |
template | no | — | The api_config.yml.j2 Jinja template — a workspace path OR an https:// URL (http:// is refused). Omit when path/templates/api_config.yml.j2 already exists. |
mode | no | merge | How defaults and values_file combine: merge (deep, arrays concatenated) or overwrite (deep, arrays replaced). |
deploy | no | true | Deploy after applying (—with-deploy, or —with-start on create). |
lint | no | true | Run gio … definition lint before writing to the Management API. |
manage_plans_on_update | no | false | DANGER — leave this false unless you fully understand it. On UPDATE, false (default) strips plans from the payload so the import never touches existing plans, keeping every active subscription intact. true sends plans on update and lets Gravitee reconcile them — which can duplicate, alter, close, or remove a plan, and a plan that changes or disappears BREAKS ITS ACTIVE SUBSCRIPTIONS. Plans are best managed out-of-band; only enable this when the payload’s plans exactly match production. Ignored on first create (a new API has no plans to endanger). |
method_policy | no | warn | Security gate for the keyless path-based model — off | warn | block. Flags an enabled path rule without an explicit methods list (applies to ALL methods by default), and a path with no enabled rules / an empty list (nothing matches → everything reaches the backend). Disabled rules (enabled: false) are ignored. warn (default) logs each finding; block fails the run before touching the Management API; off disables the check. |
auth_policy | no | off | Security gate — off | warn | block. Flags methods a path serves that have NO auth policy (see auth_policies), i.e. reachable unauthenticated under the keyless plan. mock rules count as safe (they terminate, never proxy). Heuristic — intentionally-public paths may flag, so it’s opt-in (default off); start with warn. |
auth_policies | no | oauth2,jwt,api-key | Comma-separated policy names that count as authentication for the auth_policy check. |
org | no | DEFAULT | Gravitee organization (GIO_APIM_ORG). |
env | no | DEFAULT | Gravitee environment (GIO_APIM_ENV). |
ssl_verify | no | true | TLS verification (GIO_SSL_VERIFY): true, false, or a CA path. Prefer ca_bundle to pin a CA over disabling verification. |
ca_bundle | no | — | Management-API CA certificate, PEM or base64 (auto-detected). Written to a file and used to verify TLS. Reference a secret. |
config_token | no | — | Bearer token for fetching defaults/template from a private URL (sent as an Authorization header). Reference a secret. |
Examples
API defined in the repo, base config from a pinned URL
The per-API api.yml lives in the pipeline repo; the shared base values and template come from a versioned config repo by URL (pinned to a tag for reproducibility).
secrets: - GRAVITEE_TOKENuses: ghcr.io/klinux/gocdnext-plugin-gravitee@v1with: api_name: orders url: https://gravitee.example.com/management token: ${{ GRAVITEE_TOKEN }} path: gravitee/apis/orders defaults: https://raw.githubusercontent.com/acme-org/gravitee-base/v1.4.0/values-default-stage.yml template: https://raw.githubusercontent.com/acme-org/gravitee-base/v1.4.0/api_config.yml.j2everything in the repo, CA pinned via a secret
api.yml, settings/ and templates/ all checked in; the Management API CA is pinned with a secret instead of disabling TLS verify.
secrets: - GRAVITEE_TOKEN - GRAVITEE_CAuses: ghcr.io/klinux/gocdnext-plugin-gravitee@v1with: api_name: payments url: https://gravitee.example.com/management token: ${{ GRAVITEE_TOKEN }} path: gravitee/apis/payments defaults: gravitee/base/values-default-prod.yml mode: overwrite ca_bundle: ${{ GRAVITEE_CA }}helm
deploy — Run any helm subcommand against a cluster — upgrade, install, diff, rollback, repo add/update, etc. Pairs with gocdnext/kubectl for the deploy half of a pipeline. Kubeconfig discovery mirrors the kubectl plugin: path relative to workspace, inline YAML, or base64-encoded YAML, auto-detected. Empty kubeconfig defers to the agent’s in-cluster service account.
Image: ghcr.io/klinux/gocdnext-plugin-helm:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | helm subcommand + args, word-split. Examples: “upgrade —install api ./chart -n prod”, “diff upgrade api ./chart”, “repo add stable https://charts.helm.sh/stable”. |
kubeconfig | no | — | Kubeconfig source (path / raw YAML / base64 YAML). Auto-detected. Empty = use the agent’s in-cluster service account. |
namespace | no | — | Default namespace for the command (--namespace). Commands with an explicit -n flag override this. |
working-dir | no | . | Directory under the workspace to cd into before running helm. Useful when the chart lives at ./deploy/chart. |
Examples
upgrade prod release
Typical “CI deploys” shape — kubeconfig from a secret, helm upgrade with values file from the repo.
secrets: - PROD_KUBECONFIG_B64uses: ghcr.io/klinux/gocdnext-plugin-helm@v1with: kubeconfig: ${{ PROD_KUBECONFIG_B64 }} working-dir: deploy/chart command: upgrade --install api . -n prod -f values-prod.yamldiff before deploy
Preview the diff — useful on pull requests.
secrets: - PROD_KUBECONFIG_B64uses: ghcr.io/klinux/gocdnext-plugin-helm@v1with: kubeconfig: ${{ PROD_KUBECONFIG_B64 }} working-dir: deploy/chart command: diff upgrade api . -n prod -f values-prod.yamlhelm-push
registry — Package + push a Helm chart to an OCI registry (default), a ChartMuseum instance, or a Nexus hosted-helm repo. backend: picks the transport. The plugin runs helm package once and hands the tgz to the right publish flow — same bytes, three different HTTP endpoints.
Image: ghcr.io/klinux/gocdnext-plugin-helm-push:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
chart-dir | no | . | Workspace-relative path to the chart directory (must contain a Chart.yaml). |
version | no | — | —version override for helm package. Uses the Chart.yaml value when omitted. |
app-version | no | — | —app-version override for helm package. |
backend | no | oci | oci | chartmuseum | nexus |
oci-repo | no | — | backend=oci: target repo including the oci:// scheme, e.g. “oci://ghcr.io/acme/charts”. |
repo-url | no | — | backend=chartmuseum | nexus: base URL of the chart repo. |
username | no | — | Registry user. Pipe via secrets:. |
password | no | — | Registry token / password. Pipe via secrets:. |
Examples
push to GHCR via OCI
Package the chart in ./charts/app and push to GHCR. Credentials come from the job’s secrets so the token never touches the log.
secrets: - GHCR_USERNAME - GHCR_TOKENuses: ghcr.io/klinux/gocdnext-plugin-helm-push@v1with: chart-dir: charts/app version: ${CI_COMMIT_SHORT_SHA} oci-repo: oci://ghcr.io/acme/charts username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}publish to a Nexus helm repo
Same package step; the PUT lands at a Nexus hosted-helm repository. ChartMuseum looks identical — swap backend.
secrets: - NEXUS_USERNAME - NEXUS_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-helm-push@v1with: chart-dir: charts/app version: 1.4.${CI_RUN_COUNTER} backend: nexus repo-url: https://nexus.corp/repository/helm/ username: ${{ NEXUS_USERNAME }} password: ${{ NEXUS_PASSWORD }}image-copy
release — Promote a container image (multi-arch manifest list preserved) from one registry to another via registry-API copy primitives. Three interchangeable backends: crane (default, smallest + fastest), skopeo (broader OCI tooling), buildx-imagetools (when the job already has docker: true). None of the three transfer cosign signatures — re-sign by the PROMOTED_DIGEST output instead. Closes the multi-arch manifest-list gap that gocdnext/docker- push has — docker tag + docker push from the agent’s local daemon loses the index for multi-platform images, so consumers on other architectures fail to pull. image-copy uses the registry’s native cross-repository BLOB MOUNT (crane / skopeo) or the buildkit manifest-list builder, both of which keep the full manifest tree intact. Always emits the promoted DIGEST to a workspace file so a downstream cosign-sign job can anchor by digest rather than the mutable tag — closing the “what got signed?” question.
Image: ghcr.io/klinux/gocdnext-plugin-image-copy:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
source | yes | — | Source image ref. Either tag form (registry/repo:tag) or digest pin (registry/repo@sha256:...). Digest form is recommended for promotion flows so a concurrent retag of staging doesn’t substitute the image between scan and promote. Must include a real registry host (e.g. ghcr.io/org/app:tag, not the Docker Hub shorthand org/app:tag) — image-copy is a cross-registry tool and refuses ambiguous refs. |
target | yes | — | Target image ref, tag form only (registry/repo:tag). Digest form is rejected — a promotion has to write to a tag the registry can actually create; you can’t “tag” something at a content digest. One destination per job; use extra-tags: for additional tags pointing at the same digest in the SAME target repo (latest, v1, v1.2). |
extra-tags | no | — | Newline-separated additional tags applied to the same target repo after the primary copy. Each tag must match the OCI tag spec: starts with [A-Za-z0-9_], then 0-127 of [A-Za-z0-9_.-]. The plugin validates per-tag and bails on the first bad one so a typo never lands a half-promoted set. Crane retags via crane tag (cheap — repoints, no blob transfer), skopeo via a second skopeo copy, buildx-imagetools by passing all --tag flags in one invocation. |
backend | no | crane | Which copy tool runs. crane (default) is single-binary and multi-arch native. skopeo is the broader OCI Swiss- army knife — picks an order of magnitude more output formats and exposes --multi-arch all explicitly. buildx-imagetools requires the job to declare docker: true so the docker socket is mounted; pick this when your build pipeline already needs the daemon. NONE of the three backends transfer cosign signatures or attestations today. Those live as SEPARATE registry artifacts (sha256-X.sig tags discovered via the cosign triangulation or the OCI referrers API). To preserve a signed image across promotion, re-sign with cosign at the target registry instead — anchored to the PROMOTED_DIGEST this plugin writes to the output file so the signature attests the exact bytes promoted. Native cosign-signature preservation is roadmap (would add a cosign-copy backend wrapping cosign copy SRC DST). |
username | no | — | Registry user for the TARGET. Sourced via secrets:. The plugin writes credentials to a 0600 mktemp docker config.json that crane/skopeo read via DOCKER_CONFIG / —authfile — never on argv. The EXIT/INT/TERM trap wipes the auth file on every exit path. |
password | no | — | Registry password / token for the TARGET. Sourced via secrets:. |
source-username | no | — | Registry user for the SOURCE. Defaults to PLUGIN_USERNAME ONLY when source + target share the same host (the common single-registry promotion). On cross-registry copies the target token is NOT shared with the source — a missing source credential leaves the source anonymous (legitimate for public mirrors) rather than silently forwarding the target token to a stranger. |
source-password | no | — | Source password / token. Same defaulting rule as source-username — only shared with target creds on same-host promotion, never on cross-registry copies. |
output | no | .gocdnext/image-copy.env | Workspace-relative output file. Created with mkdir -p. Shell-sourceable (PROMOTED_DIGEST=, SOURCE=, TARGET=, BACKEND=). Path must be relative; absolute and .. segments are rejected so a typo can’t write outside the job workspace. Pair with artifacts.paths: to feed the digest into a downstream cosign-sign job. |
Examples
native outputs (gocdnext v0.11+) — sign by digest, no env file
Declare outputs: on the promote job and the cosign step references ${{ needs.promote.outputs.digest }} directly in with:. The cosign job can stay as a uses: plugin (instead of image+script that sources an env file), because the digest is substituted at dispatch — no runtime file read required. The legacy .gocdnext/image-copy.env workspace file is still emitted in parallel for older agents.
jobs: promote: stage: promote secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-image-copy@v1 outputs: digest: PROMOTED_DIGEST with: source: ghcr.io/org/app@sha256:abc123… target: ghcr.io/org/app:${CI_TAG_NAME} username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}
cosign-sign: stage: sign needs: [promote] secrets: [COSIGN_PRIVATE_KEY, COSIGN_PASSWORD, GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-cosign@v1 with: # Resolved at dispatch — no image+script gymnastics. image: ghcr.io/org/app@${{ needs.promote.outputs.digest }} action: sign key-content: ${{ COSIGN_PRIVATE_KEY }} key-password: ${{ COSIGN_PASSWORD }} registry: ghcr.io username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}same-registry promote staging → release
Build pushes to a staging tag, trivy scans it, then image-copy promotes to the release tag in the SAME registry (common pattern for GHCR-only flows). One set of credentials, no source-username needed.
jobs: build: stage: build docker: true secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-buildx@v1 with: image: ghcr.io/org/app tags: staging-${CI_COMMIT_SHORT_SHA} platforms: linux/amd64,linux/arm64 push: "true" registry: ghcr.io username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}
scan-staging: stage: scan needs: [build] secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-trivy@v1 with: scan-type: image target: ghcr.io/org/app:staging-${CI_COMMIT_SHORT_SHA} severity: HIGH,CRITICAL exit-code: "1" username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}
promote: stage: promote needs: [scan-staging] secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-image-copy@v1 with: source: ghcr.io/org/app:staging-${CI_COMMIT_SHORT_SHA} target: ghcr.io/org/app:${CI_TAG_NAME} extra-tags: | latest username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }} artifacts: paths: [".gocdnext/image-copy.env"]cross-registry (private staging → public prod)
Staging in a private internal registry, prod in GHCR. Different credentials for each side. crane handles cross-registry copy in one shot via cross-repo BLOB MOUNT where the registries cooperate, or a re-upload where they don’t.
promote: stage: promote secrets: [STAGING_USER, STAGING_TOKEN, GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-image-copy@v1 with: source: staging.internal:5000/org/app:rc-${CI_COMMIT_SHORT_SHA} target: ghcr.io/org/app:${CI_TAG_NAME} source-username: ${{ STAGING_USER }} source-password: ${{ STAGING_TOKEN }} username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}skopeo backend (explicit —multi-arch all)
Functionally equivalent to crane for the multi-arch copy case; pick skopeo when your operator runbooks already assume it, or you need one of skopeo’s other output formats (OCI archive, dir, etc.) in a follow-up step. Same cosign caveat as crane — re-sign at the target.
promote: stage: promote secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-image-copy@v1 with: backend: skopeo source: ghcr.io/org/app:staging-${CI_COMMIT_SHORT_SHA} target: ghcr.io/org/app:${CI_TAG_NAME} username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}digest-pinned promotion + cosign sign-by-digest
Source as @sha256:... (the scanned digest), promote, then sign the PROMOTED digest by reading the output file. Closes the “what got signed?” race entirely — everything anchors on one digest from scan through signature. Note that gocdnext jobs are uses: <plugin> XOR image: <base> + script: — never both. The cosign sign-by-digest step here is shaped as an image+script job (not a gocdnext/cosign plugin job) because the digest is only known at runtime via the sourced env file, and the cosign plugin’s image: input is a parse-time substitution. For pipelines where the operator passes the digest at trigger time, the gocdnext/cosign plugin form would work and is cleaner; this shape is for the auto-promoted case. The cosign step uses alpine:3.20 + apk add cosign (cosign 2.2.x is in alpine main since 3.19). Avoids the official gcr.io/projectsigstore/cosign distroless image (no shell, won’t work with script:) and the cost of downloading cosign from GitHub on every run.
jobs: promote: stage: promote secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-image-copy@v1 with: source: ghcr.io/org/app@sha256:abc123… target: ghcr.io/org/app:${CI_TAG_NAME} username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }} artifacts: paths: [".gocdnext/image-copy.env"]
cosign-sign: stage: sign needs: [promote] needs_artifacts: - from_job: promote paths: [".gocdnext/image-copy.env"] secrets: [COSIGN_PRIVATE_KEY, COSIGN_PASSWORD, GHCR_USERNAME, GHCR_TOKEN] image: alpine:3.20 script: - apk add --no-cache cosign - | # `.` (POSIX-portable) sources the env file written # by image-copy. PROMOTED_DIGEST + TARGET come from # there. . .gocdnext/image-copy.env echo "Signing ${TARGET} at ${PROMOTED_DIGEST}" # Authenticate so cosign can upload the signature # manifest to the registry. --password-stdin keeps # the token off argv. echo "${GHCR_TOKEN}" | cosign login ghcr.io \ --username "${GHCR_USERNAME}" --password-stdin # cosign reads COSIGN_PASSWORD + the key via env — # nothing on argv, no PEM material in the script # source. Anchor on @sha256:<digest> rather than # the mutable tag so the signature attests the # exact promoted bytes. cosign sign --yes \ --key env://COSIGN_PRIVATE_KEY \ "ghcr.io/org/app@${PROMOTED_DIGEST}"buildx-imagetools backend (job already has docker:true)
When the build job already needs docker: true, reuse that daemon rather than installing crane/skopeo in a separate step. Multi-tag in one invocation.
promote: stage: promote docker: true secrets: [GHCR_USERNAME, GHCR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-image-copy@v1 with: backend: buildx-imagetools source: ghcr.io/org/app:staging-${CI_COMMIT_SHORT_SHA} target: ghcr.io/org/app:${CI_TAG_NAME} extra-tags: | latest v1 username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}jdk-base
base-image — Shared base image used by every gocdnext JVM plugin (gradle, maven, and future sbt/ant/kotlin-cli). Ships Temurin 11, 17, 21, and 25 in parallel and a select-jdk.sh helper that maps jdk: "<version>" on the consuming plugin’s step to JAVA_HOME + PATH. Not invoked directly by operators — list here so the plugin catalog stays a single source of truth for every published image.
Image: ghcr.io/klinux/gocdnext-plugin-jdk-base:v1
Inputs
No inputs. Examples
jdk-base is not a runnable plugin
Operators don’t uses: this image directly. Reference the gradle or maven plugin and pass jdk: "11" (or 17, 21, 25) to switch JDKs without changing the plugin tag.
uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1with: command: test jdk: "11" # selected by jdk-base's select-jdk.shkaniko
container — Rootless container build. Complement to gocdnext/docker when the agent runs in Kubernetes without DinD — kaniko writes straight to a remote registry from a userspace container, no daemon/socket/privileged mode required. Slower than a warm docker daemon but far cheaper on K8s.
Image: ghcr.io/klinux/gocdnext-plugin-kaniko:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
image | yes | — | Fully-qualified target tag (e.g. “ghcr.io/org/app:v1”). Kaniko writes straight to the registry, so a bare name without a registry host would fall back to docker.io implicitly — prefer explicit paths in CI. |
dockerfile | no | Dockerfile | Path to the Dockerfile, relative to the build context. |
context | no | . | Build context directory, relative to the workspace. |
build-args | no | — | KEY=VALUE list (comma- or newline-separated) forwarded as --build-arg to kaniko. Malformed entries (missing =) are skipped with a warning. |
cache | no | false | Set to “true” to enable layer caching — kaniko writes cached layers to <image>-cache in the same registry. Trade-off: faster rebuilds on the same base, extra bandwidth + storage. |
registry | no | — | Registry hostname for auth. Defaults to the prefix of image up to the first ”/”. Irrelevant when username is empty (no login, anonymous/no-push builds). |
username | no | — | Registry username. Empty = anonymous. |
password | no | — | Registry token/password. Pipe from the job’s secrets: list so the plaintext never lands in the log. |
push | no | true | Set to “false” to skip the registry push — useful on PR preview builds that want to validate the Dockerfile without publishing. |
Examples
build + push to GHCR
Rootless build in the K8s engine — no DinD sidecar required. Credentials piped from secrets.
secrets: - GHCR_USERNAME - GHCR_TOKENuses: ghcr.io/klinux/gocdnext-plugin-kaniko@v1with: image: ghcr.io/org/app:${CI_COMMIT_SHORT_SHA} username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}cached multi-stage build
Enable layer caching against a cache repo alongside the image — speeds up rebuilds when the base image or early RUN layers don’t change.
secrets: - GHCR_USERNAME - GHCR_TOKENuses: ghcr.io/klinux/gocdnext-plugin-kaniko@v1with: image: ghcr.io/org/app:${CI_COMMIT_SHORT_SHA} cache: "true" username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}kubectl
deploy — Run any kubectl subcommand against a cluster — apply, rollout status, scale, get, delete, etc. Kubeconfig comes from the optional kubeconfig input (path under the workspace OR inline YAML OR base64-encoded YAML); leave it empty when the agent runs in-cluster and should use its service account.
Image: ghcr.io/klinux/gocdnext-plugin-kubectl:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | kubectl subcommand + args, word-split. Examples: “apply -f k8s/”, “rollout status deploy/api -n prod”, “get pods -l app=api -o wide”. |
kubeconfig | no | — | Kubeconfig source. Accepts a path relative to the workspace, a raw YAML blob, or a base64-encoded YAML blob — auto-detected. Empty = use the agent’s in-cluster service account. |
namespace | no | — | Default namespace for the command (equivalent to -n). Commands that already specify -n override this at their own position in the arg list. |
Examples
apply manifests with service account
Agent runs in-cluster; no kubeconfig needed — SA RBAC decides what the apply touches.
uses: ghcr.io/klinux/gocdnext-plugin-kubectl@v1with: command: apply -f k8s/ namespace: prodrollout status from external secret
Prod kubeconfig stored as base64 in a secret. The plugin auto-detects the base64 shape and decodes it.
secrets: - PROD_KUBECONFIG_B64uses: ghcr.io/klinux/gocdnext-plugin-kubectl@v1with: kubeconfig: ${{ PROD_KUBECONFIG_B64 }} command: rollout status deploy/api -n prodkustomize
deploy — Render a kustomization tree and (optionally) apply or diff it against a cluster. Bundles the official kustomize binary + kubectl so operators don’t pipe between containers — pick an action and the plugin does the rest. Kubeconfig discovery mirrors the kubectl + helm plugins (path / inline / base64), so all three deploy plugins share one convention.
Image: ghcr.io/klinux/gocdnext-plugin-kustomize:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
path | yes | — | Workspace-relative path to the kustomization root (folder with kustomization.yaml). Example: deploy/overlays/prod. |
action | no | apply | apply | build | diff. apply renders + kubectl apply; build renders to stdout only; diff renders + kubectl diff (exit 1 on differences is not a failure — only ≥2 fails the job). |
kubeconfig | no | — | Kubeconfig source: path under /workspace, raw YAML, or base64-encoded YAML. Auto-detected. Empty defers to the agent’s in-cluster service account. |
namespace | no | — | Default namespace passed as --namespace. Manifests with an explicit metadata.namespace win. |
prune | no | false | ”true” enables kubectl apply --prune so resources removed from the kustomization are deleted from the cluster. Requires prune_label. |
prune_label | no | — | Label selector used with prune (e.g. app.kubernetes.io/managed-by=gocdnext). Required when prune=true. |
extra_args | no | — | Forwarded verbatim to kubectl apply (e.g. --server-side --force-conflicts). |
Examples
deploy prod overlay
Typical CD shape — render the prod overlay and apply. Kubeconfig comes from a project secret; namespace pinned so manifests without ns inherit it.
uses: ghcr.io/klinux/gocdnext-plugin-kustomize@v1with: path: deploy/overlays/prod kubeconfig: ${{ KUBECONFIG_PROD }} namespace: apisecrets: [KUBECONFIG_PROD]dry-run diff before apply
Two-step canary deploy: first render + diff so reviewers see what’s about to change, then a separate apply step gated by an approval. Diff exits 0 even when there are differences (kubectl exit 1 is not a job failure).
jobs: diff-prod: stage: review uses: ghcr.io/klinux/gocdnext-plugin-kustomize@v1 with: path: deploy/overlays/prod action: diff kubeconfig: ${{ KUBECONFIG_PROD }} secrets: [KUBECONFIG_PROD]
approve-prod: stage: deploy approval: approvers: [release-managers] description: "Promote ${CI_COMMIT_SHORT_SHA} to prod?"
apply-prod: stage: deploy needs: [approve-prod] uses: ghcr.io/klinux/gocdnext-plugin-kustomize@v1 with: path: deploy/overlays/prod kubeconfig: ${{ KUBECONFIG_PROD }} secrets: [KUBECONFIG_PROD]prune dropped resources
Apply with prune so a manifest deleted from the kustomization gets cleaned up from the cluster. The label selector is what apply --prune uses to know which existing resources to consider for deletion — set it on every kustomization via a commonLabels: block so additions/removals stay in sync.
uses: ghcr.io/klinux/gocdnext-plugin-kustomize@v1with: path: deploy/overlays/staging kubeconfig: ${{ KUBECONFIG_STAGING }} prune: "true" prune_label: "app.kubernetes.io/managed-by=gocdnext"secrets: [KUBECONFIG_STAGING]render to artifacts (no apply)
Use the build action when the rendered YAML is the output itself — for review pipelines that publish the manifests as an artifact rather than applying them.
uses: ghcr.io/klinux/gocdnext-plugin-kustomize@v1with: path: deploy/overlays/prod action: buildartifacts: paths: [rendered.yaml]lighthouse-ci
quality — Run Google’s Lighthouse CI suite against one or more URLs — collect metrics, assert against a performance budget, and upload the report. Ships with Chromium bundled so preview URLs don’t need a separate browser install. The assert step fails the job when the budget is breached; toggle it off for advisory-only runs.
Image: ghcr.io/klinux/gocdnext-plugin-lighthouse-ci:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
urls | no | — | Newline- or comma-separated URLs to audit. Ignored when config: is set; skipped when a lighthouserc.* file sits at the working-dir root (LHCI auto-discovery). |
config | no | — | Explicit lighthouserc config path (workspace-relative). Overrides urls:; use this when you need to customize categories, budgets, or assertions beyond the default. |
number-of-runs | no | 3 | Samples per URL. Higher = more stable median, linearly longer job. |
upload-target | no | temporary-public-storage | ”temporary-public-storage” | “filesystem” | “lhci”. Temporary public storage is Google’s 7-day bucket — zero config, one link per run. |
upload-server-base-url | no | — | Required when upload-target=lhci. Base URL of your self-hosted LHCI server. |
upload-token | no | — | Build token for a self-hosted LHCI server. Pipe via secrets:. |
assertions | no | on | ”on” runs lhci assert against the config’s assertion rules and fails the job on breach. “off” skips the assert step entirely — useful for advisory-only runs. |
Examples
audit a preview URL
Hit a preview URL three times, upload to Google’s public storage, and fail the build if any assertion breaches.
uses: ghcr.io/klinux/gocdnext-plugin-lighthouse-ci@v1with: urls: https://preview-${CI_RUN_COUNTER}.myapp.devself-hosted LHCI server
Upload to an internal LHCI server with a build token. Gets trend charts + PR comments without leaking reports to a public bucket.
secrets: - LHCI_BUILD_TOKENuses: ghcr.io/klinux/gocdnext-plugin-lighthouse-ci@v1with: config: .lighthouserc.json upload-target: lhci upload-server-base-url: https://lhci.corp upload-token: ${{ LHCI_BUILD_TOKEN }}liquibase
deploy — Run Liquibase changesets as a pipeline step. Connection material comes EXCLUSIVELY via the job’s secrets: list (LIQUIBASE_COMMAND_URL / _USERNAME / _PASSWORD — Liquibase reads its own env natively; values never touch argv or the persisted pipeline definition). update-sql is the real dry-run: prints the SQL update would execute without touching the database — run it on PR pipelines so reviewers see the exact DDL in the job log. Rollback commands are deliberately not exposed (migrations are forward-only in pipelines; ship a corrective changeset).
Image: ghcr.io/klinux/gocdnext-plugin-liquibase:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | status | validate | update-sql | update. Convention: validate + update-sql on PR pipelines (no effect, full DDL preview in the log); update only on the protected-branch pipeline, ideally behind an approval gate. |
changelog-file | no | db/changelog/db.changelog-master.yaml | Path to the changelog root, relative to working-dir. The plugin fails loud when the file doesn’t exist. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
DDL preview on PR (update-sql)
The reviewer sees the exact SQL in the job log — no effect on the database. Pair with validate in a prior task.
secrets: [LIQUIBASE_COMMAND_URL, LIQUIBASE_COMMAND_USERNAME, LIQUIBASE_COMMAND_PASSWORD]uses: ghcr.io/klinux/gocdnext-plugin-liquibase@v1with: command: update-sqlupdate gated by approval, before the canary
Schema first, deploy second — expand/contract means schema N+1 must serve app N and N+1 simultaneously during the canary (see the migrations concept page). The mutating command sits behind an approval gate, in a pipeline whose material listens to push only.
jobs: approve-migration: stage: migrate approval: approvers: [dba, platform-lead]
migrate: stage: migrate needs: [approve-migration] secrets: [LIQUIBASE_COMMAND_URL, LIQUIBASE_COMMAND_USERNAME, LIQUIBASE_COMMAND_PASSWORD] uses: ghcr.io/klinux/gocdnext-plugin-liquibase@v1 with: command: update changelog-file: db/changelog-master.yamlPostgres lock hygiene via JDBC options
Liquibase has no initSql equivalent — inject lock_timeout / statement_timeout through the JDBC url’s options parameter (set inside the secret value): a DDL that can’t take its lock fails fast instead of queueing production traffic. Mutating, therefore gated like every apply.
# LIQUIBASE_COMMAND_URL secret value:# jdbc:postgresql://db:5432/app?options=-c%20lock_timeout%3D5s%20-c%20statement_timeout%3D15minjobs: approve-migration: stage: migrate approval: approvers: [dba, platform-lead]
migrate: stage: migrate needs: [approve-migration] secrets: [LIQUIBASE_COMMAND_URL, LIQUIBASE_COMMAND_USERNAME, LIQUIBASE_COMMAND_PASSWORD] uses: ghcr.io/klinux/gocdnext-plugin-liquibase@v1 with: command: updatematrix
notifications — Post a message to a Matrix room via the client-server API. Works against any Matrix-compatible homeserver (Synapse, Conduit, Dendrite, matrix.org) without a bridge. Uses a bot account’s access token for auth — pipe via secrets: so the token never touches the log.
Image: ghcr.io/klinux/gocdnext-plugin-matrix:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
homeserver | yes | — | Homeserver base URL — e.g. “https://matrix.org” or “https://chat.mycorp.com”. No trailing slash required. |
token | yes | — | Bearer access token of the bot user. Obtain once from the homeserver (login flow or admin panel). Populate via secrets:. |
room-id | yes | — | Room id (“!abc:server”) OR alias (“#eng:server”). Aliases get resolved to ids before send. |
body | no | — | Plain-text body. Defaults to “pipeline #N → status (sha)” using CI_* vars. |
html | no | — | Rich HTML body. When set, the message ships as HTML with the plain body as fallback so clients that don’t render HTML still see something readable. |
msgtype | no | m.text | ”m.text” (default) renders like a regular chat message; “m.notice” renders muted — useful for bot chatter that shouldn’t compete visually with real conversation. |
Examples
on-failure notification to the
Fires a notice to the engineering room when a prod deploy fails. Room alias resolved once per run.
when: failuresecrets: - MATRIX_TOKENuses: ghcr.io/klinux/gocdnext-plugin-matrix@v1with: homeserver: https://chat.mycorp.com token: ${{ MATRIX_TOKEN }} room-id: "#eng:chat.mycorp.com" msgtype: m.notice body: | [PROD] ${CI_PIPELINE_NAME} #${CI_RUN_COUNTER} failed commit ${CI_COMMIT_SHA} on ${CI_COMMIT_BRANCH}HTML release announce
Rich HTML into a release-announce room. The plain-text body is mandatory as a fallback.
when: successsecrets: - MATRIX_TOKENuses: ghcr.io/klinux/gocdnext-plugin-matrix@v1with: homeserver: https://matrix.org token: ${{ MATRIX_TOKEN }} room-id: "#releases:matrix.org" body: "Released ${RELEASE_TAG}" html: | <h4>${RELEASE_TAG} shipped</h4> <p>Commit <code>${CI_COMMIT_SHA}</code></p> <p><a href="${CI_RUN_URL}">Run on gocdnext</a></p>maven
build — Run Maven commands (verify, package, deploy, etc.) with optional Nexus/Artifactory auth via synthesised settings.xml. Ships Maven 3.9.9 + Temurin 11/17/21/25 (LTS); select the JDK at runtime via jdk: (default 21). Pipe credentials from the job’s secrets: list so the plaintext stays out of logs.
Image: ghcr.io/klinux/gocdnext-plugin-maven:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | mvn subcommand + args, word-split. Examples: “verify”, “clean package -DskipTests”, “deploy -Pprod”. |
working-dir | no | . | Directory under the workspace to cd into — useful on multi-module builds where the root pom.xml sits deeper than the repo root. |
settings | no | — | Path to a pre-made settings.xml relative to the workspace. When set, the nexus-* inputs are ignored — operators bringing their own file are trusted to encode auth themselves. |
nexus-url | no | — | Base URL of the repository manager. Informational only; currently not injected into settings.xml (Maven reads repositoryManagement from the pom) — set here so the plugin can grow a mirror entry later without a breaking change. |
nexus-username | no | — | Username for the synthesised settings.xml <server> entries. Paired with nexus-password; both empty = no settings.xml is synthesised. |
nexus-password | no | — | Password/token for the synthesised settings.xml. Populate via the job’s secrets: list. |
snapshot-repo-id | no | snapshots | Server id embedded in the synthesised settings.xml for the snapshot repository. Must match the id in the pom’s distributionManagement.snapshotRepository. |
release-repo-id | no | releases | Server id for the release repository. Must match the id in the pom’s distributionManagement.repository. |
maven-opts | no | — | Extra JVM args exported as MAVEN_OPTS. Most-requested knob is -Xmx for big multi-module reactors — default JDK heap (~256 MB) OOMs hundred-module builds fast. Example: -Xmx4g -XX:+UseG1GC. |
parallel | no | — | Pass -T <value> for parallel module builds. “1C” = one thread per CPU core (Maven autodetection); explicit numbers (e.g. “4”) cap the count. Empty = serial (Maven default). Use only when the reactor tolerates parallel builds — legacy projects sharing state across modules need to stay serial. |
build-cache | no | — | Toggle the Apache Maven Build Cache Extension (when the project’s .mvn/extensions.xml registers it). “true” passes -Dmaven.build.cache.enabled=true; “false” disables it for this run (recommended on deploy runs to avoid publishing a cache-hit artefact). Empty leaves the extension’s own default in place. |
jdk | no | 21 | JDK version to run mvn (and the surefire/failsafe forks) against. Accepts “11”, “17”, “21”, or “25”; default “21”. Backed by the shared jdk-base image so a single plugin tag covers every supported toolchain — legacy apps stuck on JDK 11 don’t need a separate plugin variant. Typos fail loud at the entrypoint (jdk: "211" aborts with exit 2 instead of falling through to the default). |
Examples
test
Standard CI step — compile + run tests. The plugin always passes --batch-mode --no-transfer-progress so the log stays clean (no “Downloading…” spam).
uses: ghcr.io/klinux/gocdnext-plugin-maven@v1with: command: verifyparallel reactor with big heap
Multi-module reactor with -T 1C (one thread per core) and 4 GB heap. Drops a typical 10-module Spring Boot build by ~40% on a 4-vCPU agent vs serial. Only safe when the reactor’s modules don’t share mutable state.
uses: ghcr.io/klinux/gocdnext-plugin-maven@v1with: command: verify parallel: "1C" maven-opts: "-Xmx4g -XX:+UseG1GC"Maven Build Cache Extension
Apache Maven Build Cache Extension memoises module build outputs against input hashes — Gradle-style task cache. Register the extension in the project’s .mvn/extensions.xml, then gate it from CI: true for every-PR runs (huge win on monorepos), false for the release/deploy run so the artefact gets a fresh build.
uses: ghcr.io/klinux/gocdnext-plugin-maven@v1with: command: verify build-cache: "true" parallel: "1C"deploy to Nexus
Publish to a private Nexus repository; credentials piped from secrets land in an auto-synthesised settings.xml at /tmp. Plaintext never touches the log.
secrets: - NEXUS_USERNAME - NEXUS_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-maven@v1with: command: deploy -Pprod -DskipTests nexus-username: ${{ NEXUS_USERNAME }} nexus-password: ${{ NEXUS_PASSWORD }} snapshot-repo-id: acme-snapshots release-repo-id: acme-releases build-cache: "false" # release run: fresh buildcached build
Tar + restore the local ~/.m2 repo across runs. The plugin redirects -Dmaven.repo.local to .m2-repo/ inside the workspace so the platform’s cache: block captures it. Hash-keyed on every pom.xml in the reactor (**/pom.xml) so a dependency bump in ANY module invalidates the cache, not just the root. Different branches sharing the same poms share the cache (multi-version safe).
cache: - key: m2-{{ hash "**/pom.xml" }} paths: [.m2-repo]uses: ghcr.io/klinux/gocdnext-plugin-maven@v1with: command: verifytestcontainers integration tests
docker: true mounts the agent’s docker.sock + wires host.docker.internal. The plugin auto-exports TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE and TESTCONTAINERS_HOST_OVERRIDE ONLY when the socket is really present — in the Kubernetes DinD path (DOCKER_HOST=tcp://localhost:2375) Testcontainers auto-detects via DOCKER_HOST without explicit overrides.
uses: ghcr.io/klinux/gocdnext-plugin-maven@v1docker: truecache: - key: m2-{{ hash "**/pom.xml" }} paths: [.m2-repo]with: command: verify -Pintegration parallel: "1C"maven-central-publish
registry — Upload a deployment bundle to Maven Central via the Sonatype Central Portal API, then poll validation to a terminal state. Scope is deliberate: this plugin UPLOADS a pre-built bundle — producing it (jars + sources + javadoc + poms + .asc signatures + checksums in the Central layout) is the build job’s responsibility, and GPG signing stays in the build job where the key lives; this container never sees private key material. The portal token rides the job’s secrets: through a curl config file, never argv. Validation failures surface the portal’s per-file errors in the log and fail the job.
Image: ghcr.io/klinux/gocdnext-plugin-maven-central-publish:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
bundle | yes | — | Path to the deployment zip (Central layout). Maven’s central-publishing-maven-plugin and Gradle’s equivalent produce exactly this file. |
publishing-type | no | AUTOMATIC | AUTOMATIC publishes on successful validation; USER_MANAGED stops at VALIDATED for a human to release in the portal UI. |
wait | no | true | Poll the deployment to a terminal state (30-min cap, fails loud). false = fire-and-forget upload. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
build the bundle, then publish gated
Maven produces the bundle (signing happens there, where the GPG key lives); the upload is approval-gated like every mutating release step.
jobs: bundle: stage: build uses: ghcr.io/klinux/gocdnext-plugin-maven@v1 with: command: -Prelease deploy # central-publishing plugin zips the bundle artifacts: [target/central-publishing/central-bundle.zip]
approve-publish: stage: release approval: approvers: [release-managers]
publish: stage: release needs: [approve-publish] needs_artifacts: - from_job: bundle paths: [target/central-publishing/central-bundle.zip] secrets: [CENTRAL_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-maven-central-publish@v1 with: bundle: target/central-publishing/central-bundle.zipnetlify
deploy — Deploy a built directory to Netlify (netlify-cli, —no-build — building belongs to the build job). Token + site id ride env via the job’s secrets: (the CLI’s native contract), never argv. prod: false (default) creates a preview deploy; alias: pins a stable preview URL per PR.
Image: ghcr.io/klinux/gocdnext-plugin-netlify:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir | yes | — | The built site directory. |
site-id | no | — | Netlify site id (or provide NETLIFY_SITE_ID via secrets — treat it as config, not secret, your call). |
prod | no | false | true = production deploy; false = preview. |
alias | no | — | Preview alias — e.g. pr-${CI_PULL_REQUEST_KEY} gives every PR a stable URL. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
PR preview with stable alias
secrets: [NETLIFY_AUTH_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-netlify@v1with: dir: dist site-id: 0a1b2c3d-… alias: pr-${CI_PULL_REQUEST_KEY}production on push to main
secrets: [NETLIFY_AUTH_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-netlify@v1with: dir: dist site-id: 0a1b2c3d-… prod: "true"nexus
registry — Upload to + download from a Sonatype Nexus Maven2 repository. One plugin, action: upload \| download, so a family of jobs can both publish its own artefacts AND pull a shared-library version into the workspace. version: LATEST resolves via Nexus’s REST search endpoint — newest-first, no client-side semver sort. Non-Maven repositories aren’t covered; drop to script: + curl for those.
Image: ghcr.io/klinux/gocdnext-plugin-nexus:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
action | yes | — | upload | download |
url | yes | — | Nexus base URL, e.g. “https://nexus.corp”. No trailing slash required. |
repository | yes | — | Repository name. Hosted for upload; hosted or group for download. |
group-id | yes | — | Maven groupId (e.g. “com.acme.shared”). |
artifact-id | yes | — | Maven artifactId (e.g. “common-lib”). |
extension | no | jar | Packaging extension. |
classifier | no | — | Maven classifier (sources, javadoc, linux-x86_64, …). |
version | no | — | action=download: exact version OR the literal “LATEST” to pull the newest matching asset via REST search. |
dest | no | . | action=download: destination dir (file name derived from URL) OR full file path inside the workspace. |
file | no | — | action=upload: workspace-relative path of the artefact to publish. |
upload-version | no | — | action=upload: version label to publish under. Separate from version so download-LATEST and upload-1.2.3 don’t share a knob. |
username | no | — | Basic-auth user. Populate via secrets:. |
password | no | — | Basic-auth password / deploy token. Populate via secrets:. |
Examples
pull the latest shared lib before build
Fetches the newest com.acme.shared:common-lib jar from the internal releases repo into lib/, then runs the build with it on the classpath.
secrets: - NEXUS_USERNAME - NEXUS_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-nexus@v1with: action: download url: https://nexus.corp repository: releases group-id: com.acme.shared artifact-id: common-lib version: LATEST dest: lib/ username: ${{ NEXUS_USERNAME }} password: ${{ NEXUS_PASSWORD }}publish after build
Upload the built jar to releases under the commit SHA as version — pairs with a separate “promote to latest” step once QA signs off.
secrets: - NEXUS_USERNAME - NEXUS_PASSWORDuses: ghcr.io/klinux/gocdnext-plugin-nexus@v1with: action: upload url: https://nexus.corp repository: releases group-id: com.acme.backend artifact-id: my-service upload-version: ${CI_COMMIT_SHORT_SHA} file: target/my-service.jar username: ${{ NEXUS_USERNAME }} password: ${{ NEXUS_PASSWORD }}node
build — Node.js install + run with multi-manager support (pnpm, npm, yarn v3+). Auto-detects the manager from the lockfile, sets up corepack + workspace-local store, runs frozen-lockfile install (skippable), then executes the user’s shell command via bash -lc. v2 BREAKING vs v1: command: is a shell command (not a pnpm subcommand) and is no longer auto-prefixed; new install: flag (default true) separates install from command:; yarn v1 is rejected with a migration message (use v3+ or set manager: none).
Image: ghcr.io/klinux/gocdnext-plugin-node:v2
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | no | — | Shell command to run AFTER install. Executed via bash -lc so &&, pipes, redirects, and env expansion work. NOT prefixed with a package manager — you write pnpm test, npm run build, etc. Empty string + install:true = install-only job (artifact uploader / cache warmer pattern). Empty + install:false is rejected at runtime — that’s a silent no-op job. |
working-dir | no | . | Directory under the workspace to cd into before running the install + command. Monorepos with a sub-app set this. |
manager | no | auto | Package manager to use: pnpm | npm | yarn | none | auto. auto detects from lockfile (priority: pnpm-lock.yaml > yarn.lock > package-lock.json). none skips install + setup entirely — useful for jobs that run plain node script.js or pre-built tooling without touching the package manager. Yarn v1 is rejected with a migration message; use none + script if you’re stuck on it. |
install | no | true | Run the manager’s frozen install step before command:. Set false for downstream jobs that consume a node_modules/ artifact via needs_artifacts — install would otherwise re-resolve the lockfile and undo the artifact restore. Mirrors the python plugin’s no-install knob (inverted polarity for consistency with the rest of the plugin’s bool flags). |
frozen | no | true | Use the manager’s frozen-lockfile mode. Per-manager translation: - pnpm → --frozen-lockfile - npm → npm ci (vs npm install) - yarn v3+ → --immutable Set false to allow the manager to update the lockfile during install. Almost always you want true in CI — a drifted lockfile should fail the build, not silently get rewritten. |
prod | no | false | Skip development dependencies for production builds. Per-manager: - pnpm → --prod - npm → --omit=dev - yarn v3+ → yarn workspaces focus --all --production (NOT yarn install --production — Yarn deprecated that flag in v2 and silently maps it to focus anyway. The plugin calls focus directly so the operator sees the actual command in the log.) Combine with install: true for a slim install before producing a production artifact. Tests/lints want prod: false (default) because they typically depend on dev tooling (vitest, eslint). Yarn caveat: workspaces focus belongs to the @yarnpkg/plugin-workspace-tools plugin, which Yarn 4 bundles by default but Yarn 3 requires opting into via yarn plugin import workspace-tools (writes an entry to .yarnrc.yml + .yarn/plugins/). Plugin preflights the command’s availability and surfaces a remediation message with the import command + upgrade-to-Yarn-4 alternative when missing — the operator gets an actionable error instead of yarn’s opaque “Couldn’t find a script named focus”. |
Examples
install-only job + downstream lints via artifact
The canonical pattern for monorepos: ONE install job tars node_modules + uploads it, downstream lint/test jobs restore via needs_artifacts and skip their own install. Faster than every job running its own pnpm install, but the artifact transfer cost can dominate on slow storage backends — see the next example for the cache-only variant.
install: stage: install uses: ghcr.io/klinux/gocdnext-plugin-node@v2 cache: - key: pnpm-store-{{ hash "pnpm-lock.yaml" }} paths: [.pnpm-store] with: # install: true (default), command: "" (default) → # install-only run. working-dir: . artifacts: paths: [node_modules]
lint: stage: lint uses: ghcr.io/klinux/gocdnext-plugin-node@v2 needs: [install] needs_artifacts: - from_job: install paths: [node_modules] with: install: false # reuse artifact, skip re-resolve command: pnpm --filter @app/web lintcache-only per-job install (no artifact transfer)
Each job runs its own pnpm install against a shared content-addressable store. pnpm install --frozen-lockfile with a warm store materialises node_modules in seconds via symlinks — cheaper than tarring + transferring the entire tree across an NFS-backed PV. Best fit when the artifact backend (storage class, network, …) is the bottleneck. The hash-keyed cache invalidates on lockfile changes automatically.
lint: stage: lint uses: ghcr.io/klinux/gocdnext-plugin-node@v2 cache: - key: pnpm-store-{{ hash "pnpm-lock.yaml" }} paths: [.pnpm-store] with: # install: true + frozen: true (defaults) → pnpm install # --frozen-lockfile happens automatically before command. command: pnpm --filter @app/web lintshell-chained command (codegen + build)
command: is shell-evaluated via bash -lc, so &&, pipes, redirects, and env expansion all work. Replaces v1’s “two separate jobs to chain commands” pattern.
build: uses: ghcr.io/klinux/gocdnext-plugin-node@v2 cache: - key: pnpm-store-{{ hash "pnpm-lock.yaml" }} paths: [.pnpm-store] with: command: | pnpm --filter @app/types generate && pnpm --filter @app/web build artifacts: paths: [apps/web/dist]production install for container build
prod: true skips dev dependencies — the resulting node_modules is smaller and ready to copy into a production image layer. Lint/test jobs in the same pipeline keep the default prod: false because they need vitest / eslint / tsc which are devDependencies.
docker-build: uses: ghcr.io/klinux/gocdnext-plugin-node@v2 with: prod: true command: pnpm --filter @app/web buildnpm with package-lock.json
Auto-detection picks npm when only package-lock.json is present. frozen: true (default) maps to npm ci, which is the frozen-by-design variant of npm install — there’s no separate flag on npm.
lint: uses: ghcr.io/klinux/gocdnext-plugin-node@v2 cache: - key: npm-cache-{{ hash "package-lock.json" }} paths: [.npm-cache] with: command: npm run lintnpm-publish
registry — Publish a package to npm (or any compatible registry). The token comes via the job’s secrets: and is written to a USER-scoped .npmrc — never argv, never the workspace (a workspace .npmrc could leak into artifacts). if-exists: skip makes retried pipelines idempotent: name@version is immutable on npm, and a rerun that already published should not fail the run. dry-run packs and validates without uploading.
Image: ghcr.io/klinux/gocdnext-plugin-npm-publish:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir | no | . | Package directory (where package.json lives). |
tag | no | — | npm dist-tag (default registry behaviour: latest). |
access | no | — | public | restricted — required as “public” for the FIRST publish of a scoped package. |
registry | no | https://registry.npmjs.org | Registry URL — point at a private registry if needed. |
if-exists | no | fail | fail (default) or skip when name@version is already published — set skip for idempotent retried pipelines. |
dry-run | no | false | npm publish —dry-run: packs + prints what would ship, uploads nothing, needs no token. Run it on PR pipelines. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
publish on tag, idempotent
Tag-triggered release pipeline; a rerun after a flake won’t fail on the already-published version.
secrets: [NPM_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-npm-publish@v1with: access: public if-exists: skipdry-run on PRs
Validates the publish (files list, version, dist-tag) without uploading — no token needed.
uses: ghcr.io/klinux/gocdnext-plugin-npm-publish@v1with: dry-run: "true"osv-scanner
security — Dependency vulnerability scan (SCA) with Google’s osv-scanner — reads lockfiles across ecosystems (go.mod, package-lock.json, pnpm-lock.yaml, poetry.lock, Cargo.lock, …) against the OSV.dev database. Complements trivy: trivy looks at images and filesystems, osv-scanner at the dependency graph your lockfiles declare. Report written as SARIF (default), json or table; fail-on: none turns findings into report-only WITHOUT silencing scan errors — a broken lockfile never fakes a clean report. Ignores / acknowledged vulnerabilities go in an osv-scanner.toml (the baseline mechanism).
Image: ghcr.io/klinux/gocdnext-plugin-osv-scanner:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir | no | . | Scan root, recursive. Lockfiles are auto-detected. |
format | no | sarif | sarif | json | table. The report file extension follows. |
report-file | no | — | Output path, default osv-report. |
fail-on | no | any | any (default): exit 1 when vulnerabilities are found. none: report-only — findings don’t fail the job, but scan ERRORS (bad lockfile, network) still do. |
config | no | — | Path to an osv-scanner.toml with ignore rules — the acknowledged-vulnerability baseline (each entry carries a reason and optional expiry). |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
gate the build on known vulns
Scans every lockfile in the repo; any known vulnerability fails the job. The SARIF ships as an artifact either way.
uses: ghcr.io/klinux/gocdnext-plugin-osv-scanner@v1artifacts: optional: [osv-report.sarif]report-only on PRs
Findings don’t block the PR; the report lands as an artifact (pair with pr-comment for in-review visibility).
uses: ghcr.io/klinux/gocdnext-plugin-osv-scanner@v1with: fail-on: noneartifacts: optional: [osv-report.sarif]acknowledged vulns via osv-scanner.toml
The baseline mechanism: ignores live in a reviewed file with reasons, not in pipeline flags.
# osv-scanner.toml:# [[IgnoredVulns]]# id = "GO-2024-1234"# reason = "not reachable — vendored CLI only used in tests"uses: ghcr.io/klinux/gocdnext-plugin-osv-scanner@v1with: config: osv-scanner.tomlphp
build — PHP build + test runner — official php:8.3-cli image with composer 2. Runs composer install automatically when a composer.json is present (skip with no-install: true to reuse a vendor/ shipped via artifacts), then executes the provided command. Composer’s cache is redirected into the workspace (.composer-cache/) so the native cache: block captures it.
Image: ghcr.io/klinux/gocdnext-plugin-php:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | Shell command to run after the install step. Examples: “vendor/bin/phpunit”, “vendor/bin/phpstan analyse —error-format=raw”. |
working-dir | no | . | Directory under the workspace to cd into first. |
no-install | no | false | Skip composer install and run the command against the vendor/ already present in the workspace — pair with an install job exposing vendor/ as an artifact for the install-once-reuse-N pattern. |
Examples
phpunit with composer cache
Lock-file-keyed cache so dependency bumps invalidate automatically.
cache: - key: composer-{{ hash "composer.lock" }} paths: [.composer-cache]uses: ghcr.io/klinux/gocdnext-plugin-php@v1with: command: vendor/bin/phpunitinstall once, reuse across jobs
One install job ships vendor/; lint and test consume it without re-resolving.
jobs: install: stage: build uses: ghcr.io/klinux/gocdnext-plugin-php@v1 with: { command: "true" } artifacts: [vendor/]
phpstan: stage: test needs_artifacts: - from_job: install paths: [vendor/] uses: ghcr.io/klinux/gocdnext-plugin-php@v1 with: command: vendor/bin/phpstan analyse src no-install: "true"playwright
quality — Run Playwright end-to-end tests with Chromium + Firefox + WebKit pre-installed (via Microsoft’s official image — saves the ~1.2 GB browser download per run). Detects pnpm / yarn / npm from the lockfile, installs deps, then invokes playwright test with the operator’s filters (project, grep, shard, config). JUnit report is pinned to test-results/junit.xml so the pipeline’s test_reports: block picks it up without extra config.
Image: ghcr.io/klinux/gocdnext-plugin-playwright:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
working-dir | no | . | Directory under workspace to cd into first. |
command | no | test | Sub-command passed to npx playwright. “test” is the default; “show-report” + “install” also valid. |
config | no | — | —config path override. Playwright auto-loads playwright.config.{ts,js} from working-dir when omitted. |
project | no | — | —project filter — run a single project from the config (chromium, firefox, mobile-chrome, …). |
grep | no | — | —grep regex, scopes tests by title. |
shard | no | — | —shard=N/M for parallel fan-out. “1/4” runs slice 1 of 4 — pair with a pipeline matrix to spread across jobs. |
reporter | no | — | —reporter=
|
extra-args | no | — | Freeform trailing flags forwarded verbatim to playwright test (—retries=2, —workers=4, …). |
install-deps | no | true | Runs the detected manager’s install before the tests. Set “false” when a prior job already warmed node_modules via gocdnext/node + cache:. |
Examples
e2e chromium only
Run the chromium project against a preview URL. The Playwright image ships with browsers pre-installed so no separate playwright install step needed. test_reports + artifacts are the gocdnext conventions that surface the JUnit + HTML outputs for the Tests tab and artifact viewer.
uses: ghcr.io/klinux/gocdnext-plugin-playwright@v1with: working-dir: web project: chromiumtest_reports: - web/test-results/junit.xmlartifacts: optional: - web/playwright-report - web/test-resultssharded run (4-way matrix)
Spread tests across four parallel jobs via a matrix + the SHARD_INDEX env injected by the matrix expansion. The pipeline matrix is the idiomatic way to fan shards out; each job picks its own slice via ${SHARD_INDEX}/4.
parallel: matrix: - SHARD_INDEX: ["1", "2", "3", "4"]uses: ghcr.io/klinux/gocdnext-plugin-playwright@v1with: working-dir: web project: chromium shard: "${SHARD_INDEX}/4"test_reports: - web/test-results/junit.xmlreuse warmed node_modules
Prior job in the same pipeline installed deps via gocdnext/node + cache:. Skip the install step here since node_modules is already on disk.
cache: - key: pnpm-store-${CI_COMMIT_BRANCH} paths: [web/.pnpm-store, web/node_modules]uses: ghcr.io/klinux/gocdnext-plugin-playwright@v1with: working-dir: web install-deps: "false" project: chromiumtest_reports: - web/test-results/junit.xmlpr-comment
notifications — Post — or update in place — a comment on the pull request / merge request that triggered the run. GitHub, GitLab and Bitbucket Cloud, resolved automatically from the run’s PR context (CI_PULL_REQUEST_URL): no repo or provider inputs needed, self-hosted GitHub Enterprise / GitLab included. Upsert mode (default) finds its own previous comment via a hidden marker and edits it, so successive pushes don’t stack duplicates. Non-PR runs (push, manual, tag) are a loud success no-op, so on: [push, pull_request] pipelines don’t fail their push leg. Bodies over 60 KB are truncated with an explicit notice (GitHub’s hard cap is 65536 chars). Auth via the job’s secrets: — tokens never touch argv.
Image: ghcr.io/klinux/gocdnext-plugin-pr-comment:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
body | no | — | Comment content (markdown). Mutually exclusive with body-file; one of the two is required. |
body-file | no | — | Workspace path to read the content from — the natural fit for output produced by an earlier job (terraform plan, liquibase update-sql) shipped over via artifacts. |
marker | no | — | Upsert identity, embedded as an HTML comment. Default: “gocdnext/pr-comment: |
mode | no | upsert | upsert (default): edit the previous comment carrying the marker, create when none exists. create: always post a new comment. |
provider | no | auto | auto (default — detected from the PR URL path shape, which also covers self-hosted hosts) | github | gitlab | bitbucket. |
api-base | no | — | API base URL override. Rarely needed — GHE and self-hosted GitLab are derived from the PR URL host automatically; this exists for API proxies and testing. |
dry-run | no | false | Resolve provider/endpoints/payload and print them without calling the API. |
Examples
terraform plan on the PR (atlantis-style)
The plan job saves the plan as an artifact; pr-comment ships it into the PR. Reviewers see the exact diff without opening the CI. GITHUB_TOKEN needs repo comment scope.
jobs: plan: stage: check image: hashicorp/terraform:1.10 script: - terraform init -input=false - terraform plan -no-color -input=false > plan.txt artifacts: [plan.txt]
comment-plan: stage: check needs: [plan] needs_artifacts: - from_job: plan paths: [plan.txt] secrets: [GITHUB_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-pr-comment@v1 with: body-file: plan.txtmigration DDL preview on the MR (GitLab)
Pairs with the liquibase plugin’s update-sql dry-run: the DDL the merge would apply, posted on the MR and updated in place on every push.
jobs: ddl-preview: stage: check image: liquibase/liquibase:4.31-alpine script: - liquibase --changelog-file=db/changelog-master.yaml update-sql > ddl.sql secrets: [LIQUIBASE_COMMAND_URL, LIQUIBASE_COMMAND_USERNAME, LIQUIBASE_COMMAND_PASSWORD] artifacts: [ddl.sql]
comment-ddl: stage: check needs: [ddl-preview] needs_artifacts: - from_job: ddl-preview paths: [ddl.sql] secrets: [GITLAB_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-pr-comment@v1 with: body-file: ddl.sql marker: ddl-previewstatic body, always a fresh comment
mode: create skips the upsert lookup — every run posts a new comment (use sparingly; upsert is the polite default).
secrets: [GITHUB_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-pr-comment@v1with: body: ":rocket: preview environment ready at https://pr-${CI_PULL_REQUEST_KEY}.preview.example.com" mode: createpypi-publish
registry — Upload built distributions (dist/*.whl, *.tar.gz) to PyPI or a compatible index via twine. Building is the BUILD job’s responsibility (python -m build) — this plugin uploads what dist/ holds, shipped over via artifacts. twine check always runs first (a broken long_description otherwise fails AFTER the version number is burned). check-only: true is the no-token PR preflight; skip-existing makes retried pipelines idempotent (files on PyPI are immutable). Trusted publishing note: PyPI’s OIDC exchange accepts an allowlist of issuers (GitHub, GitLab, Google, ActiveState) — gocdnext id_tokens can’t mint PyPI tokens until custom issuers are accepted, so auth is token-based.
Image: ghcr.io/klinux/gocdnext-plugin-pypi-publish:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dist-dir | no | dist | Directory with the built distributions. |
repository-url | no | — | Upload endpoint override — test.pypi.org or a private index. Default is PyPI. |
skip-existing | no | false | twine —skip-existing: already-uploaded files don’t fail the run (set true on retry-prone release pipelines). |
check-only | no | false | Run twine check and stop — metadata validation without upload, no token needed. The PR preflight. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
build job + publish job
Canonical split: build produces dist/, publish (gated to the tag pipeline) uploads it.
jobs: build: stage: build uses: ghcr.io/klinux/gocdnext-plugin-python@v1 with: { command: "python -m build" } artifacts: [dist/]
publish: stage: release needs_artifacts: - from_job: build paths: [dist/] secrets: [PYPI_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-pypi-publish@v1 with: skip-existing: "true"metadata preflight on PRs
twine check without upload — no token in the PR context.
uses: ghcr.io/klinux/gocdnext-plugin-pypi-publish@v1with: check-only: "true"python
build — Python build + test runner. Installs dependencies with the detected manager (poetry → uv → pip), then executes the provided command. Auto-detection priority: poetry.lock → uv.lock → requirements.txt → pyproject.toml. Use manager: none to skip the install step entirely.
Image: ghcr.io/klinux/gocdnext-plugin-python:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | Shell command to run after dependency install. Examples: “pytest -q”, “ruff check . && pytest”, “mypy src tests”. |
working-dir | no | . | Directory under the workspace to cd into before running the command. Monorepos with a sub-package set this. |
manager | no | auto | Which package manager to use. “auto” picks the best match on disk. Options: auto | uv | pip | poetry | none. |
requirements | no | — | Override path to a requirements file (pip manager only). Default is “requirements.txt” at the working-dir root. |
extras | no | — | Comma- or space-separated STRING of optional-dependency extras (PEP 621 [project.optional-dependencies]) to enable on the install step. Example: extras: dev, test. Pipeline with: is map[string]string so YAML list syntax (extras: [dev, test]) is NOT accepted by the parser — pass a single string. Translation per manager: uv → repeated --extra X, poetry → --extras "X Y", pip → installs the project itself with .[X,Y] after the requirements file. Empty = base install only. |
all-extras | no | false | Install every optional-dependency extra defined by the project. uv/poetry pass --all-extras directly. pip has no equivalent (extras must be enumerated against a package spec), so the flag is honoured as a no-op + warn so a pipeline that runs on multiple managers doesn’t break on the pip leg. |
no-install | no | false | Skip the dependency-install step entirely and run the command against the .venv already present in the workspace. The shebang rewriter and activate-venv path still run, so a .venv/ restored via needs_artifacts: from an upstream install job is usable as-is. Pair with an install job that sets all-extras: true (or extras: dev, test) and exposes services/<x>/.venv/ as an artifact to get the install-once-reuse-N pattern without paying the resolve cost in every downstream job. |
Examples
pytest with poetry
Library project using poetry.lock. Auto-detected; no extra config needed.
uses: ghcr.io/klinux/gocdnext-plugin-python@v1with: command: pytest -qpytest with uv (pyproject only)
App with pyproject.toml but no uv.lock yet. uv sync resolves on the fly.
uses: ghcr.io/klinux/gocdnext-plugin-python@v1with: command: pytest -q --maxfail=1publish to a private index (secret token)
Twine upload piping the index credentials via secrets so the token never touches the log.
secrets: - PYPI_TOKENuses: ghcr.io/klinux/gocdnext-plugin-python@v1with: command: | python -m build && \ twine upload --username __token__ --password "${PYPI_TOKEN}" dist/*cached test run
Tar + restore the pip/uv/poetry download caches across runs. The plugin redirects PIP_CACHE_DIR, UV_CACHE_DIR and POETRY_CACHE_DIR to .cache/paths: [.cache] entry covers all three.
cache: - key: py-${CI_COMMIT_BRANCH} paths: [.cache]uses: ghcr.io/klinux/gocdnext-plugin-python@v1with: command: pytest -qinstall once, reuse across jobs (extras + no-install)
The fan-out pattern for monorepos / fast pipelines: a single install job resolves the venv WITH every dev extra, exposes .venv/ as an artifact, and every downstream job (lint, typecheck, test, …) consumes that venv without re-resolving. no-install: true tells the plugin to skip its own sync and trust the artifact — but still rewrite venv shebangs and activate the venv so the .venv/bin entry-points spawn correctly under the new job’s workspace path.
jobs: - name: install uses: ghcr.io/klinux/gocdnext-plugin-python@v1 with: command: "true" # nothing to run, the install IS the job all-extras: true # sync ruff, mypy, pytest, ... artifacts: [.venv/]
- name: ruff needs_artifacts: - from_job: install paths: [.venv/] uses: ghcr.io/klinux/gocdnext-plugin-python@v1 with: command: ruff check . no-install: true # reuse the artifact venv as-is
- name: pytest needs_artifacts: - from_job: install paths: [.venv/] uses: ghcr.io/klinux/gocdnext-plugin-python@v1 with: command: pytest -q no-install: trueinstall with explicit extras (not all)
Same fan-out pattern but enabling only specific extras — handy when [project.optional-dependencies] declares a heavy gpu group nobody wants in CI.
uses: ghcr.io/klinux/gocdnext-plugin-python@v1with: command: "true" extras: dev, test # uv → --extra dev --extra testartifacts: [.venv/]release-notes
release — Generate a changelog between two git refs and write it to a workspace file. Two formats: plain (one bullet per commit — works with any repo) and conventional (groups commits into Features / Bug Fixes / Chores / Other via the Conventional Commits prefix; unclassifiable commits fall through to Other, nothing gets dropped). Downstream plugins like gocdnext/github-release and gocdnext/email can read the resulting file directly.
Image: ghcr.io/klinux/gocdnext-plugin-release-notes:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
from | no | — | Starting ref (exclusive). Defaults to the nearest prior tag via git describe --tags --abbrev=0; on a never-tagged repo it falls back to the root commit so the first release still produces notes. |
to | no | HEAD | Ending ref (inclusive). |
output | no | RELEASE_NOTES.md | Workspace-relative output file. |
format | no | plain | plain | conventional |
heading | no | — | First-line heading written above the body, e.g. ”## v1.2.3”. Empty = no heading; body starts at the first bullet. |
Examples
plain notes for a release step
Build notes from the prior tag to HEAD, then hand the file to gocdnext/github-release as the body.
uses: ghcr.io/klinux/gocdnext-plugin-release-notes@v1with: output: RELEASE_NOTES.md heading: "## ${RELEASE_TAG}"conventional commits changelog
Grouped by commit type. Pairs well with a tag: step that used the same ${RELEASE_TAG} env var upstream.
uses: ghcr.io/klinux/gocdnext-plugin-release-notes@v1with: format: conventional from: ${PREVIOUS_TAG} to: ${RELEASE_TAG} output: CHANGELOG-${RELEASE_TAG}.md heading: "# ${RELEASE_TAG} — $(date +%Y-%m-%d)"release-pr
release — Open (or update) a curated release PR. A release manager runs this from a manual pipeline; it bumps a VERSION file on a release/on: push, paths: [VERSION] pipeline keys on to cut the git tag, which then drives build + deploy. Curation and approval live in git/GitHub; this plugin only prepares the PR, it never deploys. Version selection is the caller’s job — pass the computed version in (typically ${{ needs.bump.outputs.next }} from a semver-bump step). The release is “everything on the base up to the cut”; to hold a merged change back, revert it on the base branch rather than cherry-picking.
Image: ghcr.io/klinux/gocdnext-plugin-release-pr:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
version | yes | — | The release version — bare (“1.3.0”) or prefixed (“v1.3.0”). A leading tag_prefix is stripped and the result validated as SemVer, so wiring ${{ needs.bump.outputs.next }} from a semver-bump step with prefix: v (which emits “v1.3.0”) works without double-prefixing. The VERSION file is written bare. |
token | yes | — | GitHub token with contents:write + pull-requests:write. Used both for the branch push (via GIT_ASKPASS) and gh (GH_TOKEN). Pipe via the job’s secrets: list. |
repo | no | — | “owner/name”. Optional — gh infers it from the workspace git remote when omitted. |
base | no | main | Base branch the release PR targets. The release branch is cut FROM this (fetched fresh), not from whatever ref the manual run checked out — so the PR carries only the VERSION bump on top of base, never stray commits from a wrong-branch trigger. |
version_file | no | VERSION | File the version is written into. This is both the release marker and the path the downstream tag pipeline filters on, so keep it in sync with that pipeline’s when.paths. |
notes_file | no | — | Path to a changelog/notes file used as the PR body (e.g. a release-notes step’s artifact). Falls back to a one-line body when unset or missing. |
label | no | release | Label applied to the PR. Pairs well with approval quorum_by_label: to gate sign-off by this label. The label must already exist in the repo. |
title | no | — | PR title. Defaults to “Release <tag_prefix> |
branch_prefix | no | release/ | Prefix for the bot branch (branch = prefix + tag). |
tag_prefix | no | v | Prefix joined to the version for the release name + branch (v1.3.0). Keep it equal to the semver-bump / tag step’s prefix. |
Examples
open a curated release PR
Manual pipeline a release manager triggers. semver-bump computes the next version, release-notes builds the changelog, and release-pr opens the PR. Peers approve it in GitHub; merging it (touching VERSION) fires the tag pipeline.
when: event: [manual]stages: [prepare, propose]jobs: bump: stage: prepare uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 with: { prefix: v } outputs: { next: NEXT } notes: stage: prepare uses: ghcr.io/klinux/gocdnext-plugin-release-notes@v1 with: format: conventional output: CHANGELOG-NEXT.md artifacts: paths: [CHANGELOG-NEXT.md] open-pr: stage: propose needs: [bump, notes] needs_artifacts: - from_job: notes paths: [CHANGELOG-NEXT.md] uses: ghcr.io/klinux/gocdnext-plugin-release-pr@v1 with: version: ${{ needs.bump.outputs.next }} repo: acme-org/web base: main notes_file: CHANGELOG-NEXT.md token: ${{ RELEASE_TOKEN }} secrets: [RELEASE_TOKEN]ruby
build — Ruby build + test runner — ruby:3.3 with build-base + common headers (the ecosystem leans hard on native extensions: nokogiri, pg, ffi). Runs bundle install automatically when a Gemfile is present (skip with no-install: true), with BUNDLE_PATH redirected into the workspace (vendor/bundle) so the native cache: block and the install-once-reuse-N artifact pattern both work.
Image: ghcr.io/klinux/gocdnext-plugin-ruby:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | Shell command to run after the install step. Examples: “bundle exec rspec”, “bundle exec rubocop —format simple”. |
working-dir | no | . | Directory under the workspace to cd into first. |
no-install | no | false | Skip bundle install and run against the workspace’s vendor/bundle as-is. |
Examples
rspec with gem cache
Lock-file-keyed cache so dependency bumps invalidate automatically.
cache: - key: gems-{{ hash "Gemfile.lock" }} paths: [vendor/bundle]uses: ghcr.io/klinux/gocdnext-plugin-ruby@v1with: command: bundle exec rspecrubocop reusing an installed bundle
vendor/bundle shipped by an install job; rubocop consumes it without re-resolving.
jobs: install: stage: build uses: ghcr.io/klinux/gocdnext-plugin-ruby@v1 with: { command: "true" } artifacts: [vendor/bundle/]
rubocop: stage: test needs_artifacts: - from_job: install paths: [vendor/bundle/] uses: ghcr.io/klinux/gocdnext-plugin-ruby@v1 with: command: bundle exec rubocop no-install: "true"rust
build — Run cargo subcommands — build, test, clippy, fmt, publish. Ships with the stable 1.83 toolchain + rustfmt + clippy. Switch toolchains via toolchain: nightly or a pinned 1.82.0 when a specific version is required. Pair the image registry / crates.io token inputs with secrets: so they don’t end up in the log.
Image: ghcr.io/klinux/gocdnext-plugin-rust:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | Cargo subcommand + flags, word-split. Examples: “test —workspace”, “clippy —all-targets — -D warnings”, “fmt —check”, “build —release”. |
working-dir | no | . | Directory under the workspace to cd into before cargo runs. Monorepos with a sub-crate set this. |
toolchain | no | stable | Rust toolchain to use. “stable” (default), “beta”, “nightly”, or a pinned “1.83.0”. Installed on demand. |
rustflags | no | — | Exported as RUSTFLAGS for the command. Useful for a global “-D warnings” or “-C target-cpu=native” on release builds. |
Examples
test workspace
Run tests across a cargo workspace.
uses: ghcr.io/klinux/gocdnext-plugin-rust@v1with: command: test --workspaceclippy as gate
Clippy failing the build on any warning. Classic CI shape.
uses: ghcr.io/klinux/gocdnext-plugin-rust@v1with: command: clippy --all-targets -- -D warningspublish crate with secret token
cargo publish needs a crates.io token. Pipe it through secrets so the plaintext never touches the log.
secrets: - CARGO_REGISTRY_TOKENuses: ghcr.io/klinux/gocdnext-plugin-rust@v1with: command: publish --token ${{ CARGO_REGISTRY_TOKEN }}cached build
Tar + restore the crates.io registry index + crate archives + git DB across runs. The plugin redirects $CARGO_HOME to .cargo-home/ inside the workspace so the cache: block covers it. target/ is added to the same cache entry so incremental compilation reuses prior artifacts — shaves most of the cold-start cost off subsequent runs.
cache: - key: cargo-${CI_COMMIT_BRANCH} paths: - .cargo-home - targetuses: ghcr.io/klinux/gocdnext-plugin-rust@v1with: command: test --workspaces3
registry — Upload + download objects against an S3-compatible bucket (AWS, MinIO, Cloudflare R2, DO Spaces, Backblaze B2, Wasabi, …). endpoint-url: lets the same plugin talk to any self-hosted backend. Auth travels via AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY in the job env — IRSA / WIF both work automatically when the SDK finds their markers.
Image: ghcr.io/klinux/gocdnext-plugin-s3:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
action | yes | — | upload | download |
bucket | yes | — | Bucket name (no “s3://” prefix). |
key | yes | — | Object key inside the bucket. Destination path on upload, source path on download. |
region | no | — | AWS region. Falls back to AWS_DEFAULT_REGION from env when omitted. |
endpoint-url | no | — | Custom S3 endpoint for MinIO / R2 / DO Spaces / etc. e.g. “https://myminio.corp:9000”. |
file | no | — | action=upload only — workspace-relative source path. |
dest | no | — | action=download: workspace-relative destination (dir or full path). Defaults to the key’s basename in the current directory. |
acl | no | — | Canned ACL applied on upload (private | public-read | …). Leave unset to defer to the bucket policy. |
content-type | no | — | Content-Type metadata stamped on the uploaded object. Defaults to the CLI’s own extension-based inference. |
Examples
upload a release build
Push a signed release tarball to the releases bucket, scoped by the commit SHA so re-runs don’t overwrite each other.
secrets: - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEYuses: gocdnext/s3@v1with: action: upload bucket: mycorp-releases key: myapp/${CI_COMMIT_SHORT_SHA}/myapp.tar.gz file: dist/myapp.tar.gz region: us-east-1env: AWS_ACCESS_KEY_ID: ${{ AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ AWS_SECRET_ACCESS_KEY }}pull a shared asset from MinIO
Same shape against a self-hosted MinIO using endpoint-url.
secrets: - MINIO_KEY - MINIO_SECRETuses: gocdnext/s3@v1with: action: download bucket: shared key: assets/icons.tar.gz endpoint-url: https://myminio.corp:9000 dest: assets/env: AWS_ACCESS_KEY_ID: ${{ MINIO_KEY }} AWS_SECRET_ACCESS_KEY: ${{ MINIO_SECRET }}semgrep
security — SAST scan with semgrep — the de-facto OSS static analyzer (registry packs like p/golang, p/owasp-top-ten, or your own rule files). The FULL report (every severity) is always written as SARIF; the fail-on threshold only decides the exit code, so the artifact never loses findings to the gate. Telemetry is off by default (—metrics=off): a CI scanner phoning home is an opt-in, never a default. Baseline mode (—baseline-commit) reports only findings introduced since a commit — the PR-pipeline pattern that keeps legacy debt from blocking new work.
Image: ghcr.io/klinux/gocdnext-plugin-semgrep:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
config | no | p/default | Rule source(s), comma- or space-separated: registry packs (“p/golang, p/owasp-top-ten”), local rule files or dirs. Each entry becomes its own —config flag. |
fail-on | no | error | Severity threshold for exit 1: error | warning | info | none. “none” = report-only (the job stays green; pair with artifacts to keep the SARIF). The report always contains every severity regardless of the gate. |
report-file | no | semgrep.sarif | SARIF output path — attach via the job’s artifacts: so the report survives the run. |
baseline-commit | no | — | Only report findings NEW since this commit (semgrep —baseline-commit). Typical PR usage: ${CI_PULL_REQUEST_BASE} resolved to a SHA, or origin/main. |
paths | no | . | Scan target path(s), comma- or space-separated, relative to working-dir. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
gate on errors, keep the full report
Fails the job on any error-severity finding; warnings and infos land in the SARIF artifact without blocking.
uses: ghcr.io/klinux/gocdnext-plugin-semgrep@v1with: config: p/golang, p/owasp-top-tenartifacts: optional: [semgrep.sarif]report-only on PRs, comment the count
fail-on: none keeps PRs green while the SARIF ships as an artifact — pair with pr-comment for in-review visibility.
uses: ghcr.io/klinux/gocdnext-plugin-semgrep@v1with: config: p/default fail-on: noneartifacts: optional: [semgrep.sarif]only NEW findings on the PR (baseline)
Legacy findings don’t block; anything the PR introduces does. Requires full git history in the workspace.
uses: ghcr.io/klinux/gocdnext-plugin-semgrep@v1with: config: p/default fail-on: warning baseline-commit: origin/mainsemver-bump
release — Compute the next SemVer version from the Conventional Commits between the prior tag and HEAD. Writes a shell-sourceable file under the workspace (NEXT, CURRENT, KIND, PREV_SHA) that downstream jobs source to drive git tag, build labels, or publish steps — no operator-typed TAG variable at trigger time. Bump rules (Conventional Commits): - any feat!: / fix!: (etc.) subject OR BREAKING CHANGE: in any commit body → major - any feat: / feat(scope): → minor - else → patch When no prior tag exists, the initial version (PLUGIN_INITIAL, default 0.1.0) is emitted as-is. When no commits exist since the prior tag, KIND=none and NEXT=CURRENT so downstream can short-circuit without re-tagging.
Image: ghcr.io/klinux/gocdnext-plugin-semver-bump:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
prefix | no | v | Tag prefix to scope detection against (e.g. “v” reads v1.2.3-style tags; empty reads SemVer-only tags; “release-” reads release-1.2.3-style). Used with git describe --match '<prefix>[0-9]*' so the digit guard rejects non-SemVer siblings sharing the prefix (vfoo, vnext, vendor-1) AND a repo with mixed tag tracks (release/ + v) doesn’t cross-pollute. Must match [A-Za-z0-9._/-]* — no shell metas (the value lands in the shell-sourceable output file). |
initial | no | 0.1.0 | First version emitted when no prior tag exists. Must be SemVer (major.minor.patch with optional pre-release suffix). Default 0.1.0 matches the SemVer-recommended start for a library — bump-major detection still applies on top once the first tag lands. |
pre-release | no | — | Pre-release suffix appended to the computed next version, e.g. rc1 → v1.3.0-rc1. Must match [A-Za-z0-9.-]+ so a typo doesn’t end up in a tag name. Empty (default) emits a stable tag. |
force-kind | no | — | Override the auto-detected bump kind. One of major, minor, patch. Empty (default) uses the Conventional Commits scan. Use this when the operator KNOWS the next release is e.g. a major even if commits read as patch (doc- only cycle that ships a deprecated-API removal flagged separately). |
prior-tag | no | — | Override for the prior-tag detection. Must itself be a SemVer-shaped tag (e.g. v1.2.3) — the plugin parses this verbatim as the baseline whose version gets bumped. Defaults to the nearest reachable matching tag via git describe --tags --match '<prefix>[0-9]*' --abbrev=0. The [0-9]* filter (not just *) rejects non-SemVer siblings sharing the prefix (vfoo, vnext, vendor-*) that would otherwise crash the SemVer parser. Use this input ONLY to pin a specific prior tag — not to point at an arbitrary ref like main or a SHA. |
output | no | .gocdnext/semver.env | Workspace-relative output file. Created with mkdir -p. Shell-sourceable format (NEXT=, CURRENT=, KIND=, PREV_SHA=). Pair with artifacts.paths: on this job and needs_artifacts: on the consumer in isolated runtimes (Kubernetes engine, isolated mode) so the file crosses job boundaries. |
Examples
native outputs (gocdnext v0.11+) — no needs_artifacts plumbing
Declare outputs: on the bump job and downstream jobs reference ${{ needs.bump.outputs.next }} from variables: / plugin with:. The scheduler substitutes these at dispatch and the resolved values then expand inside script: via normal shell $VAR references — no needs_artifacts: + source .gocdnext/semver.env step needed. The legacy workspace-file path is still emitted in parallel for older agents / explicit consumers. Important: gocdnext’s ${{ ... }} substitution runs on env: / variables: / plugin with: only — NOT on the raw script: lines (so shell-side ${HOME} etc. survive verbatim). The pattern is to land the value in a variables: env var and reference it in the script via normal $NAME.
jobs: bump: stage: tag uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 outputs: next: NEXT kind: KIND
create-tag: stage: tag needs: [bump] image: alpine:3.20 # Land needs outputs into env so the script sees them # as plain shell vars. Substitution happens here at # dispatch; the script then runs with NEXT/KIND set. variables: NEXT: ${{ needs.bump.outputs.next }} KIND: ${{ needs.bump.outputs.kind }} script: - apk add --no-cache git - | if [ "$KIND" = "none" ]; then echo "No commits — skipping tag" exit 0 fi git tag -a "$NEXT" -m "Release" # ... push via GIT_ASKPASS pattern, etc.auto-bump on every release pipeline
Cuts the next version automatically — no TAG variable at trigger time. The downstream create-tag job sources the output and pushes whatever semver-bump decided.
jobs: bump: stage: tag uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 artifacts: paths: [".gocdnext/semver.env"]
create-tag: stage: tag needs: [bump] needs_artifacts: - from_job: bump paths: [".gocdnext/semver.env"] secrets: [GH_RELEASE_TOKEN] image: alpine:3.20 script: - apk add --no-cache git - | # Use `.` (POSIX) rather than `source` (bash-only) so # the script works on Alpine's default ash shell. The # output file is intentionally shell-portable for # exactly this reason. . .gocdnext/semver.env if [ "$KIND" = "none" ]; then echo "No commits since $CURRENT — skipping tag" exit 0 fi git config user.email "ci-release@example.com" git config user.name "release-bot" git tag -a "$NEXT" -m "Release $NEXT" # GIT_ASKPASS pattern (see trunk-based-release recipe) # to push without leaking the token on argv. ...pre-release cycle (release candidates)
Stamp every push to a release/* branch with an incrementing -rc<N> pre-release. Combine with force-kind: patch so RC bumps don’t accidentally land a major on the wrong cycle.
jobs: rc-bump: stage: tag uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 with: pre-release: rc${CI_RUN_COUNTER} force-kind: patch artifacts: paths: [".gocdnext/semver.env"]SemVer-only tags (no v prefix)
Helm chart repos often tag with bare SemVer (1.2.3) instead of v1.2.3. Empty prefix: matches that scheme.
jobs: bump: stage: tag uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 with: prefix: "" initial: "0.1.0" artifacts: paths: [".gocdnext/semver.env"]separate release tracks (multi-track monorepo)
A monorepo with independently versioned components — each component has its own prefix and its own bump. Run one semver-bump job per track with a track-scoped output file.
jobs: bump-api: stage: tag uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 with: prefix: api-v output: .gocdnext/api.env artifacts: paths: [".gocdnext/api.env"]
bump-web: stage: tag uses: ghcr.io/klinux/gocdnext-plugin-semver-bump@v1 with: prefix: web-v output: .gocdnext/web.env artifacts: paths: [".gocdnext/web.env"]sentry-release
notifications — Create + finalize a Sentry release, optionally setting commit metadata (—auto, degrading gracefully on shallow clones), uploading sourcemaps and marking a deploy to an environment. Version precedence: explicit input → CI_TAG_NAME → CI_COMMIT_SHA, so tag-triggered release pipelines need zero config. Auth via the job’s secrets: (sentry-cli’s native env), never argv.
Image: ghcr.io/klinux/gocdnext-plugin-sentry-release:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
version | no | — | Release identifier. Default: the triggering tag (CI_TAG_NAME), falling back to the commit SHA. |
org | no | — | Sentry organization slug (or SENTRY_ORG env). |
project | no | — | Sentry project slug (or SENTRY_PROJECT env). |
set-commits | no | true | Attach commit metadata via —auto —ignore-missing; skipped gracefully when the repo isn’t mapped in Sentry or the clone is too shallow. |
sourcemaps | no | — | Directory of sourcemaps to upload for this release. |
finalize | no | true | Mark the release finalized (sets the released-at timestamp). |
environment | no | — | Also record a deploy of this release to the environment (shows up in Sentry’s Releases → Deploys). |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
release on tag with deploy marker
secrets: [SENTRY_AUTH_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-sentry-release@v1with: org: acme-org project: shop environment: productionfrontend release with sourcemaps
secrets: [SENTRY_AUTH_TOKEN]uses: ghcr.io/klinux/gocdnext-plugin-sentry-release@v1with: org: acme-org project: shop-web sourcemaps: dist/assetsslack
notifications — Post a notification to a Slack incoming webhook. Defaults to a concise “pipeline #N → status (sha)” line using the CI_* env vars the agent injects; override via template: for richer copy. Minimal by design — the richer “blocks” shape lands if operators ask.
Image: ghcr.io/klinux/gocdnext-plugin-slack:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
webhook | yes | — | Slack incoming webhook URL. Populate via the job’s secrets: list so the token-bearing URL stays out of logs. |
channel | no | — | Override the webhook’s default channel (e.g. “#deploys”). Empty = use whatever the webhook’s Slack config routes to. |
template | no | — | Message body. Defaults to a short status line built from CI_PIPELINE, CI_RUN_COUNTER, CI_PIPELINE_STATUS and CI_COMMIT_SHA. Supports Slack mrkdwn. |
Examples
default status line
Post the default “pipeline #N → status (sha)” line to the webhook’s configured channel.
secrets: - SLACK_WEBHOOKuses: ghcr.io/klinux/gocdnext-plugin-slack@v1with: webhook: ${{ SLACK_WEBHOOK }}custom copy on prod deploy
Richer notification with explicit channel + mrkdwn.
secrets: - SLACK_WEBHOOKuses: ghcr.io/klinux/gocdnext-plugin-slack@v1with: webhook: ${{ SLACK_WEBHOOK }} channel: "#deploys" template: ":rocket: *${CI_PIPELINE}* shipped to prod (${CI_COMMIT_SHA})"sonar
quality — Analyze a project with SonarQube (self-hosted) or SonarCloud (SaaS). Bundles three scanner front-ends in one image — Maven (mvn sonar:sonar), Gradle (gradle sonar), and the language-agnostic Scanner CLI (sonar-scanner) for JS/TS, Python, Go, C/C++, etc. Mode is auto-detected from the project layout, or set explicitly via mode:. Auth via the job’s secrets: list — the scanner reads SONAR_TOKEN from env so the token never appears in ps auxww or job logs. SonarCloud is the default host (https://sonarcloud.io). Point host-url: at your SonarQube base URL for the self- hosted case. SonarQube Community Edition does NOT support branch / pull-request analysis — those properties are rejected by the server. Use Developer Edition+ or SonarCloud when you need PR decoration.
Image: ghcr.io/klinux/gocdnext-plugin-sonar:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
token | no | — | Sonar auth token. Generated at User → My Account → Security (SonarCloud) or Administration → Security → Users (SonarQube). Populate via the job’s secrets: list — the plugin exports it as SONAR_TOKEN env so the scanner picks it up without landing on the CLI argv. required: false because the agent already injects the named secret as SONAR_TOKEN env when you list secrets: [SONAR_TOKEN] on the job, and the plugin accepts the env directly. The runtime fails fast with a clear message when NEITHER token: nor SONAR_TOKEN env is present. |
host-url | no | https://sonarcloud.io | Sonar server base URL. https://sonarcloud.io for SonarCloud (default); for SonarQube self-hosted set to your install (https://sonar.example.com). |
organization | no | — | SonarCloud organization key (my-org). REQUIRED when host-url points at SonarCloud — the plugin fails fast with a clear error otherwise. Ignored on SonarQube. |
project-key | yes | — | sonar.projectKey — uniquely identifies the project on the Sonar server. SonarCloud convention: <org>_<repo>. SonarQube convention: any unique slug (my-app). |
project-name | no | — | sonar.projectName — display name on the Sonar dashboard. Defaults to project-key. |
project-version | no | — | sonar.projectVersion — surfaces on the dashboard + attached to each new issue. Defaults to ${CI_COMMIT_SHORT_SHA} so every analysis carries a stable audit trail back to the commit. |
mode | no | auto | auto | maven | gradle | scanner-cli. auto detects from project layout (pom.xml → maven, build.gradle{,.kts} → gradle, else scanner-cli). Override when you want to force the scanner-cli path on a JVM project (rare). |
working-dir | no | . | Directory under the workspace to cd into before running the scanner. Monorepos with the project root deeper than the repo root set this. |
sources | no | — | sonar.sources — comma-separated source dirs. Defaults to auto-detect: maven/gradle modes read the project model; scanner-cli mode defaults to . (current directory). |
tests | no | — | sonar.tests — comma-separated test dirs. Same auto-detect defaults as sources. |
branch | no | — | sonar.branch.name — sets the branch this analysis writes to. Skip on SonarQube Community Edition (branch analysis is a paid feature; the server rejects the property). Empty = main-branch analysis. PR mode (see pull-request-key) takes precedence. |
pull-request-key | no | — | sonar.pullrequest.key — PR/MR id from the SCM provider (GitHub PR number, GitLab MR iid, Bitbucket PR id). When set, the scanner switches to PR-decoration mode and branch: is ignored. |
pull-request-branch | no | — | sonar.pullrequest.branch — source branch of the PR. Required for PR mode (the GitHub/GitLab integration decorates the PR with the analysis result). |
pull-request-base | no | — | sonar.pullrequest.base — target branch of the PR (typically main or develop). |
wait-for-quality-gate | no | false | true → pass -Dsonar.qualitygate.wait=true; the scanner polls the server until the Quality Gate has been evaluated and exits non-zero if the gate fails. Use this to block a PR pipeline on gate failure. Trade-off: adds 1-3 minutes of wait per scan. false (default) = scan and go; gate evaluation happens server-side and surfaces on the dashboard but doesn’t fail the CI job. |
quality-gate-timeout | no | 300 | Seconds to wait for the gate when wait-for-quality-gate is true. Default 300 (5 minutes). Long-running gates on large projects may need a higher value. |
extra-props | no | — | Newline-separated -Dsonar.X=Y pairs forwarded verbatim to the scanner — ONE property per line (values may contain whitespace; -Dsonar.projectDescription=my app reaches the scanner as a single argv). Blank lines and #-prefixed comments are ignored. Use for niche properties not covered by dedicated inputs — coverage report paths, exclusions, language-specific settings. Auth-bearing properties (sonar.token, sonar.login, sonar.password) are REJECTED at runtime, case- insensitive — route auth via secrets: + token:. |
Examples
SonarCloud + Maven (recommended JVM path)
Most common path: SonarCloud + a Maven project. The plugin auto-detects mode: maven from pom.xml, redirects the local Maven repo to .m2-repo/ so the platform’s cache: block can tar it across runs, and the scanner cache lands at .sonar-cache/ (also captured). sonar:sonar reads Surefire/JaCoCo report paths from the pom — pre-run your tests in a previous job; this one only analyses.
sonar-scan: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 needs: [test] needs_artifacts: - from_job: test paths: [target/site/jacoco/jacoco.xml, target/surefire-reports/] cache: - key: sonar-{{ hash "**/pom.xml" }} paths: [.sonar-cache, .m2-repo] with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_my-app project-name: My App token: ${{ SONAR_TOKEN }}SonarCloud + Gradle
Requires the project’s build.gradle{,.kts} to apply the org.sonarqube plugin. The plugin invokes the sonar task (renamed in plugin v3.0+, 2022). Projects pinned to older versions of org.sonarqube still on the legacy sonarqube task name need to bump the plugin — there’s no per-job task-name override input today.
sonar-scan: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 needs: [test] cache: - key: sonar-{{ hash "**/*.gradle*" }} paths: [.sonar-cache, .gradle-home/caches, .gradle-home/wrapper] with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_my-app token: ${{ SONAR_TOKEN }}SonarQube self-hosted + Scanner CLI (JS/TS, Python, Go)
Non-JVM project against a self-hosted SonarQube. The Scanner CLI handles 30+ languages out of the box; pass coverage report paths via extra-props: because the property name is language-specific. Examples: - JS/TS: sonar.javascript.lcov.reportPaths=coverage/lcov.info - Go: sonar.go.coverage.reportPaths=cover.out - Python: sonar.python.coverage.reportPaths=coverage.xml
sonar-scan: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 needs: [test] needs_artifacts: - from_job: test paths: [coverage/lcov.info] cache: - key: sonar-cli paths: [.sonar-cache] with: host-url: https://sonar.example.com project-key: my-app project-version: ${CI_COMMIT_SHORT_SHA} mode: scanner-cli sources: src tests: test token: ${{ SONAR_TOKEN }} extra-props: | -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info -Dsonar.sourceEncoding=UTF-8PR decoration
When the run is for a PR/MR, set the three pullrequest-* inputs. The scanner switches to PR-incremental mode (only analyses changed files vs the base branch) and SonarCloud/ SonarQube Developer+ posts a comment on the PR with the result. wait-for-quality-gate: "true" makes the job fail when the gate fails so the PR is blocked. gocdnext materialises PR metadata as CI_PULL_REQUEST_* env vars server-side from the webhook payload (since v0.9.0 — see the YAML reference for the full set). No variables: block or trigger-flow plumbing required; PR runs see the values automatically.
sonar-pr: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_my-app pull-request-key: ${CI_PULL_REQUEST_KEY} pull-request-branch: ${CI_PULL_REQUEST_BRANCH} pull-request-base: ${CI_PULL_REQUEST_BASE} wait-for-quality-gate: "true" quality-gate-timeout: "600"Quality Gate as a CI blocker
Main-branch scan that blocks the pipeline on gate failure. Pair with notification fan-out so the team is paged when main breaks the gate. The scanner polls the server until the gate verdict is in — typical wait is 30s-2min for small projects, longer for monorepos.
sonar-main: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 cache: - key: sonar-{{ hash "**/pom.xml" }} paths: [.sonar-cache, .m2-repo] with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_my-app token: ${{ SONAR_TOKEN }} wait-for-quality-gate: "true" quality-gate-timeout: "600"Monorepo with multiple Sonar projects
Each sub-project gets its own Sonar project (SonarCloud charges per-project, so flat per-app keys instead of one umbrella project keeps team scoping clean). The platform doesn’t substitute ${{ MATRIX_VAR }} inside cache.key or other workspace-relative paths today, so the recipe here uses one explicit job per app rather than parallel.matrix:. The cache dirs .m2-repo/ .sonar-cache capture the workspace-rooted absolute paths the entrypoint creates — they’re shared across every per-app scan in the same run, which is exactly what you want.
stages: [quality]
jobs: sonar-api: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 cache: - key: sonar-api-{{ hash "apps/api/pom.xml" }} paths: [.sonar-cache, .m2-repo] with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_api project-name: api working-dir: apps/api
sonar-web: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 cache: - key: sonar-web-{{ hash "apps/web/package-lock.json" }} paths: [.sonar-cache] with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_web project-name: web mode: scanner-cli working-dir: apps/web
sonar-worker: stage: quality secrets: [SONAR_TOKEN] uses: ghcr.io/klinux/gocdnext-plugin-sonar@v1 cache: - key: sonar-worker-{{ hash "apps/worker/go.sum" }} paths: [.sonar-cache] with: host-url: https://sonarcloud.io organization: my-org project-key: my-org_worker project-name: worker mode: scanner-cli working-dir: apps/worker extra-props: | -Dsonar.go.coverage.reportPaths=cover.outssh
deploy — Primitive deploy-over-SSH: rsync files to a remote host, run a script on a remote host, or both. Aimed at the universe the k8s deployments: primitive doesn’t cover — VPS, bare-metal, edge boxes, legacy apps, one-off ops on a DB host.
Image: ghcr.io/klinux/gocdnext-plugin-ssh:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
host | yes | — | Remote hostname or IP. |
user | yes | — | SSH user. |
port | no | 22 | SSH port. |
key | no | — | Private key PEM content (multi-line). Pass via secrets: so it stays masked in logs. One of key or password is required. |
password | no | — | Password auth fallback for legacy hosts that don’t take keys. Strongly discouraged vs. key. Mutually exclusive with key. |
known_hosts | no | — | Output of ssh-keyscan -p <port> <host>. One or more lines. Required when host_key_check is on (the default). Pass via secrets: so a key rotation is audit-trailed. |
host_key | no | — | Single host-key line (same shape as a known_hosts entry). Use when you have one fingerprint to pin and don’t want a full known_hosts blob. |
host_key_check | no | yes | ”yes” (default) enforces StrictHostKeyChecking and refuses to connect without known_hosts/host_key. “no” disables — logs a warn so the trade-off shows up in the run output. Never pick “no” on a real deploy. |
upload | no | — | Path(s) on the agent’s workspace to rsync to the remote. File or directory. Multiple paths via newline OR comma separator. Empty = nothing copied. |
target | no | — | Destination dir on the remote. Required when upload is set. rsync’s —mkpath creates the dir if missing. |
rsync_opts | no | -az --delete | Extra flags passed to rsync. The default mirrors a typical “rsync the build output” deploy. Override with bigger flags if you need them (“-azv —delete —exclude .git”). |
script | no | — | Multi-line script to run on the remote (single ssh invocation, runs under set -euo pipefail). Empty = upload-only mode. |
working-dir | no | . | Directory under the agent’s workspace to cd into before resolving upload paths. Has no effect on the remote — for that, prefix the script with cd /target. |
Examples
deploy a Go binary to a VPS
Build artefact + restart systemd service. Key + known_hosts come from project secrets; the run output stays mask-clean.
uses: ghcr.io/klinux/gocdnext-plugin-ssh@v1with: host: api.example.com user: deploy key: ${{ secrets.SSH_DEPLOY_KEY }} known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} upload: dist/api-server target: /opt/api/ script: | sudo systemctl restart api sudo systemctl status api --no-pagerrun a one-off remote command
No upload — just connect and run. Useful for migrations on a dedicated DB host or restart-after-config-push.
uses: ghcr.io/klinux/gocdnext-plugin-ssh@v1with: host: db.example.com user: ops key: ${{ secrets.SSH_OPS_KEY }} known_hosts: ${{ secrets.DB_KNOWN_HOSTS }} script: | /opt/migrate/run.sh --env=prodcopy artefact tree, no remote command
Sync dist/ to the host, replacing what’s there. Service reload happens via inotify on the remote, so no script is needed.
uses: ghcr.io/klinux/gocdnext-plugin-ssh@v1with: host: assets.example.com user: assets key: ${{ secrets.ASSETS_DEPLOY_KEY }} known_hosts: ${{ secrets.ASSETS_KNOWN_HOSTS }} upload: dist/ target: /var/www/assets/tag
release — Create a git tag on the current commit and push it to origin. Annotated when message: is set (needed for GitHub Releases + semver tooling); lightweight otherwise. Auth over HTTPS only — pipe a Personal Access Token / Job Token via secrets:. SSH remotes aren’t supported in v1; fall back to script: + git for those.
Image: ghcr.io/klinux/gocdnext-plugin-tag:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
name | yes | — | Tag name, e.g. “v1.2.3” or “latest”. |
message | no | — | Annotated-tag message. When set the tag is created with -a -m; when omitted a lightweight (ref-only) tag is pushed instead — fine for floating markers, not great for release tooling. |
revision | no | HEAD | Commit SHA to tag. Defaults to the run’s HEAD. |
remote | no | origin | Git remote name. |
username | no | — | HTTPS auth user. Providers ignore/require different values: GitHub “x-access-token”, GitLab “oauth2”, Bitbucket the account name. Defaults to “git” which most of them accept next to a token. |
token | yes | — | HTTPS auth token. Populate via secrets: — the only credential on the wire. |
force | no | false | Set “true” to push with —force. Useful for moving a “latest” pointer tag; never use for a semver release tag. |
Examples
annotated release tag
Standard semver release flow — annotate with the changelog entry and push. Uses the pipeline-computed RELEASE_TAG var (typically set by a release-notes: step upstream).
secrets: - RELEASE_TOKENuses: ghcr.io/klinux/gocdnext-plugin-tag@v1with: name: ${RELEASE_TAG} message: "Release ${RELEASE_TAG} (${CI_COMMIT_SHORT_SHA})" username: x-access-token token: ${{ RELEASE_TOKEN }}move “latest” pointer
Floating lightweight tag that always points at the newest main commit.
secrets: - RELEASE_TOKENuses: ghcr.io/klinux/gocdnext-plugin-tag@v1with: name: latest force: "true" username: x-access-token token: ${{ RELEASE_TOKEN }}teams
notifications — Post a message to a Microsoft Teams Incoming Webhook. Sibling of the slack/discord plugins — same “one HTTP POST, no SDK” idea with a MessageCard wire format. Pipe the webhook URL via secrets:; the URL doubles as the auth token.
Image: ghcr.io/klinux/gocdnext-plugin-teams:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
webhook | yes | — | Teams channel’s Incoming Webhook URL. Populate via the job’s secrets: list so the URL never lands in logs. |
title | no | — | Card title. Defaults to “pipeline #N → status” using CI_* vars so a team gets a meaningful notification without extra config. |
message | no | — | Body text (Markdown allowed). Defaults to “commit SHA on BRANCH”. |
theme-color | no | — | 6-digit hex accent bar color (no leading #). Auto-tints by status when omitted: green on success, red on failure, amber on canceled, gray otherwise. |
payload-file | no | — | Path (under workspace) to a pre-built JSON body. Skips every other input and POSTs verbatim — escape hatch for Adaptive Card content. |
Examples
failure notification on prod
Fire a red card to the on-call channel when the prod deploy fails. Webhook URL comes from secrets so it never touches the log.
when: failuresecrets: - TEAMS_WEBHOOKuses: ghcr.io/klinux/gocdnext-plugin-teams@v1with: webhook: ${{ TEAMS_WEBHOOK }} title: "[PROD] ${CI_PIPELINE_NAME} #${CI_RUN_COUNTER} failed" message: | Commit: ${CI_COMMIT_SHA} Branch: ${CI_COMMIT_BRANCH} [Open run](${CI_RUN_URL})adaptive-card payload
Pre-built Adaptive Card JSON shipped as a workspace file. Skips all default fields and POSTs verbatim.
secrets: - TEAMS_WEBHOOKuses: ghcr.io/klinux/gocdnext-plugin-teams@v1with: webhook: ${{ TEAMS_WEBHOOK }} payload-file: notifications/adaptive-card.jsonterraform
deploy — Run terraform subcommands — init, plan, apply, destroy, fmt, validate, output. Backend credentials (AWS_, GOOGLE_, ARM_*, etc) come through the job’s secrets: list so nothing lands in the log. Pair plan → approval-gate → apply to get manual confirmation on non-trivial changes.
Image: ghcr.io/klinux/gocdnext-plugin-terraform:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
command | yes | — | terraform subcommand + args, word-split. Examples: “init -input=false”, “plan -out=tfplan”, “apply -input=false tfplan”, “fmt -check -recursive”. |
working-dir | no | . | Directory under the workspace containing the terraform config. Monorepos with a nested module set this. |
version | no | latest | Terraform version. “latest” uses the bundled 1.9.8; pin to a specific version when the module demands determinism. |
var-file | no | — | Path (under the workspace) to a -var-file. Auto-applied for plan/apply/destroy/refresh/import; silently ignored for fmt/validate/output. |
Examples
init + plan with AWS creds from secrets
Classic shape — credentials from secrets so the log never sees them. Two-step: init first, then plan.
secrets: - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEYuses: ghcr.io/klinux/gocdnext-plugin-terraform@v1with: command: init -input=false working-dir: infra/prodplan with var-file, artifact the tfplan
Run plan -out=tfplan and surface the file as an artifact so the downstream approval-gate + apply job can consume it.
uses: ghcr.io/klinux/gocdnext-plugin-terraform@v1with: command: plan -out=tfplan working-dir: infra/prod var-file: envs/prod.tfvarsartifacts: paths: [infra/prod/tfplan]apply the previously planned tfplan
Downstream of an approval-gate. Consumes the tfplan artifact produced by plan. -input=false skips any interactive prompts.
uses: ghcr.io/klinux/gocdnext-plugin-terraform@v1needs_artifacts: - from_job: plan paths: [infra/prod/tfplan]with: command: apply -input=false tfplan working-dir: infra/prodcached provider plugins
Terraform downloads provider plugins (hashicorp/aws, hashicorp/google, etc.) into .terraform/providers/ on every init. Bigger modules pull 100+ MB. Cache the .terraform/ directory across runs to skip the download — the lockfile (.terraform.lock.hcl) pins provider versions so the cache stays consistent. Pair with downstream jobs that needs_artifacts: the .terraform/ dir if they run on different containers — the plugin doesn’t re-init when the dir is already populated.
init: stage: plan uses: ghcr.io/klinux/gocdnext-plugin-terraform@v1 cache: - key: tf-providers-{{ hash "infra/prod/.terraform.lock.hcl" }} paths: [infra/prod/.terraform] with: command: init -input=false working-dir: infra/prodtrivy
security — Scan images, filesystems, git repos, or Kubernetes manifests for CVEs and leaked secrets using Aqua’s Trivy. By default fails the job on HIGH/CRITICAL findings that have a fix available — tune severity + exit_code to make the scan advisory instead of blocking.
Image: ghcr.io/klinux/gocdnext-plugin-trivy:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
scan-type | no | fs | What to scan: “image”, “fs”, “repo”, or “config”. Defaults to “fs” (the workspace tree). |
target | no | — | What to point the scan at. Required for image + repo; defaults to ”.” (the workspace root) for fs + config. |
skip-db-update | no | false | Skip the upstream HEAD that checks whether the cached vulnerability DB is fresh. Default off — trivy verifies freshness on every run and only downloads when stale (24h policy). Force “true” on fully offline / air-gapped runners where the freshness check itself would error. Pair with cache: [{ key: trivy-db, paths: [.cache/trivy] }] to persist the DB across runs. |
severity | no | HIGH,CRITICAL | Comma-separated severity filter. Anything below the threshold is ignored for exit-code purposes but still appears in the report. |
exit-code | no | 1 | Exit code when the scan finds anything in the severity filter. Set to “0” to make the scan advisory. |
ignore-unfixed | no | true | When “true”, CVEs without an upstream fix are skipped — reduces noise on libraries you can’t patch anyway. |
skip-dirs | no | — | Comma-separated directories/globs to exclude from an fs/repo scan (e.g. “node_modules,vendor”). Use it to skip build-time dependency trees that never reach the runtime image, so their CVEs don’t fail a source scan — pair with a separate scan-type: image job to cover what actually ships. Forwarded verbatim as trivy’s --skip-dirs. |
format | no | table | Report format — table (human), json, sarif, or template. |
username | no | — | Registry username for scan-type: image against a private registry (GHCR-private, ECR, GAR, internal Harbor, …). The plugin promotes this to TRIVY_USERNAME env which trivy reads natively. Populate via the job’s secrets: list so the value flows through the gocdnext masking layer + the docker-engine env propagation (NAME-only on argv, value via cmd.Env). Empty = anonymous pull (works on public registries). |
password | no | — | Registry token/password. Same path as username:: promoted to TRIVY_PASSWORD env. Populate via secrets:. |
output | no | — | Path (under the workspace) to write the report to. Pairs well with artifacts.paths: to attach the report to the run. |
Examples
scan built image
Scan the image just built by gocdnext/docker.
uses: ghcr.io/klinux/gocdnext-plugin-trivy@v1with: scan-type: image target: ghcr.io/org/app:${CI_COMMIT_SHORT_SHA} severity: HIGH,CRITICALscan published image on a private registry
Pulls the image from a private registry for scanning. The login buildx did in the build job doesn’t carry across job containers, so trivy needs its own creds — username: and password: promote to TRIVY_USERNAME / TRIVY_PASSWORD which trivy reads natively. Use this in scan-after-publish pipelines where the build job already pushed.
uses: ghcr.io/klinux/gocdnext-plugin-trivy@v1secrets: [GHCR_USERNAME, GHCR_TOKEN]with: scan-type: image target: ghcr.io/org/app:${CI_COMMIT_SHORT_SHA} severity: HIGH,CRITICAL username: ${{ GHCR_USERNAME }} password: ${{ GHCR_TOKEN }}advisory fs scan
Scan the workspace tree but don’t fail the build — attach the JSON report as optional artifact.
uses: ghcr.io/klinux/gocdnext-plugin-trivy@v1with: scan-type: fs format: json output: trivy-report.json exit-code: "0"artifacts: optional: [trivy-report.json]cached vulnerability DB
Trivy downloads its CVE database (~50 MB) at scan start and re-validates freshness on a 24h policy. Persist .cache/trivy across runs to skip the download on warm runs; pair with skip-db-update: "true" on air-gapped agents. The cache key is constant (trivy-db) because the database itself is the artefact, not anything project-derived — all projects in the same agent fleet share the warm DB.
trivy-fs: stage: scan uses: ghcr.io/klinux/gocdnext-plugin-trivy@v1 cache: - key: trivy-db paths: [.cache/trivy] with: scan-type: fs target: . severity: HIGH,CRITICAL skip-db-update: "false"vercel
deploy — Deploy to Vercel. Headless linkage via VERCEL_ORG_ID + VERCEL_PROJECT_ID (+ VERCEL_TOKEN through secrets:). prebuilt: true deploys the .vercel/output a prior job produced with vercel build — keeping the build in the build stage and the deploy step instant.
Image: ghcr.io/klinux/gocdnext-plugin-vercel:v1
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
prod | no | false | true = production deploy; false = preview URL. |
prebuilt | no | false | Deploy .vercel/output from a prior vercel build job (shipped via artifacts) instead of building remotely. |
working-dir | no | . | Directory under the workspace to cd into first. |
Examples
simple deploy (Vercel builds remotely)
jobs: deploy: stage: deploy secrets: [VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID] uses: ghcr.io/klinux/gocdnext-plugin-vercel@v1 with: { prod: "true" }build once, deploy prebuilt
jobs: # The Vercel CLI has no env-var auth and --token would put # the secret on argv — write its auth.json instead (the # vercel plugin below does this internally). build: stage: build secrets: [VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID] uses: ghcr.io/klinux/gocdnext-plugin-node@v1 with: command: | VCONF="$(mktemp -d)" trap 'rm -rf "$VCONF"' EXIT node -e 'require("fs").writeFileSync(process.argv[1],JSON.stringify({token:process.env.VERCEL_TOKEN}),{mode:0o600})' "$VCONF/auth.json" npx vercel@37 pull --yes --environment=production --global-config "$VCONF" npx vercel@37 build --prod --yes --global-config "$VCONF" artifacts: [.vercel/output/]
deploy: stage: deploy needs_artifacts: - from_job: build paths: [.vercel/output/] secrets: [VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID] uses: ghcr.io/klinux/gocdnext-plugin-vercel@v1 with: prod: "true" prebuilt: "true"