# GitHub Actions runner templates

Install the Runzivo GitHub App, select a private repository owned by a GitHub organization, then use the selected template's published runner label.

## Write a template

### Node.js

Install locked npm dependencies, test, and build.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: package.json and package-lock.json.

Commit package-lock.json before using the npm dependency cache.

```yaml
name: Node.js

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: nodejs-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: package-lock.json
      - run: npm ci
      - run: npm test
      - run: npm run build
```

### Python

Install requirements and run pytest.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: requirements.txt and tests.

```yaml
name: Python

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: python-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
        with:
          python-version: "3.13"
          cache: pip
          cache-dependency-path: requirements.txt
      - run: python -m pip install --requirement requirements.txt
      - run: python -m pytest
```

### Go

Vet, test, and build a Go module.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: go.mod and go.sum.

```yaml
name: Go

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: go-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
        with:
          go-version-file: go.mod
          cache: true
      - run: go vet ./...
      - run: go test ./...
      - run: go build ./...
```

### Java with Maven

Build and test a Maven project with its committed Maven Wrapper.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: pom.xml, mvnw, and .mvn/wrapper/.

Preserve the executable bit on mvnw.

```yaml
name: Java with Maven

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: java-maven-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
        with:
          distribution: temurin
          java-version: "21"
          cache: maven
      - run: ./mvnw --batch-mode verify
```

### Docker build and GHCR publish

Build pull requests and publish only from pushes to main.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required file: Dockerfile.

```yaml
name: Docker build and GHCR publish

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: docker-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  build:
    if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
      - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
        with:
          context: .
          push: false
          cache-from: type=gha
          cache-to: type=gha,mode=max
  publish:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 25
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
      - id: metadata
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: ghcr.io/${{ github.repository }}
      - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
        with:
          context: .
          push: true
          tags: ${{ steps.metadata.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

### Terraform

Plan pull requests and apply main after you configure required environment reviewers.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required directory: terraform/.

Configure a remote backend, provider credentials, and the terraform-apply environment with required reviewers before using apply.

Commit .terraform.lock.hcl whenever the configuration uses providers.

For a private repository, confirm the GitHub plan supports required environment reviewers before relying on the gate.

Protect main so reviewed changes are required before a push can reach apply.

An environment name alone is not an approval gate.

The apply job plans again before applying, so it applies what main holds at that moment, not the pull request's earlier plan.

Give the pull request plan job read-only cloud credentials; keep write credentials only on the terraform-apply environment.

```yaml
name: Terraform

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: terraform-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  validate:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 15
    defaults:
      run:
        working-directory: terraform
    env:
      TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform.d/plugin-cache
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
        with:
          terraform_version: 1.16.7
          terraform_wrapper: false
      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: .terraform.d/plugin-cache
          key: terraform-${{ runner.os }}-${{ hashFiles('terraform/.terraform.lock.hcl', 'terraform/*.tf') }}
      - run: mkdir -p "$TF_PLUGIN_CACHE_DIR"
      - run: terraform fmt -check -recursive
      - run: terraform init -backend=false -input=false -lockfile=readonly
      - run: terraform validate
  plan:
    if: github.event_name == 'pull_request'
    needs: validate
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 15
    defaults:
      run:
        working-directory: terraform
    env:
      TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform.d/plugin-cache
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
        with:
          terraform_version: 1.16.7
          terraform_wrapper: false
      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: .terraform.d/plugin-cache
          key: terraform-${{ runner.os }}-${{ hashFiles('terraform/.terraform.lock.hcl', 'terraform/*.tf') }}
      - run: mkdir -p "$TF_PLUGIN_CACHE_DIR"
      - run: terraform init -input=false -lockfile=readonly
      - run: terraform plan -input=false -lock=false
  apply:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: validate
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 20
    environment: terraform-apply
    defaults:
      run:
        working-directory: terraform
    env:
      TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform.d/plugin-cache
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
        with:
          terraform_version: 1.16.7
          terraform_wrapper: false
      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: .terraform.d/plugin-cache
          key: terraform-${{ runner.os }}-${{ hashFiles('terraform/.terraform.lock.hcl', 'terraform/*.tf') }}
      - run: mkdir -p "$TF_PLUGIN_CACHE_DIR"
      - run: terraform init -input=false -lockfile=readonly
      - run: terraform apply -auto-approve -input=false
```

### Ansible

Lint, syntax-check, and check-mode a local playbook.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: ansible/requirements-ci.txt, inventory.yml, and playbook.yml.

```yaml
name: Ansible

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: ansible-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  validate:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 20
    defaults:
      run:
        working-directory: ansible
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
        with:
          python-version: "3.13"
          cache: pip
          cache-dependency-path: ansible/requirements-ci.txt
      - run: python -m pip install --requirement requirements-ci.txt
      - run: ansible-lint playbook.yml
      - run: ansible-playbook --inventory inventory.yml --syntax-check playbook.yml
      - run: ansible-playbook --inventory inventory.yml --check playbook.yml
```

### Rust

Format-check, lint, test, and build a Cargo project.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: Cargo.toml and Cargo.lock.

Commit Cargo.lock before using the Cargo cache.

```yaml
name: Rust

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: rust-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
        with:
          toolchain: stable
          components: rustfmt, clippy
      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            target/
          key: rust-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
      - run: cargo fmt --check
      - run: cargo clippy --all-targets -- -D warnings
      - run: cargo test
      - run: cargo build --release
```

### .NET

Restore, build, and test a .NET solution.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: global.json and a solution.

```yaml
name: .NET

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: dotnet-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
        env:
          DOTNET_INSTALL_DIR: ${{ runner.temp }}/dotnet
        with:
          global-json-file: global.json
      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: ~/.nuget/packages
          key: dotnet-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'global.json') }}
      - run: dotnet restore
      - run: dotnet build --no-restore --configuration Release
      - run: dotnet test --no-build --configuration Release
```

### Reusable Go workflow

Call a reusable Go validation workflow.

rzv-ubuntu-24.04-x64-2vcpu-8gb

Required files: go.mod and go.sum.

Copy the selected file into .github/workflows/ before calling it from another workflow.

```yaml
name: Reusable Go validation

on:
  workflow_call:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: reusable-go-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  test:
    runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gb
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
        with:
          go-version-file: go.mod
          cache: true
      - run: go vet ./...
      - run: go test ./...
      - run: go build ./...
```
