6 Commits
Author SHA1 Message Date
adrianbonpin dfa63f5157 ci: use Gitea API for releases (softprops/action-gh-release targets GitHub, fails on Gitea with 405)
build-extension / build (push) Successful in 37s
build-extension / build (pull_request) Successful in 35s
2026-08-19 03:17:42 +08:00
adrianbonpin 7c578d2d16 ci: install PyMuPDF in a venv (PEP 668 externally-managed env blocks system pip)
build-extension / build (push) Failing after 35s
build-extension / build (pull_request) Successful in 32s
2026-08-19 03:10:47 +08:00
adrianbonpin 41e7a4b81e ci: install PyMuPDF in runner; skip PDF text assertions when fitz is unavailable
build-extension / build (push) Failing after 20s
2026-08-19 03:08:46 +08:00
adrianbonpin 92a7408100 docs: move Install section to top of README
build-extension / build (push) Failing after 18s
2026-08-19 03:06:57 +08:00
adrianbonpin c1567aca00 docs: add self-hosted Firefox install route (about:config) while AMO is under maintenance
build-extension / build (push) Failing after 16s
2026-08-19 03:04:34 +08:00
adrianbonpin 40fa9cc9b5 fix(chrome): replace URL.createObjectURL with base64 data URL for downloads (Chrome MV3 service worker has no createObjectURL)
build-extension / build (push) Failing after 15s
2026-08-19 02:57:20 +08:00
4 changed files with 117 additions and 24 deletions
+35 -8
View File
@@ -6,6 +6,11 @@ on:
tags: ['v*'] tags: ['v*']
pull_request: pull_request:
# Ensure the job token can create releases and upload assets.
permissions:
contents: write
releases: write
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -21,6 +26,12 @@ jobs:
- name: Install dev dependencies - name: Install dev dependencies
run: npm ci || npm install run: npm ci || npm install
- name: Install PyMuPDF (PDF text assertions)
run: |
python3 -m venv /tmp/venv
/tmp/venv/bin/pip install --quiet pymupdf
echo "/tmp/venv/bin" >> "$GITHUB_PATH"
- name: Run tests - name: Run tests
run: npm test run: npm test
@@ -36,11 +47,27 @@ jobs:
- name: Publish release on version tag - name: Publish release on version tag
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
# softprops/action-gh-release works on Gitea Actions: it uses the # Gitea-native release: softprops/action-gh-release targets GitHub's API
# automatically-provided GITHUB_TOKEN to create a Release for the tag # and fails on Gitea (405). Instead we call the Gitea API directly with
# and attaches the supplied files. # the runner-provided GITHUB_TOKEN and GITHUB_API_URL.
uses: softprops/action-gh-release@v2 env:
with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
files: dist/*.zip run: |
name: ${{ github.ref_name }} set -euo pipefail
generate_release_notes: true REPO="${{ github.repository }}"
TAG="${{ github.ref_name }}"
API="${GITHUB_API_URL:-https://git.ranio.xyz/api/v1}"
# Create the release for the tag
RELEASE_JSON=$(curl -sS -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":\"ISMIS Helper ${TAG}\"}" \
"${API}/repos/${REPO}/releases")
RELEASE_ID=$(echo "${RELEASE_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# Attach both zips as release assets
for f in dist/*.zip; do
curl -sS -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-F "attachment=@${f}" \
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=$(basename "${f}")"
done
+48 -11
View File
@@ -10,17 +10,6 @@ extension for the USC ISMIS student portal that:
One codebase builds for **both browsers** (see [Build](#build)). One codebase builds for **both browsers** (see [Build](#build)).
## Features
- **Saved login profiles** (stored in `browser.storage.local`) — only one enabled at
a time, with a master on/off toggle.
- **One-click Download PDF** button on the Study Load page.
- PDF settings in the popup:
- Truncate long course descriptions to match the official PDF (default on).
- Show/hide `Year Level`, and set a custom Year Level value.
- **Bundled Segoe UI fonts** for a 1:1 look with the official document.
- Builds and packages automatically via Gitea Actions (zips + versioned releases).
## Install ## Install
> Temporary/development install. For distribution, publish the built zips to > Temporary/development install. For distribution, publish the built zips to
@@ -54,6 +43,54 @@ One codebase builds for **both browsers** (see [Build](#build)).
> For a persistent install, package and publish via Firefox AMO / Chrome Web Store > For a persistent install, package and publish via Firefox AMO / Chrome Web Store
> with the same built zips. > with the same built zips.
### Distribute to real users
#### Firefox (self-hosted, temporary)
> **Note:** Firefox blocks unsigned extensions for normal installs. The proper
> fix is to sign the add-on via **addons.mozilla.org (AMO)** and publish it there
> (or self-host a signed `.xpi`). AMO is currently **under maintenance**, so until
> it returns, the only way to install the unsigned build is to lower Firefox's
> signing check on each user's machine. This is a per-user browser setting and is
> **not** suitable for a broad audience — use it for personal / small-group use.
1. Build the Firefox variant (or download `ismis-helper-firefox.zip` from a release):
```bash
node scripts/build.js firefox
```
2. In Firefox, open `about:config` and accept the warning.
3. Search for `xpinstall.signatures.required` and set it to **`false`**.
4. **Restart Firefox** for the change to take effect.
5. Install the add-on from the release zip: drag `ismis-helper-firefox.zip` into
Firefox (or File → Open File… and select it).
> When AMO is back online, prefer the signed route: publish the same zip to AMO
> (or sign it with `web-ext sign`) so users install it normally without touching
> `about:config`.
#### Chrome (self-hosted)
Chrome does **not** require signing for normal installs. Users can install the
built `ismis-helper-chrome.zip` directly:
1. Download `ismis-helper-chrome.zip` from a release and unzip it.
2. Open `chrome://extensions`, enable **Developer mode**, and click **Load unpacked**.
3. Select the unzipped folder.
> For a store experience, publish the same zip to the Chrome Web Store (one-time
> $5 developer registration).
## Features
- **Saved login profiles** (stored in `browser.storage.local`) — only one enabled at
a time, with a master on/off toggle.
- **One-click Download PDF** button on the Study Load page.
- PDF settings in the popup:
- Truncate long course descriptions to match the official PDF (default on).
- Show/hide `Year Level`, and set a custom Year Level value.
- **Bundled Segoe UI fonts** for a 1:1 look with the official document.
- Builds and packages automatically via Gitea Actions (zips + versioned releases).
## Build ## Build
Requires **Node.js** (the `build.js` script itself is dependency-free; `npm install` Requires **Node.js** (the `build.js` script itself is dependency-free; `npm install`
+16 -4
View File
@@ -10,6 +10,19 @@ function errMsg(err) {
return String((err && err.message) || err); return String((err && err.message) || err);
} }
// Build a download URL from PDF bytes. Chrome MV3 runs the background as a
// service worker, where URL.createObjectURL is NOT available, so we use a
// base64 data URL (works in both Firefox and Chrome). PDFs here are small
// (~150KB), well within data-URL length limits.
function pdfDataUrl(bytes) {
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return 'data:application/pdf;base64,' + btoa(binary);
}
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.type === 'generate-pdf') { if (msg && msg.type === 'generate-pdf') {
generatePdf(msg.data) generatePdf(msg.data)
@@ -32,8 +45,9 @@ function formatPeriod(period) {
async function generatePdf(data) { async function generatePdf(data) {
const bytes = await ISMIStools.buildStudyLoadPdf(data); const bytes = await ISMIStools.buildStudyLoadPdf(data);
const blob = new Blob([bytes], { type: 'application/pdf' }); // Chrome MV3 background is a service worker (no URL.createObjectURL), so we
const url = URL.createObjectURL(blob); // use a base64 data URL that works in both browsers.
const url = pdfDataUrl(bytes);
const header = data.header || {}; const header = data.header || {};
const year = parseInt(data.year, 10); const year = parseInt(data.year, 10);
const period = formatPeriod(data.period || header.semester || ''); const period = formatPeriod(data.period || header.semester || '');
@@ -55,12 +69,10 @@ async function generatePdf(data) {
const onChanged = (delta) => { const onChanged = (delta) => {
if (delta.id === id && delta.state && (delta.state.current === 'complete' || delta.state.current === 'interrupted')) { if (delta.id === id && delta.state && (delta.state.current === 'complete' || delta.state.current === 'interrupted')) {
browser.downloads.onChanged.removeListener(onChanged); browser.downloads.onChanged.removeListener(onChanged);
URL.revokeObjectURL(url);
} }
}; };
browser.downloads.onChanged.addListener(onChanged); browser.downloads.onChanged.addListener(onChanged);
} catch (err) { } catch (err) {
URL.revokeObjectURL(url);
throw err; throw err;
} }
} }
+18 -1
View File
@@ -38,7 +38,19 @@ function extractPdfText(bytes) {
return execFileSync('python3', ['-c', script], { encoding: 'utf8' }); return execFileSync('python3', ['-c', script], { encoding: 'utf8' });
} }
test('buildStudyLoadPdf returns valid PDF bytes', async () => { // PyMuPDF (fitz) is only needed for the text-extraction assertions below. It is
// installed in CI, but if it is missing locally we skip those assertions rather
// than hard-failing.
function fitzAvailable() {
try {
execFileSync('python3', ['-c', 'import fitz'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
test('buildStudyLoadPdf returns valid PDF bytes', async (t) => {
const bytes = await buildStudyLoadPdf(sampleData); const bytes = await buildStudyLoadPdf(sampleData);
assert.ok(bytes instanceof Uint8Array); assert.ok(bytes instanceof Uint8Array);
const head = Buffer.from(bytes.slice(0, 5)).toString('latin1'); const head = Buffer.from(bytes.slice(0, 5)).toString('latin1');
@@ -46,6 +58,11 @@ test('buildStudyLoadPdf returns valid PDF bytes', async () => {
const doc = await PDFLib.PDFDocument.load(bytes); const doc = await PDFLib.PDFDocument.load(bytes);
assert.strictEqual(doc.getPageCount(), 1); assert.strictEqual(doc.getPageCount(), 1);
if (!fitzAvailable()) {
t.skip('PyMuPDF (fitz) not available; skipping PDF text assertions');
return;
}
const text = extractPdfText(bytes); const text = extractPdfText(bytes);
assert.ok(text.includes('CIS 2101'), 'PDF text should contain course code CIS 2101'); assert.ok(text.includes('CIS 2101'), 'PDF text should contain course code CIS 2101');
assert.ok(text.includes("STUDENT'S STUDY LOAD"), 'PDF text should contain the title'); assert.ok(text.includes("STUDENT'S STUDY LOAD"), 'PDF text should contain the title');