Skip to content

Commit

Permalink
feat: create script to download and then execute local binary tools (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian authored Sep 18, 2023
1 parent 1b94017 commit a7a9e50
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 0 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/deploy-binary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Deploy a binary for team to use

on:
push:
branches: [main]

# Workflow to compile a binary of the script and upload it to a github release. This allows team members to download the binary and run it on their
# development machines without needing to install deno or any other language.
#
# Deployment strategy for this project is to only maintain a "latest" release version. Why?
# 1. This tool is currently used internally by our team and therefore, no need to maintain multiple versions.
# 2. Deno compiled binary files can be larger in size, no use keeping files in the repo that will probably not be used.
jobs:
update-latest-binary:
runs-on: ubuntu-latest
permissions:
contents: write # to be able to push git tags and create github releases
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x

- name: Compile script into a macOS m1 binary
run: |
deno compile --allow-all --target aarch64-apple-darwin binny.ts
mv binny binny-macos-m1
- name: Compile script into a Linux binary
run: |
deno compile --allow-all --target x86_64-unknown-linux-gnu binny.ts
mv binny binny-linux-x86_64
- name: Create a git tag 'latest' and a GitHub release 'latest' to attach compiled binary to.
uses: ncipollo/release-action@v1
with:
artifacts: "binny-macos-m1,binny-linux-x86_64" # upload these binary files to the release so team members can easily download it.
tag: "latest" # create a git tag 'latest'
commit: "main" # create a git tag 'latest' from the latest commit on the main branch
allowUpdates: true # if 'latest' release already exists, update it.
artifactErrorsFailBuild: true # fail the github action if there is an error trying to upload artifacts. The main point of this github release is to upload the binary, so if that fails, we should fail the build.
body: "Compiled binary files are attached for convenient download and executing in your development machine and CI server" # body of the github release.
makeLatest: true # make this release the latest release.
replacesArtifacts: true # replace the artifacts in the existing github release, if any exists.
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Tests

on: [pull_request]

jobs:
test-script-can-compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Compile script into a macOS m1 binary
run: deno compile --allow-all --target aarch64-apple-darwin binny.ts
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# don't commit the built binary
binny
binny-*
77 changes: 77 additions & 0 deletions binny.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* This is a script designed to run on a local development machine as well as CI server to install and run CLI tools that are used by this project.
* The problem that we need to solve is that iOS/Swift development tools today don't have a good way to install and execute versioned binary tools used for development.
* Instead, tools are mostly installed via Homebrew or manually by all developers and CI servers. This then causes the issue of everyone being on a different version of each tool, causing issues when using the tools.
*
* This tool as the following responsibilities:
* 1. Defines what tools this project uses and what version of the tool that needs to be used.
* 2. Downloads the tool automatically for you if it's not installed already. Or, if the tool is out-of-date, it will download the latest version of the tool.
* 3. Runs the tool with the correct version.
*
* If you want to update the version of a tool to a newer version, simply update the version number in the toolsUsedInProject array below and push a commit to the repo. Then, all developers and CI servers will automatically download the new version the next time they execute this script.
*
* This script is written in TypeScript and uses Deno as the runtime. In order to execute this script, you need to have Deno installed (brew install deno), or we can compile this script into a binary and run it that way.
* Command to run this script: deno run --allow-net --allow-read --allow-write --allow-run binny.ts swiftlint --strict
* Command to compile this script into a binary: make compile_run_tool
*/

interface Tool {
name: string;
version: string;
downloadUrl: string;
pathToBinaryInsideZip: string;

[propName: string]: string // added to allow the interface to work with stringFormat(). Fixes error, "Index signature for type 'string' is missing in type 'Tool'"
}

import {existsSync as doesFileExist } from "https://deno.land/std@0.201.0/fs/mod.ts";
import { format as stringFormat } from "https://deno.land/x/format@1.0.1/mod.ts";
import { decompress as unzip } from "https://deno.land/x/zip@v1.2.5/mod.ts";
import {parse as parseYaml} from "https://deno.land/std@0.201.0/yaml/mod.ts";

const nameOfToolToRun = Deno.args[0] // get the name of the tool to run from the command line arguments. Example value: 'swiftlint'
const toolsUsedInProject: Tool[] = parseYaml(Deno.readTextFileSync("binny-tools.yml")) as Tool[]; // Read binny-tools.yml file which is used to define what tools are used in this project.
const tool: Tool = toolsUsedInProject.find(tool => tool.name === nameOfToolToRun)!

const rootPathToTool = await assertToolInstalled()
await runCommand(rootPathToTool)

async function assertToolInstalled(): Promise<string> {
Deno.mkdirSync("./tools", { recursive: true }); // make the tools directory in case it does not exist. This is where all the tools will be installed

// This is the file path where we expect the tool to be installed. Each tool has it's own unique directory within the tools/ directory. It's also important to version the directory so that
// the tool can download newer versions if needed.
const toolInstallLocation = `./tools/${tool.name}_${tool.version}`;

// Check if we have already installed the tool.
if (doesFileExist(toolInstallLocation, { isDirectory: true })) {
// Tool exists.
return toolInstallLocation
}

// Tool does not exist so it must be installed.
// Each tool gets downloaded from GitHub as a zip file. We need to download the zip file and then unzip the file into a specific directory. Once it is unzipped, we consider the tool installed.
console.log(`${tool.name} is not yet installed or is out-of-date. Installing ${tool.name}, version: ${tool.version}...`)

const downloadZipUrl = stringFormat(tool.downloadUrl, tool); // the download URL is dynamic. Use string format to inject variables into the download URL string to get a versioned URL for the zip file.
const downloadedZipFilePath = `/tmp/${tool.name}_${tool.version}.zip`; // We save the tool into the /tmp directory as we do not need it after we unzip the file.

// download zip file into the /tmp directory
const response = await fetch(downloadZipUrl);
Deno.writeFileSync(downloadedZipFilePath, new Uint8Array(await response.arrayBuffer()));

// unzip the file into the tools directory.
await unzip(downloadedZipFilePath, toolInstallLocation);

console.log(`Downloaded ${tool.name}, version ${tool.version}`);

return toolInstallLocation
}

// rootPathToTool - example: ./tools/swiftlint_0.39.2 where the tool will be located inside of this given directory.
async function runCommand(rootPathToTool: string) {
const command = `${rootPathToTool}/${tool.commandToRun}` // example result: ./tools/sourcery_0.39.2/bin/sourcery which is the full path to the binary to execute
const argsToSendToCommand = Deno.args.slice(1); // remove the first argument as it is the name of the tool to run. But we allow passing other args to the command.

await new Deno.Command(command, { args: argsToSendToCommand, stdout: "inherit", stderr: "inherit" }).output();
}

0 comments on commit a7a9e50

Please sign in to comment.