Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
625f2ff
fix: Duplicate include line scoping (#176)
X-Guardian Jun 17, 2026
622ff97
fix: revalidate on edit (#178)
X-Guardian Jun 17, 2026
ea531b1
chore(release): 0.13.0 [skip ci]
github-actions[bot] Jun 17, 2026
fc0ec6a
fix: diff diagnostics (#180)
X-Guardian Jun 17, 2026
186eef9
chore(release): 0.13.1 [skip ci]
github-actions[bot] Jun 17, 2026
d32b1e6
docs(ci): document dev-scope security-alert policy in dependabot.yml …
eFAILution Jun 18, 2026
d522fe4
fix: recognise braced ${VAR} GitLab variables in component URLs (#182)
eFAILution Jun 18, 2026
ea37aac
chore(release): 0.13.2 [skip ci]
github-actions[bot] Jun 18, 2026
bd7cf97
Merge branch 'main' into beta
eFAILution Jun 22, 2026
a6c7626
fix(deps): pin @types/vscode to ^1.120.0 to match engines.vscode
eFAILution Jun 22, 2026
8b3fda8
ci(dependabot): stop @types/vscode bumps past the engines.vscode floor
eFAILution Jun 22, 2026
aab907d
chore(release): 0.13.3 [skip ci]
github-actions[bot] Jun 22, 2026
f922edb
fix: input completions offered outside the input-name slot (#185)
X-Guardian Jun 22, 2026
71ab902
chore(release): 0.13.4 [skip ci]
github-actions[bot] Jun 22, 2026
a7c4026
fix: re-requesting input suggestions after typing part of a new input…
X-Guardian Jun 24, 2026
4953f46
chore(release): 0.13.5 [skip ci]
github-actions[bot] Jun 24, 2026
b4e5b60
Merge branch 'main' into beta
eFAILution Jun 24, 2026
a5641fc
chore(release): 0.13.6 [skip ci]
github-actions[bot] Jun 24, 2026
60af4ec
feat(version-check): detect outdated component versions (#194)
eFAILution Jun 24, 2026
cdea22d
chore(release): 0.13.7 [skip ci]
github-actions[bot] Jun 24, 2026
54aa5fd
fix(version-check): expand GitLab variables before fetching versions …
eFAILution Jun 25, 2026
14bce5f
chore(release): 0.13.8 [skip ci]
github-actions[bot] Jun 25, 2026
8ce9d9b
docs: refresh, slim, and split the README (#196)
eFAILution Jun 26, 2026
ed0f034
chore(release): 0.13.9 [skip ci]
github-actions[bot] Jun 26, 2026
2cad09f
fix: Improve expired token user experience (#198)
X-Guardian Jun 26, 2026
6ab563b
chore(release): 0.13.10 [skip ci]
github-actions[bot] Jun 26, 2026
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
21 changes: 17 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Dependabot version updates.
#
# Security-alert policy (handled OUTSIDE this file): dev-scoped npm vulnerabilities are not gated.
# They are auto-dismissed by a Dependabot auto-triage rule (repo Settings -> Code security ->
# Dependabot -> Auto-triage rules: scope = development -> dismiss), mirroring the dev-group override
# in osv-scanner.toml that the Argus scan honors. Do NOT add a dev-dep `ignore:` here to suppress
# those alerts -- `ignore:` also stops the version-update PRs below, which are how dev tools stay
# current and how a transitive fix (e.g. serialize-javascript >= 7.0.5) actually lands.
version: 2
updates:
# Maintain npm dependencies
Expand Down Expand Up @@ -31,10 +39,15 @@ updates:
update-types:
- "minor"
- "patch"
# Ignore specific packages if needed
# ignore:
# - dependency-name: "package-name"
# versions: ["x.x.x"]
# @types/vscode is pinned to the engines.vscode floor (>=1.120.0): `vsce package`
# fails when @types/vscode > engines.vscode. Allow only patch bumps (which stay
# within the floor); a minor/major bump must be a deliberate engines.vscode raise,
# not an automatic dependency update. This stops the bump, not any security alert.
ignore:
- dependency-name: "@types/vscode"
update-types:
- "version-update:semver-minor"
- "version-update:semver-major"

# Maintain GitHub Actions
- package-ecosystem: "github-actions"
Expand Down
390 changes: 92 additions & 298 deletions README.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Extension API

> **Status: not yet exposed.** `activate()` does not currently return this API, so `getExtension(...).activate()` resolves to `undefined`. This documents the *intended* contract for other extensions to consume; track its implementation before depending on it. See the [README](../README.md) for user-facing features.

## Intended interface

```typescript
interface GitLabComponentAPI {
getComponentList(): Promise<Component[]>;
getComponentDetails(name: string, version?: string): Promise<ComponentDetails>;
validateComponent(component: Component): ValidationResult;
expandGitLabVariables(text: string, context?: VariableContext): string;
openComponentBrowser(context?: ComponentContext): Promise<void>;
}

interface Component {
name: string;
description: string;
parameters: ComponentParameter[];
version?: string;
source?: string;
gitlabInstance?: string;
sourcePath?: string;
availableVersions?: string[];
originalUrl?: string;
}

interface ComponentParameter {
name: string;
description?: string;
required: boolean;
type?: string;
default?: unknown;
}
```

## Intended usage

```typescript
const api = await vscode.extensions.getExtension('eFAILution.gitlab-component-helper')?.activate();
if (api) {
const components = await api.getComponentList();
// Use components...
}
```
53 changes: 53 additions & 0 deletions docs/discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Component Discovery

> Advanced configuration for how the extension scans a source repository for components. Most users need none of this — see the [README](../README.md) for the basics.

By default the extension follows the [GitLab CI Components spec](https://docs.gitlab.com/ci/components/#directory-structure) when scanning a source repository: it looks for templates in `templates/` and one subdirectory level deep, matching `*.yml` and `*.yaml`. **No configuration is required for spec-compliant repos.**

For repositories that pre-date the spec, use a custom layout, or store templates outside `templates/`, override discovery either globally or per source.

## Global defaults

```jsonc
"gitlabComponentHelper.discovery.templateRoots": ["templates", "ci/components"],
"gitlabComponentHelper.discovery.maxDepth": 2,
"gitlabComponentHelper.discovery.filePatterns": ["*.yml", "*.yaml"],
"gitlabComponentHelper.discovery.templateFileNames": ["template.yml", "template.yaml"]
```

These four settings are also editable from the **VS Code Settings UI** (search for "GitLab Component Helper Discovery").

## Per-source override

Need different rules for one repository? Add a `discovery` block to that source — its values override the global defaults for that source only.

```jsonc
"gitlabComponentHelper.componentSources": [
{
"name": "Standard CI Components",
"path": "components/opentofu",
"gitlabInstance": "gitlab.com"
// uses global discovery defaults
},
{
"name": "Legacy Internal Components",
"path": "infra/legacy-ci",
"gitlabInstance": "gitlab.company.com",
"discovery": {
"templateRoots": ["ci/components", "shared/pipelines"],
"maxDepth": 2
}
}
]
```

## Limits

To keep the extension fast and predictable:

| Field | Limit |
|---|---|
| `templateRoots` | Up to 5 roots per source |
| `maxDepth` | 0–3 (0 = root only, 1 = one subdirectory level, the spec default) |
| `filePatterns` | Filename globs only — no path globs (e.g. `*.yml` ✅, `foo/*.yml` ❌) |
| `templateFileNames` | Filenames only — no slashes |
39 changes: 39 additions & 0 deletions docs/monorepo-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Monorepo Tag Conventions

> How to scope per-component versions in a tag-per-component monorepo. Skip this for ordinary single-component repos — their tags are listed as-is. See the [README](../README.md) for the basics.

When a single repository holds **many components**, each component is usually released under its own tags that embed the component name — e.g. `deploy-app-1.1.0`, `deploy-app-2`, `build-image-4.0.0`. Without any hint, the version dropdown for *every* component would list *every* tag in the repo.

Set a **tag pattern** on the source to tell the extension how tags map to components. Each component's dropdown is then scoped to its own tags, and labels are shown without the prefix (e.g. `1.1.0`, not `deploy-app-1.1.0`). The full tag is still what gets inserted, so the GitLab include resolves correctly.

```jsonc
"gitlabComponentHelper.componentSources": [
{
"name": "Shared CI Monorepo",
"path": "infrastructure/shared-ci",
"gitlabInstance": "gitlab.com",
"tagPattern": "{name}-{version}"
}
]
```

The template uses two tokens:

| Token | Meaning |
|---|---|
| `{name}` | The component (= `templates/` directory) name. |
| `{version}` | The version shown in the dropdown. Matches anything starting with a digit. |

Everything else in the pattern is literal text, so other conventions work too:

| Tag style | Pattern |
|---|---|
| `deploy-app-1.1.0` | `{name}-{version}` |
| `apps/web/v2.0.0` | `apps/{name}/v{version}` |
| `web_1.0.0` | `{name}_{version}` |

> **Sibling names:** because `{version}` must start with a digit, a component named `build-image` won't pick up a sibling's `build-image-extra-1.0.0` tags. If you need pre-release-only tags with no leading digit (e.g. `web-rc1`), write a stricter custom pattern for that source.

Leave `tagPattern` unset for ordinary single-component repos.

> **Note:** the [version-check feature](../README.md#-stay-on-the-latest-version) only compares clean `X.Y.Z` refs, so components pinned to a full monorepo tag (e.g. `deploy-app@deploy-app-1.1.0`) are not currently flagged as outdated. Scoped monorepo comparison is planned.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 25 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "gitlab-component-helper",
"displayName": "GitLab Component Helper",
"description": "Provides intellisense for GitLab CI components",
"version": "0.12.2",
"version": "0.13.10",
"icon": "images/icon.png",
"engines": {
"node": ">=22.0.0",
Expand Down Expand Up @@ -220,6 +220,20 @@
},
"default": [],
"description": "Extra GitLab CI file globs, merged with the built-in defaults. Patterns match at any directory depth (e.g. 'ci/*.yml' is treated as '**/ci/*.yml')."
},
"gitlabComponentHelper.versionCheck.enabled": {
"type": "boolean",
"default": true,
"description": "Warn when a component pinned to a semantic version (X.Y.Z) has a newer stable release available. Checked when a GitLab CI file is opened or saved."
},
"gitlabComponentHelper.versionCheck.severity": {
"type": "string",
"enum": [
"warning",
"information"
],
"default": "warning",
"description": "Severity of the 'newer component version available' diagnostic."
}
}
},
Expand Down Expand Up @@ -259,6 +273,10 @@
{
"command": "gitlab-component-helper.showPerformanceStats",
"title": "GitLab CI: Show Performance Statistics"
},
{
"command": "gitlab-component-helper.updateAllComponentVersions",
"title": "GitLab CI: Update All Component Versions to Latest"
}
],
"menus": {
Expand All @@ -267,6 +285,11 @@
"when": "gitlabComponentHelper.isCiFile",
"command": "gitlab-component-helper.browseComponents",
"group": "navigation"
},
{
"when": "gitlabComponentHelper.isCiFile",
"command": "gitlab-component-helper.updateAllComponentVersions",
"group": "navigation"
}
]
}
Expand Down Expand Up @@ -313,7 +336,7 @@
"@types/js-yaml": "^4.0.9",
"@types/mocha": "^10.0.10",
"@types/node": "^26.0.0",
"@types/vscode": "^1.125.0",
"@types/vscode": "^1.120.0",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.61.1",
"@vscode/test-electron": "^3.0.0",
Expand Down
48 changes: 48 additions & 0 deletions src/errors/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Runtime predicates over caught error values.
*
* These narrow the error classes declared in `./types` — kept separate so `types.ts` stays purely
* declarative (enum, classes, type aliases) and behaviour lives here.
*/

import { ErrorCode, GitLabComponentError, NetworkError } from './types';

/**
* Extract an HTTP status code from an unknown thrown value.
*
* Prefers the typed `NetworkError.details.statusCode`, then falls back to a `statusCode` property on
* any error-shaped object so non-`NetworkError` throws (e.g. raw fetch errors) are still recognised.
*
* @param error The caught value (typed `unknown` at catch sites).
* @returns The HTTP status code if one can be safely extracted, otherwise `undefined`.
*/
export function extractStatusCode(error: unknown): number | undefined {
if (error instanceof NetworkError && error.details?.statusCode) {
return error.details.statusCode;
}
if (typeof error === 'object' && error !== null && 'statusCode' in error) {
const candidate = (error as { statusCode: unknown }).statusCode;
if (typeof candidate === 'number') {
return candidate;
}
}
return undefined;
}

/**
* Whether a caught value represents a GitLab authentication failure (expired/invalid/missing token).
*
* Recognises both the typed `UNAUTHORIZED` error code and a raw 401/403 status, so callers don't have
* to special-case how deep in the stack the error was constructed.
*
* @param error The caught value (typed `unknown` at catch sites).
* @returns `true` if the error is an `UNAUTHORIZED` GitLab error or carries a 401/403 status,
* otherwise `false`.
*/
export function isAuthError(error: unknown): boolean {
if (error instanceof GitLabComponentError && error.code === ErrorCode.UNAUTHORIZED) {
return true;
}
const status = extractStatusCode(error);
return status === 401 || status === 403;
}
5 changes: 5 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export {
ConfigurationError
} from './types';

export {
extractStatusCode,
isAuthError
} from './guards';

export {
ErrorHandler,
ErrorHandlerOptions,
Expand Down
26 changes: 26 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export function activate(context: vscode.ExtensionContext) {
logger.debug('[Extension] Registering addProjectToken command...', 'Extension');
const service = getComponentService();
service.setSecretStorage(context.secrets);
context.subscriptions.push(service);
registerAddProjectTokenCommand(context, service);

// Register component browser command
Expand Down Expand Up @@ -618,6 +619,31 @@ export function activate(context: vscode.ExtensionContext) {
// Initialize the validation provider
const validationProvider = new ValidationProvider(context);

// Register command to update every outdated component in the active file to its latest stable version.
logger.debug('[Extension] Registering updateAllComponentVersions command...', 'Extension');
context.subscriptions.push(
vscode.commands.registerCommand('gitlab-component-helper.updateAllComponentVersions', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('Open a GitLab CI file to update component versions.');
return;
}
try {
const updated = await validationProvider.updateAllComponentVersions(editor.document);
if (updated === 0) {
vscode.window.showInformationMessage('All components are already on their latest version.');
} else {
vscode.window.showInformationMessage(
`Updated ${updated} component${updated === 1 ? '' : 's'} to the latest version.`
);
}
} catch (error) {
logger.error(`[Extension] Failed to update component versions: ${error}`, 'Extension');
vscode.window.showErrorMessage(`Failed to update component versions: ${error}`);
}
})
);

// Keep the `gitlabComponentHelper.isCiFile` context key in sync with `isGitLabCIFile` so the editor context menu
// shows the Browse Components command on exactly the same files the providers activate on.
const updateCiFileContext = (editor: vscode.TextEditor | undefined): void => {
Expand Down
Loading
Loading