hammerkit 1.5.1 → 1.6.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/FEEDBACK.md +74 -0
- package/.agents/skills/before-commit/SKILL.md +101 -0
- package/.agents/skills/do-the-work/SKILL.md +64 -0
- package/.agents/skills/fix-ci/SKILL.md +98 -0
- package/.agents/skills/speckit-git-commit/SKILL.md +53 -0
- package/.agents/skills/speckit-git-feature/SKILL.md +72 -0
- package/.agents/skills/speckit-git-initialize/SKILL.md +54 -0
- package/.agents/skills/speckit-git-remote/SKILL.md +50 -0
- package/.agents/skills/speckit-git-validate/SKILL.md +54 -0
- package/.gitbook.yaml +5 -0
- package/.specify/extensions/.registry +37 -0
- package/.specify/extensions/git/README.md +100 -0
- package/.specify/extensions/git/commands/speckit.git.commit.md +48 -0
- package/.specify/extensions/git/commands/speckit.git.feature.md +67 -0
- package/.specify/extensions/git/commands/speckit.git.initialize.md +49 -0
- package/.specify/extensions/git/commands/speckit.git.remote.md +45 -0
- package/.specify/extensions/git/commands/speckit.git.validate.md +49 -0
- package/.specify/extensions/git/config-template.yml +62 -0
- package/.specify/extensions/git/extension.yml +140 -0
- package/.specify/extensions/git/git-config.yml +62 -0
- package/.specify/extensions.yml +149 -0
- package/.specify/feature.json +3 -0
- package/.specify/init-options.json +10 -0
- package/.specify/integration.json +15 -0
- package/.specify/integrations/claude.manifest.json +16 -0
- package/.specify/integrations/speckit.manifest.json +17 -0
- package/.specify/memory/constitution.md +162 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/constitution-template.md +50 -0
- package/.specify/templates/plan-template.md +113 -0
- package/.specify/templates/spec-template.md +131 -0
- package/.specify/templates/tasks-template.md +252 -0
- package/.specify/workflows/speckit/workflow.yml +77 -0
- package/.specify/workflows/workflow-registry.json +13 -0
- package/AGENTS.md +94 -0
- package/Dockerfile +3 -3
- package/FOLLOWUPS.md +34 -0
- package/README.md +9 -7
- package/SUMMARY.md +30 -5
- package/VALUES.md +53 -0
- package/build.schema.json +542 -408
- package/dist/cache/auto-pull.spec.js +123 -0
- package/dist/cache/backends/local-cache-backend.js +56 -0
- package/dist/cache/backends/local-cache-backend.spec.js +72 -0
- package/dist/cache/backends/s3-cache-backend.js +124 -0
- package/dist/cache/backends/s3-cache-backend.spec.js +118 -0
- package/dist/cache/cache-backend-registry.js +21 -0
- package/dist/cache/cache-backend-registry.spec.js +22 -0
- package/dist/cache/cache-backend.js +3 -0
- package/dist/cache/resolve-cache.js +60 -0
- package/dist/cache/resolve-cache.spec.js +52 -0
- package/dist/cli.js +112 -82
- package/dist/cli.spec.js +20 -2
- package/dist/console/get-logger.spec.js +23 -0
- package/dist/docker/get-default-platform.spec.js +29 -0
- package/dist/docker/longest-common-prefix.spec.js +26 -0
- package/dist/docker/package.js +283 -0
- package/dist/docker/package.spec.js +145 -0
- package/dist/docker/pull.js +4 -4
- package/dist/docker/remove-container.spec.js +32 -0
- package/dist/docker/stream.js +3 -3
- package/dist/docker/using-container.js +29 -11
- package/dist/docker/using-container.spec.js +90 -0
- package/dist/environment/merge-environment-variables.js +21 -0
- package/dist/environment/merge-environment-variables.spec.js +24 -0
- package/dist/environment/replace-env-variables.js +43 -10
- package/dist/environment/replace-env-variables.spec.js +47 -0
- package/dist/executer/abort.spec.js +18 -0
- package/dist/executer/await-completed-dependencies.js +56 -0
- package/dist/executer/cache-metadata.js +11 -0
- package/dist/executer/check-readiness.js +1 -1
- package/dist/{planner/work-node-port.js → executer/container-bind.js} +1 -1
- package/dist/executer/docker-service.js +110 -86
- package/dist/executer/docker-task.js +132 -0
- package/dist/executer/environment-mock.js +3 -4
- package/dist/executer/environment-mock.spec.js +22 -0
- package/dist/executer/event-cache.js +60 -127
- package/dist/executer/execute-command.js +4 -3
- package/dist/executer/execute-docker.js +32 -14
- package/dist/executer/execute-work-service.js +61 -0
- package/dist/executer/execute-work-task.js +107 -0
- package/dist/executer/execute-work-tree.js +27 -0
- package/dist/executer/execute.spec.js +14 -10
- package/dist/executer/execution-steps.js +47 -31
- package/dist/executer/get-container-binds.js +19 -0
- package/dist/executer/get-docker-executor.js +12 -12
- package/dist/executer/get-scheduler-down-result.js +12 -0
- package/dist/executer/get-scheduler-execute-result.js +28 -0
- package/dist/executer/get-scheduler-up-result.js +12 -0
- package/dist/executer/get-service-containers.js +14 -0
- package/dist/executer/get-service-containers.spec.js +59 -0
- package/dist/executer/get-service-env-hints.js +35 -0
- package/dist/executer/get-service-env-hints.spec.js +65 -0
- package/dist/executer/kubernetes-service.js +116 -48
- package/dist/executer/label-values.js +29 -46
- package/dist/executer/label-values.spec.js +61 -0
- package/dist/executer/lazy-resolver.js +16 -0
- package/dist/executer/lazy-resolver.spec.js +20 -0
- package/dist/executer/local-task.js +44 -0
- package/dist/executer/process-manager.js +16 -70
- package/dist/executer/process-manager.spec.js +39 -76
- package/dist/executer/reset-work-tree.js +30 -0
- package/dist/executer/scheduler/check-for-loop.js +17 -7
- package/dist/executer/scheduler/{scheduler-state.js → dependency-state.js} +1 -1
- package/dist/executer/scheduler/enqueue-next.js +83 -19
- package/dist/executer/scheduler/enqueue-next.spec.js +136 -0
- package/dist/executer/scheduler/{node-state.js → need-state.js} +1 -1
- package/dist/{parser/build-file.js → executer/scheduler/task-state.js} +1 -1
- package/dist/executer/set-user-permission.js +1 -1
- package/dist/executer/state-resolver.js +61 -0
- package/dist/executer/state-resolver.spec.js +37 -0
- package/dist/executer/state.js +37 -31
- package/dist/executer/state.spec.js +66 -0
- package/dist/executer/states.spec.js +14 -0
- package/dist/executer/watch-loop.js +68 -0
- package/dist/executer/watch-state-key.js +44 -0
- package/dist/executer/work-scope.js +1 -5
- package/dist/file/get-file-context.js +24 -3
- package/dist/file/get-file-context.spec.js +178 -0
- package/dist/file/move-files.js +2 -2
- package/dist/file/move-files.spec.js +48 -0
- package/dist/index.js +7 -8
- package/dist/json-schema.js +12 -0
- package/dist/kubernetes/apply.js +34 -0
- package/dist/kubernetes/await-running-state.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +53 -0
- package/dist/kubernetes/ensure-http-route.spec.js +70 -0
- package/dist/kubernetes/ensure-ingress.js +54 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +105 -0
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +40 -0
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +36 -0
- package/dist/kubernetes/ensure-namespace.js +43 -0
- package/dist/kubernetes/ensure-namespace.spec.js +39 -0
- package/dist/kubernetes/ensure-persistent-data.js +158 -0
- package/dist/kubernetes/get-service-ip.js +11 -0
- package/dist/kubernetes/kubernetes-instance.js +48 -0
- package/dist/kubernetes/remove-persistent-data.js +19 -0
- package/dist/kubernetes/resolve-pod-name.js +42 -0
- package/dist/kubernetes/resolve-pod-name.spec.js +81 -0
- package/dist/kubernetes/resources.js +12 -0
- package/dist/kubernetes/restore-kubernetes-data.js +42 -0
- package/dist/kubernetes/store-kubernetes-data.js +39 -0
- package/dist/kubernetes/volumes.js +64 -0
- package/dist/log.js +69 -69
- package/dist/log.spec.js +131 -0
- package/dist/logging/grouped-logger.js +17 -15
- package/dist/logging/grouped-logger.spec.js +84 -0
- package/dist/logging/interactive-logger.js +1 -1
- package/dist/logging/interactive-logger.spec.js +57 -0
- package/dist/logging/live-logger.js +2 -2
- package/dist/logging/live-logger.spec.js +37 -0
- package/dist/logging/loggers.spec.js +154 -0
- package/dist/optimizer/calculate-checksum.spec.js +34 -0
- package/dist/optimizer/get-cache-directory.js +6 -6
- package/dist/optimizer/get-cache-directory.spec.js +59 -0
- package/dist/optimizer/get-work-cache-stats.js +53 -0
- package/dist/optimizer/get-work-cache-stats.spec.js +79 -0
- package/dist/{parser/build-file-task.js → optimizer/work-cache-stats.js} +1 -1
- package/dist/optimizer/work-task-cache-description.js +25 -0
- package/dist/optimizer/work-task-cache-description.spec.js +45 -0
- package/dist/parser/default-build-file.js +14 -7
- package/dist/parser/default-build-file.spec.js +48 -0
- package/dist/parser/parse-label-arguments.spec.js +27 -0
- package/dist/parser/read-build-file.js +2 -13
- package/dist/parser/read-build-file.spec.js +89 -0
- package/dist/parser/read-env-file.js +4 -5
- package/dist/parser/read-env-file.spec.js +42 -0
- package/dist/planner/utils/append-work-dependencies.js +55 -0
- package/dist/planner/utils/append-work-environment.js +22 -0
- package/dist/planner/utils/append-work-needs.js +18 -0
- package/dist/planner/utils/append-work-service.js +107 -0
- package/dist/planner/utils/append-work-service.spec.js +71 -0
- package/dist/planner/utils/append-work-task.js +81 -0
- package/dist/planner/utils/get-container-user.js +14 -0
- package/dist/planner/utils/get-container-user.spec.js +30 -0
- package/dist/planner/utils/get-default-kube-config.js +10 -0
- package/dist/planner/utils/get-default-kube-config.spec.js +11 -0
- package/dist/planner/utils/get-work-runtime.js +44 -0
- package/dist/planner/utils/normalize-path.js +1 -1
- package/dist/planner/utils/normalize-path.spec.js +24 -0
- package/dist/planner/utils/parse-work-command.js +80 -0
- package/dist/planner/utils/parse-work-command.spec.js +21 -0
- package/dist/planner/utils/parse-work-generate.js +38 -0
- package/dist/planner/utils/parse-work-generate.spec.js +45 -0
- package/dist/planner/utils/{parse-work-node-mount.js → parse-work-mount.js} +10 -7
- package/dist/planner/utils/{parse-work-node-mount.spec.js → parse-work-mount.spec.js} +14 -11
- package/dist/planner/utils/parse-work-mounts.js +13 -0
- package/dist/planner/utils/parse-work-mounts.spec.js +19 -0
- package/dist/planner/utils/{parse-work-node-port.js → parse-work-port.js} +11 -8
- package/dist/planner/utils/parse-work-port.spec.js +24 -0
- package/dist/planner/utils/parse-work-ports.js +10 -0
- package/dist/planner/utils/parse-work-ports.spec.js +24 -0
- package/dist/planner/utils/parse-work-source.js +72 -0
- package/dist/planner/utils/parse-work-source.spec.js +58 -0
- package/dist/planner/utils/parse-work-volume.js +45 -0
- package/dist/planner/utils/parse-work-volume.spec.js +18 -0
- package/dist/planner/utils/plan-work-tasks.js +41 -0
- package/dist/planner/utils/plan-work-tasks.spec.js +30 -0
- package/dist/planner/utils/plan-work-volume.js +1 -0
- package/dist/planner/utils/plan-work-volume.spec.js +33 -0
- package/dist/planner/utils/split-name.js +2 -2
- package/dist/planner/utils/split-name.spec.js +16 -0
- package/dist/planner/utils/template-value.js +1 -2
- package/dist/planner/utils/template-value.spec.js +14 -0
- package/dist/planner/validate.js +71 -38
- package/dist/planner/validate.spec.js +251 -0
- package/dist/planner/work-cache-id.js +12 -0
- package/dist/planner/work-cache-id.spec.js +173 -0
- package/dist/planner/work-command.js +3 -0
- package/dist/planner/work-environment.js +3 -0
- package/dist/planner/work-healthcheck.js +3 -0
- package/dist/planner/{work-node-status.js → work-item-status.js} +16 -62
- package/dist/{parser/build-file-service.js → planner/work-item-validation.js} +1 -1
- package/dist/planner/work-item.js +16 -0
- package/dist/{optimizer/work-node-cache-stats.js → planner/work-kubernetes-selector.js} +1 -1
- package/dist/planner/{work-nodes.js → work-port.js} +1 -1
- package/dist/planner/work-runtime-docker.js +224 -0
- package/dist/planner/work-runtime-kubernetes.js +264 -0
- package/dist/planner/work-runtime-local.js +109 -0
- package/dist/planner/work-runtime-local.spec.js +193 -0
- package/dist/planner/work-service-id.js +15 -5
- package/dist/planner/work-source.js +3 -0
- package/dist/planner/work-task.js +8 -0
- package/dist/program-commands.spec.js +84 -0
- package/dist/program.js +219 -73
- package/dist/program.spec.js +34 -11
- package/dist/run-program.js +28 -0
- package/dist/{executer/process.js → runtime/runtime.js} +1 -1
- package/dist/schema/build-file-base-task-schema.js +27 -0
- package/dist/schema/build-file-container-service-schema.js +28 -0
- package/dist/schema/build-file-container-task-schema.js +13 -0
- package/dist/schema/build-file-environment-schema-ingress.js +14 -0
- package/dist/schema/build-file-environment-schema.js +23 -0
- package/dist/schema/build-file-kubernetes-service-schema.js +17 -0
- package/dist/schema/build-file-kubernetes-service-selector-schema.js +9 -0
- package/dist/schema/build-file-local-platform-schema.js +9 -0
- package/dist/schema/build-file-local-task-schema.js +12 -0
- package/dist/schema/build-file-need-schema.js +12 -0
- package/dist/schema/build-file-schema.js +23 -0
- package/dist/schema/build-file-schema.spec.js +61 -0
- package/dist/schema/build-file-service-container-healthcheck.js +8 -0
- package/dist/schema/build-file-service-schema.js +15 -0
- package/dist/schema/build-file-task-command-schema.js +12 -0
- package/dist/schema/build-file-task-schema.js +12 -0
- package/dist/schema/build-file-volume-schema.js +14 -0
- package/dist/schema/cache-schema.js +31 -0
- package/dist/schema/doc-build-files.spec.js +223 -0
- package/dist/schema/envs-schema.js +6 -0
- package/dist/schema/labels-schema.js +6 -0
- package/dist/schema/parse-context.js +3 -0
- package/dist/schema/parse-error.js +12 -0
- package/dist/schema/parse-error.spec.js +18 -0
- package/dist/schema/reference-parser.js +209 -0
- package/dist/schema/reference-parser.spec.js +83 -0
- package/dist/schema/schema-parser.js +66 -0
- package/dist/schema/schema-parser.spec.js +80 -0
- package/dist/schema/type-guards.spec.js +23 -0
- package/dist/schema/work-scope-parser.js +77 -0
- package/dist/schema/work-scope-parser.spec.js +66 -0
- package/dist/service/update-service-status.js +11 -0
- package/dist/service/update-service-status.spec.js +27 -0
- package/dist/testing/ensure-local-registry.js +134 -0
- package/dist/testing/example-test-suite.js +16 -18
- package/dist/testing/expect.js +46 -22
- package/dist/testing/http-get.js +21 -0
- package/dist/testing/integration/cache.spec.js +13 -14
- package/dist/testing/integration/cancellation.spec.js +14 -9
- package/dist/testing/integration/clean.spec.js +14 -11
- package/dist/testing/integration/cmd.spec.js +1 -1
- package/dist/testing/integration/concurrency.spec.js +3 -3
- package/dist/testing/integration/docker-package-registry.spec.js +44 -0
- package/dist/testing/integration/docker-service-crash.spec.js +36 -0
- package/dist/testing/integration/docker.spec.js +4 -3
- package/dist/testing/integration/env.spec.js +9 -8
- package/dist/testing/integration/environment.spec.js +97 -0
- package/dist/testing/integration/error.spec.js +5 -4
- package/dist/testing/integration/export.spec.js +22 -0
- package/dist/testing/integration/extend.spec.js +9 -12
- package/dist/testing/integration/glob.spec.js +12 -20
- package/dist/testing/integration/include.spec.js +3 -3
- package/dist/testing/integration/invalid.spec.js +2 -3
- package/dist/testing/integration/invalid_loop.spec.js +49 -6
- package/dist/testing/integration/k8s-state-cache.spec.js +41 -0
- package/dist/testing/integration/kubernetes-service.spec.js +148 -0
- package/dist/testing/integration/kubernetes.spec.js +93 -9
- package/dist/testing/integration/labels.spec.js +6 -11
- package/dist/testing/integration/local.spec.js +1 -1
- package/dist/testing/integration/monorepo.spec.js +4 -4
- package/dist/testing/integration/multi-line.spec.js +3 -3
- package/dist/testing/integration/reference.spec.js +4 -5
- package/dist/testing/integration/services.spec.js +22 -4
- package/dist/testing/integration/store.spec.js +15 -16
- package/dist/testing/integration/unknown.spec.js +20 -18
- package/dist/testing/integration/validate.spec.js +143 -26
- package/dist/testing/integration/watch-dependency.spec.js +11 -8
- package/dist/testing/integration/watch.spec.js +15 -16
- package/dist/testing/requires-kubernetes.js +18 -0
- package/dist/testing/requires-linux-containers.js +18 -0
- package/dist/testing/requires-windows-containers.js +18 -0
- package/dist/testing/test-case.js +75 -0
- package/dist/testing/test-streams.js +28 -0
- package/dist/testing/testing-timeout.js +32 -0
- package/dist/utils/abort-event.js +10 -4
- package/dist/utils/abort-event.spec.js +46 -0
- package/dist/utils/buffer-context.js +44 -0
- package/dist/utils/buffer-context.spec.js +38 -0
- package/dist/utils/ci.spec.js +53 -0
- package/dist/utils/emitter.spec.js +34 -0
- package/dist/utils/empty-writable.spec.js +18 -0
- package/dist/utils/environment-config.js +4 -1
- package/dist/utils/environment-config.spec.js +26 -0
- package/dist/utils/fail-never.spec.js +9 -0
- package/dist/version.js +9 -0
- package/dist/version.spec.js +12 -0
- package/package.json +41 -23
- package/specs/build-file/caches/spec.md +114 -0
- package/specs/build-file/environment-variables/spec.md +82 -0
- package/specs/build-file/includes/spec.md +60 -0
- package/specs/build-file/references/spec.md +56 -0
- package/specs/build-file/spec.md +62 -0
- package/specs/cli/clean/spec.md +85 -0
- package/specs/cli/down/spec.md +85 -0
- package/specs/cli/execute/spec.md +101 -0
- package/specs/cli/init/spec.md +64 -0
- package/specs/cli/ls/spec.md +66 -0
- package/specs/cli/package/spec.md +110 -0
- package/specs/cli/spec.md +73 -0
- package/specs/cli/store-restore/spec.md +74 -0
- package/specs/cli/up/spec.md +80 -0
- package/specs/cli/validate/spec.md +70 -0
- package/specs/labels/spec.md +91 -0
- package/specs/service/container/spec.md +120 -0
- package/specs/service/kubernetes/spec.md +97 -0
- package/specs/service/spec.md +105 -0
- package/specs/task/caching/spec.md +107 -0
- package/specs/task/container/spec.md +118 -0
- package/specs/task/dependencies/spec.md +70 -0
- package/specs/task/extending/spec.md +74 -0
- package/specs/task/kubernetes/spec.md +125 -0
- package/specs/task/needs/spec.md +59 -0
- package/specs/task/spec.md +87 -0
- package/specs/task/watching/spec.md +69 -0
- package/dist/console/console-context-mock.js +0 -14
- package/dist/environment/get-process-env.js +0 -15
- package/dist/executer/create-scheduler-state.js +0 -32
- package/dist/executer/docker-node.js +0 -123
- package/dist/executer/hierarchy-scheduler.js +0 -94
- package/dist/executer/local-node.js +0 -57
- package/dist/executer/schedule.js +0 -137
- package/dist/executer/start-node.js +0 -67
- package/dist/executer/watch-node.js +0 -56
- package/dist/executer/watch-service.js +0 -54
- package/dist/file/get-file-context-mock.js +0 -187
- package/dist/file/get-file-context-mock.spec.js +0 -56
- package/dist/get-work-context.js +0 -29
- package/dist/optimizer/get-work-node-cache-stats.js +0 -104
- package/dist/optimizer/read-work-node-cache.js +0 -19
- package/dist/optimizer/work-node-cache-description.js +0 -32
- package/dist/optimizer/write-work-node-cache.js +0 -21
- package/dist/parser/build-file-task-command.js +0 -3
- package/dist/parser/build-file-task-platform.js +0 -3
- package/dist/parser/build-file-task-source.js +0 -3
- package/dist/parser/get-build-file.js +0 -9
- package/dist/parser/parse-boolean.js +0 -29
- package/dist/parser/parse-boolean.spec.js +0 -48
- package/dist/parser/parse-build-file-references.js +0 -35
- package/dist/parser/parse-build-file-services.js +0 -58
- package/dist/parser/parse-build-file-task-command.js +0 -36
- package/dist/parser/parse-build-file-task-source.js +0 -63
- package/dist/parser/parse-build-file.js +0 -125
- package/dist/parser/parse-context.js +0 -13
- package/dist/parser/parse-envs.js +0 -29
- package/dist/parser/parse-healthcheck.js +0 -28
- package/dist/parser/parse-service-selector.js +0 -39
- package/dist/parser/parse-string-array.js +0 -20
- package/dist/parser/parse-string-map.js +0 -30
- package/dist/parser/parse-string.js +0 -20
- package/dist/planner/utils/get-container-mounts.js +0 -24
- package/dist/planner/utils/parse-work-service-volume.js +0 -23
- package/dist/planner/utils/plan-work-command.js +0 -21
- package/dist/planner/utils/plan-work-dependency.js +0 -36
- package/dist/planner/utils/plan-work-node.js +0 -293
- package/dist/planner/utils/plan-work-nodes.js +0 -78
- package/dist/planner/utils/plan-work-tree.js +0 -15
- package/dist/planner/work-context.js +0 -23
- package/dist/planner/work-node-command.js +0 -3
- package/dist/planner/work-node-id.js +0 -12
- package/dist/planner/work-node-id.spec.js +0 -106
- package/dist/planner/work-node-path.js +0 -2
- package/dist/planner/work-node-source.js +0 -3
- package/dist/planner/work-node-validation.js +0 -3
- package/dist/planner/work-node.js +0 -8
- package/dist/planner/work-service-volume.js +0 -3
- package/dist/planner/work-services.js +0 -3
- package/dist/start-watch-processes.js +0 -28
- package/dist/testing/cli.js +0 -73
- package/dist/testing/create-build-file.js +0 -9
- package/dist/testing/get-logger.js +0 -23
- package/dist/testing/get-node.js +0 -12
- package/dist/testing/get-test-context.js +0 -19
- package/dist/testing/integration/program.spec.js +0 -25
- package/dist/testing/lazy.js +0 -17
- package/dist/testing/start-watch-processes.js +0 -29
- package/dist/testing/test-suite-options.js +0 -3
- package/dist/utils/abortable-function.js +0 -71
- package/logs/cache-console.log +0 -0
- package/logs/cache-status.log +0 -6
- package/logs/cache-stout.log +0 -0
- package/logs/cancellation-console.log +0 -0
- package/logs/cancellation-status.log +0 -3
- package/logs/cancellation-stout.log +0 -3
- package/logs/clean-console.log +0 -0
- package/logs/clean-status.log +0 -37
- package/logs/clean-stout.log +0 -95
- package/logs/cmd-console.log +0 -0
- package/logs/cmd-status.log +0 -8
- package/logs/cmd-stout.log +0 -9
- package/logs/concurrency-console.log +0 -0
- package/logs/concurrency-status.log +0 -14
- package/logs/concurrency-stout.log +0 -88
- package/logs/docker-console.log +0 -0
- package/logs/docker-status.log +0 -14
- package/logs/docker-stout.log +0 -39
- package/logs/env-console.log +0 -1
- package/logs/env-status.log +0 -1
- package/logs/env-stout.log +0 -0
- package/logs/error-console.log +0 -0
- package/logs/error-status.log +0 -10
- package/logs/error-stout.log +0 -36
- package/logs/extend-console.log +0 -0
- package/logs/extend-status.log +0 -1
- package/logs/extend-stout.log +0 -0
- package/logs/glob-console.log +0 -28
- package/logs/glob-status.log +0 -23
- package/logs/glob-stout.log +0 -52
- package/logs/hello-world-node-console.log +0 -0
- package/logs/hello-world-node-status.log +0 -37
- package/logs/hello-world-node-stout.log +0 -41
- package/logs/include-console.log +0 -0
- package/logs/include-status.log +0 -3
- package/logs/include-stout.log +0 -0
- package/logs/invalid-console.log +0 -0
- package/logs/invalid-status.log +0 -1
- package/logs/invalid-stout.log +0 -0
- package/logs/invalid_loop-console.log +0 -0
- package/logs/invalid_loop-status.log +0 -1
- package/logs/invalid_loop-stout.log +0 -11
- package/logs/labels-console.log +0 -0
- package/logs/labels-status.log +0 -1
- package/logs/labels-stout.log +0 -0
- package/logs/local-console.log +0 -0
- package/logs/local-status.log +0 -7
- package/logs/local-stout.log +0 -8
- package/logs/monorepo-console.log +0 -0
- package/logs/monorepo-status.log +0 -152
- package/logs/monorepo-stout.log +0 -2354
- package/logs/multi-line-console.log +0 -0
- package/logs/multi-line-status.log +0 -1
- package/logs/multi-line-stout.log +0 -0
- package/logs/reference-console.log +0 -0
- package/logs/reference-status.log +0 -5
- package/logs/reference-stout.log +0 -0
- package/logs/services-console.log +0 -0
- package/logs/services-status.log +0 -129
- package/logs/services-stout.log +0 -639
- package/logs/store-restore-console.log +0 -0
- package/logs/store-restore-status.log +0 -40
- package/logs/store-restore-stout.log +0 -97
- package/logs/unknown-console.log +0 -3
- package/logs/unknown-status.log +0 -1
- package/logs/unknown-stout.log +0 -0
- package/logs/validate-console.log +0 -0
- package/logs/validate-status.log +0 -2
- package/logs/validate-stout.log +0 -0
- package/logs/watch-console.log +0 -0
- package/logs/watch-dependency-console.log +0 -0
- package/logs/watch-dependency-status.log +0 -176
- package/logs/watch-dependency-stout.log +0 -779
- package/logs/watch-status.log +0 -100
- package/logs/watch-stout.log +0 -349
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path_1 = require("path");
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const environment_mock_1 = require("../executer/environment-mock");
|
|
7
|
+
const enqueue_next_1 = require("../executer/scheduler/enqueue-next");
|
|
8
|
+
const local_cache_backend_1 = require("./backends/local-cache-backend");
|
|
9
|
+
const state_1 = require("../executer/state");
|
|
10
|
+
const cache_metadata_1 = require("../executer/cache-metadata");
|
|
11
|
+
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
12
|
+
const get_work_cache_stats_1 = require("../optimizer/get-work-cache-stats");
|
|
13
|
+
const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
|
|
14
|
+
describe('checkCacheState auto-pull', () => {
|
|
15
|
+
let remoteDir;
|
|
16
|
+
let machineHome;
|
|
17
|
+
let originalHome;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
remoteDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-remote-'));
|
|
20
|
+
machineHome = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-home-'));
|
|
21
|
+
originalHome = process.env.HOME;
|
|
22
|
+
if (process.platform === 'darwin') {
|
|
23
|
+
// getHammerkitDirectory uses Library/Application Support on macOS
|
|
24
|
+
process.env.HOME = machineHome;
|
|
25
|
+
}
|
|
26
|
+
else if (process.platform === 'win32') {
|
|
27
|
+
process.env.APPDATA = machineHome;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.env.HOME = machineHome;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
(0, fs_1.rmSync)(remoteDir, { recursive: true, force: true });
|
|
35
|
+
(0, fs_1.rmSync)(machineHome, { recursive: true, force: true });
|
|
36
|
+
if (originalHome !== undefined) {
|
|
37
|
+
process.env.HOME = originalHome;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
delete process.env.HOME;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
function makeBackend() {
|
|
44
|
+
return (0, local_cache_backend_1.createLocalCacheBackend)({ type: 'local', path: remoteDir });
|
|
45
|
+
}
|
|
46
|
+
function makeWorkItem(cwd, resolved) {
|
|
47
|
+
const storedStateKey = null;
|
|
48
|
+
return {
|
|
49
|
+
id: () => 'demo-task',
|
|
50
|
+
name: 'demo-task',
|
|
51
|
+
status: { write: vi.fn() },
|
|
52
|
+
data: {
|
|
53
|
+
type: 'local-task',
|
|
54
|
+
name: 'demo-task',
|
|
55
|
+
cwd,
|
|
56
|
+
cmds: [],
|
|
57
|
+
generates: [],
|
|
58
|
+
src: [],
|
|
59
|
+
envs: { variables: {}, replacements: [] },
|
|
60
|
+
labels: {},
|
|
61
|
+
shell: '/bin/sh',
|
|
62
|
+
continuous: false,
|
|
63
|
+
caching: resolved,
|
|
64
|
+
description: null,
|
|
65
|
+
scope: {},
|
|
66
|
+
},
|
|
67
|
+
needs: [],
|
|
68
|
+
deps: [],
|
|
69
|
+
requiredBy: [],
|
|
70
|
+
state: new state_1.State({ type: 'pending', stateKey: null }),
|
|
71
|
+
runtime: {
|
|
72
|
+
async initialize() { },
|
|
73
|
+
async remove() { },
|
|
74
|
+
async execute() { },
|
|
75
|
+
async stop() { },
|
|
76
|
+
async archive() { },
|
|
77
|
+
async restore() {
|
|
78
|
+
// simulate a successful artifact restore
|
|
79
|
+
},
|
|
80
|
+
async currentStateKey() {
|
|
81
|
+
return storedStateKey;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
it('reports cached:true on the second machine after the first pushed', async () => {
|
|
87
|
+
// Machine A: produce cache + push to remote
|
|
88
|
+
const machineAScratch = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-machine-a-'));
|
|
89
|
+
const envA = (0, environment_mock_1.environmentMock)(machineAScratch);
|
|
90
|
+
const backend = makeBackend();
|
|
91
|
+
const resolved = { name: 'remote', method: 'checksum', backend, implicit: false };
|
|
92
|
+
const itemA = makeWorkItem(machineAScratch, resolved);
|
|
93
|
+
const stats = await (0, get_work_cache_stats_1.getWorkCacheStats)(itemA.data, envA);
|
|
94
|
+
const stateKey = (0, get_work_cache_stats_1.getStateKey)(stats, 'checksum');
|
|
95
|
+
await (0, cache_metadata_1.writeCacheMetadata)(envA, itemA.id(), stats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(itemA.data));
|
|
96
|
+
const cacheDir = (0, get_cache_directory_1.getCacheDirectory)(itemA.id());
|
|
97
|
+
await backend.push(itemA.id(), stateKey, cacheDir, envA);
|
|
98
|
+
(0, fs_1.rmSync)(machineAScratch, { recursive: true, force: true });
|
|
99
|
+
// Machine B (different HOME = fresh local cache): same task, same sources → same stateKey
|
|
100
|
+
const machineBScratch = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-machine-b-'));
|
|
101
|
+
const envB = (0, environment_mock_1.environmentMock)(machineBScratch);
|
|
102
|
+
// simulate a *different* machine: reset the HOME again
|
|
103
|
+
const machineBHome = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-home-b-'));
|
|
104
|
+
process.env.HOME = machineBHome;
|
|
105
|
+
const itemB = makeWorkItem(machineBScratch, resolved);
|
|
106
|
+
const cacheState = await (0, enqueue_next_1.checkCacheState)(itemB, 'checksum', envB);
|
|
107
|
+
expect(cacheState.stateKey).toBe(stateKey);
|
|
108
|
+
expect(cacheState.cached).toBe(true);
|
|
109
|
+
(0, fs_1.rmSync)(machineBScratch, { recursive: true, force: true });
|
|
110
|
+
(0, fs_1.rmSync)(machineBHome, { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
it('reports cached:false when remote has nothing', async () => {
|
|
113
|
+
const scratch = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-machine-c-'));
|
|
114
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
115
|
+
const backend = makeBackend();
|
|
116
|
+
const resolved = { name: 'remote', method: 'checksum', backend, implicit: false };
|
|
117
|
+
const item = makeWorkItem(scratch, resolved);
|
|
118
|
+
const cacheState = await (0, enqueue_next_1.checkCacheState)(item, 'checksum', env);
|
|
119
|
+
expect(cacheState.cached).toBe(false);
|
|
120
|
+
(0, fs_1.rmSync)(scratch, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=auto-pull.spec.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLocalCacheBackend = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const os_1 = require("os");
|
|
6
|
+
function createLocalCacheBackend(spec) {
|
|
7
|
+
const root = spec.path ?? (0, path_1.join)((0, os_1.homedir)(), '.hammerkit', 'remote-cache');
|
|
8
|
+
return {
|
|
9
|
+
type: 'local',
|
|
10
|
+
async has(taskId, stateKey, environment) {
|
|
11
|
+
const statsPath = (0, path_1.join)(root, taskId, stateKey, 'stats.json');
|
|
12
|
+
try {
|
|
13
|
+
return await environment.file.exists(statsPath);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
async pull(taskId, stateKey, into, environment) {
|
|
20
|
+
const remoteDir = (0, path_1.join)(root, taskId, stateKey);
|
|
21
|
+
const statsPath = (0, path_1.join)(remoteDir, 'stats.json');
|
|
22
|
+
if (!(await environment.file.exists(statsPath))) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
await environment.file.createDirectory(into);
|
|
26
|
+
try {
|
|
27
|
+
for (const file of await environment.file.listFiles(remoteDir)) {
|
|
28
|
+
await environment.file.copy((0, path_1.join)(remoteDir, file), (0, path_1.join)(into, file));
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
async push(taskId, stateKey, from, environment) {
|
|
37
|
+
const remoteDir = (0, path_1.join)(root, taskId, stateKey);
|
|
38
|
+
await environment.file.createDirectory(remoteDir);
|
|
39
|
+
// Write data files first; stats.json last so a partial push is invisible to has()/pull().
|
|
40
|
+
const files = await environment.file.listFiles(from);
|
|
41
|
+
const ordered = [
|
|
42
|
+
...files.filter((f) => f !== 'stats.json' && f !== 'description.json'),
|
|
43
|
+
...files.filter((f) => f === 'description.json'),
|
|
44
|
+
...files.filter((f) => f === 'stats.json'),
|
|
45
|
+
];
|
|
46
|
+
for (const file of ordered) {
|
|
47
|
+
await environment.file.copy((0, path_1.join)(from, file), (0, path_1.join)(remoteDir, file));
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async clear(taskId, environment) {
|
|
51
|
+
await environment.file.remove((0, path_1.join)(root, taskId));
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
exports.createLocalCacheBackend = createLocalCacheBackend;
|
|
56
|
+
//# sourceMappingURL=local-cache-backend.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path_1 = require("path");
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const local_cache_backend_1 = require("./local-cache-backend");
|
|
7
|
+
const environment_mock_1 = require("../../executer/environment-mock");
|
|
8
|
+
describe('local cache backend', () => {
|
|
9
|
+
let root;
|
|
10
|
+
let scratch;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
root = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-local-cache-'));
|
|
13
|
+
scratch = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-local-cache-scratch-'));
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
(0, fs_1.rmSync)(root, { recursive: true, force: true });
|
|
17
|
+
(0, fs_1.rmSync)(scratch, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
it('reports no hit when nothing pushed yet', async () => {
|
|
20
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
21
|
+
const backend = (0, local_cache_backend_1.createLocalCacheBackend)({ type: 'local', path: root });
|
|
22
|
+
expect(await backend.has('taskA', 'key1', env)).toBe(false);
|
|
23
|
+
expect(await backend.pull('taskA', 'key1', (0, path_1.join)(scratch, 'pull'), env)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it('round-trips a task through push and pull', async () => {
|
|
26
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
27
|
+
const backend = (0, local_cache_backend_1.createLocalCacheBackend)({ type: 'local', path: root });
|
|
28
|
+
const sourceDir = (0, path_1.join)(scratch, 'src');
|
|
29
|
+
await env.file.createDirectory(sourceDir);
|
|
30
|
+
await env.file.writeFile((0, path_1.join)(sourceDir, 'stats.json'), '{"files":{}}');
|
|
31
|
+
await env.file.writeFile((0, path_1.join)(sourceDir, 'description.json'), '{}');
|
|
32
|
+
await env.file.writeFile((0, path_1.join)(sourceDir, 'dist-generates.tgz'), 'tarball-bytes');
|
|
33
|
+
await backend.push('taskA', 'key1', sourceDir, env);
|
|
34
|
+
expect(await backend.has('taskA', 'key1', env)).toBe(true);
|
|
35
|
+
const into = (0, path_1.join)(scratch, 'pull');
|
|
36
|
+
const ok = await backend.pull('taskA', 'key1', into, env);
|
|
37
|
+
expect(ok).toBe(true);
|
|
38
|
+
expect(await env.file.exists((0, path_1.join)(into, 'stats.json'))).toBe(true);
|
|
39
|
+
expect(await env.file.exists((0, path_1.join)(into, 'description.json'))).toBe(true);
|
|
40
|
+
expect(await env.file.exists((0, path_1.join)(into, 'dist-generates.tgz'))).toBe(true);
|
|
41
|
+
expect(await env.file.read((0, path_1.join)(into, 'dist-generates.tgz'))).toBe('tarball-bytes');
|
|
42
|
+
});
|
|
43
|
+
it('isolates entries by stateKey', async () => {
|
|
44
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
45
|
+
const backend = (0, local_cache_backend_1.createLocalCacheBackend)({ type: 'local', path: root });
|
|
46
|
+
const sourceDir = (0, path_1.join)(scratch, 'src');
|
|
47
|
+
await env.file.createDirectory(sourceDir);
|
|
48
|
+
await env.file.writeFile((0, path_1.join)(sourceDir, 'stats.json'), '{}');
|
|
49
|
+
await backend.push('taskA', 'key1', sourceDir, env);
|
|
50
|
+
expect(await backend.has('taskA', 'key2', env)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('clear removes every state key of a task without touching others', async () => {
|
|
53
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
54
|
+
const backend = (0, local_cache_backend_1.createLocalCacheBackend)({ type: 'local', path: root });
|
|
55
|
+
const sourceDir = (0, path_1.join)(scratch, 'src');
|
|
56
|
+
await env.file.createDirectory(sourceDir);
|
|
57
|
+
await env.file.writeFile((0, path_1.join)(sourceDir, 'stats.json'), '{}');
|
|
58
|
+
await backend.push('taskA', 'key1', sourceDir, env);
|
|
59
|
+
await backend.push('taskA', 'key2', sourceDir, env);
|
|
60
|
+
await backend.push('taskB', 'key1', sourceDir, env);
|
|
61
|
+
await backend.clear('taskA', env);
|
|
62
|
+
expect(await backend.has('taskA', 'key1', env)).toBe(false);
|
|
63
|
+
expect(await backend.has('taskA', 'key2', env)).toBe(false);
|
|
64
|
+
expect(await backend.has('taskB', 'key1', env)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it('clear is a no-op when the task has nothing cached', async () => {
|
|
67
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
68
|
+
const backend = (0, local_cache_backend_1.createLocalCacheBackend)({ type: 'local', path: root });
|
|
69
|
+
await expect(backend.clear('missing', env)).resolves.toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=local-cache-backend.spec.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createS3CacheBackend = void 0;
|
|
4
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
5
|
+
const stream_1 = require("stream");
|
|
6
|
+
const promises_1 = require("fs/promises");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
// S3 object keys are arbitrary strings (unlike filesystem listings), so a
|
|
9
|
+
// poisoned/shared bucket can return a key whose suffix contains `../` and escape
|
|
10
|
+
// the restore directory once joined. Resolve the target and confirm it stays
|
|
11
|
+
// within `into`; anything outside is dropped rather than written.
|
|
12
|
+
function isWithin(into, filename) {
|
|
13
|
+
const base = (0, path_1.resolve)(into);
|
|
14
|
+
const target = (0, path_1.resolve)(base, filename);
|
|
15
|
+
return target === base || target.startsWith(base + path_1.sep);
|
|
16
|
+
}
|
|
17
|
+
function createS3CacheBackend(spec) {
|
|
18
|
+
const client = new client_s3_1.S3Client({
|
|
19
|
+
region: spec.region,
|
|
20
|
+
endpoint: spec.endpoint,
|
|
21
|
+
forcePathStyle: spec.forcePathStyle ?? !!spec.endpoint,
|
|
22
|
+
});
|
|
23
|
+
const prefix = (spec.prefix ?? '').replace(/^\/+|\/+$/g, '');
|
|
24
|
+
function keyFor(taskId, stateKey, filename) {
|
|
25
|
+
return [prefix, taskId, stateKey, filename].filter((p) => p.length > 0).join('/');
|
|
26
|
+
}
|
|
27
|
+
function dirPrefix(taskId, stateKey) {
|
|
28
|
+
return [prefix, taskId, stateKey].filter((p) => p.length > 0).join('/') + '/';
|
|
29
|
+
}
|
|
30
|
+
function taskPrefix(taskId) {
|
|
31
|
+
return [prefix, taskId].filter((p) => p.length > 0).join('/') + '/';
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
type: 's3',
|
|
35
|
+
async has(taskId, stateKey) {
|
|
36
|
+
try {
|
|
37
|
+
await client.send(new client_s3_1.HeadObjectCommand({
|
|
38
|
+
Bucket: spec.bucket,
|
|
39
|
+
Key: keyFor(taskId, stateKey, 'stats.json'),
|
|
40
|
+
}));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async pull(taskId, stateKey, into, environment) {
|
|
48
|
+
try {
|
|
49
|
+
const head = await client
|
|
50
|
+
.send(new client_s3_1.HeadObjectCommand({
|
|
51
|
+
Bucket: spec.bucket,
|
|
52
|
+
Key: keyFor(taskId, stateKey, 'stats.json'),
|
|
53
|
+
}))
|
|
54
|
+
.catch(() => null);
|
|
55
|
+
if (!head) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
await environment.file.createDirectory(into);
|
|
59
|
+
const listed = await client.send(new client_s3_1.ListObjectsV2Command({
|
|
60
|
+
Bucket: spec.bucket,
|
|
61
|
+
Prefix: dirPrefix(taskId, stateKey),
|
|
62
|
+
}));
|
|
63
|
+
for (const obj of listed.Contents ?? []) {
|
|
64
|
+
if (!obj.Key)
|
|
65
|
+
continue;
|
|
66
|
+
const filename = obj.Key.substring(dirPrefix(taskId, stateKey).length);
|
|
67
|
+
if (!filename)
|
|
68
|
+
continue;
|
|
69
|
+
if (!isWithin(into, filename)) {
|
|
70
|
+
environment.console.warn(`skipping cache object outside restore dir: ${obj.Key}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const body = await client.send(new client_s3_1.GetObjectCommand({
|
|
74
|
+
Bucket: spec.bucket,
|
|
75
|
+
Key: obj.Key,
|
|
76
|
+
}));
|
|
77
|
+
if (body.Body instanceof stream_1.Readable) {
|
|
78
|
+
await environment.file.writeStream((0, path_1.join)(into, filename), body.Body);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async push(taskId, stateKey, from, environment) {
|
|
88
|
+
const files = await environment.file.listFiles(from);
|
|
89
|
+
const ordered = [
|
|
90
|
+
...files.filter((f) => f !== 'stats.json' && f !== 'description.json'),
|
|
91
|
+
...files.filter((f) => f === 'description.json'),
|
|
92
|
+
...files.filter((f) => f === 'stats.json'),
|
|
93
|
+
];
|
|
94
|
+
for (const file of ordered) {
|
|
95
|
+
const body = await (0, promises_1.readFile)((0, path_1.join)(from, file));
|
|
96
|
+
await client.send(new client_s3_1.PutObjectCommand({
|
|
97
|
+
Bucket: spec.bucket,
|
|
98
|
+
Key: keyFor(taskId, stateKey, file),
|
|
99
|
+
Body: body,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async clear(taskId) {
|
|
104
|
+
let token;
|
|
105
|
+
do {
|
|
106
|
+
const listed = await client.send(new client_s3_1.ListObjectsV2Command({
|
|
107
|
+
Bucket: spec.bucket,
|
|
108
|
+
Prefix: taskPrefix(taskId),
|
|
109
|
+
ContinuationToken: token,
|
|
110
|
+
}));
|
|
111
|
+
const objects = (listed.Contents ?? []).filter((o) => o.Key).map((o) => ({ Key: o.Key }));
|
|
112
|
+
if (objects.length > 0) {
|
|
113
|
+
await client.send(new client_s3_1.DeleteObjectsCommand({
|
|
114
|
+
Bucket: spec.bucket,
|
|
115
|
+
Delete: { Objects: objects },
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
token = listed.IsTruncated ? listed.NextContinuationToken : undefined;
|
|
119
|
+
} while (token);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
exports.createS3CacheBackend = createS3CacheBackend;
|
|
124
|
+
//# sourceMappingURL=s3-cache-backend.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const stream_1 = require("stream");
|
|
4
|
+
// vi.hoisted: vi.mock is hoisted above this file's top-level code, so the factory
|
|
5
|
+
// below can only reference variables that are themselves hoisted.
|
|
6
|
+
const { sendMock } = vi.hoisted(() => ({ sendMock: vi.fn() }));
|
|
7
|
+
vi.mock('@aws-sdk/client-s3', () => {
|
|
8
|
+
class S3ClientMock {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.send = sendMock;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
class GetObjectCommand {
|
|
14
|
+
constructor(input) {
|
|
15
|
+
this.input = input;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class PutObjectCommand {
|
|
19
|
+
constructor(input) {
|
|
20
|
+
this.input = input;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class HeadObjectCommand {
|
|
24
|
+
constructor(input) {
|
|
25
|
+
this.input = input;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
class ListObjectsV2Command {
|
|
29
|
+
constructor(input) {
|
|
30
|
+
this.input = input;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
S3Client: S3ClientMock,
|
|
35
|
+
GetObjectCommand,
|
|
36
|
+
PutObjectCommand,
|
|
37
|
+
HeadObjectCommand,
|
|
38
|
+
ListObjectsV2Command,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const s3_cache_backend_1 = require("./s3-cache-backend");
|
|
42
|
+
const fs_1 = require("fs");
|
|
43
|
+
const os_1 = require("os");
|
|
44
|
+
const path_1 = require("path");
|
|
45
|
+
const environment_mock_1 = require("../../executer/environment-mock");
|
|
46
|
+
describe('s3 cache backend', () => {
|
|
47
|
+
let scratch;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
sendMock.mockReset();
|
|
50
|
+
scratch = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-s3-spec-'));
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
(0, fs_1.rmSync)(scratch, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
it('returns false from has() when HeadObject rejects', async () => {
|
|
56
|
+
sendMock.mockRejectedValueOnce(new Error('not found'));
|
|
57
|
+
const backend = (0, s3_cache_backend_1.createS3CacheBackend)({ type: 's3', bucket: 'b', region: 'us-east-1' });
|
|
58
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
59
|
+
expect(await backend.has('task', 'state', env)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it('returns true when HeadObject resolves', async () => {
|
|
62
|
+
sendMock.mockResolvedValueOnce({});
|
|
63
|
+
const backend = (0, s3_cache_backend_1.createS3CacheBackend)({ type: 's3', bucket: 'b', region: 'us-east-1' });
|
|
64
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
65
|
+
expect(await backend.has('task', 'state', env)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('uploads files via PutObject with prefix + task + stateKey path', async () => {
|
|
68
|
+
sendMock.mockResolvedValue({});
|
|
69
|
+
const backend = (0, s3_cache_backend_1.createS3CacheBackend)({ type: 's3', bucket: 'b', prefix: 'p/', region: 'us-east-1' });
|
|
70
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
71
|
+
await env.file.writeFile((0, path_1.join)(scratch, 'stats.json'), '{}');
|
|
72
|
+
await env.file.writeFile((0, path_1.join)(scratch, 'description.json'), '{}');
|
|
73
|
+
await env.file.writeFile((0, path_1.join)(scratch, 'a-generates.tgz'), 'bytes');
|
|
74
|
+
await backend.push('task1', 'key1', scratch, env);
|
|
75
|
+
const keys = sendMock.mock.calls.map(([cmd]) => cmd.input.Key);
|
|
76
|
+
expect(keys).toContain('p/task1/key1/a-generates.tgz');
|
|
77
|
+
expect(keys).toContain('p/task1/key1/description.json');
|
|
78
|
+
expect(keys).toContain('p/task1/key1/stats.json');
|
|
79
|
+
// stats.json must be uploaded last so a partial push is invisible to has()/pull()
|
|
80
|
+
expect(keys[keys.length - 1]).toBe('p/task1/key1/stats.json');
|
|
81
|
+
});
|
|
82
|
+
it('downloads files into the local cache dir on pull', async () => {
|
|
83
|
+
const backend = (0, s3_cache_backend_1.createS3CacheBackend)({ type: 's3', bucket: 'b', region: 'us-east-1' });
|
|
84
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
85
|
+
// Order: Head, then List, then a Get per object
|
|
86
|
+
sendMock.mockResolvedValueOnce({}); // head
|
|
87
|
+
sendMock.mockResolvedValueOnce({
|
|
88
|
+
Contents: [{ Key: 'task/state/stats.json' }, { Key: 'task/state/dist.tgz' }],
|
|
89
|
+
});
|
|
90
|
+
sendMock.mockResolvedValueOnce({ Body: stream_1.Readable.from(['{}']) });
|
|
91
|
+
sendMock.mockResolvedValueOnce({ Body: stream_1.Readable.from(['tarball']) });
|
|
92
|
+
const into = (0, path_1.join)(scratch, 'pulled');
|
|
93
|
+
const ok = await backend.pull('task', 'state', into, env);
|
|
94
|
+
expect(ok).toBe(true);
|
|
95
|
+
expect(await env.file.exists((0, path_1.join)(into, 'stats.json'))).toBe(true);
|
|
96
|
+
expect(await env.file.exists((0, path_1.join)(into, 'dist.tgz'))).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it('does not write objects whose key escapes the restore dir on pull', async () => {
|
|
99
|
+
const backend = (0, s3_cache_backend_1.createS3CacheBackend)({ type: 's3', bucket: 'b', region: 'us-east-1' });
|
|
100
|
+
const env = (0, environment_mock_1.environmentMock)(scratch);
|
|
101
|
+
sendMock.mockResolvedValueOnce({}); // head
|
|
102
|
+
sendMock.mockResolvedValueOnce({
|
|
103
|
+
// a poisoned bucket can list a key whose suffix traverses out of the dir
|
|
104
|
+
Contents: [{ Key: 'task/state/../escape.txt' }, { Key: 'task/state/safe.txt' }],
|
|
105
|
+
});
|
|
106
|
+
// only the safe object should be fetched; the traversal entry is skipped before Get
|
|
107
|
+
sendMock.mockResolvedValueOnce({ Body: stream_1.Readable.from(['ok']) });
|
|
108
|
+
const into = (0, path_1.join)(scratch, 'pulled');
|
|
109
|
+
const ok = await backend.pull('task', 'state', into, env);
|
|
110
|
+
expect(ok).toBe(true);
|
|
111
|
+
expect(await env.file.exists((0, path_1.join)(into, 'safe.txt'))).toBe(true);
|
|
112
|
+
// ../escape.txt would resolve to scratch/escape.txt, outside `into`
|
|
113
|
+
expect(await env.file.exists((0, path_1.join)(scratch, 'escape.txt'))).toBe(false);
|
|
114
|
+
// the traversal entry must not trigger a GetObject (head + list + 1 get = 3)
|
|
115
|
+
expect(sendMock).toHaveBeenCalledTimes(3);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=s3-cache-backend.spec.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCacheBackend = exports.registerCacheBackend = void 0;
|
|
4
|
+
const local_cache_backend_1 = require("./backends/local-cache-backend");
|
|
5
|
+
const s3_cache_backend_1 = require("./backends/s3-cache-backend");
|
|
6
|
+
const factories = new Map();
|
|
7
|
+
function registerCacheBackend(type, factory) {
|
|
8
|
+
factories.set(type, factory);
|
|
9
|
+
}
|
|
10
|
+
exports.registerCacheBackend = registerCacheBackend;
|
|
11
|
+
function createCacheBackend(spec) {
|
|
12
|
+
const factory = factories.get(spec.type);
|
|
13
|
+
if (!factory) {
|
|
14
|
+
throw new Error(`unknown cache backend type: ${spec.type}`);
|
|
15
|
+
}
|
|
16
|
+
return factory(spec);
|
|
17
|
+
}
|
|
18
|
+
exports.createCacheBackend = createCacheBackend;
|
|
19
|
+
registerCacheBackend('local', (spec) => (0, local_cache_backend_1.createLocalCacheBackend)(spec));
|
|
20
|
+
registerCacheBackend('s3', (spec) => (0, s3_cache_backend_1.createS3CacheBackend)(spec));
|
|
21
|
+
//# sourceMappingURL=cache-backend-registry.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The factory registry registers `local` and `s3` at import time. We can
|
|
3
|
+
// exercise createCacheBackend for both registered types and the unknown-type
|
|
4
|
+
// failure without needing the AWS SDK at runtime (`local` is real; for `s3`
|
|
5
|
+
// we register a fake under a fresh type name).
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const cache_backend_registry_1 = require("./cache-backend-registry");
|
|
8
|
+
describe('createCacheBackend', () => {
|
|
9
|
+
it('builds a local backend from a {type:"local"} spec', () => {
|
|
10
|
+
const backend = (0, cache_backend_registry_1.createCacheBackend)({ type: 'local' });
|
|
11
|
+
expect(backend.type).toBe('local');
|
|
12
|
+
});
|
|
13
|
+
it('throws on an unknown backend type', () => {
|
|
14
|
+
expect(() => (0, cache_backend_registry_1.createCacheBackend)({ type: 'unknown-backend-xyz' })).toThrow(/unknown cache backend type: unknown-backend-xyz/);
|
|
15
|
+
});
|
|
16
|
+
it('dispatches to a registered custom factory', () => {
|
|
17
|
+
const fakeBackend = { type: 'fake' };
|
|
18
|
+
(0, cache_backend_registry_1.registerCacheBackend)('fake-backend', () => fakeBackend);
|
|
19
|
+
expect((0, cache_backend_registry_1.createCacheBackend)({ type: 'fake-backend' })).toBe(fakeBackend);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=cache-backend-registry.spec.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCache = exports.withBuiltinCaches = exports.builtinCaches = exports.NONE_CACHE_NAME = exports.DEFAULT_CACHE_NAME = void 0;
|
|
4
|
+
const cache_backend_registry_1 = require("./cache-backend-registry");
|
|
5
|
+
exports.DEFAULT_CACHE_NAME = 'default';
|
|
6
|
+
exports.NONE_CACHE_NAME = 'none';
|
|
7
|
+
// `default` caches to the local backend at its default path
|
|
8
|
+
// (`~/.hammerkit/remote-cache`), so results are shared across checkouts on the
|
|
9
|
+
// same machine. To use a different location or a shared/remote cache, redeclare
|
|
10
|
+
// `caches.default` or add a named cache backed by `local`/`s3`/etc and reference
|
|
11
|
+
// it from the task. `none` disables skipping entirely (its backend is inert).
|
|
12
|
+
exports.builtinCaches = {
|
|
13
|
+
[exports.DEFAULT_CACHE_NAME]: { method: 'checksum', backend: { type: 'local' } },
|
|
14
|
+
[exports.NONE_CACHE_NAME]: { method: 'none', backend: { type: 'local' } },
|
|
15
|
+
};
|
|
16
|
+
function withBuiltinCaches(declared) {
|
|
17
|
+
const merged = { ...exports.builtinCaches };
|
|
18
|
+
if (declared) {
|
|
19
|
+
for (const [name, value] of Object.entries(declared)) {
|
|
20
|
+
merged[name] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
|
25
|
+
exports.withBuiltinCaches = withBuiltinCaches;
|
|
26
|
+
const backendInstances = new WeakMap();
|
|
27
|
+
function getBackend(spec) {
|
|
28
|
+
let instance = backendInstances.get(spec);
|
|
29
|
+
if (!instance) {
|
|
30
|
+
instance = (0, cache_backend_registry_1.createCacheBackend)(spec.backend);
|
|
31
|
+
backendInstances.set(spec, instance);
|
|
32
|
+
}
|
|
33
|
+
return instance;
|
|
34
|
+
}
|
|
35
|
+
function isMethodShorthand(value) {
|
|
36
|
+
return typeof value === 'string';
|
|
37
|
+
}
|
|
38
|
+
function resolveCache(ref, catalog, taskName) {
|
|
39
|
+
if (ref === undefined || ref === null) {
|
|
40
|
+
return resolveByName(exports.DEFAULT_CACHE_NAME, undefined, catalog, taskName, true);
|
|
41
|
+
}
|
|
42
|
+
if (isMethodShorthand(ref)) {
|
|
43
|
+
return resolveByName(exports.DEFAULT_CACHE_NAME, ref, catalog, taskName, false);
|
|
44
|
+
}
|
|
45
|
+
return resolveByName(ref.name, ref.method, catalog, taskName, false);
|
|
46
|
+
}
|
|
47
|
+
exports.resolveCache = resolveCache;
|
|
48
|
+
function resolveByName(name, methodOverride, catalog, taskName, implicit) {
|
|
49
|
+
const spec = catalog[name];
|
|
50
|
+
if (!spec) {
|
|
51
|
+
throw new Error(`cache "${name}" referenced by task "${taskName}" is not defined`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
method: methodOverride ?? spec.method,
|
|
56
|
+
backend: getBackend(spec),
|
|
57
|
+
implicit,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=resolve-cache.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const resolve_cache_1 = require("./resolve-cache");
|
|
4
|
+
describe('resolveCache', () => {
|
|
5
|
+
const catalog = (0, resolve_cache_1.withBuiltinCaches)({
|
|
6
|
+
s3: { method: 'modify-date', backend: { type: 'local', path: '/tmp/fake' } },
|
|
7
|
+
});
|
|
8
|
+
it('uses the default cache when no ref is given', () => {
|
|
9
|
+
const r = (0, resolve_cache_1.resolveCache)(undefined, catalog, 'taskA');
|
|
10
|
+
expect(r.name).toBe('default');
|
|
11
|
+
expect(r.method).toBe('checksum');
|
|
12
|
+
expect(r.backend.type).toBe('local');
|
|
13
|
+
expect(r.implicit).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('treats a string ref as a method override on default', () => {
|
|
16
|
+
const r = (0, resolve_cache_1.resolveCache)('modify-date', catalog, 'taskA');
|
|
17
|
+
expect(r.name).toBe('default');
|
|
18
|
+
expect(r.method).toBe('modify-date');
|
|
19
|
+
expect(r.backend.type).toBe('local');
|
|
20
|
+
expect(r.implicit).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it('uses none method when "none" string shorthand is used', () => {
|
|
23
|
+
const r = (0, resolve_cache_1.resolveCache)('none', catalog, 'taskA');
|
|
24
|
+
expect(r.method).toBe('none');
|
|
25
|
+
});
|
|
26
|
+
it('resolves a named reference and keeps its declared method', () => {
|
|
27
|
+
const r = (0, resolve_cache_1.resolveCache)({ name: 's3' }, catalog, 'taskA');
|
|
28
|
+
expect(r.name).toBe('s3');
|
|
29
|
+
expect(r.method).toBe('modify-date');
|
|
30
|
+
});
|
|
31
|
+
it('overrides the named cache method when the task sets one', () => {
|
|
32
|
+
const r = (0, resolve_cache_1.resolveCache)({ name: 's3', method: 'checksum' }, catalog, 'taskA');
|
|
33
|
+
expect(r.name).toBe('s3');
|
|
34
|
+
expect(r.method).toBe('checksum');
|
|
35
|
+
});
|
|
36
|
+
it('throws when an unknown name is referenced', () => {
|
|
37
|
+
expect(() => (0, resolve_cache_1.resolveCache)({ name: 'missing' }, catalog, 'taskA')).toThrow(/cache "missing" referenced by task "taskA"/);
|
|
38
|
+
});
|
|
39
|
+
it('seeds default and none even when nothing is declared', () => {
|
|
40
|
+
const empty = (0, resolve_cache_1.withBuiltinCaches)(undefined);
|
|
41
|
+
expect(empty.default.backend.type).toBe('local');
|
|
42
|
+
expect(empty.none.backend.type).toBe('local');
|
|
43
|
+
});
|
|
44
|
+
it('lets the user override the built-in default', () => {
|
|
45
|
+
const cat = (0, resolve_cache_1.withBuiltinCaches)({
|
|
46
|
+
default: { method: 'modify-date', backend: { type: 'local', path: '/tmp/x' } },
|
|
47
|
+
});
|
|
48
|
+
const r = (0, resolve_cache_1.resolveCache)(undefined, cat, 'taskA');
|
|
49
|
+
expect(r.method).toBe('modify-date');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=resolve-cache.spec.js.map
|