Skip to content

release: 0.3.0 - #75

Merged
f-steff merged 37 commits into
mainfrom
develop
Oct 18, 2026
Merged

f-steff merged 37 commits into
mainfrom
develop

Conversation

@f-steff

@f-steff f-steff commented Oct 18, 2026

Copy link
Copy Markdown
Owner

First release from develop since 0.2.0, and the first stable publish that authenticates with the Microsoft Entra identity through GitHub OIDC - no PAT is stored.

Contents since 0.2.0

  • Permission-aware UI (D27) and the corrected permission probes (B10).
  • Private links, proposals and promotion (D28); the merged Links list with Status and a filter.
  • The add-to-Static bookmarklet: a private link in the Links hub, pinned to its project, with the info dialog (FR-LINK-8); TC-92 verified end to end.
  • The pending-proposal pill for callers who can manage the project settings (FR-LINK-9); TC-93.
  • Readable failures instead of [object Object] (B11).
  • overview.md renamed marketplace-advertisement.md.
  • Publishing moved to OIDC, and MARKETPLACE_PAT deleted.

Merged with a merge commit (a release must not rewrite SHAs); the push to main publishes both editions at 0.3.0.

f-steff and others added 30 commits October 14, 2026 20:24
Per the test-plan duty: the 0.2.0 block covers the automated gate, the release mechanics and process cases TC-79/TC-80/TC-81/TC-82, notes that a publish does not touch sharing, and lists the follow-up that only the stable edition records a commit status. B6 (a share run failed after a successful share) and B7 (the organization was printed through a step env block) are recorded as found and fixed. Docs only, so the publish workflows skip this push.
The dispatch-ref note still recommended syncing non-shipping workflow files to main with a housekeeping-only change. D26 forbids every route to main except a release, and that advice is exactly what caused B6: main carried share-dev.yml without the ci/ helpers it used, so a share run failed after a successful share. The note now states the two consequences: a workflow may depend only on files the released branch contains, and a report step must never fail the run that performed the action.
The project-settings page offered an Org.Settings button to everyone, and the Links hub told every member to add links under Project settings - wrong advice for a user who cannot open them. Both now follow a Security REST probe of the caller (D27): the permission bit is resolved from the namespace definition by action name, and a probe that cannot be evaluated counts as unknown, so the control stays visible rather than locking an administrator out. Adds unit tests for the response parsing and TC-83/TC-84; Q17 records that the negative path needs a non-administrator identity.
The Links hub could only show what an administrator had curated, so a member who wanted a bookmark had to ask an admin. Members can now keep private links and offer one to the project. A private link lives in the extension data service's User scope (one store per user per project), so the service does the isolation - not an owner filter we could get wrong. Share writes a frozen snapshot to the project's link-proposals; anyone who can open that project's settings may Add to project (the link is promoted, the project owns it) or Reject (the member keeps it and is told). A reviewer cannot write to another user's store, so the outcome travels back through the record and the owner's client applies it on its next visit: an accepted proposal removes the private original, a rejected one marks it until the next edit. The link form and its cascading builder move out of the settings page into common/link-editor so both surfaces share one editor, and private slugs are checked against project links too (a ?page= bookmark resolves project links first). Adds the AT-15/AT-16 unit tests and TC-85..TC-90; D28 and Q18 record the design and what still needs a second identity.
…share

Verifying the new private-link flow on the deployed 0.2.2 found the confirmation dialog unstyled in the Links hub: its overlay was position:static (so the question flowed inline) and the panel had no padding or shadow, because those rules lived only in admin.css while the hub links its own stylesheets. The dialog styles move to common/dialog.css, which both entry points link, and the settings page now links the shared editor stylesheet too so the duplicated blocks can leave admin.css (which sheds a third of its size). docs/13 gains the 0.2.2 results block, the B8 record and the corrected TC-90 expectation (proposals are project data, so they are in a backup; private links are not).
The 0.2.3 block records the B8 retest (the dialog now computes as a real overlay/panel) and that the settings page survived the stylesheet dedupe. The process note plus Q19 capture what happened on the way: the publish run failed while polling validation on a transient Marketplace error although the upload had succeeded, so the version was published untagged and every later publish of the branch failed with 'Version number must increase' until the tag was created by hand. Docs only, so no version is minted.
…on reports

A transient Marketplace failure while polling validation failed a publish run even though the upload had succeeded, which left 0.2.3 published but untagged - and because the tag is the version source of truth, every later publish of the branch then failed with 'Version number must increase' until the tag existed. scripts/tfx.js publish now passes --no-wait-validation so the tag depends only on the upload, a new validate command reports the asynchronous state, and both workflows write that report into the run summary (never gating, never annotating). D29 records the rule and Q19 moves to the resolved list.
… the log

The step wrote the Marketplace validation state into the run summary, which only exists in the web UI, so the D29 claim that the state is reported could not be verified from outside. It now echoes the same text to stdout; the step still never fails and adds no annotation.
Two tables repeated every column and pushed the private links - the only ones a member can act on - below the fold. The hub now shows one list: the same columns as the private table, with a Status of project, private or pending (a proposal awaiting a decision), Status and Actions pinned to the right edge and sized to their content, everything ordered alphabetically by title, and a filter box matching any substring of the title, slug or comment. The merge is a pure, tested function (mergedRows) so the ordering and the status rules are covered without a browser; a rejected link reads private again and its Share tooltip carries the outcome.
0.2.6 shipped the merged list but rendered it empty: linksSection() called renderRows() before the section was attached to the document, and renderRows() looks the table up by id - an unattached element cannot be found, so it returned early. The first paint's loading row is appended through a live reference, which is why only the loaded rows were missing, and typing in the filter worked (by then the table is attached). The rows are now rendered from renderList() after root.appendChild(page).
The 0.2.6/0.2.7 block records the merged list (columns, statuses, right-aligned content-sized Status/Actions, alphabetical order, all four filter behaviours) and B9: 0.2.6 rendered the table empty because renderRows() ran while the table was still detached and looks the tbody up by id. docs/10 also gains the process lesson behind three failing attempts at a pull request: a branch cut from a stale local develop re-applies merged commits, the pull request is conflicting, and GitHub runs no workflows for a conflicting pull request - so sync develop before branching, and check mergeable when checks never arrive.
The front page carried the whole development toolchain. It is now user-facing - what Static is, how to install it, what a pipeline can publish, why dynamic links matter, the documentation index, how to contribute (report bugs, suggest ideas, change code) and the licence - and everything developer-facing moved into docs/16-development-setup.md: prerequisites, build/test/run, packaging, the two editions, publishing (workflows, versioning, credentials, and that a Marketplace publish is a publisher action), and the development program with the exact enter/switch-back procedure. The dev edition is offered only to someone who reported a problem or asked for a feature, joins and unshares are requests to the developer, switching back needs no request, and only one edition can be installed at a time - which is why the backup/restore sequence exists. docs/06/07/08 gained the facts that had been asserted in the wrong place (pipeline identity under Authentication, one edition at a time in D23 and the backup warning).
Closing the link editor called setHash(""), which leaves the host URL ending in a bare '#': untidy
in the address bar and in anything that copies the URL.

