Skip to content

fix: resolve gadget version dynamically - #84

Open
SinghaAnirban005 wants to merge 3 commits into
inspektor-gadget:mainfrom
SinghaAnirban005:fix/version
Open

fix: resolve gadget version dynamically#84
SinghaAnirban005 wants to merge 3 commits into
inspektor-gadget:mainfrom
SinghaAnirban005:fix/version

Conversation

@SinghaAnirban005

Copy link
Copy Markdown
Contributor

fix: resolve gadget version dynamically instead of hardcoding to 1

The gadget version was hardcoded to 1 causing incorrect version information to be stored and displayed in the gadgets list.

fixes #83

Screencast.from.2026-03-26.00-29-18.webm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to fix incorrect gadget version information by resolving the gadget version dynamically from the provided image reference (tag) and, for :latest, querying ArtifactHub metadata instead of hardcoding the version to 1.

Changes:

  • Added helper utilities to detect latest, extract a semver from an image tag, and normalize gadget names.
  • Updated the custom gadget input flow to resolve and persist a version dynamically.
  • Updated ArtifactHub gadget “Add” flow to persist the ArtifactHub-provided version instead of 1, and added an ArtifactHub lookup for :latest images.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
src/gadgets/helper.ts Adds isLatestTag, extractVersionFromImage, and normalizeName helpers for version/name resolution.
src/gadgets/gadgetInput.tsx Resolves a version before persisting a custom gadget row (tag parse vs ArtifactHub lookup for latest).
src/gadgets/gadgetGrid.tsx Persists gadget.version from ArtifactHub into the stored gadget row instead of hardcoding 1.
src/api/artifacthub.tsx Adds fetchGadgetVersionFromArtifactHub to resolve a gadget version for latest images via ArtifactHub search.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 18 to 45
@@ -30,7 +41,7 @@ export function GadgetInput({ resource, onAddGadget }) {
isHeadless: undefined,
gadgetConfig: {
imageName: encodedImageURL,
version: 1,
version: version,
paramValues: {},

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

gadgetConfig.version is being changed from a numeric API version (previously hardcoded to 1) to a semver string resolved from the image/ArtifactHub. Elsewhere the code passes instance.gadgetConfig.version into ig.runGadget / ig.attachGadgetInstance as the protocol/version parameter (see src/gadgets/igSocket.tsx where GadgetParams.version is number), so storing a value like 0.50.1 here is likely to break gadget execution. Consider keeping gadgetConfig.version as the numeric IG API version (still 1), and store/display the container image/package version in a separate field (e.g., gadgetImageVersion) used only for UI.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +25
const handleRun = async () => {
let version: string;

if (isLatestTag(imageURL)) {
version = await fetchGadgetVersionFromArtifactHub(imageURL);
} else {
version = extractVersionFromImage(imageURL);
}

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

handleRun is now async and awaits a network call, but there is no error handling. If fetchGadgetVersionFromArtifactHub throws (network error, non-JSON response, etc.), this will result in an unhandled promise rejection and the gadget won't be added/navigated cleanly. Wrap the version-resolution logic in try/catch and surface a snackbar error (and avoid writing a partially-formed row to localStorage).

Copilot uses AI. Check for mistakes.
Comment thread src/gadgets/helper.ts
Comment on lines +28 to +37
export const isLatestTag = (imageURL: string) => {
const imageTag = imageURL.split(':')[1];

return imageTag === 'latest';
};

export function extractVersionFromImage(imageURL: string): string {
const tag = imageURL.split(':')[1];

if (!tag || tag === 'latest') return '1';

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

The new isLatestTag logic treats images without an explicit tag (e.g. ghcr.io/org/repo/image) as not latest and extractVersionFromImage will then fall back to '1'. In Docker/OCI semantics, an omitted tag typically implies :latest, so this will still store/display the wrong version for common inputs. Consider treating “no tag” as latest (or resolving via ArtifactHub) and/or returning an explicit “unknown” value instead of '1'.

Suggested change
export const isLatestTag = (imageURL: string) => {
const imageTag = imageURL.split(':')[1];
return imageTag === 'latest';
};
export function extractVersionFromImage(imageURL: string): string {
const tag = imageURL.split(':')[1];
if (!tag || tag === 'latest') return '1';
const getImageTag = (imageURL: string): string => {
const imageWithoutDigest = imageURL.split('@')[0];
const lastSlashIndex = imageWithoutDigest.lastIndexOf('/');
const lastColonIndex = imageWithoutDigest.lastIndexOf(':');
if (lastColonIndex > lastSlashIndex) {
return imageWithoutDigest.slice(lastColonIndex + 1);
}
return 'latest';
};
export const isLatestTag = (imageURL: string) => {
const imageTag = getImageTag(imageURL);
return imageTag === 'latest';
};
export function extractVersionFromImage(imageURL: string): string {
const tag = getImageTag(imageURL);
if (tag === 'latest') return '1';

Copilot uses AI. Check for mistakes.
Comment thread src/gadgets/helper.ts
Comment on lines +28 to +41
export const isLatestTag = (imageURL: string) => {
const imageTag = imageURL.split(':')[1];

return imageTag === 'latest';
};

export function extractVersionFromImage(imageURL: string): string {
const tag = imageURL.split(':')[1];

if (!tag || tag === 'latest') return '1';

const versionMatch = tag.match(/v?(\d+\.\d+\.\d+)/);

return versionMatch ? versionMatch[1] : '1';

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

isLatestTag / extractVersionFromImage use imageURL.split(':')[1] to find the tag, which breaks for valid image references that contain additional : characters (e.g. registries with ports like localhost:5000/repo:tag or digests like image@sha256:...). This can mis-detect latest and extract incorrect versions. Consider parsing the tag as the substring after the last : only if it occurs after the last / and before any @, or using a dedicated OCI/Docker image reference parser.

Copilot uses AI. Check for mistakes.
gadgetConfig: {
imageName: gadget.display_name?.split(' ').join('_'),
version: 1,
version: gadget.version,

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

This change stores gadget.version (ArtifactHub package version) into gadgetConfig.version. As with custom gadgets, other parts of the app treat gadgetConfig.version as the numeric IG API version when running/attaching gadgets, so persisting a semver string here can break runtime behavior. Consider keeping gadgetConfig.version as the IG protocol version (1) and persisting the ArtifactHub version separately for display.

Suggested change
version: gadget.version,
version: 1,
artifactHubVersion: gadget.version,

Copilot uses AI. Check for mistakes.
Comment thread src/api/artifacthub.tsx
Comment on lines +16 to +19
export async function fetchGadgetVersionFromArtifactHub(imageURL: string) {
const gadgetName = imageURL.split('/').pop()?.split(':')[0];
const normalizedImageName = normalizeName(gadgetName);

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

fetchGadgetVersionFromArtifactHub can throw at runtime when gadgetName is missing because normalizeName(gadgetName) calls toLowerCase() on a possibly-undefined value (e.g. empty input or trailing /). Add a guard/early return (or make normalizeName accept string | undefined and handle falsy values) before using gadgetName.

Copilot uses AI. Check for mistakes.
Comment thread src/api/artifacthub.tsx
Comment on lines +18 to +22
const normalizedImageName = normalizeName(gadgetName);

const response = await fetch(`${getServerURL()}/externalproxy`, {
headers: {
'Forward-To': `https://artifacthub.io/api/v1/packages/search?ts_query_web=${gadgetName}`,

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

The ArtifactHub query is built with ts_query_web=${gadgetName} without URL encoding. Gadget names can include characters that should be escaped (and if gadgetName is undefined it becomes the literal string undefined). Use encodeURIComponent and consider adding the same filters used in fetchInspektorGadgetFromArtifactHub (e.g. kind=22) to reduce false matches.

Suggested change
const normalizedImageName = normalizeName(gadgetName);
const response = await fetch(`${getServerURL()}/externalproxy`, {
headers: {
'Forward-To': `https://artifacthub.io/api/v1/packages/search?ts_query_web=${gadgetName}`,
if (!gadgetName) {
console.log('Gadget not found for', gadgetName);
return '1';
}
const normalizedImageName = normalizeName(gadgetName);
const encodedGadgetName = encodeURIComponent(gadgetName);
const response = await fetch(`${getServerURL()}/externalproxy`, {
headers: {
'Forward-To': `https://artifacthub.io/api/v1/packages/search?kind=22&ts_query_web=${encodedGadgetName}&official=true&facets=true&limit=${60}&offset=0`,

Copilot uses AI. Check for mistakes.
Comment thread src/api/artifacthub.tsx
Comment on lines +20 to +39
const response = await fetch(`${getServerURL()}/externalproxy`, {
headers: {
'Forward-To': `https://artifacthub.io/api/v1/packages/search?ts_query_web=${gadgetName}`,
},
});

const data = await response.json();

const gadget = data.packages.find(
g =>
normalizeName(g.normalized_name) === normalizedImageName ||
normalizeName(g.name) === normalizedImageName
);

if (!gadget) {
console.log('Gadget not found for', gadgetName);
return '1';
}

return gadget.version;

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

fetchGadgetVersionFromArtifactHub assumes a successful response and valid JSON. If the proxy returns a non-2xx status or a non-JSON error body, response.json() will throw and callers (e.g. GadgetInput) currently don't catch it. Check response.ok and add a try/catch to return a safe fallback while logging via console.error.

Suggested change
const response = await fetch(`${getServerURL()}/externalproxy`, {
headers: {
'Forward-To': `https://artifacthub.io/api/v1/packages/search?ts_query_web=${gadgetName}`,
},
});
const data = await response.json();
const gadget = data.packages.find(
g =>
normalizeName(g.normalized_name) === normalizedImageName ||
normalizeName(g.name) === normalizedImageName
);
if (!gadget) {
console.log('Gadget not found for', gadgetName);
return '1';
}
return gadget.version;
try {
const response = await fetch(`${getServerURL()}/externalproxy`, {
headers: {
'Forward-To': `https://artifacthub.io/api/v1/packages/search?ts_query_web=${gadgetName}`,
},
});
if (!response.ok) {
console.error('Failed to fetch gadget version from Artifact Hub', {
gadgetName,
status: response.status,
statusText: response.statusText,
});
return '1';
}
const data = await response.json();
const packages = Array.isArray(data?.packages) ? data.packages : [];
const gadget = packages.find(
g =>
normalizeName(g.normalized_name) === normalizedImageName ||
normalizeName(g.name) === normalizedImageName
);
if (!gadget) {
console.log('Gadget not found for', gadgetName);
return '1';
}
return gadget.version;
} catch (error) {
console.error('Error fetching gadget version from Artifact Hub', {
gadgetName,
error,
});
return '1';
}

Copilot uses AI. Check for mistakes.
Comment thread src/api/artifacthub.tsx
);

if (!gadget) {
console.log('Gadget not found for', gadgetName);

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

This introduces a console.log for a failure path. Elsewhere in the repo error paths use console.error (e.g. in src/gadgets/backgroundgadgets.tsx). Consider logging with console.error (or surfacing the error to the caller/UI) so failures are discoverable in production logs.

Suggested change
console.log('Gadget not found for', gadgetName);
console.error('Gadget not found for', gadgetName);

Copilot uses AI. Check for mistakes.
Comment thread src/gadgets/helper.ts
return 'http://localhost:4466';
}

export function normalizeName(name: string) {

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

normalizeName assumes name is always a non-empty string, but it is called with gadgetName derived from parsing the image URL which can be undefined. Consider making this helper resilient (accept string | undefined | null and return '' for falsy input) so callers don’t need to defensively guard every usage.

Suggested change
export function normalizeName(name: string) {
export function normalizeName(name: string | undefined | null) {
if (!name) {
return '';
}

Copilot uses AI. Check for mistakes.
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.

Incorrect gadget version shown

2 participants