## Sections
%% ## What problem does it solve %%
### Types of GitHub Secrets
GitHub (GH, GHE) offers support for 3 types of **Secrets**:
- **Organisation Secrets**
- scoped on the Organisation level, shared across multiple Repositories and 'GitHub Actions' (Workflows, Actions).
- **Repository Secrets**
- scoped on the Repository level, shared across multiple Environments and 'GitHub Actions' (Workflows, Actions).
- **Environment Secrets**
- scoped on the Environment level, shared across **only such** 'GitHub Actions' (Workflows, Actions) that have the Environment assigned to them.
#### Great, so what is the problem?
In case one needs to access the Secrets across different Repositories (e.g. for authentication purposes - Credentials), then they will use the Organisation's scope.
No need for Secrets across the entire Organisation? Then the Repository level should suffice.
Who would even need the 'Environment' scope if the Repository Secrets may be prefixed to differentiate in between Environments?
> This is where the problem begins and boy does it hit hard...
#### Limitations of Secrets
Quoting the official documentation on the product:
> "The following rules apply to secret names:
> - Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
> - Secret names must not start with the `GITHUB_` prefix.
> - Secret names must not start with a number.
> - Secret names are not case-sensitive.
> - Secret names must be unique at the level they are created at. For example, a secret created at the repository level must have a unique name in that repository, and a secret created at the organization level must have a unique name at that level.
> **If a secret with the same name exists at multiple levels, the secret at the lowest level takes precedence. For example, if an organization-level secret has the same name as a repository-level secret, then the repository-level secret takes precedence**."
Most of these limitations seem standard, the curious point regards the 'non case-sensitive' names, which may confuse some Operators. Certain Workflows may utilise GUI Environment name inputs to differentiate Secrets or Environments as properties on the Workflows. While other solutions, namely Azure DevOps offer string parsing functions, GitHub does not.
Hence the same Environment name can be used to reference different Secrets independently of which **case** they follow, whether it is a name in capital letters, small letters or mixed.
#### The real problem of GitHub Secrets
The previous limitations are not too problematic. The real problem is mentioned down the documentation page, as follows:
> "You can store up to 100 secrets per organization and 100 secrets per repository.
Secrets are limited to 48 KB in size."
Let's imagine a quite simple scenario.
- 'John DevOps' would like to create CI/CD pipelines for an Organisation. For simplified setup, the pipelines belong into a single Repository, so that the project is a Monolithic Repository.
- John is building Cloud infrastructure that requires basic authentication Secrets. There is a need for a Service Principal, perhaps custom Tokens for external 3rd party services, integrations and alike.
- John needs to export properties of Cloud infrastructure deployed Resources, such as Databases, Registries, perhaps Virtual Machine credentials or Container App references, such as IPs for further processing by other CI/CD pipelines.
Unsurprisingly, John would also like to deploy the project properly, according to the bare minimum standards of DevOps methodology, John should create Environment staging strategy and prepare for sets of mirrored Resource deployments to cover for Teams from Features, Development and Production Environments.
However, it turns out that John's deployments produce around 30 Secrets each, perhaps John is creating 30 different Database passwords or perhaps John would like to use one method for exporting Resource-specific properties without having to account for abstraction layers of 'Variables', 'Secrets' or 'Dinosaurs' and 'Sunflowers', which subsequently would require different API or CLI calls for the export procedure.
John is unhappy, with 3 staging Environments he hit the limit of 100 Secrets per Repository. A colleague - Thomas, would like to add a new authentication Secret, but the API server keeps sending '*bad request*' as the response...
%% ## How does it solve it %%
### The solution
Like 'John', many people would wonder how to overcome the limit of 100 Secrets.
The quick idea would be to use the 'Environment Secrets' for increasing the limit from the total of 100 to 300, or in other words - 100 Secrets per Environment.
Great! Let's see how that works.
#### Setting a Repository Secret
GitHub differentiates in between Caller Workflows, Reusable Workflows, Composite Actions. The GUI parameters traverse from Caller Workflows, through Reusable Workflows into Composite Actions.
It is possible to inherit secrets from Caller Workflows, either all of them or specific ones, by passing down parameterised inputs from Caller Workflows to Reusable Workflows and so on.
The Operator may write a fancy script to take care of creating Secrets for all of these pipelines. The reduced, bare logic would look like this:
```Bash
gh secret set --repo <repository-name> <secret-name> --body <secret-value>
```
A more fancy version that accounts for DELETE and PUT actions would look somewhat like this:
```Bash
if ["$COMMAND" == "delete" ]; then gh secret delete --repo <repository-name> <secret-name>; fi
if ["$COMMAND" == "create" ]; then gh secret set --repo <repository-name> <secret-name> --body <secret-value>; fi
```
Great, the Operator may now decide whether to create or delete Secrets based on commands provided into the CI/CD pipelines that are consumed by the Bash script (a more fancy version handles the errors, has log messages etc.).
This is perhaps the simplest approach. The Repository Secrets *just work* and may be consumed by Caller Workflows.
<center><img src="https://media1.tenor.com/m/rkI1a8s2Z6QAAAAd/todd-howard-it-just-works.gif"></center>
The version with Environments support would look like this (Environments must be created in advance, otherwise the response is 404):
```Bash
if ["$COMMAND" == "delete" ]; then gh secret delete --repo <repository-name> --env <environment-name> <secret-name>; fi
if ["$COMMAND" == "create" ]; then gh secret set --repo <repository-name> <secret-name> --env <environment-name> --body <secret-value>; fi
```
These CLI calls will indeed create Secrets in different Environments under the GitHub Repository. How can these 'Environment Secrets' be then consumed? As it turns out, that is indeed a fascinating question...
%% ## How to use it %%
### How to consume Environment Secrets
One would attempt to attach the 'Environment' property to the Caller Workflow to be able to reference the Environment Secrets inside the Caller Workflow, thus passing down the inputs into Reusable Workflows with nearly 0 changes to the codebase!
No, it's not the case. The 'Environment' property is not allowed for Caller Workflows.
Quote:
> "Warning: Environment secrets cannot be passed from the caller workflow as `on.workflow_call` does not support the `environment` keyword. If you include `environment` in the reusable workflow at the job level, the environment secret will be used, and not the secret passed from the caller workflow. For more information, see [Managing environments for deployment](https://docs.github.com/en/actions/deployment/targeting-different-environments/managing-environments-for-deployment#environment-secrets) and [Workflow syntax for GitHub Actions](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onworkflow_call)."
<sup>(I honestly do not understand what the Authors meant by "on.workflow_call does not support the environment keyword", as callable Reusable Workflows do support this keyword, contrary to the 'with' keyword used in Caller Workflows.)</sup>
Okay, then perhaps the other way around?
Since the 'Environment' keyword is allowed for the Reusable Workflows and Jobs within, then calling a Secret from within Jobs utilising a specific Environment should totally work.
Except it doesn't, as documented in e.g. this GitHub Issue: https://github.com/actions/runner/issues/1490
If the Reusable Workflow attempts to reference an Environment Secret through the `<input-name>: ${{ secrets.<secret-name> }}` then the resulting value is null/empty.
However, not all hope is lost, as it turns out that a special keyword should resolve the issue of non-resolvable Environment Secrets.
```YAML
jobs:
deploy:
uses:.github/workflows/<reusable-workflow-file-name>
with:
workflow_environment: <environment-name>
secrets: inherit
```
The problem with this approach is that the Caller Workflow essentially loses the argument behind being **the** Caller Workflow. The 'orchestration' logic should be encapsulated within the top-level templates, not nested within reusable components.
In this scenario, the Reusable Workflow would inherit all Secrets, then have to decide what to do with them. It is technically possible to move that logic into the component, yet it's not a good development pattern.
The components should be agnostic and provided with parameters that 'feed' them their runtime procedures, not decide how to proceed on their own like a 'black box'.
Hence, in case of the below approach, the 'inherit' keyword will not work:
```YAML
jobs:
reusable-workflow-job::
name: "Testing Environment Secrets"
uses: ./.github/workflows/<reusable-workflow-file-name>
with:
environment: <environment-name>
secrets:
<secret-1>: ${{ github.event_name != 'workflow_dispatch' && <value-1> || <condition> && <secret-2-value> || <secret-3-value>}}
```
This leaves the Operator at a crossroads, to revert to the Repository Secrets, or to go forward with something impossible to implement properly? Perhaps just switch to Azure Key Vault or HashiCorp Vault?
It turns out that against intuition, Environment Secrets are supported by the Reusable Workflow, yet in a way that e.g. I personally would never imagine to be functional.
```YAML
# Caller Workflow
jobs:
reusable-workflow-call:
name: "Caller Workflow Environment Secrets"
uses: ./.github/workflows/<reusable-workflow-file-name>
with:
workflow_environment: ${{ inputs.workflow_environment }} # Consumed from GUI upon selection
secrets:
secret_1: ${{ secrets.SECRET_ONE }} # Secret that DOES NOT exist in Repository Secrets, but exists in Environment Secrets
```
```YAML
# Reusable Workflow
on:
workflow_call:
inputs:
workflow_environment:
type: string
required: true
secrets:
secret_1:
description: Environment Secret
jobs:
reusable-workflow-job:
name: "Reusable Workflow Environment Secrets"
environment: ${{ inputs.workflow_environment }}
steps:
- name : "Step 1"
uses: /.github/actions/<composite-action-folder-name>
with:
secret_1: ${{ secrets.secret_1 }}
```
Hence, a Caller Workflow that **presumably cannot read Environment Secrets** on its own, is passing an Environment Secret name reference 'in-blanco' to the Reusable Workflow, which subsequently properly resolves the name reference due to the support for the 'Environment' property keyword.
<center><img src="https://media1.tenor.com/m/sOrcAJ9A-isAAAAd/dog-no.gif"></center>
To test this assumption I performed:
- creation of 3 Environments,
- creation of 3 independent sets of Environment Secrets with the exactly same names (yet different values) in each Environment,
- update of inputs passing routine for support of Environment Secrets.
The results were as follows:
- each Environment could have the Secrets written into it, under the same names
- for example, Environment called 'Development' could hold a reference named ''SecretABC123', an Environment called 'Production' could hold a Secret under the very same name.
- each Environment Secret would interpolate to a different value, yet Caller Workflow would reference **ALL** of them using **the same name**
- hence the presumption that some sort of mysterious 'black box logic' resides in the Reusable Workflows (which must properly distinguish in between the Environment-specific name references) seems correct.
*To say that this is expected behaviour is to say nothing.*
## References
### Section 1 - Types of GitHub Secrets
- https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/using-secrets-in-github-actions
- https://docs.github.com/en/codespaces/managing-codespaces-for-your-organization/managing-development-environment-secrets-for-your-repository-or-organization
- https://docs.github.com/en/actions/reference/evaluate-expressions-in-workflows-and-actions
- https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops#upper
### Section 2 - The solution
- https://github.com/orgs/community/discussions/129072
### Section 3 - How to consume Environment Secrets
- https://docs.github.com/en/actions/how-tos/sharing-automations/reusing-workflows#supported-keywords-for-jobs-that-call-a-reusable-workflow
- https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
- https://stackoverflow.com/questions/75386901/environments-in-github-actions-incompatible-with-reusable-workflows
- https://github.com/actions/runner/issues/1490
## Metadata
Date of creation: 2025-07-08
Date of revision: <...>