Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Bazel Github Actions Ci

BSecurity

Use when setting up or reviewing Bazel CI on GitHub Actions. Covers

8 stars
0 votes
0 copies
0 views
Added 9/20/2026
developmentpythongojavakotlingitsecurity

Security Analysis

B85/100
highPerforms destructive filesystem operations

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add tstapler/dotfiles --skill bazel-github-actions-ci --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bazel Github Actions Ci?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Bazel Github Actions Ci
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/tstapler-bazel-github-actions-ci/badge)](https://www.skillsdirectory.com/skills/tstapler-bazel-github-actions-ci)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: bazel-github-actions-ci
description: Use when setting up or reviewing Bazel CI on GitHub Actions. Covers
  setup-bazel caching strategy, per-job disk-cache isolation, Compose Desktop X11
  support, KMP test aggregation, and security hardening. Based on SteleKit migration.
---

# Bazel CI on GitHub Actions

Battle-tested patterns from migrating a Kotlin Multiplatform project to Bazel CI.
Replaces the outdated `bazelbuild/setup-bazelisk + actions/cache` approach.

---

## Core: use `bazel-contrib/setup-bazel` — not manual caching

`setup-bazel@0.19.0` replaces both `bazelbuild/setup-bazelisk` and manual
`actions/cache` for Bazel caches. It manages three orthogonal caches:

| Input | What it caches | Notes |
|---|---|---|
| `disk-cache: <name>` | Compiled build outputs (action result cache) | Isolate per job |
| `repository-cache: true` | Downloaded Maven/artifact files (CAS store) | Share across jobs |
| `external-cache: true` | Processed external repos (Compose Desktop, AndroidX) | Share across jobs |
| `cache-save: <bool>` | Whether to write the cache back | PRs read, pushes write |

```yaml
- name: Set up Bazel
  uses: bazel-contrib/setup-bazel@0.19.0
  with:
    bazelisk-version: "1.x"
    disk-cache: jvm              # unique per job — prevents cross-job collisions
    repository-cache: true       # shared across all jobs (keyed by MODULE.bazel)
    external-cache: true         # Compose Desktop etc. are large; cache separately
    cache-save: ${{ github.event_name != 'pull_request' }}
```

**Why `cache-save: ${{ github.event_name != 'pull_request' }}`**: PRs read the cache
seeded by the main branch but never write back. This prevents a poisoned PR from
corrupting the shared cache. Pushes to main write a fresh cache for the next PR.

**Why per-job `disk-cache`**: JVM and Android compiled outputs are incompatible.
Sharing a disk cache between jobs that build different targets causes cache misses and
occasional corruption. Use distinct names: `jvm`, `android`, `android-tests`, `web`.

**`repository-cache` and `external-cache` are complementary, not redundant**:
- `repository-cache` = `~/.cache/bazel/cache/repos` — raw downloaded files (content-addressed)
- `external-cache` = processed external repo directories (e.g. `external/rules_jvm_external~maven/`)
Both are useful; enabling both is correct.

---

## Security baseline

Always add `permissions: contents: read` at the workflow level. Without it, the default
`GITHUB_TOKEN` has write access to contents, packages, and deployments — far more than
a test workflow needs.

```yaml
name: Bazel CI

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

permissions:
  contents: read     # minimum required to checkout code

concurrency:
  group: bazel-ci-${{ github.ref }}
  cancel-in-progress: true
```

---

## Skipping draft PRs

```yaml
jobs:
  bazel-jvm:
    if: github.event.pull_request.draft == false
```

**This is safe for both `push` and `pull_request` events.** On `push` events,
`github.event.pull_request` is `null`, so `github.event.pull_request.draft` is `null`.
GitHub Actions coerces `null` to `0` and `false` to `0` for `==`, so `null == false`
evaluates to `true` — the job runs. No special handling needed for push vs PR.

---

## JDK: JVM jobs don't need `setup-java`

Bazel downloads its own JDK toolchain via `--java_runtime_version=21` (from
`rules_java`). JVM/desktop jobs do **not** need `actions/setup-java`.

Android jobs **do** need `setup-java` because the Android SDK host tools (aapt2,
d8, etc.) run outside Bazel's hermetic environment and require a system JDK on `PATH`.

```yaml
# JVM job — NO setup-java needed (Bazel downloads the JDK itself)
bazel-jvm:
  steps:
    - uses: actions/checkout@v4
    - uses: bazel-contrib/setup-bazel@0.19.0
      with: { disk-cache: jvm, ... }
    - run: bazel test //kmp:jvm_tests

# Android job — setup-java IS needed for host tools
bazel-android:
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-java@v4
      with: { java-version: "21", distribution: temurin }
    - uses: android-actions/setup-android@v3
    - uses: bazel-contrib/setup-bazel@0.19.0
      with: { disk-cache: android, ... }
    - run: bazel build //kmp:android_app --config=android
```

---

## Compose Desktop / X11 on Linux CI

Compose Desktop tests open real windows and require an X display. The Bazel sandbox
creates an isolated mount namespace that excludes `/tmp/.X11-unix` by default, so even
with `DISPLAY` set, the X socket is unreachable.

### .bazelrc

```
# Mount the X11 socket directory into every test sandbox (Linux only).
# Locally: uses whatever X server is running.
# CI: xvfb-run creates the socket before Bazel runs.
test --sandbox_add_mount_pair=/tmp/.X11-unix
```

### BUILD.bazel test target

```python
kt_jvm_test(
    name = "jvm_tests",
    ...
    jvm_flags = [
        "-Djava.awt.headless=false",          # Compose Desktop requires headless=false
        "--add-opens=java.base/java.lang=ALL-UNNAMED",
    ],
    env = {
        "LIBGL_ALWAYS_SOFTWARE": "1",         # software OpenGL — no GPU needed on CI
    },
    env_inherit = ["DISPLAY"],                # required with --incompatible_strict_action_env
    tags = ["requires-display"],
)
```

### CI workflow step

```yaml
- name: Install Xvfb
  run: |
    sudo apt-get install -y --no-install-recommends xvfb
    # Create the directory before Bazel validates the sandbox mount source.
    # xvfb-run creates the socket, but Bazel checks the directory at startup.
    mkdir -p /tmp/.X11-unix

- name: Run JVM tests
  run: |
    xvfb-run --auto-servernum bazel test //kmp:jvm_tests \
      --test_output=summary
```

**Why `mkdir -p /tmp/.X11-unix` before the test command**: Bazel validates
`--sandbox_add_mount_pair` mount sources at startup. If the directory doesn't exist,
Bazel refuses to start the test. `xvfb-run` creates the socket inside the directory
but the directory itself must pre-exist.

---

## Test aggregation: `test_suite` not `alias`

`alias()` does not forward the test provider — `bazel test //kmp:jvm_tests` will silently
build without running any tests if `jvm_tests` is an `alias`.

```python
# kmp/BUILD.bazel — CORRECT
test_suite(
    name = "jvm_tests",
    tests = ["//kmp/src/jvmTest/kotlin:jvm_tests"],
)

# WRONG — test provider is not forwarded
alias(
    name = "jvm_tests",
    actual = "//kmp/src/jvmTest/kotlin:jvm_tests",
)
```

---

## JUnit4 Suite aggregator (Bazel's `test_class` requirement)

Bazel's `kt_jvm_test` runner needs a real JUnit4 class as `test_class`. Kotlin test
with `@RunWith(Suite.class)` is the correct aggregator pattern.

```kotlin
@RunWith(Suite::class)
@Suite.SuiteClasses(
    FooTest::class,
    BarTest::class,
    // ... all test classes in the source set
)
class AllJvmTests
```

```python
kt_jvm_test(
    name = "jvm_tests",
    srcs = glob(["**/*.kt"]),
    test_class = "dev.stapler.stelekit.AllJvmTests",
    ...
)
```

Note: if screenshot tests use Roborazzi (no Bazel integration), exclude them:
```python
srcs = glob(
    ["**/*.kt"],
    exclude = ["**/*Screenshot*.kt", "**/screenshots/**"],
),
```

---

## `associates` vs `deps` for `internal` visibility

To access `internal` declarations from another module, use `associates`, not `deps`.
A target listed in `associates` **must not** also appear in `deps` — this is a rules_kotlin
constraint.

```python
kt_jvm_test(
    name = "business_tests",
    associates = ["//kmp/src/commonMain/kotlin:common_main_lib"],  # grants internal access
    deps = [
        # common_main_lib must NOT appear here too
        "@maven//:org_jetbrains_kotlin_kotlin_test_junit",
    ],
)
```

---

## JAR-safe test fixture extraction

Bazel packs `resources` into JARs. `File(url.toURI())` throws on `jar:` URIs.
Use `ClassLoader.getResource()` and extract to a temp dir with atomic rename:

```kotlin
fun getClasspathDirectory(loader: ClassLoader, resourceName: String): File {
    val url = loader.getResource(resourceName) ?: error("$resourceName not found")
    if (url.protocol == "file") return File(url.toURI())    // IDE / file:// path — no extraction needed
    val dest = File(
        System.getenv("TEST_TMPDIR") ?: System.getProperty("java.io.tmpdir"),
        resourceName.replace('/', '_'),
    )
    synchronized(extractionLock) {
        if (!dest.exists()) {
            val tmp = File(dest.parent, dest.name + ".tmp")
            tmp.deleteRecursively(); tmp.mkdirs()
            try {
                val conn = url.openConnection() as JarURLConnection
                conn.jarFile.use { jar ->
                    val prefix = conn.entryName + "/"
                    jar.entries().asSequence()
                        .filter { !it.isDirectory && it.name.startsWith(prefix) }
                        .forEach { entry ->
                            val target = File(tmp, entry.name.removePrefix(prefix))
                            target.parentFile?.mkdirs()
                            jar.getInputStream(entry).use { it.copyTo(target.outputStream()) }
                        }
                }
                tmp.renameTo(dest)              // atomic: dest appears fully-written or not at all
            } catch (e: Exception) {
                tmp.deleteRecursively(); throw e // clean up partial extractions
            }
        }
    }
    return dest
}
private val extractionLock = Any()
```

Use `TEST_TMPDIR` (Bazel's designated temp dir for tests) over `java.io.tmpdir` — Bazel
cleans it between runs. Do **not** use `user.home` — Bazel's sandbox blocks writes there.

---

## Sandbox restrictions: `user.home` writes fail

Inside the Bazel sandbox, the home directory (`$HOME`) is inaccessible for writes.
Replace `System.getProperty("user.home")` with `System.getProperty("java.io.tmpdir")`
or `System.getenv("TEST_TMPDIR")` in any test that creates temporary files.

---

## KMP: patching `rules_kotlin` for `common_srcs`

Kotlin Multiplatform requires `-Xcommon-sources` for expect/actual compilation.
`rules_kotlin` doesn't support this natively — patch it via `single_version_override`:

```starlark
# MODULE.bazel
single_version_override(
    module_name = "rules_kotlin",
    version = "2.3.20",
    patches = ["//third_party/patches:rules_kotlin_kmp.patch"],
    patch_strip = 1,
)
```

The patch adds a `common_srcs` attribute to `kt_jvm_library` and passes
`-Xcommon-sources=$(common_srcs)` to kotlinc. Maintain the patch in
`third_party/patches/rules_kotlin_kmp.patch`. When upgrading rules_kotlin,
re-apply and re-test.

---

## Gradle-delegating genrule (for WASM/JS)

When a Bazel target must delegate to Gradle (e.g., WASM until rules_kotlin#567 lands),
preserve Gradle's incremental cache with a separate `actions/cache` step keyed on
relevant Gradle files plus `${{ github.sha }}` (so each push seeds a fresh cache from
the previous run):

```yaml
- uses: actions/cache@v4
  with:
    path: ~/.gradle
    key: gradle-web-${{ runner.os }}-${{ hashFiles('build.gradle.kts', 'gradle.properties') }}-${{ github.sha }}
    restore-keys: |
      gradle-web-${{ runner.os }}-${{ hashFiles('build.gradle.kts', 'gradle.properties') }}-
      gradle-web-${{ runner.os }}-
```

---

## Artifact uploads: gate on push

CI artifacts on PRs add noise and consume storage quota without benefit. Gate uploads
on `github.event_name == 'push'`:

```yaml
- name: Upload web bundle
  if: github.event_name == 'push'
  uses: actions/upload-artifact@v4
  with:
    name: web-dist
    path: bazel-bin/kmp/web_dist.tar.gz
    retention-days: 7
```

---

## Local disk cache for developers

Document in `.bazelrc.user.example` (not committed):

```
# Persists build results between sessions and workspace switches.
# build --disk_cache=~/.cache/bazel-disk-cache
```

Grows unboundedly — users must manually prune with `bazel clean --expunge` or `rm -rf`.

---

## Anti-patterns

| Wrong | Correct |
|---|---|
| `bazelbuild/setup-bazelisk` + `actions/cache` for Bazel | `bazel-contrib/setup-bazel@0.19.0` |
| Single shared `disk-cache` across all jobs | One `disk-cache` name per job |
| `alias()` for test aggregation | `test_suite()` |
| `target` in both `associates` and `deps` | `associates` only — no `deps` duplication |
| `File(url.toURI())` for classpath resources | Extract via `JarURLConnection` |
| `System.getProperty("user.home")` in tests | `TEST_TMPDIR` or `java.io.tmpdir` |
| `sudo apt-get install -y <pkg>` | `sudo apt-get install -y --no-install-recommends <pkg>` |
| Uploading artifacts on every event | Gate with `if: github.event_name == 'push'` |
| No `permissions:` block in workflow | `permissions: contents: read` at minimum |

Attribution

tstaplertstapler
View sourceMore from tstapler →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →