diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ae276c8..3a6b1720 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: concurrency: @@ -13,7 +14,7 @@ permissions: jobs: module-ci: - uses: tinyhumansai/.github/.github/workflows/module-ci.yml@f4911b709333ee457da77e73b96aa9aec90d21ae + uses: tinyhumansai/.github/.github/workflows/module-ci.yml@4adc1080fb495098e7af1aade82ed7e6e3c69bb7 with: coverage-script: scripts/check-coverage.sh msrv-build-targets: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a5967c4..5bd5e266 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,256 +1,42 @@ name: Release - on: workflow_dispatch: inputs: - bump: - description: Version bump to release (ignored when existing_tag is set) - type: choice - required: true - options: - - patch - - minor - - major - existing_tag: - description: >- - Build and upload artifacts for a tag that already exists, skipping the - version bump and the tagging. Recovery path for a run that tagged but - did not produce its bundles. + tag: + description: Existing version tag (vX.Y.Z) committed through a reviewed PR type: string - required: false - default: '' - + required: true + push: + tags: ['v*'] +permissions: {} concurrency: - group: release-${{ github.ref_name }} + group: release-${{ github.event_name == 'push' && github.ref_name || inputs.tag }} cancel-in-progress: false - -permissions: - contents: write - jobs: - publish: - name: Version and tag - # This crate is **not published to crates.io**. It is consumed as a git - # submodule by its hosts and shipped to them as the module artifacts this - # workflow builds, so a registry upload would be a third distribution - # channel nobody reads. - # - # There used to be a `cargo publish` step here, and its failure was not - # cosmetic: `release-target` gates on this job succeeding, so a release - # would tag the commit, fail the upload, and then skip every artifact job — - # forcing a second run with `existing_tag` to build the bundles. `cargo - # package` below still runs, so a manifest that could not be packaged is - # still caught; nothing uploads it. - # - # The job id stays `publish` because several `needs:` and `needs.publish.*` - # references depend on it. - # - # Skipped entirely when re-cutting artifacts for an existing tag. - if: ${{ github.ref == 'refs/heads/main' && inputs.existing_tag == '' }} + resolve: runs-on: ubuntu-latest - environment: Production outputs: - crate_name: ${{ steps.version.outputs.crate_name }} - next_version: ${{ steps.version.outputs.next_version }} - tag: ${{ steps.version.outputs.tag }} + tag: ${{ steps.tag.outputs.tag }} + version: ${{ steps.tag.outputs.version }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - fetch-depth: 0 - submodules: true - - - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master - with: - toolchain: stable - components: rustfmt, clippy - - - uses: Swatinem/rust-cache@v2 - - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - - name: Test - run: cargo test --all-features - - - name: Build documentation - env: - RUSTDOCFLAGS: -D warnings - run: cargo doc --no-deps --all-features - - - name: Compute next version - id: version + - id: tag shell: bash - run: | - set -euo pipefail - - metadata="$(cargo metadata --format-version 1 --no-deps)" - crate_name="$(jq -r '.packages[] | select(.name == "tinywallet") | .name' <<< "$metadata")" - current_version="$(jq -r '.packages[] | select(.name == "tinywallet") | .version' <<< "$metadata")" - if [[ -z "$current_version" || "$current_version" == "null" ]]; then - echo "Could not resolve the current crate version" >&2 - exit 1 - fi - - IFS=. read -r major minor patch <<< "$current_version" - case "${{ inputs.bump }}" in - major) - major=$((major + 1)) - minor=0 - patch=0 - ;; - minor) - minor=$((minor + 1)) - patch=0 - ;; - patch) - patch=$((patch + 1)) - ;; - *) - echo "Unsupported bump: ${{ inputs.bump }}" >&2 - exit 1 - ;; - esac - - next_version="${major}.${minor}.${patch}" - tag="v${next_version}" - - git fetch --tags origin - if git rev-parse --verify --quiet "refs/tags/${tag}"; then - echo "Tag ${tag} already exists" >&2 - exit 1 - fi - - { - echo "crate_name=${crate_name}" - echo "current_version=${current_version}" - echo "next_version=${next_version}" - echo "tag=${tag}" - } >> "$GITHUB_OUTPUT" - - - name: Update crate version env: - CRATE_NAME: ${{ steps.version.outputs.crate_name }} - NEXT_VERSION: ${{ steps.version.outputs.next_version }} + TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} run: | set -euo pipefail - bump() { - perl -0pi -e 's/(\[package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' "$1" + [[ "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]] || { + echo "tag must be v-prefixed semver" >&2 + exit 2 } - bump Cargo.toml - # The module crate is bumped in lockstep, not left behind. Its - # `cdylib` ships in an archive named for the *root* crate's version, - # so a drift here would publish `tinywallet-module-0.2.0-*.tar.gz` - # containing a library that reports 0.1.0 — with nothing to catch it. - bump crates/tinywallet-module/Cargo.toml - # And the contract crate, for the same reason and one more: every - # bundle job builds `--locked`, so a manifest this step misses leaves - # `Cargo.lock` disagreeing with the tree at the tag and *every* - # platform job fails to resolve. That produces a tagged release with - # zero artifacts, which is exactly what happened on tinyvoice v0.1.4. - bump crates/tinywallet-bus/Cargo.toml - # The implementation siblings are bumped for the same reason: - # a manifest this step misses leaves `Cargo.lock` disagreeing with the - # tree at the tag and every `--locked` bundle job fails to resolve. - # Their path dependencies carry no version requirement, so there is no - # inter-crate pin to rewrite alongside the version. - bump crates/tinywallet-crypto/Cargo.toml - bump crates/tinywallet-x402/Cargo.toml - # The wallet flows implementation, also versioned in lockstep. - bump crates/tinywallet-web3/Cargo.toml - # Refreshes every workspace member in the lockfile. `cargo update -p` - # cannot do this: these are path dependencies, and the later - # `--locked` steps fail against a stale lock. - cargo update --workspace - - - name: Commit version bump and tag - env: - RELEASE_TAG: ${{ steps.version.outputs.tag }} - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.toml Cargo.lock \ - crates/tinywallet-crypto/Cargo.toml crates/tinywallet-x402/Cargo.toml \ - crates/tinywallet-web3/Cargo.toml \ - crates/tinywallet-bus/Cargo.toml crates/tinywallet-module/Cargo.toml - git commit -m "Release ${RELEASE_TAG}" - git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}" - - # The pure contract is the packageable leaf. Implementation crates use - # unpublished siblings by path, which cargo package rewrites to registry - # dependencies. Verify the contract with every feature; nothing uploads. - - name: Package crate - run: cargo package --locked --package tinywallet-bus --all-features - - - name: Push release commit and tag - env: - RELEASE_TAG: ${{ steps.version.outputs.tag }} - run: | - set -euo pipefail - git push origin "HEAD:${GITHUB_REF_NAME}" - git push origin "${RELEASE_TAG}" - - # What the bundles need is a tag and a version. That used to be entangled with - # a crates.io upload in one job, so a missing registry token took the module - # artifacts down with it even though nothing about them touches crates.io. - # The upload is gone now, so this job's gate means what it says: the tag - # exists and was pushed. - release-target: - name: Resolve release target - needs: publish - # `always()` is required because this job must still run when `publish` is - # *skipped* — the `existing_tag` recovery path, where the tag already - # exists. It is deliberately NOT tolerant of a `publish` *failure*: that job - # now only versions, packages and tags, and without a tag there is nothing - # for the bundles to attach to. - if: ${{ always() && (inputs.existing_tag != '' || needs.publish.result == 'success') }} - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.resolve.outputs.tag }} - next_version: ${{ steps.resolve.outputs.next_version }} - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Resolve the tag and version to build - id: resolve - shell: bash - env: - EXISTING_TAG: ${{ inputs.existing_tag }} - PUBLISHED_TAG: ${{ needs.publish.outputs.tag }} - PUBLISHED_VERSION: ${{ needs.publish.outputs.next_version }} - run: | - set -euo pipefail - if [[ -n "$EXISTING_TAG" ]]; then - git fetch --tags origin - git rev-parse --verify --quiet "refs/tags/${EXISTING_TAG}" >/dev/null \ - || { echo "tag ${EXISTING_TAG} does not exist" >&2; exit 1; } - tag="$EXISTING_TAG" - version="${EXISTING_TAG#v}" - else - tag="$PUBLISHED_TAG" - version="$PUBLISHED_VERSION" - fi - [[ -n "$tag" && -n "$version" ]] || { echo "could not resolve a release target" >&2; exit 1; } - { - echo "tag=${tag}" - echo "next_version=${version}" - } >> "$GITHUB_OUTPUT" - + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" module-release: - name: Build and verify module release - needs: release-target - if: ${{ always() && needs.release-target.result == 'success' }} - uses: tinyhumansai/.github/.github/workflows/module-release.yml@f4911b709333ee457da77e73b96aa9aec90d21ae + needs: resolve + permissions: + contents: write + uses: tinyhumansai/.github/.github/workflows/module-release.yml@4adc1080fb495098e7af1aade82ed7e6e3c69bb7 with: - tag: ${{ needs.release-target.outputs.tag }} - version: ${{ needs.release-target.outputs.next_version }} + tag: ${{ needs.resolve.outputs.tag }} + version: ${{ needs.resolve.outputs.version }} module-crate: tinywallet-module - extra-packaged-files: '["docs/specs/tinybus-module.md"]' - include-distros: true - publish-crates-io: false diff --git a/AGENTS.md b/AGENTS.md index edb958c4..0d13b5af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -248,20 +248,19 @@ explicitly declined with a reason. ## Releases -Releases run from `.github/workflows/release.yml` via a manual -`workflow_dispatch` with a `patch` / `minor` / `major` bump. The workflow -re-runs the full validation suite, computes the next version, updates -`Cargo.toml` and `Cargo.lock`, commits and tags `vX.Y.Z`, packages, pushes, and -publishes to crates.io using the `CARGO_REGISTRY_TOKEN` secret. - -Consequently: - -- Do not hand-edit the `version` field in `Cargo.toml`; the release workflow - owns it. -- Follow semantic versioning. Any change to the public surface that is not - purely additive is a breaking change and needs a major bump (pre-1.0: a minor - bump). -- The crate must be publishable at all times — `main` should always be green. +Releases use the canonical TinyHumans shared workflow. Update the package +versions and matching local dependency constraints in a PR, regenerate the +lockfiles, and merge only after the required shared CI checks pass. For a +workspace using version.workspace, change its root workspace.package version. + +Create and push a vX.Y.Z tag at the merged commit, or dispatch Release with +an existing tag input. The workflow checks that the tagged commit is on main +and that its module version matches the tag. It builds and verifies all 17 +native and distro archives before publishing the immutable GitHub release. +Release never commits or pushes version bumps to main. + +Follow semantic versioning: breaking changes require a major bump, or a minor +bump before 1.0. The module must remain packageable for every release target. ## Agent Working Agreement diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba1d1db0..472e6b11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,3 +103,7 @@ matrix. The reusable workflow contract and supported release targets are documented in [tinyhumansai/.github](https://github.com/tinyhumansai/.github/blob/main/docs/module-ci-contract.md). + +## Releasing + +Change versions through a reviewed PR, then push a vX.Y.Z tag at the merged commit. Release can also be dispatched with an existing tag. The release workflow never pushes version bumps to main. diff --git a/scripts/build-module.ps1 b/scripts/build-module.ps1 index 406c0044..af569b74 100644 --- a/scripts/build-module.ps1 +++ b/scripts/build-module.ps1 @@ -10,7 +10,7 @@ $ErrorActionPreference = 'Stop' $root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path Set-Location $root if ($Crate -ne 'tinywallet-module') { throw "Unsupported module crate: $Crate" } -if ($Version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { throw "Invalid module version: $Version" } +if ($Version -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?\z') { throw "Invalid module version: $Version" } if ($HostKey -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid release host key: $HostKey" } $pathSeparators = [char[]]@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) if ([IO.Path]::IsPathRooted($OutputDir) -or $OutputDir.Split($pathSeparators, [StringSplitOptions]::None) -contains '..') { throw 'Output directory must be a relative path inside the repository' } diff --git a/scripts/build-module.sh b/scripts/build-module.sh index 4563d248..5b7f1966 100755 --- a/scripts/build-module.sh +++ b/scripts/build-module.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" cd "$root" target_root="${CARGO_TARGET_DIR:-$root/target}" [[ "$target_root" == /* ]] || target_root="$root/$target_root" @@ -20,7 +20,7 @@ while (($#)); do *) echo "unknown argument: $1" >&2; exit 2 ;; esac done -[[ "$crate" == tinywallet-module && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && -n "$rust_target" && "$host" =~ ^[A-Za-z0-9._-]+$ ]] || { +[[ "$crate" == tinywallet-module && "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ && -n "$rust_target" && "$host" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "usage: build-module.sh --crate tinywallet-module --version VERSION --target TARGET --host HOST [--extra-files-json JSON] [--output-dir DIR]" >&2 exit 2 } @@ -44,7 +44,7 @@ jq -e 'type == "array" and all(.[]; type == "string" and test("^docs/[A-Za-z0-9. echo "extra files must be repository-local docs paths without traversal" >&2 exit 2 } -cargo build --locked --release --package "$crate" --target "$rust_target" +cargo build --locked --release --lib --package "$crate" --target "$rust_target" stage="$target_root/module-package" rm -rf "$stage" mkdir -p "$stage" "$output" diff --git a/scripts/release_workflow_tests.py b/scripts/release_workflow_tests.py index f10c3a00..a7e7d65a 100644 --- a/scripts/release_workflow_tests.py +++ b/scripts/release_workflow_tests.py @@ -11,6 +11,26 @@ ROOT = Path(__file__).resolve().parent.parent +VERSION_CASES = [ + ("1.2.3", True), + ("0.0.0", True), + ("1.2.3-beta.1", True), + ("1.2.3-0.alpha-1+build.001", True), + ("1.2.3+001.build", True), + ("1.2.3-01a", True), + ("1.2.3-..", False), + ("1.2.3-01", False), + ("1.2.3-alpha..1", False), + ("1.2.3-alpha.01", False), + ("1.2.3+build..1", False), + ("01.2.3", False), + ("1.02.3", False), + ("1.2.03", False), + ("1.2.3-", False), + ("1.2.3+", False), + ("1.2.3\n", False), +] + class ReleaseWorkflowTests(unittest.TestCase): @classmethod @@ -20,21 +40,12 @@ def setUpClass(cls): ["cargo", "metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"], cwd=ROOT ) - def test_packaging_selects_a_contract_without_unpublished_path_dependencies(self): - command = re.search(r"run: cargo package --locked --package (\S+)", self.workflow) - self.assertIsNotNone(command) - package = next(p for p in json.loads(self.metadata)["packages"] if p["name"] == command[1]) - paths = [d["name"] for d in package["dependencies"] if d.get("path")] - self.assertEqual(paths, [], "cargo package must not resolve unpublished sibling crates from crates.io") - self.assertEqual(package["name"], "tinywallet-bus") - - def test_release_job_selects_an_explicit_rust_toolchain(self): - step = re.search( - r"- uses: dtolnay/rust-toolchain@[^\n]+\n(?P\s+with:\n(?:\s+[^\n]+\n)+)", - self.workflow, - ) - self.assertIsNotNone(step) - self.assertRegex(step["with"], r"(?m)^\s+toolchain:\s+stable\s*$") + def test_release_uses_the_canonical_module_pipeline(self): + """Every archive is built by the shared pipeline from the selected tag.""" + self.assertIn("tinyhumansai/.github/.github/workflows/module-release.yml@", self.workflow) + self.assertIn("module-crate: tinywallet-module", self.workflow) + self.assertNotIn("git push", self.workflow) + self.assertNotIn("cargo package", self.workflow) def test_module_build_accepts_versioned_host_labels_without_paths(self): result = subprocess.run( @@ -188,19 +199,47 @@ def test_module_verification_canonicalizes_a_symlinked_temp_root(self): ) self.assertEqual(result.returncode, 0, result.stderr) - def test_existing_tag_recovery_does_not_skip_bundle_job(self): - job = self.workflow.split(" module-release:\n", 1)[1] - condition = re.search(r"^ if: (.+)$", job, re.MULTILINE)[1] - self.assertIn("always()", condition, "a skipped versioning ancestor must not implicitly skip recovery bundles") - self.assertIn("needs.release-target.result == 'success'", condition, "recovery must still require successful target resolution") - - def test_release_identity_comes_from_the_root_library(self): - selectors = re.findall(r"jq -r '([^']+)' <<< \"\$metadata\"", self.workflow) - self.assertGreaterEqual(len(selectors), 2) - selected = [subprocess.check_output(["jq", "-r", selector], input=self.metadata).decode().strip() for selector in selectors[:2]] - root = next(p for p in json.loads(self.metadata)["packages"] if p["name"] == "tinywallet") - self.assertEqual(selected, [root["name"], root["version"]]) - + def test_module_build_validates_semver_before_building(self): + """Packaging rejects malformed versions before invoking Cargo.""" + for version, valid in VERSION_CASES: + with self.subTest(version=version): + result = subprocess.run( + ["bash", str(ROOT / "scripts/build-module.sh"), + "--crate", "tinywallet-module", "--version", version, + "--target", "unsupported-target", "--host", "test-host"], + capture_output=True, text=True, check=False, + ) + self.assertEqual(result.returncode, 2) + self.assertIn("unsupported module target" if valid else "usage:", result.stderr) + + def test_powershell_version_pattern_validates_complete_semver(self): + """The Windows packaging guard uses the same SemVer rules.""" + script = (ROOT / "scripts/build-module.ps1").read_text() + pattern = re.search(r"\$Version -notmatch '([^']+)'", script).group(1) + self.assertTrue(pattern.endswith(r"\z"), ".NET must reject trailing newlines") + # Python's fullmatch models the strict end anchor used by .NET. + pattern = pattern.removesuffix(r"\z") + for version, valid in VERSION_CASES: + with self.subTest(version=version): + self.assertEqual(re.fullmatch(pattern, version) is not None, valid) + + def test_resolve_step_validates_tag_format_and_outputs(self): + """Tag resolution rejects invalid refs and passes valid version tags through.""" + script = self.workflow.split(" run: |\n", 1)[1].split(" module-release:", 1)[0] + script = "\n".join(line[10:] for line in script.splitlines()) + with tempfile.TemporaryDirectory() as directory: + for tag, valid in [("v" + version, valid) for version, valid in VERSION_CASES] + [ + ("main", False), ("v1.0.0; echo injected", False) + ]: + output = Path(directory) / "output" + output.write_text("") + result = subprocess.run(["bash", "-c", script], + env=dict(os.environ, TAG=tag, GITHUB_OUTPUT=str(output)), capture_output=True, text=True) + self.assertEqual(result.returncode == 0, valid, (tag, result.stderr)) + if valid: + self.assertEqual(output.read_text(), "tag="+tag+"\nversion="+tag[1:]+"\n") + else: + self.assertEqual(output.read_text(), "") if __name__ == "__main__": unittest.main() diff --git a/scripts/test-distro.sh b/scripts/test-distro.sh index 167364fe..012a46f2 100755 --- a/scripts/test-distro.sh +++ b/scripts/test-distro.sh @@ -8,7 +8,7 @@ case "$image" in *) echo "unsupported module build image: $image" >&2; exit 2 ;; esac -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" crate="" version="" rust_target="" host="" output="release-assets" extras='[]' while (($#)); do case "$1" in @@ -49,5 +49,6 @@ docker run --rm \ --env MODULE_TARGET="$rust_target" \ --env MODULE_HOST="$host" \ --env EXTRA_PACKAGED_FILES="$extras" \ + --env HOST_UID="$(id -u)" --env HOST_GID="$(id -g)" \ --env OUTPUT_DIR="${output_path#"$root"/}" \ "$image" bash /workspace/scripts/build-module-in-distro.sh diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index 521ce8ea..c1cceb7d 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" cd "$root" if [[ "${RUNNER_OS:-}" == "Windows" ]]; then pwsh -NoProfile -File scripts/test-e2e.ps1 diff --git a/scripts/verify-module.sh b/scripts/verify-module.sh index 9399c529..0f9fe854 100755 --- a/scripts/verify-module.sh +++ b/scripts/verify-module.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" if [[ "${1:-}" == --archive ]]; then shift; fi archive="${1:?usage: verify-module.sh --archive }" archive="$(cd "$(dirname "$archive")" && pwd -P)/$(basename "$archive")" diff --git a/vendor/tinybus b/vendor/tinybus index cbb2eea9..0cbc48b2 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit cbb2eea92c57b24c6901a6f0b6445da7c5da46aa +Subproject commit 0cbc48b2f610e36bb5bdcd49fd7bb54252ccf957