diff --git a/.github/scripts/CommitHeaderChecker.java b/.github/scripts/CommitHeaderChecker.java new file mode 100644 index 000000000000..51d2fe3b602d --- /dev/null +++ b/.github/scripts/CommitHeaderChecker.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import static java.util.stream.Gatherers.fold; +import static java.util.stream.Gatherers.scan; +import static java.util.stream.Gatherers.windowSliding; + +record Commit(int index, String from, String date, String subject, String blank) {} +record Result(int total, boolean green) {} + +// checks commit headers for valid author, email and commit msg formatting +// Java 23+, may require preview flag + +// java CommitHeaderChecker.java https://github.com/apache/netbeans/pull/${{ github.event.pull_request.number }} +void main(String[] args) throws IOException, InterruptedException { + + if (args.length != 1 || !args[0].startsWith("https://github.com/")) { + throw new IllegalArgumentException("PR URL expected"); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(args[0]+".patch")) + .timeout(Duration.ofSeconds(10)) + .build(); + + println("checking PR patch file..."); + Result result; + try (HttpClient client = HttpClient.newBuilder() + .followRedirects(Redirect.NORMAL).build()) { + + result = client.send(request, BodyHandlers.ofLines()).body() + // 4 line window, From/Date/Subject and extra line for blank line / overflow check + .gather(windowSliding(4)) + .filter(w -> w.size() == 4 && w.get(0).startsWith("From: ") && w.get(1).startsWith("Date: ") && w.get(2).startsWith("Subject: ")) + .gather(scan( + () -> new Commit(0, "", "", "", ""), + (c, w) -> new Commit(c.index+1, w.get(0), w.get(1), w.get(2), w.get(3)))) + .gather(fold( + () -> new Result(0, true), + (r, c) -> new Result(r.total+1, r.green & checkCommit(c)))) + .findFirst() + .orElseThrow(); + } + + println(result.total + " commit(s) checked"); + System.exit(result.green ? 0 : 1); +} + +boolean checkCommit(Commit c) { + return checkNameAndEmail(c.index, c.from) + & checkSubject(c.index, c.subject) + & checkBlankLineAfterSubject(c.index, c.blank); +} + +boolean checkNameAndEmail(int i, String from) { + // From: Duke + int start = from.indexOf('<'); + int end = from.indexOf('>'); + + String mail = end > start ? from.substring(start+1, end) : ""; + String author = start > 6 ? from.substring(6, start).strip() : ""; + + boolean green = true; + if (mail.isBlank() || !mail.contains("@") || mail.contains("noreply") || mail.contains("localhost")) { + println("::error::invalid email in commit " + i + " '" + from + "'"); + green = false; + } + // single word author -> probably the nickname/account name/root etc + // mime encoded is probably fine, since gh account names can't be encoded + if (author.isBlank() || (!author.contains(" ") && !(author.startsWith("=?") && author.endsWith("?=")))) { + println("::error::invalid author in commit " + i + " '" + author + "' (full name?)"); + green = false; + } + return green; +} + +// https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-commit.html#_discussion +boolean checkSubject(int i, String subject) { + // Subject: [PATCH] msg + subject = subject.substring(subject.indexOf(']')+1).strip(); + // single word subjects are likely not intended or should be squashed before merge + if (!subject.contains(" ")) { + println("::error::invalid subject in commit " + i + " '" + subject + "'"); + return false; + } + return true; +} + +// there should be a blank line after the subject line, some subjects can overflow though. +boolean checkBlankLineAfterSubject(int i, String blank) { +// disabled since this would produce too many warnings due to overflowing subject lines +// if (!blank.isBlank()) { +// println("::warning::blank line after subject recommended in commit " + i + " (is subject over 50 char limit?)"); +//// return false; +// } + return true; +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2d9863c1e71..55b610252af6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -296,13 +296,6 @@ jobs: echo "::error::PRs must be labeled, see: https://cwiki.apache.org/confluence/display/NETBEANS/PRs+and+You+-+A+reviewer+Guide" exit 1 - - name: Set up JDK ${{ matrix.java }} - if: ${{ !cancelled() }} - uses: actions/setup-java@v4 - with: - java-version: ${{ matrix.java }} - distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }} - - name: Checkout ${{ github.ref }} ( ${{ github.sha }} ) if: ${{ !cancelled() }} uses: actions/checkout@v4 @@ -310,11 +303,24 @@ jobs: persist-credentials: false submodules: false show-progress: false - fetch-depth: 10 - - name: Print last 10 Commits + - name: Set up JDK 23 for scripts if: ${{ github.event_name == 'pull_request' && !cancelled() }} - run: git log --oneline -n10 --pretty=format:'%h %an [%ae] %s' + uses: actions/setup-java@v4 + with: + java-version: 23 + distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }} + + - name: Check Commit Headers + if: ${{ github.event_name == 'pull_request' && !cancelled() }} + run: java --enable-preview .github/scripts/CommitHeaderChecker.java https://github.com/apache/netbeans/pull/${{ github.event.pull_request.number }} + + - name: Set up JDK ${{ matrix.java }} + if: ${{ !cancelled() }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: ${{ env.DEFAULT_JAVA_DISTRIBUTION }} - name: Check line endings and verify RAT report if: ${{ !cancelled() }}