diff --git a/.github/workflows/devel-image.yml b/.github/workflows/devel-image.yml new file mode 100644 index 00000000000..6818a7c0e90 --- /dev/null +++ b/.github/workflows/devel-image.yml @@ -0,0 +1,73 @@ +# Create a Docker image for development and publish it to GitHub registry. +name: Create Development Image + +on: + pull_request: + branches: + - master + +jobs: + # Format code + format: + name: Format code + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Checkout master + run: git checkout master + - name: Format code + run: | + npm install -g dprint + mkdir -v -p /home/runner/.cache/dprint/cache + npx dprint fmt --config .github/config/dprint.json + - name: Publish formatted code + run: | + set +e + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add --all + git commit -m "chore: code formatting" + git push + set -e + + update-indexes: + name: Publish rebuilt metrics indexes + runs-on: ubuntu-latest + needs: [ format ] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup NodeJS + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Pull changes + run: | + git checkout master + git pull + - name: Setup metrics + run: npm ci + - name: Publish rebuild metrics indexes + run: npm run build -- publish + + docker-devel: + name: Publish devel to GitHub registry + runs-on: ubuntu-latest + needs: [ update-indexes ] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Login to GitHub registry + run: echo ${{ github.token }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build docker image + run: docker build -t ghcr.io/${{ github.actor }}/lowlighter-github-metrics:devel . + + - name: Publish to GitHub registry + run: docker push ghcr.io/${{ github.actor }}/lowlighter-github-metrics:devel diff --git a/Dockerfile b/Dockerfile index 3ff5209a48a..4d9cd970970 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,28 +6,28 @@ COPY . /metrics WORKDIR /metrics # Setup -RUN chmod +x /metrics/source/app/action/index.mjs \ - # Install latest chrome dev package, fonts to support major charsets and skip chromium download on puppeteer install - # Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker - && apt-get update \ - && apt-get install -y wget gnupg ca-certificates libgconf-2-4 \ +RUN chmod +x /metrics/source/app/action/index.mjs +# Install latest chrome dev package, fonts to support major charsets and skip chromium download on puppeteer install +# Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker +RUN apt-get -q update \ + && apt-get -q install -y build-essential wget gnupg ca-certificates libgconf-2-4 \ && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update \ - && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 libx11-xcb1 libxtst6 lsb-release --no-install-recommends \ - # Install deno for miscellaneous scripts - && apt-get install -y curl unzip \ - && curl -fsSL https://deno.land/x/install/install.sh | DENO_INSTALL=/usr/local sh \ - # Install ruby to support github licensed gem - && apt-get install -y ruby-full git g++ cmake pkg-config libssl-dev \ - && gem install licensed \ - # Install python for node-gyp - && apt-get install -y python3 \ - # Clean apt/lists - && rm -rf /var/lib/apt/lists/* \ - # Install node modules and rebuild indexes - && npm ci \ - && npm run build + && apt-get -q update \ + && apt-get -q install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 libx11-xcb1 libxtst6 lsb-release --no-install-recommends +# Install deno for miscellaneous scripts +RUN apt-get install -y curl unzip \ + && curl -fsSL https://deno.land/x/install/install.sh | DENO_INSTALL=/usr/local sh +# Install ruby to support github licensed gem +RUN apt-get -q install -y ruby-full git g++ cmake pkg-config libssl-dev \ + && apt-get -q install -y ruby-dev \ + && gem install licensed +# Install python for node-gyp +RUN apt-get -q install -y python3 +# Clean apt/lists +RUN rm -rf /var/lib/apt/lists/* +# Install node modules and rebuild indexes +RUN npm ci && npm run build # Environment variables ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true diff --git a/source/plugins/isocalendar/index.mjs b/source/plugins/isocalendar/index.mjs index 1b6b1711521..11bf5bc5ae5 100644 --- a/source/plugins/isocalendar/index.mjs +++ b/source/plugins/isocalendar/index.mjs @@ -1,13 +1,76 @@ +async function loadNonGitHubContributionsFromSourceFile(options) { + const { login: username } = options + + //load contributions from github public file + + let contributions = [] + try { + const response = await fetch(`https://raw.githubusercontent.com/${username}/${username}/refs/heads/metrics-renders/.contributions/gitlab.json`) + if (response.ok) { + contributions = await response.json() + } + } + catch (error) { + console.error("Failed to fetch contributions from source file", error) + } + + return { + contributions, + //eslint-disable-next-line object-shorthand + getRange: function (from, to) { + //create week chunks + let chunks = [] + for (let start = new Date(from); start < to;) { + const end = new Date(start) + end.setUTCDate(end.getUTCDate() + 7) + + chunks.push( + { + contributionDays: [ + ...this.contributions.filter(entry => { + const date = new Date(entry.date) + + return start <= date && date < end + }) + ] + } + ) + + start = end + } + + return chunks + }, + } +} + +function findTopColorForDay(day, count) { + return Object.keys(day).reduce((acc, key, idx) => { + const { color, contributionCount } = day[key] + if (count === 0 && idx === 0) { + return color + } + if (count !== 0 && contributionCount === 0) { + return acc + } + if (acc === "") { + return color + } + + return acc + }, "") +} + //Setup -export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false, extras = false} = {}) { +export default async function ({ login, data, graphql, q, imports, queries, account }, { enabled = false, extras = false } = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met - if ((!q.isocalendar) || (!imports.metadata.plugins.isocalendar.enabled(enabled, {extras}))) + if ((!q.isocalendar) || (!imports.metadata.plugins.isocalendar.enabled(enabled, { extras }))) return null //Load inputs - let {duration} = imports.metadata.plugins.isocalendar.inputs({data, account, q}) + let { duration } = imports.metadata.plugins.isocalendar.inputs({ data, account, q }) //Compute start day const now = new Date() @@ -18,8 +81,9 @@ export default async function({login, data, graphql, q, imports, queries, accoun start.setUTCHours(-180 * 24) //Ensure start day is a sunday, and that time is set to 00:00:00.000 - if (start.getUTCDay()) + if (start.getUTCDay()) { start.setUTCHours(-start.getUTCDay() * 24) + } start.setUTCMilliseconds(0) start.setUTCSeconds(0) start.setUTCMinutes(0) @@ -27,48 +91,74 @@ export default async function({login, data, graphql, q, imports, queries, accoun //Compute contribution calendar, highest contributions in a day, streaks and average commits per day console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`) - const calendar = {weeks: []} - const {streak, max, average} = await statistics({login, graphql, queries, start, end: now, calendar}) - const reference = Math.max(...calendar.weeks.flatMap(({contributionDays}) => contributionDays.map(({contributionCount}) => contributionCount))) + const calendar = { weeks: [] } + + const { streak, max, average } = await statistics({ login, graphql, queries, start, end: now, calendar }) + const reference = Math.max( + ...calendar.weeks.flatMap( + ({ contributionDays }) => { + const val = contributionDays.map(day => Object.keys(day).map(key => day[key].contributionCount)) + + return val.flat() + }) + ) //Compute SVG console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`) - const size = 6 - let i = 0, j = 0 + const size = 6; + let i = 0; + let j = 0; let svg = ` - - ${ - [1, 2].map(k => ` - - - ${[..."RGB"].map(channel => ``).join("")} - - `) - .join("") - } - ` + + ${[1, 2].map(k => ` + + + ${[..."RGB"].map(channel => ``).join("")} + + `).join("")} + `; + //Iterate through weeks for (const week of calendar.weeks) { - svg += `` - j = 0 + svg += ``; + j = 0; + //Iterate through days for (const day of week.contributionDays) { - const ratio = (day.contributionCount / reference) || 0 - svg += ` - - - - - ` - j++ + const count = Object.keys(day).reduce((acc, key) => acc + day[key].contributionCount, 0); + const ratio = (count / reference) || 0; + + svg += ``; + + const topColor = findTopColorForDay(day, count); + svg += ``; + + let offset = 0; + Object.keys(day).forEach((key, idx) => { + const { color, contributionCount } = day[key]; + + //Find ratio of key + const r = contributionCount / reference || 0; + const shiftBy = r * size; + + svg += ` + + + `; + + offset += shiftBy; + }); + + svg += ""; + j++; } - svg += "" - i++ + svg += ""; + i++; } - svg += "" + svg += ""; //Results - return {streak, max, average, svg, duration} + return { streak, max, average, svg, duration } } //Handle errors catch (error) { @@ -76,39 +166,84 @@ export default async function({login, data, graphql, q, imports, queries, accoun } } -/**Compute max and current streaks */ -async function statistics({login, graphql, queries, start, end, calendar}) { - let average = 0, max = 0, streak = {max: 0, current: 0}, values = [] +/** + * Compute max and current streaks + * */ +async function statistics({ login, graphql, queries, start, end, calendar }) { + let average = 0, max = 0, streak = { max: 0, current: 0 }, values = [] + + const extracontribs = await loadNonGitHubContributionsFromSourceFile({ login }); + //Load contribution calendar for (let from = new Date(start); from < end;) { //Set date range - let to = new Date(from) - to.setUTCHours(+4 * 7 * 24) - if (to > end) - to = end + let to = new Date(from); + to.setUTCHours(+4 * 7 * 24); + if (to > end) { + to = end; + } + //Ensure that date ranges are not overlapping by setting it to previous day at 23:59:59.999 - const dto = new Date(to) - dto.setUTCHours(-1) - dto.setUTCMinutes(59) - dto.setUTCSeconds(59) - dto.setUTCMilliseconds(999) + const dto = new Date(to); + dto.setUTCHours(-1); + dto.setUTCMinutes(59); + dto.setUTCSeconds(59); + dto.setUTCMilliseconds(999); + //Fetch data from api - console.debug(`metrics/compute/${login}/plugins > isocalendar > loading calendar from "${from.toISOString()}" to "${dto.toISOString()}"`) - const {user: {calendar: {contributionCalendar: {weeks}}}} = await graphql(queries.isocalendar.calendar({login, from: from.toISOString(), to: dto.toISOString()})) - calendar.weeks.push(...weeks) + console.debug(`metrics/compute/${login}/plugins > isocalendar > loading calendar from "${from.toISOString()}" to "${dto.toISOString()}"`); + const { user: { calendar: { contributionCalendar: { weeks } } } } = await graphql(queries.isocalendar.calendar({ login, from: from.toISOString(), to: dto.toISOString() })); + + const extra = extracontribs.getRange(from, to); + + //Merge contributions + const entries = weeks.reduce((weekAcc, week, i) => { + const extraWeek = extra[i]; + if (!extraWeek) { + weekAcc.push({ contributionDays: week.contributionDays.map(day => ({ github: day })) }); + return weekAcc; + } + + weekAcc.push({ + contributionDays: [ + ...week.contributionDays.reduce((dayAcc, day, i) => { + const extraDay = extraWeek.contributionDays[i]; + + if (extraDay) { + dayAcc.push({ github: day, gitlab: extraDay }); + } + else { + dayAcc.push({ github: day }); + } + + return dayAcc; + }, []) + ] + }); + + return weekAcc; + }, []) + + calendar.weeks.push(...entries); + //Set next date range start - from = new Date(to) + from = new Date(to); } + //Compute streaks for (const week of calendar.weeks) { for (const day of week.contributionDays) { - values.push(day.contributionCount) - max = Math.max(max, day.contributionCount) - streak.current = day.contributionCount ? streak.current + 1 : 0 - streak.max = Math.max(streak.max, streak.current) + Object.keys(day).forEach(key => { + values.push(day[key].contributionCount); + max = Math.max(max, day[key].contributionCount); + streak.current = day[key].contributionCount ? streak.current + 1 : 0; + streak.max = Math.max(streak.max, streak.current); + }); } } + //Compute average - average = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2).replace(/[.]0+$/, "") - return {streak, max, average} + average = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2).replace(/[.]0+$/, ""); + + return { streak, max, average }; }