Skip to content

Instantly share code, notes, and snippets.

@ingmarh
Created January 9, 2025 13:14
Show Gist options
  • Save ingmarh/4651d4760e348f6358bc884f12ff09d3 to your computer and use it in GitHub Desktop.
Save ingmarh/4651d4760e348f6358bc884f12ff09d3 to your computer and use it in GitHub Desktop.
GitHub Artifact Fetcher
# Example build and run:
# docker build -t github_artifact_fetcher .
# docker run -e "GITHUB_TOKEN=$(gh auth token)" -v /var/www/artifacts:/artifacts -p 3000:3000 --name github_artifact_fetcher github_artifact_fetcher
FROM node:alpine
RUN apk add --no-cache curl jq
ENV PORT=3000 DEST_DIR=/artifacts
WORKDIR /app
ADD server.mjs .
CMD ["node", "./server.mjs"]
EXPOSE $PORT
import { createServer } from 'node:http'
import { exec } from 'node:child_process'
import { readdirSync } from 'node:fs'
// GitHub Artifact Fetcher -
// Downloads a GitHub artifact from a repository and saves it to a local directory.
//
// Example requests:
// curl -X POST -d '{"repo":"owner/repo","artifactId":1234}' http://localhost:3000/
// curl -X POST -d '{"repo":"owner/repo","artifactId":1234,"destName":"docs"}' http://localhost:3000/
//
// Note: "owner/repo" must be existing directories in `DEST_DIR`.
const TOKEN = process.env.GITHUB_TOKEN
const PORT = process.env.PORT
const DEST_DIR = process.env.DEST_DIR?.replace(/\/$/, '')
if (!TOKEN || !PORT || !DEST_DIR) {
console.error('Missing required environment variables')
process.exit(1)
}
const headers = `-H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${TOKEN}" -H "X-GitHub-Api-Version: 2022-11-28"`
const allowedRepos = getDirnames(DEST_DIR).flatMap(owner =>
getDirnames(`${DEST_DIR}/${owner}`).map(repo => `${owner}/${repo}`)
)
console.log(`Starting GitHub Artifact Fetcher server for repositories:\n ${allowedRepos.join('\n ')}\n`)
const server = createServer((req, res) => {
let params = []
req
.on('data', chunk => {
params.push(chunk)
})
.on('end', () => {
try {
params = JSON.parse(Buffer.concat(params).toString())
} catch (e) {
params = {}
}
const isValidRequest = (
req.method === 'POST' && req.url === '/' &&
(typeof params.repo === 'string' ? allowedRepos.includes(params.repo) : false) &&
Number.isInteger(params.artifactId) &&
(params.destName ? /^[a-zA-Z0-9-_.]+$/.test(params.destName) : true)
)
if (!isValidRequest) {
console.error('Invalid request', { url: req.url, method: req.method, params })
return
}
const apiUrl = `https://api.github.com/repos/${params.repo}/actions/artifacts/${params.artifactId}`
console.log(`Getting artifact ${params.artifactId} from ${params.repo}`)
exec(`curl -L ${headers} ${apiUrl} | jq -r .archive_download_url`, (err, stdout) => {
if (err) {
console.error(err)
return
}
const downloadUrl = stdout.trim()
if (downloadUrl === 'null') {
console.error(`Artifact ${params.artifactId} for ${params.repo} not found`)
return
}
console.log(`Downloading artifact from ${downloadUrl}`)
const destPath = `${DEST_DIR}/${params.repo}/${params.destName || params.artifactId}`
exec(`curl -L ${headers} -o ${destPath}.zip ${downloadUrl}`, (err) => {
if (err) {
console.error(err)
return
}
exec(`unzip -o ${destPath}.zip -d ${destPath}`, (err) => {
if (err) {
console.error(err)
return
}
console.log(`Artifact ${params.artifactId} saved to ${destPath}`)
})
})
})
})
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end()
})
server.listen(PORT, '0.0.0.0', () => {
console.log(`Listening on 0.0.0.0:${PORT}/`)
})
function getDirnames(path) {
return readdirSync(path, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment