forked from heavy-duty/ceremony
80 lines
4 KiB
Bash
80 lines
4 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# lib/closes_references.sh — "which issues does this PR body close?", parsed
|
||
|
|
# here rather than asked of a forge (issue #188, term 3).
|
||
|
|
#
|
||
|
|
# Sourced, never executed: no set -e/-u — the sourcing script owns its shell
|
||
|
|
# options, as lib/version.sh and lib/forge.sh do.
|
||
|
|
#
|
||
|
|
# WHY THIS EXISTS. issueflow-reconcile asked GitHub's GraphQL API for
|
||
|
|
# `closingIssuesReferences` — GitHub's own parse of the closing keywords in
|
||
|
|
# a PR body. **Forgejo has no GraphQL API at all**, and the runner confirms
|
||
|
|
# it from the other side: a real forgejo-runner job arrives with
|
||
|
|
# GITHUB_GRAPHQL_URL set to the empty string (probe task 278, 2026-08-02).
|
||
|
|
# So that call site could not be translated to a Forgejo endpoint — there is
|
||
|
|
# nothing to translate it to. It had to be replaced by a parse this repo
|
||
|
|
# owns, over a field both forges already return:
|
||
|
|
# `GET /repos/{owner}/{repo}/pulls` carries `number` and `body` on
|
||
|
|
# /api/v3 and /api/v1 alike (measured on both).
|
||
|
|
#
|
||
|
|
# That the replacement is honest is the point. The sibling half of the same
|
||
|
|
# GraphQL query, MERGED_REF_PR_RECORDS, was ALREADY a body parse — it pulled
|
||
|
|
# `number` and `body` and ran them through refs_references. GraphQL was
|
||
|
|
# buying pagination convenience there, nothing semantic. This file makes the
|
||
|
|
# other half symmetric: one parser this repo controls and can test, for both
|
||
|
|
# link kinds, on both forges.
|
||
|
|
#
|
||
|
|
# THE ACCEPTED DELTA, stated so it is not rediscovered as a bug: GitHub also
|
||
|
|
# records closing links attached through the pull request's development
|
||
|
|
# sidebar, which live in no body and which no body parse can see. This
|
||
|
|
# family declares its links in the body — that is what BUILDER.md's PR
|
||
|
|
# template asks for — so the delta is zero in practice here. A consumer that
|
||
|
|
# links through the sidebar would see those issues go unclosed by the sweep;
|
||
|
|
# they would need to say so in the body instead.
|
||
|
|
#
|
||
|
|
# DEPENDENCY: issue_references, from issueflow-reconcile.sh — the LOCAL /
|
||
|
|
# CROSS classifier that keeps rig#112 from ever being read as local #112
|
||
|
|
# (#61). Bash resolves function calls at call time, so the order of sourcing
|
||
|
|
# does not matter; both must simply be defined before closes_references runs.
|
||
|
|
# refs_references depends on it exactly the same way.
|
||
|
|
|
||
|
|
# closes_references — PR body on stdin -> local issue numbers this body
|
||
|
|
# declares it CLOSES, sorted, unique.
|
||
|
|
#
|
||
|
|
# The keyword set is GitHub's documented one, all three verbs in all three
|
||
|
|
# tenses. Matching is case-insensitive because bodies are written by humans
|
||
|
|
# and agents both ("Closes", "closes", "CLOSES").
|
||
|
|
#
|
||
|
|
# Deliberately NOT matched: "Refs #N". That is the other relation entirely —
|
||
|
|
# refs_references owns it, and conflating them would make every referenced
|
||
|
|
# issue look closeable, which is the post-merge transition #151 had to be
|
||
|
|
# reopened by hand over.
|
||
|
|
closes_references() {
|
||
|
|
awk '
|
||
|
|
{
|
||
|
|
line = $0
|
||
|
|
lower = tolower(line)
|
||
|
|
# Every occurrence contributes, not just the first: a body that says
|
||
|
|
# "Closes #1. Closes #2." declares two, and binding to the first
|
||
|
|
# occurrence dropped the later ones — the same defect #184 fixed in
|
||
|
|
# blocked_reference_records, kept fixed here by construction.
|
||
|
|
while (match(lower, /(^|[^[:alnum:]_-])(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]:]+/)) {
|
||
|
|
# BOTH cursors advance together. Advancing only `lower` left the
|
||
|
|
# next match offset indexing the ORIGINAL line, so the second
|
||
|
|
# declaration on a line came back as garbage — caught by the
|
||
|
|
# "two closes on one line" case, which is why it is a case.
|
||
|
|
rest = substr(line, RSTART + RLENGTH)
|
||
|
|
line = rest
|
||
|
|
lower = tolower(rest)
|
||
|
|
if (rest ~ /^(#|([[:alnum:]_.-]+\/)?[[:alnum:]_.-]+#)[0-9]+/) {
|
||
|
|
token = rest
|
||
|
|
# Stop at the first thing that cannot be part of a reference, so
|
||
|
|
# "Closes #12, and more prose" yields #12 and not the sentence.
|
||
|
|
sub(/[^[:alnum:]_.\/#-].*/, "", token)
|
||
|
|
print token
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
' | issue_references \
|
||
|
|
| awk -F '\t' '$1 == "LOCAL" { print $2 }' | sort -nu
|
||
|
|
}
|