Visor provides powerful author permission checking functions that allow you to customize workflows based on the PR author's relationship to the repository. These functions use GitHub's author_association field to determine the author's permission level.
- Overview
- Permission Hierarchy
- Available Functions
- Use Cases
- Local Mode Behavior
- Best Practices
- Examples
Author permission functions are available in:
JavaScript contexts:
ifconditions - Control whether checks runfail_ifconditions - Fail checks based on author permissionstransform_js- Transform outputs based on permissionsgoto_js/run_js- Dynamic routing based on permissions
Liquid templates:
- AI prompts - Customize prompts based on author
- Command templates - Dynamic command generation
- Messages - Personalized welcome messages
These functions enable you to:
- Run different workflows for internal vs external contributors
- Apply stricter checks to first-time contributors
- Auto-approve PRs from trusted team members
- Block sensitive changes from non-members
- Welcome new contributors with custom messages
GitHub provides the following permission levels (from highest to lowest):
| Level | Description | Includes |
|---|---|---|
OWNER |
Repository owner | Owner only |
MEMBER |
Organization member | Owner, Members |
COLLABORATOR |
Invited collaborator | Owner, Members, Collaborators |
CONTRIBUTOR |
Has contributed before | Owner, Members, Collaborators, Contributors |
FIRST_TIME_CONTRIBUTOR |
First PR to this repo | Everyone except FIRST_TIMER |
FIRST_TIMER |
First GitHub contribution ever | FIRST_TIMER only |
NONE |
No association | No one |
Check if author has AT LEAST the specified permission level (>= logic)
hasMinPermission('MEMBER') // true for OWNER, MEMBER
hasMinPermission('COLLABORATOR') // true for OWNER, MEMBER, COLLABORATOR
hasMinPermission('CONTRIBUTOR') // true for all except FIRST_TIME_CONTRIBUTOR, FIRST_TIMERWhen to use:
- Most flexible option for hierarchical permission checks
- Use when you want "this permission or higher"
- Recommended for most use cases
Examples:
# Run security scan for non-members
if: "!hasMinPermission('MEMBER')"
# Allow auto-merge for collaborators and above
if: "hasMinPermission('COLLABORATOR')"
# Require manual review for new contributors
fail_if: "!hasMinPermission('CONTRIBUTOR') && criticalIssues > 0"Check if the author is the repository owner.
# Only owners can deploy to production
deploy-prod:
type: command
exec: npm run deploy:prod
if: "isOwner()"
# Skip review for owner
skip-review:
type: command
exec: gh pr review --approve
if: "isOwner()"Check if the author is an organization member or owner.
# Members can skip certain checks
quick-check:
type: command
exec: npm run test:quick
if: "isMember()"
# Non-members need full security scan
full-security-scan:
type: command
exec: npm run security:full
if: "!isMember()"Check if the author is an invited collaborator (or higher).
# Collaborators can bypass certain validations
bypass-format-check:
type: command
exec: echo "Skipping format check"
if: "!isCollaborator()"
# Auto-approve for collaborators with passing tests
auto-approve:
type: command
exec: gh pr review --approve
if: "isCollaborator() && outputs.tests.success === true"Check if the author has contributed to the repository before.
# Welcome returning contributors
welcome-back:
type: command
exec: gh pr comment --body "Welcome back!"
if: "isContributor() && !isMember()"
# Skip CLA check for known contributors
cla-check:
type: command
exec: ./scripts/check-cla.sh
if: "!isContributor()"Check if this is the author's first contribution to this repo or to GitHub.
# Welcome first-time contributors
welcome-message:
type: command
exec: |
gh pr comment --body "🎉 Welcome to the project! Thanks for your first contribution!"
if: "isFirstTimer()"
# Require extra care from first-timers
strict-review:
type: command
exec: gh pr review --request-changes
fail_if: "isFirstTimer() && (criticalIssues > 0 || errorIssues > 2)"Run different levels of security scanning based on trust level:
steps:
# Quick scan for trusted members
security-quick:
type: command
exec: npm run security:quick
if: "hasMinPermission('MEMBER')"
# Deep scan for collaborators
security-standard:
type: command
exec: npm run security:standard
if: "hasMinPermission('COLLABORATOR') && !hasMinPermission('MEMBER')"
# Full scan for external contributors
security-full:
type: command
exec: npm run security:full
if: "!hasMinPermission('COLLABORATOR')"Block changes to sensitive files from non-members:
steps:
protect-sensitive:
type: command
exec: echo "Checking sensitive files..."
fail_if: |
!isMember() && files.some(f =>
f.filename.startsWith('secrets/') ||
f.filename.startsWith('.github/workflows/') ||
f.filename === '.env' ||
f.filename.endsWith('.key') ||
f.filename.endsWith('.pem')
)Automatically approve PRs from trusted contributors when checks pass:
steps:
tests:
type: command
exec: npm test
lint:
type: command
exec: npm run lint
auto-approve:
type: command
depends_on: [tests, lint]
exec: gh pr review --approve
if: |
// Only auto-approve for collaborators
hasMinPermission('COLLABORATOR') &&
// All checks must pass
outputs.tests.error === false &&
outputs.lint.error === false &&
// No critical issues
totalIssues === 0Create a welcoming experience for first-time contributors:
steps:
welcome-first-timer:
type: command
exec: |
gh pr comment --body "$(cat <<'EOF'
👋 Welcome to the project! Thank you for your first contribution!
Here are some tips:
- Make sure all tests pass
- Follow our code style guide
- Ask questions in the comments if you need help
Our team will review your PR soon!
EOF
)"
if: "isFirstTimer()"
assign-mentor:
type: command
exec: gh pr edit --add-assignee mentor-bot
if: "isFirstTimer()"Require different levels of review based on changes and author:
steps:
require-review:
type: command
exec: gh pr review --request-changes
fail_if: |
// First-timers need approval for any PR
(isFirstTimer()) ||
// Non-collaborators need approval for large PRs
(!hasMinPermission('COLLABORATOR') && pr.totalAdditions > 500) ||
// Non-members need approval for sensitive files
(!isMember() && files.some(f =>
f.filename.includes('security') ||
f.filename.includes('auth')
))Control who can deploy to different environments:
steps:
deploy-staging:
type: command
exec: ./scripts/deploy.sh staging
if: "hasMinPermission('COLLABORATOR')"
deploy-production:
type: command
exec: ./scripts/deploy.sh production
if: "hasMinPermission('MEMBER')"
fail_if: |
// Extra validation for production
!isOwner() && (
pr.title.includes('WIP') ||
pr.title.includes('Draft') ||
outputs.tests.failed > 0
)Save CI resources by skipping checks for trusted contributors:
steps:
expensive-integration-tests:
type: command
exec: npm run test:integration
# Skip for members (they know what they're doing)
if: "!isMember()"
format-check:
type: command
exec: npm run format:check
# Members can skip (we trust them to format correctly)
if: "!hasMinPermission('COLLABORATOR')"Permission filters are also available in Liquid templates for prompts, commands, and messages:
steps:
# Customize AI prompts based on author permission
code-review:
type: ai
prompt: |
{% if pr.authorAssociation | is_member %}
Review this PR from team member {{ pr.author }}.
Focus on architecture and design patterns.
{% else %}
Review this PR from external contributor {{ pr.author }}.
Pay extra attention to:
- Security best practices
- Code quality and style
- Proper error handling
{% endif %}
Changed files:
{% for file in files %}
- {{ file.filename }} (+{{ file.additions }}, -{{ file.deletions }})
{% endfor %}
# Conditional welcome messages
welcome:
type: command
exec: |
gh pr comment --body "$(cat <<'EOF'
{% if pr.authorAssociation | is_first_timer %}
🎉 Welcome to the project! Thank you for your first contribution!
Here's what happens next:
1. Our CI will run automated tests
2. A maintainer will review your changes
3. We may request some changes
Feel free to ask questions in the comments!
{% elsif pr.authorAssociation | is_contributor %}
👋 Welcome back, {{ pr.author }}! Thanks for another contribution.
{% else %}
Thank you for your contribution, {{ pr.author }}!
{% endif %}
EOF
)"
# Dynamic command selection
security-scan:
type: command
exec: |
{% if pr.authorAssociation | has_min_permission: "MEMBER" %}
npm run security:quick
{% else %}
npm run security:full
{% endif %}
# Conditional approval message
auto-approve-message:
type: command
depends_on: [tests, lint]
exec: |
{% if pr.authorAssociation | has_min_permission: "COLLABORATOR" %}
gh pr review --approve --body "✅ Auto-approved: All checks passed for trusted contributor"
{% else %}
gh pr comment --body "✅ All checks passed! A maintainer will review soon."
{% endif %}
if: "totalIssues === 0"Available Liquid filters:
pr.authorAssociation | has_min_permission: "LEVEL"pr.authorAssociation | is_ownerpr.authorAssociation | is_memberpr.authorAssociation | is_collaboratorpr.authorAssociation | is_contributorpr.authorAssociation | is_first_timer
See Liquid Templates Guide for more details.
When running Visor locally (outside of GitHub Actions):
- All permission checks return
true(treated as owner) isFirstTimer()returnsfalse- This prevents blocking local development and testing
Detection logic:
// Visor detects local mode by checking for GITHUB_ACTIONS env var
const isLocal = !process.env.GITHUB_ACTIONS;You can test permission logic locally by temporarily setting:
export GITHUB_ACTIONS=truePrefer hasMinPermission() over individual checks for cleaner logic:
# ✅ Good - Clear hierarchical check
if: "!hasMinPermission('MEMBER')"
# ❌ Less clear - Manual hierarchy
if: "!isOwner() && !isMember()"Permission checks work great with other context variables:
fail_if: |
// Non-members can't modify critical files
!hasMinPermission('MEMBER') && files.some(f =>
f.filename.startsWith('core/')
) ||
// Non-collaborators need clean builds
!hasMinPermission('COLLABORATOR') && outputs.build.error === trueAdd comments to explain permission logic:
steps:
deploy:
type: command
exec: ./deploy.sh
# Only members can deploy - they understand the deployment process
# and have been trained on rollback procedures
if: "hasMinPermission('MEMBER')"Use permission checks to create a positive experience:
# ✅ Good - Welcoming and helpful
if: "isFirstTimer()"
exec: gh pr comment --body "Welcome! Thanks for contributing!"
# ✅ Good - Clear expectations
fail_if: "isFirstTimer() && criticalIssues > 0"
# ❌ Avoid - Too restrictive without guidance
fail_if: "!isMember()"Always test permission-based workflows:
# Add a check that logs permission info for debugging
debug-permissions:
type: command
exec: |
echo "Author: {{ pr.author }}"
echo "Association: {{ pr.authorAssociation }}"
if: |
log("isOwner:", isOwner());
log("isMember:", isMember());
log("isCollaborator:", isCollaborator());
true // Always run this checkProvide clear messages when permission checks fail:
steps:
check-permissions:
type: command
exec: echo "Permission check"
fail_if: |
const notAllowed = !hasMinPermission('COLLABORATOR') &&
files.some(f => f.filename.startsWith('infrastructure/'));
if (notAllowed) {
log("❌ Non-collaborators cannot modify infrastructure files");
log("Please request review from a team member");
}
notAllowedHere's a complete example showing how to use author permissions in a real workflow:
version: "1.0"
steps:
# 1. Welcome new contributors
welcome:
type: command
exec: |
gh pr comment --body "👋 Welcome! Thanks for your first contribution.
A maintainer will review your PR soon."
if: "isFirstTimer()"
# 2. Run appropriate test suite based on trust level
tests-quick:
type: command
exec: npm run test:unit
if: "hasMinPermission('MEMBER')"
tests-full:
type: command
exec: npm run test:all
if: "!hasMinPermission('MEMBER')"
# 3. Security scanning for external contributors
security-scan:
type: command
exec: npm run security:scan
if: "!hasMinPermission('COLLABORATOR')"
# 4. Protect sensitive files
check-sensitive-files:
type: command
exec: echo "Checking sensitive files..."
fail_if: |
!isMember() && files.some(f =>
f.filename.includes('secrets') ||
f.filename.includes('.env')
)
# 5. Require review for significant changes from non-members
require-review:
type: command
depends_on: [tests-full, tests-quick]
exec: gh pr review --request-changes
fail_if: |
// Large PRs from non-members need review
(!hasMinPermission('MEMBER') && pr.totalAdditions > 300) ||
// Any critical issues need review
(criticalIssues > 0)
# 6. Auto-approve for trusted contributors
auto-approve:
type: command
depends_on: [tests-full, tests-quick, security-scan]
exec: gh pr review --approve && gh pr merge --auto --squash
if: |
// Only for collaborators and above
hasMinPermission('COLLABORATOR') &&
// All checks passed
totalIssues === 0 &&
// Tests passed (check whichever ran)
((outputs["tests-quick"] && outputs["tests-quick"].error === false) ||
(outputs["tests-full"] && outputs["tests-full"].error === false))For organizations that need centralized, auditable policy enforcement beyond inline if/fail_if expressions, Visor's Enterprise Edition includes an OPA-based policy engine.
| Feature | Author Permissions (OSS) | Policy Engine (EE) |
|---|---|---|
| License | None (OSS) | EE license required |
| Mechanism | JavaScript expressions in if/fail_if |
OPA Rego policies |
| Scope | Per-step conditions | Pre-execution gating, tool filtering, capability restriction |
| Role system | hasMinPermission(), isMember(), etc. |
Custom roles via policy.roles config |
When to use Author Permissions: Simple permission checks for small teams with straightforward rules.
When to use the Policy Engine: Centralized, auditable enforcement for organizations needing compliance, separation of duties, or complex role hierarchies. The policy engine evaluates before if conditions, providing an additional layer of control.
Both systems work together -- author permission functions remain available even when the policy engine is active.
Learn more: Enterprise Policy Engine documentation
- Enterprise Policy Engine - OPA-based role-based access control (EE)
- Liquid Templates - Template syntax and variables
- Debugging Guide - Debugging JavaScript expressions
- Command Provider - Command execution and transforms
- Configuration Reference - Full configuration options