Skip to content

Commit c02dc39

Browse files
committed
ci: connect puzzles with GitHub issues
Part of #1610
1 parent 0fd60da commit c02dc39

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

.github/workflows/todos-extract-from-code.yml

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Extract todos from code
1+
name: Create issues from todos
22

33
on:
44
push:
@@ -123,6 +123,36 @@ jobs:
123123
git commit --all -m "$NEW_COMMIT_MSG"
124124
git push
125125
126+
- name: Connect todos to the issues
127+
if: env.PUZZLES_FILES_MODIFIED == 'yes'
128+
env:
129+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+
run: ./src/main/scripts/ci/connect-todos-to-issues.sh generated-todos/todos-in-code.tsv
131+
132+
- name: Check whether todos-on-github.tsv has been modified
133+
if: env.PUZZLES_FILES_MODIFIED == 'yes'
134+
working-directory: generated-todos
135+
run: |
136+
PUZZLES_MAPPING_MODIFIED=no
137+
138+
if ! git diff-index --quiet HEAD -- todos-on-github.tsv; then
139+
PUZZLES_MAPPING_MODIFIED=yes
140+
echo 'todos-on-github.tsv has been modified'
141+
fi
142+
143+
# Make variable available for the next steps
144+
echo "PUZZLES_MAPPING_MODIFIED=$PUZZLES_MAPPING_MODIFIED" | tee -a "$GITHUB_ENV"
145+
146+
- name: Commit updated mapping
147+
if: env.PUZZLES_FILES_MODIFIED == 'yes' && env.PUZZLES_MAPPING_MODIFIED == 'yes'
148+
env:
149+
NEW_COMMIT_MSG: "chore: sync issues for ${{ env.COMMIT_MSG }}"
150+
working-directory: generated-todos
151+
run: |
152+
git add todos-on-github.tsv
153+
git commit todos-on-github.tsv -m "$NEW_COMMIT_MSG"
154+
git push
155+
126156
- name: Cleanup
127157
if: always()
128158
run: |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#!/bin/bash
2+
3+
# Treat unset variables and parameters as an error when performing parameter expansion
4+
set -o nounset
5+
6+
# Exit immediately if command returns a non-zero status
7+
set -o errexit
8+
9+
# Return value of a pipeline is the value of the last command to exit with a non-zero status
10+
set -o pipefail
11+
12+
if [ -z "${1:-}" ]; then
13+
echo >&2 "Usage: $(dirname "$0") /path/to/todos-in-code.tsv"
14+
exit 1
15+
fi
16+
17+
debug() {
18+
[ -z "$ENABLE_DEBUG" ] || printf 'DEBUG: %s\n' "$1"
19+
}
20+
21+
info() {
22+
printf 'INFO : %s\n' "$1"
23+
}
24+
25+
warn() {
26+
printf 'WARN : %s\n' "$1"
27+
}
28+
29+
error() {
30+
printf >&2 'ERROR: %s\n' "$1"
31+
}
32+
33+
fatal() {
34+
error "$1"
35+
exit 1
36+
}
37+
38+
# GH_NO_UPDATE_NOTIFIER: set to any value to disable update notifications.
39+
# By default, gh checks for new releases once every 24 hours and displays an upgrade notice
40+
# on standard error if a newer version was found.
41+
export GH_NO_UPDATE_NOTIFIER=yes
42+
43+
# NOTE: requires `gh auth login --hostname github.com --git-protocol https --web` prior executing the script
44+
export GH_TOKEN="$(gh auth token)"
45+
[ -n "$GH_TOKEN" ] || fatal 'gh auth token returns an empty string'
46+
47+
# DEBUG (deprecated): set to "1", "true", or "yes" to enable verbose output on standard error.
48+
# GH_DEBUG: set to a truthy value to enable verbose output on standard error. Set to "api" to additionally log details of HTTP traffic.
49+
#export GH_DEBUG=api
50+
51+
# NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output.
52+
#export NO_COLOR=yes
53+
54+
# a non-empty string enabled debug output
55+
ENABLE_DEBUG=
56+
57+
DIR="$(dirname "$1")"
58+
MAPPING_FILE="$DIR/todos-on-github.tsv"
59+
60+
if [ ! -f "$MAPPING_FILE" ]; then
61+
info "$MAPPING_FILE doesn't exist. Creating..."
62+
printf 'Id\tIssue\tState\tCreated\n' >> "$MAPPING_FILE"
63+
else
64+
info "$MAPPING_FILE exists"
65+
fi
66+
67+
PUZZLES_COUNT=0
68+
NEW_ISSUES_COUNT=0
69+
70+
while IFS=$'\t' read PUZZLE_ID TICKET TITLE REST; do
71+
PUZZLES_COUNT=$[PUZZLES_COUNT + 1]
72+
73+
# unescape CSV: "a ""quoted"" string" => a "quoted" string
74+
TITLE="$(echo "$TITLE" | sed -e 's|^"||;' -e 's|"$||' -e 's|""|"|g')"
75+
76+
debug "$PUZZLE_ID: has title: '$TITLE'"
77+
MAPPING="$(grep --max-count=1 '^'$PUZZLE_ID'[[:space:]]' "$MAPPING_FILE" || :)"
78+
if [ -n "$MAPPING" ]; then
79+
IFS=$'\t' read ID ISSUE_ID REST <<<"$MAPPING"
80+
info "$PUZZLE_ID => #$ISSUE_ID: is already linked"
81+
continue
82+
fi
83+
84+
debug "$PUZZLE_ID: isn't linked to any issues. Will search on GitHub..."
85+
# https://cli.github.com/manual/gh_search_issues
86+
# Brief example of API output:
87+
# {
88+
# "total_count": 1,
89+
# "incomplete_results": false,
90+
# "items": [
91+
# {
92+
# "html_url": "https://github.com/php-coder/mystamps/issues/760",
93+
# "number": 760,
94+
# "title": "Check src/main/config/nginx/503.*html by html5validator",
95+
# "state": "open",
96+
# "body": "The puzzle `109-a721e051` (from #109) in [`src/main/scripts/ci/check-build-and-verify.sh`] ...",
97+
# }
98+
# ]
99+
# }
100+
# NB: we search by body as a title isn't reliable: it might be modified after issue creation and don't match with code
101+
# (but later, we check a title anyway)
102+
debug "$PUZZLE_ID: search issues with a body that contain puzzle id '$PUZZLE_ID' or a title that is equal to '$TITLE'"
103+
SEARCH_BY_BODY="$(gh search issues --repo php-coder/mystamps --json number,state,url,title,body --match body "$PUZZLE_ID")"
104+
ISSUES_BY_BODY_COUNT="$(echo "$SEARCH_BY_BODY" | jq '. | length')"
105+
debug "$PUZZLE_ID: found $ISSUES_BY_BODY_COUNT issue(s) by body"
106+
107+
SEARCH_BY_TITLE="$(gh search issues --repo php-coder/mystamps --json number,state,url,title,body --match title "$TITLE")"
108+
ISSUES_BY_TITLE_COUNT="$(echo "$SEARCH_BY_TITLE" | jq '. | length')"
109+
debug "$PUZZLE_ID: found $ISSUES_BY_TITLE_COUNT issue(s) by title"
110+
111+
# KNOWN ISSUES:
112+
# 1) As there is no way to search in title with exact match, it's possible to find more than one issue if their titles are similar.
113+
# For example, when lookup for "Add validation", it find an issue with the title "Add validation" and "Add validation for e-mail".
114+
# In this case, we let the user to choose which one is needed.
115+
# 2) For each puzzle id we have to make 2 search requests instead of one because there is no possibility to use logical OR
116+
# (body contains OR title equals) in a search query. As result, we might get "HTTP 403: API rate limit exceeded" error more often
117+
# if we have a lot of issues to process or we re-run the script frequently.
118+
JSON="$(echo "$SEARCH_BY_BODY\n$SEARCH_BY_TITLE" | sed -e 's|\\n| |g' -e 's|\\r||g' -e 's|`||g' | jq --slurp 'add | unique_by(.number)')"
119+
ISSUES_COUNT="$(echo "$JSON" | jq '. | length')"
120+
debug "$PUZZLE_ID: found $ISSUES_COUNT issue(s) overall"
121+
debug "$PUZZLE_ID: result=$JSON"
122+
if [ $ISSUES_COUNT -gt 1 ]; then
123+
# LATER: include in the output type of match -- in:title or in:body
124+
error ''
125+
error "$PUZZLE_ID: found $ISSUES_COUNT issues that match the criterias:"
126+
CANDIDATES="$(echo "$JSON" | jq --raw-output '.[] | [ .number, .state, .url, .title ] | @tsv')"
127+
echo >&2 "$CANDIDATES"
128+
error "Ways to resolve:"
129+
error " 1) modify a body of one of the tickets to not contain puzzle id (or to have a different title)"
130+
error " 2) manually create a mapping between this puzzle and one of the issues:"
131+
echo "$CANDIDATES" | while read ISSUE_ID ISSUE_STATE REST; do
132+
error " echo '$PUZZLE_ID\t$ISSUE_ID\t$ISSUE_STATE\tmanually' >>$MAPPING_FILE"
133+
done
134+
fatal ''
135+
fi
136+
137+
if [ $ISSUES_COUNT -le 0 ]; then
138+
info "$PUZZLE_ID: no related issues found. Need to create a new issue: $TITLE"
139+
NEW_ISSUES_COUNT=$[NEW_ISSUES_COUNT + 1]
140+
continue
141+
fi
142+
143+
if [ $ISSUES_COUNT -eq 1 ]; then
144+
ISSUE_ID="$(echo "$JSON" | jq '.[0] | .number')"
145+
ISSUE_URL="$(echo "$JSON" | jq --raw-output '.[0] | .url')"
146+
147+
ISSUE_TITLE="$(echo "$JSON" | jq --raw-output '.[0] | .title')"
148+
if [ "$TITLE" != "$ISSUE_TITLE" ]; then
149+
warn ''
150+
warn "$PUZZLE_ID => #$ISSUE_ID: $ISSUE_URL looks identical but titles don't match!"
151+
warn "Perhaps, the issue's title was modified after issue creation"
152+
warn "Expected: $TITLE"
153+
warn "Found: $ISSUE_TITLE"
154+
warn ''
155+
else
156+
debug "$PUZZLE_ID => #$ISSUE_ID: titles match"
157+
fi
158+
159+
ISSUE_STATE="$(echo "$JSON" | jq --raw-output '.[0] | .state')"
160+
if [ "$ISSUE_STATE" = 'closed' ]; then
161+
error ''
162+
error "$PUZZLE_ID => #$ISSUE_ID: $ISSUE_URL is closed!"
163+
error "Either the issue isn't related to a puzzle or the issues was closed manually but a puzzle left in code"
164+
error "Ways to resolve:"
165+
error " 1) remove the puzzle with id $PUZZLE_ID from code"
166+
error " 2) investigate and manually resolve this collision"
167+
fatal ''
168+
169+
elif [ "$ISSUE_STATE" != 'open' ]; then
170+
error ''
171+
error "$PUZZLE_ID => #$ISSUE_ID: $ISSUE_URL has unknown state"
172+
error "Expected: 'open' or 'closed'"
173+
error "Found: $ISSUE_STATE"
174+
fatal ''
175+
fi
176+
177+
ISSUE_BODY="$(echo "$JSON" | jq --raw-output '.[0] | .body')"
178+
if ! echo "$ISSUE_BODY" | grep -q "$PUZZLE_ID"; then
179+
error ''
180+
error "$PUZZLE_ID => #$ISSUE_ID: issue looks identical but its body doesn't contain the puzzle id ($PUZZLE_ID)!"
181+
error "Perhaps, the puzzle id got changed after issue creation"
182+
error "Body: $ISSUE_BODY"
183+
error "Ways to resolve:"
184+
error " 1) edit $ISSUE_URL and modify its body to contain $PUZZLE_ID"
185+
error " 2) manually create a mapping between this puzzle and the issue:"
186+
error " echo '$PUZZLE_ID\t$ISSUE_ID\t$ISSUE_STATE\tmanually' >>$MAPPING_FILE"
187+
fatal ''
188+
else
189+
debug "$PUZZLE_ID => #$ISSUE_ID: body contains puzzle id"
190+
fi
191+
192+
info "$PUZZLE_ID => #$ISSUE_ID: link with $ISSUE_ID ($ISSUE_STATE)"
193+
printf '%s\t%s\t%s\tautomatically\n' "$PUZZLE_ID" "$ISSUE_ID" "$ISSUE_STATE" >> "$MAPPING_FILE"
194+
fi
195+
done <<< "$(grep -v '^Id' "$1")"
196+
197+
info ''
198+
info 'DONE'
199+
info "Puzzles : $PUZZLES_COUNT"
200+
info "New issues: $NEW_ISSUES_COUNT"

0 commit comments

Comments
 (0)