Repository navigation
Leave Tracker #1066
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
| name: Leave Tracker | |
| # Parses the committed leave-tracker xlsx and publishes the normalized report to the | |
| # `leave-tracker/report` branch, which the Netlify dashboard reads at runtime. | |
| # | |
| # HARDENED FLOW (simple + reproducible): this AUTO-RUNS whenever the source workbook or an | |
| # override file changes on `main` (e.g. an "Add person" PR merges), so the dashboard updates | |
| # with no manual step. The PI is inferred from the xlsx filename, so no inputs are required. | |
| # You can still trigger it by hand (workflow_dispatch) to force a refresh or override the PI. | |
| # | |
| # HOURLY GOOGLE SHEETS SYNC: the team edits leave in a Google Sheet, and for a long time the only | |
| # way that reached the dashboard was someone downloading the .xlsx and committing it by hand — so | |
| # the dashboard silently showed month-old data. When the repo variable LEAVE_SHEET_ID is set, the | |
| # hourly run re-exports that sheet as .xlsx and parses it, and the committed workbook in | |
| # leave/source/ becomes a FALLBACK for when the export is unreachable. Nothing is committed to | |
| # `main` by this job; only the report branch moves. | |
| on: | |
| schedule: | |
| - cron: "0 * * * *" # hourly: re-export the Google Sheet (see LEAVE_SHEET_ID below) | |
| push: | |
| branches: [main] | |
| paths: | |
| - "leave/overrides/**" | |
| - "leave/source/**" | |
| - ".github/workflows/leave-tracker.yml" | |
| workflow_dispatch: | |
| inputs: | |
| xlsx-path: | |
| description: "Path to the leave-tracker xlsx" | |
| type: string | |
| required: false | |
| default: "leave/source/DSE Leave Tracker - 26.4.xlsx" | |
| pi: | |
| description: "Program Increment label (blank = inferred from the xlsx filename)" | |
| type: string | |
| required: false | |
| default: "" | |
| year: | |
| description: "Override inferred year (blank = derive from PI)" | |
| type: string | |
| required: false | |
| default: "" | |
| risk-threshold: | |
| description: "Default fraction of a team OUT to flag (0-1)" | |
| type: string | |
| required: false | |
| default: "0.30" | |
| permissions: | |
| contents: write # publish the report to the leave-tracker/report branch | |
| # One report run at a time; a newer override supersedes an in-flight run. | |
| concurrency: | |
| group: leave-tracker-report | |
| cancel-in-progress: true | |
| env: | |
| DASHBOARD_URL: "https://veda-leave-dashboard.netlify.app" | |
| jobs: | |
| leave-tracker: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: "1 · Checkout repository" | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # full history so the report branch can be based/pushed | |
| # Overwrites the workspace copy of the workbook only — never committed to `main`. Keeping the | |
| # same PATH means the PI keeps being inferred from the committed filename, and a failed or | |
| # revoked export degrades to the committed workbook instead of breaking the run. | |
| - name: "1b · Refresh the workbook from Google Sheets" | |
| if: vars.LEAVE_SHEET_ID != '' | |
| env: | |
| SHEET_ID: ${{ vars.LEAVE_SHEET_ID }} | |
| XLSX: ${{ github.event.inputs.xlsx-path || 'leave/source/DSE Leave Tracker - 26.4.xlsx' }} | |
| run: | | |
| set -uo pipefail | |
| URL="https://docs.google.com/spreadsheets/d/${SHEET_ID}/export?format=xlsx" | |
| # A sheet that loses link-sharing returns 200 + an HTML sign-in page, which `curl -f` | |
| # happily accepts — so prove the download is really a zip (xlsx) before trusting it. | |
| if curl -fsSL --max-time 120 -o "$RUNNER_TEMP/sheet.xlsx" "$URL" \ | |
| && python3 -c "import sys,zipfile; sys.exit(0 if zipfile.is_zipfile(sys.argv[1]) else 1)" \ | |
| "$RUNNER_TEMP/sheet.xlsx"; then | |
| cp "$RUNNER_TEMP/sheet.xlsx" "$XLSX" | |
| echo "Refreshed from Google Sheets: $(wc -c < "$XLSX") bytes." | |
| else | |
| echo "::warning::Google Sheets export failed or was not an .xlsx — using the committed workbook." | |
| fi | |
| - name: "2 · Parse the leave tracker (reusable action)" | |
| id: run | |
| uses: ./leave | |
| with: | |
| # On `push` github.event.inputs.* are empty strings, so fall back to defaults here | |
| # (an empty xlsx-path would otherwise fail the file check). pi stays blank on purpose — | |
| # the generator infers it from the xlsx filename. | |
| xlsx-path: ${{ github.event.inputs.xlsx-path || 'leave/source/DSE Leave Tracker - 26.4.xlsx' }} | |
| pi: ${{ github.event.inputs.pi }} | |
| year: ${{ github.event.inputs.year }} | |
| risk-threshold: ${{ github.event.inputs.risk-threshold || '0.30' }} | |
| out-dir: reports | |
| - name: "3 · Job summary" | |
| run: | | |
| echo "people=${{ steps.run.outputs.people }} teams=${{ steps.run.outputs.teams }} leave-days=${{ steps.run.outputs.leave-days }}" | |
| { | |
| echo "" | |
| echo "🗓️ **Leave dashboard →** ${DASHBOARD_URL}" | |
| echo "_${{ steps.run.outputs.people }} people · ${{ steps.run.outputs.teams }} teams · ${{ steps.run.outputs.leave-days }} out-days_" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: "4 · Upload report artifact" | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: leave-tracker-report | |
| path: reports/ | |
| - name: "5 · Publish report to the leave-tracker/report branch (accumulate by PI)" | |
| run: | | |
| BR="leave-tracker/report" | |
| mkdir -p "$RUNNER_TEMP/out" && cp -R reports/. "$RUNNER_TEMP/out/" # stash before switching branches | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git fetch origin "$BR" || true | |
| if git rev-parse --verify --quiet "origin/$BR"; then | |
| git checkout -f -B "$BR" "origin/$BR" # continue history: prior PIs persist | |
| else | |
| git checkout -f -B "$BR" | |
| fi | |
| mkdir -p reports | |
| # Keep only slugged leave reports + the index (drop main's other samples / fixed names). | |
| find reports -maxdepth 1 -type f \ | |
| ! -name 'leaves_*.csv' ! -name 'leaves_*.json' ! -name 'leave_coverage_*.json' \ | |
| ! -name 'index.json' -delete 2>/dev/null || true | |
| # Copy in THIS run's slugged files (new content wins for the same slug), incl. the manifest. | |
| MAN="$RUNNER_TEMP/out/leave_manifest.json" | |
| cp "$MAN" "reports/leave_manifest.json" | |
| for k in csv json coverage; do | |
| f="$(jq -r ".files.$k" "$MAN")"; cp "$RUNNER_TEMP/out/$f" "reports/$f" | |
| done | |
| # Upsert this run's entry into reports/index.json (dedupe by slug, newest first). | |
| [ -f reports/index.json ] || echo '[]' > reports/index.json | |
| ENTRY="$(jq -c '{slug,pi,year,generated,files,stats}' "$MAN")" | |
| jq --argjson e "$ENTRY" '(map(select(.slug != $e.slug))) + [$e] | sort_by(.generated) | reverse' \ | |
| reports/index.json > reports/index.json.tmp && mv reports/index.json.tmp reports/index.json | |
| git add -f reports | |
| # The manifest carries a fresh `generated` timestamp on every run, so once this job runs | |
| # hourly an idle run would still "change" a file and commit — 24 empty commits a day. If | |
| # the ONLY staged file is the manifest and the ONLY differing lines in it are "generated", | |
| # nothing about the leave data actually moved: leave the branch alone. | |
| CHANGED="$(git diff --cached --name-only)" | |
| if [ "$CHANGED" = "reports/leave_manifest.json" ] \ | |
| && ! git diff --cached -U0 -- reports/leave_manifest.json \ | |
| | grep -E '^[+-][^+-]' | grep -qv '"generated"'; then | |
| echo "No data change for $BR — only the generated timestamp moved." | |
| elif [ -z "$CHANGED" ]; then | |
| echo "No report changes for $BR." | |
| else | |
| git commit -m "Leave tracker report — $(jq -r .slug "$MAN") (run ${{ github.run_id }})" | |
| git push origin "$BR" | |
| echo "Report pushed to branch: $BR" | |
| fi | |
| - name: "6 · Restore the default checkout" | |
| if: always() | |
| run: git checkout -f "${{ github.sha }}" 2>/dev/null || git checkout -f main |