GitHub Actions Workflow Instructions
June 29, 2026 · View on GitHub
These instructions define required conventions and security requirements for GitHub Actions workflows in the hve-core repository. All workflows MUST comply with these rules to pass CI validation.
Dependency Pinning
All third-party GitHub Actions MUST be pinned to a full commit SHA. Version tags MUST NOT be used as the reference. A semantic version MAY be included as a trailing comment for readability.
Required pattern:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Forbidden patterns:
uses: actions/checkout@v4
uses: actions/checkout@v4.2.2
Local reusable workflows referenced via relative paths are excluded from SHA pinning requirements.
Enforcement: Violations are detected by scripts/security/Test-DependencyPinning.ps1 and scripts/security/Test-SHAStaleness.ps1. CI will fail on SHA pinning violations.
Permissions
Workflows MUST declare explicit permissions following the principle of least privilege. The default permission set is contents: read. Additional permissions MUST be granted at the job level and only when required for a specific capability.
Required pattern:
permissions:
contents: read
pull-requests: write
Job-level permissions example:
jobs:
validate:
name: Validate Code
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run validation
run: |
echo "Running validation steps"
Enforcement: Violations are detected by scripts/security/Test-WorkflowPermissions.ps1. CI will fail on workflows missing a top-level permissions: block. The copilot-setup-steps.yml workflow is excluded by default.
Credentials and Secrets
Workflows MUST NOT persist GitHub credentials by default. Credential persistence MUST be enabled only when explicitly required for a specific capability. Secrets and tokens MUST be granted explicitly and scoped to the minimum required permissions.
Required pattern:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
Secret handling:
- name: Use secret
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
run: |
# Use $MY_SECRET securely
Runners
Workflows MUST run on GitHub-hosted Ubuntu runners. Other runner types are not supported in hve-core.
Required pattern:
runs-on: ubuntu-latest
Workflow Structure
Workflows MUST follow these structural expectations:
- Use descriptive names for workflows and jobs
- Group related jobs with
needs:dependencies - Use
concurrency:to prevent duplicate runs - Prefer reusable workflows for common patterns
Required structure example:
name: Descriptive Workflow Name
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
validate:
name: Validate Code
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Validation
run: |
echo "Running validation steps"
Reusable Workflows
Defining Reusable Workflows
Reusable workflows MUST use workflow_call trigger and define explicit inputs and outputs.
Reusable workflow definition:
name: Reusable Analysis Workflow
on:
workflow_call:
inputs:
threshold:
description: 'Compliance threshold percentage (0-100)'
required: false
type: number
soft-fail:
description: 'Whether to continue on violations'
required: false
type: boolean
upload-sarif:
description: 'Whether to upload SARIF results to Security tab'
required: false
type: boolean
outputs:
compliance-score:
description: 'Compliance score percentage'
value: ${{ jobs.scan.outputs.compliance-score }}
unpinned-count:
description: 'Number of unpinned dependencies found'
value: ${{ jobs.scan.outputs.unpinned-count }}
is-compliant:
description: 'Whether repository meets compliance threshold'
value: ${{ jobs.scan.outputs.is-compliant }}
permissions:
contents: read
jobs:
scan:
name: Validate Compliance
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
compliance-score: ${{ steps.analyze.outputs.compliance-score }}
unpinned-count: ${{ steps.analyze.outputs.unpinned-count }}
is-compliant: ${{ steps.analyze.outputs.is-compliant }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Analysis
id: analyze
run: |
echo "compliance-score=95" >> $GITHUB_OUTPUT
echo "unpinned-count=2" >> $GITHUB_OUTPUT
echo "is-compliant=true" >> $GITHUB_OUTPUT
Consuming Reusable Workflows
Reusable workflows MUST be called using relative paths with explicit permissions and inputs.
Example usage:
name: PR Validation Workflow
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
validate-pinning:
name: Validate Dependency Pinning
uses: ./.github/workflows/dependency-pinning-scan.yml
permissions:
contents: read
security-events: write
with:
soft-fail: false
upload-sarif: true
Validation Requirements
All workflows MUST pass the following validation checks:
actionlint Validation
- What it enforces: Syntax validation, best practices, and security checks
- Configuration: Uses actionlint with SHA256 verification
- CI blocking: Workflows fail CI if violations are detected
Dependency Pinning Validation
- Script:
scripts/security/Test-DependencyPinning.ps1 - What it enforces: All third-party actions use full SHA pins
- CI blocking: Failures block CI when configured to enforce compliance
SHA Staleness Validation
- Script:
scripts/security/Test-SHAStaleness.ps1 - What it enforces: SHA-pinned dependencies are not stale
- CI blocking: Stale dependencies generate warnings and may fail CI
Workflow Permissions Validation
- Script:
scripts/security/Test-WorkflowPermissions.ps1 - What it enforces: All workflows declare a top-level
permissions:block - CI blocking: Failures block CI when configured to enforce compliance
Security Requirements
- Never expose secrets in logs or outputs
- No personal access tokens (PATs) are used in workflows
- Use event guards for release-specific operations when needed
- Enable security features like CodeQL and dependency scanning
- All security workflows use explicit, minimal permissions
Example event guard pattern:
- name: Process Release
run: |
if [ "${{ github.event_name }}" == "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
echo "Processing release: $VERSION"
else
VERSION="${{ inputs.version }}"
echo "Processing version: $VERSION"
fi
YAML Expression Quoting
GitHub Actions expression single quotes ('...') inside ${{ }} are NOT YAML quotes. The YAML parser treats them as literal characters in a plain scalar. If the expression text contains a colon followed by whitespace (: ), the YAML parser interprets it as a mapping value indicator and fails with mapping values are not allowed in this context.
Problem pattern:
# FAILS: ': release' contains colon-space, YAML parser chokes
with:
my-input: ${{ startsWith(value, 'prefix: release') }}
Acceptable solutions (in preference order):
-
Shorten the literal to avoid colon-space. Use a prefix that does not contain a colon followed by whitespace when the specificity tradeoff is acceptable.
with: my-input: ${{ startsWith(value, 'prefix') }} -
Double-quote the entire expression. This makes the value a YAML quoted scalar, preventing the parser from interpreting internal colon-space as structure. Add a comment explaining why quotes are required.
with: # Quotes required: expression literal contains ': ' which breaks YAML plain scalars my-input: "${{ startsWith(value, 'prefix: release') }}"
Avoid format() workarounds or environment variable indirection when the simpler options above apply.
PR Validation Gate
pr-validation.yml exposes a single pr-validation-success aggregator job as the required status check that gates merge. This job is green only when every other job in the workflow passes.
Every job defined in pr-validation.yml, except pr-validation-success itself, MUST appear in the needs: list of the pr-validation-success job. Omitting a job lets that job fail silently without blocking merge, which defeats the gate.
The gate-completeness-check job enforces this rule in CI, failing the workflow whenever the gate's needs: list drifts out of sync with the defined jobs. Contributors can validate the gate locally by running npm run lint:pr-gate before pushing.
Enforcement Statement
The following scripts enforce compliance:
scripts/security/Test-DependencyPinning.ps1- Validates dependency pinningscripts/security/Test-SHAStaleness.ps1- Checks for stale dependenciesscripts/security/Test-WorkflowPermissions.ps1- Validates workflow permissions declarationsscripts/linting/Invoke-YamlLint.ps1- Runs actionlint validationscripts/security/Test-PrValidationGate.ps1- Validates the PR validation gateneeds:completeness
All workflows must pass these validation checks to be merged into the repository.