Skip to content
Open
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
56 changes: 56 additions & 0 deletions .asf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,62 @@ github:
# We prefer linear history, so creating merge commits is disabled in UI
merge: false
rebase: true
rulesets:
- name: PR checks
target: branch
enforcement: active
bypass_actors: []
conditions:
ref_name:
include: [refs/heads/main]
exclude: []
rules:
- type: required_status_checks
parameters:
strict_required_status_checks_policy: false
# Keep contexts in sync with the check names reported on PRs.
# Jenkins pr-head is deliberately not required.
required_status_checks:
- context: Check PR title
integration_id: 15368 # GitHub Actions
- context: Check single commit
integration_id: 15368 # GitHub Actions
- context: Validation
integration_id: 15368 # GitHub Actions
- context: Windows (JDK 8)
integration_id: 15368 # GitHub Actions
- context: Windows (JDK 17)
integration_id: 15368 # GitHub Actions
- context: Windows (JDK 21)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 8, oldest Guava, America/New_York Timezone)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 8, latest Guava, America/New_York Timezone)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 11, Pacific/Chatham Timezone)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 17)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 21)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 24)
integration_id: 15368 # GitHub Actions
- context: Linux (JDK 11, Avatica main)
integration_id: 15368 # GitHub Actions
- context: macOS (JDK 21)
integration_id: 15368 # GitHub Actions
- context: ErrorProne (JDK 11, latest Guava)
integration_id: 15368 # GitHub Actions
- context: CheckerFramework (JDK 11)
integration_id: 15368 # GitHub Actions
- context: CheckerFramework (JDK 11, oldest Guava)
integration_id: 15368 # GitHub Actions
- context: Druid Tests
integration_id: 15368 # GitHub Actions
- context: SonarCloud
integration_id: 57789 # GitHub Code Scanning
- context: SonarCloud Code Analysis
integration_id: 12526 # SonarQube Cloud
notifications:
commits: commits@calcite.apache.org
issues: issues@calcite.apache.org
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/pr-single-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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.

name: PR Single Commit

on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review]

permissions:
pull-requests: read

concurrency:
group: pr-single-commit-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check-single-commit:
name: Check single commit
if: contains(github.event.pull_request.labels.*.name, 'LGTM-will-merge-soon')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify approval and then count commits
uses: actions/github-script@v8
with:
retries: 3
script: |
const guide = 'https://calcite.apache.org/develop/#contributing';
const fail = message => core.setFailed(`${message} See ${guide}.`);
try {
const pullRequest = {
...context.repo,
pull_number: context.issue.number,
};
const { data: pr } = await github.rest.pulls.get(pullRequest);
if (pr.state !== 'open') {
core.info('PR is closed; nothing to check.');
return;
}
if (pr.head.sha !== context.payload.pull_request.head.sha) {
fail('PR commits changed. Use the check on the latest PR head.');
return;
}
if (!pr.labels.some(label => label.name === 'LGTM-will-merge-soon')) {
core.info('The LGTM-will-merge-soon label was removed; commit count not checked.');
return;
}

// On synchronize, the sender is the contributor, not the label author.
// Look up who applied the current label instead of trusting the push actor.
const events = await github.paginate(github.rest.issues.listEvents, {
...context.repo,
issue_number: pr.number,
per_page: 100,
});
const approval = events
.filter(event => event.event === 'labeled' && event.label?.name === 'LGTM-will-merge-soon')
.sort((a, b) => b.id - a.id)[0];
if (!approval?.actor) {
fail('Cannot verify who applied LGTM-will-merge-soon. A maintainer must remove and reapply it.');
return;
}
// Includes write access inherited from the Calcite committer/PMC teams.
// MEMBER or COLLABORATOR alone is not proof of repository write permission.
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: approval.actor.login,
});
if (!['write', 'maintain', 'admin'].includes(access.permission)) {
fail(`The LGTM-will-merge-soon label was added by ${approval.actor.login}, who lacks write access. ` +
'A committer or PMC member with repository write access must remove and reapply it.');
return;
}

const { data: latest } = await github.rest.pulls.get(pullRequest);
if (latest.head.sha !== pr.head.sha || latest.base.sha !== pr.base.sha ||
!latest.labels.some(label => label.name === 'LGTM-will-merge-soon')) {
fail('PR commits, base, or approval changed during validation. Rerun this check.');
return;
}
if (latest.commits !== 1) {
fail(`PR must contain exactly one commit after approval; found ${latest.commits}. ` +
'Squash the commits and push again.');
return;
}
core.info(`Approval verified for ${approval.actor.login}; PR contains exactly one commit.`);
} catch (error) {
fail(`Cannot verify approval and commit count: ${error.message}. Rerun this check.`);
}
149 changes: 149 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# 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.

name: PR Title

on:
pull_request:
types: [opened, edited, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: read

concurrency:
group: pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check-pr-title:
name: Check PR title
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Use the PR workflow so contributors can exercise this check before it is merged.
# Read metadata only; do not check out or execute PR code.
- name: Compare PR title, Jira summary, and commit subjects
uses: actions/github-script@v8
with:
retries: 3
script: |
const guide = 'https://calcite.apache.org/develop/#contributing';
const fail = message => core.setFailed(`${message} See ${guide}.`);
try {
const pullRequest = {
...context.repo,
pull_number: context.issue.number,
};
const { data: pr } = await github.rest.pulls.get(pullRequest);
if (pr.state !== 'open') {
core.info('PR is closed; nothing to check.');
return;
}
if (pr.head.sha !== context.payload.pull_request.head.sha) {
fail('PR commits changed. Use the check on the latest PR head.');
return;
}
const match = /^\[(CALCITE-[0-9]+)\]/.exec(pr.title);
if (!match) {
core.info('Title consistency check skipped: no [CALCITE-<digits>] prefix.');
return;
}

const key = match[1];
let summary;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
// Jira is public; never send the GitHub token to it.
const response = await fetch(
`https://issues.apache.org/jira/rest/api/2/issue/${key}?fields=summary`,
{
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(15000),
}
);
if (response.status === 404) {
fail(`Jira issue ${key} does not exist or is not public.`);
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const issue = await response.json();
summary = issue.fields?.summary;
if (typeof summary !== 'string' || !summary.trim()) {
throw new Error('Jira returned an empty summary');
}
break;
} catch (error) {
if (attempt === 3) {
fail(`Cannot read Jira issue ${key}: ${error.message}. Rerun this check.`);
return;
}
core.warning(`Jira request failed; retrying (${attempt}/3).`);
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
}
}

const expected = `[${key}] ${summary}`;
if (pr.title !== expected) {
fail(`PR title differs from Jira. Expected ${JSON.stringify(expected)}; ` +
`found ${JSON.stringify(pr.title)}.`);
return;
}

// GraphQL pagination also covers PRs beyond the REST API's 250-commit limit.
let cursor = null;
let matchingCommit = false;
do {
const { repository } = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
headRefOid
commits(first: 100, after: $cursor) {
nodes { commit { message } }
pageInfo { hasNextPage endCursor }
}
}
}
}`, { ...context.repo, number: pr.number, cursor });
const current = repository.pullRequest;
if (current.headRefOid !== pr.head.sha) {
fail('PR commits changed during validation. Use the latest PR check.');
return;
}
matchingCommit = current.commits.nodes.some(
({ commit }) => commit.message.split(/\r?\n/, 1)[0] === pr.title
);
cursor = current.commits.pageInfo.hasNextPage
? current.commits.pageInfo.endCursor : null;
} while (!matchingCommit && cursor);

const { data: latest } = await github.rest.pulls.get(pullRequest);
if (latest.head.sha !== pr.head.sha || latest.title !== pr.title ||
latest.base.sha !== pr.base.sha) {
fail('PR title, base, or commits changed during validation. Rerun this check.');
return;
}
if (!matchingCommit) {
fail(`No commit message has the required first line ${JSON.stringify(pr.title)}. ` +
'Update at least one commit subject; the message body may differ.');
return;
}
core.info('PR title, Jira summary, and at least one commit subject match exactly.');
} catch (error) {
fail(`Cannot validate title consistency: ${error.message}. Rerun this check.`);
}
Loading