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 @@ - - -# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY29weSIgY29sb3I9ImJsdWUiPjxyZWN0IHg9IjkiIHk9IjkiIHdpZHRoPSIxMyIgaGVpZ2h0PSIxMyIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PHBhdGggZD0iTTUgMTVINGEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg5YTIgMiAwIDAgMSAyIDJ2MSI+PC9wYXRoPjwvc3ZnPg==) GitHub Action: Local actions - -
- Local actions -
- ---- - - - - - - -[![Marketplace](https://img.shields.io/badge/Marketplace-local--actions-blue?logo=github-actions)](https://github.com/marketplace/actions/local-actions) -[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-common)](https://github.com/hoverkraft-tech/ci-github-common/releases) -[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-common)](http://choosealicense.com/licenses/mit/) -[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-common?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-common?style=social) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-common/blob/main/CONTRIBUTING.md) -![GitHub Verified Creator](https://img.shields.io/badge/GitHub-Verified%20Creator-4493F8?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJyZ2IoNjgsIDE0NywgMjQ4KSI+CiAgPHBhdGggZD0ibTkuNTg1LjUyLjkyOS42OGMuMTUzLjExMi4zMzEuMTg2LjUxOC4yMTVsMS4xMzguMTc1YTIuNjc4IDIuNjc4IDAgMCAxIDIuMjQgMi4yNGwuMTc0IDEuMTM5Yy4wMjkuMTg3LjEwMy4zNjUuMjE1LjUxOGwuNjguOTI4YTIuNjc3IDIuNjc3IDAgMCAxIDAgMy4xN2wtLjY4LjkyOGExLjE3NCAxLjE3NCAwIDAgMC0uMjE1LjUxOGwtLjE3NSAxLjEzOGEyLjY3OCAyLjY3OCAwIDAgMS0yLjI0MSAyLjI0MWwtMS4xMzguMTc1YTEuMTcgMS4xNyAwIDAgMC0uNTE4LjIxNWwtLjkyOC42OGEyLjY3NyAyLjY3NyAwIDAgMS0zLjE3IDBsLS45MjgtLjY4YTEuMTc0IDEuMTc0IDAgMCAwLS41MTgtLjIxNUwzLjgzIDE0LjQxYTIuNjc4IDIuNjc4IDAgMCAxLTIuMjQtMi4yNGwtLjE3NS0xLjEzOGExLjE3IDEuMTcgMCAwIDAtLjIxNS0uNTE4bC0uNjgtLjkyOGEyLjY3NyAyLjY3NyAwIDAgMSAwLTMuMTdsLjY4LS45MjhjLjExMi0uMTUzLjE4Ni0uMzMxLjIxNS0uNTE4bC4xNzUtMS4xNGEyLjY3OCAyLjY3OCAwIDAgMSAyLjI0LTIuMjRsMS4xMzktLjE3NWMuMTg3LS4wMjkuMzY1LS4xMDMuNTE4LS4yMTVsLjkyOC0uNjhhMi42NzcgMi42NzcgMCAwIDEgMy4xNyAwWk03LjMwMyAxLjcyOGwtLjkyNy42OGEyLjY3IDIuNjcgMCAwIDEtMS4xOC40ODlsLTEuMTM3LjE3NGExLjE3OSAxLjE3OSAwIDAgMC0uOTg3Ljk4N2wtLjE3NCAxLjEzNmEyLjY3NyAyLjY3NyAwIDAgMS0uNDg5IDEuMThsLS42OC45MjhhMS4xOCAxLjE4IDAgMCAwIDAgMS4zOTRsLjY4LjkyN2MuMjU2LjM0OC40MjQuNzUzLjQ4OSAxLjE4bC4xNzQgMS4xMzdjLjA3OC41MDkuNDc4LjkwOS45ODcuOTg3bDEuMTM2LjE3NGEyLjY3IDIuNjcgMCAwIDEgMS4xOC40ODlsLjkyOC42OGMuNDE0LjMwNS45NzkuMzA1IDEuMzk0IDBsLjkyNy0uNjhhMi42NyAyLjY3IDAgMCAxIDEuMTgtLjQ4OWwxLjEzNy0uMTc0YTEuMTggMS4xOCAwIDAgMCAuOTg3LS45ODdsLjE3NC0xLjEzNmEyLjY3IDIuNjcgMCAwIDEgLjQ4OS0xLjE4bC42OC0uOTI4YTEuMTc2IDEuMTc2IDAgMCAwIDAtMS4zOTRsLS42OC0uOTI3YTIuNjg2IDIuNjg2IDAgMCAxLS40ODktMS4xOGwtLjE3NC0xLjEzN2ExLjE3OSAxLjE3OSAwIDAgMC0uOTg3LS45ODdsLTEuMTM2LS4xNzRhMi42NzcgMi42NzcgMCAwIDEtMS4xOC0uNDg5bC0uOTI4LS42OGExLjE3NiAxLjE3NiAwIDAgMC0xLjM5NCAwWk0xMS4yOCA2Ljc4bC0zLjc1IDMuNzVhLjc1Ljc1IDAgMCAxLTEuMDYgMEw0LjcyIDguNzhhLjc1MS43NTEgMCAwIDEgLjAxOC0xLjA0Mi43NTEuNzUxIDAgMCAxIDEuMDQyLS4wMThMNyA4Ljk0bDMuMjItMy4yMmEuNzUxLjc1MSAwIDAgMSAxLjA0Mi4wMTguNzUxLjc1MSAwIDAgMSAuMDE4IDEuMDQyWiI+PC9wYXRoPgo8L3N2Zz4K) - - - - - - -## Overview - -Action to expose sibling local actions next to the current action directory. -It creates a symlink to the parent actions directory at `../` relative to `github.workspace` -during the main step and removes it automatically in the post step. - - - - -## Usage - -```yaml -- uses: hoverkraft-tech/ci-github-common/actions/local-actions@3a27d31e9ccefbe9609cc9165017ed100ff34a22 # 0.38.0 - with: - # The actions root path that contains the sibling local actions. - # Pass the caller actions root, typically by appending `/../..` to `github.action_path`. - # - # This input is required. - source-path: "" - - # Destination directory name created next to `github.workspace`. - # Use only a directory name (no path separators). - # - # Default: `self-actions` - destination-directory-name: self-actions -``` - - - - -## Inputs - -| **Input** | **Description** | **Required** | **Default** | -| -------------------------------- | -------------------------------------------------------------------------------------- | ------------ | -------------- | -| **`source-path`** | The actions root path that contains the sibling local actions. | **true** | - | -| | Pass the caller actions root, typically by appending `/../..` to `github.action_path`. | | | -| **`destination-directory-name`** | Destination directory name created next to `github.workspace`. | **false** | `self-actions` | -| | Use only a directory name (no path separators). | | | - - - - - - - - -## Outputs - -| **Output** | **Description** | -| ---------- | ----------------------------------------------------------- | -| **`path`** | The resolved destination path for the copied local actions. | - - - - - - - - -## Contributing - -Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-common/blob/main/CONTRIBUTING.md) for more details. - - - - - - - - -## License - -This project is licensed under the MIT License. - -SPDX-License-Identifier: MIT - -Copyright © 2026 hoverkraft - -For more details, see the [license](http://choosealicense.com/licenses/mit/). - - - - - ---- - -This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). - - - - diff --git a/actions/local-actions/action.yml b/actions/local-actions/action.yml deleted file mode 100644 index 841429d..0000000 --- a/actions/local-actions/action.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "Local actions" -description: | - Action to expose sibling local actions next to the current action directory. - It creates a symlink to the parent actions directory at `../` relative to `github.workspace` - during the main step and removes it automatically in the post step. -# FIXME: This is a workaround until this issue is resolved: https://github.com/actions/runner/issues/1348. -author: hoverkraft -branding: - icon: copy - color: blue - -inputs: - source-path: - description: | - The actions root path that contains the sibling local actions. - Pass the caller actions root, typically by appending `/../..` to `github.action_path`. - required: true - destination-directory-name: - description: | - Destination directory name created next to `github.workspace`. - Use only a directory name (no path separators). - required: false - default: self-actions - -outputs: - path: - description: The resolved destination path for the copied local actions. - -runs: - using: node24 - main: index.js - post: cleanup.js diff --git a/actions/local-actions/cleanup.js b/actions/local-actions/cleanup.js deleted file mode 100644 index 1446fba..0000000 --- a/actions/local-actions/cleanup.js +++ /dev/null @@ -1,22 +0,0 @@ -import { ActionRuntime } from "./ActionRuntime.js"; -import { LocalActionsManager } from "./LocalActionsManager.js"; - -const runtime = new ActionRuntime(); -const manager = new LocalActionsManager(); - -try { - const destinationPath = runtime.getState("local_actions_destination_path"); - const created = runtime.getState("local_actions_created") === "true"; - const cleaned = await manager.cleanup({ - created, - destinationPath, - }); - - if (cleaned) { - runtime.info(`Removed local actions from ${destinationPath}.`); - } else { - runtime.info("Skipped local actions cleanup."); - } -} catch (error) { - runtime.setFailed(error); -} diff --git a/actions/local-actions/index.js b/actions/local-actions/index.js deleted file mode 100644 index f8c0499..0000000 --- a/actions/local-actions/index.js +++ /dev/null @@ -1,44 +0,0 @@ -import { ActionRuntime } from "./ActionRuntime.js"; -import { LocalActionsManager } from "./LocalActionsManager.js"; - -const runtime = new ActionRuntime(); -const manager = new LocalActionsManager(); - -try { - const sourcePath = runtime.getInput("source-path", { required: true }); - const destinationDirectoryName = runtime.getInput( - "destination-directory-name", - ); - const workspacePath = runtime.getWorkspace(); - const sourceDirectory = await manager.resolveSourceDirectory({ sourcePath }); - const destinationPath = manager.resolveDestinationPath({ - workspacePath, - destinationDirectoryName, - }); - - runtime.info(`Resolved local actions source: ${sourceDirectory}.`); - runtime.info(`Resolved local actions destination: ${destinationPath}.`); - - const result = await manager.prepare({ - sourcePath, - workspacePath, - destinationDirectoryName, - }); - - await runtime.setOutput("path", result.destinationPath); - await runtime.saveState("local_actions_created", String(result.created)); - await runtime.saveState( - "local_actions_destination_path", - result.destinationPath, - ); - - if (result.created) { - runtime.info(`Copied local actions to ${result.destinationPath}.`); - } else { - runtime.info( - `Local actions already available at ${result.destinationPath}.`, - ); - } -} catch (error) { - runtime.setFailed(error); -} diff --git a/actions/local-actions/index.test.js b/actions/local-actions/index.test.js deleted file mode 100644 index 637a6ae..0000000 --- a/actions/local-actions/index.test.js +++ /dev/null @@ -1,188 +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 assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import test from "node:test"; - -const packageDirectory = path.dirname(new URL(import.meta.url).pathname); - -const createFixture = () => { - const sandboxDirectory = mkdtempSync( - path.join(os.tmpdir(), "local-actions-script-"), - ); - 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"); - const outputFile = path.join(sandboxDirectory, "github-output.txt"); - const stateFile = path.join(sandboxDirectory, "github-state.txt"); - - 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", - ); - writeFileSync(outputFile, "", "utf8"); - writeFileSync(stateFile, "", "utf8"); - - return { - actionsDirectory, - currentActionPath, - outputFile, - sandboxDirectory, - selfActionsPath: path.join(sandboxDirectory, "self-actions"), - stateFile, - workspaceDirectory, - teardown() { - rmSync(sandboxDirectory, { force: true, recursive: true }); - }, - }; -}; - -const runNodeScript = (scriptPath, env) => - spawnSync(process.execPath, [scriptPath], { - cwd: packageDirectory, - encoding: "utf8", - env: { - ...process.env, - ...env, - }, - }); - -test("index.js writes outputs, creates a relative symlink, and saves state", () => { - const fixture = createFixture(); - - try { - const result = runNodeScript(path.join(packageDirectory, "index.js"), { - GITHUB_WORKSPACE: fixture.workspaceDirectory, - GITHUB_OUTPUT: fixture.outputFile, - GITHUB_STATE: fixture.stateFile, - "INPUT_SOURCE-PATH": fixture.actionsDirectory, - }); - - assert.equal(result.status, 0, result.stderr); - assert.match( - result.stdout, - new RegExp( - `Resolved local actions source: ${fixture.actionsDirectory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.`, - ), - ); - assert.match( - result.stdout, - new RegExp( - `Resolved local actions destination: ${fixture.selfActionsPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.`, - ), - ); - assert.equal(lstatSync(fixture.selfActionsPath).isSymbolicLink(), true); - - // Verify relative symlink - const linkTarget = readlinkSync(fixture.selfActionsPath); - assert.equal( - path.isAbsolute(linkTarget), - false, - `Expected relative symlink target, got: ${linkTarget}`, - ); - - assert.equal( - realpathSync(fixture.selfActionsPath), - fixture.actionsDirectory, - ); - assert.equal( - existsSync( - path.join(fixture.selfActionsPath, "get-issue-number", "action.yml"), - ), - true, - ); - assert.match( - readFileSync(fixture.outputFile, "utf8"), - /^path<<.+\n.*self-actions\n.+\n$/s, - ); - assert.match( - readFileSync(fixture.stateFile, "utf8"), - /local_actions_created<<.+\ntrue\n.+\n/s, - ); - assert.match( - readFileSync(fixture.stateFile, "utf8"), - /local_actions_destination_path<<.+\n.*self-actions\n.+\n/s, - ); - } finally { - fixture.teardown(); - } -}); - -test("cleanup.js removes the created destination from saved state", () => { - const fixture = createFixture(); - - try { - mkdirSync(fixture.selfActionsPath, { recursive: true }); - writeFileSync( - path.join(fixture.selfActionsPath, "marker.txt"), - "cleanup\n", - "utf8", - ); - - const result = runNodeScript(path.join(packageDirectory, "cleanup.js"), { - STATE_local_actions_created: "true", - STATE_local_actions_destination_path: fixture.selfActionsPath, - }); - - assert.equal(result.status, 0, result.stderr); - assert.equal(existsSync(fixture.selfActionsPath), false); - } finally { - fixture.teardown(); - } -}); - -test("index.js supports a custom destination-directory-name input", () => { - const fixture = createFixture(); - const customDestinationPath = path.join( - fixture.sandboxDirectory, - "my-actions", - ); - - try { - const result = runNodeScript(path.join(packageDirectory, "index.js"), { - GITHUB_WORKSPACE: fixture.workspaceDirectory, - GITHUB_OUTPUT: fixture.outputFile, - GITHUB_STATE: fixture.stateFile, - "INPUT_SOURCE-PATH": fixture.actionsDirectory, - "INPUT_DESTINATION-DIRECTORY-NAME": "my-actions", - }); - - assert.equal(result.status, 0, result.stderr); - assert.equal(lstatSync(customDestinationPath).isSymbolicLink(), true); - assert.equal(realpathSync(customDestinationPath), fixture.actionsDirectory); - assert.match( - readFileSync(fixture.outputFile, "utf8"), - /^path<<.+\n.*my-actions\n.+\n$/s, - ); - assert.match( - readFileSync(fixture.stateFile, "utf8"), - /local_actions_destination_path<<.+\n.*my-actions\n.+\n/s, - ); - } finally { - fixture.teardown(); - } -}); diff --git a/actions/local-actions/package-lock.json b/actions/local-actions/package-lock.json deleted file mode 100644 index 9901373..0000000 --- a/actions/local-actions/package-lock.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "local-actions", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "local-actions", - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=24.0.0" - } - } - } -} diff --git a/actions/local-actions/package.json b/actions/local-actions/package.json deleted file mode 100644 index e1dfa95..0000000 --- a/actions/local-actions/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "local-actions", - "version": "1.0.0", - "description": "Expose sibling local GitHub Actions with automatic post cleanup", - "scripts": { - "test": "node --test", - "test:watch": "node --test --watch", - "test:ci": "node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test" - }, - "author": "hoverkraft", - "license": "MIT", - "type": "module", - "engines": { - "node": ">=24.0.0" - } -} diff --git a/actions/local-workflow-actions/README.md b/actions/local-workflow-actions/README.md deleted file mode 100644 index bf46daa..0000000 --- a/actions/local-workflow-actions/README.md +++ /dev/null @@ -1,131 +0,0 @@ - - -# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZG93bmxvYWQiIGNvbG9yPSJibHVlIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiPjwvcG9seWxpbmU+PGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjMiPjwvbGluZT48L3N2Zz4=) GitHub Action: Local workflow actions - -
- Local workflow actions -
- ---- - - - - - - - -[![Marketplace](https://img.shields.io/badge/Marketplace-local--workflow--actions-blue?logo=github-actions)](https://github.com/marketplace/actions/local-workflow-actions) -[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-common)](https://github.com/hoverkraft-tech/ci-github-common/releases) -[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-common)](http://choosealicense.com/licenses/mit/) -[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-common?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-common?style=social) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-common/blob/main/CONTRIBUTING.md) -![GitHub Verified Creator](https://img.shields.io/badge/GitHub-Verified%20Creator-4493F8?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJyZ2IoNjgsIDE0NywgMjQ4KSI+CiAgPHBhdGggZD0ibTkuNTg1LjUyLjkyOS42OGMuMTUzLjExMi4zMzEuMTg2LjUxOC4yMTVsMS4xMzguMTc1YTIuNjc4IDIuNjc4IDAgMCAxIDIuMjQgMi4yNGwuMTc0IDEuMTM5Yy4wMjkuMTg3LjEwMy4zNjUuMjE1LjUxOGwuNjguOTI4YTIuNjc3IDIuNjc3IDAgMCAxIDAgMy4xN2wtLjY4LjkyOGExLjE3NCAxLjE3NCAwIDAgMC0uMjE1LjUxOGwtLjE3NSAxLjEzOGEyLjY3OCAyLjY3OCAwIDAgMS0yLjI0MSAyLjI0MWwtMS4xMzguMTc1YTEuMTcgMS4xNyAwIDAgMC0uNTE4LjIxNWwtLjkyOC42OGEyLjY3NyAyLjY3NyAwIDAgMS0zLjE3IDBsLS45MjgtLjY4YTEuMTc0IDEuMTc0IDAgMCAwLS41MTgtLjIxNUwzLjgzIDE0LjQxYTIuNjc4IDIuNjc4IDAgMCAxLTIuMjQtMi4yNGwtLjE3NS0xLjEzOGExLjE3IDEuMTcgMCAwIDAtLjIxNS0uNTE4bC0uNjgtLjkyOGEyLjY3NyAyLjY3NyAwIDAgMSAwLTMuMTdsLjY4LS45MjhjLjExMi0uMTUzLjE4Ni0uMzMxLjIxNS0uNTE4bC4xNzUtMS4xNGEyLjY3OCAyLjY3OCAwIDAgMSAyLjI0LTIuMjRsMS4xMzktLjE3NWMuMTg3LS4wMjkuMzY1LS4xMDMuNTE4LS4yMTVsLjkyOC0uNjhhMi42NzcgMi42NzcgMCAwIDEgMy4xNyAwWk03LjMwMyAxLjcyOGwtLjkyNy42OGEyLjY3IDIuNjcgMCAwIDEtMS4xOC40ODlsLTEuMTM3LjE3NGExLjE3OSAxLjE3OSAwIDAgMC0uOTg3Ljk4N2wtLjE3NCAxLjEzNmEyLjY3NyAyLjY3NyAwIDAgMS0uNDg5IDEuMThsLS42OC45MjhhMS4xOCAxLjE4IDAgMCAwIDAgMS4zOTRsLjY4LjkyN2MuMjU2LjM0OC40MjQuNzUzLjQ4OSAxLjE4bC4xNzQgMS4xMzdjLjA3OC41MDkuNDc4LjkwOS45ODcuOTg3bDEuMTM2LjE3NGEyLjY3IDIuNjcgMCAwIDEgMS4xOC40ODlsLjkyOC42OGMuNDE0LjMwNS45NzkuMzA1IDEuMzk0IDBsLjkyNy0uNjhhMi42NyAyLjY3IDAgMCAxIDEuMTgtLjQ4OWwxLjEzNy0uMTc0YTEuMTggMS4xOCAwIDAgMCAuOTg3LS45ODdsLjE3NC0xLjEzNmEyLjY3IDIuNjcgMCAwIDEgLjQ4OS0xLjE4bC42OC0uOTI4YTEuMTc2IDEuMTc2IDAgMCAwIDAtMS4zOTRsLS42OC0uOTI3YTIuNjg2IDIuNjg2IDAgMCAxLS40ODktMS4xOGwtLjE3NC0xLjEzN2ExLjE3OSAxLjE3OSAwIDAgMC0uOTg3LS45ODdsLTEuMTM2LS4xNzRhMi42NzcgMi42NzcgMCAwIDEtMS4xOC0uNDg5bC0uOTI4LS42OGExLjE3NiAxLjE3NiAwIDAgMC0xLjM5NCAwWk0xMS4yOCA2Ljc4bC0zLjc1IDMuNzVhLjc1Ljc1IDAgMCAxLTEuMDYgMEw0LjcyIDguNzhhLjc1MS43NTEgMCAwIDEgLjAxOC0xLjA0Mi43NTEuNzUxIDAgMCAxIDEuMDQyLS4wMThMNyA4Ljk0bDMuMjItMy4yMmEuNzUxLjc1MSAwIDAgMSAxLjA0Mi4wMTguNzUxLjc1MSAwIDAgMSAuMDE4IDEuMDQyWiI+PC9wYXRoPgo8L3N2Zz4K) - - - - - - - -## Overview - -This action checks out the reusable workflow repository that defines the current job and moves it to `../self-workflow` relative to `github.workspace`. -Use it when consuming reusable workflows that reference local actions from the same repository—they are not automatically available in the caller repository and must be synced manually. -Local actions will be available at `../self-workflow/` relative to `github.workspace`. -Example: if `actions-path` is `.github/actions`, then local actions will be available at `../self-workflow/.github/actions`. - - - - - -## Usage - -```yaml -- uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@3a27d31e9ccefbe9609cc9165017ed100ff34a22 # 0.38.0 - with: - # Relative path(s) (inside the workflow repository) containing the local actions to expose in the current workspace. - # The same relative path will be available under `../self-workflow` relative to `github.workspace` (for example `../self-workflow/.github/actions`). - # - # Default: `.github/actions` - actions-path: .github/actions - - # A GitHub token with permissions to checkout the reusable workflow repository. - # This is required when the repository is private. For public repositories, this can be left empty and the checkout will be attempted anonymously. - # - # Default: `${{ github.token }}` - github-token: ${{ github.token }} -``` - - - - - - - -## Inputs - -| **Input** | **Description** | **Required** | **Default** | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | -| **`actions-path`** | Relative path(s) (inside the workflow repository) containing the local actions to expose in the current workspace. | **false** | `.github/actions` | -| | The same relative path will be available under `../self-workflow` relative to `github.workspace` (for example `../self-workflow/.github/actions`). | | | -| **`github-token`** | A GitHub token with permissions to checkout the reusable workflow repository. | **false** | `${{ github.token }}` | -| | This is required when the repository is private. For public repositories, this can be left empty and the checkout will be attempted anonymously. | | | - - - - - - - - - - -## Outputs - -| **Output** | **Description** | -| ---------------- | ---------------------------------------------------------------------------------- | -| **`repository`** | The reusable workflow repository that was checked out, in the format `owner/repo`. | -| **`ref`** | The Git ref or SHA of the reusable workflow repository that was checked out. | -| **`path`** | The resolved path where the reusable workflow actions are available. | - - - - - - - - - - -## Contributing - -Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-common/blob/main/CONTRIBUTING.md) for more details. - - - - - - - - -## License - -This project is licensed under the MIT License. - -SPDX-License-Identifier: MIT - -Copyright © 2026 hoverkraft - -For more details, see the [license](http://choosealicense.com/licenses/mit/). - - - - - ---- - -This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). - - - - diff --git a/actions/local-workflow-actions/action.yml b/actions/local-workflow-actions/action.yml deleted file mode 100644 index 4962e62..0000000 --- a/actions/local-workflow-actions/action.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: "Local workflow actions" -description: | - This action checks out the reusable workflow repository that defines the current job and moves it to `../self-workflow` relative to `github.workspace`. - Use it when consuming reusable workflows that reference local actions from the same repository—they are not automatically available in the caller repository and must be synced manually. - Local actions will be available at `../self-workflow/` relative to `github.workspace`. - Example: if `actions-path` is `.github/actions`, then local actions will be available at `../self-workflow/.github/actions`. - -author: hoverkraft -branding: - icon: download - color: blue - -inputs: - actions-path: - description: | - Relative path(s) (inside the workflow repository) containing the local actions to expose in the current workspace. - The same relative path will be available under `../self-workflow` relative to `github.workspace` (for example `../self-workflow/.github/actions`). - required: false - default: ".github/actions" - github-token: - description: | - A GitHub token with permissions to checkout the reusable workflow repository. - This is required when the repository is private. For public repositories, this can be left empty and the checkout will be attempted anonymously. - required: false - default: ${{ github.token }} - -outputs: - repository: - description: The reusable workflow repository that was checked out, in the format `owner/repo`. - value: ${{ job.workflow_repository }} - ref: - description: The Git ref or SHA of the reusable workflow repository that was checked out. - value: ${{ job.workflow_sha }} - path: - description: The resolved path where the reusable workflow actions are available. - value: ${{ steps.prepare.outputs.path }} - -runs: - using: "composite" - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: ./.self-workflow-source - repository: ${{ job.workflow_repository }} - ref: ${{ job.workflow_sha }} - sparse-checkout: | - ${{ inputs.actions-path }} - token: ${{ inputs.github-token || github.token }} - - - id: prepare - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const fs = require('fs'); - const path = require('path'); - const workspacePath = process.env.GITHUB_WORKSPACE; - if (!workspacePath) { - throw new Error('GITHUB_WORKSPACE is not set.'); - } - - const sourcePath = path.resolve(workspacePath, '.self-workflow-source'); - const destinationPath = path.resolve(workspacePath, '../self-workflow'); - core.setOutput('path', destinationPath); - - if (fs.existsSync(destinationPath)) { - fs.rmSync(sourcePath, { recursive: true, force: true }); - core.info(`Reusable workflow actions already available at ${destinationPath}.`); - return; - } - - fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); - fs.renameSync(sourcePath, destinationPath); - core.info(`Moved reusable workflow actions to ${destinationPath}.`);