Skip to content

Commit f0e6aec

Browse files
Add completed iteration, with test coverage
1 parent c1bb76d commit f0e6aec

19 files changed

+965
-148
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: Add permissions for CODEOWNERS
3+
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
source:
8+
description: Whether to use the marketplace version or the latest version from the repository.
9+
type: choice
10+
choices:
11+
- marketplace
12+
- repository
13+
required: true
14+
file-path:
15+
description: |
16+
The path to the CODEOWNERS file to operate on.
17+
Leave blank to search for all valid CODEOWNERS.
18+
required: false
19+
default: .github/CODEOWNERS
20+
type: string
21+
dry-run:
22+
description: Dry run?
23+
required: true
24+
default: true
25+
type: choice
26+
choices: ["true", "false"]
27+
28+
permissions:
29+
contents: write
30+
31+
jobs:
32+
add-permissions-to-codeowners:
33+
if: github.event.inputs.source == 'repository'
34+
runs-on: ubuntu-latest
35+
steps:
36+
- uses: actions/checkout@v3
37+
38+
- uses: ./
39+
with:
40+
file-path: ${{ github.event.inputs.file-path }}
41+
dry-run: ${{ github.event.inputs.dry-run }}
42+
github-token: ${{ secrets.GITHUB_TOKEN }}
43+
44+
add-permissions-to-codeowners-marketplace:
45+
if: github.event.inputs.source == 'marketplace'
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v3
49+
50+
- uses: Archetypically/add-write-permissions-for-codeowners@v1
51+
with:
52+
file-path: ${{ github.event.inputs.file-path }}
53+
dry-run: ${{ github.event.inputs.dry-run }}
54+
github-token: ${{ secrets.GITHUB_TOKEN }}

action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ inputs:
1616
https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location.
1717
required: false
1818
default: CODEOWNERS
19+
1920
dry-run:
2021
description: Whether or not to actually make the updates
2122
required: false

compiled/collaborators.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use strict";
2+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+
if (k2 === undefined) k2 = k;
4+
var desc = Object.getOwnPropertyDescriptor(m, k);
5+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+
desc = { enumerable: true, get: function() { return m[k]; } };
7+
}
8+
Object.defineProperty(o, k2, desc);
9+
}) : (function(o, m, k, k2) {
10+
if (k2 === undefined) k2 = k;
11+
o[k2] = m[k];
12+
}));
13+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+
Object.defineProperty(o, "default", { enumerable: true, value: v });
15+
}) : function(o, v) {
16+
o["default"] = v;
17+
});
18+
var __importStar = (this && this.__importStar) || function (mod) {
19+
if (mod && mod.__esModule) return mod;
20+
var result = {};
21+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22+
__setModuleDefault(result, mod);
23+
return result;
24+
};
25+
Object.defineProperty(exports, "__esModule", { value: true });
26+
exports.addUserToWriteAccess = exports.getAllDirectCollaboratorsWithAtLeastWriteAccess = void 0;
27+
const core = __importStar(require("@actions/core"));
28+
async function getAllDirectCollaboratorsWithAtLeastWriteAccess(octokit, owner, repo) {
29+
const { data } = await octokit.rest.repos.listCollaborators({
30+
owner,
31+
repo,
32+
affiliation: "direct",
33+
});
34+
const collaborators = data
35+
.filter(({ permissions }) => {
36+
return permissions?.admin || permissions?.push;
37+
})
38+
.map(({ login }) => {
39+
return login;
40+
});
41+
return [...new Set(collaborators)];
42+
}
43+
exports.getAllDirectCollaboratorsWithAtLeastWriteAccess = getAllDirectCollaboratorsWithAtLeastWriteAccess;
44+
async function addUserToWriteAccess(octokit, owner, repo, username, isDryRun) {
45+
if (isDryRun) {
46+
core.notice(`Would have added '${username}' to '${owner}/${repo}' with write access, but dry-run is enabled.`);
47+
}
48+
else {
49+
await octokit.rest.repos.addCollaborator({
50+
owner,
51+
repo,
52+
username,
53+
permission: "push",
54+
});
55+
core.notice(`Added '${username}' to '${owner}/${repo}' with write access.`);
56+
}
57+
}
58+
exports.addUserToWriteAccess = addUserToWriteAccess;

compiled/helpers.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use strict";
2+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+
if (k2 === undefined) k2 = k;
4+
var desc = Object.getOwnPropertyDescriptor(m, k);
5+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+
desc = { enumerable: true, get: function() { return m[k]; } };
7+
}
8+
Object.defineProperty(o, k2, desc);
9+
}) : (function(o, m, k, k2) {
10+
if (k2 === undefined) k2 = k;
11+
o[k2] = m[k];
12+
}));
13+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+
Object.defineProperty(o, "default", { enumerable: true, value: v });
15+
}) : function(o, v) {
16+
o["default"] = v;
17+
});
18+
var __importStar = (this && this.__importStar) || function (mod) {
19+
if (mod && mod.__esModule) return mod;
20+
var result = {};
21+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22+
__setModuleDefault(result, mod);
23+
return result;
24+
};
25+
Object.defineProperty(exports, "__esModule", { value: true });
26+
exports.getAllCodeowners = exports.getFileContents = void 0;
27+
const fs = __importStar(require("fs"));
28+
const core = __importStar(require("@actions/core"));
29+
function getFileContents(defaultFileDetectionLocations = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"]) {
30+
const filePath = core.getInput("file-path", { required: true });
31+
let locationsToCheck = defaultFileDetectionLocations;
32+
if (filePath.length > 0) {
33+
const thisPlatformPath = core.toPlatformPath(filePath);
34+
core.debug(`Using specified path: ${thisPlatformPath}`);
35+
locationsToCheck = [thisPlatformPath];
36+
}
37+
else {
38+
core.info("Did not find specified input path, using default detection method.");
39+
}
40+
const existingPaths = locationsToCheck.filter((path) => {
41+
return fs.existsSync(path);
42+
});
43+
return existingPaths.map((path) => {
44+
core.notice(`Found CODEOWNERS file at '${path}' to operate on.`);
45+
return {
46+
path,
47+
contents: fs.readFileSync(path, "utf8"),
48+
};
49+
});
50+
}
51+
exports.getFileContents = getFileContents;
52+
function getAllCodeowners(fileContents) {
53+
const allCodeowners = fileContents
54+
.map(({ contents }) => {
55+
return contents
56+
.split("\n")
57+
.filter((line) => {
58+
return !line.startsWith("#") && line.length > 0;
59+
})
60+
.map((line) => {
61+
const [_path, ...owners] = line.split(" ");
62+
return owners;
63+
})
64+
.flat();
65+
})
66+
.flat();
67+
return [...new Set(allCodeowners)].map((owner) => {
68+
return owner.replaceAll("@", "");
69+
});
70+
}
71+
exports.getAllCodeowners = getAllCodeowners;

compiled/index.js