The list view now marks itself with #list, which the existing hash handling already treats as the
list (only #add and #edit=<id> open the editor), so Back and reload behave exactly as before.
The Marketplace documents that a publishing service principal is added to a publisher by its own
Visual Studio profile id - not by the Entra client or principal id, and not by name - which is why the
Members page could not find the application. This dispatch-only workflow signs in with the federated
credential (no secret) and prints that id, so it can be pasted into the publisher's Members page. It is
deleted again once the identity is a member.
`workflow_dispatch` only works for a workflow that exists on the default branch, and `main` here is
reachable only through a release - so the helper could not be dispatched from `develop` at all.

It now also runs on a push that touches its own file, which is exactly how it is used: landing this
change triggers it once and prints the Marketplace profile id.
…lly evaluate

Testing with a second identity showed the permission probes never asked for a permission at all: the
Security REST API returns the namespace definition as `{ count, value: [ { ...actions... } ] }`, and
`findActionBit` looked for `actions` at the top level, found nothing, and reported **unknown**.

Because the published policy for an unknown probe is "keep the control visible", the whole
permission-aware feature was inert - fail-safe, but never doing its job: the Org.Settings button and
the "ask a project administrator" wording could not appear for anyone.

The parser now accepts both the wrapped response and the bare object the unit tests use, and a
regression test covers the wrapper (including empty/malformed wrappers, which stay unknown rather than
guessing).
Run with two identities in separate sessions: the publisher/owner and a second member.

- **TC-86 passes in both directions**: each identity sees only its own private link (the owner's
  `Alpha report`, `pending`, and the member's `Beta note`, `private`), neither sees the other's, and
  `?page=alpha-report` as the member resolves nothing. The user scope really does the isolation.
- **TC-91 and the cosmetic `#list` fix** re-verified on 0.2.8.
- **B10, found by this run**: the permission probes fetched the namespace definition and then never
  asked for a permission - the API wraps it as `{count, value:[{...actions...}]}` and `findActionBit`
  looked for `actions` at the top level. Because an unknown probe is published as "keep the control
  visible", the feature was inert without ever failing a test. Fixed in 0.2.9 and re-verified live.
- **TC-83/TC-84**: the positive halves are verified with both identities; the negative halves need an
  identity without project/collection admin rights, which neither test identity is - the exact steps
  are recorded with Q17/Q18.
… missing profile

The federated credential is fixed: the portal had left `{Organization ID}` / `{Repository ID}` as literal placeholders in the subject, so it could never match. With the real ids the OIDC login succeeds ("Azure CLI login succeeds by using OIDC" in the run log).

The next step then failed with `VSS011031: There is no profile for the authenticated user`: this identity has no Visual Studio profile yet, which is also why the publisher's Members page cannot find it by name.

The helper now prints the token's `oid` claim (the Entra object id, which needs no directory permission) so the identity can be added by id, and the profile lookup is informational only.
…C identity

The publishing workflows authenticated with a personal access token, which Azure DevOps retires on
2026-12-01. They now prefer a Microsoft Entra access token obtained with the GitHub OIDC token, so no
long-lived secret is needed.

- All three publishing workflows (dev, stable, share) gain `id-token: write` and run in the
  `marketplace` **environment**, whose subject the federated credential matches.
- A login step signs in as the identity (`azure/login`, with the client and tenant ids from repository
  variables, `allow-no-subscriptions`), then a token step asks for an **Azure DevOps resource token**
  (`499b84ac-1321-427f-aa17-267ca6975798`), masks it and writes it to `TFS_PAT` for the scripts.
- The **PAT remains a fallback for this change only**: if no OIDC token can be obtained and the
  `MARKETPLACE_PAT` secret exists, that is used, and the run summary says which one published. A run
  with neither fails with a clear message.
- `scripts/tfx.js` passes `--auth-type pat`, which is what tfx expects for a PAT *and* for an Entra
  token.

This merge publishes the dev edition, so the OIDC path runs immediately: the summary should read
"Publishing with a Microsoft Entra token (OIDC, no secret)". Once that is confirmed, the fallback and
the `MARKETPLACE_PAT` secret are removed in a follow-up.
…back

With a fallback in place a green run cannot prove which token published, so the fallback goes now that
the OIDC path has been shown to work ("Azure CLI login succeeds by using OIDC", dev 0.2.10).

- The token step obtains the Azure DevOps resource token unconditionally, masks it, writes it to
  `TFS_PAT`, prints the choice to the log as well as the summary, and **fails loudly** if no token can
  be obtained.
- The sign-in step is no longer conditional: there is nothing to fall back to.
- The header comment in `publish-dev.yml` no longer describes a PAT secret, and the `marketplace`
  identity's ids stay as (non-secret) environment variables.
- The temporary `marketplace-identity.yml` diagnostic served its purpose (it produced the profile id
  that got the identity into the publisher) and is deleted, along with the one-off scripts used for
  these edits.

The merge publishes the dev edition, which is the proof: with no PAT referenced anywhere, a successful
publish can only have used the Entra identity.
The publisher's Members page lists a service-principal member by its user id, and offers no rename, so
the entry looks hostile: `<Entra tenant id>\<service principal object id>` for the `ado-static-publisher`
app registration. The Credentials section now says what that string is, that it is an identifier rather
than a credential (publishing still needs the federated credential, which only this repository's
`marketplace` environment can present), and that the page cannot be made to show a friendlier name.
…d identity

Run with the second identity holding no administrator role (removed from Project Collection
Administrators, plain membership of the two test projects):

- **TC-83 negative**: the project probe answers `MANAGE_PROPERTIES = false`, and the hub says "Ask a
  project administrator to add project links under Project settings -> Static -> Links"; the owner sees
  the "Add a project link under ..." wording.
- **TC-84 negative**: the member can open that project's settings page - a member, not an administrator
  - the collection probe answers `GENERIC_WRITE = false`, and the **Org.Settings button is absent**;
  for the owner the probe is true and the button is present.
- **B10 re-verified**: every one of those answers came from a real `permissions/...` call.

Both cases are complete in both directions, so nothing is outstanding on the permission-aware UI. The
version history gains rows for 0.2.10 (Entra identity with the PAT fallback) and 0.2.11 (fallback
removed; no repository secret left).
…, FR-ADM-13)

Adding a link meant going to Project settings and pasting the URL. Now any Azure DevOps page can hand
its own URL to the form.

- The settings page accepts `?url=<page URL>` (the host query string, the same way the hubs read
  `?page=`): it opens the **Links** tab and hands the value to the shared editor, whose paste parser
  already matches the builder - for a build log URL that means the pipeline, build, view and
  stage/job/task target are pre-selected. Title, slug and comment stay the administrator's job.
- `acceptablePrefillUrl` (pure, tested) accepts only absolute `http(s)` URLs within a length cap, so a
  crafted query string cannot inject a `javascript:` or `data:` target into the form. The value is
  never logged and never stored unless the link is saved.
- `docs/07` documents the bookmarklet for the released edition and says to swap `f-steff.static` for
  `f-steff.static-dev` for the development edition; the same URL can be built by a wiki page or a
  pipeline that wants to offer "add this URL to Static" as an ordinary link.
- The About tab mentions the parameter, `docs/02` gains FR-ADM-13, and `docs/13` gains TC-92 with AT-18.
Verified on the deployed 0.2.12 with the owner account: from a real build page the bookmarklet's code
navigated to that project's settings page with `?url=<encoded page URL>`, which opened on the Links tab
with the URL field filled and the builder pre-selected through to the log target
(`Logs: Publish links`), and with title and slug left for the user. The same page with
`?url=javascript:alert(1)` left the form untouched, so the guard holds in practice as well as in the
unit test.
The TC-92 run used the development extension id, because the development edition is the one installed
in the test organization. Say so in the results entry and point at the released/dev note in docs/07, so
the record is unambiguous about what was actually exercised.
The bookmarklet was only documented as a code block, which is easy to get wrong - paste it into the
address bar and the browser strips the javascript: scheme.

- The About tab now shows it as a real link: right-click it and choose "Bookmark link", or drag it to
  the bookmarks bar. It is built by addLinkBookmarklet(publisherId, extensionId) from the SDK's
  extension context, so it is automatically correct for the released and the development edition
  instead of asking the user to edit an id.
- The generated code takes the organization and project from the page it is clicked on, keeps an
  already percent-encoded project segment encoded (no double encoding of a project name with a
  space), handles the legacy {org}.visualstudio.com host, and does nothing outside a project, where a
  link has no home.
- README gains a short "Add a link from the page you are on" section; docs/07 leads with the About
  link and keeps the text form for the released edition (with the one-line development change);
  FR-ADM-13 mentions the About link; TC-92 and AT-19 cover the builder, with a pinned string so the
  code block in the docs cannot drift from what the extension generates.
- D14 in docs/08 now records the per-project hub hiding idea as considered and not possible (hub
  groups and their hubs are static manifest contributions), so it is not implemented.
The settings page and the host's "failed to load" banner showed "[object Object]" when initialization
failed: an account outside the organization's directory gets TF400813, but the REST client rejects
with a TFS.WebApi.Exception that has no message of its own - the server's text sits inside
responseText - so describeError fell through to String(error).

- describeError now reads the JSON body's message, then falls back to the exception name and the HTTP
  status, and bounds a non-JSON body. The new unit test pins every branch.
- Every remaining ad-hoc "instanceof Error ? error.message : String(error)" site now uses it: the
  settings page (both the on-page error and notifyLoadFailed), the hubs, the Links hub, the link
  editor and dynamic-link resolution. The organization settings page already used it.
- AT-20 records the behaviour, and the next results block records B11 with the live evidence.
- AGENTS.md gains "Readable by a human first (mandatory)": plain statements, named values, one
  statement per line, meaningful names, one thing per function, and the same bar for tests, CI
  scripts, pipeline YAML and PowerShell. Correct but unreadable is not done.
- docs/13 records what 0.2.13 and 0.2.14 verified: the About-tab bookmarklet (edition-correct href,
  tooltip, grab cursor) and B11 (the "[object Object]" failure now names TF400813 on the page and in
  the host banner), plus the one outstanding step: the end-to-end run needs a member session.
`overview.md` in the repository root looked like documentation that duplicated README.md, but it is the
Marketplace listing text referenced by the manifest's content.details and packaged into the VSIX. The
new name says what it is, so it cannot be deleted as a stray document.

- Manifest: content.details.path now points at marketplace-advertisement.md.
- docs/03, docs/10 and TC-53 use the new name; the publish workflows' paths-ignore comment too.
- AGENTS.md records the file's role: never delete or rename it without updating the manifest.
- Verified by packaging: the generated manifest points at the new file and the VSIX contains it.
With the member identity signed in, the About tab's bookmarklet ran from a real build page and the
Links form arrived pre-filled down to the log target; the non-http(s) guard still holds. The older
NOTE row is replaced and the version rows for 0.2.13/0.2.15 are updated.
The About tab's bookmarklet pointed at the project settings page, so the form it pre-filled created a
project link - not what a member wants from "add this page". It now targets the Links hub, whose
`?url=` receiver opens this member's private-link editor (FR-LINK-8). The settings page keeps its own
`?url=` receiver for project links (FR-ADM-13), so both forms remain available; only the bookmarklet
is private-only.

- addLinkBookmarklet builds /_apps/hub/{publisher}.{extension}.static-links-hub?url=...
- links.ts reads ?url= after the hash handling, opens the private-link editor and parses the URL into
  the builder, so the same page-matching as the paste path applies.
- The About text, docs/07, README, docs/02 and docs/13 say which form each entry point opens.
- Tests: the bookmarklet tests pin the hub path (including the generated string); the project receiver
  keeps its coverage.
…n the right place

A page inside a project names its project, but a fixed link's page does not - and the old derivation
invented a project from whatever path it saw (on example.com/report it produced /report). The About tab
knows the project, so its link now embeds that project as a fallback:

- a page inside a project uses that project;
- any other page (external, or an organization page whose first segment starts with '_') uses the
  pinned project;
- the text-only bookmarklet in docs/07 has no pinned project and does nothing outside Azure DevOps.

The generated code only derives a project on dev.azure.com and {org}.visualstudio.com, so an external
path can never be mistaken for one. Tests cover the fixed-link case, another project, an organization
page, and a project name with a space.
… 0.2.16/0.2.17)

