Skip to content
Open
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
10 changes: 10 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ on:
type: boolean
required: false
default: false
node-package-manager-allow-scripts:
description: |
Allow npm/yarn/pnpm lifecycle scripts (preinstall, install, postinstall, prepare, etc.)
to run during dependency installation.
Disabled by default for security. Only enable as a last resort if a plugin truly
requires install-time scripts and cannot work without them.
type: boolean
required: false
default: false
allow-unsigned:
description: |
Allow to publish unsigned plugins.
Expand Down Expand Up @@ -757,6 +766,7 @@ jobs:
plugin-directory: ${{ inputs.plugin-directory }}
plugin-version-suffix: ${{ needs.setup.outputs.plugin-version-suffix }}
npm-registry-auth: ${{ inputs.npm-registry-auth }}
node-package-manager-allow-scripts: ${{ inputs.node-package-manager-allow-scripts }}

go-version: ${{ inputs.go-version }}
go-setup-caching: ${{ inputs.go-setup-caching }}
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ on:
type: boolean
required: false
default: false
node-package-manager-allow-scripts:
description: |
Allow npm/yarn/pnpm lifecycle scripts (preinstall, install, postinstall, prepare, etc.)
to run during dependency installation.
Disabled by default for security. Only enable as a last resort if a plugin truly
requires install-time scripts and cannot work without them.
type: boolean
required: false
default: false

# Playwright
run-playwright:
Expand Down Expand Up @@ -490,7 +499,7 @@ jobs:
- name: Spellcheck
run: |
if [ -f "./cspell.config.json" ]; then
npx --yes cspell@6.13.3 -c cspell.config.json "**/*.{ts,tsx,js,go,md,mdx,yml,yaml,json,scss,css}"
npx --yes --ignore-scripts cspell@6.13.3 -c cspell.config.json "**/*.{ts,tsx,js,go,md,mdx,yml,yaml,json,scss,css}"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

cspell doesn't have any lifecycle scripts for install, so it's safe to always skip the, regardless of the value of node-package-manager-allow-scripts

else
echo "❌ Spellcheck is not configured."
fi
Expand Down Expand Up @@ -534,6 +543,7 @@ jobs:
plugin-directory: ${{ inputs.plugin-directory }}
secrets: ${{ (fromJson(steps.workflow-context.outputs.result).isTrusted && inputs.frontend-secrets != '') && inputs.frontend-secrets || '' }}
npm-registry-auth: ${{ inputs.npm-registry-auth }}
allow-scripts: ${{ inputs.node-package-manager-allow-scripts }}

- name: Test and build backend
id: backend
Expand Down Expand Up @@ -741,6 +751,7 @@ jobs:
secrets: ${{ (fromJson(needs.test-and-build.outputs.workflow-context).isTrusted && inputs.playwright-secrets != '') && inputs.playwright-secrets || '' }}
node-version: ${{ inputs.node-version }}
npm-registry-auth: ${{ inputs.npm-registry-auth }}
node-package-manager-allow-scripts: ${{ inputs.node-package-manager-allow-scripts }}

playwright-docker:
name: Plugins - Dockerized Playwright E2E tests
Expand Down
43 changes: 42 additions & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ on:
type: boolean
required: false
default: false
node-package-manager-allow-scripts:
description: |
Allow npm/yarn/pnpm lifecycle scripts (preinstall, install, postinstall, prepare, etc.)
to run during dependency installation.
Disabled by default for security. Only enable as a last resort if a plugin truly
requires install-time scripts and cannot work without them.
type: boolean
required: false
default: false
permissions:
contents: read
id-token: write
Expand Down Expand Up @@ -146,8 +155,40 @@ jobs:
working-directory: ${{ inputs.plugin-directory }}
run: GOOGLE_APPLICATION_CREDENTIALS=${{ env.GOOGLE_APPLICATION_CREDENTIALS }} npx google-artifactregistry-auth --credential-config ./.npmrc

- name: Configure package manager script policy
id: install-cmd
shell: bash
run: |
INSTALL_CMD="${FROZEN_INSTALL_CMD}"
if [ "${ALLOW_SCRIPTS}" != "true" ]; then
# Yarn Berry does not support --ignore-scripts on the CLI (it errors with YN0050).
# For Yarn Berry, scripts are suppressed via the YARN_ENABLE_SCRIPTS env var.
# npm, pnpm, and Yarn Classic (agent=yarn) all accept --ignore-scripts on the CLI.
if [ "${PACKAGE_MANAGER_AGENT}" != "yarnBerry" ]; then
INSTALL_CMD="${INSTALL_CMD} --ignore-scripts"
fi
# Belt-and-suspenders: also export env vars so that scripts are blocked even if a
# wrapper or sub-process re-invokes the package manager without our CLI flags.
# npm and pnpm both honour NPM_CONFIG_IGNORE_SCRIPTS.
# Yarn Berry honours YARN_ENABLE_SCRIPTS (its env-var form of enableScripts config).
# Yarn Classic also honours npm_config_ignore_scripts.
echo "NPM_CONFIG_IGNORE_SCRIPTS=true" >> "$GITHUB_ENV"
echo "YARN_ENABLE_SCRIPTS=false" >> "$GITHUB_ENV"
printf '::act-debug::msg="npm-config-ignore-scripts" value=true\n'
printf '::act-debug::msg="yarn-enable-scripts" value=false\n'
else
printf '::act-debug::msg="npm-config-ignore-scripts" value=not-set\n'
printf '::act-debug::msg="yarn-enable-scripts" value=not-set\n'
fi
echo "cmd=${INSTALL_CMD}" >> "$GITHUB_OUTPUT"
printf '::act-debug::msg="install-cmd" cmd=%s\n' "${INSTALL_CMD}"
env:
FROZEN_INSTALL_CMD: ${{ steps.setup.outputs.node-package-manager-frozen-install-cmd }}
PACKAGE_MANAGER_AGENT: ${{ steps.setup.outputs.node-package-manager-agent }}
ALLOW_SCRIPTS: ${{ inputs.node-package-manager-allow-scripts }}

- name: Install npm dependencies
run: ${{ steps.setup.outputs.node-package-manager-frozen-install-cmd }}
run: ${{ steps.install-cmd.outputs.cmd }}
shell: bash
working-directory: ${{ inputs.plugin-directory }}

Expand Down
42 changes: 41 additions & 1 deletion actions/internal/plugins/frontend/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ inputs:
If true, the root of the plugin repository must contain a `.npmrc` file.
required: false
default: "false"
allow-scripts:
description: |
Allow npm/yarn/pnpm lifecycle scripts (preinstall, install, postinstall, prepare, etc.)
to run during dependency installation.
Disabled by default for security. Only enable as a last resort if a plugin truly
requires install-time scripts and cannot work without them.
required: false
default: "false"

runs:
using: composite
Expand Down Expand Up @@ -47,10 +55,42 @@ runs:
with:
working-directory: ${{ inputs.plugin-directory }}

- name: Configure package manager script policy
id: install-cmd
shell: bash
run: |
INSTALL_CMD="${FROZEN_INSTALL_CMD}"
if [ "${ALLOW_SCRIPTS}" != "true" ]; then
# Yarn Berry does not support --ignore-scripts on the CLI (it errors with YN0050).
# For Yarn Berry, scripts are suppressed via the YARN_ENABLE_SCRIPTS env var.
# npm, pnpm, and Yarn Classic (agent=yarn) all accept --ignore-scripts on the CLI.
if [ "${PACKAGE_MANAGER_AGENT}" != "yarnBerry" ]; then
INSTALL_CMD="${INSTALL_CMD} --ignore-scripts"
fi
# Belt-and-suspenders: also export env vars so that scripts are blocked even if a
# wrapper or sub-process re-invokes the package manager without our CLI flags.
# npm and pnpm both honour NPM_CONFIG_IGNORE_SCRIPTS.
# Yarn Berry honours YARN_ENABLE_SCRIPTS (its env-var form of enableScripts config).
# Yarn Classic also honours npm_config_ignore_scripts.
echo "NPM_CONFIG_IGNORE_SCRIPTS=true" >> "$GITHUB_ENV"
echo "YARN_ENABLE_SCRIPTS=false" >> "$GITHUB_ENV"
printf '::act-debug::msg="npm-config-ignore-scripts" value=true\n'
printf '::act-debug::msg="yarn-enable-scripts" value=false\n'
else
printf '::act-debug::msg="npm-config-ignore-scripts" value=not-set\n'
printf '::act-debug::msg="yarn-enable-scripts" value=not-set\n'
fi
echo "cmd=${INSTALL_CMD}" >> "$GITHUB_OUTPUT"
printf '::act-debug::msg="install-cmd" cmd=%s\n' "${INSTALL_CMD}"
env:
FROZEN_INSTALL_CMD: ${{ steps.package-manager.outputs.frozenInstallCmd }}
PACKAGE_MANAGER_AGENT: ${{ steps.package-manager.outputs.agent }}
ALLOW_SCRIPTS: ${{ inputs.allow-scripts }}

- name: Install dependencies
shell: bash
working-directory: ${{ inputs.plugin-directory }}
run: ${{ steps.package-manager.outputs.frozenInstallCmd }}
run: ${{ steps.install-cmd.outputs.cmd }}

- name: Lint
shell: bash
Expand Down
3 changes: 3 additions & 0 deletions actions/internal/plugins/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ outputs:
node-package-manager-exec-local-cmd:
description: The command to execute a locally installed package (e.g. npx, yarn dlx, pnpm exec).
value: ${{ steps.package-manager.outputs.execLocalCmd }}
node-package-manager-agent:
description: The agent identifier for the detected Node.js package manager (e.g. npm, pnpm, yarn, yarnBerry).
value: ${{ steps.package-manager.outputs.agent }}

runs:
using: composite
Expand Down
4 changes: 4 additions & 0 deletions tests/act/internal/workflow/ci/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ type WorkflowInputs struct {
Testing *bool
BackendBuildTarget *string

NodePackageManagerAllowScripts *bool

DistArtifactsRetentionDays *int
}

Expand Down Expand Up @@ -141,6 +143,8 @@ func SetCIInputs(dst *workflow.Job, inputs WorkflowInputs) {
workflow.SetJobInput(dst, "testing", inputs.Testing)
workflow.SetJobInput(dst, "backend-build-target", inputs.BackendBuildTarget)

workflow.SetJobInput(dst, "node-package-manager-allow-scripts", inputs.NodePackageManagerAllowScripts)

workflow.SetJobInput(dst, "dist-artifacts-retention-days", inputs.DistArtifactsRetentionDays)
}

Expand Down
98 changes: 98 additions & 0 deletions tests/act/main_install_scripts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package main

import (
"fmt"
"path/filepath"
"strings"
"testing"

"github.com/grafana/plugin-ci-workflows/tests/act/internal/act"
"github.com/grafana/plugin-ci-workflows/tests/act/internal/workflow"
"github.com/grafana/plugin-ci-workflows/tests/act/internal/workflow/ci"
"github.com/stretchr/testify/require"
)

// TestInstallScripts verifies the "Configure package manager script policy" logic
// in the frontend action against the three real test plugins (npm, yarn, pnpm),
// with allow-scripts both true and false.
//
// TODO: add a simple-frontend-yarn-berry test plugin and cover the yarnBerry agent path,
// where --ignore-scripts is NOT appended to the CLI (Berry rejects it) and scripts are
// suppressed solely via the YARN_ENABLE_SCRIPTS env var.
func TestInstallScripts(t *testing.T) {
type testCase struct {
folder string
allowScripts bool
wantCmdContains string
wantCmdNotContains string
wantNPMIgnore bool
wantYarnDisable bool
}

for _, tc := range []testCase{
{folder: "simple-frontend", allowScripts: false, wantCmdContains: "--ignore-scripts", wantNPMIgnore: true, wantYarnDisable: true},
{folder: "simple-frontend-yarn", allowScripts: false, wantCmdContains: "--ignore-scripts", wantNPMIgnore: true, wantYarnDisable: true},
{folder: "simple-frontend-pnpm", allowScripts: false, wantCmdContains: "--ignore-scripts", wantNPMIgnore: true, wantYarnDisable: true},
{folder: "simple-frontend", allowScripts: true, wantCmdNotContains: "--ignore-scripts", wantNPMIgnore: false, wantYarnDisable: false},
{folder: "simple-frontend-yarn", allowScripts: true, wantCmdNotContains: "--ignore-scripts", wantNPMIgnore: false, wantYarnDisable: false},
{folder: "simple-frontend-pnpm", allowScripts: true, wantCmdNotContains: "--ignore-scripts", wantNPMIgnore: false, wantYarnDisable: false},
} {
t.Run(fmt.Sprintf("%s/allow-scripts=%v", tc.folder, tc.allowScripts), func(t *testing.T) {
t.Parallel()

runner, err := act.NewRunner(t)
require.NoError(t, err)

wf, err := ci.NewWorkflow(
ci.WithWorkflowInputs(ci.WorkflowInputs{
PluginDirectory: workflow.Input(filepath.Join("tests", tc.folder)),
DistArtifactsPrefix: workflow.Input(tc.folder + "-"),
RunPlaywright: workflow.Input(false),
NodePackageManagerAllowScripts: workflow.Input(tc.allowScripts),
}),
ci.MutateCIWorkflow().With(
workflow.WithOnlyOneJob(t, "test-and-build", true),
workflow.WithRemoveAllStepsAfter(t, "test-and-build", "frontend"),
),
)
require.NoError(t, err)

r, err := runner.Run(wf, act.NewPushEventPayload("main"))
require.NoError(t, err)
require.True(t, r.Success, "workflow should succeed")

var installCmdAnnotation *act.Annotation
for i, ann := range r.Annotations {
if ann.Level == act.AnnotationLevelDebug && strings.Contains(ann.Message, `msg="install-cmd"`) {
installCmdAnnotation = &r.Annotations[i]
break
}
}
require.NotNil(t, installCmdAnnotation, "install-cmd annotation should be present")
if tc.wantCmdContains != "" {
require.Contains(t, installCmdAnnotation.Message, tc.wantCmdContains)
}
if tc.wantCmdNotContains != "" {
require.NotContains(t, installCmdAnnotation.Message, tc.wantCmdNotContains)
}

npmIgnoreVal := "not-set"
if tc.wantNPMIgnore {
npmIgnoreVal = "true"
}
require.Contains(t, r.Annotations, act.Annotation{
Level: act.AnnotationLevelDebug,
Message: fmt.Sprintf(`msg="npm-config-ignore-scripts" value=%s`, npmIgnoreVal),
})

yarnVal := "not-set"
if tc.wantYarnDisable {
yarnVal = "false"
}
require.Contains(t, r.Annotations, act.Annotation{
Level: act.AnnotationLevelDebug,
Message: fmt.Sprintf(`msg="yarn-enable-scripts" value=%s`, yarnVal),
})
})
}
}
Loading