Lines changed: 59 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -23,86 +23,84 @@ var __importStar = (this && this.__importStar) || function (mod) {
2323
return result;
2424
};
2525
Object.defineProperty(exports, "__esModule", { value: true });
26-
exports.main = exports.getAllTeamsWithAtLeastWriteAccess = exports.getAllCodeowners = exports.getFileContents = void 0;
26+
exports.main = void 0;
2727
/* eslint-disable no-process-env */
2828
const util_1 = require("util");
29-
const fs = __importStar(require("fs"));
3029
const core = __importStar(require("@actions/core"));
3130
const github = __importStar(require("@actions/github"));
32-
function getFileContents(defaultFileDetectionLocations = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"]) {
33-
const filePath = core.getInput("file-path", { required: true });
34-
let locationsToCheck = defaultFileDetectionLocations;
35-
if (filePath.length > 0) {
36-
const thisPlatformPath = core.toPlatformPath(filePath);
37-
core.debug(`Using specified path: ${thisPlatformPath}`);
38-
locationsToCheck = [thisPlatformPath];
39-
}
40-
else {
41-
core.info("Did not find specified input path, using default detection method.");
42-
}
43-
const existingPaths = locationsToCheck.filter((path) => {
44-
return fs.existsSync(path);
45-
});
46-
return existingPaths.map((path) => {
47-
core.notice(`Found CODEOWNERS file at '${path}' to operate on.`);
48-
return {
49-
path,
50-
contents: fs.readFileSync(path, "utf8"),
51-
};
52-
});
53-
}
54-
exports.getFileContents = getFileContents;
55-
function getAllCodeowners(fileContents) {
56-
const allCodeowners = fileContents
57-
.map(({ contents }) => {
58-
return contents
59-
.split("\n")
60-
.filter((line) => {
61-
return !line.startsWith("#") && line.length > 0;
62-
})
63-
.map((line) => {
64-
const [_path, ...owners] = line.split(" ");
65-
return owners;
66-
})
67-
.flat();
68-
})
69-
.flat();
70-
return [...new Set(allCodeowners)];
71-
}
72-
exports.getAllCodeowners = getAllCodeowners;
73-
async function getAllTeamsWithAtLeastWriteAccess(octokit, owner, repo) {
74-
let { data } = await octokit.rest.repos.listTeams({ owner, repo });
75-
console.log(data);
76-
const teams = data
77-
.filter(({ permission }) => {
78-
return ["admin", "push"].includes(permission);
79-
})
80-
.map(({ slug }) => {
81-
return slug;
82-
});
83-
return [...new Set(teams)];
84-
}
85-
exports.getAllTeamsWithAtLeastWriteAccess = getAllTeamsWithAtLeastWriteAccess;
31+
const helpers_1 = require("./helpers");
32+
const teams_1 = require("./teams");
33+
const collaborators_1 = require("./collaborators");
8634
async function main() {
8735
try {
8836
const isDryRun = (core.getInput("dry-run", { required: false }) || "false") === "true";
8937
if (isDryRun) {
9038
core.notice('"dry-run" enabled; will not make changes.');
9139
}
92-
const currentCodeowners = getFileContents();
40+
const currentCodeowners = (0, helpers_1.getFileContents)();
9341
if (currentCodeowners.length === 0) {
9442
const errorMsg = "No CODEOWNERS file(s) found.";
9543
core.error(errorMsg);
9644
throw new Error(errorMsg);
9745
}
98-
const allCodeowners = getAllCodeowners(currentCodeowners);
46+
const allCodeowners = (0, helpers_1.getAllCodeowners)(currentCodeowners);
9947
core.notice(`Found ${allCodeowners.length} unique codeowners.`);
10048
core.debug(`All codeowners: ${(0, util_1.inspect)(allCodeowners)}`);
49+
if (allCodeowners.length === 0) {
50+
core.notice("No CODEOWNERS found; nothing to do.");
51+
return;
52+
}
53+
const teamCodeowners = allCodeowners.filter((codeowner) => {
54+
return codeowner.includes("/");
55+
});
56+
core.notice(`Found ${teamCodeowners.length} unique team codeowners.`);
57+
core.debug(`Team codeowners: ${(0, util_1.inspect)(teamCodeowners)}`);
58+
const userCodeowners = allCodeowners.filter((codeowner) => {
59+
return !codeowner.includes("/");
60+
});
61+
core.notice(`Found ${userCodeowners.length} unique user codeowners.`);
62+
core.debug(`User codeowners: ${(0, util_1.inspect)(userCodeowners)}`);
10163
const githubToken = core.getInput("github-token", { required: true });
10264
const octokit = github.getOctokit(githubToken);
103-
const allTeamsWithAtLeastWriteAccess = await getAllTeamsWithAtLeastWriteAccess(octokit, github.context.repo.owner, github.context.repo.repo);
104-
core.notice(`Found ${allTeamsWithAtLeastWriteAccess.length} unique teams with write access.`);
105-
core.debug(`All teams with write access: ${allTeamsWithAtLeastWriteAccess}`);
65+
// TEAM-Y OPERATIONS
66+
const allTeamsWithAtLeastWriteAccess = await (0, teams_1.getAllTeamsWithAtLeastWriteAccess)(octokit, github.context.repo.owner, github.context.repo.repo);
67+
core.info(`Found ${allTeamsWithAtLeastWriteAccess.length} teams with at least write access.`);
68+
core.debug(`All teams with at least write access: ${(0, util_1.inspect)(allTeamsWithAtLeastWriteAccess)}`);
69+
const failedTeams = [];
70+
teamCodeowners.forEach(async (team) => {
71+
const [orgName, teamSlug] = team.replaceAll("@", "").split("/");
72+
core.debug(`Found org: '${orgName}' and team: '${teamSlug}' from '${team}'.`);
73+
if (allTeamsWithAtLeastWriteAccess.includes(teamSlug)) {
74+
core.notice(`Team '${teamSlug}' already has at least write access; skipping.`);
75+
}
76+
else {
77+
await (0, teams_1.addTeamToWriteAccess)(octokit, orgName, github.context.repo.repo, teamSlug, isDryRun).catch((error) => {
78+
failedTeams.push(teamSlug);
79+
core.warning(`Failed to give write access to team '${teamSlug}': ${error}`);
80+
});
81+
}
82+
});
83+
// USER-Y OPERATIONS HERE
84+
const allUsersWithAtLeastWriteAccess = await (0, collaborators_1.getAllDirectCollaboratorsWithAtLeastWriteAccess)(octokit, github.context.repo.owner, github.context.repo.repo);
85+
core.info(`Found ${allUsersWithAtLeastWriteAccess.length} users with at least write access.`);
86+
core.debug(`All users with at least write access: ${(0, util_1.inspect)(allUsersWithAtLeastWriteAccess)}`);
87+
const failedUsers = [];
88+
userCodeowners.forEach(async (user) => {
89+
if (allUsersWithAtLeastWriteAccess.includes(user)) {
90+
core.notice(`User '${user}' already has at least write access; skipping.`);
91+
}
92+
else {
93+
await (0, collaborators_1.addUserToWriteAccess)(octokit, github.context.repo.owner, github.context.repo.repo, user, isDryRun).catch((error) => {
94+
failedUsers.push(user);
95+
core.warning(`Failed to give write access to user '${user}': ${error}`);
96+
});
97+
}
98+
});
99+
if (failedTeams.length > 0 || failedUsers.length > 0) {
100+
const errorMsg = `Failed to give write access to teams: ${failedTeams.join(", ")}. ` +
101+
`Failed to give write access to users: ${failedUsers.join(", ")}.`;
102+
throw new Error(errorMsg);
103+
}
106104
}
107105
catch (error) {
108106
core.debug((0, util_1.inspect)(error, false, 2, true));

compiled/teams.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use strict";
2+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+
if (k2 === undefined) k2 = k;
4+
var desc = Object.getOwnPropertyDescriptor(m, k);
5+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+
desc = { enumerable: true, get: function() { return m[k]; } };
7+
}
8+
Object.defineProperty(o, k2, desc);
9+
}) : (function(o, m, k, k2) {
10+
if (k2 === undefined) k2 = k;
11+
o[k2] = m[k];
12+
}));
13+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+
Object.defineProperty(o, "default", { enumerable: true, value: v });
15+
}) : function(o, v) {
16+
o["default"] = v;
17+
});
18+
var __importStar = (this && this.__importStar) || function (mod) {
19+
if (mod && mod.__esModule) return mod;
20+
var result = {};
21+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22+
__setModuleDefault(result, mod);
23+
return result;
24+
};
25+
Object.defineProperty(exports, "__esModule", { value: true });
26+
exports.addTeamToWriteAccess = exports.getAllTeamsWithAtLeastWriteAccess = void 0;
27+
const core = __importStar(require("@actions/core"));
28+
async function getAllTeamsWithAtLeastWriteAccess(octokit, owner, repo) {
29+
let { data } = await octokit.rest.repos.listTeams({ owner, repo });
30+
const teams = data
31+
.filter(({ permission }) => {
32+
return ["admin", "push"].includes(permission);
33+
})
34+
.map(({ slug }) => {
35+
return slug;
36+
});
37+
return [...new Set(teams)];
38+
}
39+
exports.getAllTeamsWithAtLeastWriteAccess = getAllTeamsWithAtLeastWriteAccess;
40+
async function addTeamToWriteAccess(octokit, owner, repo, team, isDryRun) {
41+
if (isDryRun) {
42+
core.notice(`Would have added ${team} to ${owner}/${repo} with write access, but dry-run is enabled.`);
43+
}
44+
else {
45+
await octokit.rest.teams.addOrUpdateRepoPermissionsInOrg({
46+
org: owner,
47+
owner: owner,
48+
repo,
49+
team_slug: team,
50+
permission: "push",
51+
});
52+
core.notice(`Added '${team}' to '${owner}/${repo}' with write access.`);
53+
}
54+
}
55+
exports.addTeamToWriteAccess = addTeamToWriteAccess;

compiled/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });

0 commit comments

Comments
 (0)