Skip to content
Merged
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
11 changes: 0 additions & 11 deletions .github/CODEOWNERS

This file was deleted.

48 changes: 43 additions & 5 deletions .github/OWNERS
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Maintainers (can approve any PR)
* @andrewnester @anton-107 @denik @pietern @shreyas-goenka @simonfaltum
* @andrewnester @anton-107 @denik @pietern @shreyas-goenka @simonfaltum @renaudhartert-db

# Bundles
/bundle/ @andrewnester @anton-107 @denik @pietern @shreyas-goenka @lennartkats-db
/cmd/bundle/ @andrewnester @anton-107 @denik @pietern @shreyas-goenka @lennartkats-db
/acceptance/bundle/ @andrewnester @anton-107 @denik @pietern @shreyas-goenka @lennartkats-db
/libs/template/ @andrewnester @anton-107 @denik @pietern @shreyas-goenka @simonfaltum @lennartkats-db
/bundle/ team:bundle @lennartkats-db
/cmd/bundle/ team:bundle @lennartkats-db
/acceptance/bundle/ team:bundle @lennartkats-db
/libs/template/ team:bundle @lennartkats-db

# Pipelines
/cmd/pipelines/ @jefferycheng1 @kanterov @lennartkats-db
Expand All @@ -21,5 +21,43 @@
/libs/apps/ @databricks/eng-apps-devex
/acceptance/apps/ @databricks/eng-apps-devex

# Auth
/cmd/auth/ team:platform
/libs/auth/ team:platform
/acceptance/auth/ team:platform

# Filesystem & sync
/cmd/fs/ team:platform
/cmd/sync/ team:platform
/libs/filer/ team:platform
/libs/sync/ team:platform

# Core CLI infrastructure
/cmd/root/ team:platform
/cmd/version/ team:platform
/cmd/completion/ team:platform
/cmd/configure/ team:platform
/cmd/cache/ team:platform
/cmd/api/ team:platform
/cmd/selftest/ team:platform
/cmd/psql/ team:platform
/libs/psql/ team:platform

# Libs (general)
/libs/databrickscfg/ team:platform
/libs/env/ team:platform
/libs/flags/ team:platform
/libs/cmdio/ team:platform
/libs/log/ team:platform
/libs/telemetry/ team:platform
/libs/process/ team:platform
/libs/git/ team:platform

# Integration tests
/integration/ team:platform

# Internal
/internal/ team:platform

# Experimental
/experimental/aitools/ @databricks/eng-apps-devex @lennartkats-db
6 changes: 6 additions & 0 deletions .github/OWNERTEAMS
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Team aliases for OWNERS file.
# Use "team:<name>" in OWNERS to reference a team defined here.
# Format: team:<name> @member1 @member2 ...

team:bundle @andrewnester @anton-107 @denik @janniklasrose @pietern @shreyas-goenka
team:platform @simonfaltum @renaudhartert-db @hectorcast-db @parthban-db @tanmay-db @Divyansh-db @tejaskochar-db @mihaimitrea-db @chrisst @rauchy
70 changes: 59 additions & 11 deletions .github/scripts/owners.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,56 @@
const fs = require("fs");
const path = require("path");

/**
* Read a file and return non-empty, non-comment lines split by whitespace.
* Returns [] if the file does not exist.
*
* @param {string} filePath
* @returns {string[][]} array of whitespace-split tokens per line
*/
function readDataLines(filePath) {
let content;
try {
content = fs.readFileSync(filePath, "utf-8");
} catch (e) {
if (e.code === "ENOENT") return [];
throw e;
}
const result = [];
for (const raw of content.split("\n")) {
const line = raw.trim();
if (!line || line.startsWith("#")) continue;
const parts = line.split(/\s+/);
if (parts.length >= 2) result.push(parts);
}
return result;
}

/**
* Parse an OWNERTEAMS file into a map of team aliases.
* Format: "team:<name> @member1 @member2 ..."
* Returns Map<string, string[]> where key is "team:<name>" and value is member logins.
*
* @param {string} filePath - absolute path to the OWNERTEAMS file
* @returns {Map<string, string[]>}
*/
function parseOwnerTeams(filePath) {
const teams = new Map();
for (const parts of readDataLines(filePath)) {
if (!parts[0].startsWith("team:")) continue;
const members = parts.slice(1).filter((p) => p.startsWith("@")).map((p) => p.slice(1));
teams.set(parts[0], members);
}
return teams;
}

/**
* Parse an OWNERS file (same format as CODEOWNERS).
* Returns array of { pattern, owners } rules.
*
* If an OWNERTEAMS file exists alongside the OWNERS file, "team:<name>"
* tokens are expanded to their member lists.
*
* By default, team refs (org/team) are filtered out and @ is stripped.
* Pass { includeTeams: true } to keep team refs (with @ stripped).
*
Expand All @@ -13,18 +60,19 @@ const fs = require("fs");
*/
function parseOwnersFile(filePath, opts) {
const includeTeams = opts && opts.includeTeams;
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
const teamsPath = path.join(path.dirname(filePath), "OWNERTEAMS");
const teams = parseOwnerTeams(teamsPath);
const rules = [];
for (const raw of lines) {
const line = raw.trim();
if (!line || line.startsWith("#")) continue;
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
for (const parts of readDataLines(filePath)) {
const pattern = parts[0];
const owners = parts
.slice(1)
.filter((p) => p.startsWith("@") && (includeTeams || !p.includes("/")))
.map((p) => p.slice(1));
const owners = [];
for (const p of parts.slice(1)) {
if (p.startsWith("team:") && teams.has(p)) {
owners.push(...teams.get(p));
} else if (p.startsWith("@") && (includeTeams || !p.includes("/"))) {
owners.push(p.slice(1));
}
}
rules.push({ pattern, owners });
}
return rules;
Expand Down Expand Up @@ -89,4 +137,4 @@ function getOwnershipGroups(filenames, rules) {
return groups;
}

module.exports = { parseOwnersFile, ownersMatch, findOwners, getMaintainers, getOwnershipGroups };
module.exports = { parseOwnerTeams, parseOwnersFile, ownersMatch, findOwners, getMaintainers, getOwnershipGroups };
94 changes: 94 additions & 0 deletions .github/scripts/owners.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const os = require("os");
const path = require("path");

const {
parseOwnerTeams,
ownersMatch,
parseOwnersFile,
findOwners,
Expand Down Expand Up @@ -125,6 +126,99 @@ describe("parseOwnersFile", () => {
});
});

// --- parseOwnerTeams ---

describe("parseOwnerTeams", () => {
let tmpDir;

before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ownerteams-test-"));
});

after(() => {
fs.rmSync(tmpDir, { recursive: true });
});

it("parses team definitions", () => {
const teamsPath = path.join(tmpDir, "OWNERTEAMS");
fs.writeFileSync(teamsPath, "team:platform @alice @bob @carol\n");
const teams = parseOwnerTeams(teamsPath);
assert.equal(teams.size, 1);
assert.deepEqual(teams.get("team:platform"), ["alice", "bob", "carol"]);
});

it("parses multiple teams", () => {
const teamsPath = path.join(tmpDir, "OWNERTEAMS");
fs.writeFileSync(teamsPath, "team:platform @alice @bob\nteam:bundle @carol @dave\n");
const teams = parseOwnerTeams(teamsPath);
assert.equal(teams.size, 2);
assert.deepEqual(teams.get("team:platform"), ["alice", "bob"]);
assert.deepEqual(teams.get("team:bundle"), ["carol", "dave"]);
});

it("skips comments and blank lines", () => {
const teamsPath = path.join(tmpDir, "OWNERTEAMS");
fs.writeFileSync(teamsPath, "# comment\n\nteam:platform @alice\n");
const teams = parseOwnerTeams(teamsPath);
assert.equal(teams.size, 1);
});

it("returns empty map if file does not exist", () => {
const teams = parseOwnerTeams(path.join(tmpDir, "NONEXISTENT"));
assert.equal(teams.size, 0);
});
});

// --- parseOwnersFile with team aliases ---

describe("parseOwnersFile with OWNERTEAMS", () => {
let tmpDir;
let ownersPath;
let teamsPath;

before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "owners-teams-test-"));
ownersPath = path.join(tmpDir, "OWNERS");
teamsPath = path.join(tmpDir, "OWNERTEAMS");
});

after(() => {
fs.rmSync(tmpDir, { recursive: true });
});

it("expands team aliases to members", () => {
fs.writeFileSync(teamsPath, "team:platform @alice @bob\n");
fs.writeFileSync(ownersPath, "/cmd/auth/ team:platform\n");
const rules = parseOwnersFile(ownersPath);
assert.equal(rules.length, 1);
assert.deepEqual(rules[0].owners, ["alice", "bob"]);
});

it("mixes team aliases with individual owners", () => {
fs.writeFileSync(teamsPath, "team:platform @alice @bob\n");
fs.writeFileSync(ownersPath, "/cmd/auth/ team:platform @carol\n");
const rules = parseOwnersFile(ownersPath);
assert.equal(rules.length, 1);
assert.deepEqual(rules[0].owners, ["alice", "bob", "carol"]);
});

