Skip to content

Commit 30badb2

Browse files
committed
Rebuild maintenance automation on GitHub Actions (README-as-data CI)
The old automation was entirely dead: .travis.yml ran awesome_bot on Travis OSS (shut down), and .github/stale.yml targeted a defunct probot integration. Result: dozens of dead links, years-old broken-link issues, and duplicate PRs fixing the same link. Adds scripts/check_readme.py, a stdlib-only Python validator that parses README.md as structured data (grammar shared by every subcommand so lint/PR checks/the weekly sweep can't drift apart): - `lint`: whole-file grammar, duplicate-URL, and Table of Contents checks - `check-diff`: flags only PR-added lines (shorteners, new http://, malformed entries) so pre-existing link rot can't fail unrelated PRs - `check-links`: liveness-checks URLs with a 2-strike state machine, treating bot-hostile domains (Medium, Reddit, etc.) as BLOCKED rather than dead - `report`: renders the weekly link-rot issue body Wires this up via three workflows: validate-pr.yml (fork-safe PR gate), link-rot.yml (weekly sweep, maintains one consolidated issue instead of scattered reports), and stale-prs.yml (PRs only -- issues are explicitly exempted, unlike the old probot config that buried broken-link reports). Also replaces the generic issue/PR templates with ones that match what the validator actually enforces, and fixes CONTRIBUTING.md's `*`-bullet examples (the real source of format drift) to `-`.
1 parent f55fe25 commit 30badb2

17 files changed

Lines changed: 1394 additions & 115 deletions
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: 🔗 Broken link
2+
description: Report a dead or broken tutorial link in README.md
3+
title: "[Broken link]: "
4+
labels: ["broken-link"]
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Before filing: dead links are also tracked automatically in the weekly
10+
[🔗 Link rot report](../issues?q=is%3Aissue+is%3Aopen+label%3Alink-rot).
11+
Please check there first -- if it's already listed, there's no need to
12+
open a new issue.
13+
- type: input
14+
id: entry-title
15+
attributes:
16+
label: Entry title
17+
description: The exact title text of the README entry.
18+
placeholder: Build an Interpreter
19+
validations:
20+
required: true
21+
- type: input
22+
id: url
23+
attributes:
24+
label: Broken URL
25+
description: The URL currently in README.md that is broken.
26+
placeholder: https://example.com/tutorial
27+
validations:
28+
required: true
29+
- type: dropdown
30+
id: section
31+
attributes:
32+
label: Section
33+
description: Which README section is this entry under?
34+
options:
35+
- C#
36+
- C/C++
37+
- Clojure
38+
- Dart
39+
- Elixir
40+
- Erlang
41+
- F#
42+
- Go
43+
- Haskell
44+
- HTML and CSS
45+
- Java
46+
- JavaScript
47+
- Kotlin
48+
- Lua
49+
- OCaml
50+
- PHP
51+
- Python
52+
- R
53+
- Ruby
54+
- Rust
55+
- Scala
56+
- Swift
57+
- Additional Resources
58+
validations:
59+
required: true
60+
- type: textarea
61+
id: evidence
62+
attributes:
63+
label: Evidence / suggested replacement
64+
description: >-
65+
What did you see (404, parked domain, paywall, etc.)? If the tutorial
66+
moved rather than disappeared, please link the new URL here -- we
67+
prefer updating the entry in place over replacing it with an
68+
archive.org snapshot.
69+
validations:
70+
required: false
71+
- type: checkboxes
72+
id: checks
73+
attributes:
74+
label: Checks
75+
options:
76+
- label: I searched the open 🔗 Link rot report issue and this link is not already listed there.
77+
required: true
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: 💡 Tutorial suggestion
2+
description: Suggest a tutorial to add to README.md
3+
title: "[Suggestion]: "
4+
labels: ["suggestion"]
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Prefer opening a pull request directly if you can -- see
10+
[CONTRIBUTING.md](../blob/master/CONTRIBUTING.md). Use this form if
11+
you'd rather flag an idea for someone else to add.
12+
- type: input
13+
id: title
14+
attributes:
15+
label: Tutorial title
16+
placeholder: Build an Interpreter
17+
validations:
18+
required: true
19+
- type: input
20+
id: url
21+
attributes:
22+
label: URL
23+
placeholder: https://example.com/tutorial
24+
validations:
25+
required: true
26+
- type: dropdown
27+
id: section
28+
attributes:
29+
label: Section
30+
description: Which README section should this go under? Pick "New section" if none fit.
31+
options:
32+
- C#
33+
- C/C++
34+
- Clojure
35+
- Dart
36+
- Elixir
37+
- Erlang
38+
- F#
39+
- Go
40+
- Haskell
41+
- HTML and CSS
42+
- Java
43+
- JavaScript
44+
- Kotlin
45+
- Lua
46+
- OCaml
47+
- PHP
48+
- Python
49+
- R
50+
- Ruby
51+
- Rust
52+
- Scala
53+
- Swift
54+
- Additional Resources
55+
- New section (describe below)
56+
validations:
57+
required: true
58+
- type: checkboxes
59+
id: quality
60+
attributes:
61+
label: Quality bar
62+
options:
63+
- label: Free and open -- no paywall, login wall, or required newsletter signup.
64+
required: true
65+
- label: Project-based -- following it, the reader builds a complete, working artifact (not just a concept explainer).
66+
required: true
67+
- type: dropdown
68+
id: relationship
69+
attributes:
70+
label: Your relationship to this tutorial
71+
options:
72+
- No relationship -- just a reader
73+
- I am the author
74+
- I am affiliated with the author or site (employee, sponsor, etc.)
75+
validations:
76+
required: true

.github/ISSUE_TEMPLATE/Bug_report.md

Lines changed: 0 additions & 23 deletions
This file was deleted.

.github/ISSUE_TEMPLATE/Feature_request.md

Lines changed: 0 additions & 26 deletions
This file was deleted.

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: 💬 Chat / general discussion
4+
url: https://gitter.im/practical-tutorials/community
5+
about: For general questions or discussion, join the Gitter room instead of opening an issue.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
<!--- Provide a general summary of your changes in the Title above -->
1+
<!--- One tutorial per pull request, please -- it makes review much faster. -->
22

3-
## Description
4-
<!--- Describe your changes in detail -->
3+
## What is this?
54

6-
## Motivation and Context
7-
<!--- Why is this change required? What problem does it solve? -->
8-
<!--- If it fixes an open issue, please link to the issue here. -->
5+
- Title:
6+
- URL:
7+
- Section:
98

10-
## How Has This Been Tested?
11-
<!--- Please describe in detail how you tested your changes. -->
12-
<!--- Include details of your testing environment, and the tests you ran to -->
13-
<!--- see how your change affects other areas of the code, etc. -->
9+
## Checklist
1410

15-
## Types of changes
16-
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
17-
- [ ] Content Update (change which fixes an issue or updates an already existing submission)
18-
- [ ] New Article (change which adds functionality)
19-
- [ ] Documentation change
20-
21-
## Checklist:
22-
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
23-
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
24-
- [ ] My code follows the code style of this project.
25-
- [ ] I have updated the documentation accordingly.
26-
- [ ] I have read the **CONTRIBUTING** document.
27-
- [ ] I have made checks to ensure URLs and other resources are valid
11+
- [ ] This PR adds/fixes exactly one tutorial (or is a docs/tooling-only change).
12+
- [ ] Free and open -- no paywall, login wall, or required newsletter signup.
13+
- [ ] Project-based -- following it, the reader builds a complete, working artifact.
14+
- [ ] I searched README.md for this URL and title -- it is not already listed.
15+
- [ ] Entry uses `- [Title](https://...)` format, placed under the correct section (nested `- [Part N](...)` for multi-part series).
16+
- [ ] No URL shorteners.
17+
- [ ] I ran `python3 scripts/check_readme.py lint` locally and it exits 0.
18+
- [ ] Relationship disclosure: no relationship to the author / I am the author / I am affiliated with the author or site (delete the two that don't apply).

.github/link-rot-state.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

.github/stale.yml

Lines changed: 0 additions & 21 deletions
This file was deleted.

.github/workflows/link-rot.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Link rot sweep
2+
3+
on:
4+
schedule:
5+
- cron: "17 6 * * 1"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
issues: write
11+
12+
concurrency:
13+
group: link-rot-sweep
14+
cancel-in-progress: false
15+
16+
jobs:
17+
sweep:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 30
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23+
with:
24+
fetch-depth: 0
25+
26+
- name: Check every link in README.md
27+
run: |
28+
# check-links exits 1 as soon as any URL is dead/suspect -- that's the
29+
# expected, reportable outcome of a link-rot sweep, not a script
30+
# failure, so `|| true` here (state commit + issue upsert below are
31+
# what actually need to keep running either way).
32+
python3 scripts/check_readme.py check-links --all --state .github/link-rot-state.json --json > /tmp/results.json || true
33+
python3 -c "import json; print('stats:', json.load(open('/tmp/results.json'))['stats'])" >> "$GITHUB_STEP_SUMMARY"
34+
35+
- name: Commit updated sweep state
36+
continue-on-error: true
37+
run: |
38+
if git diff --quiet -- .github/link-rot-state.json; then
39+
echo "No state changes to commit."
40+
else
41+
git config user.name "github-actions[bot]"
42+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43+
git add .github/link-rot-state.json
44+
git commit -m "chore(link-rot): update sweep state [skip ci]"
45+
git pull --rebase origin "${{ github.ref_name }}"
46+
git push origin "HEAD:${{ github.ref_name }}"
47+
fi
48+
49+
- name: Render report body
50+
if: always()
51+
run: python3 scripts/check_readme.py report --results /tmp/results.json --state .github/link-rot-state.json > /tmp/report-body.md
52+
53+
- name: Upsert consolidated link-rot report issue
54+
if: always()
55+
env:
56+
GH_TOKEN: ${{ github.token }}
57+
run: |
58+
existing=$(gh issue list --repo "${{ github.repository }}" --search "🔗 Link rot report in:title" --label link-rot --state open --json number --jq '.[0].number // empty')
59+
if [ -n "$existing" ]; then
60+
gh issue edit "$existing" --repo "${{ github.repository }}" --body-file /tmp/report-body.md
61+
else
62+
gh issue create --repo "${{ github.repository }}" --title "🔗 Link rot report" --label link-rot --body-file /tmp/report-body.md
63+
fi

.github/workflows/stale-prs.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# NOTE: PRs only -- issues are explicitly exempted (days-before-issue-stale: -1).
2+
# The old .github/stale.yml (probot, 30/14-day issue staling) is what buried
3+
# legitimate broken-link reports for years; this workflow deliberately never
4+
# staples that behavior back onto issues.
5+
name: Stale PRs
6+
7+
on:
8+
schedule:
9+
- cron: "30 7 * * *"
10+
workflow_dispatch:
11+
12+
permissions:
13+
issues: write
14+
pull-requests: write
15+
16+
concurrency:
17+
group: stale-prs
18+
cancel-in-progress: false
19+
20+
jobs:
21+
stale:
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 15
24+
steps:
25+
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
26+
with:
27+
days-before-issue-stale: -1
28+
days-before-pr-stale: 90
29+
days-before-pr-close: 30
30+
exempt-pr-labels: keep-open
31+
operations-per-run: 60
32+
stale-pr-message: >
33+
This pull request has been automatically marked as stale because it has had
34+
no activity in 90 days. It will be closed in 30 days if no further activity
35+
occurs. If it's still relevant, rebasing or leaving a comment will keep it open --
36+
thank you for the contribution!
37+
close-pr-message: >
38+
Closing due to inactivity. Please feel free to reopen or resubmit if you'd like
39+
to pick this back up -- thank you for the contribution!

0 commit comments

Comments
 (0)