From 31ba739b9f1e1edd9b3e5614bb1ed1ec8e9401e8 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Tue, 14 Jul 2026 15:38:35 +0200 Subject: [PATCH] chore: require OCI references for dynamicArtifact in metadata validation The dynamicArtifact field is now required and must start with oci://ghcr.io. Metadata validation emits an error for missing or non-OCI values, and the update-overlay script fails hard if it encounters invalid state during PR creation. Refs: RHIDP-15484, RHDHPLAN-934, RHDHPLAN-256 --- .github/workflows/test-validate-metadata.yaml | 88 +++++++++++++++++++ update-overlay/create-pr-if-necessary.js | 28 +++--- validate-metadata/README.md | 6 +- .../test-missing-dynamic-artifact.yaml | 11 +++ .../plugins-list.yaml | 1 + .../metadata/test-wrapper-path.yaml | 12 +++ .../cases/fail-wrapper-path/plugins-list.yaml | 1 + .../cases/pass/metadata/test-backend.yaml | 12 --- .../test/cases/pass/plugins-list.yaml | 1 - validate-metadata/validate-metadata.ts | 19 +++- 10 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 validate-metadata/test/cases/fail-missing-dynamic-artifact/metadata/test-missing-dynamic-artifact.yaml create mode 100644 validate-metadata/test/cases/fail-missing-dynamic-artifact/plugins-list.yaml create mode 100644 validate-metadata/test/cases/fail-wrapper-path/metadata/test-wrapper-path.yaml create mode 100644 validate-metadata/test/cases/fail-wrapper-path/plugins-list.yaml delete mode 100644 validate-metadata/test/cases/pass/metadata/test-backend.yaml diff --git a/.github/workflows/test-validate-metadata.yaml b/.github/workflows/test-validate-metadata.yaml index cf478b3..b7b49ec 100644 --- a/.github/workflows/test-validate-metadata.yaml +++ b/.github/workflows/test-validate-metadata.yaml @@ -550,3 +550,91 @@ jobs: assert.equal(VALIDATION_PASSED, 'true'); assert.equal(VALIDATION_ERROR_COUNT, '0'); assert.equal(VALIDATION_ERRORS, '[]'); + + test-validation-fail-wrapper-path: + name: Test Validation (Wrapper Path Rejected) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js 24.x + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24.x + + - name: Run Validate Metadata + id: validate + continue-on-error: true + uses: ./validate-metadata + with: + overlay-root: ${{ env.TEST_DIR }}/cases/fail-wrapper-path + plugins-root: ${{ env.TEST_DIR }}/source + target-backstage-version: ${{ env.TARGET_BACKSTAGE_VERSION }} + image-repository-prefix: ${{ env.IMAGE_REPOSITORY_PREFIX }} + + - name: Verify validation failed with dynamicArtifact error + env: + STEP_OUTCOME: ${{ steps.validate.outcome }} + VALIDATION_PASSED: ${{ steps.validate.outputs.validation-passed }} + VALIDATION_ERROR_COUNT: ${{ steps.validate.outputs.validation-error-count }} + VALIDATION_ERRORS: ${{ steps.validate.outputs.validation-errors }} + shell: node {0} + run: | + import assert from 'node:assert/strict'; + const { STEP_OUTCOME, VALIDATION_PASSED, VALIDATION_ERROR_COUNT, VALIDATION_ERRORS } = process.env; + const errors = JSON.parse(VALIDATION_ERRORS); + + assert.equal(STEP_OUTCOME, 'failure'); + assert.equal(VALIDATION_PASSED, 'false'); + assert.ok(Number(VALIDATION_ERROR_COUNT) >= 1); + + const dynamicArtifactError = errors.find(e => e.field === 'dynamicArtifact'); + assert.ok(dynamicArtifactError, 'Expected a dynamicArtifact error'); + assert.equal(dynamicArtifactError.kind, 'mismatch'); + assert.equal(dynamicArtifactError.file, 'test-wrapper-path.yaml'); + assert.match(dynamicArtifactError.message, /must be an OCI reference/); + + test-validation-fail-missing-dynamic-artifact: + name: Test Validation (Missing dynamicArtifact) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js 24.x + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24.x + + - name: Run Validate Metadata + id: validate + continue-on-error: true + uses: ./validate-metadata + with: + overlay-root: ${{ env.TEST_DIR }}/cases/fail-missing-dynamic-artifact + plugins-root: ${{ env.TEST_DIR }}/source + target-backstage-version: ${{ env.TARGET_BACKSTAGE_VERSION }} + image-repository-prefix: ${{ env.IMAGE_REPOSITORY_PREFIX }} + + - name: Verify validation failed with missing dynamicArtifact error + env: + STEP_OUTCOME: ${{ steps.validate.outcome }} + VALIDATION_PASSED: ${{ steps.validate.outputs.validation-passed }} + VALIDATION_ERROR_COUNT: ${{ steps.validate.outputs.validation-error-count }} + VALIDATION_ERRORS: ${{ steps.validate.outputs.validation-errors }} + shell: node {0} + run: | + import assert from 'node:assert/strict'; + const { STEP_OUTCOME, VALIDATION_PASSED, VALIDATION_ERROR_COUNT, VALIDATION_ERRORS } = process.env; + const errors = JSON.parse(VALIDATION_ERRORS); + + assert.equal(STEP_OUTCOME, 'failure'); + assert.equal(VALIDATION_PASSED, 'false'); + assert.ok(Number(VALIDATION_ERROR_COUNT) >= 1); + + const dynamicArtifactError = errors.find(e => e.field === 'dynamicArtifact'); + assert.ok(dynamicArtifactError, 'Expected a dynamicArtifact missing-field error'); + assert.equal(dynamicArtifactError.kind, 'missing-field'); + assert.equal(dynamicArtifactError.file, 'test-missing-dynamic-artifact.yaml'); + assert.equal(dynamicArtifactError.message, 'Missing required field: dynamicArtifact'); diff --git a/update-overlay/create-pr-if-necessary.js b/update-overlay/create-pr-if-necessary.js index fa07b08..54efbab 100644 --- a/update-overlay/create-pr-if-necessary.js +++ b/update-overlay/create-pr-if-necessary.js @@ -198,17 +198,23 @@ module.exports = async ({github, context, core}) => { } const dynamicArtifact = doc.getIn(['spec', 'dynamicArtifact']); - if (typeof dynamicArtifact === 'string') { - const ociMatch = dynamicArtifact.match(ociGhcrTagPattern); - if (ociMatch) { - const newTag = `bs_${targetBackstageVersion}__${newVersion}`; - const newDynamicArtifact = `${ociMatch[1]}${newTag}${ociMatch[2] ?? ''}`; - if (newDynamicArtifact !== dynamicArtifact) { - doc.setIn(['spec', 'dynamicArtifact'], newDynamicArtifact); - core.info(` Updated dynamicArtifact tag in ${entry.name}`); - modified = true; - } - } + if (typeof dynamicArtifact !== 'string') { + core.setFailed(`${entry.name}: missing required field "dynamicArtifact"`); + return null; + } + const ociMatch = dynamicArtifact.match(ociGhcrTagPattern); + if (!ociMatch) { + core.setFailed( + `${entry.name}: dynamicArtifact must be an OCI reference starting with "oci://ghcr.io", got "${dynamicArtifact}"` + ); + return null; + } + const newTag = `bs_${targetBackstageVersion}__${newVersion}`; + const newDynamicArtifact = `${ociMatch[1]}${newTag}${ociMatch[2] ?? ''}`; + if (newDynamicArtifact !== dynamicArtifact) { + doc.setIn(['spec', 'dynamicArtifact'], newDynamicArtifact); + core.info(` Updated dynamicArtifact tag in ${entry.name}`); + modified = true; } const currentSupportedVersions = doc.getIn(['spec', 'backstage', 'supportedVersions']); diff --git a/validate-metadata/README.md b/validate-metadata/README.md index b37f516..e1e08c8 100644 --- a/validate-metadata/README.md +++ b/validate-metadata/README.md @@ -10,11 +10,13 @@ For each YAML file in the `metadata/` folder of the overlay workspace, the follo 2. **Version Match**: The `version` field in the metadata matches the `version` field in the corresponding plugin's `package.json` -3. **OCI Reference Validation** (if `dynamicArtifact` starts with `oci://ghcr.io`): +3. **dynamicArtifact Required**: The `dynamicArtifact` field must be present and must be an OCI reference starting with `oci://ghcr.io`. + +4. **OCI Reference Validation**: For `dynamicArtifact` values starting with `oci://ghcr.io`: - **Tag Format**: The image tag should be `bs___` - **Reference Format**: The image reference (without tag) should be `/` -4. **Backstage Supported Versions Match**: The `backstage.supportedVersions` field in the metadata matches the major.minor version of `supportedVersions` in the plugin's `dist-dynamic/package.json` +5. **Backstage Supported Versions Match**: The `backstage.supportedVersions` field in the metadata matches the major.minor version of `supportedVersions` in the plugin's `dist-dynamic/package.json` ## Usage diff --git a/validate-metadata/test/cases/fail-missing-dynamic-artifact/metadata/test-missing-dynamic-artifact.yaml b/validate-metadata/test/cases/fail-missing-dynamic-artifact/metadata/test-missing-dynamic-artifact.yaml new file mode 100644 index 0000000..de5e8e3 --- /dev/null +++ b/validate-metadata/test/cases/fail-missing-dynamic-artifact/metadata/test-missing-dynamic-artifact.yaml @@ -0,0 +1,11 @@ +apiVersion: extensions.backstage.io/v1alpha1 +kind: Package +metadata: + name: test-missing-dynamic-artifact + namespace: rhdh +spec: + packageName: '@test-org/plugin-test' + version: 1.0.0 + backstage: + role: frontend-plugin + supportedVersions: 1.42.5 diff --git a/validate-metadata/test/cases/fail-missing-dynamic-artifact/plugins-list.yaml b/validate-metadata/test/cases/fail-missing-dynamic-artifact/plugins-list.yaml new file mode 100644 index 0000000..f273645 --- /dev/null +++ b/validate-metadata/test/cases/fail-missing-dynamic-artifact/plugins-list.yaml @@ -0,0 +1 @@ +plugins/test-plugin: diff --git a/validate-metadata/test/cases/fail-wrapper-path/metadata/test-wrapper-path.yaml b/validate-metadata/test/cases/fail-wrapper-path/metadata/test-wrapper-path.yaml new file mode 100644 index 0000000..fd0a1e4 --- /dev/null +++ b/validate-metadata/test/cases/fail-wrapper-path/metadata/test-wrapper-path.yaml @@ -0,0 +1,12 @@ +apiVersion: extensions.backstage.io/v1alpha1 +kind: Package +metadata: + name: test-wrapper-path + namespace: rhdh +spec: + packageName: '@test-org/plugin-test' + dynamicArtifact: ./dynamic-plugins/dist/test-org-plugin-test + version: 1.0.0 + backstage: + role: frontend-plugin + supportedVersions: 1.42.5 diff --git a/validate-metadata/test/cases/fail-wrapper-path/plugins-list.yaml b/validate-metadata/test/cases/fail-wrapper-path/plugins-list.yaml new file mode 100644 index 0000000..f273645 --- /dev/null +++ b/validate-metadata/test/cases/fail-wrapper-path/plugins-list.yaml @@ -0,0 +1 @@ +plugins/test-plugin: diff --git a/validate-metadata/test/cases/pass/metadata/test-backend.yaml b/validate-metadata/test/cases/pass/metadata/test-backend.yaml deleted file mode 100644 index 91474d6..0000000 --- a/validate-metadata/test/cases/pass/metadata/test-backend.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: extensions.backstage.io/v1alpha1 -kind: Package -metadata: - name: test-backend - namespace: rhdh -spec: - packageName: '@test-org/plugin-test-backend' - dynamicArtifact: ./dynamic-plugins/dist/test-org-plugin-test-backend - version: 2.0.0 - backstage: - role: backend-plugin - supportedVersions: 1.42.5 diff --git a/validate-metadata/test/cases/pass/plugins-list.yaml b/validate-metadata/test/cases/pass/plugins-list.yaml index 6c62493..f273645 100644 --- a/validate-metadata/test/cases/pass/plugins-list.yaml +++ b/validate-metadata/test/cases/pass/plugins-list.yaml @@ -1,2 +1 @@ plugins/test-plugin: -plugins/test-backend: diff --git a/validate-metadata/validate-metadata.ts b/validate-metadata/validate-metadata.ts index d9e17d3..c17ae00 100644 --- a/validate-metadata/validate-metadata.ts +++ b/validate-metadata/validate-metadata.ts @@ -402,8 +402,23 @@ function validateMetadataFile(metadataFilePath: string, pluginsMapping: Map