it("unknown team alias is ignored", () => {
fs.writeFileSync(teamsPath, "team:platform @alice\n");
fs.writeFileSync(ownersPath, "/cmd/auth/ team:unknown @bob\n");
const rules = parseOwnersFile(ownersPath);
assert.deepEqual(rules[0].owners, ["bob"]);
});

it("works without OWNERTEAMS file", () => {
const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "owners-noteams-"));
const ownersPath2 = path.join(tmpDir2, "OWNERS");
fs.writeFileSync(ownersPath2, "* @alice\n");
const rules = parseOwnersFile(ownersPath2);
assert.deepEqual(rules[0].owners, ["alice"]);
fs.rmSync(tmpDir2, { recursive: true });
});
});

// --- findOwners ---

describe("findOwners", () => {
Expand Down
57 changes: 30 additions & 27 deletions .github/workflows/maintainer-approval.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,11 @@ module.exports = async ({ github, context, core }) => {
const prNumber = context.issue.number;
const authorLogin = pr?.user?.login;
const sha = pr.head.sha;
const statusParams = {
const checkParams = {
owner: context.repo.owner,
repo: context.repo.repo,
sha,
context: STATUS_CONTEXT,
head_sha: sha,
name: STATUS_CONTEXT,
};

const reviews = await github.paginate(github.rest.pulls.listReviews, {
Expand All @@ -464,10 +464,11 @@ module.exports = async ({ github, context, core }) => {
if (maintainerApproval) {
const approver = maintainerApproval.user.login;
core.info(`Maintainer approval from @${approver}`);
await github.rest.repos.createCommitStatus({
...statusParams,
state: "success",
description: `Approved by @${approver}`,
await github.rest.checks.create({
...checkParams,
status: "completed",
conclusion: "success",
output: { title: STATUS_CONTEXT, summary: `Approved by @${approver}` },
});
await deleteMarkerComments(github, owner, repo, prNumber);
return;
Expand All @@ -481,10 +482,11 @@ module.exports = async ({ github, context, core }) => {
);
if (hasAnyApproval) {
core.info(`Maintainer-authored PR approved by a reviewer.`);
await github.rest.repos.createCommitStatus({
...statusParams,
state: "success",
description: "Approved (maintainer-authored PR)",
await github.rest.checks.create({
...checkParams,
status: "completed",
conclusion: "success",
output: { title: STATUS_CONTEXT, summary: "Approved (maintainer-authored PR)" },
});
await deleteMarkerComments(github, owner, repo, prNumber);
return;
Expand Down Expand Up @@ -517,10 +519,11 @@ module.exports = async ({ github, context, core }) => {
// Set commit status. Approved PRs return early (commit status is sufficient).
if (result.allCovered && approverLogins.length > 0) {
core.info("All ownership groups have per-path approval.");
await github.rest.repos.createCommitStatus({
...statusParams,
state: "success",
description: "All ownership groups approved",
await github.rest.checks.create({
...checkParams,
status: "completed",
conclusion: "success",
output: { title: STATUS_CONTEXT, summary: "All ownership groups approved" },
});
await deleteMarkerComments(github, owner, repo, prNumber);
return;
Expand All @@ -532,10 +535,10 @@ module.exports = async ({ github, context, core }) => {
`Files need maintainer review: ${fileList}. ` +
`Maintainers: ${maintainers.join(", ")}`;
core.info(msg);
await github.rest.repos.createCommitStatus({
...statusParams,
state: "pending",
description: msg.length > 140 ? msg.slice(0, 137) + "..." : msg,
await github.rest.checks.create({
...checkParams,
status: "in_progress",
output: { title: STATUS_CONTEXT, summary: msg },
});
} else if (result.uncovered && result.uncovered.length > 0) {
const groupList = result.uncovered
Expand All @@ -545,18 +548,18 @@ module.exports = async ({ github, context, core }) => {
core.info(
`${msg}. Alternatively, any maintainer can approve: ${maintainers.join(", ")}.`
);
await github.rest.repos.createCommitStatus({
...statusParams,
state: "pending",
description: msg.length > 140 ? msg.slice(0, 137) + "..." : msg,
await github.rest.checks.create({
...checkParams,
status: "in_progress",
output: { title: STATUS_CONTEXT, summary: msg },
});
} else {
const msg = `Waiting for maintainer approval: ${maintainers.join(", ")}`;
core.info(msg);
await github.rest.repos.createCommitStatus({
...statusParams,
state: "pending",
description: msg.length > 140 ? msg.slice(0, 137) + "..." : msg,
await github.rest.checks.create({
...checkParams,
status: "in_progress",
output: { title: STATUS_CONTEXT, summary: msg },
});
}

Expand Down
Loading
Loading