Repository: redhat-appstudio/rhtap-backstage-plugins
Pull request #7: feat(tekton): add B64 encoded annotation
Author: login: CryptoRodeo
Created: 2025-04-10T20:33:41Z | Merged: 2025-04-22T13:49:54Z

# Feature:
- Create a script that generates the `index.json` file similar to how `@janus-idp/cli` does and generate a base64 has from it.
- Create a Tekton task that generates the `io.backstage.dynamic-packages` annotation with the value being the base64 hash generated from the script. Export it to a file
- Use that file for setting the annotations


Diff:
diff --git a/.tekton/rhtap-backstage-plugins-pull-request.yaml b/.tekton/rhtap-backstage-plugins-pull-request.yaml
index e715e670..d06e6955 100644
--- a/.tekton/rhtap-backstage-plugins-pull-request.yaml
+++ b/.tekton/rhtap-backstage-plugins-pull-request.yaml
@@ -219,6 +219,32 @@ spec:
             workspace: git-auth
           - name: netrc
             workspace: netrc
+      - name: generate-annotations
+        params:
+          - name: ociStorage
+            value: $(params.output-image).script
+          - name: ociArtifactExpiresAfter
+            value: $(params.image-expires-after)
+          - name: SCRIPT_RUNNER_IMAGE
+            value: registry.redhat.io/rhel9/nodejs-20:latest
+          - name: SCRIPT
+            value: |
+              #!/bin/bash
+
+              node ./utils/generate-annotations.js
+          - name: SOURCE_ARTIFACT
+            value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT)
+        runAfter:
+          - prefetch-dependencies
+        taskRef:
+          params:
+            - name: name
+              value: run-script-oci-ta
+            - name: bundle
+              value: quay.io/konflux-ci/tekton-catalog/task-run-script-oci-ta:0.1@sha256:c0f627069353ebd6d1ed03c8657e281eaf11be63706ea38cc53caf16cf4ffd65
+            - name: kind
+              value: task
+          resolver: bundles
       - name: build-container
         params:
           - name: IMAGE
@@ -241,17 +267,19 @@ spec:
           - name: BUILD_ARGS_FILE
             value: $(params.build-args-file)
           - name: SOURCE_ARTIFACT
-            value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT)
+            value: $(tasks.generate-annotations.results.SCRIPT_ARTIFACT)
           - name: CACHI2_ARTIFACT
             value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT)
+          - name: ANNOTATIONS_FILE
+            value: "annotations.txt"
         runAfter:
-          - prefetch-dependencies
+          - generate-annotations
         taskRef:
           params:
             - name: name
               value: buildah-oci-ta
             - name: bundle
-              value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.4@sha256:ff54d822edc622ac35bad58dacc06063123f9a8cbc79f73913345c7c485430c4
+              value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.4@sha256:03be9d41b9617edc1436ae5a29cbd130f5101e5031d198f24c463672009754ac
             - name: kind
               value: task
           resolver: bundles
