GitHub Actions Capabilities Summary
Table of Contents
- How GitHub Actions Work
- Key Extensibility Capabilities
- How Security Works
- Most Important Limitations
- Appendix — Diginsight Components Workflow Architecture
- Appendix — Comparison: GitHub Actions vs Azure DevOps Pipelines
1. How GitHub Actions Work
Workflow Lifecycle
A GitHub Actions workflow goes through three distinct phases:
| Phase | What Happens |
|---|---|
| 1. Trigger evaluation | An event occurs (push, pull_request, schedule, workflow_dispatch, etc.). GitHub evaluates which workflows have matching on: triggers and filters (branches, paths, tags). Matching workflows are queued. |
| 2. Job planning | Jobs within the workflow are organized by their needs: dependencies. Independent jobs are scheduled for parallel execution. Matrix strategies are expanded into individual job instances. |
| 3. Job execution | Each job is dispatched to a runner (GitHub-hosted or self-hosted). Steps execute sequentially within a job. Expressions (${{ }}) are evaluated at runtime. Outputs, artifacts, and caches are persisted between steps/jobs. |
Critical distinction from Azure DevOps: GitHub Actions has a single expression syntax (
${{ }}) that is evaluated at workflow runtime. There is no separate “compile-time” vs “runtime” phase — all expressions are evaluated when the workflow runs. This eliminates the class of “compile-time validation” errors common in Azure DevOps, but means all references must be valid at execution time.
Core Structure
name: CI/CD Pipeline # Workflow name
on: # Trigger configuration
push:
branches: [main]
paths: ['src/**']
pull_request:
branches: [main]
workflow_dispatch: # Manual trigger
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
schedule:
- cron: '0 6 * * 1' # Cron-based scheduling
permissions: # Workflow-level OIDC permissions
contents: read
id-token: write
concurrency: # Prevent duplicate runs
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env: # Workflow-level environment variables
DOTNET_VERSION: '9.0.x'
jobs:
build:
runs-on: ubuntu-latest # Runner selection
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- run: dotnet build --configuration Release
- id: ver
run: echo "version=1.0.${{ github.run_number }}" >> $GITHUB_OUTPUT
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.azurewebsites.net
steps:
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- uses: azure/webapps-deploy@v3
with:
app-name: my-appKey Concepts
| Concept | Description |
|---|---|
| Workflow | A YAML file in .github/workflows/ that defines an automated process. Equivalent to a full pipeline definition. |
| Event / Trigger | The condition that starts a workflow (push, pull_request, schedule, workflow_dispatch, repository_dispatch, etc.). |
| Job | A unit of work that runs on a single runner. Jobs run in parallel by default; use needs: for sequential ordering. |
| Step | A single task within a job — either a shell command (run:) or an action (uses:). Steps always run sequentially. |
| Action | A reusable unit of automation — can be a JavaScript action, Docker container action, or composite action. Referenced via uses:. |
| Runner | The machine that executes a job. Can be GitHub-hosted (ubuntu-latest, windows-latest, macos-latest) or self-hosted. |
| Environment | A named deployment target (e.g., production, staging) with optional protection rules, secrets, and approval gates. |
| Artifact | A file or directory produced by one job and consumed by another via actions/upload-artifact / actions/download-artifact. |
| Cache | Persisted dependency data across workflow runs via actions/cache — speeds up builds by avoiding repeated downloads. |
| Secret | An encrypted variable stored at repository, environment, or organization level. Accessed via ${{ secrets.NAME }}. |
| Variable | A non-encrypted configuration value stored at repository, environment, or organization level. Accessed via ${{ vars.NAME }}. |
| Matrix | A strategy that generates multiple job instances from a set of variable combinations (also available in Azure DevOps). |
| Concurrency | Controls whether workflows or jobs can run simultaneously, with optional cancellation of in-progress runs. |
| Reusable Workflow | A workflow designed to be called by other workflows via workflow_call trigger, enabling cross-workflow reuse. |
Expression Syntax
GitHub Actions uses a single expression syntax ${{ }} evaluated at runtime:
# Accessing contexts
${{ github.ref }} # Git ref that triggered the workflow
${{ github.event_name }} # Event type (push, pull_request, etc.)
${{ secrets.API_KEY }} # Repository or environment secret
${{ vars.VERSION_PREFIX }} # Repository or environment variable
${{ needs.build.outputs.version }} # Output from a dependent job
${{ steps.my_step.outputs.result }} # Output from a previous step
${{ matrix.os }} # Current matrix variable value
${{ runner.os }} # Runner operating system
${{ env.MY_VAR }} # Environment variable
${{ inputs.environment }} # Workflow dispatch input valueVariable Scoping and Precedence
Variables are resolved in this order (later overrides earlier):
- Organization-level variables (
vars.*) - Repository-level variables (
vars.*) - Environment-level variables (
vars.*) — if an environment is specified - Workflow-level
env: - Job-level
env: - Step-level
env: - Runtime
$GITHUB_ENVfile writes - Step outputs via
$GITHUB_OUTPUT
# Setting an output variable in a step
- id: set_version
run: echo "version=1.0.42" >> $GITHUB_OUTPUT
# Consuming in the same job, later step
- run: echo "Version is ${{ steps.set_version.outputs.version }}"
# Consuming in a different job (requires job-level outputs declaration)
jobs:
build:
outputs:
version: ${{ steps.set_version.outputs.version }}
deploy:
needs: build
steps:
- run: echo "Deploying version ${{ needs.build.outputs.version }}"Context Objects
GitHub Actions provides rich context objects:
| Context | Contents |
|---|---|
github |
Event payload, repository, ref, SHA, actor, workflow name, run number, API URL |
env |
Environment variables set at workflow, job, or step scope |
vars |
Configuration variables from repository, environment, or organization settings |
secrets |
Encrypted secrets from repository, environment, or organization settings |
job |
Current job status, container info, services |
steps |
Outputs and status of completed steps in the current job |
runner |
Runner metadata (OS, architecture, temp directory, tool cache path) |
needs |
Outputs and results of all jobs that the current job depends on |
strategy |
Matrix information for the current job |
matrix |
Current matrix variable values for the current job |
inputs |
Workflow dispatch or reusable workflow input values |
2. Key Extensibility Capabilities
2.1 Reusable Workflows (workflow_call)
Reusable workflows are the primary composition mechanism — equivalent to Azure DevOps templates. A workflow can declare itself as callable and define typed inputs, outputs, and secrets:
# Reusable workflow definition (e.g., .github/workflows/deploy.yml)
name: Deploy to Azure
on:
workflow_call:
inputs:
environment:
required: true
type: string
app-name:
required: true
type: string
secrets:
AZURE_CLIENT_SECRET:
required: false
outputs:
deployed-url:
description: 'URL of the deployed app'
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.webapp-url }}
steps:
- uses: azure/webapps-deploy@v3
id: deploy
with:
app-name: ${{ inputs.app-name }}# Caller workflow
jobs:
deploy-staging:
uses: ./.github/workflows/deploy.yml
with:
environment: staging
app-name: my-app-staging
secrets: inherit # Pass all secrets from callerKey differences from Azure DevOps templates:
- Reusable workflows operate at the job level — each reusable workflow call produces one or more complete jobs
- You cannot inject individual steps or stages via reusable workflows
- Inputs are typed (
string,boolean,number) — noobject,stepList, orstageListtypes secrets: inheritpasses all caller secrets without explicit listing
2.2 Composite Actions
Composite actions package a sequence of steps into a reusable unit — the closest equivalent to Azure DevOps step templates:
# action.yml in a repository or local directory
name: 'Build .NET Project'
description: 'Restore, build, and test a .NET solution'
inputs:
solution:
description: 'Path to solution file'
required: true
configuration:
description: 'Build configuration'
default: 'Release'
outputs:
artifact-path:
description: 'Path to build output'
value: ${{ steps.build.outputs.path }}
runs:
using: 'composite'
steps:
- run: dotnet restore ${{ inputs.solution }}
shell: bash
- run: dotnet build ${{ inputs.solution }} -c ${{ inputs.configuration }}
shell: bash
id: build# Consumer workflow
steps:
- uses: ./.github/actions/build-dotnet # Local composite action
with:
solution: src/MyApp.sln
- uses: my-org/shared-actions/build@v2 # Cross-repo composite action
with:
solution: src/MyApp.sln2.3 Matrix Strategy
Matrices generate multiple parallel job instances from variable combinations:
jobs:
test:
strategy:
fail-fast: false # Don't cancel other jobs if one fails
max-parallel: 4 # Limit concurrent jobs
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
dotnet: ['8.0.x', '9.0.x']
include: # Add specific combinations
- os: ubuntu-latest
dotnet: '9.0.x'
experimental: true
exclude: # Remove specific combinations
- os: macos-latest
dotnet: '8.0.x'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet }}
- run: dotnet test2.4 Dynamic Matrix Generation
Matrices can be computed dynamically using job outputs:
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix={"environment":["dev","staging","prod"],"include":[{"environment":"prod","approval":true}]}' >> $GITHUB_OUTPUT
deploy:
needs: prepare
strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to ${{ matrix.environment }}"2.5 Workflow Dispatch Inputs
Manual triggers with typed, validated inputs:
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- development
- staging
- production
dry-run:
description: 'Dry run mode'
type: boolean
default: false
version:
description: 'Version to deploy'
type: string
required: false2.6 Expressions and Functions
GitHub Actions provides built-in functions for use in ${{ }} expressions:
| Category | Functions |
|---|---|
| Comparison | ==, !=, <, >, <=, >= |
| Logical | &&, \|\|, ! |
| Status checks | success(), failure(), always(), cancelled() |
| String | contains(), startsWith(), endsWith(), format() |
| JSON | toJSON(), fromJSON() |
| Hash | hashFiles() |
| Conditionals | if on steps, jobs |
# Step condition
- run: echo "Deploying to prod"
if: github.ref == 'refs/heads/main' && success()
# Job condition
deploy:
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/v')
needs: build
# Using fromJSON for dynamic values
- run: echo "${{ fromJSON(steps.data.outputs.result).name }}"2.7 Job and Workflow Dependencies
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
test:
runs-on: ubuntu-latest
steps: [...]
build:
needs: [lint, test] # Runs after BOTH lint and test succeed
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: build
if: always() && needs.build.result == 'success'
runs-on: ubuntu-latest
steps: [...]2.8 Artifacts and Data Flow Between Jobs
# Upload in build job
- uses: actions/upload-artifact@v4
with:
name: build-output
path: ./publish/
retention-days: 5
if-no-files-found: error
# Download in deploy job
- uses: actions/download-artifact@v4
with:
name: build-output
path: ./deploy-package/Artifacts are the primary mechanism for passing files between jobs (since jobs may run on different runners).
2.9 Caching
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-Caches persist across workflow runs within the same branch (with fallback to the default branch). This is fundamentally different from artifacts, which are scoped to a single workflow run.
2.10 Service Containers
Jobs can spin up sidecar containers for integration testing:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- run: dotnet test # Tests can connect to localhost:5432 and localhost:63792.11 Cross-Repository Workflow Calls
Reusable workflows can be hosted in a separate repository:
jobs:
deploy:
uses: my-org/shared-workflows/.github/workflows/deploy.yml@main
with:
environment: production
secrets: inheritThis enables a centralized workflow library similar to Azure DevOps shared templates.
2.12 Repository Dispatch (Event-Driven)
Trigger workflows programmatically via the GitHub API:
on:
repository_dispatch:
types: [deploy-request]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying version ${{ github.event.client_payload.version }}"Triggered via API call:
curl -X POST -H "Authorization: token $TOKEN" \
-d '{"event_type":"deploy-request","client_payload":{"version":"1.2.3"}}' \
https://api.github.com/repos/OWNER/REPO/dispatches3. How Security Works
3.1 Secrets Management
Secrets are encrypted values stored at three levels:
| Level | Scope | Access |
|---|---|---|
| Organization secrets | All repos (or selected repos) in the org | ${{ secrets.NAME }} |
| Repository secrets | Single repository | ${{ secrets.NAME }} |
| Environment secrets | Single environment within a repo | ${{ secrets.NAME }} (only when job uses that environment) |
Key behaviors:
- Secrets are masked in logs — GitHub automatically redacts any log output matching a secret value
- Secrets are not passed to workflows triggered by forks (for pull_request events from forks)
- Secrets are not accessible in template expressions in reusable workflows — must be passed explicitly or via
secrets: inherit - GITHUB_TOKEN is automatically generated per workflow run with configurable permissions
3.2 Configuration Variables
Non-sensitive configuration values stored at organization, repository, or environment level:
steps:
- run: echo "Deploying version prefix ${{ vars.VERSION_PREFIX }}"
- run: echo "Client ID: ${{ vars.AZURE_CLIENT_ID }}"Variables are not masked in logs — use secrets for sensitive values.
3.3 GITHUB_TOKEN and Permissions
Every workflow run receives an automatic GITHUB_TOKEN with configurable permissions:
# Workflow-level permissions (restrictive)
permissions:
contents: read
pull-requests: write
id-token: write # Required for OIDC (e.g., Azure federated credentials)
# Job-level permissions (override workflow-level)
jobs:
deploy:
permissions:
id-token: write
contents: readAvailable permission scopes: actions, checks, contents, deployments, id-token, issues, packages, pages, pull-requests, repository-projects, security-events, statuses.
Default behavior: If permissions is not specified, GITHUB_TOKEN gets the default permissions configured in repository settings (typically read for all, or write for all).
3.4 Environments and Protection Rules
Environments provide deployment gates and approval workflows:
jobs:
deploy:
environment:
name: production
url: https://myapp.example.comEnvironment protection rules include:
- Required reviewers — one or more users/teams must approve before the job runs
- Wait timer — delay execution by a specified number of minutes
- Branch/tag restrictions — only specific branches or tags can deploy to the environment
- Custom deployment protection rules — third-party integrations (e.g., ServiceNow, Datadog) can gate deployments
- Deployment branches — restrict which branches can deploy to the environment
- Prevent self-review — prevent the person who triggered the workflow from approving their own deployment
3.5 OpenID Connect (OIDC) — Federated Credentials
GitHub Actions supports OIDC for secretless authentication to cloud providers:
permissions:
id-token: write # Required for OIDC
steps:
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
# No client secret needed — uses OIDC federated credentialThis eliminates the need to store cloud provider credentials as secrets. The cloud provider trusts GitHub’s OIDC token issuer and maps it to a specific identity based on claims (repository, branch, environment).
3.6 Fork Security Model
| Trigger | Secret Access | GITHUB_TOKEN Permissions |
|---|---|---|
pull_request from same repo |
✅ Full access | ✅ Full configured permissions |
pull_request from fork |
❌ No secrets | 🔒 Read-only |
pull_request_target from fork |
✅ Full access (target repo secrets) | ✅ Full configured permissions |
Warning:
pull_request_targetruns in the context of the base (target) repository, not the fork. It has access to all secrets. Use with extreme caution — never checkout and run untrusted code from the fork’s PR branch within apull_request_targetworkflow.
3.7 Concurrency Controls
Prevent duplicate or conflicting runs:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true # Cancel older runs in the same group3.8 Dependency Review and Supply Chain Security
| Feature | Description |
|---|---|
| Dependabot | Automated dependency update PRs with security vulnerability detection |
| Dependency review action | actions/dependency-review-action blocks PRs that introduce vulnerable dependencies |
| Code scanning | github/codeql-action for SAST analysis integrated into PR checks |
| Secret scanning | Automatic detection of leaked secrets in commits (push protection) |
| Software Bill of Materials | SBOM generation via actions/dependency-submission |
| Artifact attestations | actions/attest-build-provenance for SLSA provenance attestation |
3.9 Branch Protection and Rulesets
Branch protection rules complement Actions security:
- Required status checks — specific workflow jobs must pass before merge
- Required reviews — PRs need approval before merge
- Signed commits — enforce GPG/SSH commit signatures
- Merge queue — automated merge with required checks
- Branch rulesets — organization-wide rules that apply to multiple repositories
3.10 Private Action Access Controls
- Actions from public repos are available by default
- Actions from internal repos require explicit opt-in via repo settings
- Actions from private repos in the same org require
actions: readpermission on the action’s repo - Organizations can restrict which actions are allowed (all, GitHub-authored only, verified creators, or an explicit allowlist)
4. Most Important Limitations
Execution & Platform Limits
4.0.1 Concurrency and Billing
| Aspect | Free (Public) | Free (Private) | Team (Private) | Enterprise |
|---|---|---|---|---|
| Included minutes/month | Unlimited | 2,000 | 3,000 | 50,000 |
| Max concurrent jobs | 20 | 20 | 60 | 500 (org-wide) |
| Max concurrent macOS jobs | 5 | 5 | 5 | varies |
| Storage (artifacts + caches) | 500 MB | 500 MB | 2 GB | 50 GB |
Minute multipliers (for private repos): | Runner OS | Multiplier | |———–|———–| | Linux | 1× | | Windows | 2× | | macOS | 10× |
Key constraints:
- Concurrency limits are org-wide — all repos share the same concurrent job pool
- Self-hosted runners are free and unlimited — no minute charges or concurrency limits beyond what your infrastructure supports
- Spending limits can be set to prevent unexpected charges from overage minutes
- Jobs queued beyond concurrency limits are queued (not rejected) and will run when a slot becomes available
4.0.2 Job and Workflow Timeouts
| Scope | Default | Maximum |
|---|---|---|
| Job timeout (GitHub-hosted) | 360 min (6 hrs) | 360 min |
| Job timeout (self-hosted) | 360 min (6 hrs) | 35 days |
| Workflow run duration | None | 35 days |
| Workflow run queue time | N/A | 24 hours (then cancelled) |
| API request wait | N/A | 10 min per individual request |
| Job matrix (max generated) | N/A | 256 per workflow |
jobs:
long-running:
timeout-minutes: 120 # Override default timeout4.0.3 GitHub-Hosted Runner Constraints
- Ephemeral — every job gets a fresh VM; no state persists between jobs
- Available images:
ubuntu-latest(22.04),ubuntu-24.04,windows-latest(2022),windows-2025,macos-latest(14),macos-15, and ARM64 variants - Larger runners available (Team/Enterprise): 4–64 vCPU Linux/Windows, GPU runners (limited), ARM64 runners
- 14 GB RAM / 14 GB SSD for standard runners (2 vCPU)
- Cannot SSH/RDP into runners for debugging (but
tmateaction exists as a workaround) - Pre-installed software: each image comes with a large set of pre-installed tools (documented in runner-images)
- Cold start latency: typically 15–45 seconds for runner provisioning
4.0.4 Self-Hosted Runner Constraints
- Free — no per-minute or per-runner charges
- No concurrency limits beyond your own infrastructure
- Runner groups for organization-level management (Enterprise)
- Runner labels for targeting specific runner capabilities
- Auto-scaling via Actions Runner Controller (ARC) for Kubernetes-based scaling
- Ephemeral runners (
--ephemeralflag) — runner de-registers after one job, ensuring clean state - Security risk: self-hosted runners persist state unless ephemeral mode is used — public repos should never use non-ephemeral self-hosted runners (risk of malicious PR code executing on your infrastructure)
- Runner application auto-updates — runners auto-update within 30 days of a new version release
4.0.5 Retention and Storage
| Setting | Default | Maximum |
|---|---|---|
| Artifact retention | 90 days | 400 days (configurable per-repo or per-upload) |
| Cache retention | 7 days (unused) | Evicted LRU when > 10 GB per repo |
| Log retention | Same as artifact retention | 400 days |
| Workflow run retention | 90 days | 400 days |
Key notes:
- Artifacts count toward storage billing (500 MB free for private repos)
- Caches are branch-scoped — a cache created on a feature branch can be restored on the default branch, but not vice versa (except for fallback to the default branch via
restore-keys) - Caches are repository-scoped — caches cannot be shared across repositories
- Old caches are evicted when total cache size exceeds 10 GB per repository (LRU eviction)
4.0.6 Rate Limits and API Throttling
| Limit | Value |
|---|---|
| Workflow runs per hour per repo | 500 (across all events) |
| API requests per hour (GITHUB_TOKEN) | 1,000 per repo |
| API requests per hour (PAT) | 5,000 per user |
| Concurrent workflow runs per repo | 500 queued, based on plan concurrency |
| Job matrix per workflow | 256 jobs max |
| Workflow file size | 512 KB max |
| Reusable workflow nesting depth | 4 levels |
| Env variables per workflow | 100 |
4.0.7 Pricing (as of 2025)
| Item | Cost |
|---|---|
| Free tier (public repos) | Unlimited minutes |
| Free tier (private repos) | 2,000 minutes/month (Linux) |
| Linux minute overage | $0.008/min |
| Windows minute overage | $0.016/min |
| macOS minute overage | $0.08/min |
| Larger runners | Varies by vCPU count; 4-vCPU Linux ≈ $0.032/min |
| Self-hosted runners | Free (your infrastructure costs) |
| Storage overage | $0.25/GB/month |
Prices are indicative — check GitHub Actions pricing for current rates.
Workflow Composition Limitations
4.1 Reusable Workflow Constraints
Reusable workflows operate at the job level only. Unlike Azure DevOps templates that can inject steps, jobs, or stages, reusable workflows always produce complete jobs. You cannot:
- Call a reusable workflow as a step within a job
- Use a reusable workflow to inject individual steps into a calling job
- Nest reusable workflows more than 4 levels deep
- Have a single reusable workflow produce multiple stages (GitHub Actions has no stage concept)
# DOES NOT WORK — reusable workflows cannot be steps
jobs:
build:
steps:
- uses: ./.github/workflows/reusable.yml # ❌ Not allowed4.2 No Stage / Phase Concept
GitHub Actions has no built-in stage concept. All organization is at the job level. To simulate stages, you use job dependencies:
# Simulating stages via job dependencies
jobs:
# "Build Stage"
build:
runs-on: ubuntu-latest
steps: [...]
# "Test Stage"
unit-tests:
needs: build
runs-on: ubuntu-latest
steps: [...]
integration-tests:
needs: build
runs-on: ubuntu-latest
steps: [...]
# "Deploy Stage"
deploy:
needs: [unit-tests, integration-tests]
runs-on: ubuntu-latest
environment: production
steps: [...]This means you cannot visualize or manage “stages” as a first-class concept in the GitHub UI — the dependency graph is flat.
4.3 Limited Input Types for Reusable Workflows
Reusable workflow inputs support only: string, boolean, number. There is no object type — you cannot pass structured data like lists of environments. Workaround: serialize as JSON string and deserialize with fromJSON().
# Workaround for passing structured data
on:
workflow_call:
inputs:
environments:
type: string # JSON-encoded array
required: true
jobs:
deploy:
strategy:
matrix:
env: ${{ fromJSON(inputs.environments) }}4.4 No Dynamic Job Generation from Parameters
Unlike Azure DevOps ${{ each }} which can generate N stages at compile time from a parameter list, GitHub Actions cannot dynamically create jobs based on inputs. The closest equivalent is matrix strategy (which requires a fixed set of values or a JSON-encoded dynamic matrix).
4.5 Expression Limitations
- No regular expressions in expressions (use
contains(),startsWith(),endsWith()instead) - Limited string manipulation — no
split(),replace(),substring(),toLower()natively - No arithmetic — no addition, subtraction, etc. in expressions (use shell scripts)
- Expression evaluation is permissive — invalid property access returns empty string rather than an error, making typos hard to detect
- No
coalesce()equivalent — use chained||operator:${{ inputs.version || 'latest' }}
4.6 Concurrency and Cancellation
- Concurrency groups are string-based — typos in group names silently create separate groups
cancel-in-progress: truecancels the older run, which may be undesirable for deployment workflows- No built-in priority system — all queued jobs are equal priority
- No exclusive locks on environments beyond the built-in “one deployment at a time” protection rule
4.7 Cross-Job Communication Constraints
- No direct variable sharing between jobs — must use
outputs+needsor artifacts - Job outputs are strings only — complex data must be JSON-serialized
- Artifact upload/download adds overhead — each artifact operation takes 10–30+ seconds
- No artifact streaming — entire artifact must be downloaded before use
- Environment variables set via
$GITHUB_ENVare scoped to the current job only
4.8 Trigger and Event Limitations
pathsfilters are not supported withscheduleorworkflow_dispatch— path filters only work withpushandpull_request- No cross-repository triggers for private repos without
repository_dispatchor workflow_run workflow_runis limited — can only be triggered by workflows in the same repository, and only on the default branch- Scheduled workflows run on the default branch only — cron triggers ignore non-default branches
- Cron schedule minimum interval is 5 minutes (but actual execution may be delayed by up to 15 minutes during high load)
- Scheduled workflows are disabled after 60 days of repository inactivity
4.9 Environment and Deployment Constraints
- Environments are repository-scoped — cannot be shared across repositories
- Deployment approvals are per-environment — no way to require approval for a specific job without an environment
- No programmatic approval API — approvals require manual interaction via the GitHub UI or mobile app
- Deployment history is tracked per-environment, but limited querying capability
4.10 Workflow File Constraints
| Constraint | Limit |
|---|---|
| Max workflow file size | 512 KB |
| Max workflows per repository | Unlimited (but 500 runs/hour limit) |
| Max jobs per workflow | 500 |
| Max steps per job | 1,000 |
| Reusable workflow nesting depth | 4 levels |
| Matrix combinations per workflow | 256 |
| Env variables per step | 100 |
| Total env size | 256 KB |
5. Appendix — Diginsight Components Workflow Architecture
Workflow Inventory
| Workflow File | Purpose | Trigger | Runner |
|---|---|---|---|
v3.yml |
Build and publish NuGet packages for v3+ releases | Tag push (v3*, v4*, …) |
ubuntu-latest |
v2_99.Package.CICD.yml |
Main CI/CD pipeline: build, package, publish NuGet | Push/PR to main (paths: src/Diginsight.Components*/**), manual dispatch |
self-hosted |
v2_00.InstallActions.yml |
Reusable: checkout, LFS, .NET SDK, NuGet setup | workflow_call |
self-hosted |
v2_01.GetCompositeVariables.yml |
Reusable: compute assembly version from run number + offset | workflow_call (outputs: assemblyVersion) |
self-hosted |
v2_02.GetKeyVaultSecrets.yml |
Reusable: Azure login + Key Vault secret retrieval | workflow_call (outputs: secrets) |
self-hosted |
20.DeploySamples.yml |
Build and deploy sample apps to Azure App Service | Manual dispatch | self-hosted |
21.DeployAppService.yml |
Reusable: deploy a single app to Azure App Service | workflow_call (inputs: environment, appName, webAppName) |
self-hosted + ubuntu-latest |
quarto-publish.yml |
Render Quarto docs and deploy to GitHub Pages | Push/PR to main, manual dispatch |
ubuntu-latest |
github-pages.ym_ |
Legacy GitHub Pages deployment (disabled via .ym_ extension) |
Push to main |
ubuntu-latest |
Workflow Dependency Graph
v2_99.Package.CICD.yml (main CI/CD)
│
├─► v2_00.InstallActions.yml (checkout, .NET, NuGet)
│
├─► v2_01.GetCompositeVariables.yml (version computation)
│ └─ depends on: installActions
│
├─► build (job: restore, build Debug+Release, upload artifacts)
│ └─ depends on: getCompositeVariables
│
├─► publishNugetPackage (job: download artifact, push to nuget.org)
│ └─ depends on: build, getCompositeVariables
│
└─► upload2AzureFolder (job: download artifact, copy to Azure Storage)
└─ depends on: build, getCompositeVariables
20.DeploySamples.yml (sample deployment)
│
├─► build (job: checkout, build, publish samples, upload artifact)
│
├─► afterBuild (job: dump outputs)
│ └─ depends on: build
│
├─► deployAuthenticationSampleApi
│ └─ calls: 21.DeployAppService.yml (matrix: [Testms])
│ └─ depends on: afterBuild, build
│
└─► deployAuthenticationSampleServerApi
└─ calls: 21.DeployAppService.yml (matrix: [Testms])
└─ depends on: afterBuild, build
v3.yml (tag-based NuGet publish)
└─► build-and-publish (single job: checkout, restore, build, push NuGet)
Key Design Patterns Used
| Pattern | Implementation |
|---|---|
| Reusable workflows | v2_00, v2_01, v2_02, 21.DeployAppService are workflow_call workflows composed into main pipelines |
| Job output chaining | getCompositeVariables outputs assemblyVersion → consumed by build and publishNugetPackage via needs.*.outputs.* |
| Matrix deployments | 20.DeploySamples.yml uses strategy.matrix.environment: [Testms] for per-environment deployment |
| Artifact flow | Build job uploads .nupkg files → publish/deploy jobs download via actions/download-artifact |
| OIDC authentication | 21.DeployAppService.yml uses azure/login@v2 with federated credentials (client-id, tenant-id, subscription-id from vars.*) |
| Secrets inheritance | Caller workflows pass secrets: inherit to reusable workflows |
| Path-scoped triggers | CI/CD pipeline triggers only on changes to src/Diginsight.Components*/** |
| Tag-based versioning | v3.yml extracts version from git tag (GITHUB_REF_NAME minus v prefix) |
| Cross-repo checkout | 20.DeploySamples.yml checks out diginsight/components.internal for private appsettings |
| Concurrency control | quarto-publish.yml uses concurrency.group: "pages" to prevent overlapping Pages deployments |
| Environment protection | 21.DeployAppService.yml references environment: ${{ inputs.environment }} for deployment gates |
| NuGet cache | 20.DeploySamples.yml uses actions/cache@v4 with hashFiles('**/packages.lock.json') |
| Disabled workflows | github-pages.ym_ — renamed extension to disable without deleting |
6. Appendix — Comparison: GitHub Actions vs Azure DevOps Pipelines
6.1 Terminology Mapping
| Azure DevOps | GitHub Actions | Notes |
|---|---|---|
| Pipeline | Workflow | Top-level automation definition |
| Stage | (no equivalent) | GitHub uses job dependencies to simulate stages |
| Job | Job | Unit of work on a single agent/runner |
| Deployment Job | Job + Environment | deployment: keyword vs environment: property |
| Step | Step | Single task within a job |
Task (e.g., DotNetCoreCLI@2) |
Action (e.g., actions/setup-dotnet@v4) |
Reusable automation unit |
| Template | Reusable Workflow / Composite Action | Two mechanisms depending on granularity |
| Variable Group | Variables (repo/org/env) | Centralized configuration |
| Service Connection | Secret / OIDC Federated Credential | External service authentication |
| Agent Pool | Runner / Runner Group | Execution environment |
| Demands | Runner Labels | Agent/runner capability matching |
| Artifact | Artifact | Files passed between jobs |
| Environment | Environment | Deployment target with protection rules |
| Pipeline Artifact | Artifact via upload-artifact/download-artifact actions |
|
extends |
(no equivalent) | GitHub has no enforced template inheritance |
${{ }} (compile-time) |
${{ }} (runtime) |
Same syntax, different evaluation timing |
$[ ] (runtime) |
(no equivalent) | GitHub has only one expression syntax |
condition: |
if: |
Job/step conditional execution |
##vso[task.setvariable] |
>> $GITHUB_OUTPUT / >> $GITHUB_ENV |
Setting outputs and env vars |
6.2 Structural Comparison
| Capability | Azure DevOps | GitHub Actions |
|---|---|---|
| Pipeline hierarchy | Stages → Jobs → Steps | Jobs → Steps (no stages) |
| Template/reuse granularity | Steps, Jobs, Stages, Variables templates | Composite Actions (steps), Reusable Workflows (jobs) |
| Template parameters | string, boolean, number, object, step, stepList, job, jobList, stage, stageList |
string, boolean, number only |
| Dynamic generation | ${{ each }} iterates over parameter lists at compile time |
Matrix strategy or dynamic JSON matrix via job outputs |
| Object spread | ${{ insert }}: ${{ item }} |
Not available |
| Compile-time vs runtime | Separate phases — ${{ }} at compile-time, $[ ] and condition: at runtime |
Single phase — all ${{ }} evaluated at runtime |
| Stage-level deployment gates | Via Environments with checks | Via Environment protection rules |
| Approval workflow | Environment checks + ManualValidation@0 task |
Environment required reviewers + wait timers |
| Artifact mechanism | PublishPipelineArtifact/download: current |
upload-artifact/download-artifact actions |
6.3 Security Comparison
| Capability | Azure DevOps | GitHub Actions |
|---|---|---|
| Secret storage | Variable Groups, Key Vault integration | Repository/Org/Environment Secrets |
| Secret masking | Manual (issecret=true logging command) |
Automatic (all secrets masked in logs) |
| External auth | Service Connections (Azure RM, Kubernetes, Docker, etc.) | OIDC Federated Credentials + Secrets |
| Secretless cloud auth | Workload Identity Federation (preview) | OIDC natively supported (GA) |
| Fork PR security | Not applicable (no fork model in Azure Repos) | Secrets blocked from fork PRs; pull_request_target for controlled access |
| Template enforcement | extends + required template checks |
No equivalent — org-level action restrictions only |
| Branch protection for deployments | Environment branch filters | Environment deployment branch rules |
| Pipeline/workflow authorization | Service connections require per-pipeline authorization | Secrets available to all workflows in the repo (unless environment-scoped) |
| Built-in SAST | Microsoft Security DevOps extension | GitHub CodeQL (Advanced Security) |
| Built-in SCA | None built-in (use third-party) | Dependabot + Dependency Review Action |
| Secret scanning | None built-in | GitHub Secret Scanning + Push Protection |
| Supply chain attestation | None built-in | Artifact Attestations (SLSA provenance) |
| Audit logging | Azure DevOps audit logs | GitHub Audit Log |
6.4 Extensibility Comparison
| Capability | Azure DevOps | GitHub Actions |
|---|---|---|
| Step-level reuse | Steps templates (template: steps/build.yml) |
Composite Actions (uses: ./.github/actions/build) |
| Job-level reuse | Jobs templates | Reusable Workflows (uses: ./.github/workflows/deploy.yml) |
| Stage-level reuse | Stages templates | Not available (no stage concept) |
| Cross-repo templates | resources.repositories + template: file@repo |
uses: org/repo/.github/workflows/file.yml@ref |
| Marketplace | Azure DevOps Marketplace (tasks, extensions) | GitHub Marketplace (20,000+ actions) |
| Custom task/action types | TypeScript/PowerShell extensions | JavaScript, Docker, Composite actions |
| Matrix builds | strategy.matrix (limited) |
strategy.matrix with include/exclude, dynamic JSON |
| Service containers | Container jobs | services: sidecar containers |
| Caching | Cache@2 task |
actions/cache@v4 (more tightly integrated) |
| Manual approval | ManualValidation@0 task (server job) |
Environment protection rules (required reviewers) |
| Event-driven triggers | Limited (CI/CD triggers, scheduled, pipeline completion) | Rich (25+ event types including issues, discussion, release, workflow_run) |
| API trigger | Not natively (use service hooks) | repository_dispatch + API call |
6.5 Limitations Comparison
| Limitation Area | Azure DevOps | GitHub Actions |
|---|---|---|
| Compile-time service connection validation | ⚠️ Major — connections validated even behind condition: guards; must use ${{ if }} |
✅ Not an issue — no compile-time validation |
| Dynamic connection names | ❌ Not supported at runtime | ✅ Secrets can be dynamically selected via expressions |
| Cross-stage variable passing | ⚠️ Verbose stageDependencies syntax |
⚠️ needs.*.outputs.* syntax (similar verbosity) |
| Stages | ✅ First-class concept | ❌ No stages — must simulate with job dependencies |
| Template parameter types | ✅ Rich (object, stepList, stageList, etc.) | ❌ Limited (string, boolean, number only) |
| Dynamic stage/job generation | ✅ ${{ each }} over parameter lists |
⚠️ Matrix strategy + fromJSON() (less flexible) |
| String manipulation in expressions | ❌ No split/replace/substring | ❌ No split/replace/substring |
| Arithmetic in expressions | ❌ Not supported | ❌ Not supported |
| Template enforcement | ✅ extends + required template checks |
❌ No equivalent (org-level restrictions only) |
| YAML anchors | ❌ Not supported | ❌ Not supported |
| Reusable nesting depth | ✅ Unlimited (templates reference templates) | ⚠️ 4 levels maximum |
| Max job duration | 60 min (free) / 360 min (paid) hosted | 360 min (hosted) / 35 days (self-hosted) |
| Scheduled triggers on non-default branches | ✅ Supported | ❌ Default branch only |
| Fork PR model | N/A (Azure Repos has no forks) | ✅ Built-in fork security model |
| Agent/runner GPU support | ❌ No hosted GPU | ⚠️ Limited larger runners with GPU (Enterprise) |
| Environment sharing across projects/repos | ❌ Project-scoped | ❌ Repository-scoped |
| Workflow file size | No documented limit | 512 KB maximum |
| Free tier (private) | 1 parallel job, 1,800 min/month | 2,000 min/month Linux (with multipliers) |
| Self-hosted agent cost | ~$15/month per parallel slot | Free (unlimited) |
6.6 When to Choose Which
| Scenario | Recommended | Rationale |
|---|---|---|
| Source code on GitHub | GitHub Actions | Native integration, no external CI/CD setup needed |
| Source code on Azure Repos | Azure DevOps | Native integration; GitHub Actions requires mirror setup |
| Complex multi-stage pipelines | Azure DevOps | First-class stage concept, richer template parameters, ${{ each }} |
| Open-source projects | GitHub Actions | Unlimited free minutes, rich community action ecosystem |
| Enterprise governance | Azure DevOps | extends enforcement, required template checks, tighter control |
| Cloud-native deployments | GitHub Actions | Native OIDC, simpler Azure/AWS/GCP integration |
| Matrix testing across OS/versions | GitHub Actions | More flexible matrix strategy with include/exclude |
| Self-hosted runners at scale | GitHub Actions | Free, unlimited runners; ARC for K8s auto-scaling |
| Integration with Azure Boards | Azure DevOps | Native work item linking, board integration |
| Integration with GitHub Issues/PRs | GitHub Actions | Native event triggers, status checks, PR comments |
| Security scanning | GitHub Actions | Built-in CodeQL, Dependabot, secret scanning, SBOM |
| Hybrid (Azure Repos + GitHub Actions) | Either | Both support cross-platform triggers via webhooks/dispatch |