Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
- name: Lint
run: npm run lint

- name: Type-check tab
run: npm run typecheck

- name: Test
run: npm test

Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Publishes the extension to the Visual Studio Marketplace when a v* tag is
# pushed. Requires a one-time setup by a maintainer:
# Publishes the extension to the Visual Studio Marketplace. This is a DELIBERATE
# manual action (workflow_dispatch) — pushing a tag builds a GitHub Release with
# the .vsix (see release.yml) but does NOT publish to the Marketplace.
#
# Requires a one-time setup by a maintainer:
# 1. Bump "version" in package.json (the Marketplace rejects a version that
# already exists), commit, then tag: git tag v0.3.0 && git push --tags
# already exists) and commit it before running this.
# 2. Add a repository secret AZURE_DEVOPS_MARKETPLACE_PAT — a Marketplace
# "Manage" Personal Access Token for the `testingbot` publisher.
#
Expand All @@ -11,8 +14,6 @@
name: Publish

on:
push:
tags: [ 'v*' ]
workflow_dispatch:

permissions:
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Builds the .vsix and attaches it to a GitHub Release. This does NOT publish to
# the Visual Studio Marketplace — that is a separate, deliberate step
# (.github/workflows/publish.yml, run manually).
#
# Trigger by pushing a tag, e.g. `git tag v0.3.0 && git push origin v0.3.0`,
# or run manually from the Actions tab (workflow_dispatch).
name: Release

on:
push:
tags: [ 'v*' ]
workflow_dispatch:
Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the workflow file and any nearby release-related files.
git ls-files .github/workflows/release.yml .github/workflows || true

echo '--- release.yml ---'
cat -n .github/workflows/release.yml

echo '--- search for gh-release usage / tag_name / workflow_dispatch inputs ---'
rg -n "softprops/action-gh-release|tag_name|workflow_dispatch|inputs:|release" .github/workflows -S

Repository: testingbot/vso-testingbot-plugin

Length of output: 2506


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact workflow structure if the first pass is inconclusive.
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/release.yml')
print(p.read_text())
PY

Repository: testingbot/vso-testingbot-plugin

Length of output: 1148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the release workflow with line numbers and surrounding context for the gh-release step.
sed -n '1,220p' .github/workflows/release.yml

Repository: testingbot/vso-testingbot-plugin

Length of output: 1147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how workflow_dispatch behaves in GitHub Actions docs locally unavailable;
# instead inspect whether the workflow explicitly binds a ref for release creation.
rg -n "ref:|tag_name:|softprops/action-gh-release|workflow_dispatch" .github/workflows/release.yml -n -A4 -B4

Repository: testingbot/vso-testingbot-plugin

Length of output: 850


Provide an explicit tag for manual releases.
workflow_dispatch falls back to the triggering ref, so a manual run from a branch can create a release for that branch instead of a v* tag. Add a required dispatch input and pass it to tag_name (and checkout that ref for the build) so manual releases target the intended tag.

🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 9-9: truthy value should be one of [false, true]

(truthy)


[error] 11-11: too many spaces inside brackets

(brackets)


[error] 11-11: too many spaces inside brackets

(brackets)

🪛 zizmor (1.26.1)

[warning] 9-12: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting

(concurrency-limits)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 9 - 12, Add a required tag input
to the workflow_dispatch configuration, then use that input as the release
tag_name and checkout ref for manual runs. Keep push-triggered releases using
their existing tag ref, while ensuring manually triggered builds and releases
target the explicitly provided v* tag.


permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
Comment on lines +14 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Serialize release runs by ref.

Add a workflow-level concurrency group keyed by the workflow and ref, with cancel-in-progress: false, so repeated manual runs or tag retries cannot race while creating or updating the same draft release.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 15-15: overly broad permissions (excessive-permissions): contents: write is overly broad at the workflow level

(excessive-permissions)


[warning] 15-15: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)


[info] 18-18: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 14 - 19, Add workflow-level
concurrency configuration in release.yml keyed by the workflow name and ref, and
set cancel-in-progress to false. Place it alongside the top-level
permissions/jobs configuration so release runs for the same ref serialize
without canceling earlier runs.

Source: Linters/SAST tools

steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,120p' .github/workflows/release.yml | cat -n

Repository: testingbot/vso-testingbot-plugin

Length of output: 1434


Pin the release workflow actions to commit SHAs
actions/checkout@v4, actions/setup-node@v4, and softprops/action-gh-release@v2 are tag refs, not immutable pins. Because this workflow has contents: write, pin them to reviewed full-length commit SHAs before shipping release artifacts.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 21-21: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 21, Update the release workflow action
references for actions/checkout, actions/setup-node, and
softprops/action-gh-release from version tags to reviewed, full-length commit
SHA pins. Preserve each action’s current version and configuration while
ensuring all release actions are immutable.

Source: Linters/SAST tools

with:
persist-credentials: false

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install
run: npm ci

- name: Package
run: npm run package

- name: Create GitHub Release with the VSIX
uses: softprops/action-gh-release@v2
with:
files: Packages/*.vsix
draft: true
generate_release_notes: true
26 changes: 14 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,25 @@ service endpoint. The tab never sees raw credentials outside that attachment.
`package.json` has no `dependencies`, so nothing is installed into
`dist/tb-stop-tunnel`.

- **Web/tab code** (`tb-build-info/scripts/*.js`) runs in the browser inside Azure
DevOps and is bundled by webpack (`webpack.config.js`) with AMD output and
`@babel/preset-env`. The two entry points (`info`, `dialog`) emit to
`dist/tb-build-info/scripts/`. The host-provided SDK modules (`TFS/*`, `VSS/*`,
`react`) are declared as webpack **externals** (`externalsType: 'amd'`) — they
are resolved by the VSS module loader at runtime, not bundled. Add any new
host-provided module to the `externals` list in `webpack.config.js`, or the
bundle will try to inline it. `info.js` still uses `Buffer`, so the config
provides a `buffer` polyfill (removed when the tab is rewritten — see
`MODERNIZATION.md` Phase 3).
- **Web/tab code** (`tb-build-info/scripts/*.ts`) runs in the browser inside Azure
DevOps. It is TypeScript using `azure-devops-extension-sdk` (v4) +
`azure-devops-extension-api` (v5) — `SDK.init`/`SDK.ready`/`SDK.getService`,
`BuildRestClient` for attachments, `ServiceEndpointRestClient` for the data
source, `IHostPageLayoutService.openCustomDialog` for the embed dialog. webpack
(`webpack.config.js`) bundles it **self-contained** (the SDK/API and `md5` are
bundled in, not external) via `@babel/preset-env` + `@babel/preset-typescript`;
the two entry points (`info`, `dialog`) emit to `dist/tb-build-info/scripts/`.
`infoTab.html` / `embedDialog.html` load those bundles directly with a plain
`<script>` — there is no `VSS.SDK.js` and no AMD loader anymore. babel only
strips types, so `npm run typecheck` (`tb-build-info/tsconfig.json`) type-checks
the tab separately.

## webpack pipeline specifics

`webpack.config.js` does the bundle plus a `copy-webpack-plugin` pass that copies
the static files into `dist/`: `images`, `overview.md`, `vss-extension.json`, the
`tb-main` / `tb-stop-tunnel` folders, everything in `tb-build-info` **except**
`scripts/**` (those are bundled, not copied), and `VSS.SDK.js` into `dist/lib/`.
`tb-main` / `tb-stop-tunnel` folders, and everything in `tb-build-info` **except**
`scripts/**` (those are bundled, not copied).

`bin/upload_all.js` is a helper that uploads the `tb-*` build task definitions
directly to a collection via tfx-cli (separate from packaging the extension).
Expand Down
9 changes: 5 additions & 4 deletions MODERNIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ _Date: 2026-07-17. Applies to repo `vso-testingbot-plugin` (marketplace: `testin
> secret leaves the browser (finding #1) — **blocked**: the TestingBot public
> API exposes no endpoint that returns the `md5(key:secret:session_id)` share
> hash, so keeping the embedded `/mini` viewer requires the secret in the
> browser until TestingBot adds such an endpoint; (b) the `vss-web-extension-sdk`
> → `azure-devops-extension-sdk` / React port (large, and the old SDK keeps
> working, so deferred to its own PR).
> browser until TestingBot adds such an endpoint. (b) The SDK port is now
> **DONE (PR #21):** the tab is TypeScript on `azure-devops-extension-sdk` v4 +
> `azure-devops-extension-api` v5 (plain DOM, no jQuery/Buffer), bundled
> self-contained; `VSS.SDK.js` is gone. (React/`azure-devops-ui` was not needed.)
> - **Phase 4 — MOSTLY DONE (PR #20):** GitHub Actions build workflow (lint +
> tests + package on every PR), ESLint flat config wired up (`npm run lint`),
> CodeQL workflow fixed (v3 actions), dead Jenkinsfile removed, README usage
Expand Down Expand Up @@ -110,7 +111,7 @@ Goal: a results tab with no secrets in the browser, no CDN scripts, and a suppor
| Step | Effort | Detail / rationale |
|------|--------|--------------------|
| 3.1 Delete the unpkg script tag | **S** | `infoTab.html:5` — remove entirely; `window.TextDecoder` is native (its sole use is `info.js:303`). Do this immediately; it does not depend on the SDK migration. |
| 3.2 Port from `vss-web-extension-sdk` 1.106 to `azure-devops-extension-sdk` | **L** | Use sdk **v4.x** for now (api 5.275.0's peerDependency is `^2 \|\| ^3 \|\| ^4`, not yet v5) + `azure-devops-extension-api` 5.275.0 with subpath imports (`azure-devops-extension-api/Build`), optionally `azure-devops-ui` 2.276.0 (hard peer dep: React 16.8.x, not 18). Get build context via `SDK.getService(BuildServiceIds.BuildPageDataService).getBuildPageData()` — `SDK.getConfiguration()` is unreliable for `ms.vss-build-web.build-results-tab`. The old VSS.SDK keeps working meanwhile ("will continue to work indefinitely" per Microsoft), so this can trail Phases 1-2 without a flag day. |
| 3.2 Port from `vss-web-extension-sdk` 1.106 to `azure-devops-extension-sdk` | **L** | _(DONE — PR #21: sdk v4.2.0 + api 5.275.0, TypeScript, plain DOM, self-contained bundle; `BuildRestClient.getAttachments`, `BuildPageDataService`, `IHostPageLayoutService.openCustomDialog`. No React.)_ Use sdk **v4.x** for now (api 5.275.0's peerDependency is `^2 \|\| ^3 \|\| ^4`, not yet v5) + `azure-devops-extension-api` 5.275.0 with subpath imports (`azure-devops-extension-api/Build`), optionally `azure-devops-ui` 2.276.0 (hard peer dep: React 16.8.x, not 18). Get build context via `SDK.getService(BuildServiceIds.BuildPageDataService).getBuildPageData()` — `SDK.getConfiguration()` is unreliable for `ms.vss-build-web.build-results-tab`. The old VSS.SDK keeps working meanwhile ("will continue to work indefinitely" per Microsoft), so this can trail Phases 1-2 without a flag day. |
| 3.3 Remove all credential use from the browser | **M** | Drop the client-built Basic header (`info.js:200-203`) — `executeServiceEndpointRequest` proxies auth server-side via the endpoint's stored credential. Read only non-secret metadata from the (fixed) attachment. For per-test share links (`info.js:246`, md5(key:secret:session_id) — TestingBot's own scheme), add a service-endpoint data source or a TestingBot API endpoint that returns share URLs/hashes server-side; until then, degrade to linking testingbot.com directly rather than shipping the secret. Delete the 180-line inline MD5 implementation. |
| 3.4 Fix pagination + error UX | **S** | Fixed page size from the offset-0 response (`meta` envelope: `{data, meta:{offset,count,total}}`, count max 500); guard zero counts; render error states instead of eternal "LOADING"; try/catch the pagination click handler. |
| 3.5 Validate `embedDialog` URL | **S** | `new URL(params.url)` must be `https:` + host `testingbot.com`, else render nothing (`dialog.js:16-20`). |
Expand Down
39 changes: 11 additions & 28 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ export default tseslint.config(
'tb-*/index.js',
'tb-*/index.js.map',
'tb-*/tests/**/*.js',
'**/*.js.map',
// Third-party runtime, not ours to lint.
'lib/**'
'**/*.js.map'
]
},

