GitHub Workflows
This guide explains Pseudata’s CI/CD architecture, how to work with the workflows, and how to troubleshoot issues.
Architecture Overview
Section titled “Architecture Overview”Pseudata uses a reusable workflow pattern that provides consistent testing across all language SDKs while minimizing duplication.
Workflow Structure
Section titled “Workflow Structure”ci.yml (Aggregator) ↓ discovers and watches ├─ CI (Go) ├─ CI (Java) ├─ CI (Python) ├─ CI (TypeScript) └─ CI (TypeSpec) ↓ all call reusable-ci.yml ↓ uses .github/actions/setup-env ↓ then runs task <sdk>:setup task <sdk>:lint task <sdk>:test
release.yml ↓ calls validate.yml (reusable) ↓ uses .github/actions/setup-env ↓ then runs task setup task lint task test task generate (drift check)Key Concepts
Section titled “Key Concepts”1. CI Status Aggregator (ci.yml)
- Single status check for branch protection
- Auto-discovers workflows matching
CI (*) - Handles path filter behavior (passes when no workflows run)
- Fails if any discovered workflow fails
2. Reusable Workflow (reusable-ci.yml)
- Shared CI logic for all SDKs
- Uses
setup-envcomposite action for toolchain setup - Runs
task <sdk>:setup,lint,test - Cross-platform support (ubuntu, windows, macos)
3. Language-Specific CIs (ci-<lang>.yml)
- Triggers reusable workflow for specific SDK
- Path filtering (only runs when relevant files change)
- Matrix optimization (ubuntu on PRs, full matrix on push)
4. Validation Workflow (validate.yml)
- Reusable workflow for comprehensive checks
- Runs
task lint,task test,task generate(drift check) - Used by release workflow as pre-publish gate
- Manually triggerable for on-demand validation
5. Setup Environment Action (.github/actions/setup-env)
- Composite action for consistent toolchain setup
- Installs Go, Python, Java, Node.js, and Task
- Supports granular setup (all languages or specific ones)
- SHA-pinned dependencies, automatic caching
6. Release Automation (release.yml)
- Uses release-please for version management
- Pre-publish validation gate (calls
validate.yml) - Publishes to PyPI, NPM, Maven Central
- Go published via Git tags
Workflows Reference
Section titled “Workflows Reference”ci.yml - Status Aggregator
Section titled “ci.yml - Status Aggregator”Purpose: Single required status check that aggregates all language CIs
Triggers:
- Pull requests to
main - Pushes to
main
How It Works:
- Waits 15 seconds for GitHub to register workflow runs
- Uses
gh run listto discover workflows matching^CI\\(.+\\) - Watches each with
gh run watch --exit-status - Passes if all pass or if none found (path filters)
- Fails if any fail
Branch Protection Setup: Only require “CI” status check - individual language workflows are auto-discovered.
Example: Adding a new language
# Create ci-rust.yml with name: "CI (Rust)"# No changes needed to ci.yml - auto-discovered!reusable-ci.yml - Shared Logic
Section titled “reusable-ci.yml - Shared Logic”Purpose: DRY - single definition for setup, lint, test across all SDKs
Inputs:
sdk: Language identifier (go, java, python, typescript, typespec)path: Directory for caching dependenciesos: Runner OS (default: ubuntu-latest)
Steps:
- Checkout code
- Setup Development Environment (via
.github/actions/setup-env)- Conditionally installs toolchains based on
sdkinput - Installs Task runner
- Configures language-specific caching
- Conditionally installs toolchains based on
- Run
task <sdk>:setup - Run
task <sdk>:lint - Run
task <sdk>:test
Language Versions (defined in setup-env action):
- Go: 1.25 (from go.mod)
- Java: 17 (Temurin)
- Python: 3.14 (from .python-version)
- Node: 24 (from package.json)
Adding a New Language:
Update the setup-env action to include your language:
# In .github/actions/setup-env/action.yml- name: Setup Rust if: inputs.all == 'true' || inputs.setup-rust == 'true' uses: actions-rust-lang/setup-rust-toolchain@<SHA> # version tag with: toolchain: stable cache: trueThen update the reusable-ci.yml to pass the right flag:
- name: Setup Development Environment uses: ./.github/actions/setup-env with: all: "false" setup-rust: ${{ inputs.sdk == 'rust' }}ci-<language>.yml - Language CIs
Section titled “ci-<language>.yml - Language CIs”Purpose: Trigger reusable workflow for specific SDK with path filtering
Pattern:
name: CI (YourLanguage) # Must match this pattern!on: push: branches: ["main"] paths: ["sdks/yourlang/**", "Taskfile.yml", ".github/**"] pull_request: branches: ["main"] paths: ["sdks/yourlang/**", "Taskfile.yml", ".github/**"]
jobs: check: strategy: fail-fast: false matrix: os: [ubuntu-latest] include: - os: ${{ github.event_name == 'push' && 'windows-latest' || '' }} - os: ${{ github.event_name == 'push' && 'macos-latest' || '' }} exclude: - os: ""
uses: ./.github/workflows/reusable-ci.yml with: sdk: yourlang path: sdks/yourlang os: ${{ matrix.os }}Path Filters Include:
- SDK directory (
sdks/yourlang/**) - Root Taskfile (
Taskfile.yml) - All GitHub infrastructure (
.github/**) - covers workflows, actions, and any CI-related files
Matrix Strategy:
- Pull Request: Ubuntu only (fast feedback ~1-2 min)
- Push to main: Ubuntu + Windows + macOS (full coverage ~5-10 min)
- Savings: ~60% CI minutes on PRs
.github/actions/setup-env - Environment Setup
Section titled “.github/actions/setup-env - Environment Setup”Purpose: Composite action for consistent toolchain setup across all workflows
Features:
- All-in-one: Single action installs Go, Python, Java, Node.js, and Task
- Granular control: Can install all languages or specific ones
- Automatic caching: Language-specific dependency caching configured
- Version pinning: Reads versions from config files (go.mod, .python-version, package.json)
- SHA-pinned: All actions referenced by commit SHA for security
Inputs:
all: Setup all languages (default: “true”)setup-go: Setup Go only (requires all=“false”)setup-python: Setup Python only (requires all=“false”)setup-java: Setup Java only (requires all=“false”)setup-node: Setup Node.js only (requires all=“false”)
Usage Examples:
# Setup all languages (typical for validate workflow)- uses: ./.github/actions/setup-env
# Setup specific language (typical for CI workflows)- uses: ./.github/actions/setup-env with: all: "false" setup-python: "true"
# Setup multiple languages- uses: ./.github/actions/setup-env with: all: "false" setup-go: "true" setup-node: "true"Why Composite Action?:
- DRY: Single source of truth for toolchain versions and setup
- Consistency: Same environment across CI, validation, and release
- Maintainability: Update Go version once, applies everywhere
- Caching: Dependency caching configured once, works everywhere
Installed Tools:
- Go: With golangci-lint v2.7.2
- Python: With pip caching
- Java: With Maven caching
- Node.js: With npm caching
- Task: v3.x
validate.yml - Comprehensive Validation
Section titled “validate.yml - Comprehensive Validation”Purpose: Reusable workflow for full codebase validation
Dual Trigger:
- workflow_call: Called by other workflows (e.g., release)
- workflow_dispatch: Manual trigger from GitHub UI or CLI
Steps:
- Checkout code
- Setup all development environments
- Install dependencies (
task setup) - Run linters (
task lint) - Run tests (
task test) - Code generation drift check (
task generate+ git diff)- Ensures generated code is in sync
- Fails if uncommitted changes detected
- Prevents stale generated code in production
Used By:
- Release workflow: Pre-publish validation gate
- Manual validation: On-demand checks for any branch
Manual Trigger:
# Via GitHub CLIgh workflow run validate.yml --ref your-branch
# Or in GitHub UI: Actions → Validation → Run workflowWhy Separate from CI?:
- Comprehensive: Tests ALL languages, not just changed ones
- Drift detection: Validates generated code is up-to-date
- Release gate: Final quality check before publishing
- On-demand: Can validate any branch anytime
release.yml - Publishing
Section titled “release.yml - Publishing”Purpose: Automated version management and package publishing
Trigger: Push to main
Flow:
- release-please calculates versions, creates/updates PR
- When release PR merged: creates Git tag
- Validation gate (
validate.yml):- Runs comprehensive tests (all languages)
- Checks code generation drift
- Must pass before any publishing
- Publishes packages in parallel (after validation):
- Python → PyPI (via twine)
- TypeScript → NPM (@pseudata/core)
- Java → Maven Central (with GPG signing)
- Go → Validates module (published via Git tags)
Job Dependencies:
release-please ↓validate (must pass) ↓├─ publish-python├─ publish-npm├─ publish-java└─ validate-goRequired Secrets:
PYPI_TOKEN- PyPI API tokenNPM_TOKEN- NPM automation tokenMAVEN_CENTRAL_USERNAME,MAVEN_CENTRAL_TOKEN- Maven Central Portal (https://central.sonatype.com/)MAVEN_GPG_PRIVATE_KEY,MAVEN_GPG_PASSPHRASE- Java artifact signing
Validation Gate Benefits:
- Prevents publishing broken packages
- Catches code generation drift before release
- Tests all languages comprehensively
- Single failure blocks all publishing (safe by default)
Security Practices
Section titled “Security Practices”1. SHA Pinning (Required)
Section titled “1. SHA Pinning (Required)”All GitHub Actions MUST be pinned to commit SHAs, not tags.
❌ Wrong:
uses: actions/checkout@v4✅ Correct:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.2.2Why: Prevents tag manipulation attacks. Tags are mutable; commit SHAs are immutable.
How to Find SHAs:
- Go to action’s GitHub releases page
- Find the tag (e.g., v4.2.2)
- Click tag → copy commit SHA
- Add version comment for readability
2. Minimal Permissions
Section titled “2. Minimal Permissions”Use least privilege principle:
permissions: actions: read # For gh CLI to query workflows contents: read # For checkout
# Release workflow needs more:permissions: contents: write # Create tags/releases pull-requests: write # Create release PRs3. Secret Management
Section titled “3. Secret Management”Storing Secrets:
- Repository Settings → Secrets and variables → Actions
- Never commit secrets to code
- Use environment-specific secrets
Scoping Secrets:
publish-python: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} # Scoped to this job4. Dependency Caching
Section titled “4. Dependency Caching”Language-specific caching improves speed and reduces supply chain attacks:
- name: Setup Python uses: actions/setup-python@<SHA> with: python-version: "3.14" cache: "pip" cache-dependency-path: "sdks/python/pyproject.toml"Troubleshooting
Section titled “Troubleshooting”CI Failing on One Platform
Section titled “CI Failing on One Platform”Problem: Tests pass on Ubuntu but fail on Windows/macOS
Common Causes:
- Path separators: Use
path.join()or/(not\) - Line endings: Configure
.gitattributesor use\n - Case sensitivity: Windows/macOS are case-insensitive, Linux is not
- File permissions:
chmod +xneeded on Linux/macOS
Debug Steps:
# Add to workflow for debugging:- name: Debug run: | echo "OS: ${{ runner.os }}" echo "Path: $PATH" ls -laPath Filters Not Triggering
Section titled “Path Filters Not Triggering”Problem: Changed files but CI didn’t run
Check Path Patterns:
# Workflow must match changed filespaths: ["sdks/python/**", "Taskfile.yml", ".github/**"]
# TypeSpec changes might need to trigger SDK tests:# Consider: typespec/** → regenerates code → affects all SDKsCommon Pattern: Use .github/** to automatically capture:
- Workflow file changes (
.github/workflows/) - Action changes (
.github/actions/setup-env/) - Any other CI infrastructure changes
Testing Path Filters:
- Create test branch with small change
- Check if workflow appears in Actions tab
- Adjust paths if needed
Secrets Not Configured
Section titled “Secrets Not Configured”Problem: Publishing job fails with authentication error
Check Secrets:
- Repository Settings → Secrets and variables → Actions
- Verify secret name matches workflow:
PYPI_TOKENnotPYPI_API_TOKEN - Check secret has correct permissions (read/write)
Test Secrets (without exposing):
- name: Check Secret run: | if [ -z "${{ secrets.PYPI_TOKEN }}" ]; then echo "Secret not set!" exit 1 fi echo "Secret is configured"Aggregator Hanging
Section titled “Aggregator Hanging”Problem: CI status check never completes
Causes:
- Self-waiting: Aggregator shouldn’t discover itself (fixed with regex)
- Long-running workflow: Individual CI taking >60 minutes
- Workflow not found: Name doesn’t match
CI (*)
Fix:
# Ensure name matches pattern:name: "CI (YourLanguage)" # Not "CI-YourLanguage" or "YourLanguage CI"Release Not Publishing
Section titled “Release Not Publishing”Problem: Tag created but package not published
Check:
- Conditional: Verify
releases_createdoutput is true - Secrets: Publishing job has required tokens
- Version: Package version in files matches tag
- Build: Package builds successfully in CI
Debug:
- name: Debug Release run: | echo "Releases created: ${{ needs.release-please.outputs.releases_created }}" echo "Ref: ${{ github.ref }}"Maintenance Tasks
Section titled “Maintenance Tasks”Updating GitHub Actions
Section titled “Updating GitHub Actions”Frequency: Monthly or when security advisories published
Process:
-
Check action’s releases page for updates
-
Find new tag → get commit SHA
-
Update all workflows using that action:
# Beforeuses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0# Afteruses: actions/setup-go@<new-sha> # v6.2.0 -
Test in PR before merging
Tools:
- Dependabot can auto-update (configure
.github/dependabot.yml) - Renovate Bot alternative
Adding New Platforms
Section titled “Adding New Platforms”To test on new OS (e.g., ubuntu-arm64):
-
Update matrix in language CI:
matrix:os: [ubuntu-latest, ubuntu-arm64] # Add new platform -
Test separately first:
# Temporary test workflowon:workflow_dispatch:jobs:test-arm:runs-on: ubuntu-arm64steps:- uses: actions/checkout@<SHA>- run: task test -
Roll out gradually: Add to one SDK CI, then expand
Configuring New Secrets
Section titled “Configuring New Secrets”For new publishing target:
- Generate token at package registry (e.g., crates.io, rubygems.org)
- Add to GitHub: Settings → Secrets → New repository secret
- Name convention:
<REGISTRY>_TOKENor<REGISTRY>_API_KEY - Test: Create dummy release workflow in test repo first
Testing Workflows
Section titled “Testing Workflows”Local Testing (limited):
# Install act (GitHub Actions locally)brew install act
# Run workflowact pushBranch Testing (recommended):
# Create test branchgit checkout -b test/workflow-update
# Modify workflowvim .github/workflows/ci-python.yml
# Push and check Actions tabgit add .github/git commit -m "test: update Python CI"git push origin test/workflow-updatePerformance Optimization
Section titled “Performance Optimization”CI Minute Usage
Section titled “CI Minute Usage”Current Strategy:
- PRs: Ubuntu only → ~2 min/SDK → ~10 min total
- Push: 3 platforms → ~5 min/SDK → ~25 min total
- Path filters: Skip unchanged SDKs → save ~80%
Monthly Usage (example):
- 100 PRs/month × 10 min = 1,000 minutes
- 100 pushes × 25 min = 2,500 minutes
- Total: ~3,500 minutes/month (GitHub Free: 2,000 minutes)
Caching Strategy
Section titled “Caching Strategy”What’s Cached:
- Go modules (~100 MB)
- Maven dependencies (~200 MB)
- Pip packages (~50 MB)
- NPM packages (~150 MB)
Cache Hits: Save 30-60 seconds per run
Cache Keys:
# Go: uses go.sumcache: true
# Maven: uses pom.xmlcache: 'maven'cache-dependency-path: 'sdks/java/pom.xml'
# NPM: uses package-lock.jsoncache: 'npm'cache-dependency-path: 'sdks/typescript/package-lock.json'Cache Miss: Dependencies changed → new cache created
Parallel Execution
Section titled “Parallel Execution”All language CIs run in parallel:
- Without: 5 SDKs × 5 min = 25 min sequential
- With: max(5 min) = 5 min parallel
- Savings: 80%
Release publishing also runs in parallel:
- Python, TypeScript, Java all publish simultaneously
Best Practices
Section titled “Best Practices”For SDK Maintainers
Section titled “For SDK Maintainers”✅ Do:
- Pin actions to commit SHAs
- Use path filters to skip unnecessary runs
- Keep tests fast (under 5 min on Ubuntu)
- Test workflow changes in PRs
- Document required secrets
❌ Don’t:
- Pin to tags (security risk)
- Skip tests with
[skip ci](breaks consistency) - Add heavy dependencies (slows caching)
- Commit secrets to code
For Contributors
Section titled “For Contributors”✅ Do:
- Run
task testlocally before pushing - Check CI failures in Actions tab
- Ask for help in PR comments if CI fails
- Test on all platforms if possible
❌ Don’t:
- Ignore CI failures (“works on my machine”)
- Push repeatedly to trigger CI (run locally first)
- Modify workflows without understanding impact
Adding New Language SDK
Section titled “Adding New Language SDK”See Add New SDK Guide for complete instructions.
© 2025 Pseudata Project. Open Source under Apache License 2.0. · RSS Feed