Reusable GitHub Actions and workflows for the dallay organization.
This repository provides composite actions and reusable workflows that standardize CI/CD patterns across all repositories in the organization. Instead of duplicating workflow logic, every repo calls these shared building blocks.
common-actions/
├── actions/ # Composite Actions
│ ├── setup-node/action.yml # Node.js + pnpm setup with caching
│ ├── setup-java/action.yml # Java + Gradle setup with caching
│ ├── setup-rust/action.yml # Rust toolchain + caching
│ ├── setup-python/action.yml # Python + pip/uv caching
│ ├── setup-buildx/action.yml # QEMU + Docker Buildx for multi-arch
│ ├── docker-build-push/action.yml # Multi-registry Docker build & push
│ ├── docker-security-scan/action.yml # Trivy vulnerability scanning + SARIF
│ ├── codecov/action.yml # Upload coverage to Codecov
│ └── playwright-test/action.yml # Playwright E2E tests with caching
│
└── .github/workflows/ # Workflows (reusable + local)
├── semantic-pr.yml # Lint PR titles (Conventional Commits)
├── gitguardian-scan.yml # Scan repository history for exposed secrets
├── pr-labeler.yml # Path-based PR labeling
├── issue-labeler.yml # Keyword-based issue labeling
├── labels-sync.yml # Sync labels from YAML config
├── semantic-release.yml # Semantic Release with GitHub App
├── stale.yml # Close stale issues/PRs
├── greetings.yml # Welcome first-time contributors
├── cleanup-cache.yml # Cleanup caches for closed branches
├── cache-maintenance.yml # Cleanup caches by age/type (workflow_call)
├── dependabot-auto-merge.yml # Auto-merge Dependabot PRs (workflow_call)
├── release.yml # Release this repository
├── pr-size-labeler.yml # Label PRs by size (XS, S, M, L, XL)
└── test-actions.yml # Smoke-test all composite actions
| Feature | Composite Action | Reusable Workflow |
|---|---|---|
| Scope | Steps within a job | Entire jobs |
| Secrets | Inherits caller context | Must be passed explicitly |
| Logging | Shows as single step | Each job/step logged individually |
| Use case | Setup tools, build steps | Standalone processes |
Sets up Node.js and pnpm with caching and dependency installation.
steps:
- uses: actions/checkout@v6
- uses: dallay/common-actions/actions/setup-node@v1
with:
node-version: "24.12.0" # optional, this is the default
# node-version-file: ".nvmrc" # alternative: read from file
# cache-dependency-path: "pnpm-lock.yaml" # optional: override lockfile path
# cwd: "./frontend" # optional: working directory for install
# install-deps: "true" # optional: run pnpm install (default: true)
# ignore-scripts: "true" # optional: add --ignore-scripts to pnpm installSets up QEMU and Docker Buildx for multi-platform container builds.
steps:
- uses: dallay/common-actions/actions/setup-buildx@v1
# with:
# buildx-version: "v0.12.0" # optional
# buildkit-image: "moby/buildkit:v0.12.5" # optionalBuilds and pushes Docker images to both GHCR and Docker Hub with multi-arch support and registry-based caching.
steps:
- uses: dallay/common-actions/actions/setup-buildx@v1
- uses: dallay/common-actions/actions/docker-build-push@v1
with:
image-name: my-app
image-title: My App
image-description: "My application"
dockerfile: ./Dockerfile
github-owner: ${{ github.repository_owner }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
github-token: ${{ secrets.GITHUB_TOKEN }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
version: "1.2.3"
major-version: "1"
# platforms: "linux/amd64,linux/arm64" # optional, this is the default
# build-args: | # optional
# VERSION=1.2.3Outputs: tags, digest, image-id
Sets up Java JDK and Gradle with dependency caching.
steps:
- uses: actions/checkout@v6
- uses: dallay/common-actions/actions/setup-java@v1
with:
java-version: "24" # optional, this is the default
distribution: "temurin" # optional (temurin, corretto, zulu, etc.)
# cache: "gradle" # optional: gradle (default), maven, sbt, or "" to disableSets up Rust toolchain with optional components, cross-compile targets, and Swatinem/rust-cache.
Supports optional sccache for distributed compilation.
steps:
- uses: actions/checkout@v6
- uses: dallay/common-actions/actions/setup-rust@v1
with:
rust-version: "stable" # optional (stable, nightly, beta, or semver)
# components: "clippy,rustfmt" # optional, this is the default
# targets: "wasm32-unknown-unknown" # optional: cross-compile targets
# cache: "true" # optional: Swatinem/rust-cache
# cache-workspaces: "" # optional: extra paths for cache key
# sccache: "false" # optional: distributed compilationSets up Python with pip or uv caching and optional dependency installation.
steps:
- uses: actions/checkout@v6
- uses: dallay/common-actions/actions/setup-python@v1
with:
python-version: "3.12" # optional, this is the default
cache: "" # optional: "pip", "uv", or empty (default; disables caching)
# cache-dependency-path: "requirements.txt" # required for pip caching; uv paths are relative to working-directory
# install-deps: "true" # optional: install requirements.txt / pyproject.toml
# working-directory: "." # optionalUploads code coverage reports to Codecov with graceful failure handling. Skips upload if no token is provided.
steps:
- uses: dallay/common-actions/actions/codecov@v1
with:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
files: "**/build/reports/jacoco/test/jacocoTestReport.xml"
flags: backend
name: backend-coverage
slug: owner/repo
# fail-ci-if-error: "false" # optional
# fail-on-upload-error: "false" # optional
# verbose: "false" # optionalOutputs: upload-failed (boolean string)
Runs Playwright E2E tests with intelligent browser caching, artifact uploads, and step summary.
Prerequisite: Node.js and pnpm must be set up before using this action. Use
actions/setup-nodefirst.
steps:
- uses: actions/checkout@v6
- uses: dallay/common-actions/actions/setup-node@v1
- uses: dallay/common-actions/actions/playwright-test@v1
with:
browser: chromium # optional: chromium (default), firefox, webkit, or all
# working-directory: "." # optional: directory with playwright config
# test-command: "test:e2e" # optional: pnpm script to run
# headed: "false" # optional
# upload-results: "true" # optional: upload report + traces
# retention-days: "30" # optionalOutputs: tests-status (success or failed), report-path
Scans Docker images for vulnerabilities using Trivy. Uploads SARIF reports to the GitHub Security tab and saves results as artifacts.
Runner note: If you upgrade
github/codeql-action/upload-sariforactions/upload-artifactto versions that use upload-artifact v6, ensure self-hosted runners are >= 2.327.1 and Node.js 24 is available.
steps:
- uses: dallay/common-actions/actions/docker-security-scan@v1
with:
image-ref: ghcr.io/owner/repo/my-app:latest
report-name: my-app-security
category: my-app-trivy
# severity: "HIGH,CRITICAL" # optional
# fail-on-error: "true" # optional: fail pipeline on vulnerabilities (default: true)Outputs: scan-result (success, failed, or error), sarif-file
Tip: Most reusable workflows accept an optional
github-tokensecret to override the defaultGITHUB_TOKEN(e.g., use a GitHub App token). The semantic-release workflow uses its own GitHub App credentials.
Runs GitGuardian's ggshield secret scan. The calling repository chooses the trigger events and
must define a GITGUARDIAN_API_KEY Actions secret.
# .github/workflows/gitguardian-scan.yml in the consuming repository
name: GitGuardian scan
on: [ push, pull_request ]
permissions:
contents: read
jobs:
scan:
uses: dallay/common-actions/.github/workflows/gitguardian-scan.yml@v1
secrets:
gitguardian-api-key: ${{ secrets.GITGUARDIAN_API_KEY }}The reusable workflow checks out the full history and pins GitGuardian/ggshield to a commit SHA.
GitHub does not provide repository secrets to workflows triggered by pull requests from forks, so
this scan cannot authenticate for those events with the repository secret. Do not switch to
pull_request_target and check out untrusted PR code to work around that restriction; use
GitGuardian's GitHub integration if fork coverage is required.
Validates PR titles follow Conventional Commits. Posts a helpful sticky comment on failure.
# .github/workflows/semantic-pr.yml
name: Lint PR title
on:
pull_request_target:
types: [ opened, edited, synchronize ]
jobs:
lint:
uses: dallay/common-actions/.github/workflows/semantic-pr.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional override
# with:
# skip-bot: "dependabot[bot]" # optional, this is the defaultAuto-labels PRs based on changed file paths. Requires a .github/labeler.yml config in your repo.
# .github/workflows/pr-labeler.yml
name: PR Labeler
on:
pull_request_target:
types: [ opened, synchronize, reopened ]
jobs:
label:
uses: dallay/common-actions/.github/workflows/pr-labeler.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional override
# with:
# sync-labels: true # optional
# configuration-path: ".github/labeler.yml" # optionalAuto-labels issues based on title/body keywords. Requires a .github/issue-labeler-config.yml in
your repo.
# .github/workflows/issue-labeler.yml
name: Issue Labeler
on:
issues:
types: [ opened, edited ]
jobs:
label:
uses: dallay/common-actions/.github/workflows/issue-labeler.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional overrideSynchronizes GitHub labels from a YAML config file. Requires a .github/labels.yml in your repo.
# .github/workflows/labels-sync.yml
name: Sync Labels
on:
push:
branches: [ main ]
paths: [ ".github/labels.yml" ]
workflow_dispatch:
jobs:
sync:
uses: dallay/common-actions/.github/workflows/labels-sync.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional overrideRuns semantic-release with a GitHub App token for authentication. Outputs version info for downstream jobs.
# .github/workflows/release.yml
name: Release
on:
push:
branches: [ main ]
jobs:
release:
uses: dallay/common-actions/.github/workflows/semantic-release.yml@v1
secrets:
app-id: ${{ secrets.APP_ID }}
app-private-key: ${{ secrets.APP_PRIVATE_KEY }}
# with:
# node-version: "24.12.0"
# dry-run: false
# install-deps: true
# semantic-release-command: "pnpm exec semantic-release"
# Use release outputs in downstream jobs
deploy:
needs: release
if: needs.release.outputs.new-release-published == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying version ${{ needs.release.outputs.new-release-version }}"Outputs: new-release-published, new-release-version, new-release-major-version,
new-release-minor-version, new-release-patch-version, new-release-git-tag
This repo ships a local release workflow at
.github/workflows/release.ymlthat calls the reusable semantic-release workflow usingnpx semantic-releaseand the.releaserc.jsonconfig.
Marks inactive issues and PRs as stale, then closes them after a grace period.
# .github/workflows/stale.yml
name: Stale
on:
schedule:
- cron: "0 2 * * *"
jobs:
stale:
uses: dallay/common-actions/.github/workflows/stale.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional override
# with:
# days-before-stale: 120
# days-before-close: 60
# exempt-issue-labels: "priority:high,security"Posts a welcome message for first-time issue creators and PR authors.
# .github/workflows/greetings.yml
name: Greetings
on: [ pull_request, issues ]
jobs:
greet:
uses: dallay/common-actions/.github/workflows/greetings.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional override
# with:
# issue-message: "Thanks for opening this issue!"
# pr-message: "Thanks for your contribution!"Generates a quality report for PR contributors based on merge rate, account age, and reactions.
# .github/workflows/contributor-report.yml
name: Contributor Report
on:
pull_request:
types: [ opened, reopened, synchronize, edited ]
jobs:
report:
uses: dallay/common-actions/.github/workflows/contributor-report.yml@v1
secrets: inherit
# with:
# threshold-pr-merge-rate: '0.3'
# threshold-account-age: '30'
# on-fail: 'comment'Removes GitHub Actions caches associated with a closed PR's branch.
# .github/workflows/cleanup-cache.yml
name: Cleanup caches
on:
pull_request:
types: [ closed ]
jobs:
cleanup:
uses: dallay/common-actions/.github/workflows/cleanup-cache.yml@v1
secrets: inherit
# secrets:
# github-token: ${{ secrets.GITHUB_TOKEN }} # optional override
# with:
# pr-number: ${{ github.event.pull_request.number }} # optional overrideCleans up GitHub Actions caches older than a configurable retention period. Supports filtering by cache type (pnpm, playwright, build, or all). Outputs a summary table with deletion counts.
Unlike
cleanup-cache.yml(which targets a specific PR branch), this workflow cleans all caches across the repository based on age and optional type prefix.
# .github/workflows/cache-maintenance.yml
name: Cache Maintenance
on:
workflow_dispatch:
inputs:
retention-days:
description: "Delete caches older than this many days"
type: number
required: false
default: 7
cache-types:
description: "Which cache types to clean: pnpm, playwright, build, or all"
type: string
required: false
default: "all"
schedule:
- cron: "0 3 * * *" # Run daily at 3 AM
jobs:
cleanup:
uses: dallay/common-actions/.github/workflows/cache-maintenance.yml@v1
secrets: inherit
# with:
# retention-days: 7
# cache-types: "all"Automatically labels PRs based on their size (number of lines changed). Uses codelytv/pr-size-labeler to categorize PRs into XS, S, M, L, or XL.
# .github/workflows/pr-size-labeler.yml
name: PR Size Labeler
on:
pull_request_target:
types: [ opened, synchronize, reopened ]
workflow_call:
jobs:
label:
uses: dallay/common-actions/.github/workflows/pr-size-labeler.yml@v1
secrets: inheritSize thresholds (default):
| Label | Max changes |
|---|---|
size/xs |
10 |
size/s |
100 |
size/m |
500 |
size/l |
1000 |
size/xl |
> 1000 |
You can customize labels and thresholds by calling the action directly:
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: CodelyTV/pr-size-labeler@095a41fca88b8764fd9e008ad269bcdb82bb38b9 # v1.10.4
with:
xs_label: 'size/xs'
xs_max_size: '10'
l_label: 'size/l'
l_max_size: '1000'
fail_if_xl: 'true'
files_to_ignore: 'package-lock.json *.lock'Automatically approves and merges Dependabot (and Renovate) PRs once all checks pass. Supports GitHub App authentication for merged commits and posts sticky status comments on the PR.
# .github/workflows/dependabot-auto-merge.yml
name: Dependabot Auto-Merge
on:
workflow_call:
inputs:
target:
description: 'Merge strategy: squash, merge, or rebase'
required: false
type: string
default: 'squash'
approve:
description: 'Auto-approve the PR before merging'
required: false
type: boolean
default: true
secrets:
APP_ID:
description: 'GitHub App ID (optional)'
required: false
APP_PRIVATE_KEY:
description: 'GitHub App Private Key (optional)'
required: false
token:
description: 'Fallback GitHub token'
required: false
jobs:
auto-merge:
uses: dallay/common-actions/.github/workflows/dependabot-auto-merge.yml@v1
secrets: inherit
# with:
# target: squash
# approve: trueSends a single JSON POST to a full URL through a temporary Tailscale connection using OAuth or OIDC. OIDC remains the default for compatibility. The caller controls when to deploy, job dependencies, and the JSON body. An HTTP 2xx confirms only that the request was accepted: the workflow does not wait for or verify deployment completion.
Contract:
deployment-urlinput (optional): non-secret full HTTP/HTTPS URL, including a path and query if needed; no credentials, fragment, or spaces.deployment-urlsecret (optional): full HTTP/HTTPS URL for credential-bearing webhook URLs. When set, it takes precedence over the input. Do not put secret URL values inwithinputs.deployment-payloadinput (optional): non-secret valid JSON, sent without modification.deployment-payload-secretsecret (optional): valid JSON body for sensitive payloads. When set, it takes precedence over the input.api-key-header(optional): HTTP header name; defaults tox-api-key.extra-header-nameandextra-header-value(optional, non-secret): one additional static request header sent with the POST. Provide both or neither. Use it for endpoints that require a provider event header, not for credentials.tailscale-auth-mode(optional):oidc(default) oroauth.tailscale-client-idandtailscale-tagsinputs (required inoidcmode): the OAuth client ID and authorized Tailscale tags.tailscale-client-id-secretandtailscale-tags-secretsecrets (required inoauthmode): the OAuth client ID and authorized tags. When provided, these secrets take precedence over the matching inputs.tailscale-audience(required inoidcmode): the audience configured for the Tailscale federated identity provider. It is not used inoauthmode.tailscale-oauth-secretsecret (required inoauthmode): the OAuth client secret. It is not used inoidcmode.api-keysecret (optional): full header value, without line breaks. For an API that usesAuthorization, include the required prefix in the secret, for exampleBearer …. When omitted, the request does not include the API-key header.
OIDC inputs are visible in workflow metadata and must not contain secrets. OAuth credentials can be passed as GitHub secrets; the secret forms take precedence over matching inputs when both are provided. Secret values use GitHub masking and are not printed by the workflow scripts.
Consumer example (replace <IMMUTABLE_SHA> with a reviewed, real 40-character
commit SHA; the placeholder is not an executable reference):
name: Request private deployment
on:
workflow_dispatch:
permissions: { }
jobs:
deploy:
permissions:
id-token: write
uses: dallay/common-actions/.github/workflows/private-deploy.yml@<IMMUTABLE_SHA>
with:
tailscale-auth-mode: oidc
deployment-url: http://internal.service:8080/api/deploy
deployment-payload: '{"version":"1.2.3"}'
api-key-header: x-api-key
tailscale-client-id: ${{ vars.TAILSCALE_CLIENT_ID }}
tailscale-audience: ${{ vars.TAILSCALE_AUDIENCE }}
tailscale-tags: tag:ci-deploy
secrets:
# Optional when the endpoint does not require an API-key header
api-key: ${{ secrets.DEPLOYMENT_API_KEY }}
# Use this for webhook URLs that contain a credential in the path
deployment-url: ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}For OIDC, configure a Tailscale federated identity for the GitHub Actions OIDC
issuer and audience. Restrict trust to the caller's authorized repositories and
branches with token conditions. For OAuth, pass the client secret explicitly as
tailscale-oauth-secret; grant the OAuth client only the required tags and
network access. Do not authorize every repository or branch. Limit ACLs/grants to
the service destination and port. The service and its private DNS must be
reachable from the Tailscale network. Do not inherit all secrets.
OAuth caller example (use the same immutable SHA and endpoint inputs as above):
jobs:
deploy:
uses: dallay/common-actions/.github/workflows/private-deploy.yml@<IMMUTABLE_SHA>
with:
tailscale-auth-mode: oauth
deployment-url: http://internal.service:8080/api/deploy
secrets:
deployment-payload-secret: ${{ secrets.DEPLOYMENT_PAYLOAD }}
tailscale-client-id-secret: ${{ secrets.TAILSCALE_CLIENT_ID }}
tailscale-tags-secret: ${{ secrets.TAILSCALE_TAGS }}
tailscale-oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
deployment-url: ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}The OAuth job inside the reusable workflow does not receive id-token: write. GitHub validates
permissions across the complete reusable-workflow file, so the calling job must allow
id-token: write even when tailscale-auth-mode is oauth; the OAuth job itself keeps
permissions: {}.
The reusable workflow grants id-token: write only to its OIDC job. The calling
workflow must also grant that permission to the reusable-workflow job in OIDC mode.
OAuth mode does not use id-token: write at runtime. Both jobs use ubuntu-latest;
the deployment jobs have a five-minute timeout. It serializes requests by repository, caller
workflow, ref, and target
URL without canceling active requests. GitHub may replace pending runs; this is
not a FIFO queue or a global lock across workflows or branches.
The request has a 10-second connection timeout and a 60-second total timeout. It
does not follow redirects or retry the POST. The workflow discards the response
and curl diagnostics and does not print the URL, body, or secrets in script
output. HTTPS verifies TLS; HTTP is allowed inside the tunnel. A transport error
can occur after the service accepts the POST. Check the status before retrying
manually.
The Tailscale action is pinned by SHA and registers a post-job logout for normal cleanup, including when the POST fails. The node is ephemeral. Cleanup failure does not fail the job. Abrupt runner termination can prevent the post-job step; node removal then depends on Tailscale ephemeral-node cleanup, not on a guarantee that this workflow disconnects immediately.
Local verification, without requests to the private network:
node --test test/*.test.mjs
actionlint .github/workflows/private-deploy.yml- SHA Pinning: All third-party actions are pinned to full commit SHAs (not mutable tags)
- GitHub App Tokens: Used instead of PATs for release operations
- Minimal Permissions: Each workflow declares only the permissions it needs
- No Secrets in Logs: Secrets are never echoed or exposed in step summaries
- Fork Safety: Label sync workflows skip forks that lack permissions
This repository uses Semantic Versioning. Pin your usage to a major version tag:
uses: dallay/common-actions/actions/setup-node@v1 # Recommended: major version
uses: dallay/common-actions/actions/setup-node@v1.2.3 # Exact version
uses: dallay/common-actions/actions/setup-node@main # Latest (not recommended)- Create a feature branch
- Make your changes
- Test locally where possible
- Open a PR with a Conventional Commits title
- Request review
This project is open source under the MIT License.