Records what the deployed builds verified: the bookmarklet now opens the Links hub's private-link form
(B12), the settings page keeps its project-link receiver, and the pinned project makes a fixed link
land in the right place - external page, another project, organization page.
…ands it out

Members could only get the bookmarklet from the settings page, which most of them cannot open, and
"bookmarklet" is jargon as a button label. The Links hub now shows a circled-i beside its subtitle:

- the dialog explains what the link does, offers the draggable "Add to Static" link (pinned to this
  project) and says the link stays private until it is shared;
- infoDialog is a new shared modal: title, caller-built body, one Close button, and both Escape and a
  click on the dimmed backdrop dismiss it;
- the About tabs no longer carry the bookmarklet (the hub is the members' entry point), and the chip
  styles that only the About page used are gone.
The hub already knew how many links are proposed for the project, but only the settings page showed
them. A caller who can open that page now sees a red pill beside the info button.

- It counts every proposal in the project, not only the caller's own.
- Clicking it opens Project settings -> Static -> Links, where the proposals are accepted or rejected.
- It is permission-aware: the probe hides it only when it says the caller cannot open the settings
  page, and an unknown answer keeps it visible (D27).
- The sidebar hub icon cannot change (hub groups are static manifest contributions), so the indicator
  lives in the hub header, next to the info button.
- pendingLabel keeps the wording in one place and is unit-tested.
…C-92/TC-93)

Verified on 0.2.18/0.2.19: the info button and its dialog (outside click and Escape both close it, the
link is pinned), the pill hidden for a member the probe says cannot manage the project, and the pill
rendering and click-through to the proposals with only the permission response stubbed. The member's
enabled Add/Reject buttons are recorded as an observation for D27.
…t section

The section is about the private-link bookmarklet; the settings-page alternative only added noise.
@f-steff
f-steff merged commit 92fdc1b into main Oct 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant