EbookFoundation/free-programming-books

7 workflows · maturity 50% · 2 patterns · GitHub ↗

Security 21.43/100

Practices

✓ Matrix✓ Permissions○ Security scan○ AI review○ Cache✓ Concurrency○ Reusable workflows

Detected patterns

Security dimensions

permissions
21.4
security scan
0
supply chain
0
secret handling
0
harden runner
0

Workflows (7)

check-urls matrix perms .github/workflows/check-urls.yml
Triggers
push, pull_request
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
get-changed-files, check-urls, reporter
Matrix
Actions
tj-actions/changed-files, ruby/setup-ruby
Commands
  • echo "fetch_depth=0" >> $GITHUB_OUTPUT if [ "${{ github.event_name }}" == "pull_request" ]; then echo "fetch_depth=0" >> $GITHUB_OUTPUT fi
  • 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
  • echo "{\"file\":${{ steps.changed-files.outputs.all_changed_files }}}" \ | sed -e 's/^/matrix=/' \ >> $GITHUB_OUTPUT
  • gem install awesome_bot
  • 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"
  • awesome_bot "${{ matrix.file }}" --allow-redirect --allow-dupe --allow-ssl || true;
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
Triggers
workflow_run
Runs on
ubuntu-latest
Jobs
upload
Commands
  • unzip pr.zip
  • 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
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
Triggers
workflow_dispatch, push, pull_request_target
Runs on
ubuntu-latest
Jobs
detect-prs
Actions
eps1lon/actions-label-merge-conflict
Commands
  • echo ${{ join(steps.pr-labeler.outputs.*, ',') }}
  • 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
  • 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
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
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
build
Commands
  • npm install -g free-programming-books-lint
  • fpb-lint books casts courses more &> output.log
  • 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
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
Triggers
issues
Runs on
ubuntu-latest
Jobs
labeler
Actions
actions-ecosystem/action-add-labels, actions-ecosystem/action-remove-labels
Commands
  • echo "$INPUT_SUMMARY" >> $GITHUB_STEP_SUMMARY;
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
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
lint
Actions
tj-actions/changed-files
Commands
  • git fetch --no-tags --prune --depth=50 origin main
  • pip install python-bidi PyYAML
  • 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"
  • 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
  • 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
  • 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
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 found
stale perms .github/workflows/stale.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
stale
Actions
actions/stale
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