Skip to content

Commit

Permalink
Misc refinement (#1)
Browse files Browse the repository at this point in the history
* Extract pidIsRunning function

* Fix eslint configuration

* Use free port and random token

* Remove port from test workflow

* Add action branding
  • Loading branch information
trappar authored Feb 19, 2023
1 parent 1ce55cf commit 363c213
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 155 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
"plugin:prettier/recommended"
],
"plugins": ["@typescript-eslint"],
"root": true
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,5 @@ jobs:
- name: Run TurboRepo Remote Cache Server
uses: ./
with:
port: 42042
storage-provider: local
storage-path: test
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,29 @@ A GitHub action which runs a [ducktors/turborepo-remote-cache](https://github.co

The Team ID to use. This controls the directory where cache entries will be saved. Default `"ci"`.

### `port`

The port to run the server on. This is configurable in case of a port collision, but shouldn't normally need to be changed. Default `"42042"`.

## Environment variables

You may also need to set environment variables to provide credentials to the storage provider. See [supported storage providers](https://ducktors.github.io/turborepo-remote-cache/supported-storage-providers.html) for more information.

### Note

> If you are familiar with ducktors/turborepo-remote-cache, you may be wondering why there is a lack of other inputs for other environmental variables. The reasons are as follows:
> * "`PORT`" - Set automatically by the action to a random free port on the runner.
> * "`TURBO_TOKEN`" - Set automatically by the action to a random secure token on each workflow run.
> * "`NODE_ENV`", "`LOG_LEVEL`", "`STORAGE_PATH_USE_TMP_FOLDER`", and "`BODY_LIMIT`" - These can be set manually using the `env` input to the action if needed, but it's not recommended to change them.
## Example usage

```yaml
- uses: trappar/turborepo-remote-cache-gh-action@v1
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: TurboRepo Remote Cache Server
uses: trappar/turborepo-remote-cache-gh-action@v1
with:
storage-provider: s3
storage-path: my-bucket-name
```
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

No further configuration is required. The action will automatically set environmental variables which TurboRepo uses to connect to the remote cache server.
- name: Run Build
run: turbo build
```
7 changes: 3 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: "TurboRepo Remote Cache Server"
description: "Runs a TurboRepo remote cache server based on ducktors/turborepo-remote-cache."
author: trappar
branding:
icon: server
color: blue
inputs:
storage-provider:
description: "Possible values are s3, google-cloud-storage, or azure-blob-storage. Local storage is technically supported but is useless."
Expand All @@ -12,10 +15,6 @@ inputs:
description: "Sets the TURBO_TEAM env variable, which controls the directory where cache entries will be saved."
required: false
default: "ci"
port:
description: "Port to listen on."
required: false
default: "42042"
runs:
using: "node16"
main: "dist/start/index.js"
Expand Down
21 changes: 12 additions & 9 deletions dist/post/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2851,6 +2851,17 @@ const getLogDir = () => {
return logDir;
};

;// CONCATENATED MODULE: ./src/utils/pidIsRunning.ts
function pidIsRunning(pid) {
try {
process.kill(pid, 0);
return true;
}
catch (e) {
return false;
}
}

;// CONCATENATED MODULE: ./node_modules/.pnpm/indent-string@5.0.0/node_modules/indent-string/index.js
function indentString(string, count = 1, options = {}) {
const {
Expand Down Expand Up @@ -2897,15 +2908,7 @@ function indentString(string, count = 1, options = {}) {



function pidIsRunning(pid) {
try {
process.kill(pid, 0);
return true;
}
catch (e) {
return false;
}
}

async function post() {
const pid = parseInt((0,core.getState)("pid"));
if (pidIsRunning(pid)) {
Expand Down
203 changes: 194 additions & 9 deletions dist/start/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5914,16 +5914,201 @@ var core = __nccwpck_require__(7733);
var external_path_ = __nccwpck_require__(1017);
// EXTERNAL MODULE: ./node_modules/.pnpm/tcp-port-used@1.0.2/node_modules/tcp-port-used/index.js
var tcp_port_used = __nccwpck_require__(6542);
// EXTERNAL MODULE: external "crypto"
var external_crypto_ = __nccwpck_require__(6113);
;// CONCATENATED MODULE: external "node:net"
const external_node_net_namespaceObject = require("node:net");
;// CONCATENATED MODULE: external "node:os"
const external_node_os_namespaceObject = require("node:os");
;// CONCATENATED MODULE: ./node_modules/.pnpm/get-port@6.1.2/node_modules/get-port/index.js



class Locked extends Error {
constructor(port) {
super(`${port} is locked`);
}
}

const lockedPorts = {
old: new Set(),
young: new Set(),
};

// On this interval, the old locked ports are discarded,
// the young locked ports are moved to old locked ports,
// and a new young set for locked ports are created.
const releaseOldLockedPortsIntervalMs = 1000 * 15;

const minPort = 1024;
const maxPort = 65_535;

// Lazily create interval on first use
let interval;

const getLocalHosts = () => {
const interfaces = external_node_os_namespaceObject.networkInterfaces();

// Add undefined value for createServer function to use default host,
// and default IPv4 host in case createServer defaults to IPv6.
const results = new Set([undefined, '0.0.0.0']);

for (const _interface of Object.values(interfaces)) {
for (const config of _interface) {
results.add(config.address);
}
}

return results;
};

const checkAvailablePort = options =>
new Promise((resolve, reject) => {
const server = external_node_net_namespaceObject.createServer();
server.unref();
server.on('error', reject);

server.listen(options, () => {
const {port} = server.address();
server.close(() => {
resolve(port);
});
});
});

const getAvailablePort = async (options, hosts) => {
if (options.host || options.port === 0) {
return checkAvailablePort(options);
}

for (const host of hosts) {
try {
await checkAvailablePort({port: options.port, host}); // eslint-disable-line no-await-in-loop
} catch (error) {
if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) {
throw error;
}
}
}

return options.port;
};

const portCheckSequence = function * (ports) {
if (ports) {
yield * ports;
}

yield 0; // Fall back to 0 if anything else failed
};

async function getPorts(options) {
let ports;
let exclude = new Set();

if (options) {
if (options.port) {
ports = typeof options.port === 'number' ? [options.port] : options.port;
}

if (options.exclude) {
const excludeIterable = options.exclude;

if (typeof excludeIterable[Symbol.iterator] !== 'function') {
throw new TypeError('The `exclude` option must be an iterable.');
}

for (const element of excludeIterable) {
if (typeof element !== 'number') {
throw new TypeError('Each item in the `exclude` option must be a number corresponding to the port you want excluded.');
}

if (!Number.isSafeInteger(element)) {
throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
}
}

exclude = new Set(excludeIterable);
}
}

if (interval === undefined) {
interval = setInterval(() => {
lockedPorts.old = lockedPorts.young;
lockedPorts.young = new Set();
}, releaseOldLockedPortsIntervalMs);

// Does not exist in some environments (Electron, Jest jsdom env, browser, etc).
if (interval.unref) {
interval.unref();
}
}

const hosts = getLocalHosts();

for (const port of portCheckSequence(ports)) {
try {
if (exclude.has(port)) {
continue;
}

let availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop
while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
if (port !== 0) {
throw new Locked(port);
}

availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop
}

lockedPorts.young.add(availablePort);

return availablePort;
} catch (error) {
if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) {
throw error;
}
}
}

throw new Error('No available ports found');
}

function portNumbers(from, to) {
if (!Number.isInteger(from) || !Number.isInteger(to)) {
throw new TypeError('`from` and `to` must be integer numbers');
}

if (from < minPort || from > maxPort) {
throw new RangeError(`'from' must be between ${minPort} and ${maxPort}`);
}

if (to < minPort || to > maxPort) {
throw new RangeError(`'to' must be between ${minPort} and ${maxPort}`);
}

if (from > to) {
throw new RangeError('`to` must be greater than or equal to `from`');
}

const generator = function * (from, to) {
for (let port = from; port <= to; port++) {
yield port;
}
};

return generator(from, to);
}

;// CONCATENATED MODULE: ./src/start.ts






async function main() {
const port = parseInt((0,core.getInput)("port", {
required: true,
trimWhitespace: true,
}));
const port = await getPorts();
const storageProvider = (0,core.getInput)("storage-provider", {
required: true,
trimWhitespace: true,
Expand All @@ -5933,25 +6118,25 @@ async function main() {
trimWhitespace: true,
});
const teamId = (0,core.getInput)("team-id", { trimWhitespace: true });
const turboToken = process.env.TURBO_TOKEN || "turbo-token";
const token = (0,external_crypto_.randomBytes)(24).toString("hex");
(0,core.exportVariable)("TURBO_API", `http://127.0.0.1:${port}`);
(0,core.exportVariable)("TURBO_TOKEN", turboToken);
(0,core.exportVariable)("TURBO_TEAM", `team_${teamId}`);
(0,core.exportVariable)("TURBO_TOKEN", token);
(0,core.exportVariable)("TURBO_TEAM", teamId);
const subprocess = (0,external_child_process_namespaceObject.spawn)("node", [(0,external_path_.resolve)(__dirname, "../start_and_log")], {
detached: true,
stdio: "ignore",
env: {
...process.env,
PORT: port.toString(),
TURBO_TOKEN: turboToken,
TURBO_TOKEN: token,
STORAGE_PROVIDER: storageProvider,
STORAGE_PATH: storagePath,
},
});
const pid = subprocess.pid?.toString();
subprocess.unref();
try {
await (0,tcp_port_used/* waitUntilUsed */.BZ)(port, 500, 20000);
await (0,tcp_port_used/* waitUntilUsed */.BZ)(port, 250, 5000);
(0,core.info)("Spawned Turbo Cache Server:");
(0,core.info)(` PID: ${pid}`);
(0,core.info)(` Listening on port: ${port}`);
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
},
"dependencies": {
"@actions/core": "^1.10.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"get-port": "^6.1.2",
"indent-string": "^5.0.0",
"prettier": "^2.8.4",
"tcp-port-used": "^1.0.2"
Expand All @@ -28,7 +25,12 @@
"@tsconfig/node16": "^1.0.3",
"@types/node": "^18.13.0",
"@types/tcp-port-used": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"@vercel/ncc": "^0.36.1",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5",
"rimraf": "^4.1.2",
"ts-node": "^10.9.1",
Expand Down
Loading

0 comments on commit 363c213

Please # to comment.