Usage Guide#
This guide shows how to consume reusable workflows and composite actions from another repository.
Reusable workflows#
Create a workflow in your repo, then call the reusable workflow via uses.
Gitleaks on PRs:
name: Secrets Scan
on:
pull_request:
branches: [ main, master ]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run gitleaks scan
uses: nikolareljin/ci-helpers/.github/actions/gitleaks-scan@production
with:
scan_path: "."
fail_on_findings: "true"
upload_artifact: "true"
PHP scan (unit + framework lint + WP-CLI when detected):
name: PHP Scan
on:
pull_request:
branches: [ main, master ]
jobs:
php_scan:
uses: nikolareljin/ci-helpers/.github/workflows/php-scan.yml@production
with:
php_version: "8.4"
Python scan (unit + Django when detected):
name: Python Scan
on:
pull_request:
branches: [ main, master ]
jobs:
python_scan:
uses: nikolareljin/ci-helpers/.github/workflows/python-scan.yml@production
with:
python_version: "3.13"
Go scan (tests + gosec):
name: Go Scan
on:
pull_request:
branches: [ main, master ]
jobs:
go_scan:
uses: nikolareljin/ci-helpers/.github/workflows/go-scan.yml@production
Rust scan (tests + cargo-audit):
name: Rust Scan
on:
pull_request:
branches: [ main, master ]
jobs:
rust_scan:
uses: nikolareljin/ci-helpers/.github/workflows/rust-scan.yml@production
Java scan (tests + dependency check):
name: Java Scan
on:
pull_request:
branches: [ main, master ]
jobs:
java_scan:
uses: nikolareljin/ci-helpers/.github/workflows/java-scan.yml@production
with:
java_version: "17"
C# scan (tests + vulnerable packages):
name: C# Scan
on:
pull_request:
branches: [ main, master ]
jobs:
csharp_scan:
uses: nikolareljin/ci-helpers/.github/workflows/csharp-scan.yml@production
with:
dotnet_version: "8.0.x"
Node.js scan (lint/test/audit):
name: Node Scan
on:
pull_request:
branches: [ main, master ]
jobs:
node_scan:
uses: nikolareljin/ci-helpers/.github/workflows/node-scan.yml@production
with:
node_version: "22"
React scan (lint/test/build/audit):
name: React Scan
on:
pull_request:
branches: [ main, master ]
jobs:
react_scan:
uses: nikolareljin/ci-helpers/.github/workflows/react-scan.yml@production
with:
node_version: "22"
Vue scan (lint/test/build/audit):
name: Vue Scan
on:
pull_request:
branches: [ main, master ]
jobs:
vue_scan:
uses: nikolareljin/ci-helpers/.github/workflows/vue-scan.yml@production
with:
node_version: "22"
Docker scan (Trivy + Snyk):
name: Docker Scan
on:
pull_request:
branches: [ main, master ]
jobs:
docker_scan:
uses: nikolareljin/ci-helpers/.github/workflows/docker-scan.yml@production
with:
image_name: "app:ci"
secrets:
snyk_token: ${{ secrets.SNYK_TOKEN }}
Composite actions#
Composite actions run inside a normal job. Always include actions/checkout.
Gitleaks composite action:
name: Secrets Scan
on:
pull_request:
branches: [ main, master ]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Gitleaks scan
uses: nikolareljin/ci-helpers/.github/actions/gitleaks-scan@production
with:
scan_path: "."
fail_on_findings: "true"
upload_artifact: "true"
Trivy composite action:
name: Trivy Scan
on:
pull_request:
branches: [ main, master ]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy scan
uses: nikolareljin/ci-helpers/.github/actions/trivy-scan@production
with:
scan_path: "."
format: "sarif"
output: "trivy-results.sarif"
fail_on_findings: "true"
upload_sarif: "true"
Data-safety composite action:
name: Data Safety
on:
pull_request:
branches: [ main, master ]
jobs:
data_safety:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Data safety scan
uses: nikolareljin/ci-helpers/.github/actions/data-safety-scan@production
with:
scan_path: backend/app/data
WordPress plugin-check composite action:
name: WP Plugin Check
on:
pull_request:
branches: [ main, master ]
jobs:
plugin_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Plugin check
uses: nikolareljin/ci-helpers/.github/actions/wp-plugin-check@production
with:
plugin_slug: my-plugin
plugin_src_env: MY_PLUGIN_SRC
plugin_src: "."
php_version: "8.4"
phpunit_command: "vendor/bin/phpunit"
phpcs_warning_command: "vendor/bin/phpcs -p -s --warning-severity=1 --error-severity=0 ."
fail_on_findings: "true"
Semver compare:
name: Compare Versions
on:
pull_request:
branches: [ main, master ]
jobs:
semver:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compare versions
id: semver
uses: nikolareljin/ci-helpers/.github/actions/semver-compare@production
with:
version_a: "1.2.3"
version_b: "1.4.0"
- name: Use result
env:
RESULT: ${{ steps.semver.outputs.result }}
run: echo "result=$RESULT"
Release notes come from the release workflows, through script-helpers'
release_notes.sh. release-build.yml, go-release.yml and
rust-release.yml generate them when generate_release_notes is true, or
take your own text in release_notes. deb-build.yml, fpc-release.yml,
rust-release-tarballs.yml and tauri-release.yml generate them whenever
they upload to a release.
Release tag guard (PR):
name: Release Tag Guard (PR)
on:
pull_request:
branches: [ main, master ]
jobs:
release_guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
- name: Guard release tag
id: release_guard
uses: nikolareljin/ci-helpers/.github/actions/check-release-tag@production
with:
release_branch: ${{ github.head_ref }}
fetch_tags: true
- name: Use version
env:
VERSION: ${{ steps.release_guard.outputs.version }}
run: echo "version=$VERSION"
Release tag guard (release):
name: Release Tag Guard (Release)
on:
release:
types: [ created, published ]
jobs:
release_guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
- name: Guard release tag
id: release_guard
uses: nikolareljin/ci-helpers/.github/actions/check-release-tag@production
with:
release_branch: ${{ github.ref_name }}
fetch_tags: true
- name: Use version
env:
VERSION: ${{ steps.release_guard.outputs.version }}
run: echo "version=$VERSION"
Release tag gate (reusable workflow):
name: Release Tag Gate
on:
pull_request:
jobs:
gate:
uses: nikolareljin/ci-helpers/.github/workflows/release-tag-gate.yml@production
Auto tag release (reusable workflow):
name: Auto Tag Release
on:
push:
branches: [ main, master ]
permissions:
contents: write
pull-requests: read
jobs:
tag:
uses: nikolareljin/ci-helpers/.github/workflows/auto-tag-release.yml@production
Install specific versions from Git tags#
When you publish tagged releases, you can install a specific version directly:
Go:
Rust:
Python:
Java (build from tag):
Production tag (optional)#
This repo includes a repo-local workflow that updates the production tag to
the latest non-rc tag and moves the production branch to the same commit
(create_production.sh does both unless passed --no-branch; consumers that
reference @production get it either way). It is not a reusable workflow; copy
it into other repos only if you want the same behavior. On the automated path
the tag is cut by auto-tag.yml after a merge to main, and the security gate
runs in auto-tag-release-push.yml before it.
name: Update Production Tag
on:
push:
tags:
- "*.*.*"
- "v*.*.*"
permissions:
contents: write
jobs:
security-gate:
if: ${{ !contains(github.ref_name, 'rc') && !contains(github.ref_name, 'RC') }}
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @ 2026-09-10
with:
fetch-depth: 0
- name: Check stale SHA pins
run: bash scripts/update_pinned_actions.sh --check
env:
GH_TOKEN: ${{ github.token }}
- name: Detect floating action refs
run: bash scripts/check_floating_refs.sh
production:
needs: [security-gate]
if: ${{ !contains(github.ref_name, 'rc') && !contains(github.ref_name, 'RC') }}
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @ 2026-09-10
with:
fetch-depth: 0
fetch-tags: true
- name: Update production tag
run: ./scripts/create_production.sh -t "${GITHUB_REF_NAME}" --fetch-tags
This mirrors .github/workflows/production-branch.yml (which additionally
bootstraps vendor/script-helpers when the repository carries it as a
submodule). contents: write is granted only to the job that pushes; the gate
reads.
What create_production.sh checks before it moves anything:
- the tag exists on the remote and names the same commit as the local tag -- a tag that was never pushed, or was re-cut locally, is refused;
- with
--fetch-tags, a fetch that fails stops the run (exit 1) instead of continuing on whatever tags the checkout had; --name main,masterandHEADare refused (exit 2);- the production branch is pushed with a lease on the value the script read from the remote before it pushed anything, so a concurrent move of that branch makes the push fail instead of being overwritten;
- the tag is a release,
X.Y.ZorvX.Y.Z(--allow-prereleasealso acceptsX.Y.Z-<suffix>); - the tag's commit is on the remote's default branch, as the remote has it
(
--base-branchnames another branch). A fetch error or missing history refuses too; - the version is newer than the one
productionis on now, read from the version tags at its commit (--allow-rollbacklifts this). The same tag again is a no-op success.
Manual override (point production at an older tag):
This force-moves a shared ref. The production tag is pushed with
--force and the production branch with a lease, so every consumer pinned to
@production picks up the target on its next run. Run it only as a deliberate
release decision, and prefer update-production.yml or the tag workflow above.
Release build (generic):
name: Release Build
on:
push:
tags: [ "v*.*.*" ]
jobs:
release:
uses: nikolareljin/ci-helpers/.github/workflows/release-build.yml@production
with:
test_command: "go test ./..."
build_command: "go build -o dist/myapp ./cmd/myapp"
artifact_paths: "dist/*"
binary_links: |
Linux|myapp-linux
macOS|myapp-mac
PPA publish:
name: PPA
on:
push:
branches: [ main ]
jobs:
ppa:
uses: nikolareljin/ci-helpers/.github/workflows/ppa-deb.yml@production
with:
working_directory: "."
ppa_target: "ppa:your-launchpad-id/distrodeck"
deb_fullname: "Nikola Reljin"
deb_email: "nikola.reljin@gmail.com"
signing_key_id: ${{ secrets.PPA_GPG_KEY_ID }}
secrets:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
launchpad_ssh_private_key: ${{ secrets.PPA_SSH_PRIVATE_KEY }}
Required secrets for PPA publish:
PPA_GPG_PRIVATE_KEY: armored private key used to sign the source package.PPA_GPG_PASSPHRASE: passphrase for the signing key.PPA_GPG_KEY_ID: key ID or fingerprint (passed as an input).-
PPA_SSH_PRIVATE_KEY: SSH key registered with Launchpad for uploads. Optional inputs: -
series: distro codename override fordebian/changelog(usesdch).
Debian package build:
name: Debian Build
on:
push:
branches: [ main ]
jobs:
deb:
uses: nikolareljin/ci-helpers/.github/workflows/deb-build.yml@production
with:
working_directory: "."
RPM package build:
name: RPM Build
on:
push:
branches: [ main ]
jobs:
rpm:
uses: nikolareljin/ci-helpers/.github/workflows/rpm-build.yml@production
with:
working_directory: "."
prebuild_command: "./tools/gen-man.sh"
spec_path: "packaging/myapp.spec"
artifact_glob: "dist/*.rpm"
Homebrew package build + publish:
name: Homebrew
on:
push:
branches: [ main ]
jobs:
brew:
uses: nikolareljin/ci-helpers/.github/workflows/homebrew-package.yml@production
with:
working_directory: "."
name: "myapp"
desc: "My CLI tool"
homepage: "https://github.com/owner/myapp"
deps: "curl,jq"
entrypoint: "bin/myapp"
man_path: "docs/man/myapp.1"
use_libexec: "true"
env_var: "MYAPP_ROOT"
release_repo: "owner/myapp"
publish: ${{ vars.HOMEBREW_PUBLISH_ENABLED }}
tap_repo: ${{ vars.HOMEBREW_TAP_REPO }}
tap_branch: ${{ vars.HOMEBREW_TAP_BRANCH }}
secrets:
tap_deploy_key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
Rust release build:
name: Rust Release
on:
push:
tags: [ "v*.*.*" ]
jobs:
release:
uses: nikolareljin/ci-helpers/.github/workflows/rust-release.yml@production
with:
bin_name: "image-view"
linux_gnu_aliases: "deb,pacman,yum,redhat"
Go release build:
name: Go Release
on:
push:
tags: [ "v*.*.*" ]
jobs:
release:
uses: nikolareljin/ci-helpers/.github/workflows/go-release.yml@production
with:
bin_name: "myapp"
main_path: "./cmd/myapp"
build_targets: "linux/amd64,windows/amd64,darwin/amd64"
Java release build (Maven example):
name: Java Release
on:
push:
tags: [ "v*.*.*" ]
jobs:
release:
uses: nikolareljin/ci-helpers/.github/workflows/release-build.yml@production
with:
java_version: "17"
test_command: "mvn -B test"
build_command: "mvn -B package -DskipTests"
artifact_paths: "target/*.jar"
Release tagging in external repos (recommended setup):
1) Add the PR gate to block merges when the tag already exists. 2) Require the gate in branch protection for your default branch. 3) Add the auto-tag workflow on the default branch; it fails if the tag already exists.
Minimal setup: