mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af1b2897e | ||
|
|
db8aaaf9f3 | ||
|
|
e556eba832 | ||
|
|
4ea39559c0 | ||
|
|
00d6a6e071 | ||
|
|
cc22deda3f | ||
|
|
1ed7338bb9 | ||
|
|
5dc4ba0204 | ||
|
|
ad95a046ed | ||
|
|
2d0d4443f8 | ||
|
|
656a0ae268 | ||
|
|
f1ee0bdbab | ||
|
|
abc9a08566 | ||
|
|
ab51bcf0f0 | ||
|
|
0ba8b9fcc1 | ||
|
|
e43af565ab | ||
|
|
58326adee4 | ||
|
|
fd9f52d756 | ||
|
|
91f4376371 | ||
|
|
29df488b4e | ||
|
|
e3ff34efd1 |
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"${CLAUDE_PROJECT_DIR}/scripts/lint/comment-lint-hook.mjs"
|
||||
],
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
name: feature-walkthrough
|
||||
description: >-
|
||||
Explain the full logic and process of the current branch end-to-end so someone
|
||||
with no prior knowledge of the task can understand, review, and reproduce it.
|
||||
Scopes the change from the branch diff, traces the flow across every layer it
|
||||
touches (frontend tool/hook/component, Java controller/service/endpoint, Python
|
||||
engine, config, i18n, tests), and produces a self-contained walkthrough document
|
||||
with Mermaid diagrams (sequence/flow/architecture), annotated file map with
|
||||
clickable references, before/after behavior, screenshots where a UI is involved,
|
||||
a "try it locally" section, and edge cases/risks. Use when asked for a feature or
|
||||
branch walkthrough, "explain what this branch does", a design/logic writeup, PR
|
||||
reviewer onboarding, or a hand-off doc. Pass --html to also emit a rendered HTML
|
||||
version; --no-screens to skip screenshots.
|
||||
argument-hint: "[branch-or-area] [--html] [--no-screens]"
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
||||
---
|
||||
|
||||
# Feature / Branch Walkthrough
|
||||
|
||||
Turn the current branch into a walkthrough a newcomer can follow. Audience:
|
||||
**someone who has never seen this task**. Explain the *why*, the *flow*, and *how to
|
||||
try it* - not just a diff summary.
|
||||
|
||||
`$ARGUMENTS` may name a branch or area to focus on; default is the current branch
|
||||
vs `main`. Flags: `--html` (also emit a rendered HTML twin), `--no-screens`.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Scope the change
|
||||
- `git log --oneline main..HEAD` and `git diff --stat main...HEAD` for the shape.
|
||||
- Read the PR description / commit messages for stated intent. Do **not** invent
|
||||
history or motivation that isn't evidenced (state current behavior in present tense).
|
||||
- Classify touched files by layer:
|
||||
- **Frontend**: tools (`frontend/editor/src/core/components/tools/*` or `.../core/tools/*`),
|
||||
hooks (`core/hooks/tools/*`, `useToolOperation`), contexts, routes, i18n
|
||||
(`public/locales/en-US`).
|
||||
- **Java backend**: controllers (`.../controller/api/...`), services, models, config.
|
||||
- **Engine**: `engine/src/stirling/{agents,contracts,api,services}`.
|
||||
- **Config / build / docker / tests.**
|
||||
|
||||
### 2. Trace the flow end-to-end
|
||||
Follow one real path from user action to result. For a typical PDF tool that's:
|
||||
UI control → `useToolOperation` hook → `POST /api/v1/...` → Spring controller →
|
||||
service (PDFBox / LibreOffice / engine call) → response → review panel → download.
|
||||
Read the actual files so the narrative is true to the code, and collect the exact
|
||||
file:line anchors you'll cite.
|
||||
|
||||
### 3. Draw the diagrams (Mermaid)
|
||||
Pick what fits; usually 2-3 of:
|
||||
- **Sequence diagram** - request/response across frontend → backend → engine.
|
||||
- **Flowchart** - the core decision/branching logic of the feature.
|
||||
- **Architecture/component** - new pieces and how they wire to existing ones.
|
||||
- **State** - if the feature has modes/steps.
|
||||
Keep nodes labeled in plain language. Validate the Mermaid parses before shipping.
|
||||
|
||||
### 4. Screenshots (unless --no-screens)
|
||||
If a UI is involved, capture key states with the stubbed Playwright harness
|
||||
(see the **ui-walkthrough** skill and `files-page-screenshots.spec.ts` for the
|
||||
pattern) or, for before/after, capture `main` then the branch. Drop PNGs in
|
||||
`walkthrough/<feature>/` and reference them from the doc. For backend-only
|
||||
changes, show request/response examples (curl + JSON) instead.
|
||||
|
||||
### 5. Write the walkthrough
|
||||
Create `walkthrough/<feature>/FEATURE-WALKTHROUGH.md` with:
|
||||
1. **TL;DR** - what the branch does and who it's for, in 3-4 sentences.
|
||||
2. **Problem & approach** - what wasn't possible before; the chosen solution.
|
||||
3. **Architecture diagram** + 1-paragraph orientation.
|
||||
4. **End-to-end flow** - the sequence diagram + a numbered walk of each step,
|
||||
each citing the real file (clickable `path:line`).
|
||||
5. **Key files** - annotated map (path → one line on its role).
|
||||
6. **Logic deep-dive** - the flowchart + prose for the non-obvious decisions.
|
||||
7. **Behavior** - before vs after; screenshots or request/response examples.
|
||||
8. **Try it locally** - exact steps (`task dev` / `task dev:all`, the route to
|
||||
open or the curl to run, any env like `DOCKER_ENABLE_SECURITY` or a test
|
||||
license key). Make it copy-pasteable.
|
||||
9. **Edge cases, risks, follow-ups** - what's untested, known limits, gotchas.
|
||||
|
||||
Markdown is the primary deliverable - it renders with diagrams in GitHub PRs and
|
||||
IDEs, no build step, ideal for review.
|
||||
|
||||
### 6. If `--html`
|
||||
Also emit `walkthrough/<feature>/walkthrough.html`: the same content with Mermaid
|
||||
rendered via `mermaid.initialize({startOnLoad:true})` (script from CDN; note in
|
||||
the file that rendering diagrams needs network, the `.md` is the offline copy) and
|
||||
screenshots inline. Keep it self-contained otherwise.
|
||||
|
||||
### 7. Deliver
|
||||
Give the doc path and a short chat summary. Offer to `SendUserFile` it.
|
||||
|
||||
## Principles
|
||||
- **True to the code.** Every claim traces to a file you read; cite `path:line`.
|
||||
No fabricated migration/version history.
|
||||
- **Newcomer-first.** Define repo-specific terms (FileContext, `useToolOperation`,
|
||||
the `@app/*` layer cascade, stubbed vs live tests) on first use.
|
||||
- **Show, don't assert.** Prefer a diagram + a real example over adjectives.
|
||||
- Don't commit the `walkthrough/` output unless asked.
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
name: pr-quiz
|
||||
description: >-
|
||||
Quiz the PR author on their own branch before they request review, to prove they
|
||||
actually understand the change - especially code an AI wrote for them. Scopes the
|
||||
branch diff vs its base, reads the changed code, then asks graded questions about
|
||||
what changed, why, how it works, what it could break, and which edge cases it must
|
||||
handle. Presents all questions first, waits for the author's answers, then grades
|
||||
each honestly against the real code (Correct / Partial / Incorrect with the true
|
||||
answer and file:line), scores it, and gives a readiness verdict that names the
|
||||
areas to re-study before asking humans to review. Use when asked to quiz me on my
|
||||
PR/branch, "test my understanding before review", a self-check gate before opening
|
||||
a PR, or before requesting reviewers. Administered as an interactive
|
||||
multiple-choice quiz (clickable options) by default; pass --free-text for
|
||||
written answers, --questions N to set count, --save to write a scorecard.
|
||||
argument-hint: "[branch-or-base-ref] [--questions N] [--free-text] [--save]"
|
||||
allowed-tools: Bash, Read, Grep, Glob, Write, AskUserQuestion
|
||||
---
|
||||
|
||||
# PR Quiz
|
||||
|
||||
Test whether the **author** genuinely understands their own branch before they ask
|
||||
other people to spend time reviewing it. This is a self-check gate: the point is to
|
||||
catch changes - often AI-written - that the author would not be able to explain or
|
||||
defend in review. Be a fair but honest examiner, not a pushover.
|
||||
|
||||
`$ARGUMENTS` may name a base ref or branch to diff against; default is this branch
|
||||
vs where it forked from the main line. Flags:
|
||||
- `--questions N` - target N questions (else scale to diff size, see below).
|
||||
- `--free-text` - administer as a written numbered list instead of the default
|
||||
interactive multiple-choice.
|
||||
- `--save` - also write a scorecard file after grading.
|
||||
|
||||
## Integrity rules (read first - the whole skill depends on these)
|
||||
|
||||
1. **Present every question before revealing any answer.** Ask, then wait. Never
|
||||
show the answer key alongside the questions.
|
||||
2. **Do not give hints or the answer while the quiz is open.** If the author asks
|
||||
"what's the answer?" or "is it X?" before committing, decline warmly and tell
|
||||
them to give their best answer first - guessing is part of the signal.
|
||||
3. **Grade truthfully.** Vague, hand-wavy, or "the AI did it" non-answers are
|
||||
Partial or Incorrect, not Correct. Do not inflate the score to be nice; a false
|
||||
pass defeats the entire purpose.
|
||||
4. **Ground everything in code you actually read.** Every question and every model
|
||||
answer must trace to a real line in the diff. Cite `path:line`. No trivia
|
||||
("how many lines?"), no invented behavior.
|
||||
5. **Credit real understanding.** If the author explains it correctly in their own
|
||||
words, mark it Correct even if worded differently than your key.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Scope the change (silently)
|
||||
- Find the base. Prefer the fork point off the main line so the quiz covers only
|
||||
this branch's work:
|
||||
```bash
|
||||
git fetch -q origin 2>/dev/null; \
|
||||
BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main); \
|
||||
git diff --stat "$BASE"...HEAD
|
||||
```
|
||||
If `$ARGUMENTS` names a ref, diff against that instead.
|
||||
- If the diff is empty, stop and say there's nothing to quiz on.
|
||||
- Read commit messages / PR description for the *stated* intent, but verify it
|
||||
against the actual diff - a mismatch is itself a good question.
|
||||
|
||||
### 2. Understand the code well enough to examine on it
|
||||
Read the full diff plus enough surrounding context and related files to answer
|
||||
every question you plan to ask. You cannot grade understanding you don't have.
|
||||
Note the non-obvious parts: the design decisions, the risky lines, the edge cases,
|
||||
the cross-file ripples, and anything that violates or upholds repo conventions
|
||||
(for this repo e.g. `@app/*` import layering, all file ops via FileContext,
|
||||
Jackson 3 / Spring Boot 4 APIs, engine typed-contract boundaries).
|
||||
|
||||
### 3. Build the question set
|
||||
Scale count to the change unless `--questions N` is given:
|
||||
small (< ~50 changed lines) 3-4, medium 5-8, large 9-12. Cap at 12.
|
||||
Draw from these categories - weight toward the ones the diff actually exercises:
|
||||
- **Intent** - what problem this solves; why it was needed now.
|
||||
- **Mechanism** - how a specific non-trivial piece actually works ("walk me
|
||||
through what `foo()` does when called with X").
|
||||
- **Decisions & alternatives** - why this approach over an obvious alternative;
|
||||
what a reviewer would reasonably push back on.
|
||||
- **Blast radius** - what else this touches or could break; what you'd retest.
|
||||
- **Edge cases** - inputs/states the change must handle (null, empty, large,
|
||||
concurrent, error paths).
|
||||
- **Conventions & correctness** - does it follow the repo's rules; is there a
|
||||
latent bug the author should be able to spot.
|
||||
Prefer questions the author can only answer if they read and understood the code.
|
||||
Keep a private answer key with `path:line` for each - do **not** show it yet.
|
||||
|
||||
### 4. Administer the quiz
|
||||
- **Default (multiple choice):** use the `AskUserQuestion` tool. Per question write
|
||||
3-4 options where **every** option is independently plausible - each distractor a
|
||||
real-but-wrong reading of the code, not filler. Two hard rules so the answer
|
||||
can't be spotted by shape rather than knowledge:
|
||||
- **Randomise the correct option's position** across questions - never default
|
||||
it to first. Spread it roughly evenly over the slots.
|
||||
- **Keep all options the same depth and length.** Do not describe the correct
|
||||
one more fully than the distractors - a longer or more-detailed option is a
|
||||
dead giveaway. Trim the right answer or flesh out the wrong ones until a
|
||||
reader can't tell them apart by size.
|
||||
The tool caps a call at 4 questions, so ask in batches of 4 - but run them as
|
||||
one continuous flow: fire the next batch immediately after the previous
|
||||
returns, with no narration ("Round 2 of 3") and no grading between batches.
|
||||
The author always has an "Other" free-text escape, which is fine.
|
||||
- **`--free-text`:** present all questions in one numbered list, then say
|
||||
"Answer in one reply; number your answers. I won't grade until you're done."
|
||||
Wait for the author's answers.
|
||||
- Do not proceed to grading until every answer is in.
|
||||
|
||||
### 5. Grade
|
||||
For each question, in order:
|
||||
- Verdict: **Correct** / **Partial** / **Incorrect**.
|
||||
- The model answer in one or two sentences, citing the real `path:line`.
|
||||
- One line on the gap when Partial/Incorrect - what they missed and where to look.
|
||||
Then a **Score** (e.g. 6/8, counting Partial as half) and a one-line summary of
|
||||
the pattern (e.g. "solid on intent, shaky on the error paths").
|
||||
|
||||
### 6. Readiness verdict
|
||||
End with a clear call:
|
||||
- **Ready for review** - understanding is sound; note anything to mention to
|
||||
reviewers proactively.
|
||||
- **Study first** - list the specific files/concepts to re-read before requesting
|
||||
review, each as a clickable `path:line`. Be concrete: "re-read the null handling
|
||||
in X before you send this out."
|
||||
Keep it honest - if they'd get grilled in review on something, say so now.
|
||||
|
||||
### 7. If `--save`
|
||||
Write `pr-quiz/<branch>-scorecard.md`: the questions, their answers, your grades
|
||||
and model answers, the score, and the verdict. Don't commit it unless asked.
|
||||
|
||||
## Principles
|
||||
- **The author is the examinee, not the collaborator.** During the quiz you withhold
|
||||
answers; you're measuring them, not helping them pass.
|
||||
- **A failed quiz is a successful outcome** - it caught a gap before a human's time
|
||||
was spent. Frame it that way, not as a scolding.
|
||||
- **True to the code.** Every question, answer, and grade traces to a line you read.
|
||||
- **Terse and direct** in chat - the questions and the verdict, minimal preamble.
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
name: ui-before-after
|
||||
description: >-
|
||||
Analyse a branch or PR and automatically capture before/after screenshots of
|
||||
every UI surface its changes touch, then pixel-diff the pairs to surface what
|
||||
actually changed and assemble PR-ready before/after montage images. Generic and
|
||||
diff-driven: it derives the capture targets from the diff (changed tools/routes →
|
||||
URLs) instead of hand-listing screens, captures "before" from the base branch and
|
||||
"after" from the head, then keeps only the views that visually differ. Each
|
||||
comparison is auto-cropped to the region that actually changed (the bounding box of
|
||||
differing pixels), falling back to the full page only when the change spans most of
|
||||
it. Use for before/after shots, a visual diff of a branch/PR, "screenshots for the
|
||||
PR description", "show what changed in the UI", or a side-by-side of UI changes.
|
||||
Takes a PR number/URL (resolved via gh) or a branch; defaults to the current branch
|
||||
vs its base. Flags: --scope <selector>, --base <ref|merge-base>, --theme
|
||||
light|dark|both, --all (capture every route, not just changed), --no-autocrop,
|
||||
--pagewide <n>, --threshold <n>.
|
||||
argument-hint: "[PR# | PR-url | branch] [--scope <sel>] [--base <ref>] [--theme both] [--all] [--no-autocrop]"
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
||||
---
|
||||
|
||||
# UI Before / After (generic visual diff)
|
||||
|
||||
Point it at a branch or PR; it figures out which UI changed, screenshots every
|
||||
affected surface **before** (base) and **after** (head), pixel-diffs the pairs, and
|
||||
montages the ones that actually changed into images for the PR description.
|
||||
|
||||
`$ARGUMENTS`: a PR number/URL, a branch, or nothing (current branch vs base).
|
||||
By default it captures the full viewport and auto-crops each comparison to the region
|
||||
that changed. Flags: `--scope <css>` (narrow the *capture* to a container, e.g.
|
||||
`[data-sidebar="tool-panel"]`, when you already know where the change is),
|
||||
`--no-autocrop` (keep full frames), `--pagewide <fraction>` (above this share of the
|
||||
page, skip cropping; default 0.6), `--base <ref|merge-base>`,
|
||||
`--theme light|dark|both`, `--all` (walk every route, not just changed),
|
||||
`--threshold <fraction>` (diff sensitivity, default 0.001).
|
||||
|
||||
Shares the capture harness with **ui-walkthrough** - read its SKILL.md for the
|
||||
stubbed-Playwright setup, worktree node_modules + `generate-icons`, the
|
||||
stale-`:5173` gotcha, and the dark-mode init-script. Bundled helpers:
|
||||
[capture-spec.template.ts](capture-spec.template.ts), [diff-shots.mjs](diff-shots.mjs),
|
||||
[montage-template.html](montage-template.html), [shoot-sections.mjs](shoot-sections.mjs).
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Resolve target + base
|
||||
```
|
||||
gh pr view <pr> --json number,title,headRefName,baseRefName,url,files # PR
|
||||
# or branch: base = merge-base(main, HEAD); head = HEAD
|
||||
gh pr diff <pr> --name-only # or: git diff --name-only <base>...HEAD
|
||||
```
|
||||
|
||||
### 2. Derive capture targets from the diff (the "analyse" step - no hand-listing)
|
||||
Map changed frontend files to URLs generically:
|
||||
- **Tools**: a changed `components/tools/<toolDir>/…` or `hooks/tools/<tool>/…` →
|
||||
toolId → URL via the repo's own rule `getToolUrlPath` in
|
||||
[toolsTaxonomy.ts:200](frontend/editor/src/core/data/toolsTaxonomy.ts): `/` + the
|
||||
id kebab-cased (`addPageNumbers` → `/add-page-numbers`).
|
||||
- **Pages/routes**: changed `filesPage/*` → `/files`, etc.
|
||||
- `--all`: enumerate every tool in the registry instead of just changed ones.
|
||||
Write `frontend/editor/screenshots/ui-diff/targets.json` =
|
||||
`[{ "id":"compress", "url":"/compress", "name":"Compress" }]`. This is what makes
|
||||
it generic - the spec never names a tool.
|
||||
|
||||
### 3. Capture AFTER (head) then BEFORE (base)
|
||||
Copy [capture-spec.template.ts](capture-spec.template.ts) →
|
||||
`src/core/tests/stubbed/ui-before-after.spec.ts` (it loops `targets.json`, seeds a
|
||||
sample PDF so file-dependent panels render, navigates to each URL, and screenshots
|
||||
the full viewport - or the `--scope` container if given). Ensure the harness is ready
|
||||
(node_modules + icons).
|
||||
```
|
||||
# after = current head
|
||||
cd frontend/editor && PR_SHOT_SIDE=after PR_SHOT_THEME=light \
|
||||
npx playwright test --project=stubbed ui-before-after.spec.ts
|
||||
# before = base, in an isolated worktree (copy the spec + targets.json in)
|
||||
git worktree add ../ba-base origin/<baseRefName> # or the merge-base
|
||||
# set up its frontend, copy spec + screenshots/ui-diff/targets.json across, then:
|
||||
cd ../ba-base/frontend/editor && PR_SHOT_SIDE=before PR_SHOT_THEME=light \
|
||||
npx playwright test --project=stubbed ui-before-after.spec.ts
|
||||
# copy its screenshots/ui-diff/before/ back next to after/. Repeat with
|
||||
# PR_SHOT_THEME=dark if --theme includes dark. Remove worktree when done.
|
||||
```
|
||||
|
||||
### 4. Auto-diff (surface what changed)
|
||||
```
|
||||
cd frontend/editor && node <skill>/diff-shots.mjs \
|
||||
screenshots/ui-diff/before screenshots/ui-diff/after screenshots/ui-diff
|
||||
```
|
||||
Produces `diff-report.json` classifying each view `unchanged | changed | added |
|
||||
removed`. For each changed view it computes the bounding box of differing pixels and
|
||||
writes cropped `__before_crop.png` / `__after_crop.png` / `__diff.png` to that region
|
||||
(+ padding) - **unless** the change covers more than `--pagewide` of the frame, where
|
||||
it keeps the full frame (`pageWide:true`). Drop `unchanged` - that's the noise the
|
||||
user doesn't want.
|
||||
|
||||
### 5. Montage the changes
|
||||
Build the manifest from the non-unchanged entries (group by tab/tool; each becomes a
|
||||
state row with before/after). For changed views use the cropped `cropBefore` /
|
||||
`cropAfter` from `diff-report.json` (tight on the affected region; full frame when
|
||||
`pageWide`); `added`/`removed` render the "not present" placeholder. Fill
|
||||
[montage-template.html](montage-template.html) (replace the `window.__BA__` data
|
||||
block; base64-inline the PNGs for portability), then render one PNG per section with
|
||||
[shoot-sections.mjs](shoot-sections.mjs). Optionally include the `__diff.png` overlay
|
||||
as a third column.
|
||||
|
||||
### 6. Deliver
|
||||
Output the `montage_<tab>.png` files + a short summary (N changed / added / removed,
|
||||
M unchanged skipped) and a paste-ready Markdown block. GitHub has no PR-body image
|
||||
API, so tell the user to drag the PNGs into the description. Do **not** post to the
|
||||
PR.
|
||||
|
||||
## Gotchas
|
||||
- Two installs (base worktree + head); junction main's node_modules only if its deps
|
||||
match that ref, else `npm ci` (see ui-walkthrough's stale-dep note).
|
||||
- A view that errors on one side (refactored/removed) → that side is missing; the
|
||||
diff marks it added/removed rather than failing the run.
|
||||
- Pixel diff needs equal dimensions, so capture at a fixed viewport (the template
|
||||
does); a view whose size changed is reported as "changed (dimensions differ)",
|
||||
uncropped.
|
||||
- Auto-crop uses a single bounding box, so two far-apart changes give one large crop
|
||||
(or trip `--pagewide`); narrow with `--scope` if that happens.
|
||||
- `getToolUrlPath` is the source of truth for tool URLs - use it, don't guess slugs.
|
||||
- Don't commit `screenshots/`, the throwaway spec, or the base worktree.
|
||||
@@ -1,67 +0,0 @@
|
||||
// Generic before/after capturer. NOT app-specific: it walks a targets.json that
|
||||
// the ui-before-after skill generates from the branch/PR diff, so nothing here is
|
||||
// hand-listed. Copy to src/core/tests/stubbed/ui-before-after.spec.ts, then run
|
||||
// once per (side, theme):
|
||||
// PR_SHOT_SIDE=after PR_SHOT_THEME=light \
|
||||
// npx playwright test --project=stubbed ui-before-after.spec.ts
|
||||
//
|
||||
// targets.json shape: [{ "id":"compress", "url":"/compress", "name":"Compress",
|
||||
// "needsFile": true }]
|
||||
import { test } from "@app/tests/helpers/stub-test-base";
|
||||
import type { Page } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SIDE = process.env.PR_SHOT_SIDE ?? "after";
|
||||
const THEME = process.env.PR_SHOT_THEME ?? "light";
|
||||
// Capture the full viewport by default so the affected region is in frame
|
||||
// wherever it is; diff-shots.mjs crops each comparison to what actually changed.
|
||||
// Set PR_SHOT_SCOPE to a selector to narrow the capture to one container.
|
||||
const SCOPE = process.env.PR_SHOT_SCOPE ?? "";
|
||||
const ROOT = path.resolve(process.cwd(), "screenshots", "ui-diff");
|
||||
const OUT = path.join(ROOT, SIDE);
|
||||
// A tiny sample PDF so file-dependent tool panels render. Point at a real fixture.
|
||||
const SAMPLE_PDF = process.env.PR_SHOT_SAMPLE ?? "src/core/tests/test-fixtures/sample.pdf";
|
||||
|
||||
type Target = { id: string; url: string; name?: string; needsFile?: boolean };
|
||||
const targets: Target[] = JSON.parse(fs.readFileSync(path.join(ROOT, "targets.json"), "utf-8"));
|
||||
|
||||
test.use({ autoGoto: false, viewport: { width: 1600, height: 900 }, seedJwt: true });
|
||||
|
||||
async function applyTheme(page: Page): Promise<void> {
|
||||
if (THEME !== "dark") return;
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("mantine-color-scheme", "dark");
|
||||
localStorage.setItem("mantine-color-scheme-value", "dark");
|
||||
});
|
||||
await page.emulateMedia({ colorScheme: "dark" });
|
||||
}
|
||||
|
||||
async function seedFile(page: Page): Promise<void> {
|
||||
if (!fs.existsSync(SAMPLE_PDF)) return;
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("files-button").click().catch(() => {});
|
||||
await page.locator('[data-testid="file-input"]').setInputFiles(SAMPLE_PDF).catch(() => {});
|
||||
await page.locator(".file-sidebar-file-item").first().isVisible({ timeout: 8_000 }).catch(() => {});
|
||||
}
|
||||
|
||||
for (const t of targets) {
|
||||
// One test per target so a single failure doesn't drop the rest.
|
||||
test(`${SIDE}/${THEME} ${t.id}`, async ({ page }) => {
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
await applyTheme(page);
|
||||
if (t.needsFile !== false) await seedFile(page);
|
||||
await page.goto(t.url, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(400); // settle Mantine portals/transitions
|
||||
const shot = path.join(OUT, `${t.id}__${THEME}.png`);
|
||||
if (SCOPE) {
|
||||
const scope = page.locator(SCOPE).first();
|
||||
if (await scope.isVisible({ timeout: 8_000 }).catch(() => false)) {
|
||||
await scope.screenshot({ path: shot });
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Full viewport (fixed size → stable dimensions for pixel diffing).
|
||||
await page.screenshot({ path: shot });
|
||||
});
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Auto-diff before/ vs after/ screenshots, classify each as
|
||||
// unchanged | changed | added | removed, and CROP each changed pair to the
|
||||
// affected region (bounding box of differing pixels + padding) - unless the
|
||||
// change spans most of the page, in which case the full frame is kept.
|
||||
// Run from frontend/editor (so deps resolve):
|
||||
// node <skill>/diff-shots.mjs <beforeDir> <afterDir> [outDir]
|
||||
// Env:
|
||||
// DIFF_THRESHOLD min fraction of differing pixels to count as changed (default 0.001)
|
||||
// DIFF_PAD padding px around the affected region (default 24)
|
||||
// DIFF_PAGEWIDE if affected bbox area / image area exceeds this, keep full frame (default 0.6)
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(path.join(process.cwd(), "noop.js"));
|
||||
const pm = require("pixelmatch");
|
||||
const pixelmatch = pm.default || pm;
|
||||
const { PNG } = require("pngjs");
|
||||
|
||||
const beforeDir = path.resolve(process.argv[2]);
|
||||
const afterDir = path.resolve(process.argv[3]);
|
||||
const outDir = path.resolve(process.argv[4] || afterDir);
|
||||
const THRESHOLD = Number(process.env.DIFF_THRESHOLD ?? "0.001");
|
||||
const PAD = Number(process.env.DIFF_PAD ?? "24");
|
||||
const PAGEWIDE = Number(process.env.DIFF_PAGEWIDE ?? "0.6");
|
||||
|
||||
const read = (p) => PNG.sync.read(fs.readFileSync(p));
|
||||
const isShot = (f) => f.endsWith(".png") && !/__(diff|before_crop|after_crop)\.png$/.test(f);
|
||||
const list = (d) => (fs.existsSync(d) ? fs.readdirSync(d).filter(isShot) : []);
|
||||
const names = [...new Set([...list(beforeDir), ...list(afterDir)])].sort();
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
function cropPNG(src, x, y, w, h) {
|
||||
const out = new PNG({ width: w, height: h });
|
||||
PNG.bitblt(src, out, x, y, w, h, 0, 0);
|
||||
return out;
|
||||
}
|
||||
const writePNG = (p, png) => fs.writeFileSync(p, PNG.sync.write(png));
|
||||
|
||||
// Bounding box of differing pixels using a diff mask (alpha>0 where changed).
|
||||
function changedBBox(before, after, w, h) {
|
||||
const mask = new PNG({ width: w, height: h });
|
||||
pixelmatch(before.data, after.data, mask.data, w, h, { threshold: 0.1, diffMask: true });
|
||||
let minX = w, minY = h, maxX = -1, maxY = -1, count = 0;
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
if (mask.data[(y * w + x) * 4 + 3] > 0) {
|
||||
count++;
|
||||
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxX < 0 ? null : { minX, minY, maxX, maxY, count };
|
||||
}
|
||||
|
||||
const report = [];
|
||||
for (const name of names) {
|
||||
const id = name.replace(/\.png$/, "");
|
||||
const bp = path.join(beforeDir, name), ap = path.join(afterDir, name);
|
||||
const hasB = fs.existsSync(bp), hasA = fs.existsSync(ap);
|
||||
if (hasB && !hasA) { report.push({ id, status: "removed", before: bp }); continue; }
|
||||
if (!hasB && hasA) { report.push({ id, status: "added", after: ap }); continue; }
|
||||
|
||||
const before = read(bp), after = read(ap);
|
||||
if (before.width !== after.width || before.height !== after.height) {
|
||||
report.push({ id, status: "changed", note: "dimensions differ", before: bp, after: ap });
|
||||
continue;
|
||||
}
|
||||
const w = after.width, h = after.height;
|
||||
const overlay = new PNG({ width: w, height: h });
|
||||
const px = pixelmatch(before.data, after.data, overlay.data, w, h, { threshold: 0.1 });
|
||||
const ratio = px / (w * h);
|
||||
if (ratio <= THRESHOLD) { report.push({ id, status: "unchanged", ratio: Number(ratio.toFixed(5)), before: bp, after: ap }); continue; }
|
||||
|
||||
const box = changedBBox(before, after, w, h);
|
||||
// Pad + clamp the affected region.
|
||||
const x = Math.max(0, box.minX - PAD), y = Math.max(0, box.minY - PAD);
|
||||
const x2 = Math.min(w, box.maxX + 1 + PAD), y2 = Math.min(h, box.maxY + 1 + PAD);
|
||||
const bw = x2 - x, bh = y2 - y;
|
||||
const pageWide = (bw * bh) / (w * h) > PAGEWIDE;
|
||||
|
||||
const entry = { id, status: "changed", ratio: Number(ratio.toFixed(5)), before: bp, after: ap, pageWide };
|
||||
if (pageWide) {
|
||||
// Change spans most of the page - keep the full frame, full overlay.
|
||||
const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, overlay);
|
||||
entry.diff = dp;
|
||||
} else {
|
||||
entry.bbox = { x, y, w: bw, h: bh };
|
||||
const cb = path.join(outDir, `${id}__before_crop.png`); writePNG(cb, cropPNG(before, x, y, bw, bh));
|
||||
const ca = path.join(outDir, `${id}__after_crop.png`); writePNG(ca, cropPNG(after, x, y, bw, bh));
|
||||
const dp = path.join(outDir, `${id}__diff.png`); writePNG(dp, cropPNG(overlay, x, y, bw, bh));
|
||||
entry.cropBefore = cb; entry.cropAfter = ca; entry.diff = dp;
|
||||
}
|
||||
report.push(entry);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(outDir, "diff-report.json"), JSON.stringify(report, null, 2));
|
||||
const changed = report.filter((r) => r.status !== "unchanged");
|
||||
console.log(`diffed ${report.length} view(s): ${changed.length} changed/added/removed, ${report.length - changed.length} unchanged`);
|
||||
for (const r of changed) {
|
||||
const tail = r.status !== "changed" ? ""
|
||||
: r.pageWide ? " (page-wide → full frame)"
|
||||
: ` (${(r.ratio * 100).toFixed(2)}%, cropped to ${r.bbox.w}×${r.bbox.h})`;
|
||||
console.log(` ${r.status.padEnd(9)} ${r.id}${tail}${r.note ? " - " + r.note : ""}`);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Build EXAMPLE.html from montage-template.html using REAL files-page shots as
|
||||
stand-in before/after pairs (layout demo, not an actual PR diff). Inlines PNGs as
|
||||
data URIs so the HTML is portable. Run: python make_example.py"""
|
||||
import base64
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
SHOTS = pathlib.Path(
|
||||
r"C:\Users\systo\git\Stirling-PDFNew\.claude\worktrees\kind-faraday-522a30"
|
||||
r"\frontend\editor\screenshots\files-page"
|
||||
)
|
||||
|
||||
|
||||
def uri(fname):
|
||||
p = SHOTS / fname
|
||||
return "data:image/png;base64," + base64.b64encode(p.read_bytes()).decode() if p.exists() else None
|
||||
|
||||
|
||||
data = {
|
||||
"pr": "DEMO",
|
||||
"title": "EXAMPLE — before/after montage (layout demo, real Files-page shots; not a real PR diff)",
|
||||
"base": "main", "head": "demo-branch",
|
||||
"cropSelector": "[data-sidebar=\"tool-panel\"] (real runs crop to the side; these demo shots are full-page)",
|
||||
"tabs": [
|
||||
{"id": "files", "title": "Files page", "ctx": "Each row = one flow state; left = base branch, right = this PR.",
|
||||
"states": [
|
||||
{"name": "Empty folder", "before": uri("01_empty_state_ctas.png"), "after": uri("02_empty_state_storage_off.png")},
|
||||
{"name": "Files + details panel", "before": uri("03_subtoolbar_with_files.png"), "after": uri("06_details_panel_save_to_server.png")},
|
||||
{"name": "Delete folder confirm", "before": None, "after": uri("19_delete_folder_dialog.png"), "note": "New in this PR"},
|
||||
]},
|
||||
{"id": "move", "title": "Move-to-folder dialog",
|
||||
"states": [
|
||||
{"name": "Dialog opened", "before": uri("07_move_dialog_collapsed.png"), "after": uri("08_move_dialog_create_folder_expanded.png")},
|
||||
{"name": "After folder created", "before": None, "after": uri("08b_move_dialog_after_create_folder.png"), "note": "New flow"},
|
||||
]},
|
||||
],
|
||||
}
|
||||
|
||||
tpl = (HERE / "montage-template.html").read_text(encoding="utf-8")
|
||||
out = re.sub(
|
||||
r"/\*__DATA__\*/.*?/\*__END__\*/",
|
||||
lambda _m: "/*__DATA__*/" + json.dumps(data) + "/*__END__*/",
|
||||
tpl, count=1, flags=re.S,
|
||||
)
|
||||
(HERE / "EXAMPLE.html").write_text(out, encoding="utf-8")
|
||||
print("wrote", HERE / "EXAMPLE.html", "(", (HERE / "EXAMPLE.html").stat().st_size // 1024, "KB )")
|
||||
@@ -1,106 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Before/After montage for a PR description. The ui-before-after skill replaces
|
||||
the JSON in the window.__BA__ data block below with the captured manifest, then
|
||||
screenshots each .tab-section (id="section-<tabId>") into a PNG to drag into the
|
||||
PR description. Self-contained; images may be relative paths or data URIs.
|
||||
|
||||
Data shape:
|
||||
{
|
||||
"pr":"6552","title":"...","base":"main","head":"feat/x",
|
||||
"cropSelector":"[data-sidebar=\"tool-panel\"]",
|
||||
"tabs":[
|
||||
{ "id":"sign","title":"Sign tool","states":[
|
||||
{"name":"Initial","before":"before/sign__initial.png","after":"after/sign__initial.png"},
|
||||
{"name":"Cert selected","before":null,"after":"after/sign__cert.png","note":"New in this PR"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Before / After</title>
|
||||
<style>
|
||||
:root { --bg:#ffffff; --ink:#0b0c0e; --muted:#6b7280; --line:#e5e7eb;
|
||||
--before:#6b7280; --after:#1f883d; --frame:#f3f4f6; --note:#b45309; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--ink);
|
||||
font:14px/1.5 -apple-system,"Segoe UI",Roboto,system-ui,sans-serif; }
|
||||
.wrap { max-width:1100px; margin:0 auto; padding:24px; }
|
||||
.doc-head { margin-bottom:8px; }
|
||||
.doc-head h1 { font-size:18px; margin:0 0 2px; }
|
||||
.doc-head .sub { color:var(--muted); font-size:12.5px; }
|
||||
.legend { display:flex; gap:14px; align-items:center; margin:10px 0 4px; font-size:12px; color:var(--muted); }
|
||||
.chip { font-size:10px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
|
||||
padding:2px 8px; border-radius:999px; color:#fff; }
|
||||
.chip.before { background:var(--before); } .chip.after { background:var(--after); }
|
||||
|
||||
.tab-section { border:1px solid var(--line); border-radius:14px; padding:18px 18px 8px;
|
||||
margin:18px 0; background:var(--bg); }
|
||||
.tab-section > h2 { font-size:16px; margin:0 0 2px; }
|
||||
.tab-section > .ctx { color:var(--muted); font-size:12px; margin-bottom:14px; }
|
||||
.state { margin-bottom:18px; }
|
||||
.state .name { font-weight:600; font-size:13.5px; margin-bottom:8px; display:flex; gap:8px; align-items:center; }
|
||||
.state .name .note { font-weight:500; color:var(--note); font-size:12px; }
|
||||
.pair { display:grid; grid-template-columns:1fr 1fr; gap:14px; align-items:start; }
|
||||
.cell { border:1px solid var(--line); border-radius:10px; overflow:hidden; background:var(--frame); }
|
||||
.cell .cap { display:flex; align-items:center; gap:8px; padding:7px 10px; border-bottom:1px solid var(--line);
|
||||
background:var(--bg); }
|
||||
.cell .cap .meta { color:var(--muted); font-size:11px; }
|
||||
.cell img { display:block; width:100%; height:auto; background:#fff; }
|
||||
.cell.empty .ph { display:flex; align-items:center; justify-content:center; height:160px; color:var(--muted);
|
||||
font-size:12.5px; text-align:center; padding:0 16px; }
|
||||
.single .pair { grid-template-columns:1fr; }
|
||||
.empty-doc { color:var(--muted); padding:40px; text-align:center; }
|
||||
@media (max-width:760px){ .pair{ grid-template-columns:1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap" id="root"></div>
|
||||
|
||||
<script id="data">
|
||||
window.__BA__ = /*__DATA__*/{"pr":"","title":"No data","base":"","head":"","cropSelector":"","tabs":[]}/*__END__*/;
|
||||
</script>
|
||||
<script>
|
||||
(function(){
|
||||
var D = window.__BA__ || { tabs: [] };
|
||||
var root = document.getElementById("root");
|
||||
function el(html){ var t=document.createElement("template"); t.innerHTML=html.trim(); return t.content.firstChild; }
|
||||
function esc(s){ return (s==null?"":String(s)).replace(/[&<>]/g, function(c){return {"&":"&","<":"<",">":">"}[c];}); }
|
||||
|
||||
function cell(kind, src){
|
||||
if (src) {
|
||||
return '<div class="cell"><div class="cap"><span class="chip '+kind+'">'+kind+'</span></div>'+
|
||||
'<img src="'+esc(src)+'" alt="'+kind+'"/></div>';
|
||||
}
|
||||
return '<div class="cell empty"><div class="cap"><span class="chip '+kind+'">'+kind+'</span>'+
|
||||
'<span class="meta">not present</span></div><div class="ph">No '+kind+' screenshot for this state</div></div>';
|
||||
}
|
||||
|
||||
var head = '<div class="doc-head"><h1>'+esc(D.title || ("PR #"+D.pr))+'</h1>'+
|
||||
'<div class="sub">Before / after · base <code>'+esc(D.base)+'</code> → head <code>'+esc(D.head)+'</code>'+
|
||||
(D.cropSelector ? ' · cropped to <code>'+esc(D.cropSelector)+'</code>' : '')+'</div></div>'+
|
||||
'<div class="legend"><span class="chip before">Before</span> base branch'+
|
||||
'<span class="chip after">After</span> this PR</div>';
|
||||
root.appendChild(el('<div>'+head+'</div>'));
|
||||
|
||||
if (!D.tabs || !D.tabs.length){ root.appendChild(el('<div class="empty-doc">No tabs captured yet.</div>')); return; }
|
||||
|
||||
D.tabs.forEach(function(tab){
|
||||
var states = (tab.states||[]).map(function(s){
|
||||
var onlyOne = (!s.before || !s.after);
|
||||
return '<div class="state'+(onlyOne?' ':'')+'">'+
|
||||
'<div class="name">'+esc(s.name)+(s.note?'<span class="note">'+esc(s.note)+'</span>':'')+'</div>'+
|
||||
'<div class="pair">'+cell("before", s.before)+cell("after", s.after)+'</div></div>';
|
||||
}).join("");
|
||||
var sec = '<section class="tab-section" id="section-'+esc(tab.id)+'">'+
|
||||
'<h2>'+esc(tab.title)+'</h2>'+
|
||||
(tab.ctx?'<div class="ctx">'+esc(tab.ctx)+'</div>':'')+
|
||||
states+'</section>';
|
||||
root.appendChild(el(sec));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,25 +0,0 @@
|
||||
// Render each .tab-section of a montage HTML into its own PNG (the PR-ready image).
|
||||
// Run from frontend/editor (so @playwright/test resolves):
|
||||
// node <skill>/shoot-sections.mjs <montage.html> <outDir>
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(path.join(process.cwd(), "noop.js"));
|
||||
const { chromium } = require("@playwright/test");
|
||||
|
||||
const htmlPath = path.resolve(process.argv[2]);
|
||||
const outDir = path.resolve(process.argv[3] || path.dirname(htmlPath));
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1200, height: 1200 }, deviceScaleFactor: 2 });
|
||||
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "load" });
|
||||
await page.waitForTimeout(250); // let images/fonts paint
|
||||
const ids = await page.$$eval(".tab-section", (els) => els.map((e) => e.id));
|
||||
if (!ids.length) { console.error("no .tab-section found"); process.exit(1); }
|
||||
for (const id of ids) {
|
||||
const name = id.replace(/^section-/, "");
|
||||
await page.locator("#" + id).screenshot({ path: path.join(outDir, `montage_${name}.png`) });
|
||||
console.log("wrote montage_" + name + ".png");
|
||||
}
|
||||
await browser.close();
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
name: ui-walkthrough
|
||||
description: >-
|
||||
Full UI investigation of the current branch's feature. Enumerates every view
|
||||
and state (empty, populated, loading, error, each dialog/menu/panel, responsive
|
||||
breakpoints, light + dark + RTL), captures them with the stubbed Playwright
|
||||
harness, assembles a single-image HTML walkthrough with a global light/dark
|
||||
toggle slider, then runs two review passes: visual/consistency (alignment,
|
||||
spacing, professionalism, dark/light parity, contrast, truncation) and
|
||||
UX/ease-of-use (flow, discoverability, affordances, empty/error states,
|
||||
expectations). Use when asked for a UI walkthrough, screenshot review, design
|
||||
or QA pass, "find anywhere to make it easier/better for users", or before
|
||||
merging frontend work. Pass --fix to auto-apply safe frontend fixes and
|
||||
re-capture; --theme to limit themes; --no-rtl to skip RTL.
|
||||
argument-hint: "[feature/area] [--fix] [--theme light|dark|both] [--no-rtl] [--breakpoints]"
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
||||
---
|
||||
|
||||
# UI Walkthrough
|
||||
|
||||
Produce a reviewable HTML walkthrough of a feature's UI in every state and theme,
|
||||
then critique it. Optionally auto-fix and re-capture.
|
||||
|
||||
`$ARGUMENTS` may name the feature/area to focus on. If empty, scope from the
|
||||
current branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),
|
||||
`--no-rtl`, `--breakpoints` (also capture phone/narrow widths).
|
||||
|
||||
## What this repo gives you (use it, don't reinvent)
|
||||
|
||||
- **Stubbed Playwright project** = backend-free screenshots via `page.route()` mocks.
|
||||
Reference implementation: `frontend/editor/src/core/tests/stubbed/files-page-screenshots.spec.ts`.
|
||||
It already shows the light / **dark** / **RTL** passes, JWT seeding, IndexedDB
|
||||
seeding, and dumping PNGs to a `screenshots/<area>/` folder. Copy its shape.
|
||||
- Helpers: `frontend/editor/src/core/tests/helpers/ui-helpers.ts`
|
||||
(`uploadFiles`, `openSettings`, `waitForModalOpen`, `dismissTourTooltip`, …)
|
||||
and the `stub-test-base` fixtures (`autoGoto`, `seedJwt`, `viewport`).
|
||||
- Config: `frontend/editor/playwright.config.ts` (run from `frontend/editor/`).
|
||||
- Report template: [report-template.html](report-template.html) - self-contained,
|
||||
one big image at a time, a global light/dark slider that flips every shot,
|
||||
thumbnail rail, prev/next + arrow keys, and a Findings tab.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Scope the feature
|
||||
- If `$ARGUMENTS` is empty: `git diff --name-only main...HEAD` and read the PR/commits.
|
||||
Identify changed pages, tools (`core/components/tools/<tool>` or `core/tools/<tool>`),
|
||||
dialogs, panels, and routes.
|
||||
- Enumerate **every view and state** to capture, e.g.:
|
||||
empty / populated / loading / error / disabled; each dialog, menu, popover, tooltip;
|
||||
each tab or step; selection + multi-select; success/result panel; and (if relevant)
|
||||
permission/role variants. Write the list down before capturing - it's the report's spine.
|
||||
|
||||
### 2. Prepare the harness (worktree-safe)
|
||||
Worktrees have no `node_modules` and no generated icons. From repo root:
|
||||
```
|
||||
cd frontend && npm ci # or junction main's node_modules (see memory)
|
||||
cd frontend/editor && node scripts/generate-icons.js
|
||||
```
|
||||
Kill any stale dev server first (it serves old modules):
|
||||
`Get-NetTCPConnection -LocalPort 5173 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`
|
||||
|
||||
### 3. Write the capture spec
|
||||
Create `frontend/editor/src/core/tests/stubbed/<feature>-walkthrough.spec.ts`,
|
||||
modeled on `files-page-screenshots.spec.ts`. For each enumerated view:
|
||||
- stub the APIs it needs, drive the UI to that state, wait on a real locator
|
||||
(not a fixed sleep), `await settle(page)` for Mantine portals, then
|
||||
`page.screenshot({ path: shotPath("NN_name_<theme>") })`.
|
||||
- Capture each view in **light and dark** (and RTL unless `--no-rtl`). Reuse the
|
||||
`enableDarkMode` / `enableRtl` init-script pattern from the reference spec
|
||||
(`localStorage["mantine-color-scheme"]="dark"` + `emulateMedia({colorScheme:"dark"})`).
|
||||
- Name shots `NN_<view>_<theme>.png` so light/dark pair up by suffix.
|
||||
- Prefer **stable test-ids** over translated accessible names (RTL/i18n breaks text locators).
|
||||
|
||||
Run it: `cd frontend/editor && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.
|
||||
Add `--project=stubbed-firefox`/`-webkit` only if cross-browser layout matters.
|
||||
|
||||
### 4. Build the report
|
||||
- Copy `report-template.html` to `screenshots/<feature>/walkthrough.html` (so the
|
||||
relative `screenshots/...` image paths resolve, or rewrite paths to sit beside it).
|
||||
- Build the manifest and inject it: replace the JSON between the
|
||||
`/*__DATA__*/` … `/*__END__*/` markers with one `views[]` entry per view
|
||||
(`{id,title,light,dark,viewport,notes}`) and an empty `findings` object you'll
|
||||
fill in step 5. Keep `light`/`dark` as relative paths.
|
||||
- The toggle slider answers the "one big image + flip light/dark for all" request:
|
||||
it shows a single large screenshot, and switching the slider re-themes every view.
|
||||
|
||||
### 5. Review pass 1 - visual & consistency
|
||||
Open each screenshot (Read the PNG) and judge against the others:
|
||||
alignment & spacing rhythm, control placement, button hierarchy, typography,
|
||||
**light/dark parity** (contrast, invisible borders, washed-out text, wrong tokens),
|
||||
truncation/overflow, RTL mirroring, focus states, icon consistency, professional polish.
|
||||
Record each issue as a finding `{severity:high|med|low, view, title, detail, fix}`.
|
||||
|
||||
### 6. Review pass 2 - UX & ease of use
|
||||
Walk the flow as a first-time user: discoverability, number of steps, affordance
|
||||
clarity, empty-state guidance, error recovery, destructive-action confirmation,
|
||||
defaults, loading feedback, mobile reachability, accessible names, and whether the
|
||||
UI matches user expectations for this kind of tool. Record findings the same way.
|
||||
|
||||
Write both finding lists into the report's `findings.visual` / `findings.ux`,
|
||||
and add short per-view `notes`. Re-inject the manifest.
|
||||
|
||||
### 7. If `--fix`
|
||||
Only safe, self-contained frontend fixes (spacing, alignment, tokens, missing
|
||||
dark-mode colors, labels, aria, obvious copy). For each: edit the component/CSS,
|
||||
mark the finding `fixed:true` with what changed, then **re-run the spec** to
|
||||
re-capture the affected shots and regenerate the report. Run `task frontend:check`.
|
||||
Leave anything risky or ambiguous as a finding, not a change.
|
||||
|
||||
### 8. Deliver
|
||||
Tell the user the report path and give a tight chat summary: N views ×
|
||||
themes captured, top findings by severity, and (if `--fix`) what changed.
|
||||
Optionally `SendUserFile` the `walkthrough.html`.
|
||||
|
||||
## Gotchas
|
||||
- Stale `:5173` server serves old bundles - kill it before capturing (see step 2).
|
||||
- Missing `material-symbols-icons.json` → blank app → every shot times out. Run
|
||||
`generate-icons.js` first.
|
||||
- `await settle(page)` before shots or portals/transitions tear mid-capture.
|
||||
- Don't commit the generated `screenshots/` or the throwaway spec unless asked.
|
||||
@@ -1,116 +0,0 @@
|
||||
"""Build a self-contained EXAMPLE.html from report-template.html with mock
|
||||
light/dark screenshots, so the viewer + global theme slider can be demoed
|
||||
without a real capture run. Run: python make_example.py"""
|
||||
import base64
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
|
||||
|
||||
def svg(bg, fg, panel, accent, muted, label, kind):
|
||||
"""A simple fake 'screen' SVG: title bar, sidebar, content varies by kind."""
|
||||
parts = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">',
|
||||
f'<rect width="1600" height="900" fill="{bg}"/>',
|
||||
# top bar
|
||||
f'<rect width="1600" height="64" fill="{panel}"/>',
|
||||
f'<circle cx="40" cy="32" r="12" fill="{accent}"/>',
|
||||
f'<rect x="64" y="24" width="160" height="16" rx="6" fill="{muted}"/>',
|
||||
f'<rect x="1430" y="20" width="130" height="24" rx="12" fill="{accent}"/>',
|
||||
# left sidebar
|
||||
f'<rect x="0" y="64" width="220" height="836" fill="{panel}"/>',
|
||||
]
|
||||
for i in range(6):
|
||||
y = 100 + i * 56
|
||||
parts.append(f'<rect x="24" y="{y}" width="172" height="32" rx="8" fill="{bg}"/>')
|
||||
if kind == "empty":
|
||||
parts += [
|
||||
f'<rect x="700" y="360" width="200" height="120" rx="16" fill="none" stroke="{muted}" stroke-width="3" stroke-dasharray="10 8"/>',
|
||||
f'<rect x="690" y="510" width="220" height="44" rx="10" fill="{accent}"/>',
|
||||
f'<text x="800" y="600" fill="{muted}" font-family="sans-serif" font-size="26" text-anchor="middle">{label}</text>',
|
||||
]
|
||||
elif kind == "form":
|
||||
for i in range(4):
|
||||
y = 140 + i * 90
|
||||
parts.append(f'<rect x="280" y="{y}" width="160" height="16" rx="6" fill="{muted}"/>')
|
||||
parts.append(f'<rect x="280" y="{y+26}" width="900" height="44" rx="8" fill="{panel}" stroke="{muted}" stroke-width="1"/>')
|
||||
parts.append(f'<rect x="280" y="560" width="200" height="50" rx="10" fill="{accent}"/>')
|
||||
parts.append(f'<text x="800" y="850" fill="{muted}" font-family="sans-serif" font-size="24" text-anchor="middle">{label}</text>')
|
||||
else: # dialog
|
||||
parts += [
|
||||
f'<rect width="1600" height="900" fill="{fg}" opacity="0.45"/>',
|
||||
f'<rect x="520" y="280" width="560" height="360" rx="18" fill="{panel}"/>',
|
||||
f'<rect x="556" y="320" width="280" height="22" rx="8" fill="{fg}"/>',
|
||||
f'<rect x="556" y="372" width="488" height="14" rx="6" fill="{muted}"/>',
|
||||
f'<rect x="556" y="398" width="420" height="14" rx="6" fill="{muted}"/>',
|
||||
f'<rect x="820" y="560" width="110" height="44" rx="9" fill="{bg}" stroke="{muted}"/>',
|
||||
f'<rect x="946" y="560" width="98" height="44" rx="9" fill="{accent}"/>',
|
||||
f'<text x="800" y="700" fill="#fff" font-family="sans-serif" font-size="24" text-anchor="middle">{label}</text>',
|
||||
]
|
||||
parts.append("</svg>")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def data_uri(s):
|
||||
return "data:image/svg+xml;base64," + base64.b64encode(s.encode()).decode()
|
||||
|
||||
|
||||
LIGHT = dict(bg="#ffffff", fg="#111418", panel="#f1f3f6", accent="#2f6fed", muted="#c2c8d0")
|
||||
DARK = dict(bg="#16181c", fg="#000000", panel="#1f232a", accent="#5b8cff", muted="#3a414b")
|
||||
|
||||
|
||||
def pair(kind, label):
|
||||
return (
|
||||
data_uri(svg(LIGHT["bg"], LIGHT["fg"], LIGHT["panel"], LIGHT["accent"], LIGHT["muted"], label, kind)),
|
||||
data_uri(svg(DARK["bg"], DARK["fg"], DARK["panel"], DARK["accent"], DARK["muted"], label, kind)),
|
||||
)
|
||||
|
||||
|
||||
views = []
|
||||
for idx, (kind, title, label) in enumerate([
|
||||
("empty", "Empty state", "Drop a PDF to start"),
|
||||
("form", "Tool options panel", "Compress options"),
|
||||
("dialog", "Confirm dialog", "Replace original file?"),
|
||||
], start=1):
|
||||
light, dark = pair(kind, label)
|
||||
views.append({
|
||||
"id": f"{idx:02d}_{kind}",
|
||||
"title": title,
|
||||
"light": light,
|
||||
"dark": dark,
|
||||
"viewport": "1600x900",
|
||||
"notes": ["This is mock data to demo the viewer."],
|
||||
})
|
||||
|
||||
data = {
|
||||
"feature": "EXAMPLE - Compress PDF (mock data)",
|
||||
"branch": "demo",
|
||||
"generated": "example",
|
||||
"views": views,
|
||||
"findings": {
|
||||
"visual": [
|
||||
{"severity": "high", "view": "03_dialog", "title": "Dialog buttons too close",
|
||||
"detail": "Cancel/Confirm have only 8px gap; easy to misclick.",
|
||||
"fix": "Increase gap to var(--mantine-spacing-md)."},
|
||||
{"severity": "low", "view": "02_form", "title": "Field labels low contrast in dark mode",
|
||||
"detail": "Muted token fails WCAG AA on the dark panel.",
|
||||
"fix": "Use --mantine-color-dimmed instead of a hard-coded grey."},
|
||||
],
|
||||
"ux": [
|
||||
{"severity": "med", "view": "01_empty", "title": "Primary CTA below the dropzone",
|
||||
"detail": "Users expect the action button adjacent to the dropzone.",
|
||||
"fix": "Move the button directly under the dashed zone."},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
tpl = (HERE / "report-template.html").read_text(encoding="utf-8")
|
||||
out = re.sub(
|
||||
r"/\*__DATA__\*/.*?/\*__END__\*/",
|
||||
lambda _m: "/*__DATA__*/" + json.dumps(data) + "/*__END__*/",
|
||||
tpl, count=1, flags=re.S,
|
||||
)
|
||||
(HERE / "EXAMPLE.html").write_text(out, encoding="utf-8")
|
||||
print("wrote", (HERE / "EXAMPLE.html"))
|
||||
@@ -1,298 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
UI Walkthrough report template (self-contained, works from file://).
|
||||
The ui-walkthrough skill replaces the JSON in the window.__WALKTHROUGH__ data
|
||||
block below with the captured manifest. Do not add external CDN deps - it must open offline.
|
||||
|
||||
Data shape:
|
||||
{
|
||||
"feature": "Compress PDF tool",
|
||||
"branch": "claude/...",
|
||||
"generated": "2026-06-21",
|
||||
"views": [
|
||||
{ "id": "01_empty", "title": "Empty state",
|
||||
"light": "screenshots/compress/01_empty_light.png",
|
||||
"dark": "screenshots/compress/01_empty_dark.png",
|
||||
"viewport": "1600x900",
|
||||
"notes": ["Heading is centered", "Primary CTA below the fold on mobile"] }
|
||||
],
|
||||
"findings": {
|
||||
"visual": [ { "severity":"high", "view":"01_empty", "title":"...", "detail":"...", "fix":"..." } ],
|
||||
"ux": [ { "severity":"med", "view":"03_dialog", "title":"...", "detail":"...", "fix":"..." } ]
|
||||
}
|
||||
}
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>UI Walkthrough</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f7f9; --panel: #ffffff; --panel-2: #f0f2f5; --text: #1a1b1e;
|
||||
--muted: #6b7280; --border: #e2e5ea; --accent: #2f6fed; --accent-weak: #e8f0fe;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.06);
|
||||
--hi: #d92d20; --med: #d98e00; --low: #2f6fed; --stage: #0b0c0e;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
--bg: #0d0e10; --panel: #16181c; --panel-2: #1d2024; --text: #e6e8eb;
|
||||
--muted: #9aa3ad; --border: #2a2e35; --accent: #5b8cff; --accent-weak: #1a2336;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,.5), 0 8px 24px rgba(0,0,0,.4); --stage: #000;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font: 14px/1.5 -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
|
||||
background: var(--bg); color: var(--text); }
|
||||
header { display: flex; align-items: center; gap: 16px; padding: 12px 20px;
|
||||
background: var(--panel); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 5; }
|
||||
header h1 { font-size: 15px; margin: 0; font-weight: 650; }
|
||||
header .sub { color: var(--muted); font-size: 12px; }
|
||||
.spacer { flex: 1; }
|
||||
.counter { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 13px; }
|
||||
.tabs { display: flex; gap: 4px; }
|
||||
.tab { border: 1px solid var(--border); background: var(--panel-2); color: var(--text);
|
||||
padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 13px; }
|
||||
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
|
||||
/* Light/Dark slider */
|
||||
.theme-toggle { display: flex; align-items: center; gap: 9px; user-select: none; }
|
||||
.theme-toggle .lbl { font-size: 12px; color: var(--muted); }
|
||||
.theme-toggle .lbl.on { color: var(--text); font-weight: 600; }
|
||||
.switch { position: relative; width: 52px; height: 28px; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; inset: 0; cursor: pointer; background: var(--panel-2);
|
||||
border: 1px solid var(--border); border-radius: 999px; transition: .2s; }
|
||||
.slider:before { content: ""; position: absolute; height: 20px; width: 20px; left: 3px; top: 3px;
|
||||
background: #fbbf24; border-radius: 50%; transition: .2s; box-shadow: 0 1px 2px rgba(0,0,0,.3); }
|
||||
.switch input:checked + .slider { background: var(--accent); }
|
||||
.switch input:checked + .slider:before { transform: translateX(24px); background: #c7d2fe; }
|
||||
|
||||
main { display: grid; grid-template-columns: 240px 1fr; height: calc(100vh - 53px); }
|
||||
.rail { border-right: 1px solid var(--border); overflow-y: auto; background: var(--panel); padding: 8px; }
|
||||
.rail .group-label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
|
||||
color: var(--muted); padding: 10px 8px 4px; }
|
||||
.thumb { display: flex; gap: 9px; align-items: center; padding: 7px; border-radius: 8px;
|
||||
cursor: pointer; border: 1px solid transparent; }
|
||||
.thumb:hover { background: var(--panel-2); }
|
||||
.thumb.active { background: var(--accent-weak); border-color: var(--accent); }
|
||||
.thumb img { width: 64px; height: 40px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border); background: var(--stage); }
|
||||
.thumb .t { font-size: 12.5px; line-height: 1.3; }
|
||||
.thumb .badge { font-size: 10px; color: var(--muted); }
|
||||
.thumb .dot { width: 7px; height: 7px; border-radius: 50%; margin-left: auto; flex: none; }
|
||||
|
||||
.stagewrap { display: flex; flex-direction: column; min-width: 0; }
|
||||
.stage { flex: 1; display: flex; align-items: center; justify-content: center; padding: 22px;
|
||||
background: var(--stage); position: relative; min-height: 0; }
|
||||
.stage img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px;
|
||||
box-shadow: 0 4px 30px rgba(0,0,0,.4); background: #fff; }
|
||||
html[data-theme="dark"] .stage img { background: #16181c; }
|
||||
.nav-btn { position: absolute; top: 50%; transform: translateY(-50%); width: 42px; height: 42px;
|
||||
border-radius: 50%; border: 1px solid var(--border); background: var(--panel);
|
||||
color: var(--text); cursor: pointer; font-size: 18px; opacity: .85; }
|
||||
.nav-btn:hover { opacity: 1; } .nav-btn.prev { left: 16px; } .nav-btn.next { right: 16px; }
|
||||
.nav-btn:disabled { opacity: .25; cursor: default; }
|
||||
.missing { color: var(--muted); font-size: 13px; text-align: center; }
|
||||
|
||||
.detail { border-top: 1px solid var(--border); background: var(--panel); padding: 14px 20px;
|
||||
max-height: 38vh; overflow-y: auto; }
|
||||
.detail h2 { margin: 0 0 4px; font-size: 15px; }
|
||||
.detail .meta { color: var(--muted); font-size: 12px; margin-bottom: 10px; }
|
||||
.notes { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
|
||||
.notes li { display: flex; gap: 8px; align-items: flex-start; }
|
||||
.sev { font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 2px 7px; border-radius: 999px;
|
||||
color: #fff; flex: none; margin-top: 1px; }
|
||||
.sev.high { background: var(--hi); } .sev.med { background: var(--med); } .sev.low { background: var(--low); }
|
||||
.finding .fix { color: var(--muted); font-size: 12.5px; }
|
||||
.finding .fix b { color: var(--text); font-weight: 600; }
|
||||
|
||||
/* Summary tab */
|
||||
.summary { padding: 20px 28px; overflow-y: auto; }
|
||||
.summary h2 { font-size: 16px; margin: 22px 0 8px; }
|
||||
.summary .empty { color: var(--muted); }
|
||||
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
|
||||
padding: 12px 14px; margin-bottom: 8px; box-shadow: var(--shadow); }
|
||||
.card .head { display: flex; gap: 8px; align-items: center; }
|
||||
.card a { color: var(--accent); text-decoration: none; cursor: pointer; }
|
||||
.hide { display: none !important; }
|
||||
kbd { font: 11px ui-monospace, monospace; background: var(--panel-2); border: 1px solid var(--border);
|
||||
border-radius: 4px; padding: 1px 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1 id="feature-title">UI Walkthrough</h1>
|
||||
<div class="sub" id="feature-sub"></div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="viewer">Walkthrough</button>
|
||||
<button class="tab" data-tab="summary">Findings</button>
|
||||
</div>
|
||||
<div class="counter" id="counter"></div>
|
||||
<label class="theme-toggle" title="Toggle light / dark for every screenshot">
|
||||
<span class="lbl" id="lbl-light">Light</span>
|
||||
<span class="switch"><input type="checkbox" id="theme-switch" /><span class="slider"></span></span>
|
||||
<span class="lbl" id="lbl-dark">Dark</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<main id="viewer-pane">
|
||||
<aside class="rail" id="rail"></aside>
|
||||
<section class="stagewrap">
|
||||
<div class="stage">
|
||||
<button class="nav-btn prev" id="prev" aria-label="Previous">‹</button>
|
||||
<img id="stage-img" alt="" />
|
||||
<div class="missing hide" id="missing"></div>
|
||||
<button class="nav-btn next" id="next" aria-label="Next">›</button>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<h2 id="view-title"></h2>
|
||||
<div class="meta" id="view-meta"></div>
|
||||
<ul class="notes" id="view-notes"></ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section class="summary hide" id="summary-pane"></section>
|
||||
|
||||
<script id="data">
|
||||
window.__WALKTHROUGH__ = /*__DATA__*/{"feature":"No data","branch":"","generated":"","views":[],"findings":{"visual":[],"ux":[]}}/*__END__*/;
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
var D = window.__WALKTHROUGH__ || { views: [], findings: { visual: [], ux: [] } };
|
||||
var views = D.views || [];
|
||||
var state = { i: 0, theme: localStorage.getItem("ui-wt-theme") || "light", tab: "viewer" };
|
||||
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
function sevClass(s) { return s === "high" ? "high" : s === "med" || s === "medium" ? "med" : "low"; }
|
||||
|
||||
function applyChrome() {
|
||||
document.documentElement.setAttribute("data-theme", state.theme);
|
||||
$("theme-switch").checked = state.theme === "dark";
|
||||
$("lbl-light").classList.toggle("on", state.theme === "light");
|
||||
$("lbl-dark").classList.toggle("on", state.theme === "dark");
|
||||
}
|
||||
|
||||
function srcFor(v) { return state.theme === "dark" ? (v.dark || v.light) : (v.light || v.dark); }
|
||||
|
||||
function findingsForView(id) {
|
||||
var all = (D.findings && D.findings.visual || []).concat(D.findings && D.findings.ux || []);
|
||||
return all.filter(function (f) { return f.view === id; });
|
||||
}
|
||||
|
||||
function renderRail() {
|
||||
var rail = $("rail");
|
||||
rail.innerHTML = "";
|
||||
if (!views.length) { rail.innerHTML = '<div class="group-label">No views captured</div>'; return; }
|
||||
views.forEach(function (v, idx) {
|
||||
var fs = findingsForView(v.id);
|
||||
var worst = fs.some(function (f){return sevClass(f.severity)==="high";}) ? "var(--hi)"
|
||||
: fs.some(function (f){return sevClass(f.severity)==="med";}) ? "var(--med)"
|
||||
: fs.length ? "var(--low)" : "transparent";
|
||||
var el = document.createElement("div");
|
||||
el.className = "thumb" + (idx === state.i ? " active" : "");
|
||||
el.innerHTML = '<img src="' + srcFor(v) + '" alt="" />' +
|
||||
'<div><div class="t">' + (v.title || v.id) + '</div>' +
|
||||
'<div class="badge">' + (v.viewport || "") + '</div></div>' +
|
||||
'<span class="dot" style="background:' + worst + '"></span>';
|
||||
el.onclick = function () { state.i = idx; render(); };
|
||||
rail.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
applyChrome();
|
||||
if (!views.length) {
|
||||
$("missing").classList.remove("hide"); $("stage-img").classList.add("hide");
|
||||
$("missing").textContent = "No screenshots in this report yet.";
|
||||
$("counter").textContent = ""; return;
|
||||
}
|
||||
var v = views[state.i];
|
||||
var src = srcFor(v);
|
||||
var img = $("stage-img");
|
||||
if (src) {
|
||||
img.classList.remove("hide"); $("missing").classList.add("hide");
|
||||
img.src = src; img.alt = v.title || v.id;
|
||||
} else {
|
||||
img.classList.add("hide"); $("missing").classList.remove("hide");
|
||||
$("missing").textContent = "No " + state.theme + " screenshot for this view.";
|
||||
}
|
||||
$("counter").textContent = (state.i + 1) + " / " + views.length;
|
||||
$("view-title").textContent = v.title || v.id;
|
||||
$("view-meta").textContent = [v.viewport, state.theme + " mode"].filter(Boolean).join(" · ");
|
||||
var notes = $("view-notes"); notes.innerHTML = "";
|
||||
var fs = findingsForView(v.id);
|
||||
(v.notes || []).forEach(function (n) {
|
||||
var li = document.createElement("li"); li.textContent = "· " + n; notes.appendChild(li);
|
||||
});
|
||||
fs.forEach(function (f) {
|
||||
var li = document.createElement("li"); li.className = "finding";
|
||||
li.innerHTML = '<span class="sev ' + sevClass(f.severity) + '">' + (f.severity || "note") + '</span>' +
|
||||
'<span><b>' + (f.title || "") + '</b> — ' + (f.detail || "") +
|
||||
(f.fix ? ' <span class="fix"><b>Fix:</b> ' + f.fix + '</span>' : '') + '</span>';
|
||||
notes.appendChild(li);
|
||||
});
|
||||
$("prev").disabled = state.i === 0;
|
||||
$("next").disabled = state.i === views.length - 1;
|
||||
renderRail();
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
var pane = $("summary-pane");
|
||||
function block(title, arr) {
|
||||
var h = '<h2>' + title + ' (' + arr.length + ')</h2>';
|
||||
if (!arr.length) return h + '<div class="empty">None found.</div>';
|
||||
return h + arr.map(function (f) {
|
||||
return '<div class="card"><div class="head">' +
|
||||
'<span class="sev ' + sevClass(f.severity) + '">' + (f.severity || "note") + '</span>' +
|
||||
'<b>' + (f.title || "") + '</b>' +
|
||||
(f.view ? ' <a data-jump="' + f.view + '">' + f.view + '</a>' : '') + '</div>' +
|
||||
'<div style="margin-top:6px">' + (f.detail || "") + '</div>' +
|
||||
(f.fix ? '<div class="finding" style="margin-top:6px"><span class="fix"><b>Fix:</b> ' + f.fix + '</span></div>' : '') +
|
||||
'</div>';
|
||||
}).join("");
|
||||
}
|
||||
pane.innerHTML = block("Visual & consistency", (D.findings && D.findings.visual) || []) +
|
||||
block("UX & ease of use", (D.findings && D.findings.ux) || []);
|
||||
pane.querySelectorAll("[data-jump]").forEach(function (a) {
|
||||
a.onclick = function () {
|
||||
var id = a.getAttribute("data-jump");
|
||||
var idx = views.findIndex(function (v) { return v.id === id; });
|
||||
if (idx >= 0) { state.i = idx; setTab("viewer"); }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setTab(t) {
|
||||
state.tab = t;
|
||||
document.querySelectorAll(".tab").forEach(function (b) { b.classList.toggle("active", b.dataset.tab === t); });
|
||||
$("viewer-pane").classList.toggle("hide", t !== "viewer");
|
||||
$("summary-pane").classList.toggle("hide", t !== "summary");
|
||||
if (t === "viewer") $("viewer-pane").style.display = "grid";
|
||||
if (t === "summary") renderSummary();
|
||||
}
|
||||
|
||||
// wiring
|
||||
$("feature-title").textContent = D.feature || "UI Walkthrough";
|
||||
$("feature-sub").textContent = [D.branch, D.generated].filter(Boolean).join(" · ");
|
||||
$("theme-switch").onchange = function () {
|
||||
state.theme = this.checked ? "dark" : "light";
|
||||
localStorage.setItem("ui-wt-theme", state.theme);
|
||||
render();
|
||||
};
|
||||
$("prev").onclick = function () { if (state.i > 0) { state.i--; render(); } };
|
||||
$("next").onclick = function () { if (state.i < views.length - 1) { state.i++; render(); } };
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (state.tab !== "viewer") return;
|
||||
if (e.key === "ArrowLeft") $("prev").click();
|
||||
if (e.key === "ArrowRight") $("next").click();
|
||||
if (e.key.toLowerCase() === "t") $("theme-switch").click();
|
||||
});
|
||||
document.querySelectorAll(".tab").forEach(function (b) { b.onclick = function () { setTab(b.dataset.tab); }; });
|
||||
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -119,6 +119,8 @@
|
||||
"extensions": [
|
||||
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
|
||||
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
|
||||
"ms-python.black-formatter", // Python code formatter using Black
|
||||
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
|
||||
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
|
||||
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||
|
||||
+1
-8
@@ -27,6 +27,7 @@ node_modules/
|
||||
**/node_modules/
|
||||
frontend/node_modules/
|
||||
frontend/editor/dist/
|
||||
frontend/dist-portal/
|
||||
frontend/editor/playwright-report/
|
||||
.npm/
|
||||
.yarn/
|
||||
@@ -96,14 +97,6 @@ configs/
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
|
||||
# Python virtualenvs. Large, platform-specific, and their symlinks break the build.
|
||||
.venv/
|
||||
**/.venv/
|
||||
venv/
|
||||
**/venv/
|
||||
*.egg-info/
|
||||
**/*.egg-info/
|
||||
|
||||
# Local env
|
||||
.env
|
||||
.env.*
|
||||
|
||||
+13
-2
@@ -22,15 +22,26 @@ indent_size = 4
|
||||
|
||||
[*.html]
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{*.js,*.jsx,*.mjs,*.ts,*.tsx,*.mts}]
|
||||
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
||||
indent_size = 2
|
||||
|
||||
[*.{json,jsonc}]
|
||||
[*.jsonc]
|
||||
# JSONC (JSON with comments) files also follow the standard JSON formatting with an indent size of 2 spaces.
|
||||
indent_size = 2
|
||||
|
||||
+13
-23
@@ -1,28 +1,18 @@
|
||||
# Review ownership is assigned to teams where possible.
|
||||
# Teams can only contain org members, so outside collaborators are listed by hand.
|
||||
#
|
||||
# @Stirling-Tools/maintainers - Frooodle, jbrunton96, ConnorYoh
|
||||
# @Stirling-Tools/backend-reviewers - Frooodle, jbrunton96, ConnorYoh
|
||||
# @Stirling-Tools/frontend-reviewers - Frooodle, jbrunton96, ConnorYoh, reecebrowne, EthanHealy01
|
||||
# @Stirling-Tools/devops-reviewers - Frooodle, jbrunton96, ConnorYoh
|
||||
# @Stirling-Tools/all - all of the above
|
||||
#
|
||||
# Outside collaborators (need Write access to count as owners): @Ludy87 @balazs-szucs
|
||||
|
||||
# Default owners for everything
|
||||
* @Stirling-Tools/maintainers @Ludy87
|
||||
# All PRs must be approved by Frooodle or Ludy87
|
||||
* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
|
||||
|
||||
# Backend
|
||||
/app/** @Stirling-Tools/backend-reviewers @Ludy87 @balazs-szucs
|
||||
/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh @balazs-szucs
|
||||
|
||||
# V2 frontend
|
||||
/frontend/** @Stirling-Tools/frontend-reviewers @balazs-szucs
|
||||
/app/core/src/main/resources/static/** @Stirling-Tools/frontend-reviewers @Ludy87 @balazs-szucs
|
||||
#V2 frontend
|
||||
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @balazs-szucs
|
||||
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 @balazs-szucs
|
||||
|
||||
# V2 docker
|
||||
/docker/backend/** @Stirling-Tools/devops-reviewers @Ludy87
|
||||
/docker/frontend/** @Stirling-Tools/frontend-reviewers @Stirling-Tools/devops-reviewers @Ludy87
|
||||
/docker/compose/** @Stirling-Tools/frontend-reviewers @Stirling-Tools/devops-reviewers @Ludy87
|
||||
#V2 docker
|
||||
/docker/backend/** @Frooodle @Ludy87 @DarioGii
|
||||
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
|
||||
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
|
||||
|
||||
# GHA (all users)
|
||||
/.github/** @Stirling-Tools/all @Ludy87 @balazs-szucs
|
||||
|
||||
#GHA (All users)
|
||||
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 @balazs-szucs
|
||||
|
||||
@@ -98,13 +98,6 @@ body:
|
||||
description: |
|
||||
If you have any additional information that might help us understand and resolve the issue, provide it here.
|
||||
|
||||
- type: textarea
|
||||
id: sample-files
|
||||
attributes:
|
||||
label: Sample Files
|
||||
description: |
|
||||
If possible, attach the PDF or other input files needed to reproduce the issue. Remove any sensitive information before sharing.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
||||
@@ -67,13 +67,6 @@ body:
|
||||
description: |
|
||||
If you have any additional information, comments, or resources you think would support or be relevant to your feature request, include them here.
|
||||
|
||||
- type: textarea
|
||||
id: sample-files
|
||||
attributes:
|
||||
label: Example Files
|
||||
description: |
|
||||
If the feature request depends on specific PDFs or other example files, attach them here when available. Remove any sensitive information before sharing.
|
||||
|
||||
- type: checkboxes
|
||||
id: search-confirmation
|
||||
attributes:
|
||||
|
||||
+16
-79
@@ -1,53 +1,16 @@
|
||||
# CI routing infrastructure. Changes to the top-level router (build.yml) or
|
||||
# this filter configuration rerun every area's jobs. Every job-gating filter
|
||||
# therefore includes *ci, so routing changes exercise the jobs they affect
|
||||
# instead of matching only the project filter.
|
||||
ci: &ci
|
||||
- .github/workflows/build.yml
|
||||
- .github/workflows/gradle-cache-prime.yml
|
||||
- .github/config/.files.yaml
|
||||
|
||||
build: &build
|
||||
- *ci
|
||||
- buildSrc/**
|
||||
- build.gradle
|
||||
- gradle/spotless.gradle
|
||||
- app/(common|core|proprietary|saas)/build.gradle
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
- Taskfile.yml
|
||||
- .taskfiles/backend.yml
|
||||
- .github/workflows/check-licence.yml
|
||||
|
||||
# Backend build inputs. This is intentionally broader than `build`: Java and
|
||||
# backend resource changes must exercise the backend matrix even when Gradle
|
||||
# build scripts themselves are unchanged.
|
||||
backend: &backend
|
||||
- *ci
|
||||
- *build
|
||||
- gradle/**
|
||||
- gradle.properties
|
||||
- gradlew
|
||||
- gradlew.bat
|
||||
- settings.gradle
|
||||
- app/(common|core|proprietary|saas)/src/(main|test)/java/**
|
||||
- "app/(common|core|proprietary|saas)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||
- scripts/db-migration/**
|
||||
- .github/workflows/backend-build.yml
|
||||
|
||||
openapi: &openapi
|
||||
- *ci
|
||||
- *build
|
||||
- app/(common|core|proprietary|saas)/src/main/java/**
|
||||
- .github/workflows/check-openapi.yml
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
|
||||
docker-base: &docker-base
|
||||
- docker/base/Dockerfile
|
||||
|
||||
# Dockerfiles only (base, embedded, and unoserver). The slow multi-architecture
|
||||
# (arm64) leg of the PR Docker test build runs only when a Dockerfile changes,
|
||||
# rather than for every code PR.
|
||||
dockerfiles: &dockerfiles
|
||||
- docker/**/Dockerfile*
|
||||
|
||||
docker: &docker
|
||||
- docker/embedded/Dockerfile
|
||||
- docker/embedded/Dockerfile.fat
|
||||
@@ -60,11 +23,13 @@ docker: &docker
|
||||
- *docker-base
|
||||
|
||||
project: &project
|
||||
- *ci
|
||||
- app/(common|core|proprietary|saas)/src/(main|test)/java/**
|
||||
- app/(common|core|proprietary)/src/(main|test)/java/**
|
||||
- *build
|
||||
- "app/(common|core|proprietary|saas)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||
- "app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||
- exampleYmlFiles/**
|
||||
- gradle/**
|
||||
- libs/**
|
||||
- "testing/**/!(requirements*.txt|requirements*.in)*"
|
||||
- *docker
|
||||
- *docker-base
|
||||
- gradle.properties
|
||||
@@ -80,12 +45,10 @@ project: &project
|
||||
- .taskfiles/docker.yml
|
||||
- scripts/db-migration/**
|
||||
- .github/workflows/db-migration-test.yml
|
||||
- .github/workflows/docker-compose-tests.yml
|
||||
- .github/workflows/test-build-docker.yml
|
||||
|
||||
frontend: &frontend
|
||||
- *ci
|
||||
- frontend/**
|
||||
- .github/workflows/testdriver.yml
|
||||
- testing/**
|
||||
- docker/**
|
||||
- scripts/translations/*.py
|
||||
@@ -100,15 +63,10 @@ frontend: &frontend
|
||||
- Taskfile.yml
|
||||
- .taskfiles/frontend.yml
|
||||
- .taskfiles/e2e.yml
|
||||
- .github/workflows/frontend-validation.yml
|
||||
- .github/workflows/frontend-a11y.yml
|
||||
- .github/workflows/e2e-stubbed.yml
|
||||
- .github/workflows/e2e-live.yml
|
||||
|
||||
# Files that affect the Tauri desktop bundle. Changes to any of these files
|
||||
# trigger the multi-OS Tauri build job.
|
||||
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
|
||||
# job on changes to any of these.
|
||||
tauri: &tauri
|
||||
- *ci
|
||||
- frontend/editor/src-tauri/**
|
||||
- frontend/editor/src/desktop/**
|
||||
- frontend/editor/tsconfig.desktop.vite.json
|
||||
@@ -119,35 +77,16 @@ tauri: &tauri
|
||||
- Taskfile.yml
|
||||
- .taskfiles/desktop.yml
|
||||
|
||||
# Files that affect the AI engine, including its Python tool models, fixers,
|
||||
# and tests. The engine validation job also runs when the Java tool surfaces
|
||||
# used to generate those models change.
|
||||
# Files that affect the AI engine (Python tool models, fixers, tests). Gate
|
||||
# the engine validation job on changes to engine sources or to the Java
|
||||
# tool surfaces it generates models from.
|
||||
engine: &engine
|
||||
- *ci
|
||||
- engine/**
|
||||
- app/(common|core|proprietary|saas)/src/main/java/**
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
- .github/workflows/ai-engine.yml
|
||||
- Taskfile.yml
|
||||
- .taskfiles/engine.yml
|
||||
|
||||
# Files that can make the committed generated API models (frontend tool API
|
||||
# types and engine tool models) stale: their Java sources, generators,
|
||||
# generated outputs (to catch hand edits), and generation tasks. Broad
|
||||
# frontend, Docker, and testing globs are intentionally excluded, so a CSS-only
|
||||
# PR does not start the backend to rebuild the specification.
|
||||
generated-models: &generated-models
|
||||
- *ci
|
||||
- *openapi
|
||||
- frontend/editor/scripts/generate-tool-api-types.mts
|
||||
- frontend/editor/src/core/types/toolApiTypes.ts
|
||||
- frontend/editor/src/core/types/toolIO.ts
|
||||
- engine/scripts/generate_tool_models.py
|
||||
- engine/src/stirling/models/tool_models.py
|
||||
- engine/src/stirling/models/tool_io.py
|
||||
- .taskfiles/frontend.yml
|
||||
- .taskfiles/engine.yml
|
||||
- .github/workflows/check-generated-models.yml
|
||||
|
||||
licenses-frontend: &licenses-frontend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
- "frontend/package.json"
|
||||
@@ -158,10 +97,9 @@ licenses-backend: &licenses-backend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
- *build
|
||||
|
||||
# Files that can affect premium or enterprise behaviour. Changes to any of
|
||||
# these files trigger the enterprise Playwright job for pull requests.
|
||||
# Files that can affect premium / enterprise behaviour. Gate the enterprise
|
||||
# Playwright job on changes to any of these on PRs.
|
||||
proprietary: &proprietary
|
||||
- *ci
|
||||
- app/proprietary/**
|
||||
- frontend/editor/src/proprietary/**
|
||||
- frontend/editor/src/core/tests/enterprise/**
|
||||
@@ -176,5 +114,4 @@ proprietary: &proprietary
|
||||
- configs/settings.yml.template
|
||||
- build.gradle
|
||||
- app/proprietary/build.gradle
|
||||
- gradle/spotless.gradle
|
||||
- .github/workflows/build-enterprise.yml
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"LaserKaspar",
|
||||
"sbplat",
|
||||
"reecebrowne",
|
||||
"DarioGii",
|
||||
"ConnorYoh",
|
||||
"EthanHealy01",
|
||||
"jbrunton96",
|
||||
|
||||
+11
-30
@@ -8,16 +8,14 @@ updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directories:
|
||||
- "/" # Location of package manifests
|
||||
- "/app/common"
|
||||
- "/app/core"
|
||||
- "/app/proprietary"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
groups:
|
||||
simple-java-mail:
|
||||
patterns:
|
||||
- "org.simplejavamail:simple-java-mail"
|
||||
- "org.simplejavamail:outlook-module"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
@@ -34,19 +32,6 @@ updates:
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
groups:
|
||||
ubuntu:
|
||||
patterns:
|
||||
- "ubuntu"
|
||||
eclipse-temurin:
|
||||
patterns:
|
||||
- "eclipse-temurin"
|
||||
uv:
|
||||
patterns:
|
||||
- "ghcr.io/astral-sh/uv"
|
||||
gradle:
|
||||
patterns:
|
||||
- "gradle"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
@@ -60,7 +45,6 @@ updates:
|
||||
directories:
|
||||
- /devTools
|
||||
- /frontend
|
||||
- /testing/compose/mcp-client-check
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
@@ -89,13 +73,14 @@ updates:
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
tanstack:
|
||||
typescript-eslint:
|
||||
patterns:
|
||||
- "@tanstack/*"
|
||||
typescript:
|
||||
- "@typescript-eslint/*"
|
||||
- "typescript-eslint"
|
||||
eslint:
|
||||
patterns:
|
||||
- "typescript"
|
||||
- "@typescript/*"
|
||||
- "eslint"
|
||||
- "@eslint/*"
|
||||
vite:
|
||||
patterns:
|
||||
- "vite"
|
||||
@@ -124,10 +109,6 @@ updates:
|
||||
patterns:
|
||||
- "@posthog/*"
|
||||
- "posthog-js"
|
||||
storybook:
|
||||
patterns:
|
||||
- "storybook"
|
||||
- "@storybook/*"
|
||||
supabase:
|
||||
patterns:
|
||||
- "@supabase/*"
|
||||
@@ -174,8 +155,8 @@ updates:
|
||||
- "tokio"
|
||||
- "tokio-*"
|
||||
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/engine"
|
||||
- package-ecosystem: pip
|
||||
directory: /testing/cucumber
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
|
||||
@@ -63,11 +63,9 @@ labels:
|
||||
files:
|
||||
- 'app/core/src/main/resources/static/.*'
|
||||
- 'app/proprietary/src/main/resources/static/.*'
|
||||
- 'app/saas/src/main/resources/static/.*'
|
||||
- 'frontend/**'
|
||||
- 'frontend/.*'
|
||||
- 'frontend/**/.*'
|
||||
- '.taskfiles/frontend.yml'
|
||||
|
||||
- label: 'Tauri'
|
||||
files:
|
||||
@@ -85,7 +83,6 @@ labels:
|
||||
- 'app/common/src/main/java/.*.java'
|
||||
- 'app/proprietary/src/main/java/.*.java'
|
||||
- 'app/core/src/main/java/.*.java'
|
||||
- 'app/saas/src/main/java/.*.java'
|
||||
|
||||
- label: 'Back End'
|
||||
files:
|
||||
@@ -93,9 +90,6 @@ labels:
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/.*'
|
||||
- 'app/core/src/main/resources/settings.yml.template'
|
||||
- 'app/core/src/main/resources/application.properties'
|
||||
- 'app/proprietary/src/main/resources/application-proprietary.properties'
|
||||
- 'app/saas/src/main/resources/application-dev.properties'
|
||||
- 'app/saas/src/main/resources/application-saas.properties'
|
||||
- 'app/core/src/main/resources/banner.txt'
|
||||
- 'app/core/src/main/resources/static/python/png_to_webp.py'
|
||||
- 'app/core/src/main/resources/static/python/split_photos.py'
|
||||
@@ -159,12 +153,11 @@ labels:
|
||||
- 'app/common/src/test/.*'
|
||||
- 'app/proprietary/src/test/.*'
|
||||
- 'app/core/src/test/.*'
|
||||
- 'app/saas/src/test/.*'
|
||||
- 'testing/.*'
|
||||
- '.github/workflows/scorecards.yml'
|
||||
- 'exampleYmlFiles/test_cicd.yml'
|
||||
|
||||
- label: 'GitHub'
|
||||
- label: 'Github'
|
||||
files:
|
||||
- '.github/.*'
|
||||
|
||||
@@ -178,4 +171,3 @@ labels:
|
||||
- 'app/common/build.gradle'
|
||||
- 'app/proprietary/build.gradle'
|
||||
- 'app/core/build.gradle'
|
||||
- 'app/saas/build.gradle'
|
||||
|
||||
+7
-11
@@ -5,7 +5,6 @@
|
||||
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
||||
- name: "Licenses"
|
||||
color: "EDEDED"
|
||||
description: "Issues or pull requests related to licenses"
|
||||
from_name: "licenses"
|
||||
- name: "Back End"
|
||||
color: "20CE6C"
|
||||
@@ -147,21 +146,21 @@
|
||||
description: "Changes that do not affect the meaning of the code (formatting, etc.)"
|
||||
- name: "admin"
|
||||
color: "195055"
|
||||
- name: "GitHub"
|
||||
- name: "codex"
|
||||
color: "ededed"
|
||||
description: null
|
||||
- name: "Github"
|
||||
color: "0052CC"
|
||||
description: "Issues or pull requests related to GitHub configuration and integrations"
|
||||
from_name: "Github"
|
||||
- name: "github_actions"
|
||||
color: "000000"
|
||||
description: "Pull requests that update GitHub Actions code"
|
||||
- name: "needs-changes"
|
||||
color: "A65A86"
|
||||
description: "Pull requests that require changes before they can be merged"
|
||||
- name: "on-hold"
|
||||
color: "2526F9"
|
||||
- name: "python"
|
||||
color: "2b67c6"
|
||||
description: "Pull requests that update Python code"
|
||||
- name: "engine"
|
||||
color: "2b67c6"
|
||||
description: "Issues or pull requests related to the engine"
|
||||
- name: "size:L"
|
||||
color: "eb9500"
|
||||
description: "This PR changes 100-499 lines ignoring generated files."
|
||||
@@ -202,6 +201,3 @@
|
||||
- name: "license-review-required"
|
||||
color: "EDEDED"
|
||||
description: "This PR requires a license review"
|
||||
- name: "has conflicts"
|
||||
color: "D93F0B"
|
||||
description: "Pull request has merge conflicts with the base branch"
|
||||
|
||||
@@ -20,7 +20,6 @@ Closes #(issue_number)
|
||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable)
|
||||
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable)
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] Every comment I added says something the code does not ([guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/CODE_COMMENTS.md))
|
||||
- [ ] My changes generate no new warnings
|
||||
|
||||
### Documentation
|
||||
|
||||
@@ -19,9 +19,9 @@ import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
import tomli_w # For writing TOML files
|
||||
|
||||
|
||||
@@ -133,7 +133,11 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
file_path = Path(file_path)
|
||||
language_dir = file_path.parent.name
|
||||
reference_lang_dir = reference_file.parent.name
|
||||
if language_dir == reference_lang_dir or file_path.suffix != ".toml" or file_path.parents[1].name != "locales":
|
||||
if (
|
||||
language_dir == reference_lang_dir
|
||||
or file_path.suffix != ".toml"
|
||||
or file_path.parents[1].name != "locales"
|
||||
):
|
||||
print(f"Skipping file: {file_path}")
|
||||
continue
|
||||
|
||||
@@ -194,7 +198,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.is_relative_to(base_dir):
|
||||
has_differences = True
|
||||
report.append(f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n")
|
||||
report.append(
|
||||
f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n"
|
||||
)
|
||||
continue
|
||||
|
||||
# Verify file size before processing
|
||||
@@ -208,7 +214,10 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
if basename_current_file == basename_reference_file and locale_dir == "en-US":
|
||||
continue
|
||||
|
||||
if file_normpath.suffix != ".toml" or basename_current_file != "translation.toml":
|
||||
if (
|
||||
file_normpath.suffix != ".toml"
|
||||
or basename_current_file != "translation.toml"
|
||||
):
|
||||
continue
|
||||
|
||||
only_reference_file = False
|
||||
@@ -252,7 +261,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
)
|
||||
report.append("")
|
||||
report.append(" Use the following command to remove them:")
|
||||
report.append(f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`")
|
||||
report.append(
|
||||
f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`"
|
||||
)
|
||||
report.append("")
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
@@ -260,7 +271,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
)
|
||||
report.append("")
|
||||
report.append(" Use the following command to add them:")
|
||||
report.append(f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`")
|
||||
report.append(
|
||||
f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`"
|
||||
)
|
||||
report.append("")
|
||||
|
||||
if missing_keys_list or extra_keys_list:
|
||||
@@ -275,7 +288,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
output = "\n".join(
|
||||
[
|
||||
f" - `{key}`: first at {first}, duplicate at `{duplicate}`"
|
||||
for key, first, duplicate in find_duplicate_keys(branch_path / file_normpath)
|
||||
for key, first, duplicate in find_duplicate_keys(
|
||||
branch_path / file_normpath
|
||||
)
|
||||
]
|
||||
)
|
||||
report.append("3. **Test Status:** ❌ **_Failed_**")
|
||||
@@ -298,14 +313,18 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(f"Thanks @{actor} for your help in keeping the translations up to date.")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Find missing keys in TOML translation files")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Find missing keys in TOML translation files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
pip
|
||||
setuptools
|
||||
WeasyPrint
|
||||
pdf2image
|
||||
pillow
|
||||
unoserver
|
||||
opencv-python-headless
|
||||
pre-commit
|
||||
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||
@@ -0,0 +1,515 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||
#
|
||||
# WARNING: pip install will require the following package to be hashed.
|
||||
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
|
||||
# CVE-2025-6176 mitigation: pin brotli to a specific commit
|
||||
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# fonttools
|
||||
cffi==2.0.0 \
|
||||
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
|
||||
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
|
||||
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
|
||||
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
|
||||
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
|
||||
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
|
||||
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
|
||||
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
|
||||
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
|
||||
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
|
||||
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
|
||||
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
|
||||
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
|
||||
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
|
||||
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
|
||||
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
|
||||
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
|
||||
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
|
||||
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
|
||||
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
|
||||
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
|
||||
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
|
||||
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
|
||||
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
|
||||
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
|
||||
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
|
||||
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
|
||||
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
|
||||
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
|
||||
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
|
||||
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
|
||||
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
|
||||
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
|
||||
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
|
||||
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
|
||||
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
|
||||
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
|
||||
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
|
||||
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
|
||||
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
|
||||
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
|
||||
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
|
||||
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
|
||||
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
|
||||
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
|
||||
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
|
||||
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
|
||||
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
|
||||
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
|
||||
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
|
||||
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
|
||||
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
|
||||
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
|
||||
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
|
||||
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
|
||||
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
|
||||
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
|
||||
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
|
||||
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
|
||||
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
|
||||
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
|
||||
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
|
||||
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
|
||||
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
|
||||
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
|
||||
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
|
||||
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
|
||||
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
|
||||
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
|
||||
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
|
||||
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
|
||||
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
|
||||
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
|
||||
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
|
||||
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
|
||||
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
|
||||
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
|
||||
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
|
||||
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
|
||||
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
|
||||
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
|
||||
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
|
||||
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
||||
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||
# via weasyprint
|
||||
cfgv==3.5.0 \
|
||||
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||
# via pre-commit
|
||||
cssselect2==0.9.0 \
|
||||
--hash=sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 \
|
||||
--hash=sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb
|
||||
# via weasyprint
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.29.0 \
|
||||
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
|
||||
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
fonttools==4.62.1 \
|
||||
--hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \
|
||||
--hash=sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a \
|
||||
--hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \
|
||||
--hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \
|
||||
--hash=sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82 \
|
||||
--hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \
|
||||
--hash=sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b \
|
||||
--hash=sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e \
|
||||
--hash=sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416 \
|
||||
--hash=sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae \
|
||||
--hash=sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069 \
|
||||
--hash=sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9 \
|
||||
--hash=sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 \
|
||||
--hash=sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed \
|
||||
--hash=sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800 \
|
||||
--hash=sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e \
|
||||
--hash=sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9 \
|
||||
--hash=sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b \
|
||||
--hash=sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 \
|
||||
--hash=sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe \
|
||||
--hash=sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7 \
|
||||
--hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \
|
||||
--hash=sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 \
|
||||
--hash=sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23 \
|
||||
--hash=sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae \
|
||||
--hash=sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260 \
|
||||
--hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \
|
||||
--hash=sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87 \
|
||||
--hash=sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24 \
|
||||
--hash=sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53 \
|
||||
--hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \
|
||||
--hash=sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14 \
|
||||
--hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \
|
||||
--hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \
|
||||
--hash=sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1 \
|
||||
--hash=sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca \
|
||||
--hash=sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c \
|
||||
--hash=sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d \
|
||||
--hash=sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a \
|
||||
--hash=sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782 \
|
||||
--hash=sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c \
|
||||
--hash=sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a \
|
||||
--hash=sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 \
|
||||
--hash=sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3 \
|
||||
--hash=sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7 \
|
||||
--hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d \
|
||||
--hash=sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 \
|
||||
--hash=sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4 \
|
||||
--hash=sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68 \
|
||||
--hash=sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca
|
||||
# via weasyprint
|
||||
identify==2.6.19 \
|
||||
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
|
||||
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
|
||||
# via pre-commit
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
# via pre-commit
|
||||
numpy==2.4.4 \
|
||||
--hash=sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed \
|
||||
--hash=sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50 \
|
||||
--hash=sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959 \
|
||||
--hash=sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827 \
|
||||
--hash=sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd \
|
||||
--hash=sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233 \
|
||||
--hash=sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc \
|
||||
--hash=sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b \
|
||||
--hash=sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7 \
|
||||
--hash=sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e \
|
||||
--hash=sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a \
|
||||
--hash=sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d \
|
||||
--hash=sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3 \
|
||||
--hash=sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e \
|
||||
--hash=sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb \
|
||||
--hash=sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a \
|
||||
--hash=sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0 \
|
||||
--hash=sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e \
|
||||
--hash=sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113 \
|
||||
--hash=sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103 \
|
||||
--hash=sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93 \
|
||||
--hash=sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af \
|
||||
--hash=sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5 \
|
||||
--hash=sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7 \
|
||||
--hash=sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392 \
|
||||
--hash=sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c \
|
||||
--hash=sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4 \
|
||||
--hash=sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40 \
|
||||
--hash=sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf \
|
||||
--hash=sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44 \
|
||||
--hash=sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b \
|
||||
--hash=sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5 \
|
||||
--hash=sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e \
|
||||
--hash=sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74 \
|
||||
--hash=sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0 \
|
||||
--hash=sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e \
|
||||
--hash=sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec \
|
||||
--hash=sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015 \
|
||||
--hash=sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d \
|
||||
--hash=sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d \
|
||||
--hash=sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842 \
|
||||
--hash=sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150 \
|
||||
--hash=sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8 \
|
||||
--hash=sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a \
|
||||
--hash=sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed \
|
||||
--hash=sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f \
|
||||
--hash=sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008 \
|
||||
--hash=sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e \
|
||||
--hash=sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0 \
|
||||
--hash=sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e \
|
||||
--hash=sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f \
|
||||
--hash=sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a \
|
||||
--hash=sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40 \
|
||||
--hash=sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7 \
|
||||
--hash=sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83 \
|
||||
--hash=sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d \
|
||||
--hash=sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c \
|
||||
--hash=sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871 \
|
||||
--hash=sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502 \
|
||||
--hash=sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252 \
|
||||
--hash=sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8 \
|
||||
--hash=sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115 \
|
||||
--hash=sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f \
|
||||
--hash=sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e \
|
||||
--hash=sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d \
|
||||
--hash=sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0 \
|
||||
--hash=sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119 \
|
||||
--hash=sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e \
|
||||
--hash=sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db \
|
||||
--hash=sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121 \
|
||||
--hash=sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d \
|
||||
--hash=sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e
|
||||
# via opencv-python-headless
|
||||
opencv-python-headless==4.13.0.92 \
|
||||
--hash=sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22 \
|
||||
--hash=sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e \
|
||||
--hash=sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209 \
|
||||
--hash=sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c \
|
||||
--hash=sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb \
|
||||
--hash=sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6 \
|
||||
--hash=sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b \
|
||||
--hash=sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pdf2image==1.17.0 \
|
||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pillow==12.2.0 \
|
||||
--hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \
|
||||
--hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \
|
||||
--hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \
|
||||
--hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \
|
||||
--hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \
|
||||
--hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \
|
||||
--hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \
|
||||
--hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \
|
||||
--hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \
|
||||
--hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \
|
||||
--hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \
|
||||
--hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \
|
||||
--hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \
|
||||
--hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \
|
||||
--hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \
|
||||
--hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \
|
||||
--hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \
|
||||
--hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \
|
||||
--hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \
|
||||
--hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \
|
||||
--hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \
|
||||
--hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \
|
||||
--hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \
|
||||
--hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \
|
||||
--hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \
|
||||
--hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \
|
||||
--hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \
|
||||
--hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \
|
||||
--hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \
|
||||
--hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \
|
||||
--hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \
|
||||
--hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \
|
||||
--hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \
|
||||
--hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \
|
||||
--hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \
|
||||
--hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \
|
||||
--hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \
|
||||
--hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \
|
||||
--hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \
|
||||
--hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \
|
||||
--hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \
|
||||
--hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \
|
||||
--hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \
|
||||
--hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \
|
||||
--hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \
|
||||
--hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \
|
||||
--hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \
|
||||
--hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \
|
||||
--hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \
|
||||
--hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \
|
||||
--hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \
|
||||
--hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \
|
||||
--hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \
|
||||
--hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \
|
||||
--hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \
|
||||
--hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \
|
||||
--hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \
|
||||
--hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \
|
||||
--hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \
|
||||
--hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \
|
||||
--hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \
|
||||
--hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \
|
||||
--hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \
|
||||
--hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \
|
||||
--hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \
|
||||
--hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \
|
||||
--hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \
|
||||
--hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \
|
||||
--hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \
|
||||
--hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \
|
||||
--hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \
|
||||
--hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \
|
||||
--hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \
|
||||
--hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \
|
||||
--hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \
|
||||
--hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \
|
||||
--hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \
|
||||
--hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \
|
||||
--hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \
|
||||
--hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \
|
||||
--hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \
|
||||
--hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \
|
||||
--hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \
|
||||
--hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \
|
||||
--hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \
|
||||
--hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \
|
||||
--hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \
|
||||
--hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \
|
||||
--hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \
|
||||
--hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \
|
||||
--hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.9.6 \
|
||||
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
|
||||
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
pre-commit==4.6.0 \
|
||||
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
|
||||
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via cffi
|
||||
pydyf==0.12.1 \
|
||||
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
|
||||
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||
# via weasyprint
|
||||
python-discovery==1.2.2 \
|
||||
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
|
||||
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
|
||||
# via virtualenv
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
tinycss2==1.5.1 \
|
||||
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
|
||||
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
tinyhtml5==2.1.0 \
|
||||
--hash=sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67 \
|
||||
--hash=sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a
|
||||
# via weasyprint
|
||||
unoserver==3.6 \
|
||||
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
|
||||
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==21.2.4 \
|
||||
--hash=sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac \
|
||||
--hash=sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada
|
||||
# via pre-commit
|
||||
weasyprint==68.1 \
|
||||
--hash=sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be \
|
||||
--hash=sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||
# via
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
zopfli==0.4.1 \
|
||||
--hash=sha256:02086247dd12fda929f9bfe8b3962b6bcdbfc8c82e99255aebcf367867cf0760 \
|
||||
--hash=sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e \
|
||||
--hash=sha256:27823dc1161a4031d1c25925fd45d9868ec0cbc7692341830a7dcfa25063662c \
|
||||
--hash=sha256:2f992ac7d83cbddd889e1813ace576cbc91a05d5d7a0a21b366e2e5f492e7707 \
|
||||
--hash=sha256:4238d4d746d1095e29c9125490985e0c12ffd3654f54a24af551e2391e936d54 \
|
||||
--hash=sha256:5a4c22b6161f47f5bd34637dbaee6735abd287cd64e0d1ce28ef1871bf625f4b \
|
||||
--hash=sha256:84a31ba9edc921b1d3a4449929394a993888f32d70de3a3617800c428a947b9b \
|
||||
--hash=sha256:a899eca405662a23ae75054affa3517a060362eae1185d3d791c86a50153c4dd \
|
||||
--hash=sha256:a93c2ecafff372de6c0aa2212eff18a75f6c71a100372fee7b4b129cc0b6f9a7 \
|
||||
--hash=sha256:cb136a74d14a4ecfae29cb0fdecece58a6c115abc9a74c12bc6ac62e80f229d7 \
|
||||
--hash=sha256:d7bcee1b189d64ec33d1e05cfa1b6a1268c29329c382f6ca1bd6245b04925c57 \
|
||||
--hash=sha256:fdfb7ce9f5de37a5b2f75dd2642fd7717956ef2a72e0387302a36d382440db07
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==26.0.1 \
|
||||
--hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \
|
||||
--hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==82.0.1 \
|
||||
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
@@ -0,0 +1,2 @@
|
||||
tomlkit
|
||||
tomli-w
|
||||
@@ -0,0 +1,14 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||
#
|
||||
tomli-w==1.2.0 \
|
||||
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
tomlkit==0.14.0 \
|
||||
--hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \
|
||||
--hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
@@ -4,18 +4,19 @@
|
||||
Usage: verify-updater-signatures.py <dir-to-scan> [tauri.conf.json]
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
ART_ROOT = Path(sys.argv[1])
|
||||
CONF = Path(sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json")
|
||||
CONF = Path(
|
||||
sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json"
|
||||
)
|
||||
|
||||
|
||||
def load_pubkey():
|
||||
|
||||
@@ -26,10 +26,6 @@ jobs:
|
||||
check-pr:
|
||||
if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
# Only reads the PR via pulls.get with the default GITHUB_TOKEN.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||
@@ -39,7 +35,7 @@ jobs:
|
||||
pr_ref: ${{ steps.resolve.outputs.ref }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -90,7 +86,7 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
else
|
||||
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs")
|
||||
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs")
|
||||
is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done
|
||||
if [ "$is_auth" = true ]; then
|
||||
should=true
|
||||
@@ -101,7 +97,6 @@ jobs:
|
||||
echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
environment: pr-preview
|
||||
needs: check-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true')
|
||||
@@ -112,7 +107,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
packages: write
|
||||
pull-requests: write
|
||||
env:
|
||||
# Single source of truth for whether this preview embeds the admin portal:
|
||||
@@ -121,7 +115,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -131,11 +125,20 @@ jobs:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Add deployment started comment
|
||||
id: deployment-started
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
@@ -177,12 +180,11 @@ jobs:
|
||||
with:
|
||||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||
# untrusted tree is built below - never leave credentials in .git/config
|
||||
persist-credentials: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -190,16 +192,11 @@ jobs:
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hash for app
|
||||
id: commit-hash
|
||||
@@ -223,7 +220,7 @@ jobs:
|
||||
- name: Check if image exists
|
||||
id: check-image
|
||||
run: |
|
||||
if docker manifest inspect ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Image already exists, skipping build"
|
||||
else
|
||||
@@ -231,8 +228,6 @@ jobs:
|
||||
echo "Image needs to be built"
|
||||
fi
|
||||
|
||||
env:
|
||||
IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
|
||||
- name: Build and push V2 image
|
||||
if: steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
@@ -242,7 +237,7 @@ jobs:
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-latest
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
||||
tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
BUILD_PORTAL=${{ env.BUILD_PORTAL }}
|
||||
@@ -251,11 +246,9 @@ jobs:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
env:
|
||||
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
- name: Deploy V2 to VPS
|
||||
id: deploy
|
||||
run: |
|
||||
@@ -268,7 +261,7 @@ jobs:
|
||||
services:
|
||||
stirling-pdf-v2:
|
||||
container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
ports:
|
||||
- "${V2_PORT}:8080"
|
||||
volumes:
|
||||
@@ -280,8 +273,8 @@ jobs:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${TEST_LOGIN_USERNAME}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${TEST_LOGIN_PASSWORD}"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
|
||||
@@ -295,9 +288,9 @@ jobs:
|
||||
EOF
|
||||
|
||||
# Deploy to VPS
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose-v2.yml
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose-v2.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||
# Create V2 PR-specific directories
|
||||
mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs,storage}
|
||||
|
||||
@@ -322,19 +315,12 @@ jobs:
|
||||
# Set port for output
|
||||
echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
|
||||
TEST_LOGIN_USERNAME: ${{ secrets.TEST_LOGIN_USERNAME }}
|
||||
TEST_LOGIN_PASSWORD: ${{ secrets.TEST_LOGIN_PASSWORD }}
|
||||
NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
|
||||
# ---- Storybook preview (only when this PR touches stories/.storybook) ----
|
||||
# Runs inside the same approved-contributor-gated deploy job, so it deploys
|
||||
# under the exact same access rules as the app preview.
|
||||
- name: Detect Storybook changes
|
||||
id: sb-changes
|
||||
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
with:
|
||||
list-files: json
|
||||
filters: |
|
||||
@@ -345,7 +331,7 @@ jobs:
|
||||
|
||||
- name: Set up Node.js for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -353,7 +339,7 @@ jobs:
|
||||
|
||||
- name: Install Task for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Build and deploy Storybook
|
||||
id: storybook
|
||||
@@ -393,9 +379,8 @@ jobs:
|
||||
env:
|
||||
SB_URL: ${{ steps.storybook.outputs.url }}
|
||||
SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
@@ -416,7 +401,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${v2Port}`;
|
||||
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${v2Port}`;
|
||||
|
||||
// Only mention the portal when this image actually embeds it.
|
||||
// Use the direct IP URL - the SSL hostname isn't supported yet.
|
||||
@@ -462,10 +447,6 @@ jobs:
|
||||
});
|
||||
|
||||
cleanup-v2-deployment:
|
||||
# Tearing a preview down is not a deployment - no deployment object.
|
||||
environment:
|
||||
name: pr-preview
|
||||
deployment: false
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -475,17 +456,26 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Clean up V2 deployment comments
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
@@ -514,14 +504,12 @@ jobs:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
env:
|
||||
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
- name: Cleanup V2 deployment
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH'
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found V2 PR directory, proceeding with cleanup..."
|
||||
|
||||
@@ -554,11 +542,8 @@ jobs:
|
||||
# Only remove PR-specific containers and directories
|
||||
ENDSSH
|
||||
|
||||
env:
|
||||
NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key docker-compose.yml storybook.tgz
|
||||
rm -f ../private.key
|
||||
continue-on-error: true
|
||||
|
||||
@@ -37,8 +37,7 @@ jobs:
|
||||
check-comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # actions/checkout
|
||||
issues: write # add reaction to the triggering issue comment
|
||||
issues: write
|
||||
if: |
|
||||
vars.CI_PROFILE != 'lite' && (
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
@@ -55,6 +54,7 @@ jobs:
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'balazs-szucs' ||
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
@@ -70,13 +70,22 @@ jobs:
|
||||
enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Get PR data
|
||||
id: get-pr
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -147,7 +156,7 @@ jobs:
|
||||
id: add-eyes-reaction
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`);
|
||||
try {
|
||||
@@ -166,47 +175,49 @@ jobs:
|
||||
}
|
||||
|
||||
deploy-pr:
|
||||
environment: pr-preview
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # actions/checkout, incl. the PR merge ref
|
||||
issues: write # reactions, 'pr-deployed' label, deployment URL comment
|
||||
issues: write
|
||||
pull-requests: write
|
||||
packages: write # push PR image to ghcr.io
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
# untrusted tree gets built below - never leave credentials in .git/config
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-deploy-pr-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
||||
@@ -222,18 +233,13 @@ jobs:
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push PR-specific image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
@@ -243,7 +249,7 @@ jobs:
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-latest
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
||||
tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: |
|
||||
VERSION_TAG=alpha
|
||||
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
|
||||
@@ -253,22 +259,20 @@ jobs:
|
||||
if: needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
context: ./engine
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-engine
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-engine
|
||||
tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
env:
|
||||
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
- name: Deploy to VPS
|
||||
id: deploy
|
||||
run: |
|
||||
@@ -286,11 +290,11 @@ jobs:
|
||||
# Set pro/enterprise settings (enterprise implies pro)
|
||||
if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then
|
||||
PREMIUM_ENABLED="true"
|
||||
PREMIUM_KEY="${ENTERPRISE_KEY}"
|
||||
PREMIUM_KEY="${{ secrets.ENTERPRISE_KEY }}"
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
||||
elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then
|
||||
PREMIUM_ENABLED="true"
|
||||
PREMIUM_KEY="${PRO_KEY}"
|
||||
PREMIUM_KEY="${{ secrets.PREMIUM_KEY }}"
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
||||
else
|
||||
PREMIUM_ENABLED="false"
|
||||
@@ -300,6 +304,7 @@ jobs:
|
||||
|
||||
ENABLE_PROTOTYPES="${{ needs.check-comment.outputs.enable_prototypes }}"
|
||||
PR_NUMBER="${{ needs.check-comment.outputs.pr_number }}"
|
||||
DOCKER_USER="${{ secrets.DOCKER_HUB_USERNAME }}"
|
||||
|
||||
# Build engine env vars for backend (only set when prototypes enabled)
|
||||
if [ "$ENABLE_PROTOTYPES" == "true" ]; then
|
||||
@@ -309,9 +314,9 @@ jobs:
|
||||
ENGINE_SERVICE="
|
||||
stirling-pdf-engine:
|
||||
container_name: stirling-pdf-engine-pr-${PR_NUMBER}
|
||||
image: ${IMAGE_BASE}:engine-pr-${PR_NUMBER}
|
||||
image: ${DOCKER_USER}/test:engine-pr-${PR_NUMBER}
|
||||
environment:
|
||||
ANTHROPIC_API_KEY: \"${ANTHROPIC_API_KEY}\"
|
||||
ANTHROPIC_API_KEY: \"${{ secrets.ANTHROPIC_API_KEY }}\"
|
||||
networks:
|
||||
- pr-network
|
||||
restart: on-failure:5"
|
||||
@@ -334,7 +339,7 @@ jobs:
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: stirling-pdf-pr-${PR_NUMBER}
|
||||
image: ${IMAGE_BASE}:pr-${PR_NUMBER}
|
||||
image: ${DOCKER_USER}/test:pr-${PR_NUMBER}
|
||||
ports:
|
||||
- "${PR_NUMBER}:8080"
|
||||
volumes:
|
||||
@@ -358,9 +363,9 @@ jobs:
|
||||
EOF
|
||||
|
||||
# Then copy the file and execute commands
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||
# Create PR-specific directories
|
||||
mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs}
|
||||
|
||||
@@ -376,19 +381,11 @@ jobs:
|
||||
# Set output for use in PR comment
|
||||
echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV
|
||||
|
||||
env:
|
||||
ENTERPRISE_KEY: ${{ secrets.ENTERPRISE_KEY }}
|
||||
# named PRO_KEY, not PREMIUM_KEY, so the shell var it feeds is not self-referential
|
||||
PRO_KEY: ${{ secrets.PREMIUM_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
|
||||
NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
- name: Add success reaction to comment
|
||||
if: success() && github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
||||
try {
|
||||
@@ -423,7 +420,7 @@ jobs:
|
||||
if: failure() && github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
||||
try {
|
||||
@@ -442,17 +439,15 @@ jobs:
|
||||
- name: Post deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||
const securityStatus = process.env.security_status || "Security Disabled";
|
||||
|
||||
const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${prNumber}`;
|
||||
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${prNumber}`;
|
||||
const commentBody = `## 🚀 PR Test Deployment\n\n` +
|
||||
`Your PR has been deployed for testing!\n\n` +
|
||||
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` +
|
||||
@@ -477,22 +472,26 @@ jobs:
|
||||
handle-label-commands:
|
||||
if: ${{ github.event.issue.pull_request != null }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # actions/checkout, reads repo_devs.json and labels.yml
|
||||
issues: write # add/remove labels, delete the command comment
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Apply label commands
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -7,33 +7,41 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SERVER_IP: ${{ secrets.NEW_VPS_IP }} # Add this to your GitHub secrets
|
||||
CLEANUP_PERFORMED: "false" # Add flag to track if cleanup occurred
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
# Tearing a preview down is not a deployment - no deployment object.
|
||||
environment:
|
||||
name: pr-preview
|
||||
deployment: false
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # actions/checkout
|
||||
pull-requests: write
|
||||
issues: write # list/remove labels, list/delete comments
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Remove 'pr-deployed' label if present
|
||||
id: remove-label-comment
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
const owner = context.repo.owner;
|
||||
@@ -92,22 +100,14 @@ jobs:
|
||||
if: steps.remove-label-comment.outputs.present == 'true'
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
env:
|
||||
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cleanup PR deployment
|
||||
if: steps.remove-label-comment.outputs.present == 'true'
|
||||
id: cleanup
|
||||
# ENDSSH heredoc is quoted, so its body is sent literally: secrets inside it
|
||||
# must stay as GitHub expressions, a shell var would be empty on the remote host.
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH'
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found PR directory, proceeding with cleanup..."
|
||||
|
||||
@@ -122,8 +122,8 @@ jobs:
|
||||
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Remove the Docker images
|
||||
docker rmi --no-prune ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ github.event.pull_request.number }} || true
|
||||
docker rmi --no-prune ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ github.event.pull_request.number }} || true
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "PERFORMED_CLEANUP"
|
||||
else
|
||||
@@ -131,9 +131,6 @@ jobs:
|
||||
echo "NO_CLEANUP_NEEDED"
|
||||
fi
|
||||
ENDSSH
|
||||
env:
|
||||
NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
name: Auto SaaS Dev Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- saas-prod
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FRONTEND_PORT: "901"
|
||||
BACKEND_PORT: "902"
|
||||
DEPLOY_DIR: /stirling/SAAS-DEV
|
||||
|
||||
jobs:
|
||||
deploy-saas-dev:
|
||||
runs-on: ubuntu-latest
|
||||
environment: saas-dev
|
||||
concurrency:
|
||||
group: saas-dev-deploy
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check SaaS configuration
|
||||
id: config
|
||||
env:
|
||||
PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }}
|
||||
run: |
|
||||
echo "supabase_url=https://${PROJECT_REF}.supabase.co" >> "$GITHUB_OUTPUT"
|
||||
echo "meter_endpoint=https://${PROJECT_REF}.supabase.co/functions/v1/meter-payg-units" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit-hash
|
||||
run: echo "app_short=$(git rev-parse --short=8 HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push backend image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-saas-backend
|
||||
cache-to: type=gha,mode=max,scope=stirling-saas-backend
|
||||
tags: |
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-backend-${{ steps.commit-hash.outputs.app_short }}
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-backend-latest
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
STIRLING_FLAVOR=saas
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push frontend image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-saas-frontend
|
||||
cache-to: type=gha,mode=max,scope=stirling-saas-frontend
|
||||
tags: |
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-frontend-${{ steps.commit-hash.outputs.app_short }}
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-frontend-latest
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
STIRLING_FLAVOR=saas
|
||||
VITE_BUILD_MODE=development
|
||||
VITE_SUPABASE_URL=${{ steps.config.outputs.supabase_url }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${{ secrets.SAAS_SUPABASE_PUBLISHABLE_KEY }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push AI engine image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-saas-engine
|
||||
cache-to: type=gha,mode=max,scope=stirling-saas-engine
|
||||
tags: |
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-engine-${{ steps.commit-hash.outputs.app_short }}
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-engine-latest
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "$SSH_KEY" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy to VPS
|
||||
env:
|
||||
IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
|
||||
IMAGE_TAG: ${{ steps.commit-hash.outputs.app_short }}
|
||||
GHCR_USER: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ github.token }}
|
||||
VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
SAAS_DB_URL: ${{ secrets.SAAS_DB_URL }}
|
||||
SAAS_DB_USERNAME: ${{ secrets.SAAS_DB_USERNAME || 'postgres' }}
|
||||
SAAS_DB_PASSWORD: ${{ secrets.SAAS_DB_PASSWORD }}
|
||||
SAAS_DB_PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }}
|
||||
SUPABASE_EDGE_FUNCTION_SECRET: ${{ secrets.SUPABASE_EDGE_FUNCTION_SECRET }}
|
||||
PAYG_METER_ENDPOINT: ${{ steps.config.outputs.meter_endpoint }}
|
||||
STIRLING_KEYGEN_ENABLED: ${{ secrets.KEYGEN_ACCOUNT_ID != '' && secrets.KEYGEN_API_TOKEN != '' && secrets.KEYGEN_POLICY_ID != '' }}
|
||||
KEYGEN_ACCOUNT_ID: ${{ secrets.KEYGEN_ACCOUNT_ID }}
|
||||
KEYGEN_API_TOKEN: ${{ secrets.KEYGEN_API_TOKEN }}
|
||||
KEYGEN_POLICY_ID: ${{ secrets.KEYGEN_POLICY_ID }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="http://${VPS_HOST}:${FRONTEND_PORT}"
|
||||
|
||||
yaml() {
|
||||
printf "'%s'" "$(printf '%s' "$1" | sed -e "s/'/''/g" -e 's/\$/$$/g')"
|
||||
}
|
||||
|
||||
ENGINE_SECRET="$(openssl rand -hex 32)"
|
||||
AI_BACKEND_VARS="
|
||||
SYSTEM_AIENGINE_ENABLED: \"true\"
|
||||
SYSTEM_AIENGINE_URL: \"http://saas-engine:5001\"
|
||||
APP_AI_SERVICEBASEURL: \"http://saas-engine:5001\"
|
||||
STIRLING_ENGINE_SHARED_SECRET: $(yaml "$ENGINE_SECRET")"
|
||||
AI_SERVICE="
|
||||
|
||||
saas-engine:
|
||||
container_name: stirling-saas-dev-engine
|
||||
image: ${IMAGE_BASE}:saas-engine-${IMAGE_TAG}
|
||||
environment:
|
||||
ANTHROPIC_API_KEY: $(yaml "$ANTHROPIC_API_KEY")
|
||||
VOYAGE_API_KEY: $(yaml "$VOYAGE_API_KEY")
|
||||
STIRLING_ENGINE_SHARED_SECRET: $(yaml "$ENGINE_SECRET")
|
||||
restart: on-failure:5"
|
||||
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
saas-backend:
|
||||
container_name: stirling-saas-dev-backend
|
||||
image: ${IMAGE_BASE}:saas-backend-${IMAGE_TAG}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080"
|
||||
volumes:
|
||||
- ${DEPLOY_DIR}/config:/configs:rw
|
||||
- ${DEPLOY_DIR}/logs:/logs:rw
|
||||
- ${DEPLOY_DIR}/storage:/storage:rw
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: "saas"
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
SAAS_DB_URL: $(yaml "$SAAS_DB_URL")
|
||||
SAAS_DB_USERNAME: $(yaml "$SAAS_DB_USERNAME")
|
||||
SAAS_DB_PASSWORD: $(yaml "$SAAS_DB_PASSWORD")
|
||||
SAAS_DB_PROJECT_REF: $(yaml "$SAAS_DB_PROJECT_REF")
|
||||
SUPABASE_EDGE_FUNCTION_SECRET: $(yaml "$SUPABASE_EDGE_FUNCTION_SECRET")
|
||||
PAYG_METER_ENDPOINT: $(yaml "$PAYG_METER_ENDPOINT")
|
||||
STIRLING_KEYGEN_ENABLED: $(yaml "$STIRLING_KEYGEN_ENABLED")
|
||||
KEYGEN_ACCOUNT_ID: $(yaml "$KEYGEN_ACCOUNT_ID")
|
||||
KEYGEN_API_TOKEN: $(yaml "$KEYGEN_API_TOKEN")
|
||||
KEYGEN_POLICY_ID: $(yaml "$KEYGEN_POLICY_ID")
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "${BASE_URL}"
|
||||
baseUrl: "${BASE_URL}"${AI_BACKEND_VARS}
|
||||
restart: on-failure:5
|
||||
|
||||
saas-frontend:
|
||||
container_name: stirling-saas-dev-frontend
|
||||
image: ${IMAGE_BASE}:saas-frontend-${IMAGE_TAG}
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://saas-backend:8080"
|
||||
depends_on:
|
||||
- saas-backend
|
||||
restart: on-failure:5${AI_SERVICE}
|
||||
EOF
|
||||
|
||||
SSH_OPTS=(-i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
|
||||
|
||||
scp "${SSH_OPTS[@]}" docker-compose.yml "${VPS_USERNAME}@${VPS_HOST}:/tmp/saas-dev-docker-compose.yml"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" -T "${VPS_USERNAME}@${VPS_HOST}" << ENDSSH
|
||||
set -e
|
||||
mkdir -p ${DEPLOY_DIR}/{config,logs,storage}
|
||||
mv /tmp/saas-dev-docker-compose.yml ${DEPLOY_DIR}/docker-compose.yml
|
||||
chmod 600 ${DEPLOY_DIR}/docker-compose.yml
|
||||
cd ${DEPLOY_DIR}
|
||||
printf '%s' "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin
|
||||
docker-compose down --remove-orphans 2>/dev/null || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker logout ghcr.io >/dev/null 2>&1 || true
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Wait for the backend to answer
|
||||
env:
|
||||
VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
run: |
|
||||
URL="http://${VPS_HOST}:${BACKEND_PORT}/api/v1/info/status"
|
||||
for i in $(seq 1 60); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL" || true)
|
||||
if [ "$code" = "200" ]; then echo "Healthy after $((i * 10))s"; exit 0; fi
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::SaaS dev backend did not become healthy within 10 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: rm -f ../private.key docker-compose.yml
|
||||
continue-on-error: true
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
is_fork: ${{ steps.decide.outputs.is_fork }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
+102
-18
@@ -1,9 +1,9 @@
|
||||
name: AI Engine CI
|
||||
|
||||
# Runs the engine quality gate (lint, type-check, format-check, tests). Called
|
||||
# from build.yml on PRs and merge_group; also runs directly on push to main as
|
||||
# a post-merge safety net. Freshness of the generated tool_models.py is checked
|
||||
# by the shared check-generated-models workflow.
|
||||
# Validates the Python AI engine: regenerates tool models and runs the
|
||||
# engine quality gate (lint, type-check, format-check, tests). Called from
|
||||
# build.yml on PRs and merge_group; also runs directly on push to main as
|
||||
# a post-merge safety net.
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -28,15 +28,107 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Regenerate tool models
|
||||
run: task engine:tool-models
|
||||
|
||||
- name: Verify tool models are up to date
|
||||
id: tool-models-check
|
||||
continue-on-error: true
|
||||
run: git diff --exit-code engine/src/stirling/models/tool_models.py
|
||||
|
||||
- name: Comment on tool models check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
# issue/PR number, which doesn't exist on merge_group runs.
|
||||
if: steps.tool-models-check.outcome == 'failure' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- tool-models-check -->';
|
||||
const body = [
|
||||
marker,
|
||||
'### Tool Models Check Failed',
|
||||
'',
|
||||
'The generated `engine/src/stirling/models/tool_models.py` is out of date with the Java OpenAPI spec and will need to be regenerated before it can be merged in.',
|
||||
'',
|
||||
'Run `task engine:tool-models` to regenerate, then commit the updated file.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail if tool models check failed
|
||||
if: steps.tool-models-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Tool Models Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "The generated engine/src/stirling/models/tool_models.py"
|
||||
echo "is out of date with the Java OpenAPI spec and will"
|
||||
echo "need to be regenerated before it can be merged in."
|
||||
echo ""
|
||||
echo "Run 'task engine:tool-models' to regenerate, then"
|
||||
echo "commit the updated file."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Remove tool models check comment on success
|
||||
if: steps.tool-models-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- tool-models-check -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Quality-check engine
|
||||
id: engine-check
|
||||
@@ -98,14 +190,6 @@ jobs:
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Build engine production image
|
||||
if: always()
|
||||
run: docker build --file engine/Dockerfile --tag stirling-pdf-engine:ci .
|
||||
|
||||
- name: Build engine development image
|
||||
if: always()
|
||||
run: docker build --file engine/Dockerfile.dev --tag stirling-pdf-engine-dev:ci .
|
||||
|
||||
- name: Remove engine check comment on success
|
||||
if: steps.engine-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
name: AI - PR Title Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
branches: [main]
|
||||
|
||||
permissions: # required for secure-repo hardening
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ai-title-review:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
models: read
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git to suppress detached HEAD warning
|
||||
run: git config --global advice.detachedHead false
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Check if actor is repo developer
|
||||
id: actor
|
||||
run: |
|
||||
if [[ "${{ github.actor }}" == *"[bot]" ]]; then
|
||||
echo "PR opened by a bot – skipping AI title review."
|
||||
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -f .github/config/repo_devs.json ]; then
|
||||
echo "Error: .github/config/repo_devs.json not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Validate JSON and extract repo_devs
|
||||
REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; })
|
||||
# Convert developer list into Bash array
|
||||
mapfile -t DEVS_ARRAY <<< "$REPO_DEVS"
|
||||
if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then
|
||||
echo "is_repo_dev=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get PR diff
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: get_diff
|
||||
run: |
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff
|
||||
echo "diff<<EOF" >> $GITHUB_OUTPUT
|
||||
cat pr.diff >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check and sanitize PR title
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: sanitize_pr_title
|
||||
env:
|
||||
PR_TITLE_RAW: ${{ github.event.pull_request.title }}
|
||||
run: |
|
||||
# Sanitize PR title: max 72 characters, only printable characters
|
||||
PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g')
|
||||
if [[ ${#PR_TITLE} -lt 5 ]]; then
|
||||
echo "PR title is too short. Must be at least 5 characters." >&2
|
||||
fi
|
||||
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AI PR Title Analysis
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: ai-title-analysis
|
||||
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt-file: ".github/config/system-prompt.txt"
|
||||
prompt: |
|
||||
Based on the following input data:
|
||||
|
||||
{
|
||||
"diff": "${{ steps.get_diff.outputs.diff }}",
|
||||
"pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}"
|
||||
}
|
||||
|
||||
Respond ONLY with valid JSON in the format:
|
||||
{
|
||||
"improved_rating": <0-10>,
|
||||
"improved_ai_title_rating": <0-10>,
|
||||
"improved_title": "<ai generated title>"
|
||||
}
|
||||
|
||||
- name: Validate and set SCRIPT_OUTPUT
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
run: |
|
||||
cat <<EOF > ai_response.json
|
||||
${{ steps.ai-title-analysis.outputs.response }}
|
||||
EOF
|
||||
|
||||
# Validate JSON structure
|
||||
jq -e '
|
||||
(keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and
|
||||
(.improved_rating | type == "number" and . >= 0 and . <= 10) and
|
||||
(.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and
|
||||
(.improved_title | type == "string")
|
||||
' ai_response.json
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Invalid AI response format" >&2
|
||||
cat ai_response.json >&2
|
||||
exit 1
|
||||
fi
|
||||
# Parse JSON fields
|
||||
IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json)
|
||||
IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json)
|
||||
# Limit comment length to 1000 characters
|
||||
COMMENT=$(cat <<EOF
|
||||
## 🤖 AI PR Title Suggestion
|
||||
|
||||
**PR-Title Rating**: $IMPROVED_RATING/10
|
||||
|
||||
### ⬇️ Suggested Title (copy & paste):
|
||||
|
||||
\`\`\`
|
||||
$IMPROVED_TITLE
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
*Generated by GitHub Models AI*
|
||||
EOF
|
||||
)
|
||||
echo "$COMMENT" > /tmp/ai-title-comment.md
|
||||
# Log input and output to the GitHub Step Summary
|
||||
echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
cat ai_response.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Post comment on PR if needed
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8');
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split('/');
|
||||
const issue_number = context.issue.number;
|
||||
|
||||
const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/);
|
||||
const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null;
|
||||
|
||||
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
|
||||
|
||||
const existing = comments.data.find(c =>
|
||||
c.user?.login === expectedActor &&
|
||||
c.body.includes("## 🤖 AI PR Title Suggestion")
|
||||
);
|
||||
|
||||
if (rating === null) {
|
||||
console.log("No rating found in AI response – skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rating <= 5) {
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo,
|
||||
comment_id: existing.id,
|
||||
body
|
||||
});
|
||||
console.log("Updated existing suggestion comment.");
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number,
|
||||
body
|
||||
});
|
||||
console.log("Created new suggestion comment.");
|
||||
}
|
||||
} else {
|
||||
const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo,
|
||||
comment_id: existing.id,
|
||||
body: praise
|
||||
});
|
||||
console.log("Replaced suggestion with praise.");
|
||||
} else {
|
||||
console.log("Rating > 5 and no existing comment – skipping comment.");
|
||||
}
|
||||
}
|
||||
|
||||
- name: is not repo dev
|
||||
if: steps.actor.outputs.is_repo_dev != 'true'
|
||||
run: |
|
||||
exit 0 # Skip the AI title review for non-repo developers
|
||||
|
||||
- name: Clean up
|
||||
if: always()
|
||||
run: |
|
||||
rm -f pr.diff ai_response.json /tmp/ai-title-comment.md
|
||||
echo "Cleaned up temporary files."
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -66,12 +66,11 @@ jobs:
|
||||
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
publish-aur:
|
||||
environment: package-publish
|
||||
needs: get-release-info
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -107,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Publish stirling-pdf-desktop to AUR
|
||||
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
|
||||
uses: KSXGitHub/github-actions-deploy-aur@084b0d9b15415bf9cdb65d44dad1efe37a354050 # v4.2.0
|
||||
uses: KSXGitHub/github-actions-deploy-aur@da03e160361ce01bf087e790b6ffd196d7dccff7 # v4.1.3
|
||||
with:
|
||||
pkgname: stirling-pdf-desktop
|
||||
pkgbuild: .github/aur/stirling-pdf-desktop/PKGBUILD
|
||||
|
||||
@@ -13,21 +13,26 @@ jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # checkout + labeler fetching its config from the repo
|
||||
pull-requests: write # read changed files, apply labels to the PR
|
||||
issues: write # labels are applied through the issues API
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0
|
||||
with:
|
||||
config_path: .github/labeler-config-srvaroa.yml
|
||||
use_local_config: false
|
||||
fail_on_error: true
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
GITHUB_TOKEN: "${{ steps.setup-bot.outputs.token }}"
|
||||
|
||||
@@ -20,9 +20,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -31,28 +28,35 @@ jobs:
|
||||
flavor: [core, proprietary, saas]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Check Java formatting (Spotless)
|
||||
# Runs once per matrix combination - pick the cheapest leg
|
||||
# (core - no proprietary, no saas) so we don't wait for the
|
||||
@@ -153,9 +157,6 @@ jobs:
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
STIRLING_FLAVOR: ${{ matrix.flavor }}
|
||||
# Configure the Gradle daemon explicitly; GRADLE_OPTS alone only
|
||||
# configures the Gradle client JVM.
|
||||
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -XX:+UseG1GC"
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
if: always()
|
||||
@@ -195,14 +196,12 @@ jobs:
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Install uv
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py parses JaCoCo XML through defusedxml to
|
||||
# silence security scanners that pattern-match on the stdlib
|
||||
# xml.etree.ElementTree.parse call.
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: JaCoCo coverage step summary
|
||||
# Only the saas leg posts the JUnit summary - it's a strict
|
||||
@@ -211,7 +210,7 @@ jobs:
|
||||
# near-identical tables crowding out the aggregate report.
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Backend JUnit coverage (JDK ${{ matrix.jdk-version }})" \
|
||||
--jacoco "common=app/common/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
--jacoco "core=app/core/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
@@ -242,7 +241,7 @@ jobs:
|
||||
# so skip it for merge_group runs and workflow_dispatch.
|
||||
if: github.event_name == 'pull_request'
|
||||
id: jacoco
|
||||
uses: madrapps/jacoco-report@e51ce1f46f7f8b5331593f935e59cbaf44b84920 # v1.8.0
|
||||
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
|
||||
with:
|
||||
paths: |
|
||||
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
|
||||
@@ -15,11 +15,6 @@ name: Enterprise E2E (Playwright)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
use_shared_cache:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
@@ -42,9 +37,6 @@ jobs:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
needs: pick
|
||||
# Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE,
|
||||
# so the suite can't boot premium and would fail. See the header comment.
|
||||
@@ -58,44 +50,24 @@ jobs:
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
if: inputs.use_shared_cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Restore cache Gradle
|
||||
if: inputs.use_shared_cache == false
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-playwright-e2e-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (needed for playwright's vite preview webServer)
|
||||
@@ -323,80 +295,3 @@ jobs:
|
||||
name: playwright-report-enterprise-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f /tmp/helpers.sh /tmp/backend.log /tmp/backend.pid
|
||||
continue-on-error: true
|
||||
|
||||
# Multi-node regression: builds + seeds the clustered stack (testing/compose/docker-compose-multinode.yml)
|
||||
# and runs behave features/multinode. Licence-gated, so it runs after the Playwright job (not in parallel).
|
||||
multinode-e2e:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
needs: [pick, playwright-e2e-enterprise]
|
||||
# Nightly cron + manual dispatch only (heavy build), fork-gated for the licence secret.
|
||||
if: >-
|
||||
always() && needs.pick.outputs.is_fork != 'true'
|
||||
&& (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
PREMIUM_ENABLED: "true"
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
MN_COMPOSE: docker-compose-multinode.yml
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
- name: Install behave test deps
|
||||
run: |
|
||||
uv sync --project engine --locked --group cucumber
|
||||
- name: Build the multi-node image
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f "$MN_COMPOSE" build
|
||||
- name: Bring up the cluster and wait for both nodes healthy
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
docker compose -f "$MN_COMPOSE" up -d
|
||||
for i in $(seq 1 90); do
|
||||
h1=$(docker inspect -f '{{.State.Health.Status}}' multinode-stirling-1 2>/dev/null || echo starting)
|
||||
h2=$(docker inspect -f '{{.State.Health.Status}}' multinode-stirling-2 2>/dev/null || echo starting)
|
||||
if [ "$h1" = healthy ] && [ "$h2" = healthy ]; then echo "both nodes healthy"; exit 0; fi
|
||||
sleep 5
|
||||
done
|
||||
echo "::error::nodes did not become healthy"
|
||||
docker compose -f "$MN_COMPOSE" logs --tail=200 stirling-1 stirling-2
|
||||
exit 1
|
||||
- name: Seed the cluster (teams, users, S3 connection, policy)
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f "$MN_COMPOSE" --profile seed run --rm seed
|
||||
- name: Run multi-node regression (implemented guarantees)
|
||||
working-directory: testing/cucumber
|
||||
# -e overrides behave.ini's exclusion of features/multinode; ~@known_gap skips any tracked-gap scenarios.
|
||||
run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain
|
||||
- name: Run multi-node failover (destructive)
|
||||
working-directory: testing/cucumber
|
||||
run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain
|
||||
- name: Dump node logs on failure
|
||||
if: failure()
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f "$MN_COMPOSE" logs --tail=400 stirling-1 stirling-2
|
||||
- name: Tear down
|
||||
if: always()
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f "$MN_COMPOSE" --profile seed down -v --remove-orphans
|
||||
|
||||
+13
-85
@@ -37,38 +37,29 @@ jobs:
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
build: ${{ steps.changes.outputs.build }}
|
||||
backend: ${{ steps.changes.outputs.backend }}
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
docker-base: ${{ steps.changes.outputs.docker-base }}
|
||||
dockerfiles: ${{ steps.changes.outputs.dockerfiles }}
|
||||
tauri: ${{ steps.changes.outputs.tauri }}
|
||||
engine: ${{ steps.changes.outputs.engine }}
|
||||
generated-models: ${{ steps.changes.outputs.generated-models }}
|
||||
proprietary: ${{ steps.changes.outputs.proprietary }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
gradle-cache-prime:
|
||||
needs: [files-changed]
|
||||
uses: ./.github/workflows/gradle-cache-prime.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -83,7 +74,7 @@ jobs:
|
||||
# works after Hibernate's ddl-auto=update migrates the schema. Gated on
|
||||
# the `project` filter so doc-only PRs skip this ~5-minute job.
|
||||
if: needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/db-migration-test.yml
|
||||
@@ -91,7 +82,7 @@ jobs:
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/check-openapi.yml
|
||||
@@ -106,18 +97,6 @@ jobs:
|
||||
uses: ./.github/workflows/frontend-validation.yml
|
||||
secrets: inherit
|
||||
|
||||
# Required (in all-checks-passed). Scans the stories a branch touches in both
|
||||
# light and dark; an axe violation in either theme blocks the merge. The
|
||||
# whole-suite sweep (nightly.yml) still covers stories a change affects without
|
||||
# touching them directly.
|
||||
frontend-a11y:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/frontend-a11y.yml
|
||||
secrets: inherit
|
||||
|
||||
playwright-e2e:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed]
|
||||
@@ -128,7 +107,7 @@ jobs:
|
||||
|
||||
playwright-e2e-live:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/e2e-live.yml
|
||||
@@ -136,17 +115,15 @@ jobs:
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
if: needs.files-changed.outputs.proprietary == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-enterprise.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
use_shared_cache: true
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed, build]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/check-licence.yml
|
||||
@@ -154,7 +131,7 @@ jobs:
|
||||
|
||||
docker-compose-tests:
|
||||
if: needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
@@ -165,22 +142,8 @@ jobs:
|
||||
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
|
||||
|
||||
test-build-docker-images:
|
||||
if: |
|
||||
always() &&
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.files-changed.outputs.project == 'true' &&
|
||||
contains(fromJSON('["success", "skipped"]'), needs.gradle-cache-prime.result) &&
|
||||
contains(fromJSON('["success", "skipped"]'), needs.build.result) &&
|
||||
contains(fromJSON('["success", "skipped"]'), needs.check-generateOpenApiDocs.result) &&
|
||||
contains(fromJSON('["success", "skipped"]'), needs.check-licence.result)
|
||||
needs:
|
||||
[
|
||||
files-changed,
|
||||
build,
|
||||
check-generateOpenApiDocs,
|
||||
check-licence,
|
||||
gradle-cache-prime,
|
||||
]
|
||||
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed, build, check-generateOpenApiDocs, check-licence]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
@@ -188,25 +151,15 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
|
||||
dockerfiles-changed: ${{ needs.files-changed.outputs.dockerfiles }}
|
||||
|
||||
tauri-build:
|
||||
if: needs.files-changed.outputs.tauri == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/tauri-build.yml
|
||||
secrets: inherit
|
||||
# PR smoke build: macOS + Windows (the platforms our developers use).
|
||||
# sign: true only reaches macOS - tauri-build's per-platform gate keeps
|
||||
# Windows/Linux signing on main, and an unsigned .dmg cannot be opened.
|
||||
# The full signed multi-OS matrix runs on release;
|
||||
# nightly still warms the Rust cache with all-OS defaults.
|
||||
with:
|
||||
platform: windows-macos
|
||||
sign: true
|
||||
use_shared_cache: true
|
||||
|
||||
ai-engine:
|
||||
if: needs.files-changed.outputs.engine == 'true'
|
||||
@@ -217,22 +170,6 @@ jobs:
|
||||
uses: ./.github/workflows/ai-engine.yml
|
||||
secrets: inherit
|
||||
|
||||
# The generated frontend types and engine tool models are both derived from
|
||||
# the Java OpenAPI spec. This job regenerates and diffs them; it boots the
|
||||
# backend, so it is gated on the narrow generated-models filter (spec source,
|
||||
# generators, generated files, generation tasks) rather than the broad
|
||||
# frontend filter, so a CSS-only PR does not pay for a backend build.
|
||||
generated-models:
|
||||
if: needs.files-changed.outputs.generated-models == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/check-generated-models.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
use_shared_cache: true
|
||||
|
||||
pre-commit:
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
@@ -264,9 +201,6 @@ jobs:
|
||||
contents: read
|
||||
uses: ./.github/workflows/coverage-aggregate.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
frontend-validation-result: ${{ needs.frontend-validation.result }}
|
||||
playwright-e2e-live-result: ${{ needs.playwright-e2e-live.result }}
|
||||
|
||||
# Single status check that branch protection should mark as required.
|
||||
# Succeeds when every upstream job is either `success` or `skipped` (path-
|
||||
@@ -278,12 +212,10 @@ jobs:
|
||||
if: always()
|
||||
needs:
|
||||
- files-changed
|
||||
- gradle-cache-prime
|
||||
- build
|
||||
- db-migration-test
|
||||
- check-generateOpenApiDocs
|
||||
- frontend-validation
|
||||
- frontend-a11y
|
||||
- playwright-e2e
|
||||
- playwright-e2e-live
|
||||
- playwright-e2e-enterprise
|
||||
@@ -292,13 +224,12 @@ jobs:
|
||||
- test-build-docker-images
|
||||
- tauri-build
|
||||
- ai-engine
|
||||
- generated-models
|
||||
- pre-commit
|
||||
- dependency-review
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -306,12 +237,10 @@ jobs:
|
||||
env:
|
||||
RESULTS: |
|
||||
files-changed=${{ needs.files-changed.result }}
|
||||
gradle-cache-prime=${{ needs.gradle-cache-prime.result }}
|
||||
build=${{ needs.build.result }}
|
||||
db-migration-test=${{ needs.db-migration-test.result }}
|
||||
check-generateOpenApiDocs=${{ needs.check-generateOpenApiDocs.result }}
|
||||
frontend-validation=${{ needs.frontend-validation.result }}
|
||||
frontend-a11y=${{ needs.frontend-a11y.result }}
|
||||
playwright-e2e=${{ needs.playwright-e2e.result }}
|
||||
playwright-e2e-live=${{ needs.playwright-e2e-live.result }}
|
||||
playwright-e2e-enterprise=${{ needs.playwright-e2e-enterprise.result }}
|
||||
@@ -320,7 +249,6 @@ jobs:
|
||||
test-build-docker-images=${{ needs.test-build-docker-images.result }}
|
||||
tauri-build=${{ needs.tauri-build.result }}
|
||||
ai-engine=${{ needs.ai-engine.result }}
|
||||
generated-models=${{ needs.generated-models.result }}
|
||||
pre-commit=${{ needs.pre-commit.result }}
|
||||
dependency-review=${{ needs.dependency-review.result }}
|
||||
run: |
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
name: Check generated models
|
||||
|
||||
# Verifies the committed generated files are still in sync with the Java OpenAPI
|
||||
# spec: the request models (toolApiTypes.ts, tool_models.py) and the tool I/O
|
||||
# tables saying what each endpoint accepts and produces (toolIO.ts, tool_io.py).
|
||||
# Regenerates them all with the single top-level `task tool-models` and fails if
|
||||
# any committed file is out of date. Called from build.yml when the
|
||||
# backend Java, frontend, or engine changes; also runs on push to main as a
|
||||
# post-merge safety net.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
use_shared_cache:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generated-models:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
if: inputs.use_shared_cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Restore cache Gradle
|
||||
if: inputs.use_shared_cache == false
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-generated-models-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Verify generated models are up to date
|
||||
id: models-check
|
||||
continue-on-error: true
|
||||
run: task tool-models:check
|
||||
|
||||
- name: Comment on generated models check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
# issue/PR number, which doesn't exist on merge_group runs.
|
||||
if: steps.models-check.outcome == 'failure' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- generated-models-check -->';
|
||||
const body = [
|
||||
marker,
|
||||
'### Generated Models Check Failed',
|
||||
'',
|
||||
'One or more generated files are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.',
|
||||
'',
|
||||
'Run `task tool-models` to regenerate them, then commit the updated files.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail if generated models check failed
|
||||
if: steps.models-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Generated Models Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "One or more generated files are out of date with the Java"
|
||||
echo "OpenAPI spec and will need to be regenerated before merging."
|
||||
echo ""
|
||||
echo "Run 'task tool-models' to regenerate them, then"
|
||||
echo "commit the updated files."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Remove generated models check comment on success
|
||||
if: steps.models-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- generated-models-check -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
@@ -10,35 +10,39 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check-licence:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Check licenses for compatibility
|
||||
run: task backend:licenses:check
|
||||
env:
|
||||
|
||||
@@ -11,35 +11,39 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check-generate-openapi-docs:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Generate OpenAPI documentation
|
||||
run: task backend:swagger
|
||||
env:
|
||||
|
||||
@@ -23,23 +23,29 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # Checkout, and read translation files via the contents API
|
||||
issues: write # Allow posting comments on issues/PRs
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Get PR data
|
||||
id: get-pr-data
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const repoOwner = context.payload.repository.owner.login;
|
||||
@@ -60,18 +66,17 @@ jobs:
|
||||
- name: Fetch PR changed files
|
||||
id: fetch-pr-changes
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.get-pr-data.outputs.pr_number }}
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
run: |
|
||||
echo "Fetching PR changed files..."
|
||||
echo "Getting list of changed files from PR..."
|
||||
# Check if PR number exists
|
||||
if [ -z "${PR_NUMBER}" ]; then
|
||||
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
|
||||
echo "Error: PR number is empty"
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for TOML translation files
|
||||
gh pr view "${PR_NUMBER}" --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
|
||||
# Check if any files were found
|
||||
if [ ! -s changed_files.txt ]; then
|
||||
echo "No TOML translation files changed in this PR"
|
||||
@@ -83,37 +88,33 @@ jobs:
|
||||
- name: Determine reference file
|
||||
id: determine-file
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
# Untrusted, fork-controlled values are passed via env, never interpolated into the script
|
||||
PR_NUMBER: ${{ steps.get-pr-data.outputs.pr_number }}
|
||||
REPO_OWNER: ${{ steps.get-pr-data.outputs.repo_owner }}
|
||||
REPO_NAME: ${{ steps.get-pr-data.outputs.repo_name }}
|
||||
PR_REPO_OWNER: ${{ github.event.pull_request.head.repo.owner.login }}
|
||||
PR_REPO_NAME: ${{ github.event.pull_request.head.repo.name }}
|
||||
PR_BRANCH: ${{ steps.get-pr-data.outputs.branch }}
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Validate inputs before any use
|
||||
const validateInput = (input, regex, name) => {
|
||||
if (typeof input !== "string" || !regex.test(input)) {
|
||||
throw new Error(`Invalid ${name}: ${input}`);
|
||||
}
|
||||
return input;
|
||||
};
|
||||
const prNumber = ${{ steps.get-pr-data.outputs.pr_number }};
|
||||
const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}";
|
||||
const repoName = "${{ steps.get-pr-data.outputs.repo_name }}";
|
||||
|
||||
const repoOwner = validateInput(process.env.REPO_OWNER, /^[a-zA-Z0-9_-]+$/, "repository owner");
|
||||
const repoName = validateInput(process.env.REPO_NAME, /^[a-zA-Z0-9._-]+$/, "repository name");
|
||||
const prRepoOwner = validateInput(process.env.PR_REPO_OWNER, /^[a-zA-Z0-9_-]+$/, "PR repository owner");
|
||||
const prRepoName = validateInput(process.env.PR_REPO_NAME, /^[a-zA-Z0-9._-]+$/, "PR repository name");
|
||||
const branch = validateInput(process.env.PR_BRANCH, /^[a-zA-Z0-9._/-]+$/, "branch name");
|
||||
const prNumber = Number(validateInput(process.env.PR_NUMBER, /^[0-9]+$/, "PR number"));
|
||||
const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}";
|
||||
const prRepoName = "${{ github.event.pull_request.head.repo.name }}";
|
||||
const branch = "${{ steps.get-pr-data.outputs.branch }}";
|
||||
|
||||
console.log(`Determining reference file for PR #${prNumber}`);
|
||||
|
||||
// Validate inputs
|
||||
const validateInput = (input, regex, name) => {
|
||||
if (!regex.test(input)) {
|
||||
throw new Error(`Invalid ${name}: ${input}`);
|
||||
}
|
||||
};
|
||||
|
||||
validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner");
|
||||
validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name");
|
||||
validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name");
|
||||
|
||||
// Get the list of changed files in the PR
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: repoOwner,
|
||||
@@ -125,7 +126,7 @@ jobs:
|
||||
const changedFiles = files
|
||||
.filter(file =>
|
||||
file.status !== "removed" &&
|
||||
/^frontend\/editor\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename)
|
||||
/^frontend\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename)
|
||||
)
|
||||
.map(file => file.filename);
|
||||
|
||||
@@ -194,26 +195,21 @@ jobs:
|
||||
console.log(`Reference file path: ${referenceFilePath}`);
|
||||
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv sync --project engine --locked --group tools
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
env:
|
||||
PR_ACTOR: ${{ github.event.pull_request.user.login }}
|
||||
run: |
|
||||
echo "Running Python script to check TOML files..."
|
||||
uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py \
|
||||
--actor "${PR_ACTOR}" \
|
||||
python .github/scripts/check_language_toml.py \
|
||||
--actor ${{ github.event.pull_request.user.login }} \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--branch "pr-branch" \
|
||||
--files "${FILES_LIST[@]}" > result.txt
|
||||
@@ -246,7 +242,7 @@ jobs:
|
||||
if: env.SCRIPT_OUTPUT != ''
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
@@ -262,7 +258,7 @@ jobs:
|
||||
const comment = comments.data.find(c => c.body.includes("## 🌐 TOML Translation Verification Summary"));
|
||||
|
||||
// Only update or create comments by the action user
|
||||
const expectedActor = "github-actions[bot]";
|
||||
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||
|
||||
if (comment && comment.user.login === expectedActor) {
|
||||
// Update existing comment
|
||||
|
||||
@@ -34,33 +34,42 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install defusedxml for coverage scripts
|
||||
# Both coverage-summary.py and coverage-matrix.py parse JaCoCo
|
||||
# XML through defusedxml - see the script headers for context.
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
# Pattern matches every artifact this PR's producers might upload:
|
||||
# jacoco-exec-junit-jdk-25 (uploaded only by the saas
|
||||
@@ -72,7 +81,7 @@ jobs:
|
||||
# Each lands as a sibling dir under coverage-execs/, with the .exec
|
||||
# files preserving their original relative paths.
|
||||
- name: Download all .exec artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
pattern: jacoco-exec-*
|
||||
path: coverage-execs/
|
||||
@@ -153,7 +162,7 @@ jobs:
|
||||
# ("how much of the backend do real user flows cover?").
|
||||
if: steps.inventory.outputs.found_e2e == 'true'
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Real user-flow backend coverage (e2e:live + cucumber)" \
|
||||
--jacoco "merged=build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
@@ -164,7 +173,7 @@ jobs:
|
||||
# is meaningless when one is a strict superset of the other.
|
||||
if: steps.inventory.outputs.found_all == 'true'
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Combined backend coverage (JUnit + e2e:live + cucumber)" \
|
||||
--jacoco "merged=build/reports/jacoco/aggregate-all/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
@@ -197,7 +206,7 @@ jobs:
|
||||
# absence on backend-only runs by skipping the download entirely
|
||||
# when the producer job was not part of this workflow run.
|
||||
if: inputs.frontend-validation-result == 'success'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: matrix-inputs/vitest/
|
||||
@@ -207,7 +216,7 @@ jobs:
|
||||
# e2e-live uploads the artifact with a stable name. Skip the
|
||||
# download entirely when the producer job did not run.
|
||||
if: inputs.playwright-e2e-live-result == 'success'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: playwright-frontend-coverage
|
||||
path: matrix-inputs/playwright/
|
||||
@@ -219,7 +228,7 @@ jobs:
|
||||
# generated above) plus whichever frontend artifacts landed.
|
||||
# Every input is optional; missing ones render as "-".
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-matrix.py \
|
||||
python scripts/coverage-matrix.py \
|
||||
${{ steps.inventory.outputs.found_all == 'true' && '--jacoco-all build/reports/jacoco/aggregate-all/jacocoTestReport.xml' || '' }} \
|
||||
${{ steps.inventory.outputs.found_e2e == 'true' && '--jacoco-e2e build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml' || '' }} \
|
||||
--vitest matrix-inputs/vitest/coverage-summary.json \
|
||||
|
||||
@@ -13,36 +13,40 @@ permissions:
|
||||
|
||||
jobs:
|
||||
migration-test:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
|
||||
# Keep the normal formatting path here so this smoke test exercises the
|
||||
# same Gradle configuration as the backend build.
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
# No `-PnoSpotless` here yet because the upstream cache layer matches the
|
||||
# backend build's; reuse keeps cold-cache cost identical.
|
||||
- name: Build Stirling-PDF JAR
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
@@ -81,8 +85,3 @@ jobs:
|
||||
path: /tmp/stirling-migration-failed-*/app.log
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: rm -rf /tmp/stirling-migration-failed-*
|
||||
continue-on-error: true
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
name: Auto V2 Deploy on Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- deploy-on-v2-commit
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-v2-on-push:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-v2-frontend
|
||||
cache-to: type=gha,mode=max,scope=stirling-v2-frontend
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-v2-backend
|
||||
cache-to: type=gha,mode=max,scope=stirling-v2-backend
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
chmod 600 ../private.key
|
||||
|
||||
- name: Deploy to VPS on port 3000
|
||||
run: |
|
||||
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||
|
||||
cat > $UNIQUE_NAME << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
backend:
|
||||
container_name: stirling-v2-backend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "13000:8080"
|
||||
volumes:
|
||||
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2/config:/configs:rw
|
||||
- /stirling/V2/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: "Stirling-PDF V2"
|
||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
|
||||
baseUrl: "https://demo.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
frontend:
|
||||
container_name: stirling-v2-frontend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:13000"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Copy to remote with unique name
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||
|
||||
# SSH and rename/move atomically to avoid interference
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||
mkdir -p /stirling/V2/{data,config,logs}
|
||||
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||
cd /stirling/V2
|
||||
docker-compose down || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker system prune -af --volumes || true
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
@@ -17,9 +17,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
docker-compose-tests:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -28,27 +25,34 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
# When the PR changes the base image, test.sh builds it locally
|
||||
# (stirling-pdf-base:local) into the daemon image store. A buildx
|
||||
# container builder can't see that store, so skip it here and let
|
||||
@@ -57,7 +61,7 @@ jobs:
|
||||
# runtime token isn't exposed) since the docker driver can't use it.
|
||||
- name: Set up Docker Buildx
|
||||
if: inputs.docker-base-changed != 'true'
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
|
||||
- name: Expose GitHub runtime for Buildx cache
|
||||
@@ -66,20 +70,19 @@ jobs:
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v5.4.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
cache-dependency-path: ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Install Cucumber and coverage dependencies
|
||||
- name: Pip requirements
|
||||
run: |
|
||||
uv sync --project engine --locked --group cucumber --group tools
|
||||
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Extract JaCoCo agent for cucumber coverage
|
||||
# Stages build/jacoco/jacocoagent.jar where the coverage override
|
||||
@@ -122,10 +125,16 @@ jobs:
|
||||
echo "report=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py parses JaCoCo XML through defusedxml -
|
||||
# see the script header for context.
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: Cucumber coverage step summary
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Cucumber (docker) JaCoCo coverage" \
|
||||
--jacoco "cucumber=build/reports/jacoco/cucumber/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
|
||||
@@ -11,41 +11,28 @@ permissions:
|
||||
|
||||
jobs:
|
||||
playwright-e2e-live:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
@@ -66,11 +53,6 @@ jobs:
|
||||
# to aggregate. Chromium-only - other engines silently skip.
|
||||
PW_COVERAGE: "1"
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
# Internal mirror, as in backend-build.yml. Empty on Dependabot and
|
||||
# fork PRs, where the build falls back to Maven Central.
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
run: task e2e:live
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
@@ -84,10 +66,6 @@ jobs:
|
||||
- name: Generate JaCoCo report from e2e:live .exec
|
||||
if: always()
|
||||
id: live-coverage
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
# `if: always()` so even a failed test run still produces a
|
||||
# report from whatever flows did exercise the backend before
|
||||
# the failure. The task itself tolerates a missing .exec
|
||||
@@ -104,18 +82,20 @@ jobs:
|
||||
echo "::warning::No e2e:live .exec found at .test-state/playwright/jacoco.exec; skipping report"
|
||||
echo "report=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Install uv
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
- name: Set up Python for coverage summary
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
python-version: "3.12"
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py uses defusedxml instead of stdlib xml.etree
|
||||
# to dodge XXE / billion-laughs scanner findings.
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
- name: e2e:live coverage step summary
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Playwright (live backend) JaCoCo coverage" \
|
||||
--jacoco "e2e-live=build/reports/jacoco/e2e-live/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
@@ -138,6 +118,23 @@ jobs:
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Set up Python for frontend coverage summary
|
||||
# Separate from the backend-coverage python step because the
|
||||
# frontend path doesn't depend on a JaCoCo report - it produces
|
||||
# a summary even on backend failure, as long as some Playwright
|
||||
# tests ran far enough to dump V8 coverage.
|
||||
if: always()
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install defusedxml for frontend coverage summary
|
||||
# Idempotent re-install: the backend-coverage step may have
|
||||
# installed it already, but this leg can run on its own when the
|
||||
# backend report step skips (e.g. .exec missing).
|
||||
if: always()
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: Aggregate Playwright frontend (V8) coverage
|
||||
# Rolls per-test V8 dumps from the test-base fixture into one
|
||||
# vitest-shaped coverage-summary.json. Tolerates a missing dump
|
||||
@@ -148,7 +145,7 @@ jobs:
|
||||
run: |
|
||||
if [ -d .test-state/playwright/coverage-pw ] && \
|
||||
find .test-state/playwright/coverage-pw -name '*.json' -type f | grep -q .; then
|
||||
uv run --project engine --locked --group tools python scripts/playwright-coverage-summary.py \
|
||||
python scripts/playwright-coverage-summary.py \
|
||||
.test-state/playwright/coverage-pw \
|
||||
--out .test-state/playwright/coverage-pw-summary/coverage-summary.json
|
||||
echo "summary=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -160,10 +157,10 @@ jobs:
|
||||
- name: Playwright frontend coverage step summary
|
||||
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Playwright (live) frontend coverage" \
|
||||
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
|
||||
--github-step-summary
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Playwright (live) frontend coverage" \
|
||||
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
|
||||
--github-step-summary
|
||||
|
||||
- name: Upload Playwright frontend coverage
|
||||
# Bundle both the aggregated summary and the raw V8 dumps so
|
||||
|
||||
@@ -2,8 +2,7 @@ name: Playwright E2E (stubbed)
|
||||
|
||||
# Reusable workflow called from build.yml. Backend-free Playwright suite —
|
||||
# fast, no Spring Boot required. Runs against the `stubbed` project which
|
||||
# mocks API responses in the browser. Fans out one job per browser
|
||||
# (chromium/firefox/webkit) so all three run in parallel on their own runner.
|
||||
# mocks API responses in the browser.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -12,57 +11,32 @@ permissions:
|
||||
|
||||
jobs:
|
||||
playwright-e2e:
|
||||
name: playwright-e2e (${{ matrix.browser }})
|
||||
runs-on: ubuntu-latest
|
||||
# The image already contains the Playwright browsers and all Linux
|
||||
# dependencies. This keeps the matrix for per-browser reporting while
|
||||
# avoiding three concurrent `playwright install --with-deps` runs.
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.58.2-noble@sha256:6446946a1d9fd62d9ae501312a2d76a43ee688542b21622056a372959b65d63d
|
||||
strategy:
|
||||
# One browser breaking must not mask a failure in another - report all.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- browser: chromium
|
||||
project: stubbed
|
||||
- browser: firefox
|
||||
project: stubbed-firefox
|
||||
- browser: webkit
|
||||
project: stubbed-webkit
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
- name: Run stubbed E2E tests (${{ matrix.browser }})
|
||||
- name: Run stubbed E2E tests (chromium)
|
||||
env:
|
||||
# The official Playwright image expects its browser runtime under
|
||||
# the root home directory. Keep this scoped to Playwright and use a
|
||||
# neutral Docker config path so Docker does not read /root/.docker.
|
||||
HOME: /root
|
||||
DOCKER_CONFIG: /tmp/playwright-docker-config
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
NPM_CONFIG_PREFER_OFFLINE: "true"
|
||||
NPM_CONFIG_FETCH_RETRIES: "5"
|
||||
NPM_CONFIG_FETCH_RETRY_FACTOR: "2"
|
||||
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: "1000"
|
||||
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: "120000"
|
||||
run: task e2e:stubbed-project PROJECT=${{ matrix.project }} -- --workers=3
|
||||
run: task e2e:stubbed -- --workers=3
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
# leaves the step green, so this is the only place it surfaces. Emits
|
||||
@@ -76,6 +50,6 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-stubbed-${{ matrix.browser }}-${{ github.run_id }}
|
||||
name: playwright-report-stubbed-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Frontend a11y regression gate
|
||||
|
||||
# Reusable workflow called from build.yml when frontend sources change.
|
||||
#
|
||||
# Scans the stories this branch touches in real Chromium and runs axe against
|
||||
# each; the check fails on any axe violation, or on a story that fails to render
|
||||
# at all.
|
||||
#
|
||||
# Only changed stories, because a full sweep is ~30 minutes: far too slow to sit
|
||||
# in front of every merge. The whole suite is scanned nightly instead
|
||||
# (nightly.yml), which catches anything a branch didn't touch.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
frontend-a11y:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Need the base branch too, to diff against it.
|
||||
fetch-depth: 0
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: a11y gate (changed stories)
|
||||
run: task frontend:storybook:a11y:changed -- origin/${{ github.base_ref || 'main' }}
|
||||
- name: Upload scan reports
|
||||
# The reports carry the offending selector and help text for each
|
||||
# violation; without them a red run can only be understood by
|
||||
# reproducing the whole scan locally.
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: a11y-scan-${{ github.run_id }}
|
||||
path: frontend/.a11y-scan/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
# The reports live in a dot-directory, which upload-artifact treats as
|
||||
# hidden and silently skips by default.
|
||||
include-hidden-files: true
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
licenses-backend: ${{ steps.changes.outputs.licenses-backend }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -36,16 +36,12 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
generate-frontend-license-report:
|
||||
# ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only.
|
||||
environment:
|
||||
name: ci-bot
|
||||
deployment: false
|
||||
if: needs.files-changed.outputs.licenses-frontend == 'true'
|
||||
name: Generate Frontend License Report
|
||||
needs: files-changed
|
||||
@@ -56,7 +52,7 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -84,7 +80,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -97,7 +93,7 @@ jobs:
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Generate frontend license report (Push only)
|
||||
if: github.event_name == 'push'
|
||||
@@ -320,10 +316,6 @@ jobs:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
generate-backend-license-report:
|
||||
# ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only.
|
||||
environment:
|
||||
name: ci-bot
|
||||
deployment: false
|
||||
if: needs.files-changed.outputs.licenses-backend == 'true'
|
||||
needs: files-changed
|
||||
name: Generate Backend License Report
|
||||
@@ -334,7 +326,7 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -352,22 +344,19 @@ jobs:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-license-report-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Frontend lint, type-check, and build
|
||||
|
||||
# Reusable workflow called from build.yml when frontend / testing sources
|
||||
# change. Runs `task frontend:check:all` and uploads the
|
||||
# coverage + dist artifacts for downstream jobs.
|
||||
# change. Runs the consolidated `task frontend:check:all` (lint, types,
|
||||
# unit tests, build) and uploads the dist artifact for downstream jobs.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -15,19 +15,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Quality-check frontend
|
||||
id: frontend-check
|
||||
run: task frontend:check:all
|
||||
@@ -105,18 +105,30 @@ jobs:
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
- name: Install uv
|
||||
- name: Vitest coverage
|
||||
# Separate from `frontend:check:all` so the quality-gate run stays
|
||||
# uninstrumented (faster signal) and coverage stays an informational
|
||||
# follow-up. Continue-on-error keeps the workflow green even when
|
||||
# a handful of test files refuse to import (e.g. missing icon
|
||||
# specifiers) - the summary still gets posted with whatever
|
||||
# vitest managed to instrument.
|
||||
id: frontend-coverage
|
||||
continue-on-error: true
|
||||
run: task frontend:test:coverage
|
||||
- name: Set up Python for coverage summary
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
python-version: "3.12"
|
||||
- name: Install defusedxml for coverage summary
|
||||
# See coverage-summary.py header - it parses XML through defusedxml
|
||||
# to dodge the stdlib parser's exposure to XXE / billion-laughs.
|
||||
if: always()
|
||||
run: python -m pip install --quiet defusedxml
|
||||
- name: Vitest coverage step summary
|
||||
if: always()
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Frontend Vitest coverage" \
|
||||
--vitest frontend/editor/coverage/coverage-summary.json \
|
||||
--github-step-summary
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Prime Gradle Cache
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
gradle-cache-prime:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
name: Prime shared Gradle cache
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Calculate Gradle cache key
|
||||
id: gradle-cache-key
|
||||
shell: bash
|
||||
run: |
|
||||
echo "key=gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Gradle (lookup-only)
|
||||
id: cache-gradle-restore
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ steps.gradle-cache-key.outputs.key }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Set up JDK 25
|
||||
if: steps.cache-gradle-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Resolve backend dependencies
|
||||
if: steps.cache-gradle-restore.outputs.cache-hit != 'true'
|
||||
run: ./gradlew :stirling-pdf:classes --no-daemon
|
||||
env:
|
||||
STIRLING_FLAVOR: saas
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Save cache Gradle User Home
|
||||
if: steps.cache-gradle-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ steps.gradle-cache-key.outputs.key }}
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -30,6 +30,15 @@ on:
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
prerelease:
|
||||
description: "GitHub release prerelease flag (keep = leave the release as-is)"
|
||||
required: false
|
||||
default: "keep"
|
||||
type: choice
|
||||
options:
|
||||
- "keep"
|
||||
- "true"
|
||||
- "false"
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -38,9 +47,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
@@ -48,28 +54,35 @@ jobs:
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Cache Gradle
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
@@ -93,7 +106,7 @@ jobs:
|
||||
ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX"
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${INPUT_PLATFORM}" in
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
@@ -115,12 +128,7 @@ jobs:
|
||||
echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
env:
|
||||
INPUT_PLATFORM: ${{ github.event.inputs.platform }}
|
||||
build-jars:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
needs: determine-matrix
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -140,36 +148,33 @@ jobs:
|
||||
file_suffix: "-server"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Build JAR
|
||||
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
@@ -196,7 +201,6 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
build:
|
||||
environment: release-signing
|
||||
needs: determine-matrix
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -204,10 +208,11 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
@@ -224,7 +229,7 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -236,21 +241,13 @@ jobs:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
# x86_64 JDK is set up first so the aarch64 step below can leave its
|
||||
# JAVA_HOME as the active one. The macOS universal JRE build needs
|
||||
# jmods from both arches; the x64 path is captured into the env
|
||||
# before the second setup-java overwrites JAVA_HOME.
|
||||
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
||||
if: matrix.platform == 'macos-15'
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
@@ -262,13 +259,18 @@ jobs:
|
||||
|
||||
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
# Build the universal JRE before desktop:prepare so the jlink:runtime
|
||||
# task short-circuits on its `test -d runtime/jre` status check.
|
||||
@@ -291,7 +293,7 @@ jobs:
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
@@ -301,22 +303,22 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
|
||||
# Decode client certificate
|
||||
$certBytes = [Convert]::FromBase64String("$env:SM_CLIENT_CERT_FILE_B64")
|
||||
$certPath = "D:\Certificate_pkcs12.p12"
|
||||
# Decode client certificate (RUNNER_TEMP: arm64 runners have no D: drive)
|
||||
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
|
||||
$certPath = Join-Path $env:RUNNER_TEMP "Certificate_pkcs12.p12"
|
||||
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||
|
||||
# Set environment variables
|
||||
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
|
||||
echo "SM_HOST=$env:SM_HOST" >> $env:GITHUB_ENV
|
||||
echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV
|
||||
echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV
|
||||
echo "SM_KEYPAIR_ALIAS=$env:SM_KEYPAIR_ALIAS" >> $env:GITHUB_ENV
|
||||
echo "SM_CLIENT_CERT_FILE=$certPath" >> $env:GITHUB_ENV
|
||||
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
|
||||
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
|
||||
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
|
||||
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
|
||||
|
||||
# Get PKCS11 config path from DigiCert action
|
||||
$pkcs11Config = $env:PKCS11_CONFIG
|
||||
@@ -334,14 +336,40 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
env:
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
|
||||
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
shell: powershell
|
||||
run: |
|
||||
if ($env:WINDOWS_CERTIFICATE) {
|
||||
Write-Host "Importing Windows Code Signing Certificate..."
|
||||
|
||||
# Decode base64 certificate and save to file
|
||||
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
|
||||
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
|
||||
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||
|
||||
# Import certificate to CurrentUser\My store
|
||||
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
|
||||
|
||||
# Extract and set thumbprint as environment variable
|
||||
$thumbprint = $cert.Thumbprint
|
||||
Write-Host "Certificate imported with thumbprint: $thumbprint"
|
||||
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
|
||||
|
||||
# Clean up certificate file
|
||||
Remove-Item $certPath
|
||||
|
||||
Write-Host "Windows certificate import completed."
|
||||
} else {
|
||||
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
|
||||
}
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
|
||||
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -362,7 +390,7 @@ jobs:
|
||||
rm certificate.p12
|
||||
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
|
||||
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
@@ -378,7 +406,7 @@ jobs:
|
||||
# Without this, signCommand failures are opaque (Tauri captures but drops
|
||||
# smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
|
||||
- name: Preflight smctl
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -406,10 +434,29 @@ jobs:
|
||||
# Windows cert store first. --keypair-alias goes direct through PKCS11 and works
|
||||
# without certsync. All real-world working Tauri+smctl examples use this flag.
|
||||
#
|
||||
# Tauri's bundler needs signtool.exe to VERIFY bundled dlls/exes even when a
|
||||
# custom signCommand does the signing; its auto-lookup fails on arm64 hosts.
|
||||
- name: Locate signtool for Tauri
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$signtool = (Get-Command signtool.exe -ErrorAction SilentlyContinue).Source
|
||||
if (-not $signtool) {
|
||||
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match '\\(arm64|x64)\\' } |
|
||||
Sort-Object FullName | Select-Object -Last 1 -ExpandProperty FullName
|
||||
}
|
||||
if ($signtool) {
|
||||
Write-Host "Using signtool: $signtool"
|
||||
echo "TAURI_WINDOWS_SIGNTOOL_PATH=$signtool" >> $env:GITHUB_ENV
|
||||
} else {
|
||||
Write-Error "signtool.exe not found on runner"
|
||||
}
|
||||
|
||||
# smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
|
||||
# from env (set by prior DigiCert setup step). No --config-file needed.
|
||||
- name: Configure Windows code signing
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -430,7 +477,7 @@ jobs:
|
||||
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/editor/src-tauri/tauri.windows.conf.json
|
||||
|
||||
- name: Import release GPG signing key (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
|
||||
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||
run: |
|
||||
echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --list-secret-keys --keyid-format=long
|
||||
@@ -448,7 +495,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -462,8 +509,8 @@ jobs:
|
||||
# APPIMAGETOOL_SIGN_PASSPHRASE appimagetool uses this to unlock the GPG key non-interactively
|
||||
# SIGN_KEY appimagetool picks the key matching this fingerprint
|
||||
# Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present.
|
||||
# Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or the release branch, when secret is present.
|
||||
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }}
|
||||
# Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or V2-master, when secret is present.
|
||||
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
@@ -478,6 +525,7 @@ jobs:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
args: ${{ matrix.args }}
|
||||
updaterJsonKeepUniversal: true
|
||||
|
||||
# Bundled libwayland conflicts with the host's on some distros (Fedora
|
||||
# Wayland: EGL_BAD_PARAMETER, blank window - #6878). Repack without it,
|
||||
@@ -489,7 +537,7 @@ jobs:
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }}
|
||||
GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
run: |
|
||||
@@ -528,7 +576,7 @@ jobs:
|
||||
echo "Stripped bundled libwayland from $(basename "$AI")"
|
||||
|
||||
- name: Clear release GPG key from runner keyring (Linux)
|
||||
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
|
||||
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||
env:
|
||||
RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
run: |
|
||||
@@ -543,7 +591,7 @@ jobs:
|
||||
# artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
|
||||
# cargo output unsigned, so checking it produces false negatives.
|
||||
- name: Verify Windows Code Signature
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
timeout-minutes: 15
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -697,17 +745,6 @@ jobs:
|
||||
path: ./dist/*
|
||||
retention-days: 1
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
rm -f certificate.p12
|
||||
rm -rf "$RUNNER_TEMP/msi-verify"
|
||||
if [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
collect-and-release:
|
||||
needs: [determine-matrix, build, build-jars]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -715,7 +752,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -863,7 +900,7 @@ jobs:
|
||||
PYEOF
|
||||
|
||||
- name: Upload merged artifacts for review
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: release-artifacts
|
||||
path: ./artifacts/
|
||||
@@ -871,27 +908,21 @@ jobs:
|
||||
|
||||
# Gate publish on valid updater sigs. Runs after the review upload (so
|
||||
# artifacts survive for debugging) and before action-gh-release.
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
- name: Verify updater signatures
|
||||
run: |
|
||||
uv run --project engine --locked --only-group updater-signatures python .github/scripts/verify-updater-signatures.py \
|
||||
python3 -m pip install --quiet 'cryptography==44.0.0'
|
||||
python3 .github/scripts/verify-updater-signatures.py \
|
||||
./artifacts/tauri frontend/editor/src-tauri/tauri.conf.json
|
||||
|
||||
# workflow_dispatch path requires platform=='all' so a single-platform
|
||||
# dispatch can't overwrite an existing release's full latest.json with a
|
||||
# partial one (action-gh-release defaults overwrite_files:true).
|
||||
# release event / release branch always build the full matrix so no extra guard needed.
|
||||
# release / V2-master always build the full matrix so no extra guard needed.
|
||||
# fail_on_unmatched_files makes a missing latest.json or installer fail loudly
|
||||
# instead of silently shipping a broken auto-update.
|
||||
- name: Upload binaries to Release
|
||||
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/release'
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master'
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||
# Don't regenerate/append notes on re-runs, and don't force this into the
|
||||
@@ -913,4 +944,6 @@ jobs:
|
||||
./artifacts/**/*.AppImage
|
||||
./artifacts/latest.json
|
||||
draft: false
|
||||
prerelease: false
|
||||
# Empty = keep the release's existing prerelease flag (action treats
|
||||
# empty as undefined); dispatch input can force true/false.
|
||||
prerelease: ${{ github.event.inputs.prerelease != 'keep' && github.event.inputs.prerelease || '' }}
|
||||
|
||||
@@ -4,11 +4,6 @@ on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *" # 2 AM UTC every night
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/nightly.yml
|
||||
- testing/cucumber/**
|
||||
- docker/embedded/compose/test_cicd.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -23,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -31,14 +26,14 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install all Playwright browsers
|
||||
run: task e2e:install
|
||||
|
||||
@@ -58,63 +53,10 @@ jobs:
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# Whole-suite accessibility sweep. Pull requests only scan the stories they
|
||||
# touch (frontend-a11y.yml) because a full pass takes ~30 minutes; this covers
|
||||
# everything else, so a violation introduced by a change somewhere other than
|
||||
# the story itself — a shared component, a theme token — still surfaces within
|
||||
# a day.
|
||||
a11y-all-stories:
|
||||
name: a11y (every story)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
theme: [light, dark]
|
||||
runs-on: ubuntu-latest
|
||||
# One full sweep (~30 minutes of browser time).
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: a11y gate (every story, ${{ matrix.theme }})
|
||||
run: task frontend:storybook:a11y:${{ matrix.theme }}
|
||||
|
||||
- name: Upload scan reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: a11y-scan-nightly-${{ matrix.theme }}-${{ github.run_id }}
|
||||
path: frontend/.a11y-scan/
|
||||
retention-days: 14
|
||||
if-no-files-found: ignore
|
||||
# The reports live in a dot-directory, which upload-artifact treats as
|
||||
# hidden and silently skips by default.
|
||||
include-hidden-files: true
|
||||
|
||||
# Builds all desktop platforms on a schedule so the Rust dependency cache is
|
||||
# written on main, where PR and merge-queue tauri builds can restore it.
|
||||
#
|
||||
# The only job here still pinned to schedule/main: it primes a cache rather than
|
||||
# testing anything, and Actions scopes a cache written on a PR branch to that PR
|
||||
# alone, so a PR run costs three platform builds and produces nothing reusable.
|
||||
warm-tauri-cache:
|
||||
name: Warm Tauri Rust cache
|
||||
if: github.event_name == 'schedule' || github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -123,75 +65,3 @@ jobs:
|
||||
platform: all
|
||||
sign: false
|
||||
secrets: inherit
|
||||
|
||||
# Runs the @nightly tag (conversion scenarios) plus a 10-shard concurrency run
|
||||
# of every other feature.
|
||||
cucumber-nightly:
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
name: Cucumber (nightly scenarios + full concurrency)
|
||||
runs-on: ubuntu-latest
|
||||
# Fork pull requests get no MAVEN_* secrets, so the image build cannot work.
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Start the fat image with login and storage enabled
|
||||
run: docker compose -f docker/embedded/compose/test_cicd.yml up -d --build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Wait for the server
|
||||
# Throwaway key from test_cicd.yml; out of the header literal for gitleaks.
|
||||
env:
|
||||
TEST_API_KEY: "123456789"
|
||||
run: |
|
||||
curl --retry 90 --retry-delay 3 --retry-connrefused --retry-all-errors \
|
||||
-sf -H "X-API-KEY: $TEST_API_KEY" http://localhost:8080/api/v1/info/status
|
||||
|
||||
# Heavy LibreOffice/Calibre/Ghostscript conversions, excluded from the PR run.
|
||||
# Both tasks install the behave deps themselves, so there is no separate uv sync step.
|
||||
- name: Run @nightly scenarios
|
||||
run: task cucumber:nightly
|
||||
|
||||
# Genuinely different payloads contending on one backend.
|
||||
- name: Sharded concurrency validation
|
||||
run: task cucumber:parallel SHARDS=10
|
||||
|
||||
- name: Container logs on failure
|
||||
if: failure()
|
||||
run: docker compose -f docker/embedded/compose/test_cicd.yml logs --tail 400
|
||||
|
||||
- name: Tear down
|
||||
if: always()
|
||||
run: docker compose -f docker/embedded/compose/test_cicd.yml down -v
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -73,14 +73,13 @@ jobs:
|
||||
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
update-homebrew-and-scoop:
|
||||
environment: package-publish
|
||||
needs: get-release-info
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
name: PR conflict labeler
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- edited
|
||||
- ready_for_review
|
||||
schedule:
|
||||
- cron: "17 */6 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: pr-conflict-labeler-${{ github.event.pull_request.number || 'all-open-prs' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CONFLICT_LABEL: "has conflicts"
|
||||
|
||||
jobs:
|
||||
label-conflicts:
|
||||
name: Label conflicted PRs
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # actions/checkout
|
||||
issues: write # get/create the repo-level conflict label
|
||||
pull-requests: write # pulls.get/list plus add/remove the label on PRs
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Apply conflict label
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
const conflictLabel = process.env.CONFLICT_LABEL;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const eventPullRequest = context.payload.pull_request;
|
||||
|
||||
async function sleep(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function getPullRequestWithMergeableState(pullNumber) {
|
||||
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
|
||||
if (pull.mergeable !== null) {
|
||||
return pull;
|
||||
}
|
||||
|
||||
core.info(`PR #${pullNumber}: mergeable is not ready yet (attempt ${attempt}/6).`);
|
||||
await sleep(5000);
|
||||
}
|
||||
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
return pull;
|
||||
}
|
||||
|
||||
async function ensureConflictLabel() {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: conflictLabel,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: conflictLabel,
|
||||
color: 'D93F0B',
|
||||
description: 'Pull request has merge conflicts with the base branch',
|
||||
});
|
||||
core.info(`Created '${conflictLabel}' label.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function labelPullRequest(pull) {
|
||||
const existingLabels = pull.labels.map((label) => label.name);
|
||||
const hasConflictLabel = existingLabels.includes(conflictLabel);
|
||||
const hasConflicts = pull.mergeable === false && pull.mergeable_state === 'dirty';
|
||||
|
||||
if (hasConflicts && !hasConflictLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull.number,
|
||||
labels: [conflictLabel],
|
||||
});
|
||||
core.info(`Added '${conflictLabel}' to PR #${pull.number}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasConflicts && hasConflictLabel) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull.number,
|
||||
name: conflictLabel,
|
||||
});
|
||||
core.info(`Removed '${conflictLabel}' from PR #${pull.number}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`PR #${pull.number}: no label change needed (mergeable=${pull.mergeable}, mergeable_state=${pull.mergeable_state}).`);
|
||||
}
|
||||
|
||||
await ensureConflictLabel();
|
||||
|
||||
let pullNumbers;
|
||||
if (eventPullRequest) {
|
||||
pullNumbers = [eventPullRequest.number];
|
||||
} else {
|
||||
const pulls = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
pullNumbers = pulls.map((pull) => pull.number);
|
||||
core.info(`Checking ${pullNumbers.length} open PR(s).`);
|
||||
}
|
||||
|
||||
for (const pullNumber of pullNumbers) {
|
||||
const pull = await getPullRequestWithMergeableState(pullNumber);
|
||||
await labelPullRequest(pull);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -25,20 +25,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
|
||||
- name: Run pre-commit checks
|
||||
run: task pre-commit
|
||||
|
||||
# The fixture corpus checks the comment rules themselves, so it runs here
|
||||
# rather than on every local commit.
|
||||
- name: Check the comment-lint fixture corpus
|
||||
run: task pre-commit:comment-lint:selftest
|
||||
|
||||
@@ -17,9 +17,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push-base:
|
||||
# Own environment: docker-publish is branch-locked to release/main,
|
||||
# which excludes the baseDockerImage/accessIssueFix branches this runs on.
|
||||
environment: docker-base-publish
|
||||
if: ${{ vars.CI_PROFILE != 'lite' && github.actor == 'Frooodle' }}
|
||||
runs-on: ubuntu-24.04-8core
|
||||
permissions:
|
||||
@@ -35,11 +32,9 @@ jobs:
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
env:
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
VERSION="${INPUT_VERSION}"
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then
|
||||
VERSION="1.0.3"
|
||||
else
|
||||
@@ -48,20 +43,20 @@ jobs:
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -69,10 +64,10 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
@@ -80,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Generate tags for base image
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf-base
|
||||
@@ -90,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Build and push base image
|
||||
id: build-push-base
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: docker/base
|
||||
|
||||
@@ -18,20 +18,12 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
build_engine:
|
||||
description: "Build & push the standalone stirling-engine image."
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
force_engine_rebuild:
|
||||
description: "Rebuild stirling-engine even if its source hash is unchanged."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
- master
|
||||
- main
|
||||
- V2-master
|
||||
- testMain
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
@@ -50,7 +42,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
environment: docker-publish
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-24.04-8core
|
||||
permissions:
|
||||
@@ -60,35 +51,41 @@ jobs:
|
||||
env:
|
||||
RUN_MAIN_APP: ${{ github.event_name != 'workflow_dispatch' || inputs.build_main_app }}
|
||||
RUN_UNOSERVER: ${{ github.event_name != 'workflow_dispatch' || inputs.build_unoserver }}
|
||||
RUN_ENGINE: ${{ github.event_name != 'workflow_dispatch' || inputs.build_engine }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-push-docker-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
@@ -98,32 +95,32 @@ jobs:
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
@@ -132,7 +129,7 @@ jobs:
|
||||
- name: Generate tags for latest
|
||||
id: meta
|
||||
if: env.RUN_MAIN_APP == 'true'
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -140,14 +137,15 @@ jobs:
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/release' }}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/release' }}
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/testMain' }}
|
||||
|
||||
- name: Build and push Unified Dockerfile (latest variant)
|
||||
id: build-push-latest
|
||||
# Empty-tag guard: build-push-action errors when asked to push with no tags.
|
||||
if: env.RUN_MAIN_APP == 'true' && steps.meta.outputs.tags != ''
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
@@ -165,7 +163,7 @@ jobs:
|
||||
sbom: true
|
||||
|
||||
- name: Sign regular images
|
||||
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-latest.outputs.digest != ''
|
||||
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-latest.outputs.digest != ''
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-latest.outputs.digest }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
@@ -180,7 +178,7 @@ jobs:
|
||||
|
||||
- name: Generate tags for latest-fat
|
||||
id: meta-fat
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
with:
|
||||
images: |
|
||||
@@ -189,12 +187,12 @@ jobs:
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/release' }}
|
||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/release' }}
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||
|
||||
- name: Build and push Unified Dockerfile (fat variant)
|
||||
id: build-push-fat
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' && steps.meta-fat.outputs.tags != ''
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
@@ -211,7 +209,7 @@ jobs:
|
||||
sbom: true
|
||||
|
||||
- name: Sign fat images
|
||||
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-fat.outputs.digest != ''
|
||||
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-fat.outputs.digest != ''
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
|
||||
TAGS: ${{ steps.meta-fat.outputs.tags }}
|
||||
@@ -224,7 +222,7 @@ jobs:
|
||||
|
||||
- name: Generate tags for ultra-lite
|
||||
id: meta-lite
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
with:
|
||||
images: |
|
||||
@@ -233,12 +231,12 @@ jobs:
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }}
|
||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }}
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||
|
||||
- name: Build and push Unified Dockerfile (ultra-lite variant)
|
||||
id: build-push-lite
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' && steps.meta-lite.outputs.tags != ''
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
@@ -255,7 +253,7 @@ jobs:
|
||||
sbom: true
|
||||
|
||||
- name: Sign ultra-lite images
|
||||
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-lite.outputs.digest != ''
|
||||
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-lite.outputs.digest != ''
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
|
||||
TAGS: ${{ steps.meta-lite.outputs.tags }}
|
||||
@@ -267,7 +265,7 @@ jobs:
|
||||
done
|
||||
|
||||
# Standalone unoserver image — versioned independently via
|
||||
# docker/unoserver/VERSION. release: publish <version>+latest
|
||||
# docker/unoserver/VERSION. master/V2-master: publish <version>+latest
|
||||
# only when the version is new. main/testMain: republish :alpha only
|
||||
# when the source hash differs from the published image's annotation.
|
||||
- name: Read unoserver image version
|
||||
@@ -326,7 +324,7 @@ jobs:
|
||||
fi
|
||||
|
||||
case "$EFFECTIVE_REF" in
|
||||
refs/heads/release)
|
||||
refs/heads/master|refs/heads/V2-master)
|
||||
if [ "${FORCE_REBUILD}" = "true" ]; then
|
||||
echo "force_unoserver_rebuild=true — building stable regardless"
|
||||
mode="stable"
|
||||
@@ -367,7 +365,7 @@ jobs:
|
||||
- name: Build and push unoserver image
|
||||
id: build-push-unoserver
|
||||
if: env.RUN_UNOSERVER == 'true' && steps.unoserverDecision.outputs.mode != 'skip'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
@@ -398,119 +396,3 @@ jobs:
|
||||
else
|
||||
echo "Warning: COSIGN_PRIVATE_KEY not set, skipping unoserver image signing"
|
||||
fi
|
||||
|
||||
# Standalone AI engine image, same shape as the unoserver image above.
|
||||
- name: Compute engine image source hash
|
||||
id: engineHash
|
||||
if: env.RUN_ENGINE == 'true'
|
||||
run: |
|
||||
set -eu
|
||||
hash=$( { cat engine/Dockerfile engine/pyproject.toml engine/uv.lock engine/.env; \
|
||||
find engine/src -type f -print0 | sort -z | xargs -0 cat; } \
|
||||
| sha256sum | cut -d' ' -f1)
|
||||
echo "hash=${hash}" >> "$GITHUB_OUTPUT"
|
||||
echo "Engine source hash: ${hash}"
|
||||
|
||||
- name: Decide whether to publish engine image
|
||||
id: engineDecision
|
||||
if: env.RUN_ENGINE == 'true'
|
||||
env:
|
||||
ENGINE_VERSION: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
ENGINE_HASH: ${{ steps.engineHash.outputs.hash }}
|
||||
ENGINE_IMAGE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-engine
|
||||
ENGINE_HASH_ANNOTATION: org.stirlingpdf.engine-source-hash
|
||||
FORCE_REBUILD: ${{ inputs.force_engine_rebuild }}
|
||||
GH_REF: ${{ github.ref }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set -eu
|
||||
mode="skip"
|
||||
tags=""
|
||||
|
||||
read_published_hash() {
|
||||
local ref="$1"
|
||||
docker buildx imagetools inspect "$ref" --raw 2>/dev/null \
|
||||
| jq -r --arg key "$ENGINE_HASH_ANNOTATION" \
|
||||
'.annotations[$key] // empty' \
|
||||
2>/dev/null || true
|
||||
}
|
||||
|
||||
# Manual dispatch from any branch routes to the :alpha publish path.
|
||||
EFFECTIVE_REF="$GH_REF"
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
EFFECTIVE_REF="refs/heads/testMain"
|
||||
fi
|
||||
|
||||
case "$EFFECTIVE_REF" in
|
||||
refs/heads/release)
|
||||
if [ "${FORCE_REBUILD}" = "true" ]; then
|
||||
echo "force_engine_rebuild=true — building stable regardless"
|
||||
mode="stable"
|
||||
tags="${ENGINE_IMAGE}:${ENGINE_VERSION},${ENGINE_IMAGE}:latest"
|
||||
elif docker manifest inspect "${ENGINE_IMAGE}:${ENGINE_VERSION}" >/dev/null 2>&1; then
|
||||
echo "stirling-engine:${ENGINE_VERSION} already on GHCR — skipping"
|
||||
else
|
||||
echo "stirling-engine:${ENGINE_VERSION} is new — will publish"
|
||||
mode="stable"
|
||||
tags="${ENGINE_IMAGE}:${ENGINE_VERSION},${ENGINE_IMAGE}:latest"
|
||||
fi
|
||||
;;
|
||||
refs/heads/main|refs/heads/testMain)
|
||||
published_hash=$(read_published_hash "${ENGINE_IMAGE}:alpha")
|
||||
if [ "${FORCE_REBUILD}" = "true" ]; then
|
||||
echo "force_engine_rebuild=true — rebuilding :alpha regardless"
|
||||
mode="alpha"
|
||||
tags="${ENGINE_IMAGE}:alpha"
|
||||
elif [ -n "$published_hash" ] && [ "$published_hash" = "$ENGINE_HASH" ]; then
|
||||
echo "Published :alpha source hash matches (${published_hash}) — skipping"
|
||||
else
|
||||
if [ -z "$published_hash" ]; then
|
||||
echo ":alpha has no source-hash annotation (first publish) — will publish"
|
||||
else
|
||||
echo "Source hash changed (was ${published_hash}, now ${ENGINE_HASH}) — will publish"
|
||||
fi
|
||||
mode="alpha"
|
||||
tags="${ENGINE_IMAGE}:alpha"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Branch ${GH_REF} does not publish engine image"
|
||||
;;
|
||||
esac
|
||||
echo "mode=${mode}" >> "$GITHUB_OUTPUT"
|
||||
echo "tags=${tags}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push engine image
|
||||
id: build-push-engine
|
||||
if: env.RUN_ENGINE == 'true' && steps.engineDecision.outputs.mode != 'skip'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-engine
|
||||
cache-to: type=gha,mode=max,scope=stirling-engine
|
||||
tags: ${{ steps.engineDecision.outputs.tags }}
|
||||
# Manifest annotation read by the decision step above to detect drift.
|
||||
annotations: |
|
||||
index:org.stirlingpdf.engine-source-hash=${{ steps.engineHash.outputs.hash }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Sign engine image
|
||||
if: env.RUN_ENGINE == 'true' && steps.engineDecision.outputs.mode == 'stable'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-engine.outputs.digest }}
|
||||
TAGS: ${{ steps.engineDecision.outputs.tags }}
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "$COSIGN_PRIVATE_KEY" ]; then
|
||||
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
|
||||
done
|
||||
else
|
||||
echo "Warning: COSIGN_PRIVATE_KEY not set, skipping engine image signing"
|
||||
fi
|
||||
|
||||
@@ -13,13 +13,12 @@ permissions:
|
||||
|
||||
jobs:
|
||||
rollback:
|
||||
environment: docker-publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -27,13 +26,13 @@ jobs:
|
||||
uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 30 days stale issues
|
||||
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
- master
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
@@ -23,33 +23,27 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
# package-publish holds SWAGGERHUB_API_KEY. It requires reviewer approval and
|
||||
# is limited to main / release / v* tags, so every push to release waits on one.
|
||||
environment: package-publish
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-swagger-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
@@ -63,7 +57,7 @@ jobs:
|
||||
SWAGGERHUB_USER: "Frooodle"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
name: Sync Portal Docs
|
||||
|
||||
# Regenerates the portal Developer Docs manifest from the Stirling docs repo and
|
||||
# opens a PR when it changes. Runs weekly, on manual dispatch, or when the docs
|
||||
# repo fires a `docs-updated` repository_dispatch.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Docs repo ref (branch or tag) to sync from"
|
||||
required: false
|
||||
default: "main"
|
||||
repository_dispatch:
|
||||
types: [docs-updated]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
environment: bot-identity
|
||||
name: Sync docs manifest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
env:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Regenerate docs manifest
|
||||
working-directory: frontend
|
||||
env:
|
||||
DOCS_REF: ${{ github.event.inputs.ref || github.event.client_payload.ref || 'main' }}
|
||||
GITHUB_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
run: npm run docs:sync
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Sync portal docs from docs repo"
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: sync-portal-docs
|
||||
base: main
|
||||
title: "Sync portal docs from docs repo"
|
||||
body: |
|
||||
Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot].
|
||||
|
||||
Regenerates `frontend/editor/src/portal/generated/docsManifest.json`
|
||||
from the Stirling docs repo via `npm run docs:sync`.
|
||||
labels: |
|
||||
Documentation
|
||||
github-actions
|
||||
Front End
|
||||
add-paths: frontend/editor/src/portal/generated/docsManifest.json
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
@@ -10,7 +10,6 @@ on:
|
||||
- "app/common/build.gradle"
|
||||
- "app/core/build.gradle"
|
||||
- "app/proprietary/build.gradle"
|
||||
- "gradle/spotless.gradle"
|
||||
- "README.md"
|
||||
- "frontend/editor/public/locales/*/translation.toml"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
@@ -33,11 +32,10 @@ permissions:
|
||||
|
||||
jobs:
|
||||
sync-files:
|
||||
environment: bot-identity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -52,24 +50,27 @@ jobs:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv sync --project engine --locked --group tools
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||
|
||||
- name: Sort translation TOML files
|
||||
run: |
|
||||
@@ -82,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
uv run --project engine --locked --group tools python scripts/counter_translation_v3.py
|
||||
python scripts/counter_translation_v3.py
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
|
||||
@@ -26,10 +26,6 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
use_shared_cache:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
@@ -61,18 +57,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
# Only probes APPLE_CERTIFICATE for presence, so it stays on the unrestricted
|
||||
# signing environment - release-signing would block every PR run.
|
||||
environment:
|
||||
name: ci-signing
|
||||
deployment: false
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -112,12 +103,6 @@ jobs:
|
||||
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
# Windows/GPG signing only runs on main (see the per-step gates below), so only
|
||||
# that path needs the reviewer-gated release-signing environment. Everything else
|
||||
# (PRs, merge queue, nightly) signs macOS only and uses ci-signing, which has no
|
||||
# approval or branch restriction.
|
||||
environment:
|
||||
name: ${{ (inputs.sign && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) && 'release-signing' || 'ci-signing' }}
|
||||
needs: determine-matrix
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -125,6 +110,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
# Per-platform sign gate. macOS signs on any run with the cert available,
|
||||
@@ -134,7 +120,7 @@ jobs:
|
||||
SIGN_BUNDLE: ${{ inputs.sign && (matrix.platform == 'macos-15' && secrets.APPLE_CERTIFICATE != '' || github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -164,7 +150,7 @@ jobs:
|
||||
# only recompiles the app crate. Written on main; PRs and the merge queue
|
||||
# restore from it.
|
||||
- name: Cache Rust build
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: frontend/editor/src-tauri
|
||||
# Stable key shared across workflows so the nightly warmer.
|
||||
@@ -174,27 +160,9 @@ jobs:
|
||||
# Save the dependency cache even if a later step fails
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
if: inputs.use_shared_cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Restore cache Gradle
|
||||
if: inputs.use_shared_cache == false
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-tauri-build-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
||||
if: matrix.platform == 'macos-15'
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
@@ -206,13 +174,18 @@ jobs:
|
||||
|
||||
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Build universal macOS JRE
|
||||
if: matrix.platform == 'macos-15'
|
||||
@@ -248,26 +221,20 @@ jobs:
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
env:
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
|
||||
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
|
||||
# Decode client certificate
|
||||
$certBytes = [Convert]::FromBase64String("$env:SM_CLIENT_CERT_FILE_B64")
|
||||
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
|
||||
$certPath = "D:\Certificate_pkcs12.p12"
|
||||
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||
|
||||
# Set environment variables
|
||||
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
|
||||
echo "SM_HOST=$env:SM_HOST" >> $env:GITHUB_ENV
|
||||
echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV
|
||||
echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV
|
||||
echo "SM_KEYPAIR_ALIAS=$env:SM_KEYPAIR_ALIAS" >> $env:GITHUB_ENV
|
||||
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
|
||||
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
|
||||
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
|
||||
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
|
||||
|
||||
# Get PKCS11 config path from DigiCert action
|
||||
$pkcs11Config = $env:PKCS11_CONFIG
|
||||
@@ -285,6 +252,38 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
shell: powershell
|
||||
run: |
|
||||
if ($env:WINDOWS_CERTIFICATE) {
|
||||
Write-Host "Importing Windows Code Signing Certificate..."
|
||||
|
||||
# Decode base64 certificate and save to file
|
||||
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
|
||||
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
|
||||
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||
|
||||
# Import certificate to CurrentUser\My store
|
||||
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
|
||||
|
||||
# Extract and set thumbprint as environment variable
|
||||
$thumbprint = $cert.Thumbprint
|
||||
Write-Host "Certificate imported with thumbprint: $thumbprint"
|
||||
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
|
||||
|
||||
# Clean up certificate file
|
||||
Remove-Item $certPath
|
||||
|
||||
Write-Host "Windows certificate import completed."
|
||||
} else {
|
||||
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
|
||||
}
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
env:
|
||||
@@ -381,7 +380,7 @@ jobs:
|
||||
|
||||
- name: Build Tauri app (signed)
|
||||
if: env.SIGN_BUNDLE == 'true'
|
||||
uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -416,7 +415,7 @@ jobs:
|
||||
|
||||
- name: Build Tauri app (unsigned)
|
||||
if: env.SIGN_BUNDLE != 'true'
|
||||
uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SIGN: "0"
|
||||
@@ -442,7 +441,7 @@ jobs:
|
||||
- name: Build Tauri app (Linux AppImage)
|
||||
if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal
|
||||
continue-on-error: true
|
||||
uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SIGN: ${{ (inputs.sign && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
|
||||
@@ -677,17 +676,6 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
rm -f certificate.p12
|
||||
rm -rf "$RUNNER_TEMP/msi-verify"
|
||||
if [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
pr-comment:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -703,7 +691,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -795,7 +783,7 @@ jobs:
|
||||
if: always()
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -22,45 +22,21 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# A changed base image is shared by all three embedded-image builds. Build
|
||||
# it once and transfer it as an artifact; the matrix jobs use the local
|
||||
# Docker driver so the loaded image is visible to the build.
|
||||
prepare-base-image:
|
||||
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build base image locally
|
||||
run: docker build --platform linux/amd64 -t stirling-pdf-base:pr-test -f docker/base/Dockerfile docker/base
|
||||
|
||||
- name: Export base image
|
||||
run: docker save stirling-pdf-base:pr-test | gzip -1 > stirling-pdf-base-pr-test.tar.gz
|
||||
|
||||
- name: Upload base image
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docker-base-pr-test
|
||||
path: stirling-pdf-base-pr-test.tar.gz
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
# TODO: extract a pre-matrix `prepare` job that runs once and produces
|
||||
# shared artifacts for the three matrix entries below to consume:
|
||||
# 1. `task backend:build` — currently runs 3× in parallel with
|
||||
# identical env (DISABLE_ADDITIONAL_FEATURES=true,
|
||||
# STIRLING_PDF_DESKTOP_UI=false). Build once, upload the JAR as an
|
||||
# artifact, matrix entries download.
|
||||
# 2. The base-image `docker build` (gated on docker-base-changed) —
|
||||
# currently runs 3× in parallel against the same Dockerfile and
|
||||
# context. Build once, `docker save` to an artifact, matrix entries
|
||||
# `docker load` before the embedded build.
|
||||
# Saves ~2 full backend builds + 2 base-image builds per PR that touches
|
||||
# docker. May also be reusable from backend-build.yml's jdk-25 +
|
||||
# spring-security=true matrix entry if `task backend:build` and
|
||||
# `task backend:build:ci` produce equivalent JARs (verify before wiring).
|
||||
test-build-docker-images:
|
||||
if: always() && (needs.prepare-base-image.result == 'success' || needs.prepare-base-image.result == 'skipped')
|
||||
needs: [prepare-base-image]
|
||||
environment:
|
||||
name: ci-unsigned
|
||||
deployment: false
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -77,7 +53,7 @@ jobs:
|
||||
cache-scope: stirling-pdf-fat
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -85,7 +61,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -102,32 +78,29 @@ jobs:
|
||||
docker system prune -af || true
|
||||
echo "Disk space after cleanup:" && df -h
|
||||
|
||||
- name: Download prepared base image
|
||||
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: docker-base-pr-test
|
||||
|
||||
- name: Load prepared base image
|
||||
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
|
||||
run: gzip -dc stirling-pdf-base-pr-test.tar.gz | docker load
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Build application
|
||||
run: task backend:build
|
||||
env:
|
||||
@@ -138,11 +111,16 @@ jobs:
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build base image locally (PR base change only)
|
||||
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
|
||||
run: |
|
||||
docker build -t stirling-pdf-base:pr-test -f docker/base/Dockerfile docker/base
|
||||
|
||||
- name: Set base image and platform for this build
|
||||
id: build-params
|
||||
@@ -217,7 +195,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -225,11 +203,11 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
name: UI test with TestDriverAI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master", "UITest", "testdriver"]
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
# or a pull request is updated.
|
||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push test image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-latest
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy to VPS
|
||||
run: |
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: stirling-pdf-test-${{ github.sha }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||
ports:
|
||||
- "1337:8080"
|
||||
volumes:
|
||||
- /stirling/test-${{ github.sha }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/test-${{ github.sha }}/config:/configs:rw
|
||||
- /stirling/test-${{ github.sha }}/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: "Stirling-PDF Test"
|
||||
UI_HOMEDESCRIPTION: "Test Deployment"
|
||||
UI_APPNAMENAVBAR: "Test"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF
|
||||
mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs}
|
||||
mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml
|
||||
cd /stirling/test-${{ github.sha }}
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
EOF
|
||||
|
||||
files-changed:
|
||||
if: always()
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
filters: ".github/config/.files.yaml"
|
||||
|
||||
test:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [deploy, files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Run TestDriver.ai
|
||||
uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
|
||||
with:
|
||||
key: ${{secrets.TESTDRIVER_API_KEY}}
|
||||
prerun: |
|
||||
choco install go-task -y
|
||||
task frontend:build
|
||||
cd frontend
|
||||
npm install dashcam-chrome --save
|
||||
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337"
|
||||
Start-Sleep -Seconds 20
|
||||
prompt: |
|
||||
1. /run testing/testdriver/test.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FORCE_COLOR: "3"
|
||||
|
||||
cleanup:
|
||||
needs: [deploy, test]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup deployment
|
||||
if: always()
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF
|
||||
cd /stirling/test-${{ github.sha }}
|
||||
docker-compose down
|
||||
cd /stirling
|
||||
rm -rf test-${{ github.sha }}
|
||||
EOF
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
@@ -1,112 +0,0 @@
|
||||
name: Update Gradle
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * 1"
|
||||
|
||||
concurrency:
|
||||
group: update-gradle
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-gradle:
|
||||
name: Update Gradle and Docker images
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
|
||||
- name: Find latest Gradle release
|
||||
id: gradle
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(curl --fail --silent --show-error --retry 3 \
|
||||
https://services.gradle.org/versions/current | jq -r '.version')
|
||||
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo "Could not determine a stable Gradle version: $version" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find matching Docker image digest
|
||||
id: docker
|
||||
env:
|
||||
GRADLE_VERSION: ${{ steps.gradle.outputs.version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${GRADLE_VERSION}-jdk25"
|
||||
digest=$(curl --fail --silent --show-error --retry 3 \
|
||||
"https://hub.docker.com/v2/repositories/library/gradle/tags/${tag}" \
|
||||
| jq -r '.digest // empty')
|
||||
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || {
|
||||
echo "Docker image gradle:${tag} was not found" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "digest=$digest" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update Gradle wrapper
|
||||
env:
|
||||
GRADLE_VERSION: ${{ steps.gradle.outputs.version }}
|
||||
run: ./gradlew wrapper --gradle-version "$GRADLE_VERSION" --distribution-type bin
|
||||
|
||||
- name: Update Gradle Docker images
|
||||
env:
|
||||
DOCKER_TAG: ${{ steps.docker.outputs.tag }}
|
||||
DOCKER_DIGEST: ${{ steps.docker.outputs.digest }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
find docker -type f -name 'Dockerfile*' -print0 |
|
||||
xargs -0 sed -E -i \
|
||||
"s#gradle:[^@[:space:]]+-jdk25(@sha256:[^[:space:]]+)?#gradle:${DOCKER_TAG}@${DOCKER_DIGEST}#g"
|
||||
|
||||
- name: Verify Gradle update
|
||||
env:
|
||||
EXPECTED_VERSION: ${{ steps.gradle.outputs.version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
actual=$(./gradlew --version | sed -n 's/^Gradle \([0-9.]*\)$/\1/p')
|
||||
[[ "$actual" == "$EXPECTED_VERSION" ]] || {
|
||||
echo "Wrapper resolved Gradle $actual, expected $EXPECTED_VERSION" >&2
|
||||
exit 1
|
||||
}
|
||||
if git diff --quiet; then
|
||||
echo "Gradle is already up to date."
|
||||
exit 0
|
||||
fi
|
||||
git diff --check
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: automation/update-gradle
|
||||
delete-branch: true
|
||||
commit-message: "chore: update Gradle"
|
||||
title: "chore: update Gradle to ${{ steps.gradle.outputs.version }}"
|
||||
body: |
|
||||
Automated update of the Gradle wrapper and Gradle Docker build images.
|
||||
|
||||
Gradle version: `${{ steps.gradle.outputs.version }}`
|
||||
Docker image: `gradle:${{ steps.docker.outputs.tag }}`
|
||||
labels: dependencies
|
||||
+2
-24
@@ -22,11 +22,7 @@ pipeline/
|
||||
customFiles/
|
||||
configs/
|
||||
watchedFolders/
|
||||
# The rule above targets the app's runtime watched-folders working dir, but it
|
||||
# also matches this frontend source component dir; keep the source tracked.
|
||||
!frontend/editor/src/proprietary/components/watchedFolders/
|
||||
clientWebUI/
|
||||
policy-webhook-spool/
|
||||
# Scratch dir used by local fixture-regeneration runs (see
|
||||
# app/proprietary/src/test/resources/db-migration-fixtures/README.md).
|
||||
# Holds downloaded JARs and disposable workdirs. Never committed.
|
||||
@@ -39,7 +35,6 @@ exampleYmlFiles/stirling/
|
||||
/testing/file_snapshots
|
||||
/testing/cucumber/junit/
|
||||
/testing/cucumber/report.html
|
||||
/testing/cucumber/.parallel/
|
||||
/testing/.failed_tests
|
||||
/.test-state/
|
||||
SwaggerDoc.json
|
||||
@@ -65,23 +60,13 @@ app/core/src/main/resources/static/og_images/
|
||||
app/core/src/main/resources/static/samples/
|
||||
app/core/src/main/resources/static/manifest-classic.json
|
||||
app/core/src/main/resources/static/og-metadata.json
|
||||
app/core/src/main/resources/static/og-metadata.saas.json
|
||||
app/core/src/main/resources/static/sw-folder-retry.js
|
||||
app/core/src/main/resources/static/robots.txt
|
||||
app/core/src/main/resources/static/android-chrome-*.png
|
||||
app/core/src/main/resources/static/mstile-*.png
|
||||
app/core/src/main/resources/static/favicon.png
|
||||
app/core/src/main/resources/static/safari-pinned-tab.svg
|
||||
app/core/src/main/resources/static/pdfium/
|
||||
app/core/src/main/resources/static/pdfjs/
|
||||
app/core/src/main/resources/static/vendor/
|
||||
app/core/src/main/resources/static/**/*.gz
|
||||
app/core/src/main/resources/static/**/*.br
|
||||
app/core/src/main/resources/static/css/cookieconsent.css
|
||||
app/core/src/main/resources/static/css/cookieconsentCustomisation.css
|
||||
app/core/src/main/resources/static/mockServiceWorker.js
|
||||
app/core/src/main/resources/static/js/thirdParty/cookieconsent.umd.js
|
||||
app/core/src/main/resources/static/images/google-drive.svg
|
||||
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
|
||||
|
||||
# Gradle
|
||||
@@ -176,8 +161,6 @@ app/core/src/main/resources/static/images/google-drive.svg
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
# Real backend archives the form-bundle reader is tested against.
|
||||
!frontend/editor/src/core/tools/formFill/__fixtures__/*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.db
|
||||
@@ -298,13 +281,8 @@ docs/type3/signatures/
|
||||
|
||||
**/application-dev-local.properties
|
||||
|
||||
# Claude. Contents are ignored so personal config stays local, with the two
|
||||
# shared pieces re-included: settings.json (the comment-lint hook) and skills/.
|
||||
# The directory itself cannot be ignored or git will not look inside it.
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
!.claude/skills/
|
||||
.claude/settings.local.json
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
# Playwright MCP screenshots / traces
|
||||
.playwright-mcp/
|
||||
|
||||
+3
-8
@@ -15,20 +15,15 @@ testing/compose/validate-mcp-test.sh:curl-auth-header:92
|
||||
testing/compose/validate-mcp-test.sh:curl-auth-header:116
|
||||
|
||||
# Storybook example showing curl with a fake Bearer token placeholder (sk_live_a3f8...).
|
||||
frontend/editor/src/proprietary/ui/CodeBlock.stories.tsx:curl-auth-header:5
|
||||
frontend/shared/components/CodeBlock.stories.tsx:curl-auth-header:4
|
||||
|
||||
# Truncated placeholder API key in portal docs example (sk_live_8f2c...e10) - not a real secret.
|
||||
frontend/editor/src/portal/components/docs/GettingStartedSection.tsx:generic-api-key:30
|
||||
frontend/portal/src/components/docs/GettingStartedSection.tsx:generic-api-key:31
|
||||
|
||||
# False positive: generic-api-key matches the Java type name "X509Certificate"
|
||||
# in a method signature (CreateSignatureBase.resolveSignatureAlgorithm) - not a secret.
|
||||
app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase.java:generic-api-key:224
|
||||
app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java:generic-api-key:224
|
||||
|
||||
# Supabase publishable key (public by design, RLS-protected) used as a CI fallback
|
||||
# default in the tauri-build workflow when the GitHub secret is unset - not a real secret.
|
||||
.github/workflows/tauri-build.yml:generic-api-key:402
|
||||
|
||||
# Staging Supabase publishable key (public by design). Ignored here rather than with an
|
||||
# inline gitleaks:allow because a trailing comment in a .properties file is part of the
|
||||
# value, so the pragma would end up inside the key.
|
||||
app/saas/src/main/resources/application-staging.properties:generic-api-key:16
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"ignoredFiles": [
|
||||
"frontend/editor/src-tauri/icons/macos/*",
|
||||
"frontend/editor/src-tauri/icons/linux/*",
|
||||
"frontend/editor/src-tauri/icons/windows/*"
|
||||
]
|
||||
}
|
||||
+10
-120
@@ -26,6 +26,7 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
POLICIES_ENABLED: '{{.POLICIES_ENABLED}}'
|
||||
|
||||
dev:proprietary:
|
||||
desc: "Start backend dev server in proprietary mode"
|
||||
@@ -40,15 +41,13 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default ""}}'
|
||||
# Set by dev:linked. Inline rather than in `env:` so an empty value emits nothing
|
||||
# and cannot blank the committed default.
|
||||
ACCOUNT_LINK_SAAS_BASE_URL: '{{.ACCOUNT_LINK_SAAS_BASE_URL | default ""}}'
|
||||
POLICIES_ENABLED: '{{.POLICIES_ENABLED | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .ACCOUNT_LINK_SAAS_BASE_URL}}STIRLING_BILLING_ACCOUNT_LINK_ENABLED=true STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL={{.ACCOUNT_LINK_SAAS_BASE_URL}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .ACCOUNT_LINK_SAAS_BASE_URL}}STIRLING_BILLING_ACCOUNT_LINK_ENABLED=true STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL={{.ACCOUNT_LINK_SAAS_BASE_URL}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
|
||||
dev:bundled:
|
||||
@@ -60,128 +59,29 @@ tasks:
|
||||
- cmd: ./gradlew clean bootRun -PbuildWithFrontend=true
|
||||
platforms: [linux, darwin]
|
||||
|
||||
# SaaS backend. dev:saas -> the PR's preview branch, staging:saas -> shared v3,
|
||||
# PROFILES=none -> production against your own SAAS_DB_*. Production has no named
|
||||
# task on purpose. Use `none`, not an empty value: Go template `default` treats ""
|
||||
# as absent and would resolve back to dev.
|
||||
|
||||
dev:saas:
|
||||
desc: "Start SaaS backend against the current PR's Supabase preview branch"
|
||||
desc: "Start backend in SaaS flavor against Supabase"
|
||||
# `dotenv:` reads from the root Taskfile's directory (".") because this
|
||||
# subtaskfile is included with `dir: .`.
|
||||
dotenv: ['app/.env.saas.local', 'app/.env.saas']
|
||||
vars:
|
||||
PROFILES: '{{.PROFILES | default "dev"}}'
|
||||
cmds:
|
||||
# Don't move this check into a `sh:` var: dotenv is visible in cmds but not
|
||||
# during var evaluation, so the test would always see an empty value.
|
||||
- cmd: |
|
||||
if [ "{{.PROFILES}}" = "dev" ] && [ -z "${SAAS_DEV_PROJECT_REF:-}" ]; then
|
||||
echo ">> SAAS_DEV_PROJECT_REF is not set."
|
||||
echo ">> Testing a SaaS PR? Put its ref, DB password and publishable key in app/.env.saas.local."
|
||||
echo ">> Wanted the shared v3 project? Use 'task backend:staging:saas' instead."
|
||||
exit 1
|
||||
fi
|
||||
- task: _run:saas
|
||||
vars:
|
||||
PORT: '{{.PORT}}'
|
||||
PROFILES: '{{.PROFILES}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
APP_BASE_URL: '{{.APP_BASE_URL}}'
|
||||
BASE_PATH: '{{.BASE_PATH}}'
|
||||
|
||||
staging:saas:
|
||||
desc: "Start SaaS backend against the shared v3 staging project"
|
||||
cmds:
|
||||
- task: _run:saas
|
||||
vars:
|
||||
PORT: '{{.PORT}}'
|
||||
PROFILES: staging
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
APP_BASE_URL: '{{.APP_BASE_URL}}'
|
||||
BASE_PATH: '{{.BASE_PATH}}'
|
||||
|
||||
dev:linked:
|
||||
desc: "Self-hosted backend linked to a locally running SaaS backend (see task linked:*)"
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
SAAS_BASE_URL: '{{.SAAS_BASE_URL | default "http://localhost:8081"}}'
|
||||
cmds:
|
||||
- 'echo ">> self-hosted :{{.PORT}} linking to SaaS at {{.SAAS_BASE_URL}}"'
|
||||
# The two backends run different STIRLING_FLAVOURs, which are different Gradle
|
||||
# project graphs sharing one build/ tree. Waiting avoids overlapping builds; it
|
||||
# does not make the sharing safe, so avoid rebuilding one while the other runs.
|
||||
- cmd: |
|
||||
n=0
|
||||
while [ "$n" -lt 150 ]; do
|
||||
if curl -s -m 2 "{{.SAAS_BASE_URL}}" >/dev/null 2>&1; then
|
||||
echo ">> SaaS backend is up, starting self-hosted"
|
||||
break
|
||||
fi
|
||||
n=$((n + 1))
|
||||
{{if eq OS "windows"}}powershell -NoProfile -Command "Start-Sleep -Seconds 2"{{else}}sleep 2{{end}}
|
||||
done
|
||||
if [ "$n" -ge 150 ]; then
|
||||
echo ">> SaaS backend never answered; starting anyway"
|
||||
fi
|
||||
- task: dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.PORT}}'
|
||||
ACCOUNT_LINK_SAAS_BASE_URL: '{{.SAAS_BASE_URL}}'
|
||||
|
||||
_run:saas:
|
||||
internal: true
|
||||
# The frontend files are here only for RUN_SUBPATH, which the authorize URL needs.
|
||||
# Last, because dotenv is set-if-absent: app/* still decides everything else.
|
||||
dotenv:
|
||||
- 'app/.env.saas.local'
|
||||
- 'app/.env.saas'
|
||||
- 'frontend/editor/.env.saas.local'
|
||||
- 'frontend/editor/.env.saas'
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
# Override to "" to run the pure `saas` profile against your own SAAS_DB_*.
|
||||
PROFILES: '{{.PROFILES | default "dev"}}'
|
||||
# Built here rather than inline in the cmds below: the Windows line is an
|
||||
# unquoted YAML scalar wrapping a cmd.exe string, so a nested {{if ne .X
|
||||
# "none"}} needs escaped quotes that reach the Go template as literal
|
||||
# backslashes and fail with `unexpected "\" in operand`.
|
||||
PROFILE_ARGS: '{{if ne .PROFILES "none"}}--spring.profiles.include={{.PROFILES}}{{end}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
# Empty is the same as unset: the property defaults to empty and is blank-checked.
|
||||
APP_BASE_URL: '{{.APP_BASE_URL | default ""}}'
|
||||
# Relocates configs/pipeline/logs, for a second backend in the same directory.
|
||||
# Empty is the same as unset: the reader blank-checks it.
|
||||
BASE_PATH: '{{.BASE_PATH | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
STIRLING_FLAVOR: saas
|
||||
STIRLING_BASE_PATH: '{{.BASE_PATH}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
# Appends RUN_SUBPATH: the approval page is at <base>/link, so a subpath build
|
||||
# serves it at <base>/app/link. An explicit value still wins.
|
||||
SYSTEM_FRONTENDURL:
|
||||
sh: |
|
||||
if [ -n "${SYSTEM_FRONTENDURL:-}" ]; then
|
||||
echo "${SYSTEM_FRONTENDURL}"
|
||||
elif [ -n "{{.APP_BASE_URL}}" ] && [ -n "${RUN_SUBPATH:-}" ]; then
|
||||
echo "{{.APP_BASE_URL}}/${RUN_SUBPATH}"
|
||||
else
|
||||
echo "{{.APP_BASE_URL}}"
|
||||
fi
|
||||
cmds:
|
||||
# PROFILE_ARGS is empty when PROFILES=none, i.e. the bare `saas` profile
|
||||
# against SAAS_DB_* (production).
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args=\"{{.PROFILE_ARGS}}\"{{end}}"
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args='{{.PROFILE_ARGS}}'{{end}}
|
||||
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}}
|
||||
platforms: [linux, darwin]
|
||||
|
||||
build:
|
||||
@@ -216,15 +116,6 @@ tasks:
|
||||
- cmd: ./gradlew test
|
||||
platforms: [linux, darwin]
|
||||
|
||||
test:force:
|
||||
desc: "Run backend tests, ignoring cached results"
|
||||
aliases: [test:no-cache]
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat cleanTest test --no-build-cache"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew cleanTest test --no-build-cache
|
||||
platforms: [linux, darwin]
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
cmds:
|
||||
@@ -248,7 +139,6 @@ tasks:
|
||||
|
||||
swagger:
|
||||
desc: "Generate OpenAPI docs"
|
||||
run: once
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc"
|
||||
platforms: [windows]
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Sync the Python environment with the cucumber test dependencies"
|
||||
run: once
|
||||
# Deliberately no sources/status fingerprint: the engine venv is shared, so it can
|
||||
# already exist while synced to a different dependency group. uv no-ops when correct.
|
||||
cmds:
|
||||
- uv sync --project ../../engine --locked --group cucumber
|
||||
|
||||
run:
|
||||
desc: "Run the cucumber suite against a running server (BASE_URL, default localhost:8080)"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project ../../engine --locked --group cucumber python -m behave --no-capture -f plain {{.CLI_ARGS}}
|
||||
|
||||
nightly:
|
||||
desc: "Run the @nightly cucumber scenarios, excluded from the default run"
|
||||
summary: |
|
||||
Heavy LibreOffice/Calibre/Ghostscript conversions. behave.ini excludes @nightly,
|
||||
so this opts back in explicitly.
|
||||
|
||||
Pass extra behave flags via -- :
|
||||
task cucumber:nightly -- --tags=@convert
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project ../../engine --locked --group cucumber python -m behave --tags=@nightly --no-capture -f plain {{.CLI_ARGS}}
|
||||
|
||||
parallel:
|
||||
desc: "Run the cucumber suite as concurrent shards against one server (SHARDS, default 10)"
|
||||
summary: |
|
||||
Splits the feature files across SHARDS concurrent behave processes hitting a single
|
||||
backend, to shake out cross-request interference. Auth-coupled features are pinned
|
||||
to one shard because they change the admin password mid-scenario.
|
||||
|
||||
task cucumber:parallel
|
||||
task cucumber:parallel SHARDS=4
|
||||
BASE_URL=http://localhost:8081 task cucumber:parallel
|
||||
deps: [install]
|
||||
vars:
|
||||
SHARDS: '{{.SHARDS | default "10"}}'
|
||||
cmds:
|
||||
- bash run-parallel.sh {{.SHARDS}} {{if .CLI_ARGS}}-- {{.CLI_ARGS}}{{end}}
|
||||
@@ -195,11 +195,7 @@ tasks:
|
||||
# `desktop:build` run `jlink:clean` first to force a fresh build.
|
||||
- cmd: chmod -R u+w runtime/jre
|
||||
platforms: [linux, darwin]
|
||||
# Single-quoted so Task's shell leaves `$_` and `$false` alone. Double
|
||||
# quotes let it expand them as its own variables, and since neither is
|
||||
# set the command PowerShell actually received was
|
||||
# `ForEach-Object { .IsReadOnly = }`, which fails on every file.
|
||||
- cmd: powershell -NoProfile -Command 'Get-ChildItem -Recurse -File runtime/jre | ForEach-Object { $_.IsReadOnly = $false }'
|
||||
- cmd: powershell -NoProfile -Command "Get-ChildItem -Recurse runtime/jre | ForEach-Object { $_.IsReadOnly = $false }"
|
||||
platforms: [windows]
|
||||
status:
|
||||
- test -f runtime/jre/release
|
||||
|
||||
@@ -15,15 +15,6 @@ tasks:
|
||||
cmds:
|
||||
- npx playwright test --project=stubbed {{.CLI_ARGS}}
|
||||
|
||||
stubbed-project:
|
||||
desc: "Run the stubbed E2E suite for a single Playwright project"
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:prepare' ]
|
||||
vars:
|
||||
PROJECT: '{{.PROJECT | default "stubbed"}}'
|
||||
cmds:
|
||||
- npx playwright test --project={{.PROJECT}} {{.CLI_ARGS}}
|
||||
|
||||
live:
|
||||
desc: "Run live E2E tests"
|
||||
summary: |
|
||||
|
||||
+12
-46
@@ -2,49 +2,22 @@ version: '3'
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install engine runtime and development dependencies"
|
||||
desc: "Install engine dependencies"
|
||||
run: once
|
||||
cmds:
|
||||
- uv python install 3.13.8
|
||||
- uv sync --locked --group engine --group engine-dev
|
||||
- uv sync
|
||||
sources:
|
||||
- uv.lock
|
||||
- pyproject.toml
|
||||
status:
|
||||
- test -d .venv
|
||||
|
||||
lock:
|
||||
desc: "Update the engine lockfile from project metadata"
|
||||
cmds:
|
||||
- uv lock
|
||||
|
||||
lock:upgrade:
|
||||
desc: "Upgrade allowed engine dependencies and update the lockfile"
|
||||
cmds:
|
||||
- uv lock --upgrade
|
||||
|
||||
lock:check:
|
||||
desc: "Check whether the engine lockfile is current"
|
||||
cmds:
|
||||
- uv lock --check
|
||||
|
||||
update:
|
||||
desc: "Upgrade engine dependencies and synchronize the environment"
|
||||
cmds:
|
||||
- task: lock:upgrade
|
||||
- uv sync --locked --group engine --group engine-dev
|
||||
|
||||
update:all:
|
||||
desc: "Upgrade all Python dependency groups and synchronize the environment"
|
||||
cmds:
|
||||
- task: lock:upgrade
|
||||
- uv sync --locked --all-groups
|
||||
|
||||
prepare:
|
||||
desc: "Set up engine .env from template"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev scripts/setup_env.py
|
||||
- uv run scripts/setup_env.py
|
||||
sources:
|
||||
- scripts/setup_env.py
|
||||
generates:
|
||||
@@ -60,7 +33,7 @@ tasks:
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
cmds:
|
||||
- uv run --locked --group engine uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}"
|
||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}"
|
||||
|
||||
dev:
|
||||
desc: "Start engine dev server with hot reload"
|
||||
@@ -72,43 +45,43 @@ tasks:
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
|
||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
|
||||
|
||||
lint:
|
||||
desc: "Run linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev ruff check .
|
||||
- uv run ruff check .
|
||||
|
||||
lint:fix:
|
||||
desc: "Auto-fix lint issues"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev ruff check . --fix
|
||||
- uv run ruff check . --fix
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev ruff format .
|
||||
- uv run ruff format .
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev ruff format . --diff
|
||||
- uv run ruff format . --diff
|
||||
|
||||
typecheck:
|
||||
desc: "Run type checking"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev pyright . --warnings
|
||||
- uv run pyright . --warnings
|
||||
|
||||
test:
|
||||
desc: "Run tests"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev pytest tests
|
||||
- uv run pytest tests
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix lint + format"
|
||||
@@ -129,19 +102,12 @@ tasks:
|
||||
desc: "Generate tool_models.py from Java OpenAPI spec (SwaggerDoc.json)"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py
|
||||
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py
|
||||
sources:
|
||||
- ../SwaggerDoc.json
|
||||
- scripts/generate_tool_models.py
|
||||
generates:
|
||||
- src/stirling/models/tool_models.py
|
||||
- src/stirling/models/tool_io.py
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if the committed tool models are out of date"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- uv run --locked --group engine --group engine-dev python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py --check
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
|
||||
+112
-222
@@ -5,14 +5,6 @@ version: '3'
|
||||
# mode flag) or use `--project editor/...` for tsc — so the editor lives
|
||||
# under frontend/editor/ without each task needing a cd.
|
||||
|
||||
vars:
|
||||
# Dev-only browser-tab label so concurrent worktrees are distinguishable. Only
|
||||
# the worktree folder basename (e.g. "wt1") is exposed — never the full path,
|
||||
# hostname, or user. Dropped from production builds.
|
||||
DEV_LABEL:
|
||||
sh: >-
|
||||
{{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}}
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install dependencies"
|
||||
@@ -23,7 +15,7 @@ tasks:
|
||||
- package-lock.json
|
||||
- package.json
|
||||
status:
|
||||
- npm ls --depth=0
|
||||
- test -d node_modules
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
|
||||
@@ -88,52 +80,9 @@ tasks:
|
||||
OPEN: '{{.OPEN | default ""}}'
|
||||
env:
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
STIRLING_DEV_LABEL: '{{.DEV_LABEL}}'
|
||||
cmds:
|
||||
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
# Separate from dev:_run rather than a flag on it: Task sets an `env:` key even
|
||||
# when its value resolves to empty, and Vite treats an empty process.env VITE_* as
|
||||
# authoritative over the committed editor/.env, so folding these in blanks Supabase
|
||||
# config for the core, proprietary and desktop dev servers.
|
||||
dev:_run:saas:
|
||||
internal: true
|
||||
ignore_error: true
|
||||
# The backend's own env files, so both halves target one project. Paths are
|
||||
# relative to this taskfile's dir, `frontend`.
|
||||
dotenv: ['../app/.env.saas.local', '../app/.env.saas']
|
||||
vars:
|
||||
PORT: '{{.PORT | default "5173"}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
||||
OPEN: '{{.OPEN | default ""}}'
|
||||
SAAS_ENV: '{{.SAAS_ENV | default "dev"}}'
|
||||
env:
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
STIRLING_DEV_LABEL: '{{.DEV_LABEL}}'
|
||||
SAAS_ENV: '{{.SAAS_ENV}}'
|
||||
# A real process.env VITE_* beats a committed .env in Vite (loadEnv applies
|
||||
# process.env last), which is what lets this override editor/.env.
|
||||
#
|
||||
# These must stay `sh:`, not Go templates: dotenv values are visible to Task's
|
||||
# embedded shell but not to templates, where {{.SAAS_DEV_PROJECT_REF}} is
|
||||
# always empty.
|
||||
VITE_SUPABASE_URL:
|
||||
sh: |
|
||||
case "${SAAS_ENV:-dev}" in
|
||||
staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;;
|
||||
*) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;;
|
||||
esac
|
||||
echo "https://${ref}.supabase.co"
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY:
|
||||
sh: |
|
||||
case "${SAAS_ENV:-dev}" in
|
||||
staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;;
|
||||
*) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;;
|
||||
esac
|
||||
cmds:
|
||||
- 'echo ">> frontend {{.SAAS_ENV}}: Supabase $VITE_SUPABASE_URL, backend $BACKEND_URL"'
|
||||
- npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
dev:
|
||||
desc: "Start frontend dev server"
|
||||
cmds:
|
||||
@@ -155,33 +104,13 @@ tasks:
|
||||
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:saas:
|
||||
desc: "Start frontend dev server in SaaS mode (SAAS_ENV=dev|staging|prod)"
|
||||
desc: "Start frontend dev server in SaaS mode"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
vars:
|
||||
SAAS_ENV: '{{.SAAS_ENV | default "dev"}}'
|
||||
# prod routes to the plain runner, which sets no VITE_SUPABASE_* and so leaves
|
||||
# the committed editor/.env alone.
|
||||
RUNNER: '{{if eq .SAAS_ENV "prod"}}dev:_run{{else}}dev:_run:saas{{end}}'
|
||||
cmds:
|
||||
- task: '{{.RUNNER}}'
|
||||
vars:
|
||||
MODE: saas
|
||||
PORT: '{{.PORT}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
OPEN: '{{.OPEN}}'
|
||||
SAAS_ENV: '{{.SAAS_ENV}}'
|
||||
|
||||
staging:saas:
|
||||
desc: "Start frontend dev server against the shared v3 staging project"
|
||||
cmds:
|
||||
- task: dev:saas
|
||||
vars:
|
||||
SAAS_ENV: staging
|
||||
PORT: '{{.PORT}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
OPEN: '{{.OPEN}}'
|
||||
- task: dev:_run
|
||||
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:desktop:
|
||||
desc: "Start frontend dev server in desktop mode"
|
||||
@@ -199,6 +128,37 @@ tasks:
|
||||
- task: dev:_run
|
||||
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:portal:
|
||||
desc: "Start developer portal dev server"
|
||||
ignore_error: true
|
||||
deps: [install]
|
||||
vars:
|
||||
PORT: '{{.PORT | default "5173"}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
||||
EDITOR_URL: '{{.EDITOR_URL | default ""}}'
|
||||
OPEN: '{{.OPEN | default ""}}'
|
||||
SUBPATH: '{{.SUBPATH | default ""}}'
|
||||
MOCKS: '{{.MOCKS | default ""}}'
|
||||
env:
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
cmds:
|
||||
- '{{if .SUBPATH}}RUN_SUBPATH={{.SUBPATH}} {{end}}{{if .MOCKS}}VITE_PORTAL_MOCKS={{.MOCKS}} {{end}}{{if .EDITOR_URL}}VITE_EDITOR_URL={{.EDITOR_URL}} {{end}}npx vite portal --port {{.PORT}}{{if .OPEN}} --open{{end}}'
|
||||
|
||||
dev:portal:proxy:serve:
|
||||
internal: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "3000"}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
||||
EDITOR_DEV_URL: '{{.EDITOR_DEV_URL | default ""}}'
|
||||
PORTAL_DEV_URL: '{{.PORTAL_DEV_URL | default ""}}'
|
||||
env:
|
||||
PORT: '{{.PORT}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
EDITOR_DEV_URL: '{{.EDITOR_DEV_URL}}'
|
||||
PORTAL_DEV_URL: '{{.PORTAL_DEV_URL}}'
|
||||
cmds:
|
||||
- npx tsx scripts/dev-origin-proxy.ts
|
||||
|
||||
# ============================================================
|
||||
# Build
|
||||
# ============================================================
|
||||
@@ -245,92 +205,41 @@ tasks:
|
||||
cmds:
|
||||
- npx vite build editor --mode prototypes
|
||||
|
||||
build:portal:
|
||||
desc: "Build developer portal"
|
||||
deps: [install]
|
||||
vars:
|
||||
SUBPATH: '{{.SUBPATH | default ""}}'
|
||||
cmds:
|
||||
- '{{if .SUBPATH}}RUN_SUBPATH={{.SUBPATH}} {{end}}npx vite build portal'
|
||||
|
||||
preview:portal:proxy:
|
||||
desc: "Build + serve editor + portal behind one origin (prod-like auth testing)"
|
||||
deps: [prepare]
|
||||
vars:
|
||||
PORT: '{{.PORT | default "3000"}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
||||
env:
|
||||
PORT: '{{.PORT}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
cmds:
|
||||
- task: build:proprietary
|
||||
vars: { PREVIEW: '1' }
|
||||
- task: build:portal
|
||||
vars: { SUBPATH: portal }
|
||||
- npx tsx scripts/dev-origin-proxy.ts
|
||||
|
||||
storybook:
|
||||
desc: "Start Storybook dev server"
|
||||
deps: [prepare]
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx storybook dev -p 6006 {{.CLI_ARGS}}
|
||||
|
||||
storybook:build:
|
||||
desc: "Build static Storybook"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx storybook build {{.CLI_ARGS}}
|
||||
|
||||
storybook:browser:
|
||||
internal: true
|
||||
desc: "Install the Chromium build the story scan runs in"
|
||||
run: once
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx playwright install chromium
|
||||
|
||||
storybook:test:
|
||||
desc: "Scan every story in real Chromium: it must render and pass axe"
|
||||
deps: [prepare, storybook:browser]
|
||||
cmds:
|
||||
# Runs each story as a browser test. Pass a filter through, e.g.
|
||||
# task frontend:storybook:test -- Button
|
||||
- npx vitest run --config .storybook/vitest.config.ts {{.CLI_ARGS}}
|
||||
|
||||
storybook:a11y:light:
|
||||
desc: "a11y gate over every story in light mode"
|
||||
deps: [prepare, storybook:browser]
|
||||
cmds:
|
||||
- node .storybook/a11y-scan.mjs {{.CLI_ARGS}}
|
||||
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt
|
||||
|
||||
storybook:a11y:dark:
|
||||
desc: "a11y gate over every story in dark mode"
|
||||
deps: [prepare, storybook:browser]
|
||||
cmds:
|
||||
- SCAN_THEME=dark node .storybook/a11y-scan.mjs {{.CLI_ARGS}}
|
||||
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --baseline .storybook/a11y-baseline.dark.json
|
||||
|
||||
storybook:a11y:
|
||||
desc: "a11y gate over every story, light and dark"
|
||||
cmds:
|
||||
- task: storybook:a11y:light
|
||||
- task: storybook:a11y:dark
|
||||
|
||||
storybook:a11y:changed:
|
||||
desc: "a11y gate over the stories this branch affects (default base origin/main)"
|
||||
summary: |
|
||||
Scans the stories a branch affects, which is what pull requests run — a
|
||||
full scan takes ~30 minutes, far too long to sit in front of every merge.
|
||||
A story is affected if its file changed, or if a same-named sibling
|
||||
source file changed (editing Button.tsx or Button.css re-scans
|
||||
Button.stories.tsx — the story renders the live component, so a component
|
||||
edit changes what the story shows without touching the story file).
|
||||
Changes that ripple further than a component's own stories are covered by
|
||||
the nightly full sweep.
|
||||
|
||||
Pass a base ref through CLI_ARGS, e.g.
|
||||
task frontend:storybook:a11y:changed -- origin/release
|
||||
vars:
|
||||
BASE: '{{.CLI_ARGS | default "origin/main"}}'
|
||||
CHANGED:
|
||||
sh: node .storybook/a11y-changed.mjs {{.CLI_ARGS | default "origin/main"}}
|
||||
cmds:
|
||||
- cmd: |
|
||||
if [ -z '{{.CHANGED}}' ]; then
|
||||
echo "a11y: no story files affected vs {{.BASE}} — nothing to check"
|
||||
exit 0
|
||||
fi
|
||||
rc=0
|
||||
task frontend:storybook:a11y:light -- {{.CHANGED}} || rc=1
|
||||
task frontend:storybook:a11y:dark -- {{.CHANGED}} || rc=1
|
||||
exit $rc
|
||||
|
||||
storybook:a11y:record:
|
||||
desc: "Re-record both a11y baselines (run after intentionally fixing/adding violations)"
|
||||
deps: [prepare, storybook:browser]
|
||||
cmds:
|
||||
- node .storybook/a11y-scan.mjs
|
||||
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record
|
||||
- SCAN_THEME=dark node .storybook/a11y-scan.mjs
|
||||
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record --baseline .storybook/a11y-baseline.dark.json
|
||||
- npx storybook build {{.CLI_ARGS}}
|
||||
|
||||
# ============================================================
|
||||
# Code quality
|
||||
@@ -340,58 +249,40 @@ tasks:
|
||||
desc: "Run linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- task: lint:oxlint
|
||||
- task: lint:colors
|
||||
- task: lint:css
|
||||
- task: lint:eslint
|
||||
- task: lint:dpdm
|
||||
|
||||
lint:css:
|
||||
desc: "Lint stylesheets for duplicate selectors"
|
||||
lint:eslint:
|
||||
desc: "Run ESLint linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
# Covers the whole editor tree, including the portal/processor layer and
|
||||
# public/css. Vendored CSS and build output are excluded via ignoreFiles
|
||||
# in stylelint.config.mjs.
|
||||
- npx stylelint "editor/**/*.css"
|
||||
- npx eslint --max-warnings=0
|
||||
|
||||
lint:colors:
|
||||
desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components"
|
||||
aliases: [lint:colours]
|
||||
lint:dpdm:
|
||||
desc: "Run circular import linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/lint/theme-lint.mjs
|
||||
- node editor/scripts/lint/theme-lint.mjs css-colors
|
||||
- node editor/scripts/lint/theme-lint.mjs code-colors
|
||||
- node editor/scripts/lint/theme-lint.mjs no-primitives
|
||||
|
||||
contrast:
|
||||
desc: "Report low-contrast theme token pairs (warning only, never blocks)"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/lint/theme-lint.mjs contrast
|
||||
|
||||
lint:oxlint:
|
||||
desc: "Run oxlint linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx oxlint --config oxlint.config.ts --max-warnings=0
|
||||
# Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is
|
||||
# shell-agnostic. Covers editor, portal, and the shared design system.
|
||||
- npx dpdm "editor/src/**/*.{ts,tsx}" "portal/src/**/*.{ts,tsx}" "shared/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1
|
||||
|
||||
lint:fix:
|
||||
desc: "Auto-fix lint issues"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx oxlint --config oxlint.config.ts --fix
|
||||
- npx eslint --fix
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx oxfmt --write .
|
||||
- npx prettier --write .
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx oxfmt --check .
|
||||
- npx prettier --check .
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix lint and format"
|
||||
@@ -406,8 +297,10 @@ tasks:
|
||||
|
||||
typecheck:_run:
|
||||
internal: true
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
cmds:
|
||||
- 'npx tsc --noEmit --project {{.PROJECT}}'
|
||||
- '{{ if eq .CI "true" }}npx tsc{{ else }}npx tsgo{{ end }} --noEmit --project {{.PROJECT}}'
|
||||
|
||||
typecheck:core:
|
||||
desc: "Typecheck core build variant"
|
||||
@@ -452,6 +345,8 @@ tasks:
|
||||
desc: "Typecheck scripts"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: scripts/tsconfig.json }
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/scripts/tsconfig.json }
|
||||
|
||||
@@ -467,14 +362,14 @@ tasks:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/portal/tsconfig.json }
|
||||
vars: { PROJECT: portal/tsconfig.json }
|
||||
|
||||
typecheck:storybook:
|
||||
desc: "Typecheck Storybook config and stories"
|
||||
deps: [prepare]
|
||||
typecheck:shared:
|
||||
desc: "Typecheck the shared design system"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: .storybook/tsconfig.json }
|
||||
vars: { PROJECT: shared/tsconfig.json }
|
||||
|
||||
typecheck:all:
|
||||
desc: "Typecheck all build variants"
|
||||
@@ -487,7 +382,7 @@ tasks:
|
||||
- task: typecheck:scripts
|
||||
- task: typecheck:prototypes
|
||||
- task: typecheck:portal
|
||||
- task: typecheck:storybook
|
||||
- task: typecheck:shared
|
||||
|
||||
# ============================================================
|
||||
# Quality Gate
|
||||
@@ -516,6 +411,7 @@ tasks:
|
||||
- task: lint
|
||||
- task: format:check
|
||||
- task: build
|
||||
- task: build:portal
|
||||
- task: test
|
||||
- task: storybook:build
|
||||
|
||||
@@ -527,21 +423,19 @@ tasks:
|
||||
desc: "Run tests"
|
||||
cmds:
|
||||
- task: test:editor
|
||||
- task: test:portal
|
||||
|
||||
test:editor:
|
||||
desc: "Run editor tests"
|
||||
deps: [prepare]
|
||||
vars:
|
||||
COVERAGE: '{{.COVERAGE | default .CI | default "false"}}'
|
||||
cmds:
|
||||
- >
|
||||
npx vitest run --root editor
|
||||
{{if eq .COVERAGE "true"}}--coverage
|
||||
--coverage.provider=v8
|
||||
--coverage.reporter=text-summary
|
||||
--coverage.reporter=json-summary
|
||||
--coverage.reporter=html
|
||||
--coverage.reportsDirectory=./coverage{{end}}
|
||||
- npx vitest run --root editor
|
||||
|
||||
test:portal:
|
||||
desc: "Run portal tests"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vitest run --root portal
|
||||
|
||||
test:watch:
|
||||
desc: "Run tests in watch mode"
|
||||
@@ -551,33 +445,29 @@ tasks:
|
||||
|
||||
test:coverage:
|
||||
desc: "Run tests with coverage (one-shot; CI-friendly)."
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: test:editor
|
||||
vars: { COVERAGE: "true" }
|
||||
# `vitest run` makes this CI-safe (the bare `vitest` form enters watch
|
||||
# mode). Explicit reporter list because v8 + json-summary is what the
|
||||
# coverage-summary.py helper consumes; html/text are kept for humans.
|
||||
#
|
||||
# reportsDirectory is pinned to ./coverage relative to vitest's root
|
||||
# (--root editor), so output lands at frontend/editor/coverage/. The
|
||||
# CI upload step reads from that path. An earlier attempt with
|
||||
# `./editor/coverage` double-nested into frontend/editor/editor/coverage;
|
||||
# pinning future-proofs against vitest changing the default.
|
||||
- >
|
||||
npx vitest run --root editor --coverage
|
||||
--coverage.provider=v8
|
||||
--coverage.reporter=text-summary
|
||||
--coverage.reporter=json-summary
|
||||
--coverage.reporter=html
|
||||
--coverage.reportsDirectory=./coverage
|
||||
|
||||
# ============================================================
|
||||
# Code Generation
|
||||
# ============================================================
|
||||
|
||||
tool-models:
|
||||
desc: "Generate tool API types from the Java OpenAPI spec"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts
|
||||
- task: format
|
||||
sources:
|
||||
- editor/scripts/generate-tool-api-types.mts
|
||||
- ../SwaggerDoc.json
|
||||
generates:
|
||||
- editor/src/core/types/toolApiTypes.ts
|
||||
- editor/src/core/types/toolIO.ts
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if committed tool API types are out of date"
|
||||
cmds:
|
||||
- task: tool-models
|
||||
- git diff --exit-code -- editor/src/core/types/toolApiTypes.ts editor/src/core/types/toolIO.ts
|
||||
|
||||
licenses:generate:
|
||||
desc: "Generate frontend license report"
|
||||
deps: [install]
|
||||
@@ -591,7 +481,7 @@ tasks:
|
||||
clean:
|
||||
desc: "Clean build artifacts and caches"
|
||||
cmds:
|
||||
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist
|
||||
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist, dist-portal
|
||||
platforms: [windows]
|
||||
- cmd: rm -rf node_modules/.vite editor/dist dist
|
||||
- cmd: rm -rf node_modules/.vite editor/dist dist dist-portal
|
||||
platforms: [linux, darwin]
|
||||
|
||||
+14
-100
@@ -11,7 +11,6 @@ vars:
|
||||
'.github/scripts/*.py'
|
||||
'app/core/src/main/resources/static/python/*.py'
|
||||
':(exclude)*split_photos.py'
|
||||
':(exclude)scripts/lint/fixtures/*'
|
||||
SPELL_FILES: >-
|
||||
'*.html'
|
||||
'*.css'
|
||||
@@ -46,10 +45,6 @@ vars:
|
||||
# which owns the version and caches the binary here.
|
||||
GITLEAKS_BIN: '.task/bin/gitleaks{{if eq OS "windows"}}.exe{{end}}'
|
||||
|
||||
env:
|
||||
# Keep repository-wide checks isolated from the engine runtime environment.
|
||||
UV_PROJECT_ENVIRONMENT: '.venv-pre-commit'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: "Check formatting, spelling, and secrets across the repo"
|
||||
@@ -60,7 +55,6 @@ tasks:
|
||||
- task: gitleaks
|
||||
- task: whitespace
|
||||
- task: toml-sort
|
||||
- task: comment-lint
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix formatting, spelling, and secrets issues across the repo"
|
||||
@@ -77,25 +71,24 @@ tasks:
|
||||
vars: { FIX: '1' }
|
||||
- task: codespell
|
||||
- task: gitleaks
|
||||
- task: comment-lint
|
||||
|
||||
install:
|
||||
desc: "Install the pinned pre-commit Python tools"
|
||||
desc: "Install the pinned pre-commit Python tools (ruff, codespell, toml-sort)"
|
||||
run: once
|
||||
cmds:
|
||||
- uv sync --project engine --locked --group pre-commit
|
||||
- uv sync --project scripts/pre-commit --locked
|
||||
sources:
|
||||
- engine/uv.lock
|
||||
- engine/pyproject.toml
|
||||
- scripts/pre-commit/uv.lock
|
||||
- scripts/pre-commit/pyproject.toml
|
||||
status:
|
||||
- test -d engine/.venv-pre-commit
|
||||
- test -d scripts/pre-commit/.venv
|
||||
|
||||
clean:
|
||||
desc: "Remove the cached gitleaks binary and the pre-commit virtualenv"
|
||||
desc: "Remove the cached gitleaks binary and the tool virtualenv"
|
||||
cmds:
|
||||
- cmd: rm -rf engine/.venv-pre-commit .task/bin/gitleaks
|
||||
- cmd: rm -rf scripts/pre-commit/.venv .task/bin/gitleaks
|
||||
platforms: [linux, darwin]
|
||||
- cmd: cmd /c "rmdir /s /q engine\.venv-pre-commit & del /q .task\bin\gitleaks.exe"
|
||||
- cmd: cmd /c "rmdir /s /q scripts\pre-commit\.venv & del /q .task\bin\gitleaks.exe"
|
||||
platforms: [windows]
|
||||
ignore_error: true
|
||||
|
||||
@@ -104,26 +97,26 @@ tasks:
|
||||
ruff:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit ruff check --isolated --line-length=120 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}})
|
||||
- uv run --project scripts/pre-commit --no-sync ruff check --line-length=127 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}})
|
||||
|
||||
ruff-format:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit ruff format --isolated --line-length=120 {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}})
|
||||
- uv run --project scripts/pre-commit --no-sync ruff format {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}})
|
||||
|
||||
codespell:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
- uv run --project scripts/pre-commit --no-sync codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
|
||||
toml-sort:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit python scripts/pre-commit/sort_locale_toml.py {{if .FIX}}--fix {{end}}{{.LOCALE_TOML}}
|
||||
- uv run --project scripts/pre-commit --no-sync toml-sort --all --ignore-case {{if .FIX}}--in-place{{else}}--check{{end}} {{.LOCALE_TOML}}
|
||||
|
||||
whitespace:
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}}
|
||||
- uv run --no-project python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}}
|
||||
|
||||
gitleaks:
|
||||
deps: [gitleaks-bin]
|
||||
@@ -133,87 +126,8 @@ tasks:
|
||||
cmds:
|
||||
- "{{.GITLEAKS_BIN}} git --pre-commit --redact --staged --verbose"
|
||||
|
||||
comment-lint:
|
||||
desc: "Check comment quality on the lines this branch adds"
|
||||
summary: |
|
||||
Blocks a comment that restates the code below it, a section banner, or a
|
||||
block of commented-out code. Everything else it reports is advisory.
|
||||
|
||||
Scoped to added lines, so touching an old file never surfaces the standing
|
||||
backlog. The standard is devGuide/CODE_COMMENTS.md.
|
||||
|
||||
With no arguments it diffs the working tree against HEAD, which is what a
|
||||
pre-commit run wants: the lines you are about to commit. On a CI pull request
|
||||
it diffs against the target branch instead, via GITHUB_BASE_REF.
|
||||
|
||||
To ask what a whole branch adds instead, use the branch variant, which
|
||||
needs no argument passing:
|
||||
task comment-lint:branch
|
||||
|
||||
Full tree (report only): task pre-commit:comment-lint:all
|
||||
Fixture corpus: task pre-commit:comment-lint:selftest
|
||||
# Depends on the frontend install because the .ts/.tsx half of the rule set
|
||||
# runs as an oxlint plugin. Without it the TS engine warns and skips, which
|
||||
# would leave the frontend silently unchecked on CI.
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs {{.CLI_ARGS}}
|
||||
|
||||
comment-lint:branch:
|
||||
desc: "Check comment quality on everything this branch adds over its base"
|
||||
summary: |
|
||||
Like `task comment-lint`, but scoped to the whole branch rather than to
|
||||
uncommitted work, so it still reports after you commit.
|
||||
|
||||
Exists as its own task because passing `-- --since origin/main` through Task
|
||||
is not portable: with the npm build of Task the launcher is a PowerShell
|
||||
script, and PowerShell strips the `--` before Task sees it, leaving Task to
|
||||
print its own usage.
|
||||
|
||||
Override the base with BASE=<ref>.
|
||||
vars:
|
||||
BASE: '{{.BASE | default "origin/main"}}'
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --since {{.BASE}}
|
||||
|
||||
comment-lint:ci:
|
||||
desc: "Comment gate as CI runs it: fixture corpus, then the diff"
|
||||
summary: |
|
||||
The corpus checks the rules themselves rather than the code under review, so
|
||||
it belongs on CI and not on every local commit. Run this before changing a
|
||||
rule, and let CI run it on every pull request.
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --selftest
|
||||
- node scripts/lint/comment-lint.mjs {{.CLI_ARGS}}
|
||||
|
||||
comment-lint:hook:
|
||||
desc: "Comment gate for the editor hook: everything this turn changed"
|
||||
summary: |
|
||||
Same scope as `task comment-lint`, kept as its own name so the hook has a
|
||||
stable entry point and the taskfile shows every way the linter is invoked.
|
||||
|
||||
Not in the frontend-install dependency chain on purpose: this runs at the end
|
||||
of every turn, so it stays as short as it can be. If oxlint is missing the TS
|
||||
half warns and skips.
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs
|
||||
|
||||
comment-lint:all:
|
||||
desc: "Report every comment finding in the tree (never fails)"
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --all
|
||||
|
||||
comment-lint:selftest:
|
||||
desc: "Check both comment-lint engines against the fixture corpus"
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --selftest
|
||||
|
||||
gitleaks-bin:
|
||||
internal: true
|
||||
desc: "Ensure the pinned, checksum-verified gitleaks binary is cached in .task/bin"
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit python scripts/pre-commit/install_gitleaks.py
|
||||
- uv run --no-project python scripts/pre-commit/install_gitleaks.py
|
||||
|
||||
Vendored
+3
-2
@@ -2,6 +2,8 @@
|
||||
"recommendations": [
|
||||
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
|
||||
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
|
||||
"ms-python.black-formatter", // Python code formatter using Black
|
||||
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
|
||||
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
|
||||
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||
@@ -17,7 +19,6 @@
|
||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
||||
"tamasfe.even-better-toml", // TOML language support and formatting
|
||||
"oxc.oxc-vscode", // Oxc (oxlint) extension for JavaScript/TypeScript linting
|
||||
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+24
-81
@@ -20,9 +20,8 @@
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"ruff.configuration": "${workspaceFolder}/engine/pyproject.toml",
|
||||
"[gradle-kotlin-dsl]": {
|
||||
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||
},
|
||||
@@ -42,7 +41,7 @@
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"java.format.enabled": true,
|
||||
"java.format.settings.profile": "GoogleStyle",
|
||||
"java.format.settings.google.version": "1.35.0",
|
||||
"java.format.settings.google.version": "1.28.0",
|
||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||
// (DE) Aktiviert Kommentare im Java-Format.
|
||||
// (EN) Enables comments in Java formatting.
|
||||
@@ -73,52 +72,40 @@
|
||||
"stirling",
|
||||
],
|
||||
"java.project.resourceFilters": [
|
||||
".cache/",
|
||||
".claude/",
|
||||
".devcontainer/",
|
||||
".git/",
|
||||
".git-blame-ignore-revs",
|
||||
".gitattributes",
|
||||
".github/",
|
||||
".gitignore",
|
||||
".gradle/",
|
||||
".pre-commit-config.yaml",
|
||||
".task/",
|
||||
".taskfiles/",
|
||||
".venv/",
|
||||
".venv*/",
|
||||
".vscode/",
|
||||
"app/.gitignore",
|
||||
"app/build/",
|
||||
"app/common/.gitignore",
|
||||
"app/common/bin/",
|
||||
"app/common/build/",
|
||||
"app/core/.gitignore",
|
||||
"app/core/bin/",
|
||||
"app/core/configs/",
|
||||
"app/core/customFiles/",
|
||||
"app/core/LOCAL_APPDATA_FONTCONFIG_CACHE/",
|
||||
"app/core/logs/",
|
||||
"app/core/pipeline/",
|
||||
"app/core/storage/",
|
||||
"app/proprietary/.gitignore",
|
||||
"app/proprietary/bin/",
|
||||
"app/proprietary/storage/",
|
||||
"app/saas/.gitignore",
|
||||
"app/saas/bin/",
|
||||
"app/saas/build/",
|
||||
"bin/",
|
||||
"app/core/bin/",
|
||||
"app/common/bin/",
|
||||
"app/proprietary/bin/",
|
||||
"build/",
|
||||
"devGuide/",
|
||||
"devTools/",
|
||||
"docker/",
|
||||
"app/core/build/",
|
||||
"app/common/build/",
|
||||
"app/proprietary/build/",
|
||||
"configs/",
|
||||
"app/core/configs/",
|
||||
"customFiles/",
|
||||
"app/core/customFiles/",
|
||||
"docs/",
|
||||
"engine/",
|
||||
"frontend/",
|
||||
"exampleYmlFiles",
|
||||
"gradle/",
|
||||
"images/",
|
||||
"logs/",
|
||||
"pipeline/",
|
||||
"scripts/",
|
||||
"testings/",
|
||||
".git-blame-ignore-revs",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
"app/core/.gitignore",
|
||||
"app/common/.gitignore",
|
||||
"app/proprietary/.gitignore",
|
||||
".pre-commit-config.yaml",
|
||||
],
|
||||
// Enables signature help in Java.
|
||||
"java.signatureHelp.enabled": true,
|
||||
@@ -147,57 +134,13 @@
|
||||
"html.format.indentHandlebars": true,
|
||||
"html.format.preserveNewLines": true,
|
||||
"html.format.maxPreserveNewLines": 2,
|
||||
"stylelint.configFile": "${workspaceFolder}/devTools/.stylelintrc.json",
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"scss.lint.unknownAtRules": "ignore",
|
||||
"less.lint.unknownAtRules": "ignore",
|
||||
"stylelint.configFile": "devTools/.stylelintrc.json",
|
||||
"java.project.sourcePaths": [
|
||||
"app/core/src/main/java",
|
||||
"app/common/src/main/java",
|
||||
"app/proprietary/src/main/java"
|
||||
],
|
||||
"[javascript]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.oxc": "explicit"
|
||||
}
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.oxc": "explicit"
|
||||
}
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.oxc": "explicit"
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.oxc": "explicit"
|
||||
}
|
||||
},
|
||||
"oxc.enable.oxlint": true,
|
||||
"oxc.enable.oxfmt": false,
|
||||
"oxc.configPath": "frontend/oxlint.config.ts",
|
||||
"oxc.requireConfig": true,
|
||||
"oxc.lint.run": "onType",
|
||||
"oxc.fixKind": "safe_fix",
|
||||
"[toml]": {
|
||||
"editor.defaultFormatter": "tamasfe.even-better-toml",
|
||||
// Keep TOML formatting compatible with .editorconfig and the pre-commit
|
||||
// locale sorter. Key ordering itself is handled by task pre-commit:toml-sort.
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 4,
|
||||
"editor.rulers": [127],
|
||||
"evenBetterToml.formatter.alignEntries": false,
|
||||
"evenBetterToml.formatter.alignComments": false,
|
||||
"evenBetterToml.formatter.indentString": " ",
|
||||
"evenBetterToml.formatter.columnWidth": 127,
|
||||
"evenBetterToml.formatter.reorderKeys": false,
|
||||
"evenBetterToml.formatter.reorderArrays": false,
|
||||
"evenBetterToml.formatter.reorderInlineTables": false,
|
||||
"evenBetterToml.formatter.trailingNewline": true,
|
||||
"evenBetterToml.formatter.crlf": false
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,43 +21,6 @@ Task `desc:` fields should describe **what** the task does, not **how** it does
|
||||
- `task docker:build` — build standard Docker image
|
||||
- `task docker:up` — start Docker compose stack
|
||||
|
||||
## Comments
|
||||
|
||||
A comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.
|
||||
|
||||
Comment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so "this used to reimplement the modal internals" becomes "thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart". Future state goes in a TODO with an issue.
|
||||
|
||||
Write a comment when it does one of these four jobs:
|
||||
|
||||
- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.
|
||||
- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.
|
||||
- **Hazard.** "Must stay in sync with X", "order matters because Y", "do not remove, it prevents Z".
|
||||
- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.
|
||||
|
||||
Never write:
|
||||
|
||||
- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.
|
||||
- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.
|
||||
- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.
|
||||
- Commented-out code. Delete it.
|
||||
- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.
|
||||
- Docs on self-explanatory members with no constraint to state.
|
||||
|
||||
Two tests before keeping a comment:
|
||||
|
||||
- **Delete it.** Is any information lost? If not, it stays deleted.
|
||||
- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.
|
||||
|
||||
A comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // "%PDF"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.
|
||||
|
||||
A reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.
|
||||
|
||||
A TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.
|
||||
|
||||
A comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.
|
||||
|
||||
`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @devGuide/CODE_COMMENTS.md
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
@@ -107,7 +70,7 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
- Avoid nested functions and nested classes unless the language construct requires them.
|
||||
- Prefer composition to inheritance when combining concepts.
|
||||
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
|
||||
- Comments follow the repo-wide rules in the "Comments" section above.
|
||||
- Add comments sparingly and only when they explain non-obvious intent.
|
||||
|
||||
#### Python Typing and Models
|
||||
- Deserialize into Pydantic models as early as possible.
|
||||
@@ -176,8 +139,7 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
|
||||
#### Environment Variables
|
||||
- All `VITE_*` variables must be declared in the appropriate committed env file:
|
||||
- `frontend/editor/.env` — core and shared vars (base, loaded in every mode)
|
||||
- `frontend/editor/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)
|
||||
- `frontend/editor/.env` — core, proprietary, and shared vars
|
||||
- `frontend/editor/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)
|
||||
- `frontend/editor/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)
|
||||
- These files are committed to Git and must not contain private keys
|
||||
@@ -192,8 +154,6 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
|
||||
For a broader explanation of the frontend layering and override architecture, read @frontend/editor/DeveloperGuide.md
|
||||
|
||||
Before touching colours or theming (tokens, dark mode, accent colours), read @frontend/editor/src/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use @app/* for all imports
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
@@ -229,7 +189,7 @@ What goes where:
|
||||
- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.
|
||||
- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.
|
||||
|
||||
`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
|
||||
`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (enforced by ESLint). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
|
||||
|
||||
Rule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).
|
||||
|
||||
@@ -492,7 +452,6 @@ The frontend is organized with a clear separation of concerns:
|
||||
|
||||
- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately
|
||||
- Translation files are located in `frontend/editor/public/locales/`
|
||||
- After changing any translation file, run `task pre-commit:fix`
|
||||
|
||||
## Important Notes
|
||||
|
||||
|
||||
+5
-9
@@ -2,13 +2,6 @@
|
||||
|
||||
Thank you for your interest in contributing to Stirling-PDF! There are many ways to contribute other than writing code. For example, reporting bugs, creating suggestions, and adding or modifying translations.
|
||||
|
||||
## License
|
||||
|
||||
By contributing to this project, you agree that your contributions will be licensed under the project [license](LICENSE), which follows an open-core model.
|
||||
The codebase is a mix of MIT and source-available code, so your contribution is licensed according to the directory it is committed to.
|
||||
|
||||
PRs are welcome in any directory by any user, just be aware of which license applies to the code you change.
|
||||
|
||||
## Issue Guidelines
|
||||
|
||||
Issues can be used to report bugs, request features, or ask questions. If you have a question, you could also ask us in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||
@@ -28,7 +21,7 @@ This project uses [Task](https://taskfile.dev/) as a unified command runner. Aft
|
||||
|
||||
1. Install the `task` CLI: https://taskfile.dev/installation/
|
||||
2. Run `task install` to install all dependencies
|
||||
3. Run `task dev` to start backend + frontend or `task desktop:dev` to start the desktop application
|
||||
3. Run `task dev` to start backend + frontend
|
||||
4. Run `task check` before submitting a PR
|
||||
|
||||
Run `task --list` to see all available commands.
|
||||
@@ -42,7 +35,6 @@ Please make sure your Pull Request adheres to the following guidelines:
|
||||
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
||||
- Commits should be clear, concise, and easy to understand.
|
||||
- References to the Issue number in the Pull Request and/or Commit message.
|
||||
- Every comment in the diff should say something the code does not. See [Code comments](devGuide/CODE_COMMENTS.md); `task comment-lint` checks the mechanical part.
|
||||
|
||||
## Translations
|
||||
|
||||
@@ -71,3 +63,7 @@ For technical guides, setup instructions, and development resources:
|
||||
For configuration and usage guides, see:
|
||||
- [Database Guide](DATABASE.md) - Database setup and configuration
|
||||
- [OCR Guide](HowToUseOCR.md) - OCR setup and configuration
|
||||
|
||||
## License
|
||||
|
||||
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
||||
|
||||
+7
-40
@@ -46,8 +46,8 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- Docker
|
||||
- Git
|
||||
- Java JDK 25
|
||||
- Node.js 22+ and npm (required for frontend development)
|
||||
- Gradle 9.0 or later (Included within the repo)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
- [uv](https://docs.astral.sh/uv/) — Python package manager (required for engine development)
|
||||
- Rust and Cargo (required for Tauri desktop app development)
|
||||
- Tauri CLI (install with `cargo install tauri-cli`)
|
||||
@@ -92,7 +92,7 @@ Visit the [Lombok website](https://projectlombok.org/setup/) for installation in
|
||||
|
||||
5. Add environment variable
|
||||
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||
6. **Frontend Setup (Required for Stirling 2.0)**
|
||||
5. **Frontend Setup (Required for Stirling 2.0)**
|
||||
Navigate to the frontend directory and install dependencies using npm.
|
||||
|
||||
### Verify Setup
|
||||
@@ -158,7 +158,7 @@ Stirling-PDF/
|
||||
│ │ │ └── locales/ # Internationalization files (JSON)
|
||||
│ │ └── vite.config.ts # Vite configuration
|
||||
│ ├── package.json # Shared workspace dependencies
|
||||
│ └── oxlint.config.ts # Shared lint config
|
||||
│ └── eslint.config.mjs # Shared lint config
|
||||
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||
├── docs/ # Documentation files
|
||||
├── exampleYmlFiles/ # Example YAML configuration files
|
||||
@@ -275,7 +275,7 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
1. Set the security environment variable:
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false to enable login and security features for builds
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
@@ -305,7 +305,7 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. However, to improve build times these can often be removed depending on your use case
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
## 7. Testing
|
||||
|
||||
@@ -504,8 +504,7 @@ For Stirling 2.0, new features are built as React components:
|
||||
1. **Create a New Controller:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
|
||||
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
||||
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates.")`.
|
||||
- If the endpoint transforms a document, declare what it accepts and produces with `@ToolIO`, for example `@ToolIO(produces = ToolFormat.PDF)`. This is what lets a pipeline containing the step be checked before it runs, so a chain that cannot work is caught in the builder rather than part-way through a job. Endpoints under the tool namespaces are required to carry it - `ToolIODeclarationCoverageTest` fails the build otherwise. See [Declaring tool inputs and outputs](#declaring-tool-inputs-and-outputs).
|
||||
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
@@ -579,38 +578,6 @@ For Stirling 2.0, new features are built as React components:
|
||||
}
|
||||
```
|
||||
|
||||
### Declaring tool inputs and outputs
|
||||
|
||||
An endpoint that transforms a document declares what it accepts and produces with `@ToolIO`. This is the single source of truth: it is published into the OpenAPI spec as an `x-stirling-io` extension, and generated from there into the frontend (`toolIO.ts`) and the AI engine (`tool_io.py`). A pipeline can therefore be checked while it is being edited, instead of failing part-way through a job.
|
||||
|
||||
```java
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
```
|
||||
|
||||
`accepts` defaults to `{ ToolFormat.PDF }` and `arity` to `ToolArity.SISO`, so most tools only declare what they produce.
|
||||
|
||||
- **`ToolFormat`** is the kind of file: `PDF`, `PDF_ENCRYPTED`, `IMAGE`, `ZIP`, `WORD`, `PPT`, `EXCEL`, `CSV`, `HTML`, `XML`, `JSON`, `TEXT`, `MARKDOWN`, `JAVASCRIPT`, `EBOOK`, `EMAIL`, `POSTSCRIPT`, `VIDEO`, `CBZ`, `CBR`, plus `ANY` (accepts or produces anything) and `NONE` (returns a report, not a file). Encryption is a format rather than a flag, so the default `accepts = PDF` means an endpoint rejects an encrypted PDF unless it opts in.
|
||||
- **`ToolArity`** is how many files go in and out: `SISO`, `SIMO`, `MISO`, `MIMO`. This axis carries ZIP-as-transport. A splitter is `produces = PDF, arity = SIMO`, and the caller unpacks the archive; an endpoint whose deliverable really is an archive declares `produces = ZIP` with a single-output arity and stays packed.
|
||||
|
||||
When the output depends on a parameter, declare the exception as a case rather than picking one answer. Add Password produces an encrypted PDF unless both passwords are blank, in which case it has only set permissions:
|
||||
|
||||
```java
|
||||
@ToolIO(
|
||||
produces = ToolFormat.PDF_ENCRYPTED,
|
||||
cases =
|
||||
@ToolIOCase(
|
||||
when = {
|
||||
@ToolIOWhen(param = "password", matches = ""),
|
||||
@ToolIOWhen(param = "ownerPassword", matches = "")
|
||||
},
|
||||
produces = ToolFormat.PDF,
|
||||
arity = ToolArity.SISO))
|
||||
```
|
||||
|
||||
Every condition in a `when` must hold for the case to apply, and `matches` is compared as a string, case-insensitively, with an empty string matching an absent or blank value. If a case reads a parameter that is not set yet, the output is reported as uncertain and the chain warns rather than erroring.
|
||||
|
||||
Endpoints under the tool namespaces must carry a declaration; `ToolIODeclarationCoverageTest` fails the build for any that does not, with a short allowlist for endpoints that manage a session, a device or a stored resource rather than transforming a document. The matching rules are implemented three times (Java `ToolChainValidator`, `toolIOCompat.ts`, `tool_io_compat.py`) and pinned to the same answers by the shared fixtures in `testing/tool-io-cases.json`, so a behaviour change belongs in that file first.
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
|
||||
@@ -20,10 +20,8 @@ if that directory exists, is licensed under the license defined in "frontend/edi
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/cloud/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/prototypes/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/prototypes/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/portal/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/portal/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/portal-saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/portal-saas/LICENSE".
|
||||
* All content that resides under the "frontend/portal/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/portal/LICENSE".
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the MIT License as defined below.
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ For full installation options (including desktop and Kubernetes), see our [Docum
|
||||
|
||||
## Support
|
||||
|
||||
- **Community**: [Discord](https://discord.gg/HYmhKj45pU)
|
||||
- **Bug Reports**: [GitHub Issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
- **Community** [Discord](https://discord.gg/HYmhKj45pU)
|
||||
- **Bug Reports**: [Github issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+86
-143
@@ -25,9 +25,6 @@ includes:
|
||||
e2e:
|
||||
taskfile: .taskfiles/e2e.yml
|
||||
dir: .
|
||||
cucumber:
|
||||
taskfile: .taskfiles/cucumber.yml
|
||||
dir: testing/cucumber
|
||||
pre-commit:
|
||||
taskfile: .taskfiles/pre-commit.yml
|
||||
dir: .
|
||||
@@ -82,139 +79,98 @@ tasks:
|
||||
OPEN: "true"
|
||||
|
||||
dev:portal:
|
||||
desc: "Start backend + editor; the portal is an admin route at /portal"
|
||||
desc: "Start backend + developer portal concurrently on free ports"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
EDITOR_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
PORTAL_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
deps:
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
POLICIES_ENABLED: "true"
|
||||
- task: frontend:dev:portal
|
||||
vars:
|
||||
PORT: '{{.PORTAL_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
dev:portal:all:
|
||||
desc: "Start backend + developer portal + editor concurrently on free ports"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5174{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5174{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
PORTAL_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
EDITOR_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||
deps:
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
POLICIES_ENABLED: "true"
|
||||
- task: frontend:dev:portal
|
||||
vars:
|
||||
PORT: '{{.PORTAL_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
# Point the portal's "Editor" app switcher at the editor we spawn here.
|
||||
EDITOR_URL: 'http://localhost:{{.EDITOR_PORT}}/'
|
||||
OPEN: "true"
|
||||
- task: frontend:dev
|
||||
vars:
|
||||
PORT: '{{.EDITOR_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
|
||||
dev:portal:proxy:
|
||||
desc: "Editor + portal on ONE origin + backend via live dev servers (shared-token login)"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 3000 5173 5174{{else}}{{.FIND_FREE_PORT_SH}} 8080 3000 5173 5174{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
PROXY_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
EDITOR_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||
PORTAL_PORT: '{{index (splitList "\n" .PORTS) 3}}'
|
||||
deps:
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
POLICIES_ENABLED: "true"
|
||||
- task: frontend:dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.EDITOR_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
# Set SAAS_DEV_PROJECT_REF in app/.env.saas.local to pick the PR.
|
||||
dev:saas:
|
||||
desc: "Start SaaS backend + frontend + engine against the current PR's preview branch"
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
vars: { FRONTEND: saas, BACKEND: saas, SAAS_ENV: dev }
|
||||
|
||||
staging:saas:
|
||||
desc: "Start SaaS backend + frontend + engine against the shared v3 staging project"
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
- task: frontend:dev:portal
|
||||
vars:
|
||||
FRONTEND: saas
|
||||
BACKEND: saas
|
||||
BACKEND_TASK: backend:staging:saas
|
||||
SAAS_ENV: staging
|
||||
PORT: '{{.PORTAL_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
SUBPATH: portal
|
||||
MOCKS: 'false'
|
||||
- task: frontend:dev:portal:proxy:serve
|
||||
vars:
|
||||
PORT: '{{.PROXY_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
EDITOR_DEV_URL: 'http://localhost:{{.EDITOR_PORT}}'
|
||||
PORTAL_DEV_URL: 'http://localhost:{{.PORTAL_PORT}}'
|
||||
|
||||
dev:saas:
|
||||
desc: "Start SaaS backend + frontend concurrently on free ports"
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
vars: { FRONTEND: saas, BACKEND: saas }
|
||||
|
||||
dev:all:
|
||||
desc: "Start backend + frontend + engine concurrently on free ports"
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
|
||||
# No engine: linking never calls it.
|
||||
linked:staging:
|
||||
desc: "SaaS on the shared v3 project + a self-hosted instance linked to it"
|
||||
cmds:
|
||||
- task: linked:_all
|
||||
vars: { SAAS_ENV: staging }
|
||||
|
||||
linked:dev:
|
||||
desc: "SaaS on the current PR's preview branch + a self-hosted instance linked to it"
|
||||
cmds:
|
||||
- task: linked:_all
|
||||
vars: { SAAS_ENV: dev }
|
||||
|
||||
linked:_all:
|
||||
internal: true
|
||||
vars:
|
||||
SAAS_ENV: '{{.SAAS_ENV | default "staging"}}'
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8081 5174 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8081 5174 8080 5173{{end}}'
|
||||
SAAS_BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
SAAS_FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
APP_BACKEND_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||
APP_FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 3}}'
|
||||
deps:
|
||||
# APP_BASE_URL is the SaaS *frontend*: the approval page is served by vite, not
|
||||
# by the API. BASE_PATH moves this backend's configs/pipeline aside so it does not
|
||||
# race the self-hosted one, which keeps ./configs and its existing database.
|
||||
- task: 'backend:{{.SAAS_ENV}}:saas'
|
||||
vars:
|
||||
PORT: '{{.SAAS_BACKEND_PORT}}'
|
||||
APP_BASE_URL: 'http://localhost:{{.SAAS_FRONTEND_PORT}}'
|
||||
BASE_PATH: 'tmp/linked-saas'
|
||||
- task: frontend:dev:saas
|
||||
vars:
|
||||
PORT: '{{.SAAS_FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.SAAS_BACKEND_PORT}}'
|
||||
SAAS_ENV: '{{.SAAS_ENV}}'
|
||||
- task: backend:dev:linked
|
||||
vars:
|
||||
PORT: '{{.APP_BACKEND_PORT}}'
|
||||
SAAS_BASE_URL: 'http://localhost:{{.SAAS_BACKEND_PORT}}'
|
||||
- task: frontend:dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.APP_FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.APP_BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
- task: linked:_ready
|
||||
vars:
|
||||
SAAS_BACKEND_PORT: '{{.SAAS_BACKEND_PORT}}'
|
||||
SAAS_FRONTEND_PORT: '{{.SAAS_FRONTEND_PORT}}'
|
||||
APP_BACKEND_PORT: '{{.APP_BACKEND_PORT}}'
|
||||
APP_FRONTEND_PORT: '{{.APP_FRONTEND_PORT}}'
|
||||
|
||||
# Waits for all four to answer, then prints where they landed.
|
||||
linked:_ready:
|
||||
internal: true
|
||||
cmds:
|
||||
- cmd: |
|
||||
n=0
|
||||
ok=0
|
||||
while [ "$n" -lt 150 ]; do
|
||||
ok=1
|
||||
for u in "http://localhost:{{.SAAS_BACKEND_PORT}}" \
|
||||
"http://localhost:{{.SAAS_FRONTEND_PORT}}" \
|
||||
"http://localhost:{{.APP_BACKEND_PORT}}" \
|
||||
"http://localhost:{{.APP_FRONTEND_PORT}}"; do
|
||||
# Not -o /dev/null: Windows curl.exe treats it as a real path and exits 23.
|
||||
curl -s -m 2 "$u" >/dev/null 2>&1 || ok=0
|
||||
done
|
||||
if [ "$ok" = 1 ]; then break; fi
|
||||
n=$((n + 1))
|
||||
# `sleep` is a binary, not a builtin, and Windows has none.
|
||||
{{if eq OS "windows"}}powershell -NoProfile -Command "Start-Sleep -Seconds 2"{{else}}sleep 2{{end}}
|
||||
done
|
||||
echo ""
|
||||
if [ "$ok" = 1 ]; then
|
||||
echo ">> all four answering"
|
||||
else
|
||||
echo ">> still waiting on one or more after 5 minutes; addresses below anyway"
|
||||
fi
|
||||
echo ">> self-hosted UI http://localhost:{{.APP_FRONTEND_PORT}}/processor"
|
||||
echo ">> self-hosted api http://localhost:{{.APP_BACKEND_PORT}}"
|
||||
echo ">> saas UI http://localhost:{{.SAAS_FRONTEND_PORT}}"
|
||||
echo ">> saas api http://localhost:{{.SAAS_BACKEND_PORT}}"
|
||||
echo ""
|
||||
|
||||
dev:_all:
|
||||
internal: true
|
||||
vars:
|
||||
FRONTEND: '{{.FRONTEND | default "proprietary"}}'
|
||||
BACKEND: '{{.BACKEND | default "proprietary"}}'
|
||||
BACKEND_TASK: '{{.BACKEND_TASK | default (printf "backend:dev:%s" .BACKEND)}}'
|
||||
# Only meaningful to the saas frontend; every other flavor ignores it.
|
||||
SAAS_ENV: '{{.SAAS_ENV | default ""}}'
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
@@ -224,7 +180,7 @@ tasks:
|
||||
- task: engine:dev
|
||||
vars:
|
||||
PORT: '{{.ENGINE_PORT}}'
|
||||
- task: '{{.BACKEND_TASK}}'
|
||||
- task: 'backend:dev:{{.BACKEND}}'
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}'
|
||||
@@ -234,7 +190,6 @@ tasks:
|
||||
PORT: '{{.FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
SAAS_ENV: '{{.SAAS_ENV}}'
|
||||
|
||||
# ============================================================
|
||||
# Build
|
||||
@@ -246,6 +201,24 @@ tasks:
|
||||
- task: backend:build
|
||||
- task: frontend:build
|
||||
|
||||
preview:portal:proxy:
|
||||
desc: "Build + serve editor + portal on ONE origin + backend (prod-like auth test)"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 3000{{else}}{{.FIND_FREE_PORT_SH}} 8080 3000{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
PROXY_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
deps:
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
POLICIES_ENABLED: "true"
|
||||
- task: frontend:preview:portal:proxy
|
||||
vars:
|
||||
PORT: '{{.PROXY_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
|
||||
# ============================================================
|
||||
# Test
|
||||
# ============================================================
|
||||
@@ -266,20 +239,6 @@ tasks:
|
||||
cmds:
|
||||
- task: frontend:lint
|
||||
- task: engine:lint
|
||||
- task: comment-lint
|
||||
|
||||
comment-lint:
|
||||
desc: "Check comment quality on the lines this branch adds"
|
||||
aliases: [comments]
|
||||
cmds:
|
||||
- task: pre-commit:comment-lint
|
||||
vars: { CLI_ARGS: '{{.CLI_ARGS}}' }
|
||||
|
||||
comment-lint:branch:
|
||||
desc: "Check comment quality on everything this branch adds over its base"
|
||||
cmds:
|
||||
- task: pre-commit:comment-lint:branch
|
||||
vars: { BASE: '{{.BASE}}' }
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix all components"
|
||||
@@ -302,22 +261,6 @@ tasks:
|
||||
- task: frontend:format:check
|
||||
- task: engine:format:check
|
||||
|
||||
# ============================================================
|
||||
# Code generation
|
||||
# ============================================================
|
||||
|
||||
tool-models:
|
||||
desc: "Generate all API models from the Java OpenAPI spec"
|
||||
cmds:
|
||||
- task: frontend:tool-models
|
||||
- task: engine:tool-models
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if any committed API model is out of date"
|
||||
cmds:
|
||||
- task: frontend:tool-models:check
|
||||
- task: engine:tool-models:check
|
||||
|
||||
# ============================================================
|
||||
# Quality Gate
|
||||
# ============================================================
|
||||
|
||||
+38
-33
@@ -4,11 +4,6 @@ This guide explains how to set up Windows code signing for Stirling-PDF desktop
|
||||
|
||||
## Overview
|
||||
|
||||
Releases are signed with **DigiCert KeyLocker**, a cloud HSM: the private key never
|
||||
leaves DigiCert, and the runner signs through a PKCS#11 provider. The older approach
|
||||
of uploading a base64 `.pfx` to a repository secret has been removed from the
|
||||
workflows - the sections below describe KeyLocker, which is what actually runs.
|
||||
|
||||
Windows code signing is essential for:
|
||||
- Preventing Windows SmartScreen warnings
|
||||
- Building trust with users
|
||||
@@ -54,19 +49,29 @@ openssl pkcs12 -export -out certificate.pfx -inkey private-key.key -in certifica
|
||||
|
||||
### Required Secrets
|
||||
|
||||
Navigate to your GitHub repository → Settings → Environments → `release-signing`.
|
||||
Navigate to your GitHub repository → Settings → Secrets and variables → Actions
|
||||
|
||||
These live in the `release-signing` environment, not at repository scope. That
|
||||
environment requires reviewer approval and is limited to `main`, `release`,
|
||||
`hotfix/*` and `v*` tags. All five come from the DigiCert ONE console.
|
||||
Add the following secrets:
|
||||
|
||||
| Secret | Description |
|
||||
| --- | --- |
|
||||
| `SM_API_KEY` | KeyLocker API key. Also acts as the on/off switch: signing steps are gated on it being non-empty. |
|
||||
| `SM_CLIENT_CERT_FILE_B64` | Base64-encoded PKCS#12 client authentication certificate. |
|
||||
| `SM_CLIENT_CERT_PASSWORD` | Password for that client certificate. |
|
||||
| `SM_KEYPAIR_ALIAS` | Alias of the signing keypair to use. |
|
||||
| `SM_HOST` | DigiCert ONE host, e.g. `https://clientauth.one.digicert.com`. |
|
||||
#### 1. `WINDOWS_CERTIFICATE`
|
||||
- **Description**: Base64-encoded .pfx certificate file
|
||||
- **How to create**:
|
||||
|
||||
**On macOS/Linux:**
|
||||
```bash
|
||||
base64 -i certificate.pfx | pbcopy # Copies to clipboard
|
||||
```
|
||||
|
||||
**On Windows (PowerShell):**
|
||||
```powershell
|
||||
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Set-Clipboard
|
||||
```
|
||||
|
||||
Paste the entire base64 string into the GitHub secret.
|
||||
|
||||
#### 2. `WINDOWS_CERTIFICATE_PASSWORD`
|
||||
- **Description**: Password for the .pfx certificate
|
||||
- **Value**: The password you set when creating/exporting the .pfx file
|
||||
|
||||
### Optional Secrets for Tauri Updater
|
||||
|
||||
@@ -105,23 +110,23 @@ The Windows signing configuration is already set up:
|
||||
|
||||
### 2. GitHub Workflow (.github/workflows/tauri-build.yml)
|
||||
|
||||
The workflow includes four Windows signing steps, all gated on `SM_API_KEY` being
|
||||
set and the ref being the release branch:
|
||||
The workflow includes three Windows signing steps:
|
||||
|
||||
1. **Setup DigiCert KeyLocker**: Installs the DigiCert signing tools via `digicert/ssm-code-signing`
|
||||
2. **Setup DigiCert KeyLocker Certificate**: Writes the client cert and exports the PKCS#11 config
|
||||
3. **Configure Windows code signing / Build Tauri app**: Signs through the PKCS#11 provider
|
||||
4. **Verify Windows Code Signature**: Validates that the .exe and .msi are properly signed
|
||||
1. **Import Certificate**: Decodes and imports the .pfx certificate into Windows certificate store
|
||||
2. **Build Tauri App**: Builds and signs the application using the imported certificate
|
||||
3. **Verify Signature**: Validates that both .exe and .msi files are properly signed
|
||||
|
||||
## Testing the Setup
|
||||
|
||||
### 1. Local Testing (Windows Only)
|
||||
|
||||
KeyLocker is CI-only. To check signing locally, install your own certificate into
|
||||
the Windows store and point Tauri at it; the build no longer reads any certificate
|
||||
from an environment variable.
|
||||
Before pushing to GitHub, test locally:
|
||||
|
||||
```powershell
|
||||
# Set environment variables
|
||||
$env:WINDOWS_CERTIFICATE = [Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx"))
|
||||
$env:WINDOWS_CERTIFICATE_PASSWORD = "your-certificate-password"
|
||||
|
||||
# Build the application
|
||||
cd frontend
|
||||
npm run tauri build
|
||||
@@ -186,10 +191,9 @@ Look for:
|
||||
- Consider EV certificate for immediate reputation
|
||||
|
||||
### Certificate Not Found During Build
|
||||
- Verify `SM_API_KEY` is present in the `release-signing` environment. If it is empty
|
||||
the signing steps skip silently and the build succeeds unsigned.
|
||||
- Check `SM_CLIENT_CERT_FILE_B64` base64 encoding is correct (no extra whitespace)
|
||||
- Ensure `SM_CLIENT_CERT_PASSWORD` and `SM_KEYPAIR_ALIAS` match the DigiCert keypair
|
||||
- Verify `WINDOWS_CERTIFICATE` secret is set
|
||||
- Check base64 encoding is correct (no extra whitespace)
|
||||
- Ensure password is correct
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
@@ -216,10 +220,11 @@ Look for:
|
||||
## Certificate Lifecycle
|
||||
|
||||
### Before Expiration
|
||||
1. Renew the certificate in the DigiCert ONE console (typically annual)
|
||||
2. If the keypair alias changed, update `SM_KEYPAIR_ALIAS` in the `release-signing` environment
|
||||
3. If the client authentication certificate was reissued, update `SM_CLIENT_CERT_FILE_B64` and `SM_CLIENT_CERT_PASSWORD`
|
||||
4. Test build to verify the new certificate works
|
||||
1. Obtain new certificate from CA (typically annual renewal)
|
||||
2. Convert to .pfx format if needed
|
||||
3. Update `WINDOWS_CERTIFICATE` secret with new base64-encoded certificate
|
||||
4. Update `WINDOWS_CERTIFICATE_PASSWORD` if password changed
|
||||
5. Test build to verify new certificate works
|
||||
|
||||
### Expired Certificates
|
||||
- Signed binaries remain valid (timestamp proves signing time)
|
||||
|
||||
+17
-35
@@ -1,16 +1,15 @@
|
||||
# Stirling-PDF SaaS environment defaults. Committed, non-secret. Real values for secrets go in
|
||||
# .env.saas.local, which is loaded first and wins. Do not commit that file.
|
||||
###############################################################################
|
||||
# Stirling-PDF SaaS environment defaults.
|
||||
#
|
||||
# Three environments, each deriving its Supabase URLs, JWT issuer and JWKS from one project ref:
|
||||
# This file is committed and provides non-secret defaults loaded by
|
||||
# `task backend:dev:saas`. Put real values for secrets (passwords, project
|
||||
# refs, edge function secrets) in `.env.saas.local` - any variable set there
|
||||
# takes precedence over what's defined here.
|
||||
#
|
||||
# prod PROFILES=none SAAS_DB_* the live project
|
||||
# staging PROFILES=staging SAAS_STAGING_* pinned to v3, always there
|
||||
# dev PROFILES=dev SAAS_DEV_* follows a SaaS PR's preview branch
|
||||
#
|
||||
# dev is the default for `task backend:dev:saas`. Use staging for somewhere stable; use dev when
|
||||
# testing an open SaaS PR, since its preview branch is the only place those migrations are applied.
|
||||
# DO NOT commit `.env.saas.local`. Only `.env.saas` is checked in.
|
||||
###############################################################################
|
||||
|
||||
# ---------- Supabase project (prod / no-profile) ----------
|
||||
# ---------- Supabase project ----------
|
||||
# Project reference (the subdomain part of <ref>.supabase.co). Required.
|
||||
# Set in .env.saas.local.
|
||||
SAAS_DB_PROJECT_REF=
|
||||
@@ -18,35 +17,18 @@ SAAS_DB_PROJECT_REF=
|
||||
# Edge function secret used by billing/license rollup calls. Set in .env.saas.local.
|
||||
SUPABASE_EDGE_FUNCTION_SECRET=
|
||||
|
||||
# ---------- Database (no profile) ----------
|
||||
# Direct JDBC URL to the Supabase Postgres. Required when running without
|
||||
# `--spring.profiles.include=...`.
|
||||
# ---------- Database (saas profile) ----------
|
||||
# Direct JDBC URL to the Supabase Postgres. Required when running the plain
|
||||
# `saas` profile (i.e. without `--spring.profiles.include=dev`).
|
||||
# Example: jdbc:postgresql://db.<project-ref>.supabase.co:5432/postgres
|
||||
SAAS_DB_URL=
|
||||
SAAS_DB_USERNAME=postgres
|
||||
SAAS_DB_PASSWORD=
|
||||
|
||||
# ---------- staging profile ----------
|
||||
# The shared long-lived v3 project. application-staging.properties defaults the ref,
|
||||
# URL, database host and meter endpoint, so staging needs only the password, in
|
||||
# .env.saas.local. Set SAAS_STAGING_PROJECT_REF to repoint it; everything derives.
|
||||
#
|
||||
# The ref and publishable key are duplicated here because the task derives the
|
||||
# frontend's VITE_SUPABASE_* from them and a shell cannot read a Spring default.
|
||||
# Neither is secret: the ref is a public subdomain, the key ships in the bundle.
|
||||
SAAS_STAGING_PROJECT_REF=qacaivhsjtftfwtgjvva
|
||||
SAAS_STAGING_PUBLISHABLE_KEY=sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY # gitleaks:allow
|
||||
SAAS_STAGING_DB_USERNAME=postgres
|
||||
SAAS_STAGING_DB_PASSWORD=
|
||||
|
||||
# ---------- dev profile ----------
|
||||
# The SaaS PR's Supabase preview branch. Take the ref from that PR's "Supabase
|
||||
# Preview" check; the profile derives URL, JWT issuer, JWKS, meter endpoint and
|
||||
# database host from it, so this one value follows a different PR.
|
||||
#
|
||||
# A preview branch has its own password and keys; the parent project's will not
|
||||
# authenticate. Both go in .env.saas.local, along with the ref.
|
||||
SAAS_DEV_PROJECT_REF=
|
||||
SAAS_DEV_PUBLISHABLE_KEY=
|
||||
# ---------- Database (dev profile overrides) ----------
|
||||
# Used when `--spring.profiles.include=dev` is active. The dev profile
|
||||
# defaults the URL/username to the shared dev Supabase project, but the
|
||||
# password must still be provided in .env.saas.local.
|
||||
SAAS_DEV_DB_URL=
|
||||
SAAS_DEV_DB_USERNAME=postgres
|
||||
SAAS_DEV_DB_PASSWORD=
|
||||
|
||||
@@ -32,14 +32,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "BSD-4 License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Revised BSD"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "ISC"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT"
|
||||
@@ -56,10 +48,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT-0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.github.jai-imageio:jai-imageio-core",
|
||||
"moduleLicense": "LICENSE.txt"
|
||||
@@ -92,18 +80,10 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License, Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License, version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The Apache License, Version 2.0"
|
||||
@@ -128,10 +108,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Mozilla Public License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "CDDL+GPL License"
|
||||
@@ -156,14 +132,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "GNU Lesser Public License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The GNU Lesser General Public License"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.martiansoftware:jsap",
|
||||
"moduleLicense": "LGPL"
|
||||
@@ -204,14 +172,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License, Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "EPL-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "LGPL-2.1-only"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Ubuntu Font Licence 1.0"
|
||||
@@ -228,10 +188,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The W3C License"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.google.re2j:re2j",
|
||||
"moduleLicense": "Go License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "UnRar License"
|
||||
|
||||
+37
-36
@@ -2,11 +2,33 @@
|
||||
bootRun {
|
||||
enabled = false
|
||||
}
|
||||
spotless {
|
||||
java {
|
||||
target 'src/**/java/**/*.java'
|
||||
targetExclude 'src/main/java/org/apache/**'
|
||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
|
||||
suppressLintsFor { setStep('google-java-format') }
|
||||
|
||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||
trimTrailingWhitespace()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
yaml {
|
||||
target '**/*.yml', '**/*.yaml'
|
||||
trimTrailingWhitespace()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
format 'gradle', {
|
||||
target '**/gradle/*.gradle', '**/*.gradle'
|
||||
trimTrailingWhitespace()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
// Security-hardening utilities (zip-slip, SSRF, filename sanitization, command injection).
|
||||
// Declared as api here so core + proprietary (which depend on common) get it transitively,
|
||||
// keeping it off modules that don't need it (e.g. saas).
|
||||
api 'io.github.pixee:java-security-toolkit:1.2.3'
|
||||
api "com.google.guava:guava:${guavaVersion}"
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
@@ -14,22 +36,19 @@ dependencies {
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
api "org.apache.commons:commons-lang3:${commonsLang3}"
|
||||
api 'com.drewnoakes:metadata-extractor:2.21.0' // Image metadata extractor
|
||||
api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor
|
||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:8.0.0' // RAR archive support for CBR files
|
||||
api 'com.github.junrar:junrar:7.5.10' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.1.1'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
|
||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||
api 'org.simplejavamail:simple-java-mail:9.3.2'
|
||||
// MSG file support; exclude commons-math3 (only HSSF/formula needs it, MSG parsing doesn't)
|
||||
api('org.simplejavamail:outlook-module:9.3.2') {
|
||||
exclude group: 'org.apache.commons', module: 'commons-math3'
|
||||
}
|
||||
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
||||
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||
|
||||
@@ -43,30 +62,12 @@ dependencies {
|
||||
|
||||
api "com.stirling:jpdfium:${jpdfiumVersion}"
|
||||
|
||||
// -PjpdfiumPlatforms=auto|all|none|<csv of linux-x64,linux-arm64,linux-musl-x64,linux-musl-arm64,darwin-x64,darwin-arm64,windows-x64> (windows-arm64 natives not published yet)
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'auto').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'linux-musl-x64', 'linux-musl-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
// -PjpdfiumPlatforms=all|none|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
// 'none' skips natives entirely (windows-arm64 builds, until JPDFium ships that platform).
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
def jpdfiumPlatforms
|
||||
if (jpdfiumPlatformsProp == 'auto') {
|
||||
def osName = System.getProperty('os.name').toLowerCase()
|
||||
def osArch = System.getProperty('os.arch').toLowerCase()
|
||||
def isArm64 = osArch.contains('aarch64') || osArch.contains('arm64')
|
||||
if (osName.contains('linux')) {
|
||||
jpdfiumPlatforms = isArm64 ? ['linux-arm64'] : ['linux-x64']
|
||||
} else if (osName.contains('mac')) {
|
||||
jpdfiumPlatforms = isArm64 ? ['darwin-arm64'] : ['darwin-x64']
|
||||
} else if (osName.contains('win')) {
|
||||
if (isArm64) {
|
||||
logger.lifecycle("JPDFium natives are not available for windows-arm64; set -PjpdfiumPlatforms=none to skip bundling natives.")
|
||||
jpdfiumPlatforms = []
|
||||
} else {
|
||||
jpdfiumPlatforms = ['windows-x64']
|
||||
}
|
||||
} else {
|
||||
// Fallback: bundle all platforms when host can't be determined
|
||||
jpdfiumPlatforms = jpdfiumAllPlatforms
|
||||
}
|
||||
} else if (jpdfiumPlatformsProp == 'all') {
|
||||
if (jpdfiumPlatformsProp == 'all') {
|
||||
jpdfiumPlatforms = jpdfiumAllPlatforms
|
||||
} else if (jpdfiumPlatformsProp == 'none') {
|
||||
jpdfiumPlatforms = []
|
||||
@@ -76,7 +77,7 @@ dependencies {
|
||||
def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) }
|
||||
if (jpdfiumInvalid) {
|
||||
throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " +
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')}, 'auto', 'all' or 'none'.")
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')}, 'all' or 'none'.")
|
||||
}
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms ? jpdfiumPlatforms.join(', ') : 'none'}")
|
||||
jpdfiumPlatforms.each { platform ->
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package stirling.software.common.util;
|
||||
package org.apache.pdfbox.examples.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -6,7 +6,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -14,7 +13,6 @@ import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.PdfaLevelAServiceInterface;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -48,21 +46,17 @@ public class EndpointConfiguration {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@Getter private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
private Set<String> disabledGroups = ConcurrentHashMap.newKeySet();
|
||||
private Set<String> disabledGroups = new HashSet<>();
|
||||
private Map<String, DisableReason> endpointDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, DisableReason> groupDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||
private final boolean runningProOrHigher;
|
||||
private final boolean pdfUaAvailable;
|
||||
|
||||
public EndpointConfiguration(
|
||||
ApplicationProperties applicationProperties,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
@Autowired(required = false) PdfaLevelAServiceInterface pdfaLevelAService) {
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
// The PDF/UA tagger ships in the proprietary module, and so do its endpoints.
|
||||
this.pdfUaAvailable = pdfaLevelAService != null;
|
||||
init();
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
@@ -174,8 +168,7 @@ public class EndpointConfiguration {
|
||||
&& disabledGroups.contains(group)
|
||||
&& entry.getValue().contains(endpoint)) {
|
||||
log.debug(
|
||||
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no"
|
||||
+ " alternatives)",
|
||||
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
|
||||
original,
|
||||
group);
|
||||
return false;
|
||||
@@ -334,8 +327,7 @@ public class EndpointConfiguration {
|
||||
String.join(", ", functionallyDisabledEndpoints));
|
||||
} else if (!disabledToolGroups.isEmpty()) {
|
||||
log.info(
|
||||
"No endpoints disabled despite missing tools - fallback implementations"
|
||||
+ " available");
|
||||
"No endpoints disabled despite missing tools - fallback implementations available");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +338,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("PageOps", "split-pages");
|
||||
addEndpointToGroup("PageOps", "rearrange-pages");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "auto-rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||
addEndpointToGroup("PageOps", "booklet-imposition");
|
||||
addEndpointToGroup("PageOps", "scale-pages");
|
||||
@@ -364,7 +355,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||
addEndpointToGroup("Convert", "pdf-to-ua");
|
||||
addEndpointToGroup("Convert", "file-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-word");
|
||||
addEndpointToGroup("Convert", "pdf-to-presentation");
|
||||
@@ -404,7 +394,6 @@ public class EndpointConfiguration {
|
||||
// Backend-only endpoints (not in frontend tool registry endpoints)
|
||||
addEndpointToGroup("Security", "redact");
|
||||
addEndpointToGroup("Security", "verify-pdf");
|
||||
addEndpointToGroup("Security", "accessibility-report");
|
||||
addEndpointToGroup("Security", "sign");
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
@@ -539,8 +528,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "json-to-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-video");
|
||||
addEndpointToGroup("Java", "verify-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-ua");
|
||||
addEndpointToGroup("Java", "accessibility-report");
|
||||
addEndpointToGroup("Java", "flatten");
|
||||
addEndpointToGroup("Java", "unlock-pdf-forms");
|
||||
addEndpointToGroup("Java", "validate-signature");
|
||||
@@ -612,8 +599,6 @@ public class EndpointConfiguration {
|
||||
|
||||
// veraPDF dependent endpoints
|
||||
addEndpointToGroup("veraPDF", "verify-pdf");
|
||||
addEndpointToGroup("veraPDF", "pdf-to-ua");
|
||||
addEndpointToGroup("veraPDF", "accessibility-report");
|
||||
|
||||
// Pdftohtml dependent endpoints
|
||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||
@@ -644,11 +629,6 @@ public class EndpointConfiguration {
|
||||
disableGroup("enterprise");
|
||||
}
|
||||
|
||||
if (!pdfUaAvailable) {
|
||||
disableEndpoint("pdf-to-ua");
|
||||
disableEndpoint("accessibility-report");
|
||||
}
|
||||
|
||||
if (!applicationProperties.getSystem().isEnableUrlToPDF()) {
|
||||
disableEndpoint("url-to-pdf");
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ public class TabulaTableParser implements TableParser {
|
||||
score -= 0.3f;
|
||||
}
|
||||
|
||||
return Math.clamp(score, 0f, 1f);
|
||||
return Math.max(0f, Math.min(1f, score));
|
||||
}
|
||||
|
||||
private Bounds tableBounds(Table table) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user