This workflow REMOVES files from your release artifacts.
When a GitHub release is created, a cleaned tarball is produced that excludes directories and files you specify (or the defaults listed below). The cleaned tarball is uploaded as a release asset alongside the standard GitHub-generated source archives.
Your repository, branches, and tags are never modified. Only the tarball asset is affected.
When release-please creates a new release, org-release.yml triggers several parallel jobs:
release (release-please creates the tag and release)
|
├── release-clean -- Builds a cleaned tarball (THIS FEATURE)
├── trivy-scan -- Trivy security scan (full repo)
├── gitleaks-scan -- Secret detection across full repo history
├── notify-release -- Slack release notification (if configured)
└── notify-failure -- Slack failure notification (if any job fails)
The release-clean job:
- Checks out the code at the release tag on an isolated, ephemeral runner
- Validates all inputs against an allowlist (rejects special characters and path traversal)
- Removes
.git/(always), plus any configured directories and files - Packages the remaining files into
<repo>-<tag>-clean.tar.gz - Generates a SHA256 checksum file (
<repo>-<tag>-clean.tar.gz.sha256) - Uploads both to the GitHub release as downloadable assets
- Also uploads both as workflow artifacts (retained for 30 days)
- Writes a step summary with archive details for the Actions run log
Scan jobs are not affected. They run on their own runners with a full, unmodified checkout — .github/, docs/, and everything else are still scanned.
This is on by default. If you call org-release.yml without any with: overrides, the following are stripped from the clean tarball:
| Directory | Why |
|---|---|
.github/ |
CI/CD workflows, issue templates, PR templates — not needed by consumers |
docs/ |
Repository documentation — not needed at runtime |
.claude/ |
Claude Code project configuration — development tooling only |
| File | Why |
|---|---|
CHANGELOG.md |
Release history — available on the GitHub release page |
release-please-config.json |
Release-please configuration — release tooling only |
.release-please-manifest.json |
Release-please state — release tooling only |
.gitignore |
Git configuration — not relevant to published artifacts |
.gitattributes |
Git configuration — not relevant to published artifacts |
CLAUDE.md |
Claude Code instructions — development tooling only |
.claudeignore |
Claude Code ignore rules — development tooling only |
| Path | Why |
|---|---|
.git/ |
Git metadata — always removed, not configurable, never belongs in a release artifact |
If a listed file or directory does not exist in your repo, it is skipped gracefully — no error, no failure.
If you already call org-release.yml, you automatically get clean tarballs on your next release:
# .github/workflows/release.yml
name: Release
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
issues: write
actions: read
jobs:
create-release:
uses: Coalfire-CF/Actions/.github/workflows/org-release.yml@main
secrets: inheritAfter a release is created, the release page will have:
<repo>-<tag>-clean.tar.gz— the cleaned tarball<repo>-<tag>-clean.tar.gz.sha256— SHA256 checksum
The tarball extracts into a <repo>-<tag>/ directory, matching the GitHub auto-generated archive convention.
Add or change which directories and files are removed:
jobs:
create-release:
uses: Coalfire-CF/Actions/.github/workflows/org-release.yml@main
secrets: inherit
with:
clean_exclude_dirs: '.github,docs,.claude,.ci,tests'
clean_exclude_files: 'CHANGELOG.md,Makefile,release-please-config.json,.release-please-manifest.json,.gitignore,.gitattributes,CLAUDE.md,.claudeignore'Values are comma-separated, no spaces around commas. Each entry must match [a-zA-Z0-9._/-] — special characters and .. (path traversal) are rejected.
If you do not want a cleaned tarball attached to your releases:
jobs:
create-release:
uses: Coalfire-CF/Actions/.github/workflows/org-release.yml@main
secrets: inherit
with:
clean_release: false| Input | Type | Default | Description |
|---|---|---|---|
clean_release |
boolean | true |
Set to false to skip clean tarball creation entirely |
clean_exclude_dirs |
string | .github,docs,.claude |
Comma-separated directories to remove |
clean_exclude_files |
string | CHANGELOG.md,release-please-config.json,.release-please-manifest.json,.gitignore,.gitattributes,CLAUDE.md,.claudeignore |
Comma-separated files to remove |
| Secret | Required | Description |
|---|---|---|
RELEASE_APP_ID |
No | GitHub App ID for the release automation app |
RELEASE_APP_PRIVATE_KEY |
No | GitHub App private key (PEM format) |
When configured as org-level secrets, the workflow generates short-lived app tokens (1 hour expiry) for consistent permissions across public and private repos. If the secrets are not set, the workflow falls back to github.token automatically. Downstream repos pass these via secrets: inherit.
| Control | NIST 800-53 | Description |
|---|---|---|
| Input validation | SI-10 | All inputs validated against allowlist regex; path traversal (..) rejected |
| No shell interpolation | SC-28 | Inputs passed via env: blocks, never direct ${{ }} in shell scripts |
| GitHub App authentication | IA-2 | Optional app token for consistent permissions across public/private repos; falls back to github.token |
| SHA256 checksum | SI-7 | Integrity verification for the released artifact |
| Step summary | AU-3 | Audit trail of what was built, excluded, and uploaded |
| Least privilege | AC-6 | Only requires contents: write and actions: read |
After downloading the tarball from a release:
# Verify checksum
sha256sum -c <repo>-<tag>-clean.tar.gz.sha256
# Extract
tar -xzf <repo>-<tag>-clean.tar.gz
# Confirm excluded files are absent
ls <repo>-<tag>/GitHub's auto-generated source archives (the default .tar.gz and .zip on every release) respect .gitattributes export-ignore directives. If you want those auto-generated archives cleaned as well, add a .gitattributes to your repo:
.github/ export-ignore
docs/ export-ignore
.claude/ export-ignore
CHANGELOG.md export-ignore
CLAUDE.md export-ignore
.claudeignore export-ignore
release-please-config.json export-ignore
.release-please-manifest.json export-ignoreThis is complementary — the workflow handles the explicit -clean.tar.gz asset, .gitattributes handles the auto-generated archives. Both approaches leave the repository untouched.
Q: Will this break my CI scans (Trivy)? No. Each scan job runs on its own isolated runner with a fresh, full checkout. They never see the cleaned copy.
Q: Does this modify my repository, branches, or tags? No. The job checks out code into an ephemeral runner workspace, deletes files from that workspace only, packages the result, and the runner is destroyed when the job finishes.
Q: What if I list a file/directory that doesn't exist in my repo? It is skipped with a log message. The workflow continues without error.
Q: Does this need any additional secrets?
Optionally. If the org-level secrets RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY are configured, the workflow uses a GitHub App for authentication with short-lived tokens (1 hour). If not configured, it falls back to github.token automatically. Downstream repos should use secrets: inherit to pass these through.
Q: Can someone inject malicious input through the exclusion lists?
Inputs are validated against a strict allowlist regex (^[a-zA-Z0-9._/,-]+$) and checked for path traversal (..). Invalid inputs fail the workflow immediately.