diff --git a/.agents/rules/code-style.md b/.agents/rules/code-style.md new file mode 100644 index 0000000..71f8e97 --- /dev/null +++ b/.agents/rules/code-style.md @@ -0,0 +1,153 @@ +# Code Style Guidelines + +## Action Definition (YAML) + +### File Naming +- Main action: `action.yml` (not `action.yaml`) +- Subdirectory actions: `{subdir}/action.yml` + +### Structure +```yaml +name: 'Action Name' +description: 'Clear, concise description' +branding: + icon: 'github-octicons-name' + color: 'color-name' + +inputs: + input-name: + description: 'What this input does' + required: true|false + default: 'default-value' # if not required + +runs: + using: 'composite' + steps: + - name: descriptive step name + shell: bash + run: | + command here +``` + +### Input Naming +- Use kebab-case: `cache-base-key`, `base-ref` +- Be descriptive but concise +- Document clearly what each input does + +### Step Naming +- Use descriptive, action-oriented names +- Good: "restore tsc build cache", "fetch base branch" +- Avoid: "step 1", "cache", "files" + +## Shell Scripts + +### Commands +- Use `shell: bash` for all shell steps +- Prefer built-in commands (`find`, `touch`, `git`) over custom scripts +- Use `-c` flag with `touch` to avoid errors on missing files + +### Formatting +- For multi-line commands, use YAML's `|` or `>` operators +- For single commands, inline is fine +- Keep commands readable - break long lines with `\` + +### Example +```yaml +- name: restore timestamps of all files to base branch commit + shell: bash + run: | + find . -type f \ + -exec touch -c -m -t $(git log -1 --format=%cd --date=format:%y%m%d%H%M.%S ${{ inputs.base-ref }}) {} + +``` + +## GitHub Actions Expressions + +### Inputs +- Reference with `${{ inputs.input-name }}` +- Always use inputs for configurable values +- Don't hardcode values that might change per project + +### Steps Output +- Use step IDs for referencing outputs: `steps.step-id.outputs.output-name` +- Check conditionals: `steps.step-id.outputs.any_changed == 'true'` + +### Context Variables +- Use GitHub context thoughtfully: `github.sha`, `github.ref`, `runner.os` +- Document expected context in README examples + +## Conditional Logic + +### When to Use +- `if:` conditions for optional steps +- Common: `if: steps.changed-files.outputs.any_changed == 'true'` +- Recommend workflow-level conditions in documentation (e.g., only save on main) + +### Boolean Checks +```yaml +# Check string equality +if: steps.step-id.outputs.value == 'true' + +# Check existence +if: steps.step-id.outputs.value != '' + +# Workflow context +if: github.ref == 'refs/heads/main' +``` + +## Documentation in Code + +### Action Metadata +- `name`: User-facing action name (shows in marketplace) +- `description`: One-sentence summary of what the action does +- Input `description`: Clear explanation of purpose and format + +### Comments +- Minimal inline comments in YAML (should be self-documenting) +- Use step names to explain intent +- Complex commands deserve comments above them + +## Error Handling + +### Defensive Coding +- Use `touch -c` to avoid errors on missing files +- Use `git fetch` before referencing remote branches +- Check step outputs before using them (`if:` conditions) + +### Graceful Degradation +- Cache restore should not fail the workflow if cache misses +- Use `restore-keys` for fallback cache matches + +## Performance + +### Minimize Steps +- Combine related commands in single steps when logical +- Don't over-split simple operations +- Each step has overhead in action runner + +### Efficient Commands +- Use `find ... -exec ... +` instead of piping to `xargs` +- Minimize git operations (fetch once, use multiple times) + +## Dependencies + +### Action Versions +- Pin to major version with `@v4` (allows minor/patch updates) +- Document breaking changes if updating major versions +- Track upstream action changes for security + +### External Actions +- Prefer official GitHub actions (`actions/*`) +- Use well-maintained community actions (`tj-actions/*`) +- Document why each external action is needed + +## Consistency + +### Across Actions +- Use same input names across restore/save actions +- Match parameter descriptions +- Keep branding consistent + +### With Ecosystem +- Follow GitHub Actions conventions +- Match patterns from popular actions (e.g., `actions/cache`) +- Use standard input names when applicable (`path`, `key`) diff --git a/.agents/rules/security.md b/.agents/rules/security.md new file mode 100644 index 0000000..eea18ec --- /dev/null +++ b/.agents/rules/security.md @@ -0,0 +1,352 @@ +# Security Guidelines + +## Threat Model + +### Assets to Protect +1. **Source Code**: Repository contents and history +2. **Build Artifacts**: Cached `.tsbuildinfo` and `.d.ts` files +3. **Workflow Secrets**: GitHub tokens and credentials +4. **CI Environment**: Runner integrity and isolation + +### Threats +1. **Cache Poisoning**: Malicious cached files injected +2. **Code Injection**: Unsanitized inputs in shell commands +3. **Privilege Escalation**: Abuse of GitHub token permissions +4. **Information Disclosure**: Sensitive data in logs or cache +5. **Supply Chain**: Compromised dependencies (external actions) + +## Input Validation + +### User Inputs + +All action inputs are potentially untrusted. Validate and sanitize: + +#### Cache Keys +```yaml +# ✅ SAFE - Using GitHub context (trusted) +cache-key: ${{ github.sha }} +cache-base-key: ${{ runner.os }} + +# ⚠️ RISKY - User-provided input +cache-key: ${{ github.event.pull_request.head.ref }} +# Risk: Branch names could contain shell metacharacters +``` + +**Guidelines**: +- Prefer GitHub context variables (already sanitized) +- Avoid user-provided strings in shell commands +- If using user input, quote properly: `"${{ inputs.value }}"` + +#### File Patterns +```yaml +# ✅ SAFE - Static patterns +files: | + **/*.tsbuildinfo + **/types/**/*.d.ts + +# ⚠️ RISKY - Dynamic patterns from inputs +files: ${{ inputs.custom-pattern }} +# Risk: Pattern could access unexpected files +``` + +**Guidelines**: +- Use static file patterns when possible +- If dynamic, document security implications +- Validate patterns don't escape project directory + +#### Base Ref +```yaml +# ✅ SAFE - Using repository default branch +base-ref: ${{ github.event.repository.default_branch }} + +# ⚠️ RISKY - User-specified branch +base-ref: ${{ github.event.inputs.branch }} +# Risk: Could reference attacker-controlled branch +``` + +**Guidelines**: +- Default to repository default branch +- Validate ref exists before using +- Use `git fetch` to ensure ref available + +## Command Injection Prevention + +### Shell Commands + +Never directly interpolate user input into shell commands without quoting: + +```yaml +# ❌ VULNERABLE +run: git fetch origin ${{ inputs.base-ref }} + +# ✅ SAFE - Quoted +run: git fetch origin "${{ inputs.base-ref }}" + +# ✅ SAFER - Using actions context (pre-sanitized) +run: git fetch origin "${{ github.event.repository.default_branch }}" +``` + +### Touch Commands + +Current implementation: +```yaml +run: touch -c -m ${{ steps.changed-source-files.outputs.all_changed_files }} +``` + +**Security Note**: `tj-actions/changed-files` returns space-separated file paths. These are generated by the action (trusted) not user input. However, for defense in depth: + +```yaml +# More robust (handles spaces in filenames) +run: | + echo "${{ steps.changed-source-files.outputs.all_changed_files }}" | \ + xargs -d ' ' -I {} touch -c -m "{}" +``` + +### Find Commands + +```yaml +# ✅ SAFE - No user input in command +run: find . -type f -exec touch -c -m -t $(git log -1 ...) {} + +``` + +**Guidelines**: +- Use `-type f` to only match files +- Use `-exec` with `+` for efficiency (not security-critical) +- Avoid `-exec ... \;` with user input + +## Dependency Security + +### External Actions + +Track and pin versions: + +```yaml +# ✅ GOOD - Pinned to major version +uses: actions/cache/restore@v4 + +# ✅ BETTER - Pinned to specific SHA (immutable) +uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + +# ❌ AVOID - Using latest +uses: actions/cache/restore@main +``` + +**Current Dependencies**: +- `actions/cache/restore@v4` - Official GitHub action +- `actions/cache/save@v4` - Official GitHub action +- `tj-actions/changed-files@v45` - Community action (26k+ stars, well-maintained) + +**Security Practices**: +1. Review action source code before using +2. Pin to specific versions or SHAs +3. Monitor for security advisories +4. Use Dependabot for updates + +### Dependabot Configuration + +Create `.github/dependabot.yml`: +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "security" +``` + +## GitHub Token Permissions + +### Default Permissions + +Composite actions inherit workflow token permissions. Follow principle of least privilege: + +```yaml +# In workflows using this action +permissions: + contents: read # Required for checkout and git operations + # actions: read # NOT required (cache actions use GITHUB_TOKEN implicitly) +``` + +**This action requires**: +- `contents: read` - Git operations and changed file detection +- Cache actions use `GITHUB_TOKEN` automatically (no explicit permission needed) + +### Token Exposure + +Never log or cache tokens: +```yaml +# ❌ DANGEROUS +run: echo "Token: ${{ secrets.GITHUB_TOKEN }}" + +# ✅ SAFE - Tokens not exposed +run: git fetch origin "${{ inputs.base-ref }}" +``` + +## Cache Security + +### Cache Scope + +GitHub Actions cache is: +- **Scoped to repository** - Other repos can't access +- **Scoped to branch** - PRs from forks can't write to main branch cache +- **Read-only from forks** - Fork PRs can read but not write cache + +**Security Implications**: +- Caches from forks are isolated (safe) +- Only main branch caches affect protected branches +- Cache poisoning requires write access to repository + +### Cache Content + +Cached files should not contain: +- Secrets or credentials +- Personal information +- Source code (only build artifacts) + +**This Action's Cache**: +- `.tsbuildinfo` - Metadata about builds (safe) +- `.d.ts` files - Type definitions (safe, derived from source) + +**Validation**: +```bash +# Audit what's being cached +- run: | + echo "Files to cache:" + find . -name "*.tsbuildinfo" -o -name "*.d.ts" | head -20 +``` + +## Secrets Management + +### No Secrets Required + +This action doesn't require secrets beyond default `GITHUB_TOKEN`. + +**If extending**: +- Never pass secrets via inputs (use secrets context) +- Use `::add-mask::` to prevent accidental logging +- Validate secrets before use + +### Prevent Secret Leakage + +```yaml +# ❌ LOGS SECRET +run: echo "API_KEY=${{ secrets.API_KEY }}" + +# ✅ MASKS SECRET +run: | + echo "::add-mask::${{ secrets.API_KEY }}" + echo "API_KEY=${{ secrets.API_KEY }}" +``` + +## File System Security + +### Path Traversal + +Prevent accessing files outside project: + +```yaml +# ✅ SAFE - Relative to project root +path: ${{ inputs.files }} + +# ⚠️ VALIDATE - Don't allow absolute paths or ../ +files: | + **/*.tsbuildinfo + # NOT: /etc/passwd or ../../secrets +``` + +**GitHub Actions** runs in isolated containers, but still validate: +- File patterns are relative +- No absolute paths in user input +- Use `find` with `.` as root (doesn't escape container) + +### Permissions + +Files created/modified by action: +- Inherit runner user permissions (default) +- No special permissions needed +- Cache files readable by subsequent runs + +## Logging & Monitoring + +### Sensitive Data in Logs + +Be careful what's logged: + +```yaml +# ✅ SAFE - Logs file count, not contents +run: | + count=$(find . -name "*.tsbuildinfo" | wc -l) + echo "Found $count tsbuildinfo files" + +# ❌ RISKY - Could log sensitive filenames +run: | + find . -name "*.tsbuildinfo" +``` + +### Audit Logging + +GitHub Actions provides: +- Workflow run logs (who triggered, when, inputs) +- Cache access logs (hits/misses) +- Action usage audit trail + +**Monitor for**: +- Unexpected cache misses (possible tampering) +- Failed git operations (suspicious refs) +- Unusual file patterns (path traversal attempts) + +## Incident Response + +### If Compromise Suspected + +1. **Revoke Access**: Remove compromised tokens/keys +2. **Clear Cache**: Delete affected caches via API +3. **Audit Logs**: Review workflow runs and cache access +4. **Rotate Secrets**: Even if not directly used by action +5. **Update Dependencies**: Ensure all actions at latest secure versions + +### Cache Poisoning Response + +If malicious cache detected: +```bash +# Clear all caches for repository (requires admin) +gh api -X DELETE /repos/{owner}/{repo}/actions/caches + +# Or clear specific cache key pattern +gh api -X DELETE "/repos/{owner}/{repo}/actions/caches?key=cache-key-prefix" +``` + +## Security Best Practices Summary + +### For Action Maintainers +1. ✅ Quote all variable interpolations in shell +2. ✅ Pin external action versions +3. ✅ Validate input patterns +4. ✅ Review dependencies regularly +5. ✅ Document security considerations +6. ✅ Never log sensitive data +7. ✅ Use minimal token permissions + +### For Action Users +1. ✅ Save cache only on protected branches +2. ✅ Use static file patterns +3. ✅ Prefer GitHub context over user input +4. ✅ Monitor cache hit rates for anomalies +5. ✅ Review workflow logs periodically +6. ✅ Keep actions updated +7. ✅ Use branch protection rules + +## Security Contacts + +**Report Security Issues**: +- **Email**: security@attest.com (or appropriate contact) +- **GitHub**: Use "Report a vulnerability" feature +- **Response Time**: Within 48 hours for critical issues + +**Do Not**: +- Open public issues for security vulnerabilities +- Disclose vulnerabilities before patch available +- Test vulnerabilities on production workflows diff --git a/.agents/rules/testing.md b/.agents/rules/testing.md new file mode 100644 index 0000000..872909c --- /dev/null +++ b/.agents/rules/testing.md @@ -0,0 +1,283 @@ +# Testing Guidelines + +## Testing Strategy + +Since this is a composite GitHub Action (not a TypeScript/JavaScript action), traditional unit tests don't apply. Testing focuses on integration and real-world workflows. + +## Manual Testing + +### Test Environments + +#### Required Test Cases +1. **Fresh Cache** - First run with no cache +2. **Cache Hit** - Subsequent run with exact cache match +3. **Cache Miss (Partial)** - Run with prefix cache match only +4. **Changed Files** - Run with modified source files +5. **Unchanged Files** - Run with no changes (full cache benefit) + +#### Test Projects +- **Monorepo**: Multiple TypeScript projects with project references +- **Single Project**: Simple TypeScript project +- **Large Project**: Project with many files (test performance) + +### Test Workflow + +Create a `.github/workflows/test-typescript-cache.yml`: + +```yaml +name: Test TypeScript Cache Action + +on: + pull_request: + push: + branches: [main] + +jobs: + test-cache: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - run: pnpm install + + # Test restore action + - name: Restore TypeScript Cache + uses: ./ # Test local action + with: + cache-base-key: ${{ runner.os }}-test + cache-key: ${{ github.sha }} + base-ref: main + files: | + **/*.tsbuildinfo + **/types/**/*.d.ts + + # Run type check and measure time + - name: Type Check (First Run) + run: time pnpm exec tsc --build + + # Test save action + - name: Save TypeScript Cache + if: github.ref == 'refs/heads/main' + uses: ./save + with: + cache-base-key: ${{ runner.os }}-test + cache-key: ${{ github.sha }} + files: | + **/*.tsbuildinfo + **/types/**/*.d.ts + + # Test cache hit on same SHA + - name: Clean TypeScript outputs + run: pnpm exec tsc --build --clean + + - name: Restore TypeScript Cache (Second Run) + uses: ./ + with: + cache-base-key: ${{ runner.os }}-test + cache-key: ${{ github.sha }} + base-ref: main + files: | + **/*.tsbuildinfo + **/types/**/*.d.ts + + - name: Type Check (Cached Run) + run: time pnpm exec tsc --build +``` + +### Validation Checklist + +After running test workflow, verify: + +- [ ] Cache restore completes without errors +- [ ] Changed files detected correctly +- [ ] Timestamps set appropriately (check with `ls -lt`) +- [ ] Type check completes successfully +- [ ] Cache save completes (on main branch only) +- [ ] Subsequent run is faster (cache hit) +- [ ] Only changed packages rebuild on PR branches + +## Integration Testing + +### Cross-Platform Testing + +Test on multiple runners: +```yaml +strategy: + matrix: + os: [ubuntu-latest, macos-latest] +``` + +Verify: +- `touch` command works correctly +- `find` command behavior consistent +- Git operations succeed +- Timestamp format compatible + +### Monorepo Testing + +Test with different monorepo tools: +- **pnpm workspaces**: Test workspace dependencies +- **Nx**: Test affected project detection +- **Turborepo**: Test turbo cache interaction +- **Lerna**: Test multi-package builds + +### Edge Cases + +#### Empty Cache +- **Scenario**: First run or cache expired +- **Expected**: Action completes, type check runs fully, cache saved +- **Verify**: No errors, subsequent runs faster + +#### No Changed Files +- **Scenario**: Re-run on same commit +- **Expected**: All files have base branch timestamp +- **Verify**: Type check completes instantly (no rebuild) + +#### All Files Changed +- **Scenario**: Large refactor or branch divergence +- **Expected**: All packages rebuild +- **Verify**: Behavior same as no cache, but cache still saved + +#### Missing Base Branch +- **Scenario**: `base-ref` doesn't exist +- **Expected**: Action fails gracefully with clear error +- **Verify**: Error message helpful, workflow can catch and handle + +#### File Pattern Mismatch +- **Scenario**: `files` glob doesn't match any TypeScript outputs +- **Expected**: Cache empty but action succeeds +- **Verify**: No errors, type check runs fully + +## Performance Testing + +### Metrics to Track + +1. **Cache Restore Time**: Should be < 10s for most projects +2. **Timestamp Update Time**: Should be < 5s for most projects +3. **Type Check Time (No Cache)**: Baseline measurement +4. **Type Check Time (Full Cache)**: Should be ~90% faster +5. **Type Check Time (Partial Cache)**: Should be 50-80% faster +6. **Cache Save Time**: Should be < 10s for most projects + +### Benchmark Workflow + +```yaml +- name: Benchmark No Cache + run: | + tsc --build --clean + time tsc --build + +- name: Restore Cache + uses: ./ + with: + cache-base-key: ${{ runner.os }} + cache-key: ${{ github.sha }} + base-ref: main + files: '**/*.tsbuildinfo' + +- name: Benchmark With Cache + run: time tsc --build +``` + +Compare times and ensure cache provides meaningful speedup. + +## Debugging + +### Enable Debug Logging + +Set in workflow or repository secrets: +```yaml +env: + ACTIONS_STEP_DEBUG: true +``` + +### Inspect Timestamps + +Add debugging step: +```yaml +- name: Debug Timestamps + run: | + echo "Base branch commit time:" + git log -1 --format=%cd --date=format:%y%m%d%H%M.%S ${{ inputs.base-ref }} + echo "Sample file timestamps:" + ls -lt **/*.tsbuildinfo | head -5 + ls -lt src/**/*.ts | head -5 +``` + +### Verify Cache Contents + +```yaml +- name: Verify Cache Hit + if: steps.cache-restore.outputs.cache-hit == 'true' + run: | + echo "Cache hit! Verifying contents..." + find . -name "*.tsbuildinfo" -type f +``` + +### Test Changed File Detection + +```yaml +- name: Debug Changed Files + run: | + echo "Changed files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + echo "Any changed: ${{ steps.changed-files.outputs.any_changed }}" +``` + +## Regression Testing + +### Before Releasing + +1. Test with real Attest projects +2. Verify cache hit rates unchanged +3. Check type check durations don't regress +4. Test on both Linux and macOS runners +5. Verify with fresh cache (cache cleared) + +### Breaking Changes + +If changing: +- Input names → Major version bump +- Cache key format → Document migration path +- File patterns → Test with existing caches +- Timestamp logic → Extensive testing required + +## Continuous Validation + +### Monitoring in Production + +Track in real workflows: +- Cache hit rate (aim for >80% on PR builds) +- Type check duration trends +- Cache size growth +- Failure rates + +### Feedback Loop + +Collect feedback from: +- Attest frontend team +- GitHub Actions logs +- Developer reports +- Performance metrics + +## Documentation Testing + +### Verify Examples Work + +Copy-paste examples from README into test workflow: +- Ensure syntax correct +- Verify inputs valid +- Check outputs as expected +- Confirm best practices followed + +### Test Documentation Scenarios + +For each usage example in README: +1. Create minimal reproduction +2. Run through workflow +3. Verify expected behavior +4. Document any surprises diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbd12e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +CLAUDE.local.md +.claude/settings.local.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d62c411 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,216 @@ +# AGENTS.md + +## Project Overview + +**typescript-cache-action** is a GitHub Action that enables incremental TypeScript type checking in CI environments by managing `.tsbuildinfo` file caching and restoring proper file timestamps. + +### Core Problem +TypeScript's `tsc --build` mode relies on file modification timestamps to determine which packages need rebuilding. Git doesn't preserve timestamps, causing CI to rebuild everything unnecessarily. + +### Solution +1. Restore cached TypeScript output files (`.tsbuildinfo`, `.d.ts`) +2. Reset all file timestamps to the base branch commit timestamp +3. Update timestamps for git-changed files to current time +4. Run `tsc --build` - only rebuilds changed packages +5. Save updated cache for future runs + +## Repository Structure + +``` +typescript-cache-action/ +├── action.yml # Main restore action definition +├── save/ +│ └── action.yml # Cache save action definition +├── README.md # User-facing documentation +├── LICENSE # MIT License +└── .github/ + └── CODEOWNERS # Code ownership (@Attest/frontend) +``` + +## Architecture + +This is a **composite GitHub Action** (not a TypeScript/JavaScript action). It uses shell commands and existing GitHub Actions to orchestrate the caching workflow. + +### Main Action (action.yml) +**Purpose**: Restore cache and prepare timestamps before type checking + +**Inputs**: +- `cache-base-key`: Base key for cache lookups (e.g., `${{ runner.os }}`) +- `cache-key`: Specific cache key (e.g., `${{ github.sha }}`) +- `base-ref`: Base branch for timestamp restoration (default: `main`) +- `files`: Glob patterns for TypeScript output files (required) + +**Steps**: +1. Fetch base-ref branch +2. Restore cache using `actions/cache/restore@v4` +3. Touch all files with base branch commit timestamp +4. Detect changed files using `tj-actions/changed-files@v45` +5. Touch changed files with current timestamp + +### Save Action (save/action.yml) +**Purpose**: Save TypeScript build cache after successful type check + +**Inputs**: +- `cache-base-key`: Base key for cache storage +- `cache-key`: Specific cache key +- `files`: Glob patterns for TypeScript output files (required) + +**Steps**: +1. Save cache using `actions/cache/save@v4` + +## Usage Patterns + +### Typical Workflow + +```yaml +# Restore cache before type checking (all branches) +- uses: Attest/typescript-cache-action@main + with: + cache-base-key: ${{ runner.os }} + cache-key: ${{ github.sha }} + base-ref: ${{ github.event.repository.default_branch }} + files: | + **/types/**/*.d.ts + **/*.tsbuildinfo + +# Run type check +- run: pnpm exec tsc --build + +# Save cache (only on main branch) +- if: github.ref == 'refs/heads/main' + uses: Attest/typescript-cache-action/save@main + with: + cache-base-key: ${{ runner.os }} + cache-key: ${{ github.sha }} + files: | + **/types/**/*.d.ts + **/*.tsbuildinfo +``` + +### Best Practices +1. **Save cache only on default branch** - Prevents cache pollution from feature branches +2. **Use consistent file patterns** - Ensure restore and save use identical `files` globs +3. **Include OS in cache key** - Different OS may produce different build artifacts +4. **Use SHA for cache key** - Ensures each commit has unique cache entry + +## Key Dependencies + +### External Actions +- `actions/cache/restore@v4` - GitHub's cache restore action +- `actions/cache/save@v4` - GitHub's cache save action +- `tj-actions/changed-files@v45` - Detects files changed between commits + +### Shell Commands +- `git fetch` - Retrieves base branch reference +- `find` with `touch` - Resets file timestamps to base branch time +- `touch` - Updates timestamps for changed files + +## Technical Details + +### Timestamp Strategy + +**Goal**: Make TypeScript's incremental build detection work correctly + +**Base State** (after base branch timestamp restore): +- All source files: base commit timestamp +- All `.tsbuildinfo` files: base commit timestamp (from cache) + +**After Changed File Update**: +- Unchanged source files: base commit timestamp (older than `.tsbuildinfo`) +- Changed source files: current timestamp (newer than `.tsbuildinfo`) +- `.tsbuildinfo` files: base commit timestamp + +**TypeScript Behavior**: +- Compares source file timestamps vs `.tsbuildinfo` timestamps +- Rebuilds packages where source is newer than `.tsbuildinfo` +- Only changed packages (and dependents) are rebuilt + +### Cache Strategy + +**Cache Key Hierarchy**: +1. `{cache-base-key}-{cache-key}` - Exact match (e.g., `Linux-abc123`) +2. `{cache-base-key}-` - Prefix match (e.g., `Linux-*`) + +**Benefits**: +- Exact match: Fastest, restores exact build state +- Prefix match: Falls back to most recent build on same OS +- Graceful degradation if exact cache unavailable + +### File Patterns + +Common patterns for TypeScript projects: +``` +**/*.tsbuildinfo # TypeScript incremental build files +**/types/**/*.d.ts # Generated type definitions +**/*.d.ts # All type definition files (broader) +dist/**/*.d.ts # Distribution type definitions +build/**/*.d.ts # Build output type definitions +``` + +## Constraints & Limitations + +1. **Composite Actions Only**: No compiled TypeScript/JavaScript - only shell commands and action composition +2. **Git Required**: Depends on git for change detection and branch operations +3. **Linux/macOS Focus**: `touch` and `find` commands assume Unix-like systems +4. **Cache Size Limits**: GitHub Actions cache has 10GB limit per repository +5. **Base Branch Required**: Must have fetched base branch for timestamp restoration + +## Maintenance Guidelines + +### When Updating Actions +- Test with various monorepo structures (Nx, Turborepo, pnpm workspaces) +- Verify timestamp restoration on both Linux and macOS runners +- Check cache hit rates in real workflows +- Ensure changed file detection works with merge commits + +### Monitoring +- Cache hit rates (should be high on PR builds) +- Type check duration (should decrease with cache) +- Cache size (monitor for excessive growth) + +### Versioning +- Use semantic versioning for releases +- Maintain `@main` for latest stable +- Test breaking changes on feature branches before merging + +## Common Issues + +### Issue: Type check still rebuilds everything +**Causes**: +- Cache miss (no recent cache available) +- File patterns don't match actual output locations +- Base branch not fetched correctly + +**Debugging**: +1. Check cache hit/miss in action logs +2. Verify file patterns match actual TypeScript output +3. Ensure base-ref points to valid branch + +### Issue: Stale cache causes type errors +**Cause**: Cached `.tsbuildinfo` references files that no longer exist + +**Solution**: Clear cache or let it expire naturally (GitHub cache has 7-day retention) + +### Issue: Cache size growing too large +**Causes**: +- Too many files in `files` pattern +- Including source files instead of just build outputs +- Multiple cache entries per commit + +**Solution**: +- Narrow file patterns to only necessary build outputs +- Use cache retention policies +- Save cache only on main branch + +## Related Documentation + +- [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) +- [TypeScript Build Mode](https://www.typescriptlang.org/docs/handbook/project-references.html#build-mode-for-typescript) +- [GitHub Actions Cache](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) +- [Composite Actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) + +--- + +**Ownership**: @Attest/frontend +**License**: MIT +**Status**: Production (In use across Attest projects) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dcdfe7a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,12 @@ +@AGENTS.md +@.agents/rules/code-style.md +@.agents/rules/testing.md +@.agents/rules/security.md +@docs/ARCHITECTURE.md + + +### Response Preferences +- Be concise. Prefer code over prose. +- When uncertain about architecture, read `docs/ARCHITECTURE.md` before assuming. +- When a change would invalidate any section of `AGENTS.md` or `README.md`, + flag it and offer to update them per the instructions in AGENTS.md. diff --git a/README.md b/README.md index 0ed8c4b..a04e8fd 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,384 @@ # TypeScript Cache Action -This action allows caching TypeScript build information and types for incremental typecheck runs within github actions. +A GitHub Action for incremental TypeScript type checking in CI by caching `.tsbuildinfo` files and restoring proper timestamps. -This demonstrates and encapsulates the process for incremental builds in CI with TypeScript. -We use this in many projects and has been working for our use case, feel free to open contributions or issues to provide feedback. +[![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -## Documentation +## Overview + +TypeScript's `tsc --build` mode uses file modification timestamps to determine what needs rebuilding. Git doesn't preserve timestamps, causing CI to rebuild everything unnecessarily. This action solves that by: + +1. Restoring cached TypeScript output files (`.tsbuildinfo`, `.d.ts`) +2. Resetting all file timestamps to a stable baseline +3. Updating only changed files to current time +4. Enabling TypeScript to detect exactly what changed + +**Result**: Only changed packages (and their dependents) are rebuilt, dramatically speeding up CI type checks. + +## Quick Start + +### Restore Cache (All Branches) + +```yaml +- name: Restore TypeScript Cache + uses: Attest/typescript-cache-action@main + with: + cache-base-key: ${{ runner.os }} + cache-key: ${{ github.sha }} + base-ref: ${{ github.event.repository.default_branch }} + files: | + **/*.tsbuildinfo + **/types/**/*.d.ts + +- name: Run Type Check + run: pnpm exec tsc --build +``` + +### Save Cache (Main Branch Only) + +```yaml +- name: Save TypeScript Cache + if: github.ref == 'refs/heads/main' + uses: Attest/typescript-cache-action/save@main + with: + cache-base-key: ${{ runner.os }} + cache-key: ${{ github.sha }} + files: | + **/*.tsbuildinfo + **/types/**/*.d.ts +``` + +## Complete Workflow Example ```yaml -name: Caching TypeScript build +name: Type Check -on: push +on: + pull_request: + push: + branches: [main] jobs: - build: + typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - # Any other steps (ie generation of files) + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Dependencies + run: pnpm install - - name: Restore Typescript Cache + - name: Restore TypeScript Cache uses: Attest/typescript-cache-action@main with: - # base cache key cache-base-key: ${{ runner.os }} - # cache key cache-key: ${{ github.sha }} - # base ref to restore timestamps to base-ref: ${{ github.event.repository.default_branch }} - # output files to restore cache for files: | - **/types/**/*.d.ts **/*.tsbuildinfo - - - name: run tsc + **/types/**/*.d.ts + + - name: Type Check run: pnpm exec tsc --build - - # Save TypeScript cache (recommend only on base branch workflow) - - name: Save Typescript Cache - if: ${{ github.workflow == 'main' }} + + - name: Save TypeScript Cache + if: github.ref == 'refs/heads/main' uses: Attest/typescript-cache-action/save@main with: - # base cache key cache-base-key: ${{ runner.os }} - # cache key cache-key: ${{ github.sha }} - # output files to restore cache for files: | - **/types/**/*.d.ts **/*.tsbuildinfo + **/types/**/*.d.ts +``` + +## Inputs + +### Restore Action + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `cache-base-key` | No | `''` | Base cache key for hierarchical cache lookup (e.g., `${{ runner.os }}`) | +| `cache-key` | No | `''` | Specific cache key (typically `${{ github.sha }}`) | +| `base-ref` | No | `main` | Base branch to compare against for change detection | +| `files` | **Yes** | - | Newline-separated glob patterns for TypeScript output files | + +### Save Action + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `cache-base-key` | No | `''` | Base cache key (should match restore action) | +| `cache-key` | No | `''` | Specific cache key (should match restore action) | +| `files` | **Yes** | - | Newline-separated glob patterns (should match restore action) | + +## How It Works + +### The Problem + +TypeScript's `tsc --build` mode compares file modification timestamps: +- If source files are **newer** than `.tsbuildinfo` → rebuild project +- If source files are **older** than `.tsbuildinfo` → skip rebuild + +Git doesn't preserve timestamps. On checkout, all files get the **current** timestamp, making TypeScript think everything changed. + +### The Solution + +``` +1. Restore Cache + └─> Bring back .tsbuildinfo and .d.ts files from previous builds + +2. Reset All Timestamps + └─> Set all files to base branch commit timestamp (stable baseline) + +3. Detect Changed Files + └─> Compare current HEAD vs base branch using git + +4. Update Changed Timestamps + └─> Set only changed files to current timestamp (marking them as "new") + +5. Run TypeScript + └─> TypeScript sees: changed files = new, unchanged files = old + └─> Only rebuilds changed packages + dependents +``` + +### Timestamp State + +| Stage | Source Files | .tsbuildinfo | TypeScript Behavior | +|-------|-------------|--------------|---------------------| +| After Checkout | Current time | Missing | Rebuild everything | +| After Cache Restore | Current time | Base time | Rebuild everything | +| After Reset All | Base time | Base time | Skip everything | +| After Update Changed | Changed: Current
Unchanged: Base | Base time | Rebuild only changed | + +## File Patterns + +Choose patterns based on your TypeScript configuration: + +### Common Patterns + +```yaml +# Minimal (incremental build metadata only) +files: | + **/*.tsbuildinfo + +# With Generated Types +files: | + **/*.tsbuildinfo + **/types/**/*.d.ts + +# With Custom Output Directory +files: | + **/*.tsbuildinfo + dist/**/*.d.ts + build/**/*.d.ts + +# Aggressive (all type definitions) +files: | + **/*.tsbuildinfo + **/*.d.ts +``` + +### Pattern Guidelines + +- ✅ Cache build outputs (`.tsbuildinfo`, generated `.d.ts`) +- ❌ Don't cache source files (already in git) +- ❌ Don't cache `node_modules` (use separate cache) +- ⚠️ Larger patterns = slower cache restore/save + +## Best Practices + +### 1. Save Cache Only on Main Branch + +```yaml +- if: github.ref == 'refs/heads/main' + uses: Attest/typescript-cache-action/save@main +``` +**Why**: Prevents cache pollution from experimental feature branches. + +### 2. Use Consistent Keys + +Restore and save actions should use **identical** inputs: +```yaml +# Both actions +cache-base-key: ${{ runner.os }} +cache-key: ${{ github.sha }} +files: | + **/*.tsbuildinfo +``` + +### 3. Include OS in Cache Key + +```yaml +cache-base-key: ${{ runner.os }} +``` + +**Why**: Build artifacts may differ between Linux, macOS, Windows. + +### 4. Use SHA for Commit-Specific Cache + +```yaml +cache-key: ${{ github.sha }} +``` + +**Why**: Each commit gets its own cache entry with fallback to prefix match. + +### 5. Test File Patterns First + +```yaml +- name: Verify Cache Contents + run: | + echo "Files to cache:" + find . -name "*.tsbuildinfo" -o -name "types/**/*.d.ts" ``` -### CI problems +## Performance + +### Typical Speedup + +| Scenario | Build Time | Speedup | +|----------|-----------|---------| +| No cache (baseline) | 100% | - | +| Full cache, no changes | ~5-10% | 10-20x faster | +| Full cache, small changes | ~10-30% | 3-10x faster | +| Full cache, large refactor | ~50-80% | 1.2-2x faster | -TypeScript build mode (project references) checks what packages to build by comparing the modified timestamps of source and `.tsbuildinfo` files. If the src files are newer than the `.tsbuildinfo` file then it will detect this project and its dependencies need to be built. This works perfectly in local environments however in CI, this raises issues since git is decentralised and thus file meta information like timestamps are not stored. This makes sense for git as files being touched should not cause git to detect changes, only content changes should be checked in. +### Factors Affecting Performance -To improve the CI type checking stages the `.tsbuildinfo` and source files need to have their timestamps restored in a fashion that allows `tsc --build` mode to detect what to build, otherwise all packages will be rebuilt which is slower. +- **Cache Hit Rate**: Higher is better (aim for >80% on PRs) +- **Change Scope**: Fewer changed files = faster rebuilds +- **Dependency Graph**: Shallow dependencies = less cascading rebuilds +- **Project Structure**: Monorepos benefit more than single projects -#### Restore cache of TS output files +## Troubleshooting -Firstly this action restore the latest cache of the typescript output files (`files` input) from the default branch (`base-ref` input). This gives us the most recent and most stable cache. +### Type Check Still Rebuilds Everything + +**Possible Causes**: +1. Cache miss (no recent cache available) +2. File patterns don't match TypeScript output locations +3. Base branch not configured correctly + +**Debug**: +```yaml +- name: Debug Cache + run: | + echo "Cache files found:" + find . -name "*.tsbuildinfo" + echo "Timestamps:" + ls -lt **/*.tsbuildinfo | head -5 +``` -#### Restore timestamps to base ref +### Stale Cache Errors + +**Symptom**: Type errors about missing files that were deleted + +**Solution**: Clear cache and rebuild: +```bash +# Clear repository caches (requires admin) +gh api -X DELETE /repos/{owner}/{repo}/actions/caches +``` + +Or wait 7 days for automatic cache expiration. + +### Cache Size Growing + +**Solution**: Narrow file patterns to only necessary outputs: +```yaml +# Too broad (caches too much) +files: '**/*.d.ts' -At this stage, the cached timestamps are when the cache was created and the source files are when checked out. The modified timestamps of all files are restored to the base branch head commit timestamp. This ensures that the timestamps for all files are stable and are before any committed changes. +# Better (only generated types) +files: '**/types/**/*.d.ts' +``` + +## Compatibility + +| Platform | Status | Notes | +|----------|--------|-------| +| Ubuntu | ✅ Fully Supported | Primary platform, extensively tested | +| macOS | ✅ Fully Supported | Tested with latest runners | +| Windows | ⚠️ Untested | May require timestamp format adjustments | + +### TypeScript Requirements + +- ✅ `tsc --build` mode (project references) +- ❌ Plain `tsc` (doesn't use incremental metadata) + +### Monorepo Tools + +Compatible with: +- pnpm workspaces +- Nx +- Turborepo (alongside its own cache) +- Lerna +- Yarn workspaces + +## Advanced Usage + +### Multiple Cache Strategies + +```yaml +# OS + Node version specific cache +cache-base-key: ${{ runner.os }}-node${{ matrix.node-version }} +cache-key: ${{ github.sha }} +``` + +### Custom Base Branch + +```yaml +# For release branches +base-ref: release/v2.0 +``` + +### Conditional Cache Restore + +```yaml +# Skip cache on force rebuild +- if: "!contains(github.event.head_commit.message, '[no-cache]')" + uses: Attest/typescript-cache-action@main +``` + +## Contributing + +We use this action across Attest projects and welcome contributions! + +### Development + +```bash +# Test locally by referencing local path in workflow +uses: ./.github/actions/typescript-cache-action +``` + +### Reporting Issues + +- **Bugs**: Open an issue with workflow logs and repository structure +- **Feature Requests**: Describe use case and expected behavior +- **Security**: See [security guidelines](.agents/rules/security.md) + +## Documentation -### Restore timestamps of changed files in git -For any changed files detected by git the modified timestamps are restored to current timestamp. +- **[AGENTS.md](AGENTS.md)** - Comprehensive technical reference for AI agents +- **[ARCHITECTURE.md](docs/ARCHITECTURE.md)** - System design and diagrams +- **[Code Style](.agents/rules/code-style.md)** - YAML and shell conventions +- **[Testing](.agents/rules/testing.md)** - Testing strategies and best practices +- **[Security](.agents/rules/security.md)** - Security considerations and guidelines -This gives us a state of: +## License -- ts build modified timestamps: base commit timestamp -- unchanged source modified timestamps: base commit timestamp -- changed source modified timestamps: current timestamp +MIT License - see [LICENSE](LICENSE) for details -### Run type check +Copyright (c) 2025 Attest Technologies Limited -As the state of the modified timestamps are corrected typescript can now run and detect changed packages. It will now only rebuild these packages and their dependents. +## Acknowledgments -Run typecheck at this stage +- Built on top of GitHub's official [actions/cache](https://github.com/actions/cache) +- Uses [tj-actions/changed-files](https://github.com/tj-actions/changed-files) for change detection +- Maintained by [@Attest/frontend](https://github.com/orgs/Attest/teams/frontend) -### Save cache of ts build files +--- -After the type check as been run the ts build output has been updated. If on the default branch we save this cache to speed up further builds. \ No newline at end of file +**Status**: Production • **Maintained By**: @Attest/frontend • **License**: MIT diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b652551 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,539 @@ +# Architecture Documentation + +## Overview + +`typescript-cache-action` is a **composite GitHub Action** that solves incremental TypeScript builds in CI by managing build artifact caching and file timestamp restoration. + +## Core Problem Statement + +TypeScript's `tsc --build` mode uses file modification timestamps to determine what needs rebuilding: +- If source files are newer than `.tsbuildinfo`, rebuild that project +- If `.tsbuildinfo` doesn't exist, rebuild everything +- If source files are older than `.tsbuildinfo`, skip rebuild + +**Challenge**: Git doesn't preserve file timestamps. On checkout, all files get the current timestamp, causing TypeScript to rebuild everything every time. + +## Solution Architecture + +### High-Level Flow + +```mermaid +graph TD + A[Checkout Code] --> B[Restore Action] + B --> C[Restore Cache] + C --> D[Reset All Timestamps to Base] + D --> E[Detect Changed Files] + E --> F[Update Changed File Timestamps] + F --> G[Run tsc --build] + G --> H{On Main Branch?} + H -->|Yes| I[Save Action] + H -->|No| J[End] + I --> K[Save Cache] + K --> J +``` + +### Detailed Workflow + +```mermaid +sequenceDiagram + participant W as Workflow + participant RA as Restore Action + participant GH as GitHub Cache + participant Git as Git + participant CF as Changed Files + participant FS as File System + participant TS as TypeScript + participant SA as Save Action + + W->>RA: Trigger restore + RA->>Git: Fetch base-ref branch + Git-->>RA: Branch fetched + + RA->>GH: Restore cache (files pattern) + GH-->>RA: Cache restored (or miss) + + RA->>Git: Get base branch commit time + Git-->>RA: Timestamp + RA->>FS: Set all files to base timestamp + FS-->>RA: Timestamps updated + + RA->>CF: Detect changed files vs base-ref + CF-->>RA: List of changed files + RA->>FS: Set changed files to current time + FS-->>RA: Timestamps updated + + W->>TS: Run tsc --build + TS->>FS: Check timestamps + FS-->>TS: Changed files newer than cache + TS->>TS: Rebuild only changed projects + TS-->>W: Build complete + + alt On main branch + W->>SA: Trigger save + SA->>GH: Save cache (files pattern) + GH-->>SA: Cache saved + end +``` + +## Component Architecture + +### 1. Main Action (`action.yml`) + +**Purpose**: Restore cached build artifacts and prepare file system for incremental build + +**Inputs**: +```yaml +cache-base-key: string # OS or other stable prefix +cache-key: string # Unique key (usually commit SHA) +base-ref: string # Branch to compare against (default: main) +files: string # Glob patterns for TS output files +``` + +**Steps**: + +#### Step 1: Fetch Base Reference +```yaml +- name: get ref + shell: bash + run: git fetch origin ${{ inputs.base-ref }}:${{ inputs.base-ref }} +``` + +**Why**: Ensures base branch exists locally for comparison and timestamp extraction. + +#### Step 2: Restore Cache +```yaml +- name: restore tsc build cache + uses: actions/cache/restore@v4 + with: + path: ${{ inputs.files }} + key: ${{ inputs.cache-base-key }}-${{ inputs.cache-key }} + restore-keys: | + ${{ inputs.cache-base-key }}-${{ inputs.cache-key }} + ${{ inputs.cache-base-key }} +``` + +**Cache Strategy**: +1. Exact match: `{base-key}-{commit-sha}` (ideal) +2. Prefix match: `{base-key}-*` (fallback to recent cache) + +**Files Cached**: +- `.tsbuildinfo` - TypeScript incremental build metadata +- Generated `.d.ts` - Type definition files + +#### Step 3: Reset All Timestamps +```yaml +- name: restore timestamps of all files to base branch commit + shell: bash + run: | + find . -type f \ + -exec touch -c -m -t $(git log -1 --format=%cd --date=format:%y%m%d%H%M.%S ${{ inputs.base-ref }}) {} + +``` + +**Purpose**: Set every file's modification time to base branch HEAD commit time. + +**Result**: +- All source files: base commit timestamp +- All cached `.tsbuildinfo`: base commit timestamp (from cache) +- All files appear "unchanged" to TypeScript + +#### Step 4: Detect Changed Files +```yaml +- name: changed source files + uses: tj-actions/changed-files@v45 + id: changed-source-files + with: + base_sha: ${{ inputs.base-ref }} +``` + +**Output**: Space-separated list of files changed between current HEAD and base-ref + +#### Step 5: Update Changed File Timestamps +```yaml +- name: restore timestamps to source files with head ref timestamp + if: steps.changed-source-files.outputs.any_changed == 'true' + shell: bash + run: touch -c -m ${{ steps.changed-source-files.outputs.all_changed_files }} +``` + +**Purpose**: Mark changed files as "newer" by setting to current time. + +**Result**: +- Changed source files: current timestamp (newer than `.tsbuildinfo`) +- Unchanged source files: base commit timestamp (same as `.tsbuildinfo`) + +### 2. Save Action (`save/action.yml`) + +**Purpose**: Save updated build artifacts to cache + +**Inputs**: +```yaml +cache-base-key: string # Same as restore +cache-key: string # Same as restore +files: string # Same as restore +``` + +**Steps**: + +#### Save Cache +```yaml +- name: save tsc build cache + uses: actions/cache/save@v4 + with: + path: ${{ inputs.files }} + key: ${{ inputs.cache-base-key }}-${{ inputs.cache-key }} +``` + +**When to Run**: Only on default branch (main/master) to avoid cache pollution from feature branches. + +## Timestamp State Machine + +```mermaid +stateDiagram-v2 + [*] --> Checkout: git checkout + Checkout --> AllCurrentTime: All files have current timestamp + + AllCurrentTime --> CacheRestored: Restore cache + CacheRestored --> AllBaseTime: touch all files to base time + + AllBaseTime --> ChangedDetected: Detect changed files + ChangedDetected --> FinalState: touch changed files to current time + + FinalState --> TSCBuild: Run tsc --build + TSCBuild --> [*] + + note right of AllCurrentTime + Source: current time + .tsbuildinfo: N/A (not present) + end note + + note right of AllBaseTime + Source: base commit time + .tsbuildinfo: base commit time + end note + + note right of FinalState + Changed source: current time + Unchanged source: base commit time + .tsbuildinfo: base commit time + end note +``` + +### TypeScript's Decision Logic + +```mermaid +flowchart TD + A[TypeScript checks project] --> B{.tsbuildinfo exists?} + B -->|No| C[Rebuild project] + B -->|Yes| D{Source newer than .tsbuildinfo?} + D -->|Yes| C + D -->|No| E[Skip rebuild] + + C --> F{Has dependents?} + F -->|Yes| G[Rebuild dependents] + F -->|No| H[Done] + G --> H + E --> H +``` + +**With Proper Timestamps**: +- Unchanged files: base time = `.tsbuildinfo` time → Skip ✅ +- Changed files: current time > `.tsbuildinfo` time → Rebuild ✅ + +**Without Action** (all files current time): +- All files: current time > `.tsbuildinfo` time → Rebuild everything ❌ + +## Cache Strategy + +### Cache Key Design + +``` +{cache-base-key}-{cache-key} +``` + +**Example**: +``` +Linux-abc123def456 # Exact commit cache +Linux- # Prefix fallback +``` + +### Cache Hierarchy + +```mermaid +graph TD + A[Request Cache] --> B{Exact Key Match?} + B -->|Yes| C[Return Exact Cache] + B -->|No| D{Prefix Match?} + D -->|Yes| E[Return Latest Matching Cache] + D -->|No| F[Cache Miss] + + C --> G[Cache Hit - Full Benefit] + E --> H[Partial Hit - Some Benefit] + F --> I[No Benefit - Full Rebuild] +``` + +### Cache Lifecycle + +```mermaid +sequenceDiagram + participant PR as PR Branch + participant Main as Main Branch + participant Cache as GitHub Cache + + PR->>Cache: Request cache (prefix: Linux-) + Cache-->>PR: Return latest main cache + PR->>PR: Restore timestamps + PR->>PR: Run tsc (partial rebuild) + Note over PR: ❌ Don't save cache (not main) + + Main->>Cache: Request cache (prefix: Linux-) + Cache-->>Main: Return own cache + Main->>Main: Restore timestamps + Main->>Main: Run tsc (minimal rebuild) + Main->>Cache: ✅ Save updated cache +``` + +**Benefits**: +- PR builds get speed boost from main cache +- Only main branch saves cache (stable, trusted) +- Cache can't be poisoned by PR branches + +## File Pattern Matching + +### Common Patterns + +```yaml +files: | + **/*.tsbuildinfo # All incremental build files + **/types/**/*.d.ts # Generated types in types/ dirs + dist/**/*.d.ts # Built types in dist/ + build/**/*.d.ts # Built types in build/ +``` + +### Pattern Selection + +```mermaid +flowchart TD + A[Choose Files to Cache] --> B{Composite Build?} + B -->|Yes| C[**/*.tsbuildinfo] + B -->|No| D[*.tsbuildinfo] + + C --> E{Type Generation?} + D --> E + E -->|Yes| F[Add generated .d.ts paths] + E -->|No| G[Done] + F --> G + + F --> H{Custom Output Dir?} + H -->|Yes| I[Match output structure] + H -->|No| J[Use **/types/**/*.d.ts] +``` + +## Dependencies + +### External Actions + +```mermaid +graph LR + A[typescript-cache-action] --> B[actions/cache/restore@v4] + A --> C[actions/cache/save@v4] + A --> D[tj-actions/changed-files@v45] + + B --> E[GitHub Cache API] + C --> E + D --> F[Git Diff] +``` + +**Dependency Tree**: +- `actions/cache/*` - Official GitHub caching actions + - Handles cache storage/retrieval + - Manages cache keys and fallbacks + - Platform-agnostic + +- `tj-actions/changed-files` - File change detection + - Compares commits efficiently + - Handles renames and moves + - Outputs in consumable format + +### System Dependencies + +- **Git**: Required for branch operations and change detection +- **Bash**: Shell for composite action steps +- **find**: File traversal for timestamp updates +- **touch**: Timestamp modification + +## Performance Characteristics + +### Time Complexity + +**Cache Operations**: +- Restore: O(n) where n = number of cached files +- Save: O(n) where n = number of cached files + +**Timestamp Operations**: +- Reset all: O(m) where m = total files in repo +- Update changed: O(c) where c = number of changed files + +**TypeScript Build**: +- No cache: O(p) where p = all projects +- With cache: O(k) where k = changed projects + dependents + +### Space Complexity + +**Cache Size**: +``` +Size = Σ(tsbuildinfo files) + Σ(generated .d.ts files) +``` + +**Typical Sizes**: +- Small project: ~1-10 MB +- Medium monorepo: ~10-100 MB +- Large monorepo: ~100-500 MB + +**GitHub Limits**: +- Max cache size per entry: 10 GB +- Max total cache size per repo: 10 GB +- Caches auto-expire after 7 days of no access + +### Performance Gains + +```mermaid +graph LR + A[Full Rebuild
Baseline: 100%] --> B[With Cache
Typical: 10-20%] + A --> C[Small Changes
Best Case: 5%] + A --> D[Large Changes
Worst Case: 80%] +``` + +**Factors Affecting Performance**: +1. **Cache Hit Rate**: Higher = better performance +2. **Change Scope**: Fewer changes = faster rebuild +3. **Dependency Graph**: Shallow deps = less rebuild cascading +4. **Cache Freshness**: Recent cache = fewer cumulative changes + +## Error Handling + +### Failure Modes + +```mermaid +graph TD + A[Action Execution] --> B{Cache Restore Fails?} + B -->|Yes| C[Continue with Empty Cache] + B -->|No| D{Base Ref Missing?} + + D -->|Yes| E[Workflow Fails] + D -->|No| F{Changed Files Detection Fails?} + + F -->|Yes| G[Touch No Files
All Treated as Unchanged] + F -->|No| H[Success] + + C --> I[Full Rebuild] + G --> J[Minimal Rebuild
May Miss Changes] + I --> K[Slower but Correct] + J --> L[Faster but Potentially Incomplete] +``` + +### Graceful Degradation + +**Cache Miss**: Action succeeds, build runs fully (slow but correct) +**No Changed Files**: Action succeeds, all files old (fast but may miss changes) +**Missing Base Ref**: Action fails early (prevents incorrect results) + +## Integration Points + +### Workflow Integration + +```mermaid +graph LR + A[actions/checkout] --> B[typescript-cache-action] + B --> C[Install Dependencies] + C --> D[tsc --build] + D --> E{Main Branch?} + E -->|Yes| F[typescript-cache-action/save] + E -->|No| G[End] + F --> G +``` + +### Tool Compatibility + +**Works With**: +- `tsc --build` (native TypeScript) +- Monorepo tools (Nx, Turborepo, pnpm, Lerna) +- Any tool respecting `.tsbuildinfo` timestamps + +**Doesn't Work With**: +- `tsc` without `--build` flag (doesn't use `.tsbuildinfo`) +- Tools that ignore file timestamps (e.g., hash-based caching) + +## Design Decisions + +### Why Composite Action? + +**Alternatives Considered**: +1. **TypeScript Action**: Requires compilation, distribution, more complex +2. **Docker Action**: Slower startup, platform limitations +3. **Composite Action**: ✅ Simple, fast, maintainable + +**Trade-offs**: +- ✅ No build step required +- ✅ Easy to understand and modify +- ✅ Leverages existing actions +- ❌ Limited to shell commands +- ❌ No custom logic (conditionals, loops) + +### Why Two Separate Actions? + +**Restore** and **Save** are split because: +1. Restore runs on all branches +2. Save runs only on main branch +3. Workflow-level conditionals clearer than action-level +4. Follows `actions/cache` pattern + +### Why Touch Timestamps? + +**Alternatives Considered**: +1. **Custom TypeScript Plugin**: Complex, fragile +2. **Rebuild Detection Script**: Requires analysis of build graph +3. **Touch Timestamps**: ✅ Simple, reliable, TypeScript-native + +## Future Enhancements + +### Potential Improvements + +1. **Cache Analytics** + - Report cache hit/miss rates + - Track build time savings + - Suggest file pattern optimizations + +2. **Smart File Patterns** + - Auto-detect TypeScript config + - Suggest optimal cache patterns + - Warn about over-caching + +3. **Parallel Timestamp Updates** + - Use `xargs -P` for faster touch operations + - Meaningful for repos with 100k+ files + +4. **Cross-Platform Testing** + - Add Windows runner support + - Handle Windows timestamp format differences + - Test with Git for Windows quirks + +### Compatibility Matrix + +| Platform | Status | Notes | +|----------|--------|-------| +| Ubuntu | ✅ Supported | Primary platform | +| macOS | ✅ Supported | Fully tested | +| Windows | ⚠️ Untested | May need timestamp format adjustment | + +## References + +- [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) +- [TypeScript Build Mode](https://www.typescriptlang.org/docs/handbook/project-references.html#build-mode-for-typescript) +- [GitHub Actions Cache](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) +- [Composite Actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) +- [Touch Command](https://man7.org/linux/man-pages/man1/touch.1.html) + +--- + +**Last Updated**: 2026-03-25 +**Maintained By**: @Attest/frontend