Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions .github/workflows/stale-issues.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Close stale issues

on:
workflow_call:

jobs:
stale:
name: Close stale issues
runs-on: ubuntu-latest
steps:
- name: Get GitHub app token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app_token
with:
app-id: ${{ secrets.BUFBUILD_EXPORT_APP_ID }}
private-key: ${{ secrets.BUFBUILD_EXPORT_APP_KEY }}
- name: Process stale issues
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
WARNING_BODY='<!-- stale-warning -->
This issue has been open for 30 days with no activity. It will be automatically closed in 7 days if there is no further activity. If this issue is still relevant, please leave a comment.'
CLOSE_BODY='<!-- stale-close -->
This issue has been automatically closed due to inactivity. If you believe this issue is still relevant, feel free to reopen it or open a new issue referencing this one.'

# Collect all Triage issues for this repo from the project.
ISSUES_FILE=$(mktemp)
CURSOR=""
HAS_NEXT="true"
while [[ "${HAS_NEXT}" == "true" ]]; do
if [[ -n "${CURSOR}" ]]; then
AFTER_CLAUSE=', after: "'"${CURSOR}"'"'
else
AFTER_CLAUSE=""
fi
RESULT=$(gh api graphql -f query='
query {
organization(login: "'"${REPO_OWNER}"'") {
projectV2(number: 22) {
items(first: 100'"${AFTER_CLAUSE}"') {
pageInfo {
hasNextPage
endCursor
}
nodes {
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
optionId
}
}
content {
... on Issue {
number
updatedAt
state
repository {
name
owner { login }
}
comments(last: 1) {
nodes {
body
createdAt
}
}
}
}
}
}
}
}
}')
HAS_NEXT=$(echo "${RESULT}" | jq -r \
'.data.organization.projectV2.items.pageInfo.hasNextPage')
CURSOR=$(echo "${RESULT}" | jq -r \
'.data.organization.projectV2.items.pageInfo.endCursor')
# Filter to open Triage issues in this repo.
echo "${RESULT}" | jq -c '
.data.organization.projectV2.items.nodes[]
| select(
.fieldValueByName.optionId == "c3382ec9"
and .content.number != null
and .content.state == "OPEN"
and .content.repository.name == "'"${REPO_NAME}"'"
and .content.repository.owner.login == "'"${REPO_OWNER}"'"
)
| {
number: .content.number,
updatedAt: .content.updatedAt,
lastCommentIsWarning: (
(.content.comments.nodes[0].body // "")
| contains("<!-- stale-warning -->")
),
lastCommentAt: (
.content.comments.nodes[0].createdAt // ""
)
}' >> "${ISSUES_FILE}"
done

if [[ ! -s "${ISSUES_FILE}" ]]; then
echo "No Triage issues found for ${REPO_OWNER}/${REPO_NAME}"
rm -f "${ISSUES_FILE}"
exit 0
fi

NOW_EPOCH=$(date +%s)
while IFS= read -r item; do
number=$(echo "${item}" | jq -r '.number')
updated_at=$(echo "${item}" | jq -r '.updatedAt')
is_warning=$(echo "${item}" | jq -r '.lastCommentIsWarning')
comment_at=$(echo "${item}" | jq -r '.lastCommentAt')

if [[ "${is_warning}" == "true" && -n "${comment_at}" ]]; then
# Last comment is a stale warning. Close if the grace
# period has elapsed and no other activity occurred.
warning_epoch=$(date -d "${comment_at}" +%s)
days_since=$(( (NOW_EPOCH - warning_epoch) / 86400 ))
if [[ ${days_since} -ge 7 ]]; then
echo "Closing #${number} (warning ${days_since} days ago)"
gh api \
"repos/${REPO_OWNER}/${REPO_NAME}/issues/${number}/comments" \
-f body="${CLOSE_BODY}" \
|| echo "Failed to comment on #${number}"
gh api \
"repos/${REPO_OWNER}/${REPO_NAME}/issues/${number}" \
-X PATCH -f state=closed -f state_reason=not_planned \
|| echo "Failed to close #${number}"
else
echo "Issue #${number}: grace period (${days_since}/7 days)"
fi
else
# No stale warning yet. Warn if inactive for 30+ days.
updated_epoch=$(date -d "${updated_at}" +%s)
days_inactive=$(( (NOW_EPOCH - updated_epoch) / 86400 ))
if [[ ${days_inactive} -ge 30 ]]; then
echo "Warning #${number} (inactive ${days_inactive} days)"
gh api \
"repos/${REPO_OWNER}/${REPO_NAME}/issues/${number}/comments" \
-f body="${WARNING_BODY}" \
|| echo "Failed to warn on #${number}"
else
echo "Issue #${number}: active (${days_inactive} days)"
fi
fi
done < "${ISSUES_FILE}"
rm -f "${ISSUES_FILE}"
100 changes: 100 additions & 0 deletions .github/workflows/update-project-last-updated.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Update project last updated

on:
workflow_call:

jobs:
update-last-updated:
name: Update last updated
runs-on: ubuntu-latest
steps:
- name: Get GitHub app token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app_token
with:
app-id: ${{ secrets.BUFBUILD_EXPORT_APP_ID }}
private-key: ${{ secrets.BUFBUILD_EXPORT_APP_KEY }}
- name: Determine content number and type
id: content
env:
EVENT_NAME: ${{ github.event_name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
run: |
NUMBER=""
CONTENT_TYPE=""
case "${EVENT_NAME}" in
pull_request|pull_request_review)
NUMBER="${PR_NUMBER}"
CONTENT_TYPE="pullRequest"
;;
issue_comment)
NUMBER="${ISSUE_NUMBER}"
if [[ -n "${IS_PR_COMMENT}" ]]; then
CONTENT_TYPE="pullRequest"
else
CONTENT_TYPE="issue"
fi
;;
issues)
NUMBER="${ISSUE_NUMBER}"
CONTENT_TYPE="issue"
;;
esac
if [[ -z "${NUMBER}" ]]; then
echo "Unsupported event: ${EVENT_NAME}, skipping"
exit 0
fi
echo "number=${NUMBER}" >> "${GITHUB_OUTPUT}"
echo "type=${CONTENT_TYPE}" >> "${GITHUB_OUTPUT}"
- name: Update last updated date
if: steps.content.outputs.number != ''
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
CONTENT_TYPE: ${{ steps.content.outputs.type }}
CONTENT_NUMBER: ${{ steps.content.outputs.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
ITEM_ID=$(gh api graphql -f query='
query {
repository(
owner: "'"${REPO_OWNER}"'",
name: "'"${REPO_NAME}"'"
) {
'"${CONTENT_TYPE}"'(number: '"${CONTENT_NUMBER}"') {
projectItems(first: 100) {
nodes {
id
project { number }
}
}
}
}
}' | jq -r '
[
.data.repository.'"${CONTENT_TYPE}"'
.projectItems.nodes[]
| select(.project.number == 22)
| .id
] | .[0] // empty')
if [[ -z "${ITEM_ID}" ]]; then
echo "Item not found on project, skipping"
exit 0
fi
TODAY=$(date -u +%Y-%m-%d)
gh api graphql -f query='
mutation {
updateProjectV2ItemFieldValue(
input: {
projectId: "PVT_kwDOAzrm6s4AcQQd"
itemId: "'"${ITEM_ID}"'"
fieldId: "PVTF_lADOAzrm6s4AcQQdzg-YTKg"
value: { date: "'"${TODAY}"'" }
}
) {
clientMutationId
}
}'
echo "Updated Last Updated to ${TODAY} for item ${ITEM_ID}"
Loading