Skip to content

Commit eea6a81

Browse files
committed
ci+ui: skip ccplugins workflows without PAT; surface export errors in UI
CI: both ccplugins workflows were erroring on every push because the CCPLUGINS_PAT secret isn't set yet. Added a guard step that checks for the secret and short-circuits the rest of the job cleanly (success, not failure) when absent. Once the secret is added the workflows run normally with no code change needed. UI: export buttons were <a download> links, which saved JSON error responses as `page.pdf` / `page.docx` with a few KB of error text inside — indistinguishable from a broken download. Switched to a fetch-based flow that inspects Content-Type: - application/json → parse, show the error message + pandoc stderr in an alert dialog - binary → save as a blob with the server's filename Now a failing export shows "Export failed (pdf): pandoc not installed…" instead of downloading a 200-byte JSON file the user has to open to understand.
1 parent 9fa4aab commit eea6a81

3 files changed

Lines changed: 87 additions & 4 deletions

File tree

.github/workflows/publish-ccplugins.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,28 @@ jobs:
2727
PLUGIN_NAME: cortex
2828
CATEGORY: "Development Engineering"
2929
steps:
30+
- name: Guard — skip cleanly when CCPLUGINS_PAT is missing
31+
id: guard
32+
env:
33+
PAT: ${{ secrets.CCPLUGINS_PAT }}
34+
run: |
35+
if [ -z "$PAT" ]; then
36+
echo "CCPLUGINS_PAT secret not configured — skipping."
37+
echo "has_pat=false" >> "$GITHUB_OUTPUT"
38+
else
39+
echo "has_pat=true" >> "$GITHUB_OUTPUT"
40+
fi
41+
3042
- name: Checkout Cortex
43+
if: steps.guard.outputs.has_pat == 'true'
3144
uses: actions/checkout@v4
3245
with:
3346
path: cortex
3447
fetch-depth: 0
3548

3649
- name: Resolve release tag
3750
id: tag
51+
if: steps.guard.outputs.has_pat == 'true'
3852
run: |
3953
if [ "${{ github.event_name }}" = "release" ]; then
4054
TAG="${{ github.event.release.tag_name }}"
@@ -45,6 +59,7 @@ jobs:
4559
echo "Release tag: $TAG"
4660
4761
- name: Checkout fork of ccplugins
62+
if: steps.guard.outputs.has_pat == 'true'
4863
uses: actions/checkout@v4
4964
with:
5065
repository: ${{ env.FORK }}
@@ -53,6 +68,7 @@ jobs:
5368
fetch-depth: 0
5469

5570
- name: Sync fork with upstream
71+
if: steps.guard.outputs.has_pat == 'true'
5672
working-directory: ccplugins
5773
env:
5874
GH_TOKEN: ${{ secrets.CCPLUGINS_PAT }}
@@ -68,6 +84,7 @@ jobs:
6884
git push origin main --force-with-lease
6985
7086
- name: Write plugin bundle
87+
if: steps.guard.outputs.has_pat == 'true'
7188
working-directory: ccplugins
7289
run: |
7390
set -euo pipefail
@@ -108,6 +125,7 @@ jobs:
108125
EOF
109126
110127
- name: Update index entry in README
128+
if: steps.guard.outputs.has_pat == 'true'
111129
working-directory: ccplugins
112130
run: |
113131
python3 - <<'PY'
@@ -144,6 +162,7 @@ jobs:
144162
145163
- name: Stage, commit, diff
146164
id: diff
165+
if: steps.guard.outputs.has_pat == 'true'
147166
working-directory: ccplugins
148167
run: |
149168
git add "plugins/${PLUGIN_NAME}" README.md
@@ -164,7 +183,7 @@ jobs:
164183
Automated sync from cdeust/Cortex@${{ github.sha }}."
165184
166185
- name: Push + open (or update) PR
167-
if: steps.diff.outputs.changed == 'true' && inputs.dry_run != true
186+
if: steps.guard.outputs.has_pat == 'true' && steps.diff.outputs.changed == 'true' && inputs.dry_run != true
168187
working-directory: ccplugins
169188
env:
170189
GH_TOKEN: ${{ secrets.CCPLUGINS_PAT }}

.github/workflows/sync-ccplugins-fork.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,29 @@ jobs:
3131
UPSTREAM: ccplugins/awesome-claude-code-plugins
3232
FORK: cdeust/awesome-claude-code-plugins
3333
steps:
34+
- name: Guard — skip cleanly when CCPLUGINS_PAT is missing
35+
id: guard
36+
env:
37+
PAT: ${{ secrets.CCPLUGINS_PAT }}
38+
run: |
39+
if [ -z "$PAT" ]; then
40+
echo "CCPLUGINS_PAT secret not configured — skipping."
41+
echo "Add it at Settings → Secrets and variables → Actions."
42+
echo "has_pat=false" >> "$GITHUB_OUTPUT"
43+
else
44+
echo "has_pat=true" >> "$GITHUB_OUTPUT"
45+
fi
46+
3447
- name: Checkout fork
48+
if: steps.guard.outputs.has_pat == 'true'
3549
uses: actions/checkout@v4
3650
with:
3751
repository: ${{ env.FORK }}
3852
token: ${{ secrets.CCPLUGINS_PAT }}
3953
fetch-depth: 0
4054

4155
- name: Fast-forward main from upstream
56+
if: steps.guard.outputs.has_pat == 'true'
4257
run: |
4358
set -euo pipefail
4459
git config user.name "github-actions[bot]"

ui/unified/js/wiki.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,13 @@
550550
});
551551
actions.appendChild(editBtn);
552552
['pdf', 'tex', 'docx', 'html'].forEach(function(fmt) {
553-
var b = el('a', 'wiki-export-btn');
553+
var b = el('button', 'wiki-export-btn');
554+
b.type = 'button';
554555
b.textContent = fmt.toUpperCase();
555-
b.href = '/api/wiki/export?path=' + encodeURIComponent(data.path) + '&format=' + fmt;
556-
b.setAttribute('download', '');
557556
b.title = 'Export via Pandoc → ' + fmt;
557+
b.addEventListener('click', function() {
558+
_exportDownload(data.path, fmt, b);
559+
});
558560
actions.appendChild(b);
559561
});
560562
pageHeader.appendChild(actions);
@@ -992,6 +994,53 @@
992994
return e;
993995
}
994996

997+
// ── Export download (Phase 10) ──
998+
//
999+
// Fetches /api/wiki/export and decides between saving the blob (on
1000+
// success — binary Content-Type) and surfacing the error message
1001+
// (when the server returned JSON). Using fetch avoids the old
1002+
// <a download> trap where a JSON error response got silently saved
1003+
// as "page.pdf" with 2 KB of error text inside.
1004+
1005+
async function _exportDownload(relPath, fmt, btn) {
1006+
if (btn) { btn.disabled = true; btn.textContent = fmt.toUpperCase() + '\u2026'; }
1007+
try {
1008+
var url = '/api/wiki/export?path=' + encodeURIComponent(relPath)
1009+
+ '&format=' + fmt;
1010+
var resp = await fetch(url);
1011+
var contentType = resp.headers.get('Content-Type') || '';
1012+
if (contentType.indexOf('application/json') === 0) {
1013+
var err = await resp.json();
1014+
var msg = err.error || 'export failed';
1015+
if (err.stderr) msg += '\n\nstderr:\n' + err.stderr;
1016+
alert('Export failed (' + fmt + '):\n\n' + msg);
1017+
return;
1018+
}
1019+
if (!resp.ok) {
1020+
alert('Export failed (' + fmt + '): HTTP ' + resp.status);
1021+
return;
1022+
}
1023+
var blob = await resp.blob();
1024+
var dispo = resp.headers.get('Content-Disposition') || '';
1025+
var m = dispo.match(/filename="([^"]+)"/);
1026+
var filename = m ? m[1]
1027+
: (relPath.split('/').pop() || 'page').replace(/\.md$/, '') + '.' + fmt;
1028+
var link = document.createElement('a');
1029+
link.href = URL.createObjectURL(blob);
1030+
link.download = filename;
1031+
document.body.appendChild(link);
1032+
link.click();
1033+
setTimeout(function() {
1034+
URL.revokeObjectURL(link.href);
1035+
link.remove();
1036+
}, 200);
1037+
} catch (err) {
1038+
alert('Export failed (' + fmt + '): ' + (err.message || err));
1039+
} finally {
1040+
if (btn) { btn.disabled = false; btn.textContent = fmt.toUpperCase(); }
1041+
}
1042+
}
1043+
9951044
// ── Academic rendering layer (Phase 9) ──
9961045
//
9971046
// Three post-render passes over the already-rendered body:

0 commit comments

Comments
 (0)