|
bashdeps
Exact dependency materialization and manifest maintenance tools
|
Accepted
This Architecture Decision Record introduces manifest-manager.bash as a second first-class executable produced by the bashdeps repository and defines the initial update operation that maintains existing bashdeps dependency manifests.
The decision deliberately preserves the boundary established by ADR-001. bashdeps.bash remains an exact-artifact synchronization and verification tool. It does not discover releases, select versions, or change committed trust data. manifest-manager.bash is separate maintainer tooling that prepares reviewable manifest source changes before those changes are committed and later consumed by bashdeps.bash.
This ADR also extends the release model from one executable family to two. The products share repository governance, release version, build metadata, and release cadence, while retaining independent source closures and runtime requirements. Code required only by one product must not be incorporated into the other merely because both are built from the same repository.
Issue #16 defines the initial implementation request. Issue #17 tracks possible future add, remove, and list subcommands and remains outside this decision's initial behavior except for reserving a subcommand-oriented CLI shape.
A bashdeps manifest is trusted source code. Updating one dependency currently requires a maintainer to coordinate at least three related source changes:
Those steps are mechanical enough to automate, yet they are intentionally absent from bashdeps.bash. ADR-001 excludes dependency-update discovery and version selection from the runtime product. ADR-004 further states that retrieved bytes must never cause the approved digest to change automatically during synchronization. ADR-019 permits a maintainer or external helper to assist with an update while preserving the committed manifest as the explicit authorization step.
The distinction is important. A synchronizer answers whether local bytes match already-approved source. A manifest-maintenance tool helps a maintainer prepare a proposal to approve different bytes. Combining those responsibilities would allow runtime retrieval to rewrite its own trust authority.
The repository also already has a mature distribution pipeline. ADR-018 produces three representations of bashdeps.bash: developer, ordinary comment-stripped, and minified. ADR-019 gives each one a .sha256 companion. A separately useful manifest manager should receive the same distribution treatment without turning one executable into a superset of the other.
The initial manager is GitHub-aware because the demonstrated manifests use GitHub release/tag URLs. Requiring the GitHub CLI or a JSON parser solely to discover the latest release would enlarge deployment requirements unnecessarily. GitHub provides a canonical OWNER/REPO/releases/latest redirect whose effective URL contains the release tag, allowing the manager to use curl without parsing API JSON.
Finally, the manifest is intentionally human-readable source. Reformatting or regenerating an entire record merely to update three values would create noisy review diffs and could alter comments, physical continuation layout, line endings, or other source bytes unrelated to the requested trust change. Exact preservation therefore needs to be an architectural property rather than an implementation preference.
gh, jq, Python, or another JSON parser.The repository SHALL maintain two executable products:
bashdeps.bash retains its existing runtime responsibility and public contract.
manifest-manager.bash is a maintainer-side manifest source-management CLI. Its initial public subcommand is:
The executable SHALL not be required to run bashdeps.bash, bashdeps sync, or bashdeps verify. Likewise, bashdeps.bash SHALL never invoke manifest-manager.bash as part of dependency materialization or verification.
The manager SHALL expose only a process-oriented CLI. Its private Bash functions are not a supported sourceable API.
Both executable families SHALL use the same bashdeps project version, source revision date, and source commit metadata for a given release.
The repository SHALL not maintain an independent manifest-manager version stream. A release therefore identifies the complete tested repository product set rather than requiring separately coordinated version numbers.
make build SHALL produce these executable artifacts:
Each executable SHALL have one .sha256 companion under ADR-019:
New GitHub releases SHALL publish all twelve files.
For each product, the .dev.bash artifact retains source documentation, the ordinary .bash artifact removes full-line comments after the shebang, and the .min.bash artifact is produced from the completed ordinary artifact with the manifest-managed Bash-Minifier. Each generated executable must independently pass syntax, behavior, compatibility, and checksum validation.
The build SHALL define an explicit source inventory for each executable family.
Conceptually:
A source component SHALL be incorporated into an executable only when that product directly requires the component. The presence of a Bash file in src/, lib/, or another repository directory SHALL NOT automatically include it in either product.
A component may appear in both inventories only when it implements behavior intentionally shared by both products. Repository co-location alone is not a reason to make a component shared runtime code.
Wildcard assembly of all available libraries is therefore prohibited for these release products. This rule protects bashdeps.bash from accidentally acquiring GitHub release-selection code or manager-only runtime dependencies, and protects the manager from unrelated synchronization/publication code.
The initial manifest-manager implementation requires no shared runtime library with bashdeps.bash; each maintained executable is its own explicit source closure.
manifest-manager.bash SHALL require:
sha256sum or shasum -a 256 for digest calculation; andIt SHALL NOT require:
gh);jq or another JSON parser;bashdeps.bash itself.These requirements are independent from ADR-006's runtime capability contract for bashdeps.bash. A requirement introduced for one executable SHALL NOT become a requirement of the other unless both products genuinely use that capability.
When VERSION is omitted for an update, the manager SHALL request:
through curl, follow HTTPS redirects, and inspect curl's final effective URL. The final URL must have the matching form:
The manager SHALL percent-decode the single tag path component and use the decoded value as the exact target tag. + is ordinary path data and SHALL NOT be decoded as a space. Malformed percent escapes and NUL encodings SHALL fail.
The manager SHALL NOT enumerate tags, sort versions, query a package registry, or implement its own definition of "latest." GitHub's canonical latest release is the authority for the omitted-version operation.
The literal word latest is not reserved. If supplied explicitly as VERSION, it is the exact requested tag.
An explicitly supplied version/tag does not need a GitHub Release object. The manager uses the supplied tag exactly; successful retrieval of the derived artifact URL establishes that the requested Git ref is usable for the declared artifact. No semantic-version normalization, range interpretation, or automatic leading-v transformation is performed on the requested target.
The manager SHALL initially update only existing dependency records whose GitHub package and immutable artifact URL can be related mechanically without guessing.
Supported URL families are:
The existing OWNER/REPO must agree with the selected package. The URL ref/tag must correspond to the current identity version. Exact equality is preferred; one leading v difference is accepted for compatibility with existing manifests such as:
This compatibility relation is used only to validate the existing declaration. The new identity and URL use the exact requested or discovered target tag.
Commit-pinned dependencies are not release-updateable. An identity version that is a 40- or 64-character hexadecimal commit identifier SHALL fail rather than be silently reinterpreted as a release tag.
For a raw-content URL, a target tag containing / is ambiguous with the path that follows the ref and SHALL fail. Unsupported URL shapes likewise fail rather than trigger artifact-name, destination, or repository inference.
The manager SHALL parse manifests only to validate grammar, establish logical record boundaries, and identify the exact raw record to update. It SHALL NOT serialize parsed records back into source.
For each selected dependency, update may change only:
id value;url value; anddigest value.dest SHALL remain unchanged.
Each intended old value must occur exactly once in the selected raw record before it can be replaced. Zero occurrences or multiple occurrences are a safety failure.
The implementation SHALL retain raw physical record bytes separately from the folded parsing view. Successful updates therefore preserve unrelated:
Before output or publication, the manager SHALL prove that the candidate differs only through intended raw-record replacements. One acceptable implementation is to replace each updated raw record back with its original in reverse order and require byte-for-byte equality with the complete captured input. The candidate manifest SHALL also pass manifest validation after mutation.
Manifest input containing bytes that cannot be represented losslessly by the Bash implementation, including NUL, SHALL fail rather than be silently normalized or truncated.
For each selected record, the manager SHALL:
digest=sha256:... value; andDownloaded artifacts SHALL never be executed by the manager.
If the target URL is byte-for-byte identical to the existing immutable URL but the retrieved bytes no longer match the committed digest, the operation SHALL fail rather than silently bless changed bytes at an allegedly immutable location.
The calculated digest is not runtime trust established by the manager. It becomes a proposal in repository source. Review and commit of the manifest change remain the authorization boundary consumed later by bashdeps.bash.
manifest-manager.bash update --all SHALL attempt to update every declared dependency to its GitHub latest release.
Before release discovery, the operation SHALL validate that every dependency has an unambiguous GitHub PACKAGE@VERSION identity and that no dependency is commit-pinned. It SHALL NOT silently skip unsupported records.
All release discoveries, candidate downloads, hashes, exact substitutions, and preservation checks SHALL complete before the manifest is published or emitted. If any dependency fails, the complete --all operation fails.
This is manifest-source transactionality, not a claim about remotely retrieved artifacts or later bashdeps sync filesystem publication.
With the default dependencies.txt or -f FILE, the manager SHALL capture the complete original manifest before network work. A failed update SHALL leave the manifest path unchanged.
After the complete candidate passes validation, the manager SHALL compare the current manifest bytes with the captured original to detect an ordinary concurrent edit. If they differ, publication fails rather than overwriting the newer source.
A changed candidate SHALL be written to a destination-adjacent temporary file and renamed into place only after the complete transaction succeeds. Existing file metadata SHOULD be preserved where the available filesystem tools permit it.
The manager does not claim race-proof locking or openat-style protection against a hostile concurrent local process.
With:
the manager SHALL capture the complete STDIN manifest before performing update work.
After successful input capture, exactly one complete manifest representation is written to STDOUT:
A failure during dependency N of M therefore cannot expose a partially updated stream and cannot turn successfully captured input into an empty output file. Diagnostics remain on STDERR and the nonzero exit status still communicates that the requested update did not succeed.
If reading STDIN itself fails before complete capture, the manager cannot reproduce bytes it never successfully received. If writing STDOUT fails, it likewise cannot guarantee what the downstream consumer received. These ordinary I/O limits are explicit non-promises.
Help and version requests are informational success operations and need not consume STDIN merely because -f - also appears in their argument list.
Normal successful file-mode updates SHALL write nothing to STDOUT. Diagnostics SHALL go to STDERR. No JSON output or other structured serialization format is part of the interface.
Help and version output use STDOUT.
The manager SHALL use these public exit categories:
A successful no-op returns status 0. For an explicit target already present in the manifest, the manager may still retrieve and hash the immutable target bytes so changed bytes at the same URL are detected rather than automatically approved.
The initial public mutation operation is only update.
Future commands such as:
are tracked by issue #17 and require their own defined behavior before implementation. The existence of the manifest-manager.bash executable does not implicitly authorize those semantics.
src/manifest-manager.bash SHALL follow doc/documentation-standard.md from its initial implementation. Existing maintained scripts are not reformatted or backported to the revised standard as part of this feature.
Doxygen input SHALL include both maintained executable sources.
The manager SHALL have its own public behavior suite executed against:
src/manifest-manager.bash;dist/manifest-manager.dev.bash;dist/manifest-manager.bash; anddist/manifest-manager.min.bash.The existing bashdeps behavior suite continues to execute independently against its four source/distribution surfaces. Tests SHOULD also verify that unmistakably product-specific private symbols do not appear in the other product's developer artifact, protecting the explicit source-closure rule.
Ordinary manager tests SHALL use controlled curl fixtures rather than live GitHub state.
This would provide one executable, but it would directly contradict the separation established by ADR-001. A runtime tool that consumes committed trust declarations must not silently become the tool that selects and rewrites those declarations.
That would reduce release assets, but the CLI is useful to any repository that maintains bashdeps manifests. Shipping it as a first-class optional executable makes the behavior testable and consumable without making it a runtime dependency of bashdeps.
Independent versions could allow separate release cadences, but they would add coordination and metadata complexity while both executables are built, tested, and published from the same repository revision. One project release version is the simpler contract.
A universal bundle simplifies Make logic, but it would cause product-specific behavior and dependencies to leak across the architectural boundary. Explicit source closures are selected instead.
gh provides convenient release lookup and authentication behavior. It is not required for the public release information used here, however, and would enlarge the manager's deployment requirements. Curl effective-URL discovery is sufficient for the initial latest-release contract.
The REST API exposes tag_name directly, but parsing it correctly requires a JSON parser such as jq or a larger custom parser. The canonical release redirect provides the required datum without adding that dependency or creating a partial JSON implementation in Bash.
Sorting tags would invent semantic-version assumptions and would not necessarily match the release GitHub designates as latest. The manager follows GitHub's own latest-release selection instead.
An upstream tag can legitimately be named latest. Omission of VERSION provides an unambiguous discovery request while preserving every explicit string as literal tag data.
Regeneration would simplify field updates but would normalize source formatting and create unrelated diff noise. It could also alter CRLF/LF state, continuations, comments, or whitespace. Surgical replacement is selected because the manifest is reviewed source, not merely an interchangeable serialization.
Skipping would make mixed release/tag and commit-pinned manifests convenient, but it would make all mean "all dependencies understood by this invocation." The selected contract fails the transaction so callers cannot mistake a partial update set for complete maintenance.
A new package identity alone does not establish the correct artifact URL or local destination. Addition, removal, and listing are intentionally left to issue #17 so their contracts can be designed without expanding the first update feature.
The repository becomes a two-product project and publishes twelve release assets instead of six.
Build, test, documentation, and release automation become larger because each manifest-manager artifact is validated independently. In exchange, consumers can obtain the maintainer tool with the same artifact-quality guarantees as bashdeps.bash.
The bashdeps runtime remains small and unaware of GitHub release selection. Manager-only code and runtime requirements stay outside the bashdeps executable unless a later explicit shared-library decision establishes genuine commonality.
Manifest update diffs remain narrow. Reviewers can focus on the intended identity, URL, and digest changes without unrelated formatting churn.
Stream-mode callers gain a strong recovery property: after complete STDIN capture, a failed update returns the original manifest on STDOUT. Callers must still check the exit status to distinguish updated output from rollback output.
Commit-pinned and unsupported URL declarations make --all fail. This is an intentional consequence of making completeness explicit rather than silently partial.
The manager is GitHub-specific in its initial update support. Support for another hosting provider, URL family, authentication model, or release-discovery mechanism requires a deliberate extension rather than generic URL guessing.
Issue #17 tracks possible add, remove, and list operations.
If multiple source files later become genuinely shared between the products, the build may add an explicit shared source inventory while retaining product-specific closures and regression tests against accidental code inclusion.
If authenticated/private GitHub dependencies become a demonstrated requirement, the manager's transport and credential boundary will require a separate decision.
The existing src/bashdeps.bash documentation may be migrated to the revised doc/documentation-standard.md in separate work, as requested, without coupling that backport to this feature.
doc/documentation-standard.md