|
1 | | -name: LanguageTool (PR review) |
| 1 | +name: LanguageTool (reviewdog) |
2 | 2 |
|
3 | 3 | on: |
4 | | - # Run once when the PR is opened/re-opened. |
5 | 4 | pull_request_target: |
6 | 5 | types: [opened, reopened, labeled] |
7 | | - |
8 | | - # Allow maintainers to rerun by commenting "/languagetool" |
9 | 6 | issue_comment: |
10 | 7 | types: [created] |
11 | 8 |
|
12 | 9 | permissions: |
13 | 10 | contents: read |
14 | 11 | pull-requests: write |
15 | | - issues: write |
16 | 12 |
|
17 | 13 | concurrency: |
18 | | - group: languagetool-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} |
| 14 | + group: languagetool-${{ github.event.pull_request.number || github.event.issue.number }} |
19 | 15 | cancel-in-progress: true |
20 | 16 |
|
21 | | -env: |
22 | | - LT_LANGUAGE: en-US |
23 | | - RERUN_LABEL: languagetool:run |
24 | | - |
25 | 17 | jobs: |
26 | | - # Comment command -> adds a label -> label event triggers the real run |
27 | | - rerun_on_comment: |
28 | | - if: | |
29 | | - github.event_name == 'issue_comment' && |
30 | | - github.event.issue.pull_request && |
31 | | - contains(github.event.comment.body, '/languagetool') && |
32 | | - (github.event.comment.author_association == 'MEMBER' || |
33 | | - github.event.comment.author_association == 'OWNER' || |
34 | | - github.event.comment.author_association == 'COLLABORATOR') |
| 18 | + languagetool: |
35 | 19 | runs-on: ubuntu-latest |
| 20 | + |
36 | 21 | steps: |
37 | | - - name: Add rerun label to PR |
| 22 | + - name: Decide whether to run + gather PR info |
| 23 | + id: meta |
38 | 24 | uses: actions/github-script@v7 |
39 | 25 | with: |
40 | 26 | script: | |
41 | | - const label = process.env.RERUN_LABEL; |
42 | | - const owner = context.repo.owner; |
43 | | - const repo = context.repo.repo; |
44 | | - const issue_number = context.issue.number; // PR number for issue_comment |
45 | | -
|
46 | | - // Ensure label exists (create if missing) |
47 | | - try { |
48 | | - await github.rest.issues.getLabel({ owner, repo, name: label }); |
49 | | - } catch (e) { |
50 | | - await github.rest.issues.createLabel({ |
51 | | - owner, |
52 | | - repo, |
53 | | - name: label, |
54 | | - color: '0e8a16', |
55 | | - description: 'Rerun LanguageTool on this PR' |
| 27 | + const eventName = context.eventName; |
| 28 | +
|
| 29 | + async function getPerm(username) { |
| 30 | + const res = await github.rest.repos.getCollaboratorPermissionLevel({ |
| 31 | + owner: context.repo.owner, |
| 32 | + repo: context.repo.repo, |
| 33 | + username, |
56 | 34 | }); |
| 35 | + return res.data.permission; // admin|maintain|write|triage|read|none |
57 | 36 | } |
58 | 37 |
|
59 | | - await github.rest.issues.addLabels({ |
60 | | - owner, |
61 | | - repo, |
62 | | - issue_number, |
63 | | - labels: [label] |
64 | | - }); |
| 38 | + let run = false; |
| 39 | + let prNumber = null; |
| 40 | + let pr = null; |
| 41 | +
|
| 42 | + if (eventName === "pull_request_target") { |
| 43 | + pr = context.payload.pull_request; |
| 44 | + prNumber = pr.number; |
| 45 | +
|
| 46 | + if (context.payload.action === "labeled") { |
| 47 | + run = (context.payload.label?.name === "languagetool:rerun"); |
| 48 | + } else { |
| 49 | + // opened / reopened |
| 50 | + run = true; |
| 51 | + } |
| 52 | + } else if (eventName === "issue_comment") { |
| 53 | + // only run for PR comments |
| 54 | + if (!context.payload.issue?.pull_request) { |
| 55 | + run = false; |
| 56 | + } else { |
| 57 | + const body = (context.payload.comment?.body || "").trim(); |
| 58 | + const wants = body.startsWith("/languagetool"); |
| 59 | + if (!wants) { |
| 60 | + run = false; |
| 61 | + } else { |
| 62 | + const perm = await getPerm(context.payload.comment.user.login); |
| 63 | + run = ["admin", "maintain", "write"].includes(perm); |
| 64 | + } |
| 65 | +
|
| 66 | + prNumber = context.payload.issue.number; |
| 67 | + const prRes = await github.rest.pulls.get({ |
| 68 | + owner: context.repo.owner, |
| 69 | + repo: context.repo.repo, |
| 70 | + pull_number: prNumber, |
| 71 | + }); |
| 72 | + pr = prRes.data; |
| 73 | + } |
| 74 | + } |
65 | 75 |
|
66 | | - languagetool: |
67 | | - if: | |
68 | | - github.event_name == 'pull_request_target' && |
69 | | - ( |
70 | | - github.event.action == 'opened' || |
71 | | - github.event.action == 'reopened' || |
72 | | - (github.event.action == 'labeled' && github.event.label.name == 'languagetool:run') |
73 | | - ) |
74 | | - runs-on: ubuntu-latest |
| 76 | + core.setOutput("run", run ? "true" : "false"); |
| 77 | + if (!pr) return; |
75 | 78 |
|
76 | | - steps: |
77 | | - - name: Checkout PR (head SHA) |
| 79 | + core.setOutput("pr_number", String(prNumber)); |
| 80 | + core.setOutput("head_sha", pr.head.sha); |
| 81 | + core.setOutput("base_sha", pr.base.sha); |
| 82 | + core.setOutput("head_repo", pr.head.repo.full_name); |
| 83 | + core.setOutput("base_repo", pr.base.repo.full_name); |
| 84 | +
|
| 85 | + - name: Stop early if not requested |
| 86 | + if: steps.meta.outputs.run != 'true' |
| 87 | + run: echo "Not running LanguageTool." |
| 88 | + |
| 89 | + - name: Checkout PR head (safe) |
| 90 | + if: steps.meta.outputs.run == 'true' |
78 | 91 | uses: actions/checkout@v4 |
79 | 92 | with: |
80 | | - ref: ${{ github.event.pull_request.head.sha }} |
| 93 | + repository: ${{ steps.meta.outputs.head_repo }} |
| 94 | + ref: ${{ steps.meta.outputs.head_sha }} |
81 | 95 | fetch-depth: 0 |
| 96 | + persist-credentials: false |
| 97 | + submodules: false |
82 | 98 |
|
83 | | - - name: Build LanguageTool server image with custom dictionary |
84 | | - shell: bash |
| 99 | + - name: Fetch base SHA for diffing |
| 100 | + if: steps.meta.outputs.run == 'true' |
85 | 101 | run: | |
86 | 102 | set -euo pipefail |
| 103 | + git remote add upstream "https://github.com/${{ steps.meta.outputs.base_repo }}.git" || true |
| 104 | + git fetch --no-tags --depth=1 upstream "${{ steps.meta.outputs.base_sha }}" |
87 | 105 |
|
88 | | - WORDS_DIR=".github/languagetool" |
89 | | - SPELLING_FILE="$WORDS_DIR/spelling.en.txt" |
90 | | - IGNORE_FILE="$WORDS_DIR/ignore.en.txt" |
91 | | -
|
92 | | - mkdir -p "$WORDS_DIR" |
93 | | - test -f "$SPELLING_FILE" || : > "$SPELLING_FILE" |
94 | | - test -f "$IGNORE_FILE" || : > "$IGNORE_FILE" |
95 | | -
|
96 | | - # Safety cap (avoid someone committing a gigantic word list) |
97 | | - head -n 2000 "$SPELLING_FILE" > /tmp/spelling_additions.txt |
98 | | - head -n 2000 "$IGNORE_FILE" > /tmp/ignore_additions.txt |
99 | | -
|
100 | | - mkdir -p /tmp/lt |
101 | | - cp /tmp/spelling_additions.txt /tmp/lt/spelling_additions.txt |
102 | | - cp /tmp/ignore_additions.txt /tmp/lt/ignore_additions.txt |
| 106 | + - name: Setup Python |
| 107 | + if: steps.meta.outputs.run == 'true' |
| 108 | + uses: actions/setup-python@v5 |
| 109 | + with: |
| 110 | + python-version: "3.11" |
103 | 111 |
|
104 | | - cat > /tmp/lt/Dockerfile <<'EOF' |
105 | | - FROM erikvl87/languagetool:latest |
106 | | - USER root |
107 | | - COPY spelling_additions.txt /tmp/spelling_additions.txt |
108 | | - COPY ignore_additions.txt /tmp/ignore_additions.txt |
109 | | - RUN set -e; \ |
110 | | - if [ -s /tmp/spelling_additions.txt ]; then (echo; cat /tmp/spelling_additions.txt) >> org/languagetool/resource/en/hunspell/spelling.txt; fi; \ |
111 | | - if [ -s /tmp/ignore_additions.txt ]; then (echo; cat /tmp/ignore_additions.txt) >> org/languagetool/resource/en/hunspell/ignore.txt; fi |
112 | | - USER languagetool |
113 | | - EOF |
| 112 | + - name: Install Python deps |
| 113 | + if: steps.meta.outputs.run == 'true' |
| 114 | + run: | |
| 115 | + python -m pip install --upgrade pip |
| 116 | + python -m pip install requests |
114 | 117 |
|
115 | | - docker build -t lt-custom /tmp/lt |
| 118 | + - name: Setup reviewdog |
| 119 | + if: steps.meta.outputs.run == 'true' |
| 120 | + uses: reviewdog/action-setup@v1 |
| 121 | + with: |
| 122 | + reviewdog_version: latest |
116 | 123 |
|
117 | 124 | - name: Start LanguageTool server |
118 | | - shell: bash |
| 125 | + if: steps.meta.outputs.run == 'true' |
119 | 126 | run: | |
120 | 127 | set -euo pipefail |
121 | | - docker run -d --name languagetool -p 8010:8010 lt-custom |
| 128 | + docker run -d --rm --name languagetool -p 8010:8010 erikvl87/languagetool:latest |
122 | 129 |
|
123 | | - # Wait until the API is up |
124 | | - for i in {1..60}; do |
125 | | - if curl -fsS http://127.0.0.1:8010/v2/languages >/dev/null; then |
| 130 | + # Wait until ready |
| 131 | + for i in $(seq 1 60); do |
| 132 | + if curl -fsS "http://localhost:8010/v2/languages" >/dev/null; then |
| 133 | + echo "LanguageTool is up." |
126 | 134 | exit 0 |
127 | 135 | fi |
128 | | - sleep 1 |
| 136 | + sleep 2 |
129 | 137 | done |
130 | 138 |
|
131 | | - echo "LanguageTool server did not start in time" >&2 |
| 139 | + echo "LanguageTool did not become ready in time" >&2 |
132 | 140 | docker logs languagetool || true |
133 | 141 | exit 1 |
134 | 142 |
|
135 | | - - name: Run LanguageTool and comment suggestions on the PR |
136 | | - uses: reviewdog/action-languagetool@v1.23.0 |
137 | | - with: |
138 | | - github_token: ${{ secrets.GITHUB_TOKEN }} |
139 | | - reporter: github-pr-review |
140 | | - level: info |
141 | | - patterns: "**/*.md **/*.txt **/*.rst **/*.adoc" |
142 | | - language: ${{ env.LT_LANGUAGE }} |
143 | | - custom_api_endpoint: "http://127.0.0.1:8010" |
144 | | - |
145 | | - - name: Remove rerun label (so maintainers can trigger again) |
146 | | - if: github.event.action == 'labeled' && github.event.label.name == 'languagetool:run' |
147 | | - continue-on-error: true |
| 143 | + - name: Run LanguageTool and comment on PR |
| 144 | + if: steps.meta.outputs.run == 'true' |
| 145 | + env: |
| 146 | + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 147 | + run: | |
| 148 | + set -euo pipefail |
| 149 | + python .github/scripts/languagetool_reviewdog.py \ |
| 150 | + --api-url "http://localhost:8010/v2/check" \ |
| 151 | + --language "en-US" \ |
| 152 | + --base-sha "${{ steps.meta.outputs.base_sha }}" \ |
| 153 | + --head-sha "${{ steps.meta.outputs.head_sha }}" \ |
| 154 | + --dictionary ".languagetool/words.txt" \ |
| 155 | + | reviewdog -f=rdjson \ |
| 156 | + -name="LanguageTool" \ |
| 157 | + -reporter="github-pr-review" \ |
| 158 | + -filter-mode="file" \ |
| 159 | + -fail-level="none" \ |
| 160 | + -level="warning" |
| 161 | +
|
| 162 | + - name: Remove rerun label (so it can be added again later) |
| 163 | + if: steps.meta.outputs.run == 'true' && github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'languagetool:rerun' |
148 | 164 | uses: actions/github-script@v7 |
149 | 165 | with: |
150 | 166 | script: | |
151 | 167 | await github.rest.issues.removeLabel({ |
152 | 168 | owner: context.repo.owner, |
153 | 169 | repo: context.repo.repo, |
154 | 170 | issue_number: context.payload.pull_request.number, |
155 | | - name: 'languagetool:run', |
| 171 | + name: "languagetool:rerun", |
156 | 172 | }); |
| 173 | +
|
| 174 | + - name: Stop LanguageTool |
| 175 | + if: always() && steps.meta.outputs.run == 'true' |
| 176 | + run: docker stop languagetool || true |
0 commit comments