diff --git a/.github/actions/bot/.gitignore b/.github/actions/bot/.gitignore
new file mode 100644
index 000000000..c2658d7d1
--- /dev/null
+++ b/.github/actions/bot/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/.github/actions/bot/README.md b/.github/actions/bot/README.md
new file mode 100644
index 000000000..7b90fb7bd
--- /dev/null
+++ b/.github/actions/bot/README.md
@@ -0,0 +1,12 @@
+# bot
+
+This GitHub Action parses commands from pull request comments and executes them.
+
+Only authorized users (members and owners of this repository) are able to execute commands.
+
+Commands look like:
+```
+/echo hello world
+```
+
+Multiple commands can be included in a comment, one per line; but each command must be unique.
diff --git a/.github/actions/bot/action.yaml b/.github/actions/bot/action.yaml
new file mode 100644
index 000000000..dfb471a30
--- /dev/null
+++ b/.github/actions/bot/action.yaml
@@ -0,0 +1,13 @@
+name: "Bot"
+description: "🤖 beep boop"
+runs:
+ using: "composite"
+ steps:
+ - uses: "actions/checkout@v3"
+ - uses: "actions/github-script@v6"
+ with:
+ script: |
+ const crypto = require('crypto');
+ const uuid = crypto.randomUUID();
+ const bot = require('./.github/actions/bot/index.js');
+ await bot(core, github, context, uuid);
\ No newline at end of file
diff --git a/.github/actions/bot/index.js b/.github/actions/bot/index.js
new file mode 100644
index 000000000..b155dccbc
--- /dev/null
+++ b/.github/actions/bot/index.js
@@ -0,0 +1,155 @@
+// this script cannot require/import, because it's called by actions/github-script.
+// any dependencies must be passed in the inline script in action.yaml
+
+async function bot(core, github, context, uuid) {
+ const payload = context.payload;
+
+ if (!payload.comment) {
+ console.log("No comment found in payload");
+ return;
+ }
+ console.log("Comment found in payload");
+
+ const author = payload.comment.user.login;
+ const authorized = ["OWNER", "MEMBER"].includes(payload.comment.author_association);
+ if (!authorized) {
+ console.log(`Comment author is not authorized: ${author}`);
+ return;
+ }
+ console.log(`Comment author is authorized: ${author}`);
+
+ const commands = parseCommands(uuid, payload, payload.comment.body);
+ if (commands.length === 0) {
+ console.log("No commands found in comment body");
+ return;
+ }
+ const uniqueCommands = [...new Set(commands.map(command => typeof command))];
+ if (uniqueCommands.length != commands.length) {
+ console.log("Duplicate commands found in comment body");
+ return;
+ }
+ console.log(commands.length + " command(s) found in comment body");
+
+ for (const command of commands) {
+ const reply = await command.run(author, github);
+ if (typeof reply === 'string') {
+ github.rest.issues.createComment({
+ owner: payload.repository.owner.login,
+ repo: payload.repository.name,
+ issue_number: payload.issue.number,
+ body: reply
+ });
+ } else if (reply) {
+ console.log(`Command returned: ${reply}`);
+ } else {
+ console.log("Command did not return a reply");
+ }
+ }
+}
+
+// parseCommands splits the comment body into lines and parses each line as a command.
+function parseCommands(uuid, payload, commentBody) {
+ const commands = [];
+ if (!commentBody) {
+ return commands;
+ }
+ const lines = commentBody.split(/\r?\n/);
+ for (const line of lines) {
+ const command = parseCommand(uuid, payload, line);
+ if (command) {
+ commands.push(command);
+ }
+ }
+ return commands
+}
+
+// parseCommand parses a line as a command.
+// The format of a command is `/NAME ARGS...`.
+// Leading and trailing spaces are ignored.
+function parseCommand(uuid, payload, line) {
+ line = line.trim();
+ const command = line.match(/^\/([a-z\-]+)(?:\s+(.+))?$/);
+ if (command) {
+ return buildCommand(uuid, payload, command[1], command[2]);
+ }
+ return null;
+}
+
+// buildCommand builds a command from a name and arguments.
+function buildCommand(uuid, payload, name, args) {
+ switch (name) {
+ case "echo":
+ return new EchoCommand(uuid, payload, args);
+ case "ci":
+ return new CICommand(uuid, payload, args);
+ default:
+ console.log(`Unknown command: ${name}`);
+ return null;
+ }
+}
+
+class EchoCommand {
+ constructor(uuid, payload, args) {
+ this.phrase = args ? args : "echo";
+ }
+
+ run(author) {
+ return `@${author} *${this.phrase}*`;
+ }
+}
+
+class CICommand {
+ constructor(uuid, payload, args) {
+ this.repository_owner = payload.repository.owner.login;
+ this.repository_name = payload.repository.name;
+ this.pr_number = payload.issue.number;
+ this.comment_url = payload.comment.html_url;
+ this.uuid = uuid;
+ this.goal = "test";
+ if (args != null && args != "") {
+ this.goal = args;
+ }
+ }
+
+ async run(author, github) {
+ const pr = await github.rest.pulls.get({
+ owner: this.repository_owner,
+ repo: this.repository_name,
+ pull_number: this.pr_number
+ });
+ const mergeable = pr.data.mergeable;
+ switch (mergeable) {
+ case true:
+ break;
+ case false:
+ case null:
+ return `@${author} this PR is not currently mergeable, you'll need to rebase it first.`;
+ default:
+ throw new Error(`Unknown mergeable value: ${pr.data.mergeable}`);
+ }
+ console.log(`Dispatching workflow with UUID: ${this.uuid}`);
+ await github.rest.actions.createWorkflowDispatch({
+ owner: this.repository_owner,
+ repo: this.repository_name,
+ workflow_id: 'ci-manual.yaml',
+ ref: 'master',
+ inputs: {
+ uuid: this.uuid,
+ pr_number: `${this.pr_number}`,
+ git_sha: pr.data.merge_commit_sha,
+ goal: this.goal,
+ arguments: this.args,
+ requester: author,
+ comment_url: this.comment_url
+ }
+ });
+ return null;
+ }
+}
+
+
+module.exports = async (core, github, context, uuid) => {
+ bot(core, github, context, uuid).catch((error) => {
+ core.setFailed(error);
+ });
+}
\ No newline at end of file
diff --git a/.github/actions/bot/package-lock.json b/.github/actions/bot/package-lock.json
new file mode 100644
index 000000000..333a0db57
--- /dev/null
+++ b/.github/actions/bot/package-lock.json
@@ -0,0 +1,430 @@
+{
+ "name": "bot",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "bot",
+ "version": "1.0.0",
+ "dependencies": {
+ "@actions/core": "^1.10.0",
+ "@actions/github": "^5.1.1"
+ }
+ },
+ "node_modules/@actions/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
+ "dependencies": {
+ "@actions/http-client": "^2.0.1",
+ "uuid": "^8.3.2"
+ }
+ },
+ "node_modules/@actions/github": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz",
+ "integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==",
+ "dependencies": {
+ "@actions/http-client": "^2.0.1",
+ "@octokit/core": "^3.6.0",
+ "@octokit/plugin-paginate-rest": "^2.17.0",
+ "@octokit/plugin-rest-endpoint-methods": "^5.13.0"
+ }
+ },
+ "node_modules/@actions/http-client": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.1.tgz",
+ "integrity": "sha512-qhrkRMB40bbbLo7gF+0vu+X+UawOvQQqNAA/5Unx774RS8poaOhThDOG6BGmxvAnxhQnDp2BG/ZUm65xZILTpw==",
+ "dependencies": {
+ "tunnel": "^0.0.6"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
+ "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
+ "dependencies": {
+ "@octokit/types": "^6.0.3"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
+ "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
+ "dependencies": {
+ "@octokit/auth-token": "^2.4.4",
+ "@octokit/graphql": "^4.5.8",
+ "@octokit/request": "^5.6.3",
+ "@octokit/request-error": "^2.0.5",
+ "@octokit/types": "^6.0.3",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "6.0.12",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
+ "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
+ "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+ "dependencies": {
+ "@octokit/request": "^5.6.0",
+ "@octokit/types": "^6.0.3",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "12.11.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
+ "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "2.21.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz",
+ "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==",
+ "dependencies": {
+ "@octokit/types": "^6.40.0"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=2"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "5.16.2",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz",
+ "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==",
+ "dependencies": {
+ "@octokit/types": "^6.39.0",
+ "deprecation": "^2.3.1"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
+ "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
+ "dependencies": {
+ "@octokit/endpoint": "^6.0.1",
+ "@octokit/request-error": "^2.1.0",
+ "@octokit/types": "^6.16.1",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
+ "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "6.41.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
+ "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
+ "dependencies": {
+ "@octokit/openapi-types": "^12.11.0"
+ }
+ },
+ "node_modules/before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
+ },
+ "node_modules/deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
+ "node_modules/universal-user-agent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
+ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ }
+ },
+ "dependencies": {
+ "@actions/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
+ "requires": {
+ "@actions/http-client": "^2.0.1",
+ "uuid": "^8.3.2"
+ }
+ },
+ "@actions/github": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz",
+ "integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==",
+ "requires": {
+ "@actions/http-client": "^2.0.1",
+ "@octokit/core": "^3.6.0",
+ "@octokit/plugin-paginate-rest": "^2.17.0",
+ "@octokit/plugin-rest-endpoint-methods": "^5.13.0"
+ }
+ },
+ "@actions/http-client": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.1.tgz",
+ "integrity": "sha512-qhrkRMB40bbbLo7gF+0vu+X+UawOvQQqNAA/5Unx774RS8poaOhThDOG6BGmxvAnxhQnDp2BG/ZUm65xZILTpw==",
+ "requires": {
+ "tunnel": "^0.0.6"
+ }
+ },
+ "@octokit/auth-token": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
+ "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
+ "requires": {
+ "@octokit/types": "^6.0.3"
+ }
+ },
+ "@octokit/core": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
+ "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
+ "requires": {
+ "@octokit/auth-token": "^2.4.4",
+ "@octokit/graphql": "^4.5.8",
+ "@octokit/request": "^5.6.3",
+ "@octokit/request-error": "^2.0.5",
+ "@octokit/types": "^6.0.3",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/endpoint": {
+ "version": "6.0.12",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
+ "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
+ "requires": {
+ "@octokit/types": "^6.0.3",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/graphql": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
+ "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+ "requires": {
+ "@octokit/request": "^5.6.0",
+ "@octokit/types": "^6.0.3",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/openapi-types": {
+ "version": "12.11.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
+ "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="
+ },
+ "@octokit/plugin-paginate-rest": {
+ "version": "2.21.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz",
+ "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==",
+ "requires": {
+ "@octokit/types": "^6.40.0"
+ }
+ },
+ "@octokit/plugin-rest-endpoint-methods": {
+ "version": "5.16.2",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz",
+ "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==",
+ "requires": {
+ "@octokit/types": "^6.39.0",
+ "deprecation": "^2.3.1"
+ }
+ },
+ "@octokit/request": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
+ "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
+ "requires": {
+ "@octokit/endpoint": "^6.0.1",
+ "@octokit/request-error": "^2.1.0",
+ "@octokit/types": "^6.16.1",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/request-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
+ "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+ "requires": {
+ "@octokit/types": "^6.0.3",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "@octokit/types": {
+ "version": "6.41.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
+ "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
+ "requires": {
+ "@octokit/openapi-types": "^12.11.0"
+ }
+ },
+ "before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
+ },
+ "deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
+ "is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
+ },
+ "node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
+ },
+ "universal-user-agent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
+ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+ },
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+ },
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ }
+ }
+}
diff --git a/.github/actions/bot/package.json b/.github/actions/bot/package.json
new file mode 100644
index 000000000..0c3a320e9
--- /dev/null
+++ b/.github/actions/bot/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "bot",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "command": "./local-harness.js $@"
+ },
+ "dependencies": {
+ "@actions/core": "^1.10.0",
+ "@actions/github": "^5.1.1"
+ }
+}
diff --git a/.github/actions/ci/build/action.yaml b/.github/actions/ci/build/action.yaml
new file mode 100644
index 000000000..f7ad76035
--- /dev/null
+++ b/.github/actions/ci/build/action.yaml
@@ -0,0 +1,26 @@
+name: "[CI] Build"
+inputs:
+ git_sha:
+ required: true
+ type: string
+ build_id:
+ required: true
+ type: string
+ k8s_version:
+ required: true
+ type: string
+outputs:
+ ami_id:
+ value: ${{ steps.build.outputs.ami_id }}
+runs:
+ using: "composite"
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ inputs.git_sha }}
+ - id: build
+ shell: bash
+ run: |
+ AMI_NAME="amazon-eks-node-${{ inputs.k8s_version }}-${{ inputs.build_id }}"
+ make ${{ inputs.k8s_version }} ami_name=${AMI_NAME}
+ echo "ami_id=$(jq -r .builds[0].artifact_id "${AMI_NAME}-manifest.json" | cut -d ':' -f 2)" >> $GITHUB_OUTPUT
diff --git a/.github/actions/ci/launch/action.yaml b/.github/actions/ci/launch/action.yaml
new file mode 100644
index 000000000..15cf71788
--- /dev/null
+++ b/.github/actions/ci/launch/action.yaml
@@ -0,0 +1,50 @@
+name: '[CI] Integration test / Launch'
+inputs:
+ build_id:
+ required: true
+ type: string
+ ami_id:
+ required: true
+ type: string
+ k8s_version:
+ required: true
+ type: string
+outputs:
+ cluster_name:
+ type: string
+ value: ${{ steps.launch.outputs.cluster_name }}
+runs:
+ using: "composite"
+ steps:
+ - id: launch
+ shell: bash
+ run: |
+ wget --no-verbose -O eksctl.tar.gz "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_Linux_amd64.tar.gz"
+ tar xf eksctl.tar.gz && chmod +x ./eksctl
+
+ SANITIZED_K8S_VERSION=$(echo ${{ inputs.k8s_version }} | tr -d '.')
+ CLUSTER_NAME="$SANITIZED_K8S_VERSION-${{ inputs.build_id }}"
+
+ echo '---
+ apiVersion: eksctl.io/v1alpha5
+ kind: ClusterConfig
+ metadata:
+ name: "'$CLUSTER_NAME'"
+ region: "${{ secrets.AWS_REGION }}"
+ version: "${{ inputs.k8s_version }}"
+ nodeGroups:
+ - name: "${{ inputs.build_id }}"
+ instanceType: m5.large
+ minSize: 3
+ maxSize: 3
+ desiredCapacity: 3
+ ami: "${{ inputs.ami_id }}"
+ amiFamily: AmazonLinux2
+ overrideBootstrapCommand: |
+ #!/bin/bash
+ source /var/lib/cloud/scripts/eksctl/bootstrap.helper.sh
+ /etc/eks/bootstrap.sh "'$CLUSTER_NAME'" --kubelet-extra-args "--node-labels=${NODE_LABELS}"' >> cluster.yaml
+ cat cluster.yaml
+
+ ./eksctl create cluster --config-file cluster.yaml
+ echo "cluster_name=$CLUSTER_NAME" >> $GITHUB_OUTPUT
diff --git a/.github/actions/ci/sonobuoy/action.yaml b/.github/actions/ci/sonobuoy/action.yaml
new file mode 100644
index 000000000..e0413fbea
--- /dev/null
+++ b/.github/actions/ci/sonobuoy/action.yaml
@@ -0,0 +1,29 @@
+name: '[CI] Integration test / Sonobuoy'
+inputs:
+ cluster_name:
+ required: true
+ type: string
+runs:
+ using: "composite"
+ steps:
+ - shell: bash
+ run: |
+ aws eks update-kubeconfig --name ${{ needs.launch.outputs.cluster_name }}
+ wget --no-verbose -O sonobuoy.tar.gz "https://github.com/vmware-tanzu/sonobuoy/releases/download/v0.56.11/sonobuoy_0.56.11_linux_amd64.tar.gz"
+ tar xf sonobuoy.tar.gz && chmod +x ./sonobuoy
+ ./sonobuoy run --wait
+ ./sonobuoy results $(./sonobuoy retrieve)
+
+ steps:
+ sonobuoy:
+ if: ${{ inputs.goal == 'test' }}
+ needs: launch
+ permissions:
+ id-token: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-region: ${{ secrets.AWS_REGION }}
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+
diff --git a/.github/actions/janitor/ami-sweeper/action.yaml b/.github/actions/janitor/ami-sweeper/action.yaml
new file mode 100644
index 000000000..e7735cc32
--- /dev/null
+++ b/.github/actions/janitor/ami-sweeper/action.yaml
@@ -0,0 +1,13 @@
+name: "[Janitor] AMI sweeper"
+description: "🗑️ Deletes CI AMI's when they're no longer needed"
+inputs:
+ max_age_seconds:
+ description: "Number of seconds after creation when an AMI becomes eligible for deletion"
+ required: true
+runs:
+ using: "composite"
+ steps:
+ - run: ${{ github.action_path }}/script.sh
+ shell: bash
+ env:
+ MAX_AGE_SECONDS: ${{ inputs.max_age_seconds }}
diff --git a/.github/actions/janitor/ami-sweeper/script.sh b/.github/actions/janitor/ami-sweeper/script.sh
new file mode 100755
index 000000000..f20e6005a
--- /dev/null
+++ b/.github/actions/janitor/ami-sweeper/script.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o pipefail
+
+MAX_AGE_SECONDS=${MAX_AGE_SECONDS:-$1}
+if [ -z "${MAX_AGE_SECONDS}" ]; then
+ echo "usage: $0 MAX_AGE_SECONDS"
+ exit 1
+fi
+
+set -o nounset
+
+# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-retries.html
+AWS_RETRY_MODE=standard
+AWS_MAX_ATTEMPTS=5
+
+function jqb64() {
+ if [ "$#" -lt 2 ]; then
+ echo "usage: jqb64 BASE64_JSON JQ_ARGS..."
+ exit 1
+ fi
+ BASE64_JSON="$1"
+ shift
+ echo "$BASE64_JSON" | base64 --decode | jq "$@"
+}
+for IMAGE_DETAILS in $(aws ec2 describe-images --owners self --output json | jq -r '.Images[] | @base64'); do
+ NAME=$(jqb64 "$IMAGE_DETAILS" -r '.Name')
+ IMAGE_ID=$(jqb64 "$IMAGE_DETAILS" -r '.ImageId')
+ CREATION_DATE=$(jqb64 "$IMAGE_DETAILS" -r '.CreationDate')
+ CREATION_DATE_SECONDS=$(date -d "$CREATION_DATE" '+%s')
+ CURRENT_TIME_SECONDS=$(date '+%s')
+ MIN_CREATION_DATE_SECONDS=$(($CURRENT_TIME_SECONDS - $MAX_AGE_SECONDS))
+ if [ "$CREATION_DATE_SECONDS" -lt "$MIN_CREATION_DATE_SECONDS" ]; then
+ aws ec2 deregister-image --image-id "$IMAGE_ID"
+ for SNAPSHOT_ID in $(jqb64 "$IMAGE_DETAILS" -r '.BlockDeviceMappings[].Ebs.SnapshotId'); do
+ aws ec2 delete-snapshot --snapshot-id "$SNAPSHOT_ID"
+ done
+ echo "Deleted $IMAGE_ID: $NAME"
+ fi
+done
diff --git a/.github/actions/janitor/cluster-sweeper/action.yaml b/.github/actions/janitor/cluster-sweeper/action.yaml
new file mode 100644
index 000000000..e53de27d1
--- /dev/null
+++ b/.github/actions/janitor/cluster-sweeper/action.yaml
@@ -0,0 +1,13 @@
+name: "[Janitor] Cluster sweeper"
+description: "🗑️ Deletes CI clusters when they're no longer needed"
+inputs:
+ max_age_seconds:
+ description: "Number of seconds after creation when a cluster becomes eligible for deletion"
+ required: true
+runs:
+ using: "composite"
+ steps:
+ - run: ${{ github.action_path }}/script.sh
+ shell: bash
+ env:
+ MAX_AGE_SECONDS: ${{ inputs.max_age_seconds }}
diff --git a/.github/actions/janitor/cluster-sweeper/script.sh b/.github/actions/janitor/cluster-sweeper/script.sh
new file mode 100755
index 000000000..97c041eec
--- /dev/null
+++ b/.github/actions/janitor/cluster-sweeper/script.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+set -o errexit
+set -o pipefail
+
+MAX_AGE_SECONDS=${MAX_AGE_SECONDS:-$1}
+if [ -z "${MAX_AGE_SECONDS}" ]; then
+ echo "usage: $0 MAX_AGE_SECONDS"
+ exit 1
+fi
+
+set -o nounset
+
+# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-retries.html
+AWS_RETRY_MODE=standard
+AWS_MAX_ATTEMPTS=5
+
+function iso8601_is_eligible_for_deletion() {
+ local TIME_IN_ISO8601="$1"
+ local TIME_IN_SECONDS=$(date -d "$TIME_IN_ISO8601" '+%s')
+ local CURRENT_TIME_IN_SECONDS=$(date '+%s')
+ MIN_TIME_SECONDS=$(($CURRENT_TIME_IN_SECONDS - $MAX_AGE_SECONDS))
+ [ "$TIME_IN_SECONDS" -lt "$MIN_TIME_SECONDS" ]
+}
+function cluster_is_eligible_for_deletion() {
+ local CLUSTER_NAME="$1"
+ local CREATED_AT_ISO8601=$(aws eks describe-cluster --name $CLUSTER_NAME --query 'cluster.createdAt' --output text)
+ iso8601_is_eligible_for_deletion "$CREATED_AT_ISO8601"
+}
+function nodegroup_is_eligible_for_deletion() {
+ local CLUSTER_NAME="$1"
+ local NODEGROUP_NAME="$2"
+ local CREATED_AT_ISO8601=$(aws eks describe-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name $NODEGROUP_NAME --query 'nodegroup.createdAt' --output text)
+ iso8601_is_eligible_for_deletion "$CREATED_AT_ISO8601"
+}
+wget --no-verbose -O eksctl.tar.gz "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_Linux_amd64.tar.gz"
+tar xf eksctl.tar.gz && chmod +x ./eksctl
+for CLUSTER in $(aws eks list-clusters --query 'clusters[]' --output text); do
+ for NODEGROUP in $(aws eks list-nodegroups --cluster-name $CLUSTER --query 'nodegroups[]' --output text); do
+ if nodegroup_is_eligible_for_deletion $CLUSTER $NODEGROUP; then
+ ./eksctl delete nodegroup --cluster $CLUSTER --name $NODEGROUP
+ fi
+ done
+ if [ "$(aws eks list-nodegroups --cluster-name $CLUSTER --output json | jq '.nodegroups | length')" -gt 0 ]; then
+ echo "Skipping cluster $CLUSTER"
+ elif cluster_is_eligible_for_deletion $CLUSTER; then
+ echo "Deleting cluster $CLUSTER"
+ ./eksctl delete cluster --name "$CLUSTER"
+ fi
+done
diff --git a/.github/workflows/alas-issues.yaml b/.github/workflows/alas-issues.yaml
deleted file mode 100644
index d71611bdc..000000000
--- a/.github/workflows/alas-issues.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
----
-name: "[ALAS] Open issues for new bulletins"
-on:
- workflow_dispatch:
- inputs:
- window:
- description: "Only consider bulletins published within this relative time window (golang Duration)"
- default: "24h"
- required: true
- schedule:
- # once an hour, at the top of hour
- - cron: "0 * * * *"
-permissions:
- issues: write
-jobs:
- alas-al2-bulletins:
- runs-on: ubuntu-latest
- steps:
- - uses: guilhem/rss-issues-action@0.5.2
- with:
- repo-token: "${{ secrets.GITHUB_TOKEN }}"
- feed: "https://alas.aws.amazon.com/AL2/alas.rss"
- dry-run: "true"
- lastTime: "${{ github.event.inputs.window || '24h' }}"
- labels: "alas,alas/al2"
- titleFilter: "(medium|low)"
diff --git a/.github/workflows/bot-trigger.yaml b/.github/workflows/bot-trigger.yaml
new file mode 100644
index 000000000..d728d4f10
--- /dev/null
+++ b/.github/workflows/bot-trigger.yaml
@@ -0,0 +1,14 @@
+name: Bot
+run-name: 🤖 beep boop
+on:
+ issue_comment:
+ types:
+ - created
+jobs:
+ bot:
+ if: ${{ github.event.issue.pull_request }}
+ runs-on: ubuntu-latest
+ permissions: write-all
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/bot
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci-auto.yaml
similarity index 87%
rename from .github/workflows/ci.yaml
rename to .github/workflows/ci-auto.yaml
index 7f780e683..2cc47a2c6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci-auto.yaml
@@ -1,9 +1,6 @@
-name: CI
+name: "[CI] Auto"
on:
workflow_dispatch:
- push:
- branches:
- - 'master'
pull_request:
types:
- opened
@@ -17,7 +14,7 @@ jobs:
- run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- run: go install mvdan.cc/sh/v3/cmd/shfmt@latest
- run: make lint
- test:
+ unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/ci-manual.yaml b/.github/workflows/ci-manual.yaml
new file mode 100644
index 000000000..9e49a38c2
--- /dev/null
+++ b/.github/workflows/ci-manual.yaml
@@ -0,0 +1,181 @@
+name: '[CI] Manual'
+run-name: "#${{ inputs.pr_number }} - ${{ inputs.uuid }}"
+on:
+ workflow_dispatch:
+ inputs:
+ requester:
+ required: true
+ type: string
+ comment_url:
+ required: true
+ type: string
+ uuid:
+ required: true
+ type: string
+ pr_number:
+ required: true
+ type: string
+ git_sha:
+ required: true
+ type: string
+ goal:
+ required: true
+ type: choice
+ default: "test"
+ options:
+ - "build"
+ - "launch"
+ - "test"
+jobs:
+ setup:
+ runs-on: ubuntu-latest
+ outputs:
+ git_sha_short: ${{ steps.variables.outputs.git_sha_short }}
+ workflow_run_url: ${{ steps.variables.outputs.workflow_run_url }}
+ kubernetes_versions: ${{ steps.variables.outputs.kubernetes_versions }}
+ build_id: ${{ steps.variables.outputs.build_id }}
+ ci_step_name_prefix: ${{ steps.variables.outputs.ci_step_name_prefix }}
+ steps:
+ - uses: actions/checkout@v3
+ - id: variables
+ run: |
+ echo "git_sha_short=$(echo ${{ inputs.git_sha }} | rev | cut -c-7 | rev)" >> $GITHUB_OUTPUT
+ echo "workflow_run_url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
+ echo "kubernetes_versions=$(cat kubernetes-versions.json | jq -c .)" >> $GITHUB_OUTPUT
+ echo "build_id=ci-${{ inputs.pr_number }}-${{ needs.setup.outputs.git_sha_short }}-${{ inputs.uuid }}" >> $GITHUB_OUTPUT
+ echo 'ci_step_name_prefix=CI:' >> $GITHUB_OUTPUT
+ notify-start:
+ runs-on: ubuntu-latest
+ needs:
+ - setup
+ steps:
+ - uses: actions/github-script@v6
+ with:
+ script: |
+ const commentBody = core.summary
+ .addRaw("@${{ inputs.requester }} roger that! I've dispatched a workflow. 👍")
+ .stringify();
+ github.rest.issues.createComment({
+ owner: "${{ github.repository_owner }}",
+ repo: "${{ github.repository }}".split('/')[1],
+ issue_number: ${{ inputs.pr_number }},
+ body: commentBody
+ });
+ kubernetes-version-workflow:
+ runs-on: ubuntu-latest
+ name: ${{ matrix.k8s_version }}
+ needs:
+ - setup
+ - notify-start
+ permissions:
+ id-token: write
+ contents: read
+ strategy:
+ # don't bail out of all sub-tasks if one fails
+ fail-fast: false
+ matrix:
+ k8s_version: ${{ fromJson(needs.setup.outputs.kubernetes_versions) }}
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: 'master'
+ - uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-region: ${{ secrets.AWS_REGION }}
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ # 2 hours (job usually completes in 90 minutes)
+ role-duration-seconds: 7200
+ - name: "${{ needs.setup.outputs.ci_step_name_prefix }} Build"
+ id: build
+ uses: ./.github/actions/ci/build
+ with:
+ git_sha: ${{ inputs.git_sha }}
+ k8s_version: ${{ matrix.k8s_version }}
+ build_id: ${{ needs.setup.outputs.build_id }}
+ - if: ${{ inputs.goal == 'launch' || inputs.goal == 'test' }}
+ name: "${{ needs.setup.outputs.ci_step_name_prefix }} Launch"
+ id: launch
+ uses: ./.github/actions/ci/launch
+ with:
+ ami_id: ${{ steps.build.outputs.ami_id }}
+ k8s_version: ${{ matrix.k8s_version }}
+ build_id: ${{ needs.setup.outputs.build_id }}
+ - if: ${{ inputs.goal == 'test' }}
+ name: "${{ needs.setup.outputs.ci_step_name_prefix }} Sonobuoy"
+ id: sonobuoy
+ uses: ./.github/actions/ci/sonobuoy
+ with:
+ cluster_name: ${{ steps.launch.outputs.cluster_name }}
+ notify-outcome:
+ if: ${{ always() }}
+ runs-on: ubuntu-latest
+ needs:
+ - setup
+ - kubernetes-version-workflow
+ steps:
+ - uses: actions/github-script@v6
+ with:
+ script: |
+ const { data } = await github.rest.actions.listJobsForWorkflowRun({
+ owner: "${{ github.repository_owner }}",
+ repo: "${{ github.repository }}".split('/')[1],
+ run_id: ${{ github.run_id }}
+ });
+ const conclusionEmojis = {
+ "success": "✅",
+ "skipped": "⏭️",
+ "failure": "❌",
+ "cancelled": "🚮"
+ };
+ const uniqueStepNames = new Set();
+ const stepConclusionsByK8sVersion = new Map();
+ const ciStepNamePrefix = "${{ needs.setup.outputs.ci_step_name_prefix }}";
+ for (const job of data.jobs) {
+ if (/\d+\.\d+/.test(job.name)) {
+ const k8sVersion = job.name;
+ for (const step of job.steps) {
+ if (step.name.startsWith(ciStepNamePrefix)) {
+ const stepName = step.name.substring(ciStepNamePrefix.length).trim();
+ let stepConclusions = stepConclusionsByK8sVersion.get(k8sVersion);
+ if (!stepConclusions) {
+ stepConclusions = new Map();
+ stepConclusionsByK8sVersion.set(k8sVersion, stepConclusions);
+ }
+ stepConclusions.set(stepName, step.conclusion);
+ uniqueStepNames.add(stepName);
+ }
+ }
+ }
+ }
+ const headers = [{
+ data: 'Kubernetes version',
+ header: true
+ }];
+ for (const stepName of uniqueStepNames.values()) {
+ headers.push({
+ data: stepName,
+ header: true
+ });
+ }
+ const rows = [];
+ for (const stepConclusionsForK8sVersion of [...stepConclusionsByK8sVersion.entries()].sort()) {
+ const k8sVersion = stepConclusionsForK8sVersion[0];
+ const row = [k8sVersion];
+ for (const step of stepConclusionsForK8sVersion[1].entries()) {
+ row.push(`${step[1]} ${conclusionEmojis[step[1]]}`);
+ }
+ rows.push(row);
+ }
+ const commentBody = core.summary
+ .addRaw("@${{ inputs.requester }} the workflow that you requested has completed. 🎉")
+ .addTable([
+ headers,
+ ...rows,
+ ])
+ .stringify();
+ github.rest.issues.createComment({
+ owner: "${{ github.repository_owner }}",
+ repo: "${{ github.repository }}".split('/')[1],
+ issue_number: ${{ inputs.pr_number }},
+ body: commentBody
+ });
\ No newline at end of file
diff --git a/.github/workflows/janitor.yaml b/.github/workflows/janitor.yaml
new file mode 100644
index 000000000..c34047dc1
--- /dev/null
+++ b/.github/workflows/janitor.yaml
@@ -0,0 +1,34 @@
+name: "Janitor"
+on:
+ workflow_dispatch:
+ schedule:
+ # hourly at the top of the hour
+ - cron: "0 * * * *"
+permissions:
+ id-token: write
+ contents: read
+jobs:
+ cluster-sweeper:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-region: ${{ secrets.AWS_REGION }}
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ - uses: ./.github/actions/janitor/cluster-sweeper
+ with:
+ # 3 hours
+ max_age_seconds: 10800
+ ami-sweeper:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-region: ${{ secrets.AWS_REGION }}
+ role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+ - uses: ./.github/actions/janitor/ami-sweeper
+ with:
+ # 3 days
+ max_age_seconds: 259200
diff --git a/kubernetes-versions.json b/kubernetes-versions.json
new file mode 100644
index 000000000..fc7449f8d
--- /dev/null
+++ b/kubernetes-versions.json
@@ -0,0 +1,7 @@
+[
+ "1.23",
+ "1.24",
+ "1.25",
+ "1.26",
+ "1.27"
+]
\ No newline at end of file