Organization-wide reusable GitHub Actions workflows and composite actions for tsok-org repositories.
This repository provides a centralized collection of:
- Composite Actions - Reusable action building blocks for common setup tasks
- Reusable Workflows - Complete CI/CD workflows that can be called from any repository
- 🔧 Declarative Configuration - Define your environment in
.environment.yml - 🔐 GitHub App Authentication - Bypass branch protection with short-lived tokens
- 📦 Package Manager Auto-Detection - Works with npm, pnpm, yarn, and bun
- ☁️ Nx Cloud Integration - Distributed caching and task execution
- 🐳 Docker Multi-Platform - Build for multiple architectures
- 🔍 Security Scanning - Built-in secret detection with Gitleaks
# .environment.yml
node:
version: .node-version # or "20", "lts/*"
install: true
cache: true
docker:
buildx: true
platforms:
- linux/amd64
- linux/arm64# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, develop]
permissions:
contents: read
checks: write
# Cancel superseded runs on the same PR / branch so a fast follow-up push
# doesn't double up on runners. Set at the *caller* level — reusable workflows
# run as part of the caller's run, so this is the authoritative place to
# control cancellation.
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
ci:
uses: tsok-org/.github/.github/workflows/nx-ci.yml@v1
with:
lint: true
test: true
build: true
secrets:
nx_cloud_access_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}Versioning. Prefer pinning reusable workflows to a released tag (
@v1,@v1.2.3) rather than@main.@maintracks the tip of this repo and an accidental breakage propagates instantly to all consumers. Composite actions inside reusable workflows (e.g.environment-setup) still use floating refs today — that is an explicit trade-off so thev1contract protects the outer workflow surface, which is what consumers actually depend on.
# .github/workflows/cd.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
packages: write
jobs:
release:
uses: tsok-org/.github/.github/workflows/nx-cd.yml@v1
with:
publish_npm: true
create_release: true
secrets:
npm_token: ${{ secrets.NPM_TOKEN }}
github_app_id: ${{ vars.GITHUB_APP_ID }}
github_app_private_key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}Universal environment setup driven by .environment.yml. The action
validates the config against schema.json (draft-07), parses
it with parse-config.sh, then conditionally runs the
per-component setup steps.
For the full .environment.yml reference, see
Configuration › .environment.yml.
- name: Setup Environment
uses: tsok-org/.github/actions/environment-setup@v1
with:
github_app_id: ${{ vars.APP_ID }}
github_app_private_key: ${{ secrets.APP_PRIVATE_KEY }}| Input | Description | Default |
|---|---|---|
config |
Path to environment configuration file | .environment.yml |
working_directory |
Working directory for setup | . |
github_app_id |
GitHub App ID for authentication | - |
github_app_private_key |
GitHub App private key (PEM) | - |
github_token |
Fallback GitHub token | ${{ github.token }} |
skip |
Components to skip (comma-separated) | - |
only |
Only setup these components | - |
skip / only accept: node, python, rust, go, c, terraform,
docker, services, system_packages.
- node — Node.js via
setup-nodewith pnpm/npm/yarn detection - python — Python + pip / poetry / uv
- rust — cache, diagnostics, coverage, build knobs (toolchain comes from
rust-toolchain.toml) - go — Go via
actions/setup-go@v5 - c — gcc/clang + optional cmake/pkg-config (Linux only)
- terraform — Terraform CLI with optional Terragrunt and TFLint
- docker — Docker Buildx with registry authentication
- services — Container services (Postgres, Redis, NATS, MySQL, Mongo, …)
- system_packages — Arbitrary apt packages (Linux only)
Intelligent Node.js setup with automatic package manager detection.
- name: Setup Node.js
uses: tsok-org/.github/actions/setup-node@v1
with:
node_version: "20"
install_dependencies: true
frozen_lockfile: true| Input | Description | Default |
|---|---|---|
node_version |
Node.js version | - |
node_version_file |
File to read version from | .node-version |
install_dependencies |
Install deps after setup | true |
frozen_lockfile |
Use frozen lockfile mode | true |
working_directory |
Working directory | . |
| Output | Description |
|---|---|
package_manager |
Detected package manager (npm/pnpm/yarn/bun) |
node_version |
Installed Node.js version |
exec |
Command to execute packages (e.g., pnpm exec) |
dlx |
Command to run packages (e.g., pnpm dlx) |
| Lockfile | Package Manager | Install Command |
|---|---|---|
pnpm-lock.yaml |
pnpm | pnpm install --frozen-lockfile |
yarn.lock |
yarn | yarn install --frozen-lockfile |
bun.lockb |
bun | bun install --frozen-lockfile |
package-lock.json |
npm | npm ci |
Setup Terraform CLI with optional Terragrunt and TFLint.
- name: Setup Terraform
uses: tsok-org/.github/actions/setup-terraform@v1
with:
terraform_version: "1.12.2"
terragrunt_version: "0.68.0"
tflint_version: "0.50.0"| Input | Description | Default |
|---|---|---|
terraform_version |
Terraform version | latest |
terraform_wrapper |
Install wrapper script | true |
terragrunt_version |
Terragrunt version (empty to skip) | - |
tflint_version |
TFLint version (empty to skip) | - |
cli_config_credentials_hostname |
TFC/TFE hostname | - |
cli_config_credentials_token |
TFC/TFE token | - |
Setup Docker with Buildx, QEMU, and registry authentication.
- name: Setup Docker
uses: tsok-org/.github/actions/setup-docker@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
setup_qemu: true
platforms: linux/amd64,linux/arm64| Input | Description | Default |
|---|---|---|
registry |
Container registry URL | - |
username |
Registry username | - |
password |
Registry password/token | - |
setup_buildx |
Setup Docker Buildx | true |
setup_qemu |
Setup QEMU for multi-arch | false |
platforms |
QEMU platforms | linux/amd64,linux/arm64 |
driver |
Buildx driver | docker-container |
Continuous Integration workflow for Nx monorepos.
jobs:
ci:
uses: tsok-org/.github/.github/workflows/nx-ci.yml@v1
with:
lint: true
test: true
build: true
e2e: false
parallel: 3
affected_only: true
coverage: true
coverage_reporter: codecov
secrets:
nx_cloud_access_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
codecov_token: ${{ secrets.CODECOV_TOKEN }}| Input | Description | Default |
|---|---|---|
environment_config |
Path to .environment.yml | .environment.yml |
environment_skip |
Components to skip | docker,terraform,services |
environment_only |
Only setup these components | - |
working_directory |
Working directory (nested workspaces) | . |
lint |
Run lint task | true |
test |
Run test task | true |
build |
Run build task | true |
e2e |
Run e2e task | false |
custom_tasks |
Extra comma-separated Nx tasks (e.g. typecheck,format:check) |
- |
parallel |
Parallel task count | 3 |
affected_only |
Only affected projects | true |
exclude_projects |
Projects to exclude (glob OK) | - |
coverage |
Enable coverage | false |
coverage_reporter |
Reporter (codecov/coveralls/none) | none |
distribute_on |
Nx Cloud distribution (e.g. 3 linux-medium-js) |
- |
stop_agents_after |
Task to stop distributed agents after | build |
nx_cloud_enabled |
Enable Nx Cloud remote cache | true |
ref |
Git ref to checkout (branch/tag/SHA). Empty = triggering ref. | - |
runs_on |
Runner label | ubuntu-latest |
timeout_minutes |
Job timeout | 30 |
verbose |
Verbose diagnostics (Nx + cargo + backtraces) | false |
github_app_id |
GitHub App ID (optional) | - |
The ref input is useful for scheduled/workflow_dispatch callers that
need to test a specific branch rather than the default branch the cron
fires from; it mirrors the ref input on nx-cd.yml.
| Output | Description |
|---|---|
lint_result |
Lint task result |
test_result |
Test task result |
build_result |
Build task result |
affected_projects |
List of affected projects |
Continuous Delivery workflow with versioning and publishing.
jobs:
release:
uses: tsok-org/.github/.github/workflows/nx-cd.yml@v1
with:
preid: beta # alpha, beta, rc, or empty for stable
dist_tag: beta # npm dist-tag
publish_docker: true
docker_registry: ghcr.io
docker_platforms: linux/amd64,linux/arm64
github_prerelease: true
github_app_id: ${{ vars.GITHUB_APP_ID }}
secrets:
npm_token: ${{ secrets.NPM_TOKEN }}
github_app_private_key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
docker_username: ${{ github.actor }}
docker_password: ${{ secrets.GITHUB_TOKEN }}| Branch | preid | npm Tag | Example |
|---|---|---|---|
| develop | alpha | alpha | 1.2.3-alpha.0 |
| next | beta | beta | 1.2.3-beta.0 |
| main | rc | next | 1.2.3-rc.0 |
| manual | - | latest | 1.2.3 |
| Input | Description | Default |
|---|---|---|
environment_config |
Path to .environment.yml | .environment.yml |
environment_skip |
Components to skip | terraform,services |
environment_only |
Only setup these components | - |
working_directory |
Working directory for the release | . |
preid |
Pre-release identifier (alpha/beta/rc/…) | - |
dist_tag |
npm dist-tag | latest |
github_prerelease |
Mark GitHub release as pre-release | false |
publish_docker |
Build/push Docker images | false |
docker_registry |
Docker registry | ghcr.io |
docker_platforms |
Build platforms (comma-separated) | linux/amd64 |
dry_run |
Dry run mode | false |
first_release |
Skip changelog comparison on first release | false |
verbose |
Verbose nx release output |
false |
ref |
Git ref to checkout (empty = triggering ref) | - |
git_user_name |
Commit author name (fallback when no App) | github-actions[bot] |
git_user_email |
Commit author email (fallback when no App) | github-actions[bot]@users.noreply.github.com |
runs_on |
Runner label | ubuntu-latest |
timeout_minutes |
Job timeout | 30 |
github_app_id |
GitHub App ID (input, not secret — App ID is not sensitive) | - |
| Secret | Description |
|---|---|
github_app_private_key |
GitHub App private key (PEM) |
npm_token |
npm auth token (for nx release publish) |
docker_username |
Docker registry username |
docker_password |
Docker registry password/token |
nx_cloud_access_token |
Nx Cloud remote cache |
| Output | Description |
|---|---|
released |
Whether any projects were released |
version |
The version that was released |
Automated Nx workspace migration with PR creation.
name: Nx Update
on:
schedule:
- cron: "0 6 * * *" # Daily at 6 AM UTC
workflow_dispatch:
jobs:
migrate:
uses: tsok-org/.github/.github/workflows/nx-migrate.yml@v1
with:
channel: stable # stable, rc, beta, canary
major_version: "22" # Lock to major version
pr_auto_merge: true
secrets:
github_app_id: ${{ vars.GITHUB_APP_ID }}
github_app_private_key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}| Input | Description | Default |
|---|---|---|
version |
Explicit Nx version | - |
channel |
Release channel | stable |
major_version |
Lock to major version | - |
minor_version |
Lock to minor version | - |
branch_name_template |
Branch name template | update-nx-{version} |
pr_title_template |
PR title template | build: 📦 update Nx to {version} |
pr_auto_merge |
Enable auto-merge | true |
dry_run |
Preview mode | false |
close_existing_prs |
Close old migration PRs | true |
| Output | Description |
|---|---|
migration_version |
Target Nx version |
current_version |
Current Nx version |
pr_number |
Created PR number |
pr_url |
Created PR URL |
package_manager |
Detected package manager |
Auto-merge Dependabot PRs by update level.
name: Dependabot Auto-Merge
on:
pull_request:
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
uses: tsok-org/.github/.github/workflows/dependabot-auto-merge.yml@v1
with:
merge_level: minor # patch, minor, major
merge_method: squash
ecosystems: npm,docker # Optional filter
secrets:
github_token: ${{ secrets.GITHUB_TOKEN }}| Level | Auto-merges |
|---|---|
patch |
Patch updates only (1.0.0 → 1.0.1) |
minor |
Patch + minor (1.0.0 → 1.1.0) |
major |
All updates (1.0.0 → 2.0.0) |
| Input | Description | Default |
|---|---|---|
merge_level |
Maximum update level | patch |
merge_method |
Merge method (squash/merge/rebase) | squash |
ecosystems |
Filter by ecosystems | - |
excluded_packages |
Packages to never merge | - |
approve_only |
Only approve, don't merge | false |
Secret scanning with Gitleaks.
name: Secret Scan
on:
pull_request:
push:
branches: [main]
jobs:
scan:
uses: tsok-org/.github/.github/workflows/gitleaks.yml@v1
with:
fail_on_leak: true
upload_sarif: true # Requires GitHub Advanced Security
secrets:
gitleaks_license: ${{ secrets.GITLEAKS_LICENSE }}| Input | Description | Default |
|---|---|---|
config_path |
Custom Gitleaks config | - |
scan_depth |
Commits to scan (0 = all) | 0 |
fail_on_leak |
Fail if secrets found | true |
upload_sarif |
Upload to Security tab | false |
PR title validation for conventional commits.
name: PR Validation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate:
uses: tsok-org/.github/.github/workflows/pr-validate.yml@v1
with:
require_scope: false
auto_label: true| Type | Description |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation |
style |
Code style |
refactor |
Refactoring |
perf |
Performance |
test |
Tests |
build |
Build system |
ci |
CI/CD |
chore |
Maintenance |
revert |
Revert |
release |
Release |
| Input | Description | Default |
|---|---|---|
validate_title |
Validate PR title | true |
require_scope |
Require scope in title | false |
wip_allowed |
Allow WIP: prefix | true |
custom_types |
Additional types | - |
header_max_length |
Max title length | 100 |
auto_label |
Add labels by type | false |
Declarative environment configuration consumed by
actions/environment-setup. The authoritative type
spec is actions/environment-setup/schema.json (draft-07);
this section is the field-by-field reference.
.environment.yml is intended to grow. Today it has first-class support
for Rust, Node/TypeScript, Python, Go, and C/C++, plus
Terraform, Docker, service containers, and ad-hoc apt packages via
system_packages. Adding a new language means extending
schema.json + parse-config.sh +
action.yml — the caller's uses: …/environment-setup@v1
shape stays the same.
| Field | Type | Purpose |
|---|---|---|
version |
string, required | Schema version. Only "1" today. |
node |
bool | { version, package_manager, install, cache, frozen_lockfile } |
Node.js + pnpm/npm/yarn |
python |
bool | string | { version, package_manager } |
Python + pip/poetry/uv |
rust |
{ cache, diagnostics, coverage, build_jobs, linker } |
Rust cache + CI knobs (toolchain comes from rust-toolchain.toml) |
go |
bool | string | { version, version_file, cache, modules } |
Go toolchain via actions/setup-go |
c |
{ toolchain, cmake, pkg_config, packages[] } |
C/C++ toolchain (gcc | clang) + apt packages |
terraform |
bool | string | { version } |
Terraform CLI |
terragrunt |
bool | string | { version } |
Terragrunt alongside Terraform |
tflint |
bool | { version } |
TFLint |
docker |
bool | { buildx, platforms, registry } |
Docker + buildx |
services |
object | Service containers (postgres, redis, …) |
system_packages |
string[] | Additional apt packages on Linux |
Every field is optional except version.
# .environment.yml
version: "1"
node:
version: .node-version # or "20", "lts/*", "20.11.0"
package_manager: pnpm
install: true
cache: true
frozen_lockfile: trueversion: "1"Required. Pinned to "1" today; bumped on breaking schema changes.
node:
version: .node-version # explicit ("20") or file reference (".node-version")
package_manager: pnpm # pnpm | npm | yarn
install: true # run pkg-mgr install step; default true
cache: true # built-in setup-node cache; default true
frozen_lockfile: true # fail on lockfile drift; default trueShorthand: node: true enables with defaults.
python:
version: "3.12"
package_manager: uv # pip | poetry | uvShorthands: python: true (→ 3.12 + pip), python: "3.11".
rust:
cache: true # Swatinem/rust-cache for target/
diagnostics: true # rustup show, cargo env, connectivity smoke test
coverage: true # installs cargo-llvm-cov (Linux only)
build_jobs: 1 # pin CARGO_BUILD_JOBS (serial link for small runners)
linker: mold # lld | mold — alternative linkersToolchain selection lives in rust-toolchain.toml at the repo root;
rustup auto-installs on first cargo invocation. This block only covers
caching, diagnostics, and build knobs.
When rust is enabled, the action also validates CARGO_TERM_VERBOSE,
CARGO_TERM_COLOR, and RUST_BACKTRACE env vars emitted by the calling
workflow — empty strings fail fast instead of confusing cargo downstream.
go:
version: "1.22" # or "1.22.3"
version_file: go.mod # or ".go-version"
cache: true # module + build cache; default true
modules: true # GO111MODULE; default trueShorthands: go: true (latest), go: "1.22", go: { version_file: go.mod }.
Installed via actions/setup-go@v5.
c:
toolchain: clang # gcc | clang
cmake: true
pkg_config: true
packages: # additive to top-level system_packages
- libssl-dev
- libcurl4-openssl-devLinux only. gcc installs build-essential; clang installs clang
and lld. Use this (rather than raw system_packages) to express
what toolchain you want — the action derives the apt package set.
terraform: "1.12.2" # or true (latest), or { version: "..." }
terragrunt: "0.68.0" # only installed when terraform is enabled
tflint: true # or { version: "0.50.0" }docker:
buildx: true
platforms:
- linux/amd64
- linux/arm64
registry: ghcr.io # overridden by DOCKER_REGISTRY env if setCredentials come from the calling workflow's env block
(DOCKER_USERNAME, DOCKER_PASSWORD), not the config file.
services:
postgres:
image: postgres:16-alpine # optional — sensible default per service name
port: 5432
env:
POSTGRES_USER: test
redis:
image: redis:7-alpine
port: 6379Keys are user-chosen names. Auth env (POSTGRES_USER, POSTGRES_PASSWORD,
POSTGRES_DB, REDIS_PASSWORD, MYSQL_*) comes from the calling
workflow's env and overrides values in the config.
Recognised service defaults: postgres, redis, nats, mysql, mongo.
Unknown names default to <name>:latest.
system_packages:
- libssl-dev
- pkg-configInstalled via apt-get install on Linux runners; warned-and-skipped on
macOS/Windows. Typical use: C/C++ headers for Rust *-sys crates. For
structured intent (toolchain + cmake/pkg-config), prefer c:.
.environment.yml is validated against
schema.json (JSON Schema draft-07) by
check-jsonschema
on every run, before the parser runs. Misconfigured files fail fast
with a field-level error rather than mysterious parser drift.
On failure the step surfaces:
::error::.environment.yml failed schema validation.
─── .environment.yml ─────────────────────────────────────
version: "1"
rust:
linker: gold # <-- invalid: enum [lld, mold]
─── schema: .../schema.json ──────────────────────────────
(see errors above for the specific field)
No config file? The step exits cleanly — the parser has its own default
path and a repo without .environment.yml is a valid no-op.
For the authoritative type spec, including every field's type,
constraints, and description, read schema.json directly.
GitHub Apps provide short-lived tokens that can bypass branch protection.
-
Create a GitHub App with:
- Repository permissions: Contents (read/write), Pull requests (read/write)
- Organization permissions: Members (read) if needed
-
Add to repository:
vars.GITHUB_APP_ID- App IDsecrets.GITHUB_APP_PRIVATE_KEY- Private key (PEM)
Use for simpler setups without branch protection bypass:
secrets:
github_token: ${{ secrets.GITHUB_TOKEN }}Service credentials should be passed as environment variables:
| Variable | Description |
|---|---|
DOCKER_REGISTRY |
Registry URL |
DOCKER_USERNAME |
Registry username |
DOCKER_PASSWORD |
Registry password |
POSTGRES_USER |
PostgreSQL user |
POSTGRES_PASSWORD |
PostgreSQL password |
POSTGRES_DB |
Database name |
REDIS_PASSWORD |
Redis password |
NPM_TOKEN |
npm auth token |
MIT License - see LICENSE for details.