Skip to content

Instantly share code, notes, and snippets.

@carlosefonseca
Last active October 8, 2020 00:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlosefonseca/eafee9e40d50dd158cf8e40a62ba25e4 to your computer and use it in GitHub Desktop.
Save carlosefonseca/eafee9e40d50dd158cf8e40a62ba25e4 to your computer and use it in GitHub Desktop.
// Follow this to install the travis cli: https://github.com/travis-ci/travis.rb#installation
// Then generate the API token by running the following commands and place the token in the var
// travis login --pro
// travis token --pro
let travis_token = "insert token"
// set to your GitHub username to appear in bold
let github_user = ""
// The slug of the repo to display
let repo = "owner/repo"
//////////////////////////////
let data1 = await loadItems()
let data = transformData(data1)
let widget = await createWidget(data)
if (!config.runsInWidget) {
await widget.presentSmall()
}
Script.setWidget(widget)
Script.complete()
async function createWidget(builds) {
// I want to find a better way to do this…
// smallBuilds returns an array and we want to flatten it into the stack
let layout = [space, smallBuilds(builds), [[space, updated]]].flatMap(x => x)
let w = new ListWidget()
buildLayout(layout, w)
return w
}
// Place in a layout array to force the orientation.
function H(stack) { stack.layoutHorizontally() }
function V(stack) { stack.layoutVertically() }
// Creates an image for the specified state
function stateIcon(state) {
return function _stateIcon(stack) {
addStateIconToStack(stack, state)
}
}
// Creates a small image for the specified state
function stateIconSmall(state) {
return function _stateIcon(stack) {
addStateIconToStack(stack, state, 10)
}
}
// Flexible space. Use without parentheses.
function space(column) { column.addSpacer() }
// Fixed size space.
function spaced(size) {
return function _spaced(stack) { stack.addSpacer(size) }
}
function text(text, font = Font.systemFont(10)) {
return function _text(stack) {
let txt = stack.addText(text)
txt.font = font
}
}
function boldText(txt) {
return text(txt, Font.boldSystemFont(10))
}
function tinyText(owner) {
return text(owner, Font.systemFont(8))
}
function user(name) {
return text(name, name === github_user ? Font.boldSystemFont(8) : Font.systemFont(8))
}
// Creates the jobs per stage grid. Returns an array of stages,
// each stage is an array that contains spaces and icons for each job.
function smallStages(stages) {
return stages.map(stage =>
Object.values(stage.jobs).flatMap(j => [spaced(1), stateIconSmall(j)]))
}
// Returns the full line for a build.
function smallBuild(build) {
const prAndBuildStack = [
boldText(build.short),
tinyText(build.number)
]
const leftPart = [
[stateIcon(build.state), space, prAndBuildStack, space],
user(build.owner)
]
return [leftPart, smallStages(build.stages)]
}
// Returns an array of build lines.
function smallBuilds(builds) {
return builds.flatMap(b => [smallBuild(b), space])
}
// Adds a date for the timestamp the widget was updated
function updated(stack) {
let date = stack.addDate(new Date())
date.applyTimerStyle()
date.font = Font.systemFont(8)
}
// Function that takes a widget/stack object and transforms a layout structure into widget elements.
// Each nested array in the layout will create a stack with the inverse orientation from the container.
// Each function in the layout will be called with the parent stack and should modify the stack by adding elements or modifying stack properties.
function buildLayout(layout, origin, isVertical = true) {
return layout.map(row => {
if (Array.isArray(row)) {
let stack = origin.addStack()
if (isVertical) {
stack.layoutHorizontally()
} else {
stack.layoutVertically()
}
return buildLayout(row, stack, !isVertical)
} else {
row(origin)
}
})
}
// Adds a state icon to a stack
function addStateIconToStack(stack, state, size = 20) {
let img = stack.addImage(convertStateToImg(state))
img.tintColor = new Color(convertStateToColor(state))
img.resizable = true
img.imageSize = new Size(size, size)
}
// Returns an image for the specified state
function convertStateToImg(txt) {
return SFSymbol.named(convertStateToSFSymbolName(txt)).image
}
// Converts a state into an SFSymbol name
function convertStateToSFSymbolName(txt) {
switch (txt) {
case "canceled": return "stop.circle"
case "started": return "play.circle.fill"
case "failed": return "multiply.circle.fill"
case "passed": return "checkmark.circle.fill"
case "received":
case "queued":
case "created": return "ellipsis.circle"
}
return txt
}
// Converts a state into a HEX color code
function convertStateToColor(txt) {
switch (txt) {
case "canceled": return "#9d9d9d"
case "failed": return "#db4545"
case "passed": return "#39aa56"
case "started":
case "received":
case "queued":
case "created": return "#cdb62c"
}
return txt
}
// Converts a duration into hours/minutes text
function durationToText(seconds) {
let h = Math.floor(seconds / 60 / 60)
let m = Math.floor(seconds / 60 % 60)
h = h > 0 ? `${h}h` : ""
return `${h}${m}m`
}
// When used as `[builds].filter(onlyUnique)`, it discards any builds that are similar
function onlyUnique(value, index, self) {
return self.findIndex(i => i.pull_request_number === value.pull_request_number) === index;
}
// Downloads builds from Travis
async function loadItems() {
let url = `https://api.travis-ci.com/repo/${encodeURIComponent(repo)}/builds?include=build.jobs,build.stages&limit=5`
let req = new Request(url)
req.headers = { "Authorization": `token ${travis_token}`, "Travis-API-Version": "3" }
let json = await req.loadJSON()
return json
}
// Simplifies the Travis build data to be consumed by the createWidget method
function transformData(data) {
return data.builds.filter(onlyUnique).slice(0, 3).map(build => {
let stages = {}
build.stages.forEach(s => {
stages[s.number] = {
name: s.name,
state: s.state,
jobs: {}
}
})
build.jobs.forEach(j => {
stages[j.stage.number].jobs[j.number] = j.state
})
stages = Object.values(stages);
let duration = null
if (build.duration) {
duration = build.duration
} else if (build.started_at) {
duration = (new Date() - new Date(build.started_at)) / 1000
}
duration_txt = duration ? durationToText(duration) : ""
title = null
if (build.event_type == "pull_request") {
title = `#${build.pull_request_number} ${build.pull_request_title}`
} else {
title = build.commit.message.split("\n")[0]
}
let buildUser = build.created_by.login
out = {
title: title,
short: title.split(" ")[0],
number: build.number,
state: build.state,
updated_at: build.updated_at,
stages: stages,
url: `https://travis-ci.com/github/${build.repository.slug}/builds/${build.id}`,
duration: duration_txt,
started_at: build.started_at,
owner: buildUser,
mine: buildUser === github_user
}
return out
})
}
// For testing purposes. Loads a file simulating the Travis API response
async function loadTestData() {
const files = FileManager.iCloud()
const locationPath = files.joinPath(files.documentsDirectory(), "travis-test-data.json")
await files.downloadFileFromiCloud(locationPath)
if (files.fileExists(locationPath)) {
let data = files.readString(locationPath)
return JSON.parse(data)
}
return null
}
@carlosefonseca
Copy link
Author

Added ⏳ for received and queued states. Added time since the build started. Tapping the widget opens the travis build page.

@carlosefonseca
Copy link
Author

Time since the build started should not display if the build hasn't stated yet.

@carlosefonseca
Copy link
Author

Changed the state icons again…

@carlosefonseca
Copy link
Author

Handles push builds.

@carlosefonseca
Copy link
Author

Updated to use the new Scriptable version with Stacks and SFSymbols

@carlosefonseca
Copy link
Author

IMG_4900
This is the previous version and the current version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment