EbookFoundation/free-programming-books
7 workflows · maturity 50% · 2 patterns · GitHub ↗
Practices
✓ Matrix✓ Permissions○ Security scan○ AI review○ Cache✓ Concurrency○ Reusable workflows
Detected patterns
Security dimensions
Workflows (7)
check-urls matrix perms .github/workflows/check-urls.yml
View raw YAML
name: Check URLs from changed files
on:
push:
pull_request:
permissions:
# needed for checkout code
contents: read
# This allows a subsequently queued workflow run to interrupt/wait for previous runs
concurrency:
group: '${{ github.workflow }} @ ${{ github.run_id }}'
cancel-in-progress: false # true = interrupt, false = wait
jobs:
# NOTE: tj-actions/changed-files.
# For push events you need to include fetch-depth: 0 | 2 depending on your use case.
# 0: retrieve all history for all branches and tags
# 1: retrieve only current commit (by default)
# 2: retrieve until the preceding commit
get-changed-files:
name: Get changed files
runs-on: ubuntu-latest
outputs:
fetch-depth: ${{ steps.set-params.outputs.fetch-depth }}
files: ${{ steps.set-files.outputs.files }}
files-len: ${{ steps.set-files.outputs.files-len }}
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Determine workflow params
id: set-params
run: |
echo "fetch_depth=0" >> $GITHUB_OUTPUT
if [ "${{ github.event_name }}" == "pull_request" ]; then
echo "fetch_depth=0" >> $GITHUB_OUTPUT
fi
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: ${{ steps.set-params.outputs.fetch-depth }}
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
separator: " "
json: true
- id: set-files
run: |
echo "${{ steps.changed-files.outputs.all_changed_files }}" \
| jq --raw-output '. | join(" ")' \
| sed -e 's/^/files=/' \
>> $GITHUB_OUTPUT
echo "${{ steps.changed-files.outputs.all_changed_files }}" \
| jq --raw-output '. | length' \
| sed -e 's/^/files-len=/' \
>> $GITHUB_OUTPUT
- id: set-matrix
run: |
echo "{\"file\":${{ steps.changed-files.outputs.all_changed_files }}}" \
| sed -e 's/^/matrix=/' \
>> $GITHUB_OUTPUT
check-urls:
name: Check @ ${{ matrix.file }}
if: ${{ fromJSON(needs.get-changed-files.outputs.files-len) > 0 }}
needs: [get-changed-files]
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.get-changed-files.outputs.matrix) }}
max-parallel: 10
fail-fast: false
steps:
- name: Checkout
if: ${{ endsWith(matrix.file, '.yml') || endsWith(matrix.file, '.md') }}
uses: actions/checkout@v5
with:
fetch-depth: ${{ needs.get-changed-files.outputs.fetch-depth }}
- name: Setup Ruby v2.6
if: ${{ endsWith(matrix.file, '.yml') || endsWith(matrix.file, '.md') }}
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
- name: Install awesome_bot
if: ${{ endsWith(matrix.file, '.yml') || endsWith(matrix.file, '.md') }}
run: |
gem install awesome_bot
- name: Set output
id: set-output
# FILENAME takes the complete file path and strips everything before the final '/'
# FILEPATH replaces all '/' with '-' in the file path since '/' is not allowed in upload artifact name
# Due to a bug in actions/download-artifact, we need to rename README.md to BASE_README.md
run: |
echo "FILENAME=$(echo ${{ matrix.file }} | grep -oE '[a-zA-Z0-9_-]+(\.yml|\.md)')" >> "$GITHUB_OUTPUT"
file_path="${{ matrix.file }}"
file_path="${file_path//\//-}"
if [[ "$file_path" == "README.md" ]]; then
file_path="BASE_README.md"
fi
echo "FILEPATH=${file_path}" >> "$GITHUB_OUTPUT"
- name: "Check URLs of file: ${{ matrix.file }}"
if: ${{ endsWith(matrix.file, '.yml') || endsWith(matrix.file, '.md') }}
run: |
awesome_bot "${{ matrix.file }}" --allow-redirect --allow-dupe --allow-ssl || true;
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.set-output.outputs.FILEPATH }}
path: ${{ github.workspace }}/ab-results-*.json
reporter:
name: GitHub report
needs: [get-changed-files, check-urls]
runs-on: ubuntu-latest
steps:
- name: Checkout # for having the sources of the local action
uses: actions/checkout@v5
# download and unzip the ab-results-*.json generated by job-matrix: check-urls
- name: Download artifacts
uses: actions/download-artifact@v8
- name: Generate Summary Report
uses: ./.github/actions/awesomebot-gh-summary-action
with:
ab-root: ${{ github.workspace }}
files: ${{ needs.get-changed-files.outputs.files }}
separator: " "
append-heading: ${{ true }}
comment-pr .github/workflows/comment-pr.yml
View raw YAML
name: Comment on the pull request
on:
workflow_run:
workflows: ["free-programming-books-lint"]
types:
- completed
jobs:
upload:
permissions:
pull-requests: write
runs-on: ubuntu-latest
if: >
${{ github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' }}
steps:
- name: 'Download artifact'
uses: actions/github-script@v8
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip pr.zip
- name: 'Comment on PR'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ -s error.log ]
then
gh pr comment $(<PRurl) -b "Linter failed, fix the error(s):
\`\`\`
$(cat error.log)
\`\`\`"
gh pr edit $(<PRurl) --add-label "linter error"
else
gh pr edit $(<PRurl) --remove-label "linter error"
fi
detect-conflicting-prs perms .github/workflows/detect-conflicting-prs.yml
View raw YAML
name: "Detect conflicting PRs"
on:
workflow_dispatch: # manually
# So that PRs touching the same files as the push are updated
push:
# So that the `dirtyLabel` is removed if conflicts are resolved
pull_request_target: # - A pull request (even with conflicts)
types:
- synchronize # pushing more commits
permissions:
# no checkouts/branching needed
contents: none
# need by "eps1lon/actions-label-merge-conflict" to manage PR label/comments
pull-requests: write
# This allows a subsequently queued workflow run to interrupt/wait for previous runs
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: false # true: interrupt, false = wait for
jobs:
detect-prs:
name: Detect
if: ${{ github.actor != 'dependabot[bot]' }} # avoid dependabot PRs
runs-on: ubuntu-latest
steps:
- name: Label conflicting PRs that are open
id: pr-labeler
uses: eps1lon/actions-label-merge-conflict@v3.0.3
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
retryAfter: 30 # seconds
retryMax: 5 # atemps
dirtyLabel: conflicts
commentOnDirty: |
Oh no 😟! Conflicts have been found.
Please 🙏, take a moment and [address the merge conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts) of your pull request before we can evaluate it again.
Thanks in advance for your effort and patience ❤️!
continueOnMissingPermissions: true
- name: Print outputs
run: echo ${{ join(steps.pr-labeler.outputs.*, ',') }}
- name: Set PRs outputs
id: set-prs
run: |
echo "$INPUT_PRS" \
| jq --compact-output --raw-output 'to_entries | map({number: .key, dirty: .value})' \
| sed -e 's/^/prs=/' \
>> $GITHUB_OUTPUT
echo "$INPUT_PRS" \
| jq --raw-output 'to_entries | length' \
| sed -e 's/^/prs-len=/' \
>> $GITHUB_OUTPUT
env:
INPUT_PRS: ${{ steps.pr-labeler.outputs.prDirtyStatuses }}
- name: Write job summary
run: |
echo "### Pull Request statuses" \
>> $GITHUB_STEP_SUMMARY
# render json array to a Markdown table with an optional "No records" message if empty
echo "$INPUT_PRS" \
| jq --raw-output 'map("| [#\(.number)](\(env.GITHUB_PUBLIC_URL)/\(.number)) | \(if (.dirty) then "❌" else "✔️" end) |") | join("\n") | if (. == "") then "\nNo records.\n" else "\n| PR | Mergeable? |\n|---:|:----------:|\n\(.)\n" end' \
>> $GITHUB_STEP_SUMMARY
env:
GITHUB_PUBLIC_URL: ${{ format('{0}/{1}/pull', github.server_url, github.repository) }}
INPUT_PRS: ${{ steps.set-prs.outputs.prs }}
fpb-lint perms .github/workflows/fpb-lint.yml
View raw YAML
name: free-programming-books-lint
on: [pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '16.x'
- run: npm install -g free-programming-books-lint
- name: Pull Request
run: |
fpb-lint books casts courses more &> output.log
- name: Clean output and create artifacts
if: always()
run: |
mkdir -p ./pr
echo ${{ github.event.pull_request.html_url }} > ./pr/PRurl
cat output.log | sed -E 's:/home/runner/work/free-programming-books/|⚠.+::' | uniq > ./pr/error.log
- uses: actions/upload-artifact@v7
if: always()
with:
name: pr
path: pr/
issues-pinner perms .github/workflows/issues-pinner.yml
View raw YAML
#
# This workflow adds a label to the issue involved on event when is pinned
# and removes it when unpinned.
#
# It also is enhanced with `stale.yml` workflow: pinned issues never stales
# because that label is declared as part of it `exempt-issue-labels`
# input parameter.
#
name: Issues pinner management
on:
issues:
types:
- "pinned"
- "unpinned"
permissions:
# no checkouts/branching needed
contents: none
# needed by "action-add-labels / action-remove-labels" to CRUD labels
issues: write
# This allows a subsequently queued workflow run to interrupt/wait for previous runs
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.issue.number || github.run_id }}'
cancel-in-progress: false # true: interrupt, false = wait for
jobs:
labeler:
name: Pushpin labeler
runs-on: ubuntu-latest
steps:
- name: Add pushpin label on pinning an issue
id: if-pinned
if: github.event.action == 'pinned'
uses: actions-ecosystem/action-add-labels@v1
with:
repo: ${{ github.repository }}
number: ${{ github.event.issue.number }}
labels: |
:pushpin: pinned
- name: Remove pushpin label on unpinning an issue
id: if-unpinned
if: github.event.action == 'unpinned'
uses: actions-ecosystem/action-remove-labels@v1
with:
repo: ${{ github.repository }}
number: ${{ github.event.issue.number }}
labels: |
:pushpin: pinned
- name: GitHub reporter
# run even previous steps fails
if: always()
run: |
echo "$INPUT_SUMMARY" >> $GITHUB_STEP_SUMMARY;
env:
INPUT_SUMMARY: ${{ format('Issue [\#{2}]({0}/{1}/issues/{2}) should be `{3}`.',
github.server_url, github.repository,
github.event.issue.number,
github.event.action) }}
rtl-ltr-linter perms .github/workflows/rtl-ltr-linter.yml
View raw YAML
name: RTL/LTR Markdown Linter
on: [pull_request]
permissions:
contents: read # Required to checkout the repository content
jobs:
lint:
runs-on: ubuntu-latest
steps:
# Checkout the repository code
- name: Checkout code
uses: actions/checkout@v5
# Fetch the full history of 'main' for accurate git diff in PRs
- name: Fetch all history for main
run: git fetch --no-tags --prune --depth=50 origin main
# Set up the required Python version for the linter
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11' # Use a recent Python version for compatibility
# Install only the Python dependencies needed for the linter script
- name: Install Python dependencies
run: |
pip install python-bidi PyYAML
# (Optional) List files for debugging purposes
- name: List files in scripts directory and current path
run: |
echo "Current working directory:"
pwd
echo "Listing contents of scripts directory (if it exists at root):"
ls -la scripts/ || echo "scripts/ directory not found at root or ls failed"
# Identify all changed Markdown files in the PR using tj-actions/changed-files
- name: Get changed Markdown files
id: changed_md_files
uses: tj-actions/changed-files@v46
with:
files: |
**/*.md
# Check if the PR has the "RTL" label
- name: Check for RTL label
id: rtl_label
run: |
gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name' | grep -q '^RTL$' && echo "has_labels=true" >> $GITHUB_OUTPUT || echo "has_labels=false" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
# Check if any changed file is in ar, he, fa, ur
- name: Check for RTL language file changes
id: rtl_lang_files
run: |
RTL_CHANGED=false
for f in ${{ steps.changed_md_files.outputs.all_changed_files }}; do
if [[ "$f" =~ (ar|he|fa|ur) ]]; then
RTL_CHANGED=true
break
fi
done
echo "rtl_changed=$RTL_CHANGED" >> $GITHUB_OUTPUT
# Run the RTL/LTR Markdown linter:
# - Scans all Markdown files for issues and writes a full log
# - Prints GitHub Actions annotations only for issues on changed lines in changed files
# - Fails the job if any error or warning is found on changed lines
- name: Run RTL/LTR Markdown linter
id: run_linter
if: steps.rtl_label.outputs.has_labels == 'true' || steps.rtl_lang_files.outputs.rtl_changed == 'true'
continue-on-error: true
run: |
echo "Scanning all specified paths for full log..."
echo "Changed Markdown files for PR annotations: ${{ steps.changed_md_files.outputs.all_changed_files }}"
CHANGED_FILES_ARGS=""
if [ "${{ steps.changed_md_files.outputs.all_changed_files_count }}" -gt 0 ]; then
# Pass changed files to the script for PR annotation generation
CHANGED_FILES_ARGS="--changed-files ${{ steps.changed_md_files.outputs.all_changed_files }}"
fi
# Execute the linter.
# Annotations for changed files will be printed to stdout by the script.
# The script will also write a full log to 'rtl-linter-output.log'.
# If the script exits with a non-zero code (error found), this step will fail.
python3 scripts/rtl_ltr_linter.py books casts courses more ${CHANGED_FILES_ARGS} --log-file rtl-linter-output.log
# Upload the linter output log as a workflow artifact
# Only if the linter step was executed (success or failure)
- name: Upload linter output artifact
if: steps.run_linter.conclusion == 'success' || steps.run_linter.conclusion == 'failure'
uses: actions/upload-artifact@v7
with:
name: rtl-linter-output # Name of the artifact
path: rtl-linter-output.log # Path to the output file
if-no-files-found: ignore # Ignore if no files are foundstale perms .github/workflows/stale.yml
View raw YAML
name: 'Stale handler'
on:
schedule:
- cron: '0 0 * * *' # Run every day at midnight
workflow_dispatch:
inputs:
debug-only:
type: boolean
description: "Does a dry-run when enabled. No PR's will be altered"
required: true
default: true
permissions:
pull-requests: write
actions: write
issues: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
days-before-issue-stale: -1 # Don't mark issues as stale
days-before-issue-close: -1 # Don't close issues
stale-pr-message: |
'This Pull Request has been automatically marked as stale because it has not had recent activity during last 60 days :sleeping:
It will be closed in 30 days if no further activity occurs. To unstale this PR, draft it, remove stale label, comment with a detailed explanation or push more commits.
There can be many reasons why some specific PR has no activity. The most probable cause is lack of time, not lack of interest.
Thank you for your patience :heart:'
close-pr-message: |
This Pull Request has been automatically closed because it has been inactive during the last 30 days since being marked as stale.
As author or maintainer, it can always be reopened if you see that carry on been useful.
Anyway, thank you for your interest in contribute :heart:
days-before-pr-stale: 60
days-before-pr-close: 30
stale-pr-label: 'stale'
exempt-pr-labels: 'keep' # Don't mark PR's with this label as stale
labels-to-remove-when-unstale: 'stale'
exempt-draft-pr: true
debug-only: ${{ github.event.inputs.debug-only == 'true' }}
enable-statistics: true