5 changed files with 666 additions and 0 deletions
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
name: Permission Check |
||||
|
||||
on: |
||||
workflow_call: |
||||
inputs: |
||||
failure_mode: |
||||
description: "Permission check failure mode: fail, skip, or continue" |
||||
type: string |
||||
default: "fail" |
||||
required: false |
||||
require_permission: |
||||
description: "Required permission level: admin, write, read, or none" |
||||
type: string |
||||
default: "write" |
||||
required: false |
||||
outputs: |
||||
has_permission: |
||||
description: "Whether the user has the required permission" |
||||
value: ${{ jobs.check-permission.outputs.has_permission }} |
||||
user_permission: |
||||
description: "The actual permission level of the user" |
||||
value: ${{ jobs.check-permission.outputs.user_permission }} |
||||
should_skip: |
||||
description: "Whether subsequent jobs should be skipped" |
||||
value: ${{ jobs.check-permission.outputs.should_skip }} |
||||
|
||||
permissions: |
||||
contents: read |
||||
|
||||
jobs: |
||||
check-permission: |
||||
name: Check permission |
||||
runs-on: ubuntu-24.04 |
||||
outputs: |
||||
has_permission: ${{ steps.check.outputs.has_permission }} |
||||
user_permission: ${{ steps.check.outputs.user_permission }} |
||||
should_skip: ${{ steps.check.outputs.should_skip }} |
||||
steps: |
||||
- name: Check out repo |
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 |
||||
with: |
||||
persist-credentials: false |
||||
|
||||
- name: Check user permission |
||||
id: check |
||||
uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: ${{ inputs.require_permission }} |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: ${{ inputs.failure_mode }} |
||||
|
||||
- name: Report results |
||||
env: |
||||
ACTOR: ${{ github.triggering_actor }} |
||||
REQUIRED: ${{ inputs.require_permission }} |
||||
ACTUAL: ${{ steps.check.outputs.user_permission }} |
||||
HAS_PERM: ${{ steps.check.outputs.has_permission }} |
||||
SHOULD_SKIP: ${{ steps.check.outputs.should_skip }} |
||||
run: | |
||||
echo "🤖 Permission Check" |
||||
echo "================================" |
||||
echo "User: $ACTOR" |
||||
echo "Required: $REQUIRED" |
||||
echo "Actual: $ACTUAL" |
||||
echo "Has permission: $HAS_PERM" |
||||
echo "Should skip: $SHOULD_SKIP" |
||||
@ -0,0 +1,145 @@
@@ -0,0 +1,145 @@
|
||||
name: Test Check Permission Action |
||||
|
||||
on: |
||||
pull_request: |
||||
paths: |
||||
- "check-permission/**" |
||||
- ".github/workflows/test-check-permission.yml" |
||||
workflow_dispatch: |
||||
|
||||
permissions: |
||||
contents: read |
||||
|
||||
jobs: |
||||
test-fail-mode: |
||||
name: Test Fail Mode |
||||
runs-on: ubuntu-24.04 |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 |
||||
with: |
||||
persist-credentials: false |
||||
|
||||
- name: Check permission (should pass for repo collaborators) |
||||
id: check-fail |
||||
uses: ./check-permission |
||||
with: |
||||
require: read |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: fail |
||||
|
||||
- name: Verify output |
||||
env: |
||||
HAS_PERMISSION: ${{ steps.check-fail.outputs.has_permission }} |
||||
USER_PERMISSION: ${{ steps.check-fail.outputs.user_permission }} |
||||
SHOULD_SKIP: ${{ steps.check-fail.outputs.should_skip }} |
||||
run: | |
||||
echo "Has permission: $HAS_PERMISSION" |
||||
echo "User permission: $USER_PERMISSION" |
||||
echo "Should skip: $SHOULD_SKIP" |
||||
|
||||
test-skip-mode: |
||||
name: Test skip mode |
||||
runs-on: ubuntu-24.04 |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 |
||||
with: |
||||
persist-credentials: false |
||||
|
||||
- name: Check permission (always succeeds with skip mode) |
||||
id: check-skip |
||||
uses: ./check-permission |
||||
with: |
||||
require: admin |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
- name: Conditional step |
||||
if: steps.check-skip.outputs.should_skip != 'true' |
||||
run: echo "Would run privileged operation" |
||||
|
||||
- name: Skip notification |
||||
if: steps.check-skip.outputs.should_skip == 'true' |
||||
run: echo "Skipped privileged operation due to permissions" |
||||
|
||||
test-continue-mode: |
||||
name: Test continue mode |
||||
runs-on: ubuntu-24.04 |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 |
||||
with: |
||||
persist-credentials: false |
||||
|
||||
- name: Check permission with continue mode |
||||
id: permission |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: continue |
||||
|
||||
- name: Admin operations |
||||
if: steps.permission.outputs.user_permission == 'admin' |
||||
run: echo "Running admin-level operations" |
||||
|
||||
- name: Write operations |
||||
if: steps.permission.outputs.user_permission == 'write' |
||||
run: echo "Running write-level operations" |
||||
|
||||
- name: Read operations |
||||
if: steps.permission.outputs.user_permission == 'read' |
||||
run: echo "Running read-level operations" |
||||
|
||||
- name: Always runs |
||||
env: |
||||
USER_PERMISSION: ${{ steps.permission.outputs.user_permission }} |
||||
HAS_PERMISSION: ${{ steps.permission.outputs.has_permission }} |
||||
run: | |
||||
echo "This step always runs" |
||||
echo "User permission: $USER_PERMISSION" |
||||
echo "Has required permission: $HAS_PERMISSION" |
||||
|
||||
test-fallback-pattern: |
||||
name: Test fallback pattern |
||||
runs-on: ubuntu-24.04 |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 |
||||
with: |
||||
persist-credentials: false |
||||
|
||||
- name: Check admin permission (preferred) |
||||
id: check-admin |
||||
uses: ./check-permission |
||||
with: |
||||
require: admin |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
- name: Check write permission (fallback) |
||||
if: steps.check-admin.outputs.should_skip == 'true' |
||||
id: check-write |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
- name: Report results |
||||
env: |
||||
ACTOR: ${{ github.triggering_actor }} |
||||
ADMIN_PERM: ${{ steps.check-admin.outputs.has_permission }} |
||||
WRITE_PERM: ${{ steps.check-write.outputs.has_permission }} |
||||
ACTUAL_PERM: ${{ steps.check-admin.outputs.user_permission }} |
||||
run: | |
||||
echo "User: $ACTOR" |
||||
echo "Admin permission: $ADMIN_PERM" |
||||
echo "Write permission: $WRITE_PERM" |
||||
echo "Actual permission: $ACTUAL_PERM" |
||||
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
# Examples |
||||
|
||||
## Basic Usage |
||||
|
||||
### Fail Mode (default) |
||||
|
||||
Workflow stops if permission missing. |
||||
|
||||
```yaml |
||||
steps: |
||||
- uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: admin |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
``` |
||||
|
||||
### Skip Mode |
||||
|
||||
Workflow continues, skip protected steps. |
||||
|
||||
```yaml |
||||
steps: |
||||
- id: check |
||||
uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
- if: steps.check.outputs.should_skip != 'true' |
||||
run: ./deploy.sh |
||||
``` |
||||
|
||||
### Continue Mode |
||||
|
||||
Branch on actual permission level. |
||||
|
||||
```yaml |
||||
steps: |
||||
- id: check |
||||
uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: continue |
||||
|
||||
- if: steps.check.outputs.user_permission == 'admin' |
||||
run: ./admin-deploy.sh |
||||
|
||||
- if: steps.check.outputs.user_permission == 'write' |
||||
run: ./standard-deploy.sh |
||||
|
||||
- if: steps.check.outputs.user_permission == 'read' |
||||
run: ./read-only.sh |
||||
``` |
||||
|
||||
## Advanced Patterns |
||||
|
||||
### Multi-Environment Deploy |
||||
|
||||
```yaml |
||||
jobs: |
||||
check: |
||||
outputs: |
||||
permission: ${{ steps.check.outputs.user_permission }} |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- id: check |
||||
uses: ./check-permission |
||||
with: |
||||
require: read |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: continue |
||||
|
||||
prod: |
||||
needs: check |
||||
if: needs.check.outputs.permission == 'admin' |
||||
steps: |
||||
- run: ./deploy-prod.sh |
||||
|
||||
staging: |
||||
needs: check |
||||
if: needs.check.outputs.permission == 'write' |
||||
steps: |
||||
- run: ./deploy-staging.sh |
||||
|
||||
dev: |
||||
needs: check |
||||
if: needs.check.outputs.permission == 'read' |
||||
steps: |
||||
- run: ./deploy-dev.sh |
||||
``` |
||||
|
||||
### Permission Gate |
||||
|
||||
```yaml |
||||
jobs: |
||||
gate: |
||||
outputs: |
||||
should_skip: ${{ steps.check.outputs.should_skip }} |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- id: check |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
deploy: |
||||
needs: gate |
||||
if: needs.gate.outputs.should_skip != 'true' |
||||
steps: |
||||
- run: ./deploy.sh |
||||
``` |
||||
|
||||
### Fallback Strategy |
||||
|
||||
```yaml |
||||
steps: |
||||
- id: admin |
||||
uses: ./check-permission |
||||
with: |
||||
require: admin |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
- if: steps.admin.outputs.should_skip == 'true' |
||||
id: write |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: fail |
||||
|
||||
- if: steps.admin.outputs.has_permission == 'true' |
||||
run: ./full-deploy.sh |
||||
|
||||
- if: steps.admin.outputs.should_skip == 'true' |
||||
run: ./standard-deploy.sh |
||||
``` |
||||
|
||||
## Reusable Workflows |
||||
|
||||
### Flexible Consumer Control |
||||
|
||||
```yaml |
||||
# reusable-deploy.yml |
||||
on: |
||||
workflow_call: |
||||
inputs: |
||||
failure_mode: |
||||
type: string |
||||
default: 'fail' |
||||
|
||||
jobs: |
||||
check: |
||||
outputs: |
||||
should_skip: ${{ steps.check.outputs.should_skip }} |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- id: check |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: ${{ inputs.failure_mode }} |
||||
|
||||
deploy: |
||||
needs: check |
||||
if: needs.check.outputs.should_skip != 'true' |
||||
steps: |
||||
- run: ./deploy.sh |
||||
``` |
||||
|
||||
### Job-Level Outputs |
||||
|
||||
```yaml |
||||
on: |
||||
workflow_call: |
||||
outputs: |
||||
deployed: |
||||
value: ${{ jobs.check.outputs.has_permission }} |
||||
|
||||
jobs: |
||||
check: |
||||
outputs: |
||||
has_permission: ${{ steps.check.outputs.has_permission }} |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- id: check |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
deploy: |
||||
needs: check |
||||
if: needs.check.outputs.has_permission == 'true' |
||||
steps: |
||||
- run: ./deploy.sh |
||||
``` |
||||
|
||||
## Best Practices |
||||
|
||||
- Use `fail` for critical operations |
||||
- Use `skip` for optional features |
||||
- Use `continue` for tiered functionality |
||||
- Specify `failure_mode` explicitly |
||||
- Test with different permission levels |
||||
- Handle bot accounts separately |
||||
- Use outputs for conditional logic |
||||
- Document permission requirements |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
# Check Permission Action |
||||
|
||||
Check user permissions with configurable failure handling. |
||||
|
||||
## Features |
||||
|
||||
- Check permissions: admin, write, read, none |
||||
- Three modes: fail, skip, or continue |
||||
- Control workflow execution based on permission level |
||||
- Works in reusable workflows |
||||
|
||||
## Inputs |
||||
|
||||
| Input | Description | Required | Default | |
||||
| -------------- | ------------------------------------------------------------ | -------- | ------- | |
||||
| `require` | Required permission level (`admin`, `write`, `read`, `none`) | Yes | - | |
||||
| `username` | Username to check permissions for | Yes | - | |
||||
| `token` | GitHub token for API access | Yes | - | |
||||
| `failure_mode` | How to handle failures: `fail`, `skip`, or `continue` | No | `fail` | |
||||
|
||||
### Failure Modes |
||||
|
||||
- **`fail`**: Exit 1 when permission missing - workflow stops |
||||
- **`skip`**: Exit 0, set `should_skip=true` - skip protected steps |
||||
- **`continue`**: Exit 0 always - branch on `has_permission` output |
||||
|
||||
## Outputs |
||||
|
||||
| Output | Description | |
||||
| ----------------- | ---------------------------------------------------------------------- | |
||||
| `has_permission` | `true` if user has required permission, `false` otherwise | |
||||
| `user_permission` | Actual permission level of the user (`admin`, `write`, `read`, `none`) | |
||||
| `should_skip` | `true` when failure_mode is `skip` and permission check fails | |
||||
|
||||
## Usage Examples |
||||
|
||||
### Hard Fail (default) |
||||
|
||||
```yaml |
||||
- uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
``` |
||||
|
||||
### Skip Mode |
||||
|
||||
```yaml |
||||
- id: permission |
||||
uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: skip |
||||
|
||||
- if: steps.permission.outputs.should_skip != 'true' |
||||
run: ./deploy.sh |
||||
``` |
||||
|
||||
### Continue Mode |
||||
|
||||
```yaml |
||||
- id: permission |
||||
uses: bitwarden/gh-actions/check-permission@main |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: continue |
||||
|
||||
- if: steps.permission.outputs.user_permission == 'admin' |
||||
run: ./admin-deploy.sh |
||||
|
||||
- if: steps.permission.outputs.user_permission == 'write' |
||||
run: ./standard-deploy.sh |
||||
``` |
||||
|
||||
### Reusable Workflow |
||||
|
||||
```yaml |
||||
on: |
||||
workflow_call: |
||||
inputs: |
||||
failure_mode: |
||||
type: string |
||||
default: 'fail' |
||||
|
||||
jobs: |
||||
check: |
||||
outputs: |
||||
should_skip: ${{ steps.check.outputs.should_skip }} |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- id: check |
||||
uses: ./check-permission |
||||
with: |
||||
require: write |
||||
username: ${{ github.triggering_actor }} |
||||
token: ${{ secrets.GITHUB_TOKEN }} |
||||
failure_mode: ${{ inputs.failure_mode }} |
||||
|
||||
deploy: |
||||
needs: check |
||||
if: needs.check.outputs.should_skip != 'true' |
||||
steps: |
||||
- run: ./deploy.sh |
||||
``` |
||||
|
||||
## Permissions |
||||
|
||||
Requires `contents: read` permission. The default `GITHUB_TOKEN` works. |
||||
|
||||
```yaml |
||||
permissions: |
||||
contents: read |
||||
``` |
||||
@ -0,0 +1,113 @@
@@ -0,0 +1,113 @@
|
||||
name: "Check Permission" |
||||
description: "A composite action that checks if a user has the required permission level with configurable failure modes" |
||||
author: "Bitwarden" |
||||
branding: |
||||
icon: shield |
||||
color: blue |
||||
|
||||
inputs: |
||||
require: |
||||
description: "Required permission level (admin, write, read, none)" |
||||
required: true |
||||
username: |
||||
description: "Username to check permissions for" |
||||
required: true |
||||
token: |
||||
description: "GitHub token for API access" |
||||
required: true |
||||
failure_mode: |
||||
description: "How to handle permission failures: 'fail' (exit 1), 'skip' (exit 0 with skip output), 'continue' (exit 0 always, use outputs for branching)" |
||||
required: false |
||||
default: "fail" |
||||
|
||||
outputs: |
||||
has_permission: |
||||
description: "Whether the user has the required permission (true/false)" |
||||
value: ${{ steps.check-permission.outputs.has_permission }} |
||||
user_permission: |
||||
description: "The actual permission level of the user" |
||||
value: ${{ steps.check-permission.outputs.user_permission }} |
||||
should_skip: |
||||
description: "Whether the workflow should skip (true when failure_mode is 'skip' and permission check fails)" |
||||
value: ${{ steps.check-permission.outputs.should_skip }} |
||||
|
||||
runs: |
||||
using: "composite" |
||||
steps: |
||||
- name: Check user permission |
||||
id: check-permission |
||||
shell: bash |
||||
env: |
||||
GITHUB_TOKEN: ${{ inputs.token }} |
||||
REQUIRE: ${{ inputs.require }} |
||||
USERNAME: ${{ inputs.username }} |
||||
FAILURE_MODE: ${{ inputs.failure_mode }} |
||||
GITHUB_REPOSITORY: ${{ github.repository }} |
||||
run: | |
||||
set -e |
||||
|
||||
if [[ ! "$FAILURE_MODE" =~ ^(fail|skip|continue)$ ]]; then |
||||
echo "::error::Invalid failure_mode; it must be 'fail', 'skip', or 'continue'" |
||||
exit 1 |
||||
fi |
||||
|
||||
if [[ ! "$REQUIRE" =~ ^(admin|write|read|none)$ ]]; then |
||||
echo "::error::Invalid require level; it must be 'admin', 'write', 'read', or 'none'" |
||||
exit 1 |
||||
fi |
||||
|
||||
if [[ -z "$USERNAME" ]] || [[ ! "$USERNAME" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?(\[bot\])?$ ]]; then |
||||
echo "::error::Invalid username format. Username cannot be empty and must match GitHub's username requirements." |
||||
exit 1 |
||||
fi |
||||
|
||||
if ! USER_PERMISSION=$(gh api \ |
||||
"repos/$GITHUB_REPOSITORY/collaborators/$USERNAME/permission" \ |
||||
--jq '.permission // "none"' 2>&1); then |
||||
|
||||
echo "::warning::Failed to check user permission: $USER_PERMISSION" |
||||
|
||||
if [[ "$FAILURE_MODE" == "fail" ]]; then |
||||
echo "::error::Permission check failed and failure_mode is 'fail'" |
||||
exit 1 |
||||
else |
||||
# Treat API failure as "no permission" for skip/continue modes |
||||
echo "::notice::Treating API failure as 'none' permission due to failure_mode='$FAILURE_MODE'" |
||||
USER_PERMISSION="none" |
||||
fi |
||||
fi |
||||
|
||||
echo "user_permission=$USER_PERMISSION" >> "$GITHUB_OUTPUT" |
||||
|
||||
declare -A PERM_LEVELS=( ["none"]=0 ["read"]=1 ["write"]=2 ["admin"]=3 ) |
||||
|
||||
USER_LEVEL=${PERM_LEVELS[$USER_PERMISSION]:-0} |
||||
REQUIRED_LEVEL=${PERM_LEVELS[$REQUIRE]} |
||||
|
||||
# Check if user has required permission |
||||
if [ $USER_LEVEL -ge $REQUIRED_LEVEL ]; then |
||||
echo "✓ User has required permission" |
||||
echo "has_permission=true" >> "$GITHUB_OUTPUT" |
||||
echo "should_skip=false" >> "$GITHUB_OUTPUT" |
||||
exit 0 |
||||
else |
||||
echo "✗ User does not have required permission" |
||||
echo "has_permission=false" >> "$GITHUB_OUTPUT" |
||||
case "$FAILURE_MODE" in |
||||
fail) |
||||
echo "::error::User '$USERNAME' does not have required '$REQUIRE' permission." |
||||
echo "should_skip=false" >> "$GITHUB_OUTPUT" |
||||
exit 1 |
||||
;; |
||||
skip) |
||||
echo "::warning::User '$USERNAME' does not have required '$REQUIRE' permission. Marking for skip." |
||||
echo "should_skip=true" >> "$GITHUB_OUTPUT" |
||||
exit 0 |
||||
;; |
||||
continue) |
||||
echo "::notice::User '$USERNAME' does not have required '$REQUIRE' permission. Continuing - check outputs for branching." |
||||
echo "should_skip=false" >> "$GITHUB_OUTPUT" |
||||
exit 0 |
||||
;; |
||||
esac |
||||
fi |
||||
Loading…
Reference in new issue