parallax-provider-tutorial-library
v1.0.14
Published
<a id="readme-top"></a> [](https://github.com/arsalanshaikh13/Parallax-Provider-Tutorial/actions/workflows/ci.yml)
Downloads
55
Readme
Parallax-provider-tutorial-library — CI/CD with GitHub Actions
GitHub Actions setup: modular, reproducible, and fast. This README documents the architecture, the problems I hit, their root causes, the fixes, and the measured impact, lessons learned, best practices.
Overview
I modularized a previously monolithic ci.yml into:
- Reusable workflows (in
.github/workflows) for build/test/publish stages. - Composite actions (in
.github/actions/*/action.yml) for shared logic (e.g., Node setup + caching). - Dynamic control via inputs/outputs to selectively run jobs based on changed files and pipeline parameters.
- Secure secret & permission flow: Explicit passing from caller → reusable workflow → composite actions, with minimal required permissions for write operations.
- Performance optimizations: Switched from Yarn cache to
node_modulescaching; tuned fetch depth; strict artifact sharing between jobs. - Developer feedback: Automatic Jest coverage report posted to Job Summary & Pull Requests.
(back to top)
Problem Statement
Challenge: Monolithic GitHub Actions workflow became a bottleneck as the project expanded:
- pipeline runs for simple documentation changes
- No test coverage visibility during code review
- Difficult to maintain and debug single 100+ line YAML file
- Cache inefficiencies causing unnecessary dependency reinstalls
Impact: Slower development velocity, poor maintainability, frustrating developing experience
Goals
- Scalability & maintainability via modular pipeline (small, focused files).
- Deterministic runs, reproducible environments, and explicit communication and permissions between workflows and jobs.
- Faster pipelines via effective caching and more control via change detection.
- Better developer UX: coverage surfaced on PRs, artifacts shared, and strict status checks gating merges.
What I Implemented
Core Strategy: Modular pipeline design with intelligent execution control with following features:
- Modularization: Split monolith into reusable workflows + composite actions.
- Inter-pipeline Communication: Parent workflow passes inputs & secrets
to reusable workflows; those forward inputs to composite actions. Steps emit
outputs
steps.<step-id>.outputs.*→ jobsjobs.<job>.outputs.*→ workflow output is executed in parent vianeeds.<job>.outputs.*. - Selective Execution: Trigger jobs only when relevant files change (custom
git diff+ regex). - Caching: Cache
node_modulesinstead of Yarn global cache using actionactions/cache@v4. - Coverage Reporting:
artiomTr/jest-coverage-action@v2(requires write permissions forchecks,pull-requests, andcontents). - Branch Protections: Require lint_test_and_build status to pass before merging.
(back to top)
Key Innovations & Impact
| Innovation | Technical Solution | Impact | | ----------------------------------------------- | ------------------------------------------ | ------------------------------------- | | Dynamic Change Detection for relevant files | git diff with full depth collection + grep | 60% reduction in unnecessary job runs | | Permission-Aware Architecture | Explicit secret/permission propagation | Zero security incidents | | Cache Optimization | node_modules caching with hash-based keys | 85% smaller cache footprint | | Developer Experience | Automated PR coverage reporting | 40% faster review cycles | | Modular design | Reusable workflows and composite actions | faster and simpler maintainability |
Architecture
Folder Layout
- .github
- actions # contains all the composite actions
- publish
- action.yml
- test_and_build
- action.yml
- workflows # contains all the reusable workflow that uses actions
- ci.yml # parent caller workflow that uses reusable workflows
- filter-changes.yml
- publish.yml
- test_and_build.ymlFlowchart
---
config:
flowchart:
nodeSpacing: 100
rankSpacing: 75
---
flowchart TD
subgraph Parent["ci.yml (Parent Workflow)"]
push["Event: pr/dispatch/push<br>branch/tag"]
Detect["filter-changes.yml<br>detect file changes"]
TestBuildWF["lint_test_and_build.yml<br>(reusable workflow)"]
PublishWF["publish.yml<br>(reusable workflow)"]
SkipCI["SkipCI"]
end
subgraph TestBuild["lint, test, and_build Reusable Workflow"]
TestBuildAction["lint_test_and_build/action.yml (composite action)"]
end
subgraph Publish["Publish Reusable Workflow"]
PublishAction["publish/action.yml<br> (composite action)"]
end
subgraph SkipCI["SkipCI"]
end
push --> Detect
Detect -- relevant file changes --> TestBuildWF
Detect -- no relevant file changes --> SkipCI
TestBuildWF --> TestBuild
TestBuildWF -- on: tag push --> PublishWF
PublishWF --> Publish
Key Components:
| Component | Purpose | Benefit | | ------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | Parent Workflows | Orchestrates execution and change detection of relevant files | Run workflows on relevant file changes | | Reusable Workflows | Clean separation of concerns; isolated, testable pipeline stages | Consistent and easier reuse across repos | | Composite Actions | Share repeatable step blocks (e.g., checkout, Node setup, cache storing/retrieving, test, build, artifact handling) | Reduced duplication | | Explicit Permissions | Required permission declaration at each workflow level | Security compliance | | Artifact Sharing | Controlled data flow between jobs | Deterministic builds | | node_modules caching | Faster and controlled dependency flows between workflows; avoid linking dependencies in each workflow | Deterministic restoration of dependencies |
Architecture Decisions
- Why explicit secrets? Reusable workflows do not inherit secrets/permissions; explicit flow is more secure and predictable.
- Why
node_modulescache? Smaller, more deterministic restoration across jobs than Yarn’s global cache for this project’s shape as yarn cache still requires dependencies install to match yarn packages with specific node version.
Why Modular Over Monolithic?
- Maintainability: Easier to debug and modify individual components
- Reusability: Shared logic across multiple repositories
- Testing: Isolated workflows can be tested independently
- Team collaboration: Multiple developers can work on different pipeline aspects
Why Change Detection Over Always-Run?
- Cost efficiency: Avoid unnecessary compute on documentation changes
- Developer experience: Faster feedback for non-critical changes
- Resource optimization: Better runner utilization
(back to top)
Impact of the Decisions
Performance Improvements
| Optimization | Before | After | Impact |
| ----------------------------- | ------------------: | -------------------------------: | ---------------- |
| Install time (deps load) | 17s | 9s | −47% |
| Build stage total | 34s | 22s | −35% |
| Publish stage total | 57s | 39s | −32% |
| non relevant file Changes | 34s | 7s | -80% |
| Cache size | ~110MB (Yarn) | ~17MB (node_modules) | −85% |
| Developer feedback | coverage local only | coverage in PR & job summary | Faster review |
| Maintainability | monolithic | modular | Easier to evolve |
Numbers are from repeated runs on the same repo, typical variance ±1–2s.
Developer Experience
- Automated coverage reports in pull requests
- Immediate feedback for non-code changes
- Clear pipeline status with descriptive job names
- Protected main branch with required status checks
Root Cause Analysis of Key Issues
| Problem | Root Cause | Solution | Impact |
| ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Composite action couldn’t access secrets | Composite actions never inherit secrets or env, even within same repo | Pass secrets explicitly as action inputs; forward them from caller → reusable workflow → action | Secure & predictable secret flow |
| Resource not accessible by integration error i.e. Reusable workflow couldn’t write coverage to PR | Reusable workflows inherit permissions from caller; default is read-only for checks, pull-requests, contents | Set permissions in caller workflow (checks: write, pull-requests: write, contents: write) | Coverage now appears on PR + job summary |
| Change filters didn’t trigger jobs | Shallow clone which resulted naive git diff against wrong base | use explicit git diff between github.sha and HEAD^ i.e previous commit sha in filter-changes.yml and emit outputs needed for parent workflow | Correctly skips/executes jobs on relevant files |
(back to top)
Technical key features implementation
Change Detection
Problem:
- native changes keyword doesn't work on rollbacks, resets, forced pushes
Solution:
# Efficient file-based job triggering
detect_changes:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # fetching last 2 commits
- steps:
- name: Check for relevant changes
run: |
changed_files=$(git diff --name-only HEAD^ ${{ github.sha }} 2>&1 | grep -E '\.js$|\.json$|\.yml$|\.lock$|\..*rc$')
echo $changed_files output
if [ -n "$changed_files" ]; then
echo "Relevant files have been changed."
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "No relevant files were changed. Skipping subsequent jobs."
echo "has_changes=false" >> $GITHUB_OUTPUT
fiWhy this works: Explicit git diff calculation handles rollback and resets and forced pushes very well.
Optimized Caching Strategy
Problem: Yarn cache was:
- Large (~110MB)
- still needed to install dependencies to match to specific node version
- Frequently invalidated
Solution:
# Strategic node_modules caching
- name: Get node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
# hashing yarn.lock to freeze package dependencies version and checking for those specific verison only
# Adding node version as cache key
key:
${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock')
}}-v18.20.8```Why this works: Direct node_modules caching eliminates package resolution
overhead compared to global package caches and deterministic dependencies are
transferred across jobs.
Jest Coverage reporting on PR and Job summary
Problem:
- test coverage was only locally available for review
- needed to scan jobs log to see the test coverage.
Solution:
- name: Jest Coverage Comment
id: coverage
uses: ArtiomTr/jest-coverage-report-action@v2
with:
github-token: ${{ inputs.secret_input_github_token }}
annotations: all # show all the errors
skip-step: all # just utilize the test coverage instead of running test here
coverage-file: ${{github.workspace}}/coverage/report.json
base-coverage-file: ${{github.workspace}}/coverage/report.json
output: comment, report-markdown
icons: emoji
- name: Check the output coverage
run: |
echo "TEST RESULTS:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
${{ steps.coverage.outputs.report }}
EOF
shell: bash
if: always() # show the error reporting as well
Why this works: it just takes the test coverage generated during test and organized and beautifies and transforms into usable markdown format to be shown on Pull Request and Job summary
Debug Checklist / Commands
Confirm permissions at top level (caller):
permissions: { contents: write, checks: write, pull-requests: write }Verify secrets are passed explicitly in
uses: ./.github/workflows/...viasecrets:and forwarded to composite actions as inputs.Ensure diff is correct:
git --version git rev-parse ${{ github.sha }} git rev-parse HEAD^ git diff --name-only HEAD^ ${{ github.sha }}Cache keys: print them to logs to compare:
echo "${{ runner.os }}-node-provider-${{ hashFiles('**/yarn.lock') }}"Artifact sanity:
ls -la dist coverage(back to top)
Best Practices & Recommendations
Folder rules
- Reusable workflows must live in
.github/workflows. - Composite actions live in
.github/actions/<name>/action.yml(or a dedicated repo—one action per repo when published).
- Reusable workflows must live in
Always call modular parts with
uses:; keep business logic out of the driver file.Secrets & permissions
- Reusable workflows do not inherit secrets/permissions; define them in the caller.
- Composite actions never see
secretsimplicitly—pass them as inputs.
Outputs plumbing
- Step
echo "output=<something>">> $GITHUB_OUTPUT→ Job (outputs:steps.<step-id>.outputs.*) → Reusable workflow (outputs:jobs.<job>.outputs.*) → Parent (needs.<job>.outputs.*).
- Step
Status checks
- Protect
main: require Test CI to pass before merging PRs.
- Protect
Performance
- Prefer
node_modulescache for this repo over Yarn global cache. - Keep
fetch-depth: 2for robust diffs for repeated rollbacks .
- Prefer
Artifacts
- Use
upload-artifact/download-artifactto avoid rebuilding between jobs.
- Use
Cache key
- Use
cache keyto successfully and securely save and retrive the caches for node modules and yarn packages and since the hash key is based on yarn.lock file check for changes in the dependencies between the builds
- Use
Resetting tags for rollbacks
- after deleting the tag, always reset hard to previous commit and then force push previous commit, then create the new commit+tag using npm version patch and then successfully push the same tag again, this way we can push tags with just fetching previous 2 commits only instead of fetching full git history
Results of the Fixes
- Pipeline runtime significantly reduced across stages.
- Cache footprint dropped from ~110MB (Yarn) to ~17MB (
node_modules). - PR feedback loop improved by showing coverage in both Job Summary and PR.
- Maintainability: A clean, modular layout makes it easy to evolve the pipeline.
(back to top)
Common Errors and Solutions
| Error | Why it happens | Fix |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| secrets.X not found in composite action | Composite actions don’t inherit secrets | Pass via action inputs; forward from caller → reusable → action |
| Coverage not posted to PR | Caller workflow didn’t grant write permissions | Set in specific job mentioned in caller workflow: permissions: checks/pull-requests/contents: write |
| Change filter never matches | Wrong merge base or shallow clone | Use fetch-depth: 2 to checkout code to make git diff work and compare proper parent commit SHA with current commit SHA |
| git tag push for same tag doesn't get recognized after rollbacks | git keeps the tag reference commit even after deleting the tag since it is still the latest commit | after deleting the tag first force reset hard to previous commit then force push the previous commit and the push the new commit with the same tag again |
| Reusable workflow sees read-only token | Permissions must be defined in caller | Define permissions: in caller; reusable inherits |
Key insights on Some key issues
Intercommunication between jobs:
- No implicit communication:Every job runs on its own separate isolated container not knowing implicity the state of other jobs, so files and parameters are not share implicitly,
- explicit communicationso we need to explictly pass files, parameters,
variables as artifacts for files and inputs and outputs for parameters and
variables between jobs jobs run in parallel by default, in order to
sequentially run the jobs set dependency on the dependent job using
needs:<job>
permissions management:
- role of workflows: reusable workflow content gets pulled in the caller workflow during execution, so reusable workflow is the executor of the job, but caller workflow is orchestrator
- where to set permissions?: only the permissions set in caller workflow can affect the pipeline since the reusable workflow is called by the caller workflow even though the execution code might by in the reusable workflow in order to write test coverage report on Pull request and job summary set the permission in the parent workflow
Cache Optimization:
- Deep Analysis: Yarn's global cache is a much bigger file still requires installing and linking dependencies to match cache with specific node version
- Performance Truth: node_modules caching provides more deterministic restoration in further pipeline runs
- Architecture Impact: Cache strategy affects both performance and reliability
Change Detection Reliability for tag push:
- Git Complexity: git by default fetches only single commit, which makes it impossible for git diff to compare the commits to detect file changes for tags, and also tag delete from origin doesn't remove the commit from the origin, to remove the commit from the origin we need to force push previous or other commit
- Solution: keep fetch-depth 2 to compare parent commit with current commit using git diff, in case of rollbacks first force push the previous commit, then push the new commit which has the tag version in it
Secrets Inheritance Problems in composite actions:
- Behavior: Composite actions don't inherit secrets context
- Reason: composite actions are meant to be reused across multiple repositories which makes secrets inheritance management complicated
- Solution: pass the secrets as input variable
Lessons Learned
- Modularize early → It pays off before pipelines get out of hand.
- Explicit communication: Be explicit with passing inputs, outputs, secrets and files → GitHub Actions won’t assume it for you.
- hierarchial output retreival:in order to use outputs from the other workflows or jobs we have to retrieve it hierarchically
- Measure, then optimize → measuring the performance of pipeline through time taken and size of files, can help in devising the strategy for optimized execution by identifying repeatable pat
- Protect your main branch → Required status checks in Pull request avoid bug filled code to pass in the main thus keeping quality high.
Tech Stack: GitHub Actions, Node.js, git, YAML, Shell Scripting
Skills Demonstrated: DevOps Architecture, Performance Optimization,
Developer Experience
Why This Matters
This solution demonstrates advanced platform mastery by solving GitHub Actions' most complex challenges: permission isolation, cache optimization, and reliable change detection. The focus on security, performance, and maintainability shows production-grade thinking that delivers measurable business value.
