Skip to content

Instantly share code, notes, and snippets.

@ingmarh
Created May 14, 2020 12:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ingmarh/b7c5254bdcc6dc3bb30e1601a0c2122b to your computer and use it in GitHub Desktop.
Save ingmarh/b7c5254bdcc6dc3bb30e1601a0c2122b to your computer and use it in GitHub Desktop.
CircleCI Build Artifacts server
export { serve } from 'https://deno.land/std@0.50.0/http/server.ts'
# docker build -t circleci_build_artifacts .
# docker run -it -e CIRCLECI_TOKEN=token --init -p 8091:8091 --name circleci_build_artifacts -d circleci_build_artifacts
FROM hayd/alpine-deno:1.0.0
ENV PORT 8091
EXPOSE $PORT
WORKDIR /app
USER deno
COPY deps.ts .
RUN deno cache deps.ts
ADD . .
RUN deno cache server.js
CMD ["run", "--allow-env", "--allow-net", "server.js"]
import { serve } from './deps.ts'
const port = Number(Deno.env.get('PORT'))
const token = Deno.env.get('CIRCLECI_TOKEN')
if (!token) {
console.error('CIRCLECI_TOKEN environment variable not set.')
Deno.exit(1)
}
const s = serve({ port })
console.log(`Server started on port ${port}`)
// CircleCI build artifacts server. Requires CircleCI API token with "view-builds" scope.
// - View latest build artifacts for a repo with /:repo/latest/artifacts
// - Go to a specific build artifact with /:repo/latest/artifacts/:artifact-path
// - Specify the branch name by appending the "branch" query parameter (default branch: master)
for await (const req of s) {
const [pathname, search] = req.url.split('?')
const [, repo, artifactPath] = pathname.match(/^\/(.+?\/.+?)\/latest\/artifacts\/?(.+)?/) || ''
const branch = new URLSearchParams(search).get('branch') || 'master'
function respondNotFound(body) {
req.respond({
headers: new Headers({ 'Content-Type': 'text/html' }),
status: 404,
body: `<p>${body}</p>`,
})
}
if (!repo) {
respondNotFound('Invalid request. Required format: /:repo/latest/artifacts(/:artifact-path)')
} else {
const latestBuilds = await apiGet(`${repo}/tree/${branch}?filter=successful&shallow=true`)
if (latestBuilds.length) {
// Get the artifacts of the latest "test" job build. Fall back to using the latest build.
const build = latestBuilds.find(build => build.workflows?.job_name === 'test') || latestBuilds[0]
const buildArtifacts = await apiGet(`${repo}/${build.build_num}/artifacts`)
if (buildArtifacts) {
const artifactUrl = buildArtifacts.find(artifact => artifact.path === artifactPath)?.url
if (artifactUrl) {
req.respond({
headers: new Headers({ Location: artifactUrl }),
status: 302,
})
} else if (!artifactPath) {
req.respond({
headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(buildArtifacts),
})
} else {
respondNotFound(`Artifact "${artifactPath}" not found for <a href="${build.build_url}">build ${build.build_num}</a>.`)
}
} else {
respondNotFound(`No build artifacts for <a href="${build.build_url}">build ${build.build_num}</a>.`)
}
} else {
respondNotFound(`Couldn't find any successful builds for branch "${branch}" in ${repo} (or no access).`)
}
}
}
async function apiGet(path) {
const url = new URL(`https://circleci.com/api/v1.1/project/github/${path}`)
console.log(`Fetching ${url.href}`)
url.searchParams.set('circle-token', token)
return fetch(url.href).then(response => response.ok && response.json())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment