fix(extension): use template for Edge certification notes

This commit is contained in:
Tautvydas Derzinskas committed 2026-10-06 17:49:34 +02:00
1 parent 84b4f5bd14
commit 30be8c4349
4 files changed
+81 -8

No files matched your search

@@ -147,10 +147,13 @@ jobs:
with:
name: thingport-grab-store-zips
path: extension/dist/zips
# Edge wants complete certification notes with every submission; see the notes file.
- name: Upload and submit for certification
run: |
npx tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip --notes \
"Automated update to version ${{ needs.release.outputs.version }}. Changes: https://github.com/TautvydasDerzinskas/Thingport/releases/tag/${{ needs.release.outputs.tag }} -- testing instructions are unchanged from the previous submission; documentation: https://thingport.net/docs/grab/"
env:
RELEASE_VERSION: ${{ needs.release.outputs.version }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
EDGE_REVIEW_TEST_SERVER: ${{ secrets.EDGE_REVIEW_TEST_SERVER }}
run: npx tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip --notes-file scripts/edge-certification-notes.txt
# Its own job so it can be re-run alone. Same AMO credentials as the per-merge unlisted signing
# in extension-release.yml; see extension/CONTRIBUTING.md.
+12
View File
@@ -262,6 +262,18 @@ previous version is still in certification, the job fails with `InProgressSubmis
Microsoft has finished, open the failed run and use **Re-run failed jobs**. (The release itself,
tag and changelog included, is already done at that point and isn't repeated.)
Edge wants complete certification notes (under 2,000 characters) with every submission, and may fail
one that only points at an earlier one. The job sends
[scripts/edge-certification-notes.txt](scripts/edge-certification-notes.txt) with the version and
the release link filled in; edit that file when testing steps or permissions change. The script
refuses to upload if the filled-in notes are too long.
Reviewers need a Thingport server to test against. If the optional repo secret
`EDGE_REVIEW_TEST_SERVER` is set, its text goes into the notes (e.g. "Use the test server
https://… with email … and password …."). Without it, the notes tell the reviewer how to start a
server of their own with `install.sh`. Keep reviewer login details in that secret only: this repo
is public.
To test the script against Edge by hand, with the credentials below in your environment:
```bash
@@ -0,0 +1,18 @@
Thingport Grab {{version}}. Changes in this version: {{changes}}
WHAT IT DOES
Imports 3D-printing models from MakerWorld, Printables and Thingiverse into the user's own self-hosted Thingport server (open source: https://github.com/TautvydasDerzinskas/Thingport). It has no servers of its own and sends nothing to the developer or any third party.
HOW TO TEST
1. A Thingport server is required. {{testServer}}
2. Click the toolbar icon, enter the server address, email and password, and connect. Allow the access request for that address.
3. Open a model page, e.g. https://www.printables.com/model/3161-3d-benchy or https://makerworld.com/en/models/3080401, click the Thingport button at the bottom right of the page and choose Import. The model appears in the Thingport web app and under Recent imports in the toolbar popup.
4. MakerWorld downloads need a MakerWorld login in the same browser; Printables and Thingiverse work without one.
PERMISSIONS
- storage: the server address, sign-in and recent imports.
- cookies: reads the MakerWorld session cookie, on makerworld.com only, which MakerWorld requires for downloads.
- downloads: reads the link from a download MakerWorld's page starts, then cancels it.
- host access: the three model sites, and the user's own server address (requested at setup).
The code isn't minified or obfuscated. Documentation: https://thingport.net/docs/grab/ Privacy policy: https://github.com/TautvydasDerzinskas/Thingport/blob/main/extension/PRIVACY.md
+45 -5
View File
@@ -5,7 +5,12 @@
// Microsoft anywhere from hours to days -- this only gets the update into the queue.
//
// EDGE_PRODUCT_ID=... EDGE_CLIENT_ID=... EDGE_API_KEY=... \
// tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip [--notes "..."]
// tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip [--notes-file <path> | --notes "..."]
//
// Edge requires complete certification notes (under 2,000 characters) with every submission, and
// may fail one that only points at an earlier one. --notes-file fills a template's {{version}},
// {{changes}} and {{testServer}} from RELEASE_VERSION, RELEASE_TAG and EDGE_REVIEW_TEST_SERVER (a
// secret: a reviewer account on a test server); see edge-certification-notes.txt.
//
// Run by .github/workflows/extension-release.yml when package.json's version hasn't been
// published to Edge yet (see extension/CONTRIBUTING.md for setting up the credentials).
@@ -16,6 +21,14 @@ import { readFile } from "node:fs/promises";
const API_BASE = process.env.EDGE_API_BASE || "https://api.addons.microsoftedge.microsoft.com";
const POLL_INTERVAL_MS = 5000;
const POLL_TIMEOUT_MS = 10 * 60 * 1000;
const NOTES_MAX_CHARS = 2000;
const REPO_URL = "https://github.com/TautvydasDerzinskas/Thingport";
// Without a reviewer account, the reviewer runs a server of their own.
const SELF_HOSTED_TEST_SERVER =
"No shared test server is provided. Start one with Docker in about two minutes (Linux or macOS) by " +
"running: curl -fsSL https://thingport.net/install.sh | WEB_PORT=8080 sh\nThen open " +
"http://localhost:8080 and register; the first account becomes the admin. Use http://localhost:8080 " +
"as the server address in step 2.";
type Operation = {
id?: string;
@@ -45,6 +58,26 @@ function argValue(flag: string): string | undefined {
return index >= 0 ? process.argv[index + 1] : undefined;
}
/** The template with its placeholders filled; refuses notes Edge would reject. */
async function notesFromTemplate(path: string): Promise<string> {
const version = process.env.RELEASE_VERSION?.trim() || "update";
const tag = process.env.RELEASE_TAG?.trim();
const values: Record<string, string> = {
version,
changes: tag ? `${REPO_URL}/releases/tag/${tag}` : `${REPO_URL}/blob/main/extension/CHANGELOG.md`,
testServer: process.env.EDGE_REVIEW_TEST_SERVER?.trim() || SELF_HOSTED_TEST_SERVER,
};
const notes = (await readFile(path, "utf8"))
.replace(/\{\{(\w+)\}\}/g, (match, key: string) => values[key] ?? match)
.trim();
const unfilled = notes.match(/\{\{\w+\}\}/);
if (unfilled) throw new Error(`${path}: unknown placeholder ${unfilled[0]}`);
if (notes.length >= NOTES_MAX_CHARS) {
throw new Error(`Certification notes are ${notes.length} characters; Edge allows under ${NOTES_MAX_CHARS}.`);
}
return notes;
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/** A 202 whose Location header carries the operation to poll (documented as the bare ID; a
@@ -85,8 +118,17 @@ async function waitFor(url: string, auth: Record<string, string>, what: string):
}
async function main(): Promise<void> {
const zipPath = process.argv.slice(2).find((arg, i, all) => !arg.startsWith("--") && all[i - 1] !== "--notes");
if (!zipPath) throw new Error("Usage: tsx scripts/publish-edge.ts <package.zip> [--notes <text>]");
const zipPath = process.argv
.slice(2)
.find((arg, i, all) => !arg.startsWith("--") && all[i - 1] !== "--notes" && all[i - 1] !== "--notes-file");
if (!zipPath) {
throw new Error("Usage: tsx scripts/publish-edge.ts <package.zip> [--notes-file <path> | --notes <text>]");
}
// Checked before the upload, so a too-long note doesn't leave a half-done draft behind.
const notesFile = argValue("--notes-file");
const notes = notesFile
? await notesFromTemplate(notesFile)
: (argValue("--notes") ?? "Automated update. Testing instructions are unchanged from the previous submission.");
const productId = requireEnv("EDGE_PRODUCT_ID");
const auth = { Authorization: `ApiKey ${requireEnv("EDGE_API_KEY")}`, "X-ClientID": requireEnv("EDGE_CLIENT_ID") };
const productUrl = `${API_BASE}/v1/products/${productId}`;
@@ -101,8 +143,6 @@ async function main(): Promise<void> {
const uploaded = await waitFor(`${productUrl}/submissions/draft/package/operations/${uploadId}`, auth, "Upload");
console.log(`Upload processed: ${uploaded.message ?? "ok"}`);
const notes =
argValue("--notes") ?? "Automated update. Testing instructions are unchanged from the previous submission.";
const publishId = await startOperation(
`${productUrl}/submissions`,
{ method: "POST", headers: { ...auth, "Content-Type": "application/json" }, body: JSON.stringify({ notes }) },