diff --git a/utils/README.md b/utils/README.md
new file mode 100644
index 00000000..c1d6a9be
--- /dev/null
+++ b/utils/README.md
@@ -0,0 +1,3 @@
+# Utils
+
+Utility scripts used for various purposes.
diff --git a/utils/generate-annotations.js b/utils/generate-annotations.js
new file mode 100644
index 00000000..c9b389a2
--- /dev/null
+++ b/utils/generate-annotations.js
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+// Find all package.json files recursively
+function findPackageJsonFiles(dir, ignoreDirs = ['node_modules', '.git']) {
+  let results = [];
+  const items = fs.readdirSync(dir);
+
+  for (const item of items) {
+    if (ignoreDirs.includes(item)) continue;
+
+    const itemPath = path.join(dir, item);
+    const stat = fs.statSync(itemPath);
+
+    if (stat.isDirectory()) {
+      results = results.concat(findPackageJsonFiles(itemPath, ignoreDirs));
+    } else if (stat.isFile() && item === 'package.json') {
+      results.push(itemPath);
+    }
+  }
+
+  return results;
+}
+
+// Transform a single package.json
+function transformPackageJson(inputJson) {
+  if (!inputJson.name || !inputJson.version || !inputJson.backstage) return null;
+
+  const dynamicKey = `${inputJson.name.replace('@', '').replace('/', '-')}-dynamic`;
+
+  return {
+    [dynamicKey]: {
+      name: inputJson.name,
+      version: inputJson.version,
+      backstage: inputJson.backstage,
+      homepage: inputJson.homepage || null,
+      repository: inputJson.repository || null,
+      license: inputJson.license || null,
+      author: inputJson.author || null,
+      bugs: inputJson.bugs || null,
+      keywords: inputJson.keywords || []
+    }
+  };
+}
+
+// Main function to process plugins directory
+function main() {
+  const pluginsDir = './plugins';
+  const outputFilePath = './temp.json';
+
+  try {
+    // Find all package.json files in plugins dir
+    const packageJsonFiles = findPackageJsonFiles(pluginsDir);
+    console.log(`Found ${packageJsonFiles.length} package.json files in ${pluginsDir}`);
+
+    // Process each file and combine results
+    const results = [];
+    let processedCount = 0;
+
+    for (const filePath of packageJsonFiles) {
+      try {
+        const content = fs.readFileSync(filePath, 'utf8');
+        const packageJson = JSON.parse(content);
+        const transformedObject = transformPackageJson(packageJson);
+
+        if (transformedObject) {
+          const key = Object.keys(transformedObject)[0];
+          results.push({ [key]: transformedObject[key] });
+          processedCount++;
+        }
+      } catch (err) {
+        console.error(`Error processing ${filePath}: ${err.message}`);
+      }
+    }
+
+    // Write the combined result
+    fs.writeFileSync(outputFilePath, JSON.stringify(results, null, null), 'utf8');
+    console.log(`Successfully processed ${processedCount} plugins and saved to ${outputFilePath}`);
+
+    // Base64 encode the contents, print it.
+    const jsonContent = fs.readFileSync(outputFilePath);
+    const hash = jsonContent.toString('base64');
+    // Delete temp json file
+    fs.unlinkSync(outputFilePath);
+    fs.writeFileSync('annotations.txt', `io.backstage.dynamic-packages=${hash}`);
+    // Output so it gets grabbed by next task.
+    console.log(hash);
+  } catch (err) {
+    console.error(`Error: ${err.message}`);
+    process.exit(1);
+  }
+}
+// Run the script
+main();


Reviews:
user: login: caugello
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T06:09:26Z
user: login: CryptoRodeo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:53:33Z
user: login: caugello
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T09:50:57Z
user: login: CryptoRodeo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:12:59Z
user: login: caugello
body: LGTM !
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-22T13:44:33Z

