Skip to content

Commit 27c9c61

Browse files
priyanshu92Copilot
andcommitted
Merge latest safe ZIP validation layer
Propagate current main and the latest #385 tip through the shell and URL hardening stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2 parents 48afdd2 + 156559b commit 27c9c61

115 files changed

Lines changed: 9950 additions & 2254 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Functional unit tests for the model-apps plugin, across ubuntu × windows × macos
2+
# and Node 20 × 22.
3+
#
4+
# Why a matrix: these scripts shell out to `pac`/`az`, build filesystem paths, and
5+
# parse CLI output, so path-separator and shell differences are a real regression
6+
# class that a single-OS run would miss. Both Node versions are covered because the
7+
# plugin is developed on Node 22 while the repo's other test workflow pins Node 20 —
8+
# a version-only failure should be attributable, not a mystery.
9+
#
10+
# Deliberately mirrors `power-pages-script-tests.yml` rather than extending it: the
11+
# two plugins have independent path filters, so a change to one must not spend CI
12+
# time on the other.
13+
name: model-apps-script-tests
14+
15+
on:
16+
pull_request:
17+
branches:
18+
- main
19+
paths:
20+
- "plugins/model-apps/**"
21+
# The eval harness lives at the repo root but is owned by this plugin, so
22+
# an eval-only change must still trigger the eval job below.
23+
- "evals/model-apps/**"
24+
- ".github/workflows/model-apps-script-tests.yml"
25+
26+
jobs:
27+
test-model-apps-scripts:
28+
name: test-model-apps-scripts (${{ matrix.os }}, node ${{ matrix.node }})
29+
runs-on: ${{ matrix.os }}
30+
# Defense-in-depth: opt this CI job out of telemetry transmission so a test
31+
# that forgets to isolate emission can never POST to a real collector. The
32+
# plugin's committed ikey.json currently ships `disabled: true`, but that is
33+
# a value that can change; the guard must not depend on it. Suppresses
34+
# TRANSMISSION only — the local diagnostic mirror is still written, so this
35+
# cannot change what the tests actually assert.
36+
env:
37+
POWER_PLATFORM_SKILLS_TELEMETRY_MODEL_APPS_OPTOUT: "1"
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
os:
42+
- ubuntu-latest
43+
- windows-latest
44+
- macos-latest
45+
node:
46+
- 20
47+
- 22
48+
steps:
49+
- name: checkout
50+
uses: actions/checkout@v4
51+
52+
- name: setup-node
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: ${{ matrix.node }}
56+
57+
# run-tests.js is the plugin's own regression gate: it runs every
58+
# scripts/tests/*.test.js and exits non-zero on any failure. Invoking it
59+
# (rather than a bare `node --test`) keeps CI and the documented local
60+
# command identical, so a green PR means the same thing a green local run
61+
# does. The vendored SDK's own Jest suite is intentionally NOT run here —
62+
# it needs a Node-20-ABI `canvas` native module and the SDK source tree,
63+
# neither of which exists in this repo; the real-bundle contract tests in
64+
# scripts/tests/ cover the shipped bundle instead.
65+
- name: run-model-apps-script-tests
66+
shell: bash
67+
working-directory: plugins/model-apps
68+
run: node scripts/run-tests.js
69+
70+
test-model-apps-evals:
71+
name: test-model-apps-evals
72+
runs-on: ubuntu-latest
73+
env:
74+
POWER_PLATFORM_SKILLS_TELEMETRY_MODEL_APPS_OPTOUT: "1"
75+
steps:
76+
- name: checkout
77+
uses: actions/checkout@v4
78+
79+
- name: setup-node
80+
uses: actions/setup-node@v4
81+
with:
82+
node-version: 20
83+
84+
# The eval harness lives at the repo root (evals/model-apps/**) but is
85+
# owned by this plugin, so it is gated by the same path filter. These are
86+
# the offline scoring/fixture tests only — the Layer 1/2 runners need a
87+
# live Dataverse org and are run manually. `evals/model-apps/tests/` holds
88+
# tests for code shared by the app-builder and genpage runners; a new test
89+
# directory has to be added here or it silently never runs.
90+
- name: run-model-apps-eval-tests
91+
shell: bash
92+
run: node --test evals/model-apps/tests/*.test.js evals/model-apps/app-builder/tests/*.test.js evals/model-apps/genpage/tests/*.test.js

.npmrc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Never write registry URLs into a committed package-lock.json.
2+
#
3+
# `resolved` records the exact feed a package came from. Public npm is blocked on some Microsoft
4+
# networks, so a contributor installing through an internal Azure Artifacts feed would otherwise
5+
# rewrite every `resolved` to that internal URL and leak it into this OSS repo on the next commit.
6+
# Omitting the field keeps `integrity` (the sha512 that actually protects the supply chain) while
7+
# making the lock feed-agnostic, so it resolves against whatever registry each contributor uses.
8+
# Same setting microsoft/powerplatform-vscode uses.
9+
#
10+
# NOTE: npm reads the per-project .npmrc from the directory that OWNS package.json -- it does not
11+
# walk up. This root copy therefore does NOT cover plugins/model-apps/scripts/_vendor-build, which
12+
# has its own package.json and its own .npmrc (verified with `npm config get` from inside it). This
13+
# copy is the default for any future package.json added at the repo root.
14+
#
15+
# It likewise does NOT affect the other plugins: plugins/mobile-apps/template and the
16+
# plugins/power-pages/skills/create-site/assets/* packages each own a package.json, and
17+
# `npm config get omit-lockfile-registry-resolved` reads false in each. That is deliberate -- those
18+
# are templates scaffolded into USER projects, and forcing our lockfile policy onto a user's project
19+
# would be wrong. They also ship no lock file today. A repo-wide test
20+
# (plugins/model-apps/scripts/tests/run-tests.test.js) sweeps every committed package-lock.json and
21+
# fails if any records a non-public feed URL, so a lock added elsewhere is still covered.
22+
omit-lockfile-registry-resolved=true

AGENTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ claude --plugin-dir /path/to/plugins/<plugin-name>
4141

4242
No root-level build, lint, or test commands exist. Build/test tooling lives inside each plugin.
4343

44+
## CI
45+
46+
**Only two workflows run on every PR**`validate-keyword-case` and `validate-repository-metadata`.
47+
Both are repo-wide and enforce metadata/marketplace rules, not behavior.
48+
49+
**Every test workflow is path-filtered to a single plugin** (`power-pages``plugins/power-pages/**`;
50+
`model-apps``plugins/model-apps/**` + `evals/model-apps/**`). This is deliberate — a PR should not
51+
spend CI on a plugin it never touched — but it has a corollary: *a green PR does not mean the repo is
52+
green*, only that the paths you touched are.
53+
54+
**A test suite with no workflow silently never runs.** When you add tests to a plugin, add or extend
55+
that plugin's own path-filtered workflow in the same PR; do not widen another plugin's filter to
56+
cover yours.
57+
58+
A plugin that has adopted telemetry must also set its opt-out env var on any job that could execute a
59+
telemetry-emitting hook or script (see `## Shared Telemetry`) — e.g.
60+
`POWER_PLATFORM_SKILLS_TELEMETRY_MODEL_APPS_OPTOUT: "1"`. It suppresses transmission only, so it
61+
cannot change what a test asserts.
62+
4463
## Plugin Conventions
4564

4665
Each plugin follows this structure:
@@ -73,6 +92,8 @@ This keeps the skill discoverable in each plugin while preserving install-time p
7392

7493
Edit `shared/telemetry/` first, then refresh every adopting plugin's copied `scripts/lib/telemetry/lib` directory in the same change so the canonical source and bundled plugin content stay in sync.
7594

95+
`eventInfo` is a dynamic escape hatch whose nested keys are not enforced by `FIELD_TYPES`. Follow the approved schema in `shared/telemetry/README.md`; do not add nested fields or arbitrary payloads without privacy review and coordinated disclosure, schema, and test updates.
96+
7697
**Never reuse another plugin's instrumentation key or event stream.** When adopting telemetry in a new plugin, copy only the routing-agnostic library (`shared/telemetry/lib``plugins/<plugin>/scripts/lib/telemetry/lib`) — do **not** copy an existing adopter's real `ikey.json` (or its `resolver.js`). Each plugin's `ikey.json` carries that plugin's own instrumentation key(s), collector routing, and `event_stream_name`; start from the placeholder `shared/telemetry/ikey.json` (every region key is `PLACEHOLDER_REPLACE_BEFORE_SHIPPING` and it ships `disabled: true`) and provision a fresh, plugin-specific key before shipping. Copying a key already committed to another plugin (e.g. lifting `power-pages`'s `ikey.json` wholesale) mis-attributes the new plugin's events to the other plugin's Kusto stream and pollutes it — the copy step must bring over library code only, never another plugin's provisioned `ikey.json`/`resolver.js`.
7798

7899
This invariant is CI-enforced: `node scripts/validate-telemetry-ikeys.js` (wired into the `validate-repository-metadata` workflow) scans every `plugins/*/**/ikey.json`, ignores placeholder/empty values, and fails if the same instrumentation key or `event_stream_name` appears under two different plugins. A single plugin reusing one key across regions is allowed; only cross-plugin reuse fails. Run it locally after touching any plugin's `ikey.json`.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,6 @@ Any use of third-party trademarks or logos are subject to those third-party's po
269269
270270
## Telemetry
271271
272-
Plugins that ship 1DS telemetry (currently: `power-pages`) gather anonymous usage signals. Telemetry is default-on; users opt out per-plugin via the `/<plugin>:telemetry off` command (e.g. `/power-pages:telemetry off`), stored in `~/.power-platform-skills/config.json`. For automation/CI, each adopting plugin also honors a per-plugin opt-out environment variable `POWER_PLATFORM_SKILLS_TELEMETRY_<PLUGIN>_OPTOUT` (e.g. `POWER_PLATFORM_SKILLS_TELEMETRY_POWER_PAGES_OPTOUT=1`); when set it disables transmission with the highest precedence, overriding any `/<plugin>:telemetry` choice. See `shared/telemetry/README.md`.
272+
Power Pages and Model Apps ship 1DS telemetry code, but their committed configurations have different states. Power Pages telemetry is enabled and default-on; its usage events can include Dataverse organization and Entra tenant GUIDs when PAC is signed in, plus the signed-in user's Entra object ID when PAC exposes it. Model Apps ships hard-disabled (`disabled: true`), so it currently transmits no events and writes no local telemetry mirror. If Model Apps is enabled later, its events can include organization and tenant GUIDs but exclude the signed-in user's Entra object ID.
273+
274+
For an enabled plugin, users opt out of transmission via `/<plugin>:telemetry off` (for example, `/power-pages:telemetry off`), stored in `~/.power-platform-skills/config.json`. The local diagnostic mirror is still written after this transmission-only opt-out. Each adopting plugin also honors `POWER_PLATFORM_SKILLS_TELEMETRY_<PLUGIN>_OPTOUT` for automation and CI. The environment variable has highest precedence and disables transmission regardless of the saved command choice. See `shared/telemetry/README.md` for the full field list, kill-switch semantics, and local-mirror behavior.

evals/model-apps/app-builder/lib/fixture-loader.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ const path = require('node:path');
55
// Load each fixture subdir (named "<eval-id>-<slug>"); its app-spec.json is the graded input.
66
// Mirrors evals/model-apps/genpage/lib/fixture-loader.js, but the app-builder input is the App
77
// Spec (not .tsx). Fixture directories that lack an app-spec.json are silently skipped.
8+
//
9+
// Every failure below names the FIXTURE. A bare `JSON.parse` reports only a character offset
10+
// ("Expected double-quoted property name ... at position 25"), which tells an operator running a
11+
// corpus of fixtures nothing about which one to fix.
812
function loadFixtures(fixturesDir) {
913
if (!fs.existsSync(fixturesDir)) throw new Error(`Fixtures directory does not exist: ${fixturesDir}`);
1014
const entries = fs.readdirSync(fixturesDir, { withFileTypes: true })
@@ -17,7 +21,21 @@ function loadFixtures(fixturesDir) {
1721
const dir = path.join(fixturesDir, entry.name);
1822
const specPath = path.join(dir, 'app-spec.json');
1923
if (!fs.existsSync(specPath)) continue;
20-
fixtures.push({ id: parseInt(m[1], 10), dirName: entry.name, dir, spec: JSON.parse(fs.readFileSync(specPath, 'utf8')) });
24+
// Strip a UTF-8 BOM: editors on Windows add one by default and `JSON.parse` rejects the
25+
// leading \uFEFF, which failed the whole run for a file that is otherwise valid JSON.
26+
const raw = fs.readFileSync(specPath, 'utf8').replace(/^\uFEFF/, '');
27+
let spec;
28+
try {
29+
spec = JSON.parse(raw);
30+
} catch (e) {
31+
throw new Error(`Fixture ${entry.name}: ${specPath} is not valid JSON — ${e.message}`);
32+
}
33+
// `null`, `[]`, `"str"` and numbers all parse fine and were accepted as specs, so the real
34+
// problem surfaced much later as an opaque stage-facts error. An App Spec is a JSON object.
35+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
36+
throw new Error(`Fixture ${entry.name}: ${specPath} must contain a JSON object (an App Spec), got ${Array.isArray(spec) ? 'an array' : spec === null ? 'null' : typeof spec}`);
37+
}
38+
fixtures.push({ id: parseInt(m[1], 10), dirName: entry.name, dir, spec });
2139
}
2240
return fixtures;
2341
}

evals/model-apps/app-builder/run-app-builder.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,15 @@ const { TapReporter } = require('../genpage/lib/reporter.js');
1414
const { stageFacts } = require('./lib/facts.js');
1515
const { ASSERTIONS } = require('./lib/assertions.js');
1616

17+
// Known tiers, so a typo is rejected with the valid choices instead of silently matching no
18+
// fixture and reporting "no fixtures matched the filter" (which blames the fixtures, not the arg).
19+
// The parser is shared with the genpage runners — all three had the same silent-default defect.
20+
const { parseEvalArgs } = require('../lib/eval-args.js');
21+
1722
function parseArgs(argv) {
18-
const args = { fixtures: null, eval: null, tier: null };
19-
for (let i = 0; i < argv.length; i++) {
20-
const a = argv[i];
21-
if (a === '--fixtures') { args.fixtures = argv[++i]; }
22-
else if (a === '--eval') { args.eval = parseInt(argv[++i], 10); }
23-
else if (a === '--tier') { args.tier = argv[++i]; }
24-
else if (a === '--help' || a === '-h') {
25-
process.stdout.write('Usage: run-app-builder.js [--fixtures <dir>] [--eval <id>] [--tier <smoke|full>]\n');
26-
process.exit(0);
27-
} else {
28-
console.error(`Unknown argument: ${a}`);
29-
process.exit(2);
30-
}
31-
}
32-
return args;
23+
return parseEvalArgs(argv, {
24+
printHelp: () => process.stdout.write('Usage: run-app-builder.js [--fixtures <dir>] [--eval <id>] [--tier <smoke|full>]\n'),
25+
});
3326
}
3427

3528
function loadEvals() {

0 commit comments

Comments
 (0)