Shared GitHub Actions composite actions and reusable workflows for the labrats-work organization.
Provide standardized, reusable CI/CD building blocks so every repository in the organization can build container images, create semantic releases, and perform housekeeping tasks without duplicating workflow logic.
Composite actions are referenced by directory name:
steps:
- uses: labrats-work/actions.common/buildx-build@main
with:
context: .
destination: ghcr.io/labrats-work/my-app:latest
registry-token: ${{ secrets.GITHUB_TOKEN }}Reusable workflows are referenced by file path:
jobs:
build:
uses: labrats-work/actions.common/.github/workflows/build-image-buildx.yml@main
with:
context: .
image-name: ghcr.io/labrats-work/my-app
tag: "1.0.0"
secrets:
registry-token: ${{ secrets.GITHUB_TOKEN }}Composite actions live in top-level directories and are called as labrats-work/actions.common/<name>@main.
| Action | Description | ARC-native | Outputs |
|---|---|---|---|
| buildx-build | Build and push container images using Docker buildx. Polls for Docker daemon sidecar, creates docker-container driver builder. |
Yes | -- |
| kaniko-build | Build and push using docker/build-push-action and marketplace Buildx setup. Deprecated -- use buildx-build instead. |
No | -- |
| publish-image | High-level version-tagged publish. Extracts version from tag (supports prefixes like sidecar-), builds, pushes <image>:<version> and <image>:latest. Uses marketplace Docker actions. |
No | version |
| publish-image-buildx | Same as publish-image but delegates the build to buildx-build. | Yes | version |
| semver-release | Automatic semantic versioning from conventional commits. Reads git log since last tag, determines bump (feat! = major, feat: = minor, else patch), creates a GitHub release. Supports tag prefixes and path scoping. |
-- | version, tag, created |
| submit-ai-job | Submit a job to the ai-agents API (POST /api/jobs/submit) and optionally poll until it reaches a terminal status. |
Yes | job-id, status, result, failed-reason |
buildx-build
| Input | Required | Default | Description |
|---|---|---|---|
context |
Yes | -- | Build context path |
dockerfile |
No | Dockerfile |
Dockerfile path relative to context |
destination |
No | "" |
Image destination (ghcr.io/org/repo:tag) |
extra-destinations |
No | "" |
Additional destinations (comma-separated) |
push |
No | true |
Push to registry |
registry-token |
No | "" |
Registry auth token |
registry-user |
No | "" |
Registry username (defaults to github.actor) |
cache-repo |
No | "" |
Registry path for buildx cache |
build-args |
No | "" |
Build arguments (KEY=VALUE, one per line) |
publish-image / publish-image-buildx
| Input | Required | Default | Description |
|---|---|---|---|
tag |
Yes | -- | Release tag or manual version (e.g. sidecar-0.2.0) |
tag-prefix |
No | "" |
Prefix to strip from tag to get version |
context |
Yes | -- | Docker build context path |
dockerfile |
No | Dockerfile |
Dockerfile path relative to context |
image-name |
Yes | -- | Full image name (e.g. ghcr.io/labrats-work/apps.my-diet) |
build-args |
No | "" |
Docker build args (multiline KEY=VALUE) |
cache-repo |
No | "" |
Registry path for layer cache |
registry-token |
Yes | -- | GHCR push token |
Output: version -- the extracted version without prefix or v.
submit-ai-job
| Input | Required | Default | Description |
|---|---|---|---|
api-url |
No | https://ai-agents.hcl.labrats.work |
Base URL of the ai-agents API |
api-key |
Yes | -- | API key (Bearer token). Pass as a secret. |
source |
No | github-actions |
Origin identifier stored on the job |
type |
Yes | -- | Job type / agent trigger name (e.g. implement-issue) |
prompt |
No | "" |
Prompt for the agent (required unless type=error-reporting) |
agent-role |
No | "" |
Agent role trigger name |
priority |
No | "" |
Queue priority (lower = higher); empty = server default |
git-repo |
No | "" |
Git clone URL for the workspace |
git-ref |
No | "" |
Branch/tag to checkout |
git-depth |
No | "" |
Shallow clone depth |
model |
No | "" |
Model override (sonnet, opus, ...) |
effort |
No | "" |
Effort override (low, medium, high) |
provider |
No | "" |
Provider override (claude, codex) |
timeout-ms |
No | "" |
Per-job execution timeout in ms |
callback-url |
No | "" |
Webhook URL for lifecycle callbacks |
metadata |
No | "" |
JSON object string of arbitrary metadata |
workspace-files |
No | "" |
JSON array of {name, path} or {name, content} entries; the action base64-encodes them into the API's workspaceFiles[] |
extra-payload |
No | "" |
JSON object string deep-merged into the request body (advanced fields like pipeline, errors) |
wait |
No | true |
Poll until the job reaches a terminal status |
wait-timeout-seconds |
No | 1800 |
Max wait time when wait=true |
poll-interval-seconds |
No | 10 |
Poll interval when wait=true |
fail-on-job-failure |
No | true |
Fail the step if the job ends in failed/cancelled |
Outputs: job-id, status (when waiting), result (JSON returnvalue), failed-reason.
name: ai-implement-issue
on:
workflow_dispatch:
inputs:
issue:
description: Issue number
required: true
jobs:
run:
runs-on: k8s-hetzner-arc
steps:
- uses: labrats-work/actions.common/submit-ai-job@main
id: ai
with:
api-key: ${{ secrets.AI_AGENTS_API_KEY }}
type: implement-issue
prompt: "Implement the feature described in issue #${{ inputs.issue }}"
git-repo: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git-ref: ${{ github.ref_name }}
metadata: '{"issueNumber": ${{ inputs.issue }}}'
- run: echo "job=${{ steps.ai.outputs.job-id }} status=${{ steps.ai.outputs.status }}"Cron-driven jobs are a natural fit: pin a model, give the agent a long timeout, and don't block the runner waiting on it (use wait: false plus callback-url, or accept the long wait if your runner pool is sized for it).
name: nightly-dependency-audit
on:
schedule:
- cron: "0 3 * * *" # 03:00 UTC daily
jobs:
audit:
runs-on: k8s-hetzner-arc
steps:
- uses: labrats-work/actions.common/submit-ai-job@main
with:
api-key: ${{ secrets.AI_AGENTS_API_KEY }}
type: dependency-audit
model: opus
effort: high
timeout-ms: "1800000" # 30 min
wait-timeout-seconds: "2100" # poll a bit longer than the job timeout
git-repo: https://github.com/${{ github.repository }}.git
git-ref: main
prompt: |
Audit package.json + lockfiles. Flag CVEs, abandoned packages, and
stale majors. Open one issue per finding using `gh issue create`.
metadata: '{"trigger": "nightly", "repo": "${{ github.repository }}"}'Don't block the PR check on the agent. Submit with wait: false and let the agent post results back via a callback.
name: ai-pr-review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: k8s-hetzner-arc
steps:
- uses: labrats-work/actions.common/submit-ai-job@main
with:
api-key: ${{ secrets.AI_AGENTS_API_KEY }}
type: code-review
wait: "false"
git-repo: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git-ref: ${{ github.event.pull_request.head.sha }}
prompt: "Review PR #${{ github.event.pull_request.number }} and comment with findings."
callback-url: ${{ secrets.PR_REVIEW_CALLBACK_URL }}
metadata: |
{
"prNumber": ${{ github.event.pull_request.number }},
"headSha": "${{ github.event.pull_request.head.sha }}"
}Run the same agent against several repos on a schedule. Each repo becomes one job; submit them in parallel with a matrix.
name: weekly-readme-sweep
on:
schedule:
- cron: "0 9 * * 1" # Mondays 09:00 UTC
jobs:
sweep:
runs-on: k8s-hetzner-arc
strategy:
fail-fast: false
matrix:
repo:
- labrats-work/apps.my-budget
- labrats-work/apps.my-diet
- labrats-work/apps.my-cams
steps:
- uses: labrats-work/actions.common/submit-ai-job@main
with:
api-key: ${{ secrets.AI_AGENTS_API_KEY }}
type: docs-refresh
git-repo: https://github.com/${{ matrix.repo }}.git
git-ref: main
prompt: "Refresh README.md so it matches the current code. Open a PR if anything changes."
metadata: '{"sweep": "weekly-readme", "repo": "${{ matrix.repo }}"}'Use workspace-files to inject files into the agent's workspace. Each entry takes either a path (read from the runner's filesystem) or inline content — the action base64-encodes them for the API.
- uses: actions/checkout@v4
- uses: labrats-work/actions.common/submit-ai-job@main
with:
api-key: ${{ secrets.AI_AGENTS_API_KEY }}
type: code-review
prompt: "Review the repo using the constraints in context.md."
git-repo: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git-ref: ${{ github.ref_name }}
workspace-files: |
[
{ "name": "context.md", "path": "./prompts/context.md" },
{ "name": "BRIEF.md", "content": "Refactor the auth module. Keep the public API stable." }
]For pipelines or other advanced fields, combine with extra-payload:
- uses: labrats-work/actions.common/submit-ai-job@main
with:
api-key: ${{ secrets.AI_AGENTS_API_KEY }}
type: pdf-sections
prompt: "Extract chapter outlines from input/notes.pdf."
workspace-files: |
[{ "name": "input/notes.pdf", "path": "./uploads/notes.pdf" }]
extra-payload: '{"pipeline": {"type": "pdf-sections"}}'semver-release
| Input | Required | Default | Description |
|---|---|---|---|
tag-prefix |
No | "" |
Tag prefix (e.g. sidecar-). Empty = plain semver. |
paths |
No | "" |
Space-separated paths for git log scoping |
release-name-prefix |
No | Release |
Prefix for GitHub release name |
token |
Yes | -- | GitHub token with contents:write |
release-files |
No | "" |
Newline-separated files to attach to the release |
Outputs: version, tag, created.
Reusable workflows live in .github/workflows/ and are called as labrats-work/actions.common/.github/workflows/<file>@main.
| Workflow | Description | ARC-native |
|---|---|---|
| build-image.yml | Build and push a container image using marketplace Docker actions. Inputs: context, dockerfile, image-name, tag, extra-tags, build-args, cache-repo. Secret: registry-token. |
Yes |
| build-image-buildx.yml | Same as build-image but delegates to the buildx-build composite action. |
Yes |
| repo-info.yml | Retrieve and dump repository metadata (context, size). No inputs. | Yes |
| create-repo-issue.yml | Create an issue in the calling repository. Inputs: title, body. Secret: token. |
Yes |
| create-repo-issue-inherit.yml | Same as create-repo-issue but uses inherited GITHUB_TOKEN (no explicit secret). |
Yes |
| create-repo-issue-output.yml | Same as create-repo-issue-inherit with an issue-num output. |
Yes |
| verify-contrib-file.yml | Verify that CONTRIBUTING.md exists in the repository. No inputs. |
Yes |
| pr-version-preview.yml | PR-time bot comment showing the semver bump that release will produce on merge. Uses semver-release dry-run. Inputs: tag-prefix (optional), paths (optional). |
Yes |
| dev-smoke-test.yml | After a release publishes, waits for dev to roll, runs Playwright smoke spec, uploads evidence to the GitHub Release. Trigger via workflow_run (NOT release: published — GITHUB_TOKEN doesn't fire downstream release events). Inputs: base-url (required), version (optional), app-dir, playwright-project, max-wait-attempts. |
Yes |
| pr-image-cleanup.yml | Drops pr-<N>-* GHCR tags when a PR closes; backfill mode (workflow_dispatch) prunes tags for any closed PR. Inputs: packages (newline-separated), org (optional). |
Yes |
| release.yml | Internal workflow: creates a semver release on push to main using the semver-release action. |
Yes |
All workflows run on k8s-hetzner-arc self-hosted runners.
actions.common/
├── buildx-build/ # Composite: Docker buildx build (ARC-native)
│ └── action.yml
├── kaniko-build/ # Composite: Docker build (deprecated)
│ └── action.yml
├── publish-image/ # Composite: version-tagged publish (marketplace)
│ └── action.yml
├── publish-image-buildx/ # Composite: version-tagged publish (buildx)
│ └── action.yml
├── semver-release/ # Composite: conventional-commit releases
│ └── action.yml
├── .github/
│ └── workflows/
│ ├── build-image.yml
│ ├── build-image-buildx.yml
│ ├── repo-info.yml
│ ├── create-repo-issue.yml
│ ├── create-repo-issue-inherit.yml
│ ├── create-repo-issue-output.yml
│ ├── verify-contrib-file.yml
│ ├── release.yml
│ └── *-test.yml # Test workflows
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
└── .gitignore
- Maximum 4 levels of workflow nesting.
- Maximum 20 reusable workflows per caller (nested workflows count toward this limit).
- Environment variables do not propagate to called workflows -- pass them as inputs or secrets.
- Private repositories can only reference workflows within the same repository.
See the GitHub documentation on reusable workflows for full details.
See CONTRIBUTING.md for guidelines on adding workflows and actions.
MIT License -- see LICENSE for details.