diff --git a/.github/workflows/check-hostnames.yml b/.github/workflows/check-hostnames.yml new file mode 100644 index 000000000..636357604 --- /dev/null +++ b/.github/workflows/check-hostnames.yml @@ -0,0 +1,101 @@ +name: Check example hostnames + +# Reports placeholder hostnames on lines added by a pull request that are not +# the recommended ones (dev/example-hostnames.json), as a summary comment plus +# an inline suggested change on each flagged line. Advisory: never fails the PR. + +on: + pull_request: + paths: + - 'docs/**' + - 'dev/check-hostnames.mjs' + - 'dev/example-hostnames.json' + - 'dev/sync-review-comments.sh' + - '.github/workflows/check-hostnames.yml' + +# A new push supersedes the run for the previous one +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + check-hostnames: + name: Check example hostnames + runs-on: ubuntu-latest + steps: + - name: Check out pull request head + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Find placeholder hostnames added by this PR + id: check + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + # File links in the report open the file on the PR branch + LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} + # The script exits 1 both with findings and when it crashes. It writes + # its output in one go at the end, so a crash leaves it empty. The + # comment step reports a crash instead of posting the empty report. + run: | + git diff -U0 "$(git merge-base "$BASE_SHA" HEAD)" HEAD -- docs > "$RUNNER_TEMP/changes.diff" + if node dev/check-hostnames.mjs --format markdown \ + --diff "$RUNNER_TEMP/changes.diff" \ + --review "$RUNNER_TEMP/review.json" \ + --link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then + echo "result=clean" >> "$GITHUB_OUTPUT" + elif [ -s "$RUNNER_TEMP/report.md" ]; then + echo "result=found" >> "$GITHUB_OUTPUT" + else + echo "::warning::check-hostnames crashed, so this PR was not checked" + echo "result=crashed" >> "$GITHUB_OUTPUT" + fi + cat "$RUNNER_TEMP/report.md" + + - name: Comment on the pull request + # Fork PRs get a read-only token; the report is still in the job log + if: github.event.pull_request.head.repo.full_name == github.repository + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + RESULT: ${{ steps.check.outputs.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + marker='' + existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1) + + # Comment when there is something to report, or an earlier report to resolve + case "$RESULT" in + found) + { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; + crashed) + printf '%s\n### ⚠️ The example hostnames check could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ + "$marker" "$RUN_URL" > "$RUNNER_TEMP/comment.md" ;; + *) + [ -n "$existing_comment" ] || exit 0 + { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; + esac + + if [ -n "$existing_comment" ]; then + gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \ + --field body=@"$RUNNER_TEMP/comment.md" + else + gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md" + fi + + - name: Suggest fixes as review comments + # One suggested change per flagged line, kept in sync with the + # findings; see dev/sync-review-comments.sh + if: >- + github.event.pull_request.head.repo.full_name == github.repository + && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: dev/sync-review-comments.sh '`, + ...[...replacements].map( + ([placeholder, recommended]) => + `Placeholder: \`${placeholder}\`, use \`${recommended}\`` + ), + // A four-backtick fence so lines containing ``` cannot break out + '````suggestion', + fixed, + '````' + ]; + comments.push({path: file, line, side: 'RIGHT', body: body.join('\n')}); + } + return {event: 'COMMENT', body: '', comments}; +} + +const FORMATTERS = { + text: formatText, + json: findings => JSON.stringify(findings, null, '\t') + '\n', + markdown: formatMarkdown +}; + +function main() { + const format = FORMATTERS[FORMAT]; + if (!format) { + throw new Error( + `Unknown --format "${FORMAT}"; use text, json, or markdown` + ); + } + const {findings, fixedLines} = findPlaceholders(); + if (REVIEW_FILE) { + fs.writeFileSync( + REVIEW_FILE, + JSON.stringify(reviewRequest(findings, fixedLines), null, '\t') + + '\n' + ); + } + process.stdout.write(format(findings)); + process.exit(findings.length === 0 ? 0 : 1); +} + +main(); diff --git a/dev/example-hostnames.json b/dev/example-hostnames.json new file mode 100644 index 000000000..804af8308 --- /dev/null +++ b/dev/example-hostnames.json @@ -0,0 +1,87 @@ +{ + "$comment": "Recommended placeholder hostname β†’ placeholders the docs used instead, as literal text, a glob (`*`), or a regular expression (`/.../`); read by dev/check-hostnames.mjs, see its header for how entries match", + "bitbucket.example.com": [ + "[your-bitbucket-hostname]", + "bitbucketserver.example.com", + "my-bitbucket.example.com", + "your-bbs-instance.example.com" + ], + "example.com": [ + "/(?:my|your|our)?company\\.(?:com|io|net|org)/", + "", + "acme.com", + "internal.corp", + "sgdev.org", + "your*domain.com", + "your_instance_name.com" + ], + "example.okta.com": [ + ".okta.com" + ], + "example.onelogin.com": [ + "mycompany.onelogin.com" + ], + "example.openai.azure.com": [ + "acme-test.openai.azure.com" + ], + "example.sourcegraphcloud.com": [ + "acme.sourcegraphcloud.com" + ], + "gerrit.example.com": [ + "example.gerrit.com" + ], + "github.example.com": [ + "", + "", + "github-enterprise.example.com" + ], + "gitlab.example.com": [ + "$GITLAB_HOSTNAME", + "my-gitlab.example.com" + ], + "https://sourcegraph.example.com": [ + "$SOURCEGRAPH_BASE_URL", + "$SOURCEGRAPH_ORIGIN", + "$your_sourcegraph_url", + "${YOUR_URL}", + "", + "", + "", + "", + "", + "", + "YOUR_SOURCEGRAPH_URL" + ], + "jaeger.example.com": [ + "your.jaeger.endpoint" + ], + "oauth.example.com": [ + "oauth.your-app.com" + ], + "phabricator.example.com": [ + "my-phabricator.example.com" + ], + "registry.example.com": [ + "myregistry.*.com", + "your.private.registry.com" + ], + "registry2.example.com": [ + "myregistry2.example.com" + ], + "smtp.example.com": [ + "smtp-server.example.com" + ], + "sourcegraph.example.com": [ + "$HOSTNAME_OR_IP", + "/(?:example|test|my\\w*|your\\w*)[-.]sourcegraph\\.com/", + "/(?:your|my|our)[-_]?sourcegraph[-\\w]*\\.(?:com|io|net|org|dev)/", + "/sourcegraph\\.test(?::\\d+)?/", + "", + "", + "YOUR-SOURCEGRAPH-INSTANCE", + "[hostname]", + "cse-k8s.sgdev.org", + "domain.example.com", + "src.acme.com" + ] +} diff --git a/dev/vercel-ignore-build.sh b/dev/vercel-ignore-build.sh index 6eaaef00e..ad4d791b9 100755 --- a/dev/vercel-ignore-build.sh +++ b/dev/vercel-ignore-build.sh @@ -12,7 +12,6 @@ exec git diff --quiet HEAD^ HEAD -- . \ ':(exclude)README.md' \ ':(exclude).gitignore' \ ':(exclude)cspell*' \ - ':(exclude)dev/TODO.md' \ ':(exclude)dev/check-spelling.mjs' \ ':(exclude)dev/post-spelling-review.mjs' \ ':(exclude)dev/report-vercel-build.mjs' \