The architecture of modern software development relies heavily on automation, and GitHub Actions has become a central pillar in this ecosystem. However, as workflows grow in complexity, engineers frequently encounter the limitation of isolated job execution contexts. By design, jobs within a single workflow run in separate runners, meaning environment variables set in one job are not automatically available in another. This isolation necessitates robust mechanisms for sharing data, such as build artifacts, configuration values, and secrets, across jobs and even across distinct workflows. Understanding the distinctions between composite actions, reusable workflows, and artifact sharing is critical for constructing efficient, maintainable, and secure CI/CD pipelines.
The Limitations of Job Isolation and Environment Variables
A fundamental concept in GitHub Actions is that each job runs on a fresh runner instance. Consequently, environment variables declared within a job are ephemeral and scoped strictly to that job’s execution. An attempt to set an environment variable in Job A and access it in Job B will fail because Job B does not inherit the runtime environment of Job A. This limitation often frustrates developers attempting to pass simple values, such as a release URL or a version number, from a build job to a deployment job.
While users have historically sought extensions or third-party solutions to bridge this gap, native solutions have evolved to address these needs. For instance, some developers have utilized community extensions to share variables, often storing data in temporary locations to be retrieved by subsequent jobs. However, the native approach now relies on specific syntax for job outputs. The jobs.<job_id>.outputs syntax allows a job to expose data to downstream jobs that depend on it via the needs keyword. Despite these improvements, some practitioners note limitations in flexibility, particularly when dealing with complex parameter passing or when the value detection fails due to timing or formatting issues. The consensus among advanced users is that while the output mechanism works, it requires precise configuration to avoid "value not detected" errors.
Composite Actions for Step Reuse
When the goal is to share specific steps rather than entire workflows, composite actions provide the native solution. A composite action is essentially a group of steps intended to be used within a workflow file. This mechanism is particularly useful for scenarios such as building an Electron application across multiple platforms, where the core build logic remains consistent but the environment differs.
To create a composite action, developers must adhere to a strict directory structure. The action must reside in a directory containing a file named action.yml. Renaming this file or placing it outside the required directory structure will result in execution errors. The action.yml file must specify using: "composite" at the top level to indicate that this is a composite action.
yaml
name: "My Shared Steps"
runs:
using: "composite"
steps:
- run: echo "Hello, World"
shell: bash
These files can be stored anywhere within the repository, but a common convention is to place them in a .github/actions folder at the root, such as .github/actions/my-shared-steps/action.yml. Before a composite action can be used, the repository code must be checked out, as the action definition resides in the repository itself. This method ensures that common logic is defined once and reused across multiple workflow files, reducing redundancy and maintenance overhead.
Reusable Workflows for Cross-Workflow Automation
While composite actions handle step-level reuse, reusable workflows allow for the invocation of entire workflows from other workflows. This is distinct from using actions within job steps; reusable workflows are called directly at the job level. This feature enables organizations to centralize complex automation logic, such as security scanning or deployment pipelines, in a central repository and invoke them from many different repositories.
There are two primary syntaxes for referencing reusable workflows:
- External Repository:
{owner}/{repo}/.github/workflows/{filename}@{ref} - Same Repository:
./.github/workflows/{filename}
When referencing a workflow in an external public or private repository, the {ref} can be a commit SHA, a release tag, or a branch name. Security best practices dictate that using the commit SHA is the safest option for stability and security, as it prevents unexpected changes if a branch is updated. If a release tag and a branch share the same name, the release tag takes precedence. For workflows in the same repository, the reference is tied to the same commit as the caller workflow, and prefixes like refs/heads are not allowed. Contexts or expressions cannot be used in this keyword.
yaml
jobs:
call-workflow-1-in-local-repo:
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
call-workflow-2-in-local-repo:
uses: ./.github/workflows/workflow-2.yml
call-workflow-in-another-repo:
uses: octo-org/another-repo/.github/workflows/workflow.yml@v1
A single caller workflow can invoke multiple reusable workflows, with each invocation defined as a separate job. This modularity allows for complex orchestration where different stages of the pipeline are managed by separate, specialized workflow files.
Passing Data: Inputs, Secrets, and Outputs
Reusable workflows facilitate data exchange through inputs, secrets, and outputs. Inputs are named parameters passed from the caller to the called workflow using the with keyword. The data type of the input value must strictly match the type defined in the reusable workflow (boolean, number, or string). Secrets are passed using the secrets keyword.
yaml
jobs:
call-workflow-passing-data:
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
with:
config-path: .github/labeler.yml
secrets:
personal_access_token: ${{ secrets.token }}
For workflows within the same organization or enterprise, the inherit keyword can be used to implicitly pass all secrets from the caller to the called workflow. This simplifies configuration but requires careful consideration of security boundaries.
yaml
jobs:
call-workflow-passing-data:
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
with:
config-path: .github/labeler.yml
secrets: inherit
It is crucial to understand the propagation of secrets in chained workflows. Secrets are only passed to directly called workflows. In a chain A > B > C, if Workflow A passes secrets to Workflow B using inherit, Workflow B does not automatically pass them to Workflow C. Workflow B must explicitly define the secrets it wants to pass to Workflow C. Any secrets not explicitly passed by B will not be available to C.
```yaml
jobs:
workflowA-calls-workflowB:
uses: octo-org/example-repo/.github/workflows/B.yml@main
secrets: inherit
workflowB-calls-workflowC:
uses: different-org/example-repo/.github/workflows/C.yml@main
secrets:
repo-token: ${{ secrets.personalaccesstoken }}
```
Outputs from reusable workflows are also supported. If a reusable workflow sets outputs, these can be accessed by the caller using needs.<job_id>.outputs.<name>. If the reusable workflow executes with a matrix strategy, the output will be the value set by the last successfully completing job in the matrix that actually sets a value. This behavior is critical for aggregating results from parallelized test suites or build stages.
Artifacts for File-Based Data Sharing
While inputs and outputs handle small pieces of data like strings or booleans, artifacts are the mechanism for sharing larger files or directories between jobs. The actions/upload-artifact and actions/download-artifact actions are used for this purpose. Jobs dependent on artifacts must use the needs keyword to ensure they run after the job that created the artifact completes successfully.
In a typical scenario, Job 1 might perform a build or calculation, save the result to a file (e.g., math-homework.txt), and upload it as an artifact named homework_pre. Job 2, which depends on Job 1, can then download this artifact. If no name is specified during upload, the default name is artifact.
yaml
- name: Download a single artifact
uses: actions/download-artifact@v5
with:
name: my-artifact
To download all artifacts from a workflow run, the name parameter can be omitted. This creates a directory for each artifact, using its name as the directory name. This is useful when a workflow generates multiple distinct outputs that need to be processed together.
yaml
- name: Download all workflow run artifacts
uses: actions/download-artifact@v5
This approach is distinct from passing variables; artifacts persist as files and can be used for storing build results, logs, or test reports. For organizations using GitHub Enterprise Cloud, the audit log can be accessed via the REST API to monitor which workflows and artifacts are being used, providing an additional layer of observability and security compliance.
Conclusion
The ability to share data and logic between jobs and workflows is essential for scalable GitHub Actions implementations. Developers must choose the appropriate mechanism based on the nature of the data and the scope of the reuse. Composite actions are ideal for sharing repeated steps within workflows, while reusable workflows enable the modularization of entire automation pipelines across repositories. For data transfer, job outputs handle simple values, secrets secure sensitive information with careful propagation rules, and artifacts manage file-based data exchange. Mastery of these tools allows engineering teams to build robust, secure, and efficient CI/CD systems that minimize redundancy and maximize maintainability. As GitHub continues to refine these features, understanding the underlying mechanics ensures that teams can adapt to new capabilities without compromising pipeline integrity.