Created
January 9, 2025 13:14
-
-
Save ingmarh/4651d4760e348f6358bc884f12ff09d3 to your computer and use it in GitHub Desktop.
GitHub Artifact Fetcher
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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