// TypeScript task sources + their tests.
// TypeScript agent tasks (run on the build agent, Node) + their tests.
{
files: ['tb-*/**/*.ts'],
files: ['tb-main/**/*.ts', 'tb-stop-tunnel/**/*.ts'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
globals: { ...globals.node, ...globals.mocha }
Expand All @@ -33,37 +31,22 @@ export default tseslint.config(
}
},

// Node build scripts.
// TypeScript results-tab scripts (run in the browser via the extension SDK).
{
files: ['scripts/**/*.js', 'webpack.config.js'],
extends: [js.configs.recommended],
files: ['tb-build-info/**/*.ts'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
globals: globals.node,
sourceType: 'commonjs'
globals: globals.browser
}
},

// Legacy browser tab scripts (slated for the SDK rewrite). Lint for real
// errors only; the vendored MD5 implementation trips a lot of style rules.
// Node build scripts.
{
files: ['tb-build-info/scripts/**/*.js'],
files: ['scripts/**/*.js', 'webpack.config.js'],
extends: [js.configs.recommended],
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.browser,
VSS: 'readonly',
$: 'readonly',
jQuery: 'readonly',
Buffer: 'readonly'
}
},
rules: {
'no-unused-vars': 'off',
'no-redeclare': 'off',
'no-inner-declarations': 'off',
'no-empty': 'off',
'no-func-assign': 'off'
globals: globals.node,
sourceType: 'commonjs'
}
}
);
Loading
Loading