From d52ce77f722770635f4c7df552b5e7fa8de311eb Mon Sep 17 00:00:00 2001 From: John Trammell Date: Wed, 10 Jun 2026 12:38:10 -0500 Subject: [PATCH] add code quality checks --- .github/copilot-instructions.md | 37 ++- .github/workflows/build.yml | 6 +- .pre-commit-config.yaml | 11 - .secrets.baseline | 245 ++++++++++++++- .specify/extensions.yml | 285 +++++++++--------- .../agent-context/agent-context-config.yml | 1 + .../extensions/agent-context/extension.yml | 6 +- .specify/extensions/git/extension.yml | 5 +- .specify/workflows/speckit/workflow.yml | 5 +- .vscode/settings.json | 4 +- Dockerfile | 71 +++-- README.md | 5 +- scripts/verify-ruby-jemalloc.sh | 17 +- .../contracts/build-release-contract.md | 19 +- specs/002-multiarch-image-cache/data-model.md | 37 ++- specs/002-multiarch-image-cache/plan.md | 44 +-- specs/002-multiarch-image-cache/quickstart.md | 19 +- specs/002-multiarch-image-cache/research.md | 64 ++-- .../verification/cache-reuse.md | 2 +- .../verification/release-manifest.md | 2 +- .../verification/runtime-consistency.md | 2 +- 21 files changed, 600 insertions(+), 287 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index df8f1c8..835ccc5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,36 +1,44 @@ For additional context about technologies to be used, project structure, -shell commands, and edge-case handling details, read specs/002-multiarch-image-cache/plan.md and treat it as the authoritative implementation context. +shell commands, and edge-case handling details, read specs/002-multiarch-image-cache/plan.md +and treat it as the authoritative implementation context. # Instructions -The purpose of this project is to build a Docker container image providing the latest patch release within the Ruby 2.6.x series (intentionally pinned to 2.6, not a newer major version). The 2.6.x pin is a hard requirement. +The purpose of this project is to build a Docker container image providing the latest patch release +within the Ruby 2.6.x series (intentionally pinned to 2.6, not a newer major version). The 2.6.x pin +is a hard requirement. -The image must include the `jemalloc` implementation of `malloc` for improved memory performance. To support -development and testing, the resulting container will be multi-architecture, supporting both +The image must include the `jemalloc` implementation of `malloc` for improved memory performance. +To support development and testing, the resulting container will be multi-architecture, supporting both `linux/amd64` and `linux/arm64` architectures. -The project will use `dependabot` to keep the pinned versions of Ruby, OpenSSL, and jemalloc up to date. +The project will use `dependabot` to keep the pinned versions of Ruby, OpenSSL, and jemalloc up to + date. ## Docker Specification The resulting Docker image will be based on the "scratch" image, and have a minimal runtime footprint. -It will use a multi-stage build to limit image contents to only the compiled Ruby binaries and necessary -runtime libraries. +It will use a multi-stage build to limit image contents to only the compiled Ruby binaries, necessary +runtime libraries, and files needed for testing and validating the image. The `Dockerfile` will be formatted with "here-doc" `RUN` blocks for clarity and maintainability. ## CICD +[registry]: ghcr.io/umnlibraries/ruby2.6-jemalloc-docker + * The CI workflow will be implemented with GitHub Actions, using `Buildx` for multi-architecture builds and cache management. Buildx cache must use the GitHub Actions cache backend (`type=gha`) with `mode=max` to cache all build layers across workflow runs. -* The workflow must push the final multi-arch manifest to [registry/image:tag]. Tags must include the full Ruby - version (e.g. `2.6.10`) and a `latest` alias for the highest 2.6.x version. The registry credentials will be +* The workflow must push the final multi-arch manifest to [registry]:latest. Tags must include the full Ruby + version (e.g. `2.6.10`) and a `latest` alias for the highest 2.6.x version. The `latest` tag must be updated only + when the image being built carries a Ruby patch version higher than any previously published 2.6.x tag in the + registry; the workflow should derive the Ruby version from build args and compare it against existing registry tags + before deciding to also push `latest`. The registry credentials will be provided via GitHub Actions secrets named `REGISTRY_USERNAME` and `REGISTRY_PASSWORD`. -* Use the `pre-commit` toolchain for local development to ensure code quality and consistency. ## Version Control @@ -39,4 +47,11 @@ The `Dockerfile` will be formatted with "here-doc" `RUN` blocks for clarity and * Pull requests will be used for all changes, with code review and automated testing before merging. * Use `dependabot` to keep dependencies up to date. - \ No newline at end of file +## Validation + +* The project will use the `pre-commit` toolchain for local development to ensure code quality + and consistency. +* A `make lint` target will be used to lint all markdown files in the repository, ensuring + documentation quality. + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 459078b..bf54faa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ --- name: Build and publish Docker image -on: +"on": push: branches: [main] pull_request: @@ -149,4 +149,6 @@ jobs: - name: Verify Ruby and jemalloc runtime run: | - ./scripts/verify-ruby-jemalloc.sh "ruby2.6-jemalloc:ci-${{ matrix.arch }}" "${{ matrix.platform }}" + ./scripts/verify-ruby-jemalloc.sh \ + "ruby2.6-jemalloc:ci-${{ matrix.arch }}" \ + "${{ matrix.platform }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4f079a..b830407 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,3 @@ repos: hooks: - id: yamllint args: [--strict, -c=.yamllint.yml] - - # Ansible linter - - repo: https://github.com/ansible/ansible-lint - rev: v26.4.0 - hooks: - - id: ansible-lint - name: ansible-lint - description: Run ansible-lint on Ansible files - entry: ansible-lint - language: python - files: \.(yml|yaml)$ diff --git a/.secrets.baseline b/.secrets.baseline index f97033f..c951dd9 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -90,6 +90,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -122,6 +126,243 @@ "path": "detect_secrets.filters.heuristic.is_templated_secret" } ], - "results": {}, - "generated_at": "2026-06-04T21:06:06Z" + "results": { + ".specify/integrations/copilot.manifest.json": [ + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "c411fdf15c58e5b80af9cb5e1dbd88cb6f892dfa", + "is_verified": false, + "line_number": 6, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "8d163f350d6bb3293357e2e569c1faefac453204", + "is_verified": false, + "line_number": 7, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "e7b82dfc011fe97ba1148b179afaf7fb70e2aa62", + "is_verified": false, + "line_number": 8, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "717b90528dc337ca4a7b7534ec9b7799bfba1809", + "is_verified": false, + "line_number": 9, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "5b58af287c11f117b71eb112128daad1eb2e281b", + "is_verified": false, + "line_number": 10, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "05018fd2f129d19f907b629f68833056c34ea447", + "is_verified": false, + "line_number": 11, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "ca049b32bb963377ff1506a93f49e31e6487bb09", + "is_verified": false, + "line_number": 12, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "b887e8b2f4e4d500ac956aa7839a977f7e73295e", + "is_verified": false, + "line_number": 13, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "1e9e9555c75851ee3a8bc9bd1cef78f886160848", + "is_verified": false, + "line_number": 14, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "2a18bcff31e5fe30d3456516b5b219353ab86251", + "is_verified": false, + "line_number": 15, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "bcc56203d1b06207d79806f61f00926124bc93f6", + "is_verified": false, + "line_number": 16, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "1bd5808d29f0baf6ccdd8f64a926dd1370cdf3de", + "is_verified": false, + "line_number": 17, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "a7cf10ee7692289db628adc1e8283d0022fa7bac", + "is_verified": false, + "line_number": 18, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "62117cb86f604a29509407c32339f2411e1580e8", + "is_verified": false, + "line_number": 19, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "0970c1a55f879ff3aa7de54153c191a879a13483", + "is_verified": false, + "line_number": 20, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "53c87594c22acf617ae6a0210f6e0df6785bb6da", + "is_verified": false, + "line_number": 21, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "e0620c8016795ec1eb61894aa96a692dc567d935", + "is_verified": false, + "line_number": 22, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "b1533078191b915610220e62f11b4b3ce604d6e0", + "is_verified": false, + "line_number": 23, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/copilot.manifest.json", + "hashed_secret": "3666b0731bcc8efe81cd2eab8a15480465d39bfe", + "is_verified": false, + "line_number": 24, + "is_secret": false + } + ], + ".specify/integrations/speckit.manifest.json": [ + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "606979ec481dce48794c78b983ed49f775e4fd44", + "is_verified": false, + "line_number": 6, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "643cb98b17fa3813e10afaa7ed5af5d334da85d2", + "is_verified": false, + "line_number": 7, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "ddd6844f6a7c3ee5f7e6dd51eded83db3567faba", + "is_verified": false, + "line_number": 8, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "246bc69bc522df923194fe62fc2ef6f4cf157bfe", + "is_verified": false, + "line_number": 9, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "bb8d6742113eef5d976133e844c73d4389aa4d5c", + "is_verified": false, + "line_number": 10, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "ecde51343aeae413aaba7dc0a5eb327c83bdd97f", + "is_verified": false, + "line_number": 11, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "25c4a42ae4038007ad01f74d7e01a9d3352ef6ce", + "is_verified": false, + "line_number": 12, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "6c481bf584ec89fe5868814e71db48013022ff8e", + "is_verified": false, + "line_number": 13, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "9ca09f61b0fce77137bb336a5537503f9f28be91", + "is_verified": false, + "line_number": 14, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": ".specify/integrations/speckit.manifest.json", + "hashed_secret": "5a2c5ffb2fa4fb14d9cc8098f9aff3769a5f0894", + "is_verified": false, + "line_number": 15, + "is_secret": false + } + ] + }, + "generated_at": "2026-06-10T17:35:52Z" } diff --git a/.specify/extensions.yml b/.specify/extensions.yml index 651400d..6c7edea 100644 --- a/.specify/extensions.yml +++ b/.specify/extensions.yml @@ -1,164 +1,165 @@ +--- installed: -- agent-context -- git + - agent-context + - git settings: auto_execute_hooks: true hooks: before_constitution: - - extension: git - command: speckit.git.initialize - enabled: true - optional: false - prompt: Execute speckit.git.initialize? - description: Initialize Git repository before constitution setup - condition: null + - extension: git + command: speckit.git.initialize + enabled: true + optional: false + prompt: Execute speckit.git.initialize? + description: Initialize Git repository before constitution setup + condition: null before_specify: - - extension: git - command: speckit.git.feature - enabled: true - optional: false - prompt: Execute speckit.git.feature? - description: Create feature branch before specification - condition: null + - extension: git + command: speckit.git.feature + enabled: true + optional: false + prompt: Execute speckit.git.feature? + description: Create feature branch before specification + condition: null before_clarify: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before clarification? - description: Auto-commit before spec clarification - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before clarification? + description: Auto-commit before spec clarification + condition: null before_plan: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before planning? - description: Auto-commit before implementation planning - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before planning? + description: Auto-commit before implementation planning + condition: null before_tasks: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before task generation? - description: Auto-commit before task generation - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before task generation? + description: Auto-commit before task generation + condition: null before_implement: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before implementation? - description: Auto-commit before implementation - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before implementation? + description: Auto-commit before implementation + condition: null before_checklist: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before checklist? - description: Auto-commit before checklist generation - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before checklist? + description: Auto-commit before checklist generation + condition: null before_analyze: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before analysis? - description: Auto-commit before analysis - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before analysis? + description: Auto-commit before analysis + condition: null before_taskstoissues: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit outstanding changes before issue sync? - description: Auto-commit before tasks-to-issues conversion - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit outstanding changes before issue sync? + description: Auto-commit before tasks-to-issues conversion + condition: null after_constitution: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit constitution changes? - description: Auto-commit after constitution update - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit constitution changes? + description: Auto-commit after constitution update + condition: null after_specify: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit specification changes? - description: Auto-commit after specification - condition: null - - extension: agent-context - command: speckit.agent-context.update - enabled: true - optional: true - prompt: Execute speckit.agent-context.update? - description: Refresh agent context after specification - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit specification changes? + description: Auto-commit after specification + condition: null + - extension: agent-context + command: speckit.agent-context.update + enabled: true + optional: true + prompt: Execute speckit.agent-context.update? + description: Refresh agent context after specification + condition: null after_clarify: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit clarification changes? - description: Auto-commit after spec clarification - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit clarification changes? + description: Auto-commit after spec clarification + condition: null after_plan: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit plan changes? - description: Auto-commit after implementation planning - condition: null - - extension: agent-context - command: speckit.agent-context.update - enabled: true - optional: true - prompt: Execute speckit.agent-context.update? - description: Refresh agent context after planning - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit plan changes? + description: Auto-commit after implementation planning + condition: null + - extension: agent-context + command: speckit.agent-context.update + enabled: true + optional: true + prompt: Execute speckit.agent-context.update? + description: Refresh agent context after planning + condition: null after_tasks: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit task changes? - description: Auto-commit after task generation - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit task changes? + description: Auto-commit after task generation + condition: null after_implement: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit implementation changes? - description: Auto-commit after implementation - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit implementation changes? + description: Auto-commit after implementation + condition: null after_checklist: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit checklist changes? - description: Auto-commit after checklist generation - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit checklist changes? + description: Auto-commit after checklist generation + condition: null after_analyze: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit analysis results? - description: Auto-commit after analysis - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit analysis results? + description: Auto-commit after analysis + condition: null after_taskstoissues: - - extension: git - command: speckit.git.commit - enabled: true - optional: true - prompt: Commit after syncing issues? - description: Auto-commit after tasks-to-issues conversion - condition: null + - extension: git + command: speckit.git.commit + enabled: true + optional: true + prompt: Commit after syncing issues? + description: Auto-commit after tasks-to-issues conversion + condition: null diff --git a/.specify/extensions/agent-context/agent-context-config.yml b/.specify/extensions/agent-context/agent-context-config.yml index 479c77e..87aac94 100644 --- a/.specify/extensions/agent-context/agent-context-config.yml +++ b/.specify/extensions/agent-context/agent-context-config.yml @@ -1,3 +1,4 @@ +--- context_file: .github/copilot-instructions.md context_markers: start: diff --git a/.specify/extensions/agent-context/extension.yml b/.specify/extensions/agent-context/extension.yml index 191069e..dc4a49b 100644 --- a/.specify/extensions/agent-context/extension.yml +++ b/.specify/extensions/agent-context/extension.yml @@ -1,10 +1,14 @@ +--- schema_version: "1.0" extension: id: agent-context name: "Coding Agent Context" version: "1.0.0" - description: "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers" + description: >- + Manages coding agent context/instruction files (e.g., CLAUDE.md, + copilot-instructions.md) with project-specific plan references and + configurable markers author: spec-kit-core repository: https://github.com/github/spec-kit license: MIT diff --git a/.specify/extensions/git/extension.yml b/.specify/extensions/git/extension.yml index 13c1977..e501ade 100644 --- a/.specify/extensions/git/extension.yml +++ b/.specify/extensions/git/extension.yml @@ -1,10 +1,13 @@ +--- schema_version: "1.0" extension: id: git name: "Git Branching Workflow" version: "1.0.0" - description: "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection" + description: >- + Feature branch creation, numbering (sequential/timestamp), validation, + and Git remote detection author: spec-kit-core repository: https://github.com/github/spec-kit license: MIT diff --git a/.specify/workflows/speckit/workflow.yml b/.specify/workflows/speckit/workflow.yml index f69efea..fc0eabd 100644 --- a/.specify/workflows/speckit/workflow.yml +++ b/.specify/workflows/speckit/workflow.yml @@ -1,3 +1,4 @@ +--- schema_version: "1.0" workflow: id: "speckit" @@ -33,7 +34,9 @@ inputs: integration: type: string default: "auto" - prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)" + prompt: >- + Integration to use (e.g. claude, copilot, gemini; 'auto' uses the + project's initialized integration) scope: type: string default: "full" diff --git a/.vscode/settings.json b/.vscode/settings.json index 35cbc8b..b544fce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,8 @@ ".specify/scripts/bash/": true, ".specify/scripts/powershell/": true, "printf": true, - "true": true + "true": true, + "git worktree": true, + "pre-commit": true } } diff --git a/Dockerfile b/Dockerfile index a42f3ab..f46fa97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -92,43 +92,54 @@ make install rm -rf /tmp/build/ruby-* EOF +# Assemble a minimal runtime filesystem for the final scratch image. +RUN <<'EOF' +set -eux + +mkdir -p /runtime-root + +# Copy Ruby, OpenSSL, and CA certificates required at runtime. +mkdir -p /runtime-root/usr /runtime-root/opt /runtime-root/etc/ssl +cp -a /usr/local /runtime-root/usr/local +cp -a /opt/openssl /runtime-root/opt/openssl +cp -a /etc/ssl/certs /runtime-root/etc/ssl/certs + +# Copy dynamic libraries required by Ruby + linked shared libraries. +tmp_lib_list="$(mktemp)" +ldd /usr/local/bin/ruby | awk '{if ($1 ~ /^\//) print $1; else if ($3 ~ /^\//) print $3}' >> "$tmp_lib_list" +ldd /usr/local/lib/libruby.so.2.6 | awk '{if ($1 ~ /^\//) print $1; else if ($3 ~ /^\//) print $3}' >> "$tmp_lib_list" +ldd /usr/local/lib/libjemalloc.so.2 | awk '{if ($1 ~ /^\//) print $1; else if ($3 ~ /^\//) print $3}' >> "$tmp_lib_list" +ldd /opt/openssl/lib/libssl.so.1.1 | awk '{if ($1 ~ /^\//) print $1; else if ($3 ~ /^\//) print $3}' >> "$tmp_lib_list" +ldd /opt/openssl/lib/libcrypto.so.1.1 | awk '{if ($1 ~ /^\//) print $1; else if ($3 ~ /^\//) print $3}' >> "$tmp_lib_list" + +sort -u "$tmp_lib_list" | while read -r lib; do + [ -n "$lib" ] + [ -e "$lib" ] + resolved="$(readlink -f "$lib")" + + mkdir -p "/runtime-root$(dirname "$resolved")" + cp -a "$resolved" "/runtime-root$resolved" + + if [ "$lib" != "$resolved" ]; then + mkdir -p "/runtime-root$(dirname "$lib")" + ln -sf "$resolved" "/runtime-root$lib" + fi +done + +rm -f "$tmp_lib_list" +EOF + # ============================================================ # Stage 2: Final – minimal runtime image # ============================================================ -FROM debian:latest +FROM scratch ARG RUBY_VERSION=2.6.10 -ENV DEBIAN_FRONTEND=noninteractive \ +ENV LD_LIBRARY_PATH=/opt/openssl/lib:/usr/local/lib \ RUBY_VERSION=${RUBY_VERSION} -RUN <<'EOF' -set -eux -# Step: install runtime dependencies -apt-get update -apt-get install -y --no-install-recommends \ - ca-certificates \ - libffi8 \ - libgdbm6 \ - libncurses6 \ - libreadline8 \ - libyaml-0-2 \ - zlib1g -rm -rf /var/lib/apt/lists/* -EOF - -# Copy OpenSSL 1.1.1 libraries from builder -COPY --from=builder /opt/openssl /opt/openssl - -# Copy Ruby, gems, and jemalloc from builder -COPY --from=builder /usr/local /usr/local - -# Register shared library paths so Ruby can find OpenSSL 1.1.1 and jemalloc -RUN <<'EOF' -set -eux -# Step: register OpenSSL runtime path and refresh linker cache -echo "/opt/openssl/lib" > /etc/ld.so.conf.d/openssl.conf -ldconfig -EOF +# Copy curated runtime filesystem from builder. +COPY --from=builder /runtime-root/ / CMD ["irb"] diff --git a/README.md b/README.md index 06b9752..04f0bb1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This repository provides a multistage Docker build that compiles: - **jemalloc 5.3.0** – linked into Ruby at compile time via `--with-jemalloc` for improved memory performance. - **Ruby 2.6.10** – the latest 2.6.x release, compiled from source against the above libraries. -The multistage build keeps the final image lean by carrying over only the compiled binaries and runtime libraries from the builder stage. +The multistage build keeps the final image lean by carrying over only the compiled binaries and required runtime libraries into a `scratch`-based final image. ## Build Command Style @@ -89,8 +89,7 @@ make verify-release The verification script checks: - Ruby reports `2.6.x` -- `libruby` links against `libjemalloc` -- `ldconfig` exposes jemalloc in the runtime image +- jemalloc is mapped into the running Ruby process (`/proc/self/maps`) ## CI/CD diff --git a/scripts/verify-ruby-jemalloc.sh b/scripts/verify-ruby-jemalloc.sh index 5d82c49..83c9a33 100755 --- a/scripts/verify-ruby-jemalloc.sh +++ b/scripts/verify-ruby-jemalloc.sh @@ -16,13 +16,16 @@ fi echo "[verify] image=${image} platform=${platform:-native}" -docker run "${run_args[@]}" "$image" sh -euxc ' - ruby -v | grep -E "ruby 2\.6\." - - libruby=$(ruby -rrbconfig -e "print File.join(RbConfig::CONFIG[\"libdir\"], RbConfig::CONFIG[\"LIBRUBY_SO\"])") - ldd "$libruby" | grep -qi jemalloc - - ldconfig -p | grep -qi jemalloc +ruby_version="$(docker run "${run_args[@]}" "$image" ruby -v)" +echo "$ruby_version" | grep -E "ruby 2\.6\." + +docker run "${run_args[@]}" "$image" ruby -e ' + maps = File.read("/proc/self/maps") + unless maps.downcase.include?("jemalloc") + warn "jemalloc not found in process memory map" + exit 1 + end + puts "jemalloc runtime mapping detected" ' echo "[verify] ruby and jemalloc checks passed" diff --git a/specs/002-multiarch-image-cache/contracts/build-release-contract.md b/specs/002-multiarch-image-cache/contracts/build-release-contract.md index fa0287e..f2cebd0 100644 --- a/specs/002-multiarch-image-cache/contracts/build-release-contract.md +++ b/specs/002-multiarch-image-cache/contracts/build-release-contract.md @@ -1,8 +1,8 @@ -# Contract: Multi-Platform Build and Cache Reuse +# Contract: Minimal Runtime Multi-Platform Build and Verification ## Purpose -Define the release-build behavior that must remain stable when the workflow is updated for multi-platform publishing and cache reuse. +Define required behavior for a minimal scratch-based runtime image that is built and published for multiple architectures with cache reuse and runtime verification. ## Supported Interfaces @@ -14,7 +14,15 @@ Define the release-build behavior that must remain stable when the workflow is u - The published tag resolves to the correct runnable variant for each platform. - The release is not considered valid unless both variants are present. -### 2. Cache reuse contract +### 2. Dockerfile structure contract + +#### Dockerfile Structure Expected Behavior + +- Build steps are organized in here-doc `RUN` blocks with explicit step grouping. +- Final runtime stage is `scratch` and contains only curated runtime artifacts. +- Runtime artifact assembly includes Ruby binaries, required OpenSSL files, CA certs, and required shared libraries. + +### 3. Cache reuse contract #### Cache Reuse Expected Behavior @@ -22,18 +30,19 @@ Define the release-build behavior that must remain stable when the workflow is u - Unchanged steps should not be rebuilt when the relevant inputs are identical. - Build evidence should show cache reuse behavior for warm builds. -### 3. Runtime verification contract +### 4. Runtime verification contract #### Expected behavior - Runtime verification must pass for both platform variants before release eligibility. -- Existing Ruby and jemalloc checks remain the acceptance baseline. +- Runtime verification must include Ruby 2.6.x version confirmation and jemalloc runtime mapping confirmation. - A failure on either platform blocks release publication. ## Invariants - Ruby 2.6 runtime compatibility remains unchanged. - Pinned source versions and deterministic build inputs remain documented. +- Verification remains executable using `docker build` plus runtime container checks. - Documentation changes in README and quickstart must accompany workflow changes. ## Failure Conditions diff --git a/specs/002-multiarch-image-cache/data-model.md b/specs/002-multiarch-image-cache/data-model.md index b1da01a..75c1958 100644 --- a/specs/002-multiarch-image-cache/data-model.md +++ b/specs/002-multiarch-image-cache/data-model.md @@ -1,4 +1,4 @@ -# Data Model: Single Multi-Platform Image Build with Efficient Caching +# Data Model: Minimal Scratch Runtime and Multi-Platform Build Verification ## Entity: Multi-Platform Image Tag @@ -16,6 +16,24 @@ - A multi-platform image tag MUST include both linux/amd64 and linux/arm64 variants. - A tag MUST not be considered release-ready unless every required platform variant is present. +## Entity: Runtime Artifact Set + +**Purpose**: Defines the runtime files copied into the final scratch image. + +### Runtime Artifact Set Fields + +- `ruby_binaries`: Paths under `/usr/local` required for Ruby execution +- `openssl_runtime`: Paths under `/opt/openssl` required for TLS/runtime linkage +- `ca_certificates`: Certificate bundle paths for outbound TLS +- `shared_libraries`: Resolved dynamic libraries and soname symlinks required by Ruby, jemalloc, and OpenSSL +- `assembly_status`: Success/failure of runtime filesystem assembly + +### Runtime Artifact Set Validation Rules + +- The final image MUST contain all runtime artifacts required to execute `ruby -v`. +- Shared-library copy logic MUST include both resolved targets and expected soname paths. +- The final image MUST exclude build toolchains and package-manager caches. + ## Entity: Platform Build Result **Purpose**: Captures the build and verification outcome for one target platform. @@ -50,3 +68,20 @@ - Cache reuse MUST be measurable for repeated builds. - Unchanged build steps SHOULD be restored from cache rather than rebuilt. - A cache record MUST be associated with the build run that produced it. + +## Entity: Runtime Verification Result + +**Purpose**: Captures runtime acceptance checks executed against built images. + +### Runtime Verification Result Fields + +- `image_reference`: Tested image tag or digest +- `platform`: Tested platform (`linux/amd64` or `linux/arm64`) +- `ruby_version_check`: Pass/fail result for Ruby 2.6.x check +- `jemalloc_runtime_check`: Pass/fail result for jemalloc mapping detection in process memory +- `verified_at`: Timestamp or workflow run identifier + +### Runtime Verification Result Validation Rules + +- Both Ruby version and jemalloc checks MUST pass for each required platform. +- A release is eligible only when verification succeeds for both amd64 and arm64. diff --git a/specs/002-multiarch-image-cache/plan.md b/specs/002-multiarch-image-cache/plan.md index c5d4d87..7cdc43f 100644 --- a/specs/002-multiarch-image-cache/plan.md +++ b/specs/002-multiarch-image-cache/plan.md @@ -1,50 +1,52 @@ -# Implementation Plan: Single Multi-Platform Image Build with Efficient Caching +# Implementation Plan: Minimal Scratch Ruby 2.6 Image with Multi-Arch CI Verification -**Branch**: `[002-multiarch-image-cache]` | **Date**: 2026-06-09 | **Spec**: [spec.md](./spec.md) +**Branch**: `[main]` | **Date**: 2026-06-10 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/002-multiarch-image-cache/spec.md` ## Summary -Consolidate the release workflow into a single multi-platform image publication flow that produces one tag for both `linux/amd64` and `linux/arm64`, while maximizing cache reuse to avoid rebuilding unchanged layers and preserving runtime verification coverage. +Deliver a minimal `scratch`-based Ruby 2.6 runtime image with jemalloc enabled, preserve multi-architecture publication for `linux/amd64` and `linux/arm64`, and standardize Dockerfile build steps around here-doc `RUN` blocks while keeping verification anchored on `docker build` plus runtime checks. ## Technical Context -**Language/Version**: Dockerfile syntax v1, Bash shell scripts, GitHub Actions workflow YAML +**Language/Version**: Dockerfile syntax v1 with here-doc `RUN` blocks, Bash shell scripts, GitHub Actions workflow YAML -**Primary Dependencies**: Docker Buildx, GitHub Actions cache, docker/build-push-action, docker/metadata-action, Docker registry publishing +**Primary Dependencies**: Docker Buildx, GitHub Actions cache (`type=gha`, `mode=max`), docker/build-push-action, docker/metadata-action, Docker registry publishing -**Storage**: Container image layers and remote build cache (GitHub Actions cache) +**Storage**: Container image layers, manifest lists, and remote Buildx cache entries in GitHub Actions cache -**Testing**: `docker buildx build`, `make verify`, `make verify-amd64`, `make verify-arm64`, release workflow runs on GitHub Actions +**Testing**: `docker build`, `docker buildx build`, `make verify`, `make verify-amd64`, `make verify-arm64`, release workflow verification jobs on GitHub Actions **Target Platform**: Linux container images for `linux/amd64` and `linux/arm64` **Project Type**: Container build and release workflow repository -**Performance Goals**: Reduce repeated-build time by maximizing cache hits and avoiding duplicate work across platform builds; preserve release verification pass rate +**Performance Goals**: Minimize runtime image footprint, preserve high cache hit rates across repeated CI builds, and keep two-platform verification pass rate at release time -**Constraints**: Preserve Ruby 2.6 runtime compatibility, keep jemalloc verification intact, maintain deterministic build inputs, ensure one published tag resolves to both platforms, and keep documentation aligned with workflow changes +**Constraints**: Preserve Ruby 2.6 runtime compatibility, keep jemalloc verification intact in a shell-less scratch runtime, maintain deterministic build inputs, ensure one published tag resolves to both platforms, and keep docs aligned with release behavior -**Scale/Scope**: Single repository; primary changes in `.github/workflows/build.yml`, `Makefile`, `README.md`, and supporting verification notes under `specs/002-multiarch-image-cache/` +**Scale/Scope**: Single repository; primary changes in `Dockerfile`, `.github/workflows/build.yml`, `scripts/verify-ruby-jemalloc.sh`, `Makefile`, `README.md`, and planning artifacts under `specs/002-multiarch-image-cache/` ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Ruby runtime alignment: Plan preserves Ruby 2.6 runtime compatibility and does not alter the dependency line. -- jemalloc verification: Plan keeps runtime checks for jemalloc activation in place for both target platforms. -- Build determinism: Plan keeps pinned dependency versions and controlled build inputs while adding cache reuse. -- Required validation: Plan includes build and runtime verification for both architectures plus cache-behavior evidence. -- Documentation impact: Plan updates README and workflow guidance for the single-tag multi-platform process. +- jemalloc verification: Plan verifies allocator activation from inside Ruby runtime execution rather than relying on package-manager tooling in final image. +- Build determinism: Plan keeps pinned dependency versions, explicit source URLs, and controlled build args while adding cache reuse. +- Required validation: Plan includes `docker build`/`buildx` plus runtime verification for both architectures. +- Documentation impact: Plan updates README and workflow guidance for minimal image behavior and verification approach. ## Phase 0 Research Research findings are captured in [research.md](./research.md). -- One release tag should resolve to both architectures using a multi-platform manifest rather than separate user-facing tags. -- Build cache reuse should occur through Buildx/GHA cache so unchanged layers are restored instead of rebuilt. -- Runtime verification remains required for both architectures after publishing. +- Final runtime should use `scratch` with a curated filesystem assembled in builder stage. +- Dockerfile build flow should remain explicit and maintainable using here-doc `RUN` blocks per major build step. +- Multi-platform publication should continue as one manifest-backed tag for `linux/amd64` and `linux/arm64`. +- CI should continue Buildx GHA cache import/export to reduce repeat build duration. +- Runtime verification should prove Ruby 2.6 and active jemalloc without requiring a shell in final image. No `NEEDS CLARIFICATION` items remain after research. @@ -85,10 +87,10 @@ scripts/verify-ruby-jemalloc.sh ## Post-Design Constitution Check - Ruby runtime alignment: PASS - design keeps Ruby 2.6 runtime compatibility unchanged. -- jemalloc verification: PASS - runtime checks remain required for both platform variants. -- Build determinism: PASS - pinned versions and explicit inputs remain in place; caching is additive. -- Required validation: PASS - quickstart and contract docs define local and CI verification paths for amd64 and arm64. -- Documentation impact: PASS - release workflow and user guidance will be documented together. +- jemalloc verification: PASS - runtime checks are preserved through Ruby-executed process-map inspection. +- Build determinism: PASS - pinned versions and explicit inputs remain in place; runtime assembly is deterministic. +- Required validation: PASS - quickstart and contract docs define build plus runtime checks for amd64 and arm64. +- Documentation impact: PASS - minimal runtime and verification changes are reflected in project guidance. ## Complexity Tracking diff --git a/specs/002-multiarch-image-cache/quickstart.md b/specs/002-multiarch-image-cache/quickstart.md index 816291c..7369679 100644 --- a/specs/002-multiarch-image-cache/quickstart.md +++ b/specs/002-multiarch-image-cache/quickstart.md @@ -1,4 +1,4 @@ -# Quickstart: Multi-Platform Release Build with Efficient Caching +# Quickstart: Minimal Scratch Runtime Build and Multi-Arch Verification ## Prerequisites @@ -9,7 +9,13 @@ ## Build and Verify Locally ```sh -make build +docker build -t ruby2.6-jemalloc:local . +./scripts/verify-ruby-jemalloc.sh ruby2.6-jemalloc:local +``` + +Equivalent make target: + +```sh make verify ``` @@ -29,9 +35,18 @@ make build-arm64 make verify-arm64 ``` +## GitHub Actions Verification Path + +Release workflow expectations: + +- Build and push one multi-platform tag (`linux/amd64`, `linux/arm64`) via Buildx. +- Import/export Buildx cache via GitHub Actions backend with `mode=max`. +- Run runtime verification job for each platform variant. + ## Expected Outcomes - One published tag resolves to both amd64 and arm64 variants. +- Final runtime image remains minimal (`scratch`) while still executing Ruby successfully. - Warm builds reuse cached work when inputs are unchanged. - Runtime verification passes for both platforms. diff --git a/specs/002-multiarch-image-cache/research.md b/specs/002-multiarch-image-cache/research.md index 20f274e..9efb4eb 100644 --- a/specs/002-multiarch-image-cache/research.md +++ b/specs/002-multiarch-image-cache/research.md @@ -1,53 +1,31 @@ -# Research Notes: Single Multi-Platform Image Build with Efficient Caching +# Research Notes: Minimal Scratch Runtime with Multi-Arch CI Cache -## Topic: Single Published Image Tag +## Topic: Minimal Final Runtime Image -### Single Tag Decision +- Decision: Use a `scratch` final stage with a curated runtime filesystem copied from the builder stage. +- Rationale: This yields the smallest practical runtime footprint while preserving Ruby 2.6 + jemalloc requirements and avoids package-manager drift in final image. +- Alternatives considered: Keep `debian:latest` runtime stage with apt-installed dependencies; rejected due to larger image size and additional mutable runtime surface. -Publish one release tag that resolves to both linux/amd64 and linux/arm64 variants through a multi-platform manifest. +## Topic: Dockerfile RUN Structure -### Single Tag Rationale +- Decision: Keep build workflow in here-doc `RUN` blocks grouped by concern (build deps, OpenSSL build, jemalloc build, Ruby build, runtime filesystem assembly). +- Rationale: The structure is explicit, reviewable, and aligned with project guidance for maintainability. +- Alternatives considered: Flatten commands into long line-continuation chains; rejected due to lower readability and higher maintenance risk. -- Users should not need to choose architecture-specific tags. -- A single tag keeps release documentation and usage simpler. -- The workflow already supports GitHub Container Registry publishing, which is compatible with manifest-based multi-platform images. +## Topic: Multi-Architecture Publication -### Single Tag Alternatives Considered +- Decision: Publish one manifest-backed tag resolving to both `linux/amd64` and `linux/arm64` variants. +- Rationale: Consumers use a single tag while registries resolve architecture-specific variants automatically. +- Alternatives considered: Publish separate arch tags only; rejected due to poorer UX and release-management overhead. -- Separate architecture-specific tags. - - Rejected because it increases user confusion and creates unnecessary release surface area. +## Topic: CI Cache Reuse -## Topic: Cache Reuse Strategy +- Decision: Use Buildx cache import/export with GitHub Actions backend (`type=gha`) and `mode=max`. +- Rationale: Cache persistence across workflow runs reduces duplicate build work for unchanged layers. +- Alternatives considered: Cold builds on every run or local-only cache; rejected because hosted runners are ephemeral and local cache is not shared. -### Cache Strategy Decision +## Topic: Runtime Verification in Scratch Images -Use Buildx cache import/export with shared remote cache storage so unchanged layers can be reused across repeated builds. - -### Cache Strategy Rationale - -- Remote cache reuse reduces duplicate work between cold and warm builds. -- GitHub Actions cache support is already present in the repository workflow style. -- Cache reuse must not change build outputs; it only changes how existing work is retrieved. - -### Cache Strategy Alternatives Considered - -- Build each platform fully from scratch on every run. - - Rejected because it wastes CI time and increases release latency. -- Rely only on local builder cache. - - Rejected because CI runners are ephemeral and need remote cache persistence. - -## Topic: Runtime Verification Preservation - -### Verification Decision - -Keep runtime verification as a release gate for both amd64 and arm64 outputs after the multi-platform image is published. - -### Verification Rationale - -- Multi-platform publishing is only acceptable if the resulting image still satisfies Ruby and jemalloc expectations. -- Verification needs to be repeatable for both target platforms. - -### Verification Alternatives Considered - -- Verify only one platform and assume the other matches. - - Rejected because platform-specific builds can diverge. +- Decision: Verify runtime with `docker run` invoking Ruby directly, validating Ruby 2.6 version output and jemalloc mapping from `/proc/self/maps`. +- Rationale: Scratch images may not provide shell or `ldconfig`; verification must rely on tools guaranteed by shipped runtime. +- Alternatives considered: Shell-based checks (`sh`, `ldd`, `ldconfig`) inside final image; rejected because they are brittle or unavailable in minimal scratch runtime. diff --git a/specs/002-multiarch-image-cache/verification/cache-reuse.md b/specs/002-multiarch-image-cache/verification/cache-reuse.md index 5519e14..dd6fef9 100644 --- a/specs/002-multiarch-image-cache/verification/cache-reuse.md +++ b/specs/002-multiarch-image-cache/verification/cache-reuse.md @@ -21,4 +21,4 @@ make build-release ## Notes -- Record whether the second run reused cache layers and which steps were rebuilt. \ No newline at end of file +- Record whether the second run reused cache layers and which steps were rebuilt. diff --git a/specs/002-multiarch-image-cache/verification/release-manifest.md b/specs/002-multiarch-image-cache/verification/release-manifest.md index deaa7a7..f289dee 100644 --- a/specs/002-multiarch-image-cache/verification/release-manifest.md +++ b/specs/002-multiarch-image-cache/verification/release-manifest.md @@ -20,4 +20,4 @@ make verify-release ## Notes -- Record the release workflow run URL or build log excerpt here after execution. \ No newline at end of file +- Record the release workflow run URL or build log excerpt here after execution. diff --git a/specs/002-multiarch-image-cache/verification/runtime-consistency.md b/specs/002-multiarch-image-cache/verification/runtime-consistency.md index 999e207..a3f301f 100644 --- a/specs/002-multiarch-image-cache/verification/runtime-consistency.md +++ b/specs/002-multiarch-image-cache/verification/runtime-consistency.md @@ -21,4 +21,4 @@ make verify-release ## Notes -- Record the release run URL and verification output for both platforms here after execution. \ No newline at end of file +- Record the release run URL and verification output for both platforms here after execution.