Review comments:
diff_hunk: @@ -0,0 +1,107 @@
+apiVersion: tekton.dev/v1
+kind: Task
+metadata:
+  name: apply-backstage-annotation
+spec:
+  description: |
+    Retrieves the OCI manifest for the RHTAP plugins, generates a Base64 hash from the contents in index.json 
+    and applies the annotation 'io.backstage.dynamic-packages' with the value being the hash.
+  params:
+    - name: IMAGE_URL
+      type: string
+      description: The URL of the image to extract index.json from
+    - name: IMAGE_DIGEST
+      type: string
+      description: The digest of the image
+  workspaces:
+    - name: manifest-data
+      description: Workspace for passing the new manifest.json file
+  steps:
+    - name: generate-backstage-annotation
+      image: registry.access.redhat.com/ubi8/ubi-minimal:latest
+      script: |
+        #!/bin/bash
+        set -e
+  
+        # Change to "$(params.IMAGE_URL)@$(params.IMAGE_DIGEST)"
+        TEST_IMAGE=quay.io/redhat-user-workloads/rhtap-shared-team-tenant/rhtap-backstage-plugins:on-pr-969d69a71f6f903bd16171b4d46330242f7b4a0d@sha256:df89d0bd76b606add3c419838e64c6c58d76448807b96ff70f59e2e66698c530
+        # Used for fetching manifest
+        # Change to "$(params.IMAGE_URL)"
+        TEST_IMAGE_NO_SHA=quay.io/redhat-user-workloads/rhtap-shared-team-tenant/rhtap-backstage-plugins:on-pr-969d69a71f6f903bd16171b4d46330242f7b4a0d
+
+        # Install tools
+        echo "Installing tools..."
+        microdnf install -y tar findutils curl jq gzip
+
+        # Create directory for extracted files
+        mkdir -p /tmp/extracted
+
+        # Install Oras
+        echo "Installing ORAS..."
+        # Docs: https://oras.land/docs/installation
+        VERSION="1.2.2"
+        curl -LO "https://github.com/oras-project/oras/releases/download/v${VERSION}/oras_${VERSION}_linux_amd64.tar.gz"
path: build/tasks/apply-backstage-annotation.yaml
user: login: caugello
body: Could you into maybe split the into more tasks by using already pre-built images from a tssc pipeline like you do on next step by using `konflux-ci/oras`. 
created_at: 2025-04-15T06:09:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,107 @@
+apiVersion: tekton.dev/v1
+kind: Task
+metadata:
+  name: apply-backstage-annotation
+spec:
+  description: |
+    Retrieves the OCI manifest for the RHTAP plugins, generates a Base64 hash from the contents in index.json 
+    and applies the annotation 'io.backstage.dynamic-packages' with the value being the hash.
+  params:
+    - name: IMAGE_URL
+      type: string
+      description: The URL of the image to extract index.json from
+    - name: IMAGE_DIGEST
+      type: string
+      description: The digest of the image
+  workspaces:
+    - name: manifest-data
+      description: Workspace for passing the new manifest.json file
+  steps:
+    - name: generate-backstage-annotation
+      image: registry.access.redhat.com/ubi8/ubi-minimal:latest
+      script: |
+        #!/bin/bash
+        set -e
+  
+        # Change to "$(params.IMAGE_URL)@$(params.IMAGE_DIGEST)"
+        TEST_IMAGE=quay.io/redhat-user-workloads/rhtap-shared-team-tenant/rhtap-backstage-plugins:on-pr-969d69a71f6f903bd16171b4d46330242f7b4a0d@sha256:df89d0bd76b606add3c419838e64c6c58d76448807b96ff70f59e2e66698c530
+        # Used for fetching manifest
+        # Change to "$(params.IMAGE_URL)"
+        TEST_IMAGE_NO_SHA=quay.io/redhat-user-workloads/rhtap-shared-team-tenant/rhtap-backstage-plugins:on-pr-969d69a71f6f903bd16171b4d46330242f7b4a0d
+
+        # Install tools
+        echo "Installing tools..."
+        microdnf install -y tar findutils curl jq gzip
+
+        # Create directory for extracted files
+        mkdir -p /tmp/extracted
+
+        # Install Oras
+        echo "Installing ORAS..."
+        # Docs: https://oras.land/docs/installation
+        VERSION="1.2.2"
+        curl -LO "https://github.com/oras-project/oras/releases/download/v${VERSION}/oras_${VERSION}_linux_amd64.tar.gz"
path: build/tasks/apply-backstage-annotation.yaml
user: login: CryptoRodeo
body: Yeah this can be shrunk down using other images. I think the only dependency I need besides `oras` is `tar` and `gzip` to get the container files.
created_at: 2025-04-15T12:53:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -217,6 +217,128 @@ spec:
             workspace: git-auth
           - name: netrc
             workspace: netrc
+      - name: generate-plugin-hash
+        params:
+          - name: ociStorage
+            value: $(params.output-image).script
+          - name: ociArtifactExpiresAfter
+            value: $(params.image-expires-after)
+          - name: SCRIPT_RUNNER_IMAGE
+            value: registry.redhat.io/rhel9/nodejs-20:latest
+          - name: SCRIPT
+            value: |
+              #!/usr/bin/env node
path: .tekton/rhtap-backstage-plugins-pull-request.yaml
user: login: caugello
body: If the repo content is mounted, we can have this in `utils/` or something.
created_at: 2025-04-16T09:50:57Z
author_association: COLLABORATOR
diff_hunk: @@ -217,6 +217,128 @@ spec:
             workspace: git-auth
           - name: netrc
             workspace: netrc
+      - name: generate-plugin-hash
+        params:
+          - name: ociStorage
+            value: $(params.output-image).script
+          - name: ociArtifactExpiresAfter
+            value: $(params.image-expires-after)
+          - name: SCRIPT_RUNNER_IMAGE
+            value: registry.redhat.io/rhel9/nodejs-20:latest
+          - name: SCRIPT
+            value: |
+              #!/usr/bin/env node
path: .tekton/rhtap-backstage-plugins-pull-request.yaml
user: login: CryptoRodeo
body: Moved to `utils`
created_at: 2025-04-22T13:12:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: CryptoRodeo
created_at: 2025-04-17T13:55:06Z
body: Waiting on https://github.com/konflux-ci/build-definitions/pull/2207 to get merged. Then we can update the task image to the actual one.
author_association: CONTRIBUTOR