Search code examples
javascriptgithubcorsgithub-pages

Use asset in a github release to modify a github pages website


I'm making a website to display a project that I've got. About once a week, this project generates a new release which has a file machine_friendly.csv. This file just contains some generated data (and is different every time).

I want to create a github pages website which a user can navigate to and see a prettified version of the machine_friendly.csv file. The problem is that github's CORS doesn't allow me to directly download the file. For example, this doesn't work:

React.useEffect(() => {
    fetch('https://github.com/beyarkay/eskom-calendar/releases/download/latest/machine_friendly.csv')
    .then(response => {
        if (response.ok) return response.json()
        throw new Error('Network response was not ok.')
    })
    .then(data => console.log(data.contents))
    .catch(err => console.log(err));
}, []);

and gives CORS error messages:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at 
https://github.com/beyarkay/eskom-calendar/releases/download/latest/machine_friendly.csv. 
(Reason: CORS header ‘Access-Control-Allow-Origin’ missing). 
Status code: 302.

Is there any way of getting around this? I've tried uploading the file to a pastebin as well as to the github release, but none of the pastebins I've tried enable CORS for free. I've also tried some CORS proxies, but they either take too long or just don't work anymore. I also tried using github's API, but that also gives CORS problems.

Is there a service online that I can upload my small (<1MB) file to and download it later via javascript?


Solution

  • So I kind of found a solution, although it's not great. I ended up messaging the maintainer of dpaste.com who very kindly enabled CORS, so I can now upload my file to dpaste and download it again.

    The GH action I've got looks something like:

    jobs:
      build-and-publish-calendars:
        runs-on: ubuntu-latest
        steps:
        ...
        - name: POST machine_friendly.csv to dpaste.org
          run: |
            cat calendars/machine_friendly.csv | curl -X POST -F "expires=31536000" -F 'format=url' -F 'content=<-' https://dpaste.org/api/ > pastebin.txt
    
        - name: Write pastebin link to GH variable
          run: |
            echo "pastebin=$(cat pastebin.txt)/raw" >> $GITHUB_OUTPUT
          id: PASTEBIN
    

    And then later (funky hacks incoming) I include that pastebin link in the description of my release using a personal fork of IsaacShelton/update-existing-release (I maintain the personal fork for some performance improvements which are unrelated to this issue). The step looks like:

        ...
        - name: Update latest release with new calendars
          uses: beyarkay/update-existing-release@master
          with:
            token: ${{ secrets.GH_ACTIONS_PAT }}
            release: My Updated Release
            updateTag: true
            tag: latest
            replace: true
            files: ${{ steps.LS-CALENDARS.outputs.LS_CALENDARS }}
            body: "If you encounter CORS issues, you'll need to use this [pastebin link](${{ steps.PASTEBIN.outputs.pastebin }})"
    

    And in my website, I have a snippet like:

    const downloadFromRelease = async () => {
    
        // We need octokit in order to download the metadata about the release
        const octokit = new Octokit({
            auth: process.env.GH_PAGES_ENV_PAT || process.env.GH_PAGES_PAT
        })
        const desc = await octokit.request("GET /repos/beyarkay/eskom-calendar/releases/72143886", {
            owner: "beyarkay",
            repo: "eskom-calendar",
            release_id: "72143886"
        }).then((res) => res.data.body)
    
        // Here's some regex that matches the markdown formatted link
        const pastebin_re = /\[pastebin link\]\((https:\/\/dpaste\.org\/(\w+)\/raw)\)/gm
        const match = desc.match(pastebin_re)
    
        // Now that we've got a match, query that URL to get the data
        const url = match[0].replace("[pastebin link](", "").replace(")", "")
        console.log(`Fetching data from ${url}`)
        return fetch(url)
            .then(res => res.text())
            .then(newEvents => {
                // And finally parse the URL data into Event objects
                const events: Event[] = newEvents.split("\n").map( line => ( {
                    area_name: line.split(",")[0],
                    start:  line.split(",")[1],
                    finsh:  line.split(",")[2],
                    stage:  line.split(",")[3],
                    source: line.split(",")[4],
                }))
                return events
            })
    }
    

    Tying it all together, I can use this snippet to actually setState in react:

    
       // Define a Result type that represents data which might not be ready yet
        type Result<T, E> = { state: "unsent" }
            | { state: "loading" }
            | { state: "ready", content: T }
            | { state: "error", content: E }
        // The events start out as being "unsent"
        const [events, setEvents] =
            React.useState<Result<Event[], string>>( { state: "unsent" })
    
        // If they're unsent, then send off the request
        if (events.state === "unsent") {
            downloadMachineFriendly().then(newEvents => {
    
                // When the data comes back, use the `setEvents` hook
                setEvents({
                    state: "ready",
                    content: newEvents,
                })
    
            // If there's an error, store the error
            }).catch(err => setEvents({state: "error", content: err}))
        }