diff --git a/.gitea/workflows/depends_upstream.yaml b/.gitea/workflows/depends_upstream.yaml new file mode 100644 index 00000000..5d0c5ff2 --- /dev/null +++ b/.gitea/workflows/depends_upstream.yaml @@ -0,0 +1,46 @@ +# The status check context of this job is "checks / depends-upstream +# (pull_request)", which the required status check `checks / *` of the protected +# main branch matches, so a pull request waiting for gitea cannot be merged. +# It is a workflow of its own so that a label change does not cancel the build +# and the preview deployment of the `checks` workflow in test.yaml. +name: checks + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + - labeled + - unlabeled + +concurrency: + group: depends-upstream-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + depends-upstream: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + # the check script is taken from the base branch: a pull request must + # not be able to weaken its own gate + ref: ${{ github.event.pull_request.base.ref }} + persist-credentials: false + - name: check the upstream dependencies + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + # optional, only lifts the rate limit of the unauthenticated github api + GITHUB_COM_TOKEN: ${{ secrets.GITHUB_COM_TOKEN }} + run: | + set -euo pipefail + + # read the description and the labels from the api instead of the + # event context, so nothing from a pull request body is expanded into + # this script + api="${GITHUB_API_URL:-$GITHUB_SERVER_URL/api/v1}/repos/$GITHUB_REPOSITORY" + curl --silent --show-error --fail --output pull.json "$api/pulls/$PR_NUMBER" + + ./scripts/check-upstream-deps.sh pull.json diff --git a/.gitea/workflows/unlabel_depends_upstream.yaml b/.gitea/workflows/unlabel_depends_upstream.yaml new file mode 100644 index 00000000..37f10451 --- /dev/null +++ b/.gitea/workflows/unlabel_depends_upstream.yaml @@ -0,0 +1,29 @@ +name: unlabel merged upstream dependencies + +on: + schedule: + - cron: '15 */6 * * *' # every 6 hours, quarter past + workflow_dispatch: + +jobs: + unlabel: + if: github.repository == 'gitea/docs' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - name: drop the label from the pull requests upstream caught up with + env: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + GITHUB_COM_TOKEN: ${{ secrets.GITHUB_COM_TOKEN }} + run: | + set -euo pipefail + + if [ -z "${DEPLOY_TOKEN:-}" ]; then + echo "secrets.DEPLOY_TOKEN is missing, cannot remove the label" + exit 1 + fi + + export GITEA_API="${GITHUB_API_URL:-$GITHUB_SERVER_URL/api/v1}/repos/$GITHUB_REPOSITORY" + ./scripts/unlabel-upstream-deps.sh diff --git a/README.md b/README.md index 9615a5f2..f4913248 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,27 @@ The order of the top level sidebar groups comes from `sidebars.js` (and `versioned_sidebars/` for released versions), the label and order of every other group from the `_category_.json` of its directory. +### Documenting an unreleased feature + +A page that describes a gitea change which is not merged yet must wait for it, +otherwise the site documents something no release has. Say so in the description +of the pull request: + +``` +Depends on: gitea#35851 +``` + +The `depends-upstream` check reads those references (`gitea#` or the full +pull request url, several are allowed) and fails while any of them is unmerged, +which blocks the merge since `main` requires the `checks` contexts. Add the +`depends-on-upstream` label as well, so the pull request is easy to find; the +label alone fails the check, it has to be accompanied by a reference. + +The `unlabel merged upstream dependencies` job runs every 6 hours, removes the +label once every referenced pull request is merged and comments on the pull +request, which reruns the check. `scripts/check-upstream-deps.sh` is the check +itself and can be run locally against the json of a pull request. + ## Cutting a version ```shell diff --git a/scripts/check-upstream-deps.sh b/scripts/check-upstream-deps.sh new file mode 100755 index 00000000..e7627c0c --- /dev/null +++ b/scripts/check-upstream-deps.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# +# Fails when a pull request depends on a gitea change that is not merged yet. +# +# Usage: ./scripts/check-upstream-deps.sh +# +# The json is what the gitea api returns for a pull request; the script reads +# the description and the labels from it. A dependency is declared in the +# description as +# +# Depends on: gitea#35851 +# Depends on: https://github.com/go-gitea/gitea/pull/35851 +# +# and may appear several times. `GITHUB_COM_TOKEN` is optional and only lifts +# the rate limit of the unauthenticated github api, `UPSTREAM_LABEL` overrides +# the label the check looks for. + +set -euo pipefail + +LABEL="${UPSTREAM_LABEL:-depends-on-upstream}" +PULL_JSON="${1:-}" + +if [ -z "$PULL_JSON" ] || [ ! -f "$PULL_JSON" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +body="$(jq -r '.body // ""' "$PULL_JSON")" +labelled="$(jq -r --arg label "$LABEL" '[.labels[]?.name] | index($label) != null' "$PULL_JSON")" + +# every gitea# and every pull request url, deduplicated +refs="$(printf '%s\n' "$body" \ + | grep -oiE 'gitea#[0-9]+|github\.com/go-gitea/gitea/pull/[0-9]+' \ + | grep -oE '[0-9]+' \ + | sort -un || true)" + +if [ -z "$refs" ]; then + if [ "$labelled" = 'true' ]; then + echo "This pull request carries the \"$LABEL\" label but does not say what it" + echo "depends on. Add a line to the description, for example:" + echo + echo " Depends on: gitea#35851" + exit 1 + fi + echo "no upstream dependency declared" + exit 0 +fi + +github_api() { + local url="$1" + if [ -n "${GITHUB_COM_TOKEN:-}" ]; then + curl --silent --show-error --write-out '\n%{http_code}' \ + -H "Authorization: Bearer $GITHUB_COM_TOKEN" "$url" + else + curl --silent --show-error --write-out '\n%{http_code}' "$url" + fi +} + +status=0 +for ref in $refs; do + response="$(github_api "https://api.github.com/repos/go-gitea/gitea/pulls/$ref")" + code="$(printf '%s' "$response" | tail -n 1)" + payload="$(printf '%s' "$response" | sed '$d')" + + case "$code" in + 200) + if [ "$(printf '%s' "$payload" | jq -r '.merged')" = 'true' ]; then + echo "gitea#$ref is merged" + else + echo "gitea#$ref is not merged yet ($(printf '%s' "$payload" | jq -r '.state'))" + status=1 + fi + ;; + 404) + echo "gitea#$ref is not a pull request of go-gitea/gitea, reference the pull request that adds the feature" + status=1 + ;; + 403 | 429) + echo "the github api refused the request for gitea#$ref (rate limit?), cannot tell whether it is merged" + status=1 + ;; + *) + echo "unexpected response $code from the github api for gitea#$ref" + status=1 + ;; + esac +done + +if [ "$status" -ne 0 ]; then + echo + echo "The documentation of an unreleased feature can only be merged once the" + echo "feature itself is in gitea main. Keep the \"$LABEL\" label on this pull" + echo "request; the scheduled job removes it and reruns this check as soon as" + echo "every referenced pull request is merged." + exit 1 +fi + +if [ "$labelled" = 'true' ]; then + echo + echo "every referenced pull request is merged, the \"$LABEL\" label can be removed" +fi diff --git a/scripts/unlabel-upstream-deps.sh b/scripts/unlabel-upstream-deps.sh new file mode 100755 index 00000000..bc65615f --- /dev/null +++ b/scripts/unlabel-upstream-deps.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# +# Removes the `depends-on-upstream` label from every open pull request whose +# referenced gitea pull requests are all merged, and says so in a comment. +# Removing the label reruns the `depends-upstream` check of that pull request. +# +# Usage: ./scripts/unlabel-upstream-deps.sh +# +# Environment: +# GITEA_API api of the repository, e.g. https://gitea.com/api/v1/repos/gitea/docs +# DEPLOY_TOKEN token used to remove the label and to comment +# GITHUB_COM_TOKEN optional, lifts the rate limit of the github api +# UPSTREAM_LABEL label to look for, `depends-on-upstream` by default +# DRY_RUN set to 1 to only report what would happen + +set -euo pipefail + +LABEL="${UPSTREAM_LABEL:-depends-on-upstream}" +API="${GITEA_API:?GITEA_API is required}" +DRY_RUN="${DRY_RUN:-0}" + +gitea_api() { + local method="$1" url="$2" data="${3:-}" + local args=(--silent --show-error --fail -X "$method") + if [ -n "${DEPLOY_TOKEN:-}" ]; then args+=(-H "Authorization: token $DEPLOY_TOKEN"); fi + if [ -n "$data" ]; then args+=(-H 'Content-Type: application/json' --data "$data"); fi + curl "${args[@]}" "$url" +} + +label_id="$(gitea_api GET "$API/labels?limit=100" | jq -r --arg label "$LABEL" \ + '.[] | select(.name == $label) | .id')" +if [ -z "$label_id" ]; then + echo "the repository has no \"$LABEL\" label, nothing to do" + exit 0 +fi + +pulls="$(gitea_api GET "$API/pulls?state=open&limit=50")" +indexes="$(printf '%s' "$pulls" | jq -r --arg label "$LABEL" \ + '.[] | select([.labels[]?.name] | index($label) != null) | .number')" + +if [ -z "$indexes" ]; then + echo "no open pull request carries the \"$LABEL\" label" + exit 0 +fi + +here="$(cd "$(dirname "$0")" && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +for index in $indexes; do + gitea_api GET "$API/pulls/$index" > "$work/pull.json" + + # the check script exits 0 when every reference is merged; it needs the label + # to be absent from its input to stay quiet about it, so drop it here + jq 'del(.labels)' "$work/pull.json" > "$work/pull-unlabelled.json" + if ! output="$("$here/check-upstream-deps.sh" "$work/pull-unlabelled.json" 2>&1)"; then + echo "#$index still waits for upstream:" + printf '%s\n' "$output" | sed 's/^/ /' + continue + fi + + if printf '%s' "$output" | grep -q 'no upstream dependency declared'; then + echo "#$index carries the label but declares no dependency, leaving it alone" + continue + fi + + if [ "$DRY_RUN" = '1' ]; then + echo "#$index would lose the label:" + printf '%s\n' "$output" | sed 's/^/ /' + continue + fi + + gitea_api DELETE "$API/issues/$index/labels/$label_id" > /dev/null + comment="$(printf 'Every referenced gitea pull request is merged, so the `%s` label was removed and the check was rerun.\n\n%s' "$LABEL" "$output")" + gitea_api POST "$API/issues/$index/comments" "$(jq -n --arg body "$comment" '{body: $body}')" > /dev/null + echo "#$index unlabelled" +done