diff --git a/.github/linters/zizmor.yaml b/.github/linters/zizmor.yaml deleted file mode 100644 index 21cade0..0000000 --- a/.github/linters/zizmor.yaml +++ /dev/null @@ -1,6 +0,0 @@ -rules: - self-repository: - # Actionlint 1.7.12 (bundled with Super-Linter v9) rejects the recommended $/ syntax. - # FIXME: Re-enable when a supported Actionlint release implements it: - # https://github.com/rhysd/actionlint/issues/711 - disable: true diff --git a/.github/workflows/__main-ci.yml b/.github/workflows/__main-ci.yml index 2648346..e0e311f 100644 --- a/.github/workflows/__main-ci.yml +++ b/.github/workflows/__main-ci.yml @@ -19,7 +19,7 @@ concurrency: jobs: ci: - uses: ./.github/workflows/__shared-ci.yml + uses: $/.github/workflows/__shared-ci.yml permissions: actions: read contents: read diff --git a/.github/workflows/__pull-request-ci.yml b/.github/workflows/__pull-request-ci.yml index 7ac60fc..51bcecb 100644 --- a/.github/workflows/__pull-request-ci.yml +++ b/.github/workflows/__pull-request-ci.yml @@ -13,7 +13,7 @@ permissions: {} jobs: ci: - uses: ./.github/workflows/__shared-ci.yml + uses: $/.github/workflows/__shared-ci.yml permissions: actions: read contents: read diff --git a/.github/workflows/__shared-ci.yml b/.github/workflows/__shared-ci.yml index b540e64..19516d9 100644 --- a/.github/workflows/__shared-ci.yml +++ b/.github/workflows/__shared-ci.yml @@ -15,38 +15,38 @@ jobs: pull-requests: write security-events: write statuses: write - uses: ./.github/workflows/linter.yml + uses: $/.github/workflows/linter.yml test-action-matrix-outputs: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-matrix-outputs.yml + uses: $/.github/workflows/__test-action-matrix-outputs.yml test-action-checkout: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-checkout.yml + uses: $/.github/workflows/__test-action-checkout.yml test-action-checkout-issue-comment: needs: linter permissions: contents: read pull-requests: read - uses: ./.github/workflows/__test-action-checkout-issue-comment.yml + uses: $/.github/workflows/__test-action-checkout-issue-comment.yml test-action-get-github-actions-bot-user: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-get-github-actions-bot-user.yml + uses: $/.github/workflows/__test-action-get-github-actions-bot-user.yml test-action-get-issue-number: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-get-issue-number.yml + uses: $/.github/workflows/__test-action-get-issue-number.yml test-action-parse-ci-reports: needs: linter @@ -56,22 +56,22 @@ jobs: packages: read pull-requests: write security-events: write - uses: ./.github/workflows/__test-action-parse-ci-reports.yml + uses: $/.github/workflows/__test-action-parse-ci-reports.yml test-action-repository-owner-is-organization: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-repository-owner-is-organization.yml + uses: $/.github/workflows/__test-action-repository-owner-is-organization.yml test-action-slugify: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-slugify.yml + uses: $/.github/workflows/__test-action-slugify.yml test-action-working-directory: needs: linter permissions: contents: read - uses: ./.github/workflows/__test-action-working-directory.yml + uses: $/.github/workflows/__test-action-working-directory.yml diff --git a/.github/workflows/__test-action-checkout-issue-comment.yml b/.github/workflows/__test-action-checkout-issue-comment.yml index 46f847d..4cfb155 100644 --- a/.github/workflows/__test-action-checkout-issue-comment.yml +++ b/.github/workflows/__test-action-checkout-issue-comment.yml @@ -41,7 +41,7 @@ jobs: - name: Act - Simulate issue_comment event context and checkout id: checkout - uses: ./actions/checkout + uses: $/actions/checkout with: ref: refs/pull/${{ steps.pr-info.outputs.pr-number }}/head persist-credentials: true diff --git a/.github/workflows/__test-action-checkout.yml b/.github/workflows/__test-action-checkout.yml index 73b57e2..865c7cc 100644 --- a/.github/workflows/__test-action-checkout.yml +++ b/.github/workflows/__test-action-checkout.yml @@ -19,7 +19,7 @@ jobs: - name: Act - Run custom checkout action with defaults id: custom-checkout-defaults - uses: ./actions/checkout + uses: $/actions/checkout - name: Assert - Verify repository is checked out uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -32,7 +32,7 @@ jobs: - name: Act - Run custom checkout action with fetch-depth 0 id: custom-checkout-full-history - uses: ./actions/checkout + uses: $/actions/checkout with: fetch-depth: "0" @@ -47,7 +47,7 @@ jobs: - name: Act - Run custom checkout action with LFS disabled (default) id: custom-checkout-no-lfs - uses: ./actions/checkout + uses: $/actions/checkout with: lfs: "false" diff --git a/.github/workflows/__test-action-get-github-actions-bot-user.yml b/.github/workflows/__test-action-get-github-actions-bot-user.yml index 3ae618b..b98af86 100644 --- a/.github/workflows/__test-action-get-github-actions-bot-user.yml +++ b/.github/workflows/__test-action-get-github-actions-bot-user.yml @@ -18,7 +18,7 @@ jobs: - name: Act - Run get-github-actions-bot-user action id: get-github-actions-bot-user - uses: ./actions/get-github-actions-bot-user + uses: $/actions/get-github-actions-bot-user - name: Assert - Check get-github-actions-bot-user outputs uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/__test-action-get-issue-number.yml b/.github/workflows/__test-action-get-issue-number.yml index 725dca5..0134b83 100644 --- a/.github/workflows/__test-action-get-issue-number.yml +++ b/.github/workflows/__test-action-get-issue-number.yml @@ -19,7 +19,7 @@ jobs: - name: Act - Run get-issue-number action id: get-issue-number continue-on-error: true - uses: ./actions/get-issue-number + uses: $/actions/get-issue-number - name: Assert - Check get-issue-number behavior by event type uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/__test-action-matrix-outputs.yml b/.github/workflows/__test-action-matrix-outputs.yml index 559e2bf..943cec4 100644 --- a/.github/workflows/__test-action-matrix-outputs.yml +++ b/.github/workflows/__test-action-matrix-outputs.yml @@ -21,7 +21,7 @@ jobs: persist-credentials: false - id: set-matrix-output - uses: ./actions/set-matrix-output + uses: $/actions/set-matrix-output with: value: | { "test": "test content 1" } @@ -63,7 +63,7 @@ jobs: with: persist-credentials: false - - uses: ./actions/set-matrix-output + - uses: $/actions/set-matrix-output with: value: "" artifact-name: "test-matrix-outputs-${{ matrix.os }}" @@ -81,7 +81,7 @@ jobs: with: persist-credentials: false - - uses: ./actions/set-matrix-output + - uses: $/actions/set-matrix-output with: value: | { "test": "test content 3" } @@ -102,7 +102,7 @@ jobs: persist-credentials: false - id: get-matrix-outputs - uses: ./actions/get-matrix-outputs + uses: $/actions/get-matrix-outputs with: artifact-name: test-matrix-outputs-${{ matrix.os }} diff --git a/.github/workflows/__test-action-parse-ci-reports.yml b/.github/workflows/__test-action-parse-ci-reports.yml index 91e7314..5195416 100644 --- a/.github/workflows/__test-action-parse-ci-reports.yml +++ b/.github/workflows/__test-action-parse-ci-reports.yml @@ -89,7 +89,7 @@ jobs: - name: Act - Run parse-ci-reports action id: parse-ci-reports - uses: ./actions/parse-ci-reports + uses: $/actions/parse-ci-reports with: report-paths: | test-reports/junit.xml diff --git a/.github/workflows/__test-action-repository-owner-is-organization.yml b/.github/workflows/__test-action-repository-owner-is-organization.yml index 5f60286..47e798c 100644 --- a/.github/workflows/__test-action-repository-owner-is-organization.yml +++ b/.github/workflows/__test-action-repository-owner-is-organization.yml @@ -18,7 +18,7 @@ jobs: - name: Act - Run repository-owner-is-organization action id: repository-owner-is-organization - uses: ./actions/repository-owner-is-organization + uses: $/actions/repository-owner-is-organization - name: Assert - Check repository-owner-is-organization outputs uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/__test-action-slugify.yml b/.github/workflows/__test-action-slugify.yml index b13c735..1ef62e4 100644 --- a/.github/workflows/__test-action-slugify.yml +++ b/.github/workflows/__test-action-slugify.yml @@ -18,7 +18,7 @@ jobs: - name: Act - Run slugify action id: slugify - uses: ./actions/slugify + uses: $/actions/slugify with: value: test content diff --git a/.github/workflows/__test-action-working-directory.yml b/.github/workflows/__test-action-working-directory.yml index d113d22..4dbab87 100644 --- a/.github/workflows/__test-action-working-directory.yml +++ b/.github/workflows/__test-action-working-directory.yml @@ -18,7 +18,7 @@ jobs: - name: Act - Resolve workspace root directory id: working-directory - uses: ./actions/working-directory + uses: $/actions/working-directory with: working-directory: . @@ -46,7 +46,7 @@ jobs: - name: Act - Run outside-workspace path with enforcement enabled id: outside-workspace-disallowed continue-on-error: true - uses: ./actions/working-directory + uses: $/actions/working-directory with: working-directory: /tmp enforce-path-in-workspace: true @@ -73,7 +73,7 @@ jobs: - name: Act - Run outside-workspace path with enforcement disabled id: outside-workspace-allowed - uses: ./actions/working-directory + uses: $/actions/working-directory with: working-directory: /tmp enforce-path-in-workspace: false diff --git a/.github/workflows/linter.md b/.github/workflows/linter.md index d80ea2a..5e9157b 100644 --- a/.github/workflows/linter.md +++ b/.github/workflows/linter.md @@ -47,6 +47,30 @@ Executes: +### Shared linting image + +Super-Linter runs in the [Hoverkraft image](https://github.com/hoverkraft-tech/docker-base-images/tree/main/images/super-linter). +The image owns toolchain selection, Kubernetes schema defaults, and fallback +commitlint rules. Use the same image release for local linting to share +these defaults and the bundled linter versions. + +The local Dockerfile and workflow pin the same image release and SHA256 digest. +Update both references together when updating the image: Dependabot updates Dockerfiles but +[does not update `docker://` workflow references](https://github.com/dependabot/dependabot-core/issues/8362). + +Values in `linter-env` override the toolchain inputs and image defaults. An empty +`VALIDATE_JAVASCRIPT_TOOLCHAIN` or `VALIDATE_PYTHON_TOOLCHAIN` value disables the +corresponding automatic toolchain exclusions. `VALIDATE_GIT_COMMITLINT=false` +disables commitlint. With toolchain selectors enabled, explicit `true` keeps it +enabled without restricting the other validators. Set both selectors to empty +strings to use Super-Linter's native `VALIDATE_*=true` allowlist, including +commitlint. Do not mix `true` and `false` validator flags in that mode. + +CI uses Git change detection according to `lint-all` and publishes GitHub +summaries. Local runs use filesystem discovery by default. The workflow also +installs project dependencies when `package.json` declares Prettier plugins; +install those dependencies before running the image locally. + ### GitHub Actions Pinning **Tips:** To ensure that all GitHub Actions used in your workflows are pinned to a specific version, diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 08ec132..6ffbf0d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,7 +1,7 @@ # Reusable workflow that performs linting on the codebase. # Executes: # -# - [Super-Linter](https://github.com/super-linter/super-linter), with some opinionated defaults. +# - [Hoverkraft Super-Linter](https://github.com/hoverkraft-tech/docker-base-images/tree/main/images/super-linter), shared with local linting. # - [CodeQL](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) to analyze the code. # - [Ratchet](https://github.com/sethvargo/ratchet) to check that GitHub Action versions are pinned. @@ -94,282 +94,30 @@ jobs: PYTHON_LINTER_TOOLCHAIN: ${{ inputs.python-linter-toolchain }} with: script: | - const javascriptToolchainVariables = { - biome: { - VALIDATE_CSS: "false", - VALIDATE_CSS_PRETTIER: "false", - VALIDATE_GRAPHQL_PRETTIER: "false", - VALIDATE_HTML_PRETTIER: "false", - VALIDATE_JAVASCRIPT_ES: "false", - VALIDATE_JAVASCRIPT_PRETTIER: "false", - VALIDATE_JSON: "false", - VALIDATE_JSON_PRETTIER: "false", - VALIDATE_JSONC: "false", - VALIDATE_JSONC_PRETTIER: "false", - VALIDATE_JSX: "false", - VALIDATE_JSX_PRETTIER: "false", - VALIDATE_TYPESCRIPT_ES: "false", - VALIDATE_TYPESCRIPT_PRETTIER: "false", - VALIDATE_TSX: "false", - VALIDATE_VUE: "false", - VALIDATE_VUE_PRETTIER: "false", - }, - "eslint-prettier": { - VALIDATE_BIOME_FORMAT: "false", - VALIDATE_BIOME_LINT: "false", - }, - }; - - const pythonToolchainVariables = { - black: { - VALIDATE_PYTHON_RUFF_FORMAT: "false", - }, - "ruff-format": { - VALIDATE_PYTHON_BLACK: "false", - }, - }; - - const kubeconformSchemaLocations = [ - "default", - "https://raw.githubusercontent.com/hoverkraft-tech/crds-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json", - "https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json", - ]; - - const defaultLinterVariables = { - KUBERNETES_KUBECONFORM_OPTIONS: - kubeconformSchemaLocations - .map((schemaLocation) => `-schema-location ${schemaLocation}`) - .join(" "), - SAVE_SUPER_LINTER_SUMMARY: "true", - }; - - function parseEnvironmentLines(serializedEnvironment) { - const environment = new Map(); - - for (const rawLine of serializedEnvironment.split("\n")) { - const line = rawLine.trim(); - if (line === "") { - continue; - } - - const separatorIndex = line.indexOf("="); - if (separatorIndex <= 0) { - throw new Error(`Invalid linter-env entry: ${rawLine}`); - } - - const key = line.slice(0, separatorIndex).trim(); - const value = line.slice(separatorIndex + 1); - if (key === "") { - throw new Error(`Invalid linter-env entry: ${rawLine}`); - } - - environment.set(key, value); - } - - return environment; - } - - function getToolchainEnvironment(toolchain, toolchainVariables, variableName) { - if (toolchain === "") { - return {}; - } - - const environment = toolchainVariables[toolchain]; - if (!environment) { - throw new Error(`Unsupported ${variableName}: ${toolchain}`); - } - - return environment; - } - - function resolveToolchain(customEnvironment, variableName, defaultValue) { - if (customEnvironment.has(variableName)) { - return customEnvironment.get(variableName) ?? ""; + const environment = new Map([ + ["VALIDATE_JAVASCRIPT_TOOLCHAIN", process.env.JAVASCRIPT_LINTER_TOOLCHAIN], + ["VALIDATE_PYTHON_TOOLCHAIN", process.env.PYTHON_LINTER_TOOLCHAIN], + ]); + + for (const rawLine of (process.env.LINTER_ENV ?? "").split("\n")) { + const line = rawLine.trim(); + if (line === "") { + continue; } - return defaultValue; - } - - function mergeEnvironment(customEnvironment, defaultEnvironment) { - const mergedEnvironment = new Map(customEnvironment); - - for (const [key, value] of Object.entries(defaultEnvironment)) { - if (!mergedEnvironment.has(key)) { - mergedEnvironment.set(key, value); - } - } - - return mergedEnvironment; - } - - function isTrue(value) { - return (value ?? "").toLowerCase() === "true"; - } - - function normalizeCustomEnvironment(customEnvironment) { - const normalizedEnvironment = new Map(customEnvironment); - if (isTrue(normalizedEnvironment.get("VALIDATE_GIT_COMMITLINT"))) { - normalizedEnvironment.delete("VALIDATE_GIT_COMMITLINT"); + const separatorIndex = line.indexOf("="); + const key = line.slice(0, separatorIndex).trim(); + if (separatorIndex <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { + throw new Error(`Invalid linter-env variable name: ${key}`); } - return normalizedEnvironment; + environment.set(key, line.slice(separatorIndex + 1)); } - const customEnvironment = parseEnvironmentLines(process.env.LINTER_ENV ?? ""); - const normalizedCustomEnvironment = - normalizeCustomEnvironment(customEnvironment); - const javascriptToolchain = resolveToolchain( - normalizedCustomEnvironment, - "VALIDATE_JAVASCRIPT_TOOLCHAIN", - process.env.JAVASCRIPT_LINTER_TOOLCHAIN ?? "", - ); - const pythonToolchain = resolveToolchain( - normalizedCustomEnvironment, - "VALIDATE_PYTHON_TOOLCHAIN", - process.env.PYTHON_LINTER_TOOLCHAIN ?? "", - ); - const defaultEnvironment = { - ...defaultLinterVariables, - VALIDATE_JAVASCRIPT_TOOLCHAIN: javascriptToolchain, - VALIDATE_PYTHON_TOOLCHAIN: pythonToolchain, - ...getToolchainEnvironment( - javascriptToolchain, - javascriptToolchainVariables, - "VALIDATE_JAVASCRIPT_TOOLCHAIN", - ), - ...getToolchainEnvironment( - pythonToolchain, - pythonToolchainVariables, - "VALIDATE_PYTHON_TOOLCHAIN", - ), - }; - const mergedEnvironment = mergeEnvironment( - normalizedCustomEnvironment, - defaultEnvironment, - ); - - for (const [key, value] of mergedEnvironment) { + for (const [key, value] of environment) { core.exportVariable(key, value); } - - name: Configure commitlint - id: configure-commitlint - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - LINTER_ENV: ${{ inputs.linter-env }} - with: - script: | - const { existsSync, readFileSync, writeFileSync } = require("node:fs"); - const path = require("node:path"); - - const configurationFiles = [ - ".commitlintrc", - ".commitlintrc.json", - ".commitlintrc.yaml", - ".commitlintrc.yml", - ".commitlintrc.js", - ".commitlintrc.cjs", - ".commitlintrc.mjs", - "commitlint.config.js", - "commitlint.config.cjs", - "commitlint.config.mjs", - ]; - const defaultConfigurationPath = "commitlint.config.cjs"; - const defaultConfigurationContent = [ - "module.exports = {", - '\trules: {', - '\t\t"body-leading-blank": [1, "always"],', - '\t\t"footer-leading-blank": [1, "always"],', - '\t\t"header-max-length": [2, "always", 100],', - '\t\t"subject-case": [', - '\t\t\t2,', - '\t\t\t"never",', - '\t\t\t["sentence-case", "start-case", "pascal-case", "upper-case"],', - "\t\t],", - '\t\t"subject-empty": [2, "never"],', - '\t\t"subject-full-stop": [2, "never", "."],', - '\t\t"type-case": [2, "always", "lower-case"],', - '\t\t"type-empty": [2, "never"],', - '\t\t"type-enum": [', - '\t\t\t2,', - '\t\t\t"always",', - "\t\t\t[", - '\t\t\t\t"build",', - '\t\t\t\t"chore",', - '\t\t\t\t"ci",', - '\t\t\t\t"docs",', - '\t\t\t\t"feat",', - '\t\t\t\t"fix",', - '\t\t\t\t"perf",', - '\t\t\t\t"refactor",', - '\t\t\t\t"revert",', - '\t\t\t\t"style",', - '\t\t\t\t"test",', - "\t\t\t],", - "\t\t],", - "\t},", - "};", - "", - ].join("\n"); - - function hasPackageJsonConfiguration(workspacePath) { - const packageJsonPath = path.join(workspacePath, "package.json"); - if (!existsSync(packageJsonPath)) { - return false; - } - - const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); - return "commitlint" in packageJson; - } - - function hasConfiguration(workspacePath) { - if ( - configurationFiles.some((file) => - existsSync(path.join(workspacePath, file)), - ) - ) { - return true; - } - - return hasPackageJsonConfiguration(workspacePath); - } - - function getCommitlintValidation(serializedEnvironment) { - let validation; - - for (const rawLine of serializedEnvironment.split("\n")) { - const line = rawLine.trim(); - const separatorIndex = line.indexOf("="); - if ( - separatorIndex > 0 && - line.slice(0, separatorIndex).trim() === "VALIDATE_GIT_COMMITLINT" - ) { - validation = line.slice(separatorIndex + 1); - } - } - - return validation; - } - - const workspacePath = process.env.GITHUB_WORKSPACE ?? process.cwd(); - const defaultConfigurationAbsolutePath = path.join( - workspacePath, - defaultConfigurationPath, - ); - const commitlintValidation = getCommitlintValidation( - process.env.LINTER_ENV ?? "", - ); - if ( - (commitlintValidation ?? "").toLowerCase() !== "false" && - !hasConfiguration(workspacePath) - ) { - writeFileSync(defaultConfigurationAbsolutePath, defaultConfigurationContent); - core.setOutput( - "generated-configuration-path", - defaultConfigurationAbsolutePath, - ); - } - # FIXME: superlinter should auto install required dependencies. See https://github.com/super-linter/super-linter/issues/6089. - name: Find Prettier plugins id: has-prettier-plugins @@ -392,19 +140,13 @@ jobs: with: working-directory: ${{ steps.has-prettier-plugins.outputs.package-json-dir }} - - uses: super-linter/super-linter/slim@2da136927bd4a73596db63044b504547c62cb854 # v9.0.0 + - name: Run shared Super-Linter image + # FIXME: Keep aligned with Dockerfile; Dependabot cannot update docker:// + uses: docker://ghcr.io/hoverkraft-tech/docker-base-images/super-linter:1.1.0@sha256:f600fdd9c95e222b3831181e206db1a58494c45390b0ee0001e32a3c2ddedc75 env: VALIDATE_ALL_CODEBASE: ${{ inputs.lint-all }} - LOG_LEVEL: WARN DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GITHUB_TOKEN: ${{ secrets.github-token || secrets.GITHUB_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional - IGNORE_GITIGNORED_FILES: "true" - - - name: Remove generated commitlint configuration - if: ${{ always() && steps.configure-commitlint.outputs.generated-configuration-path != '' }} - env: - GENERATED_CONFIGURATION_PATH: ${{ steps.configure-commitlint.outputs.generated-configuration-path }} - run: rm -f -- "${GENERATED_CONFIGURATION_PATH}" codeql: if: ${{ inputs.codeql-languages }} @@ -493,7 +235,7 @@ jobs: const foundFiles = (await Promise.all(filePatterns.map( async (filePattern) => { const globber = await glob.create(filePattern, { excludeHiddenFiles: false }); - return await globber.glob(); + return await globber.glob(); } ))) .flat() @@ -501,7 +243,7 @@ jobs: return [...new Set(foundFiles)]; } - + let actionFiles = []; if (changedFilesOutput.length > 0) { actionFiles = changedFilesOutput.split(" ").filter(file => file && fs.existsSync(file)); diff --git a/Dockerfile b/Dockerfile index 2da3ff7..32d4e77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM ghcr.io/hoverkraft-tech/docker-base-images/super-linter:1.0.0 +FROM ghcr.io/hoverkraft-tech/docker-base-images/super-linter:1.1.0@sha256:f600fdd9c95e222b3831181e206db1a58494c45390b0ee0001e32a3c2ddedc75 HEALTHCHECK --interval=5m --timeout=10s --start-period=30s --retries=3 CMD ["/bin/sh","-c","test -d /github/home"] ARG UID=1000 ARG GID=1000 -USER ${UID}:${GID} \ No newline at end of file +USER ${UID}:${GID} diff --git a/README.md b/README.md index 9dba243..841487c 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,7 @@ Opinionated GitHub Actions and reusable workflows for foundational continuous-in ### Matrix & workflow data helpers - [Get matrix outputs](actions/get-matrix-outputs/README.md) - aggregates outputs across matrix jobs for downstream steps. -- [Local actions](actions/local-actions/README.md) - exposes sibling local actions for a composite action and cleans them up automatically. - [Set matrix output](actions/set-matrix-output/README.md) - writes structured outputs that can be consumed by other matrix jobs. -- [Local workflow actions](actions/local-workflow-actions/README.md) - exposes local actions from the reusable workflow repository. ### Repository insights & utilities diff --git a/actions/create-and-merge-pull-request/action.yml b/actions/create-and-merge-pull-request/action.yml index a334603..8ef190b 100644 --- a/actions/create-and-merge-pull-request/action.yml +++ b/actions/create-and-merge-pull-request/action.yml @@ -46,25 +46,8 @@ outputs: runs: using: "composite" steps: - # FIXME: This is a workaround until this issue is resolved: https://github.com/actions/runner/issues/1348. - - name: Prepare sibling actions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('node:path'); - - const sourcePath = path.resolve(process.env.GITHUB_ACTION_PATH, '..'); - const destinationPath = path.resolve(process.env.GITHUB_WORKSPACE, '..', 'self-actions'); - - await io.rmRF(destinationPath); - await io.mkdirP(destinationPath); - await io.cp(sourcePath, destinationPath, { - recursive: true, - copySourceDirectory: false, - }); - - id: github-actions-bot-user - uses: ./../self-actions/get-github-actions-bot-user + uses: $/actions/get-github-actions-bot-user - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 id: create-pull-request @@ -253,12 +236,3 @@ runs: }); await verifyMergedPullRequest(pullNumber, mergeOutputs); - - - name: Cleanup sibling actions - if: ${{ always() }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('node:path'); - - await io.rmRF(path.resolve(process.env.GITHUB_WORKSPACE, '..', 'self-actions')); diff --git a/actions/create-or-update-comment/action.yml b/actions/create-or-update-comment/action.yml index 0d74257..f8d3a9c 100644 --- a/actions/create-or-update-comment/action.yml +++ b/actions/create-or-update-comment/action.yml @@ -34,25 +34,8 @@ inputs: runs: using: "composite" steps: - # FIXME: This is a workaround until this issue is resolved: https://github.com/actions/runner/issues/1348. - - name: Prepare sibling actions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('node:path'); - - const sourcePath = path.resolve(process.env.GITHUB_ACTION_PATH, '..'); - const destinationPath = path.resolve(process.env.GITHUB_WORKSPACE, '..', 'self-actions'); - - await io.rmRF(destinationPath); - await io.mkdirP(destinationPath); - await io.cp(sourcePath, destinationPath, { - recursive: true, - copySourceDirectory: false, - }); - - id: get-issue-number - uses: ./../self-actions/get-issue-number + uses: $/actions/get-issue-number - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 id: find-comment @@ -73,12 +56,3 @@ runs: ${{ inputs.body }} reactions: ${{ inputs.reactions }} token: ${{ inputs.token || github.token }} - - - name: Cleanup sibling actions - if: ${{ always() }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('node:path'); - - await io.rmRF(path.resolve(process.env.GITHUB_WORKSPACE, '..', 'self-actions')); diff --git a/actions/local-actions/ActionRuntime.js b/actions/local-actions/ActionRuntime.js deleted file mode 100644 index e786f54..0000000 --- a/actions/local-actions/ActionRuntime.js +++ /dev/null @@ -1,62 +0,0 @@ -import { appendFile } from "node:fs/promises"; -import process from "node:process"; -import { randomUUID } from "node:crypto"; - -export class ActionRuntime { - getInput(name, { required = false } = {}) { - const value = - process.env[`INPUT_${name.replaceAll(" ", "_").toUpperCase()}`] ?? ""; - - if (required && value.trim() === "") { - throw new Error(`Input required and not supplied: ${name}`); - } - - return value; - } - - async setOutput(name, value) { - await this.#writeCommandFile(process.env.GITHUB_OUTPUT, name, value); - } - - async saveState(name, value) { - await this.#writeCommandFile(process.env.GITHUB_STATE, name, value); - } - - getState(name) { - return process.env[`STATE_${name}`] ?? ""; - } - - getWorkspace() { - const workspacePath = process.env.GITHUB_WORKSPACE ?? ""; - - if (workspacePath.trim() === "") { - throw new Error("GITHUB_WORKSPACE is required."); - } - - return workspacePath; - } - - info(message) { - console.log(message); - } - - setFailed(error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`::error::${message}`); - process.exitCode = 1; - } - - async #writeCommandFile(filePath, name, value) { - if (!filePath) { - throw new Error(`Missing command file for ${name}.`); - } - - const stringValue = String(value); - const delimiter = `ghadelimiter_${randomUUID()}`; - await appendFile( - filePath, - `${name}<<${delimiter}\n${stringValue}\n${delimiter}\n`, - "utf8", - ); - } -} diff --git a/actions/local-actions/LocalActionsManager.js b/actions/local-actions/LocalActionsManager.js deleted file mode 100644 index 1e82897..0000000 --- a/actions/local-actions/LocalActionsManager.js +++ /dev/null @@ -1,121 +0,0 @@ -import { access, mkdir, rm, stat, symlink } from "node:fs/promises"; -import process from "node:process"; -import path from "node:path"; - -export class LocalActionsManager { - async prepare({ sourcePath, workspacePath, destinationDirectoryName }) { - const sourceDirectory = await this.resolveSourceDirectory({ sourcePath }); - const destinationPath = this.resolveDestinationPath({ - workspacePath, - destinationDirectoryName, - }); - - if (await this.#exists(destinationPath)) { - return { - created: false, - destinationPath, - }; - } - - await mkdir(path.dirname(destinationPath), { recursive: true }); - const symlinkTarget = this.#resolveSymlinkTarget( - sourceDirectory, - destinationPath, - ); - await symlink(symlinkTarget, destinationPath, this.#getSymlinkType()); - - return { - created: true, - destinationPath, - }; - } - - async cleanup({ created, destinationPath }) { - if (!created || !destinationPath) { - return false; - } - - await rm(destinationPath, { force: true, recursive: true }); - - return true; - } - - resolveDestinationPath({ workspacePath, destinationDirectoryName }) { - if (!workspacePath?.trim()) { - throw new Error("Workspace path is required."); - } - - const targetDirectoryName = this.#resolveDestinationDirectoryName( - destinationDirectoryName, - ); - - return path.resolve(workspacePath, `../${targetDirectoryName}`); - } - - async resolveSourceDirectory({ sourcePath }) { - return this.#resolveActionPath(sourcePath); - } - - async #resolveActionPath(sourcePath) { - if (!sourcePath?.trim()) { - throw new Error("Input source-path is required."); - } - - const actionPath = path.resolve(sourcePath); - if (!(await this.#exists(actionPath))) { - throw new Error(`Action path does not exist: ${actionPath}`); - } - - if (!(await stat(actionPath)).isDirectory()) { - throw new Error(`Action path must be a directory: ${actionPath}`); - } - - return actionPath; - } - - async #exists(targetPath) { - try { - await access(targetPath); - return true; - } catch { - return false; - } - } - - #getSymlinkType() { - return process.platform === "win32" ? "junction" : "dir"; - } - - #resolveDestinationDirectoryName(destinationDirectoryName) { - if (destinationDirectoryName === undefined) { - return "self-actions"; - } - - const trimmedName = destinationDirectoryName.trim(); - if (!trimmedName) { - return "self-actions"; - } - - if (trimmedName.includes("/") || trimmedName.includes("\\")) { - throw new Error( - "Input destination-directory-name must be a directory name, not a path.", - ); - } - - return trimmedName; - } - - /** - * Computes the symlink target. Uses a relative path on non-Windows - * platforms so the symlink resolves correctly from both the container - * filesystem and the host filesystem (bind-mount path differs). - * Windows junctions require absolute targets. - */ - #resolveSymlinkTarget(sourceDirectory, destinationPath) { - if (this.#getSymlinkType() === "junction") { - return sourceDirectory; - } - - return path.relative(path.dirname(destinationPath), sourceDirectory); - } -} diff --git a/actions/local-actions/LocalActionsManager.test.js b/actions/local-actions/LocalActionsManager.test.js deleted file mode 100644 index 96cc117..0000000 --- a/actions/local-actions/LocalActionsManager.test.js +++ /dev/null @@ -1,203 +0,0 @@ -import { - existsSync, - lstatSync, - mkdtempSync, - mkdirSync, - readFileSync, - readlinkSync, - realpathSync, - rmSync, - writeFileSync, -} from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import assert from "node:assert/strict"; - -import { LocalActionsManager } from "./LocalActionsManager.js"; - -const createFixture = () => { - const sandboxDirectory = mkdtempSync( - path.join(os.tmpdir(), "local-actions-"), - ); - const workspaceDirectory = path.join(sandboxDirectory, "workspace"); - const actionsDirectory = path.join(workspaceDirectory, "actions"); - const currentActionPath = path.join( - actionsDirectory, - "create-or-update-comment", - ); - const siblingActionPath = path.join(actionsDirectory, "get-issue-number"); - - mkdirSync(workspaceDirectory, { recursive: true }); - mkdirSync(currentActionPath, { recursive: true }); - mkdirSync(siblingActionPath, { recursive: true }); - writeFileSync( - path.join(currentActionPath, "action.yml"), - "name: current\n", - "utf8", - ); - writeFileSync( - path.join(siblingActionPath, "action.yml"), - "name: sibling\n", - "utf8", - ); - - return { - actionsDirectory, - currentActionPath, - sandboxDirectory, - selfActionsPath: path.join(sandboxDirectory, "self-actions"), - workspaceDirectory, - teardown() { - rmSync(sandboxDirectory, { force: true, recursive: true }); - }, - }; -}; - -test("prepare creates a relative symlink to sibling actions in the destination", async () => { - const fixture = createFixture(); - const manager = new LocalActionsManager(); - - try { - const result = await manager.prepare({ - sourcePath: fixture.actionsDirectory, - workspacePath: fixture.workspaceDirectory, - }); - - assert.equal(result.created, true); - assert.equal(result.destinationPath, fixture.selfActionsPath); - assert.equal(lstatSync(fixture.selfActionsPath).isSymbolicLink(), true); - - // Verify the symlink uses a relative target - const linkTarget = readlinkSync(fixture.selfActionsPath); - assert.equal( - path.isAbsolute(linkTarget), - false, - `Expected relative symlink target, got: ${linkTarget}`, - ); - - // Verify it resolves to the correct directory - assert.equal( - realpathSync(fixture.selfActionsPath), - fixture.actionsDirectory, - ); - assert.equal( - readFileSync( - path.join(fixture.selfActionsPath, "get-issue-number", "action.yml"), - "utf8", - ), - "name: sibling\n", - ); - assert.equal( - readFileSync( - path.join( - fixture.selfActionsPath, - "create-or-update-comment", - "action.yml", - ), - "utf8", - ), - "name: current\n", - ); - assert.equal( - existsSync(path.join(fixture.selfActionsPath, "self-actions")), - false, - ); - } finally { - fixture.teardown(); - } -}); - -test("prepare reuses an existing destination without marking it for cleanup", async () => { - const fixture = createFixture(); - const manager = new LocalActionsManager(); - - try { - mkdirSync(fixture.selfActionsPath, { recursive: true }); - writeFileSync( - path.join(fixture.selfActionsPath, "marker.txt"), - "existing\n", - "utf8", - ); - - const result = await manager.prepare({ - sourcePath: fixture.actionsDirectory, - workspacePath: fixture.workspaceDirectory, - }); - - assert.equal(result.created, false); - assert.equal( - readFileSync(path.join(fixture.selfActionsPath, "marker.txt"), "utf8"), - "existing\n", - ); - } finally { - fixture.teardown(); - } -}); - -test("cleanup removes the destination only when it was created by the action", async () => { - const fixture = createFixture(); - const manager = new LocalActionsManager(); - - try { - await manager.prepare({ - sourcePath: fixture.actionsDirectory, - workspacePath: fixture.workspaceDirectory, - }); - - assert.equal( - await manager.cleanup({ - created: true, - destinationPath: fixture.selfActionsPath, - }), - true, - ); - assert.equal(existsSync(fixture.selfActionsPath), false); - - assert.equal( - await manager.cleanup({ - created: false, - destinationPath: fixture.selfActionsPath, - }), - false, - ); - } finally { - fixture.teardown(); - } -}); - -test("resolveDestinationPath resolves to workspace parent self-actions", () => { - const manager = new LocalActionsManager(); - - assert.equal( - manager.resolveDestinationPath({ - workspacePath: "/tmp/workspace", - }), - path.resolve("/tmp/workspace", "../self-actions"), - ); -}); - -test("resolveDestinationPath supports custom destination directory name", () => { - const manager = new LocalActionsManager(); - - assert.equal( - manager.resolveDestinationPath({ - workspacePath: "/tmp/workspace", - destinationDirectoryName: "custom-actions", - }), - path.resolve("/tmp/workspace", "../custom-actions"), - ); -}); - -test("resolveDestinationPath rejects destination directory names containing path separators", () => { - const manager = new LocalActionsManager(); - - assert.throws( - () => - manager.resolveDestinationPath({ - workspacePath: "/tmp/workspace", - destinationDirectoryName: "nested/custom-actions", - }), - /destination-directory-name must be a directory name, not a path/, - ); -}); diff --git a/actions/local-actions/README.md b/actions/local-actions/README.md deleted file mode 100644 index 901c96e..0000000 --- a/actions/local-actions/README.md +++ /dev/null @@ -1,118 +0,0 @@ - - -#  GitHub Action: Local actions - -