Skip to content

Commit a8a81fd

Browse files
feat: --build-timestamp + dev.ocx.sh continuous deploy
Add `--build-timestamp [datetime|date|none]` to `ocx package push` to attach UTC build metadata to published tags. Bare flag defaults to `datetime`; identifiers already carrying build metadata or lacking the `X.Y.Z` core are rejected (`#[non_exhaustive] Error::BuildMeta`, `require_equals=true`). Cascade behaviour unchanged — `Version::parent()` strips build before deriving rolling targets. Shared `BuildTimestampFormat` enum + `build_timestamp` helper move from `ocx_mirror` into `ocx_lib::package::version::build_meta`; mirror callers pass `None` because their version strings are pre-formatted via `normalize_version`. `Version::with_build` attaches build metadata via the typed model instead of string formatting. Add `deploy-dev.yml` triggered by every `main` push and manual `workflow_dispatch`. Always builds linux x86_64 and aarch64 (gnu+musl) for `ocx` and `ocx-mirror` via cargo-zigbuild, publishes to `dev.ocx.sh/ocx:<core>-dev_<TS>` and `dev.ocx.sh/ocx:mirror-<core>-dev_<TS>`. Dispatch input `include_cross_platform: true` extends matrix to darwin + windows (amd64/arm64). Version core from `git-cliff --bumped-version`; empty output fails loudly. Extract `oci-publish.yml` as reusable workflow handling `bare` (dev) and `cargo-dist` (prod) artifact sources; `post-release-oci-publish.yml` delegates platform-loop + push. `.github/actions/build-rust` gains `use_zigbuild: bool` so dev can cross-compile aarch64+musl from x86 ubuntu without leaving the native path. Dev publish job uses `dev.ocx.sh` GitHub Environment for secret gating (`REGISTRY_USER`/`REGISTRY_TOKEN` mapped to `OCX_AUTH_dev_ocx_sh_USER`/`_TOKEN`). CI security hardening from swarm-review: pin `messense/cargo-xwin` by digest (CWE-829), regex-validate `inputs.registry` (CWE-78), `persist-credentials: false` on checkouts (CWE-522), explicit `secrets` blocks replacing `secrets: inherit`, registry allowlist defense-in-depth, `shell: bash` on windows run steps, drop `|| true` masking git-cliff failures, hard-fail on missing artifacts, prerelease guard on publish job. Build timestamp baked into `version_tag` caller-side; the `apply_build_timestamp` input is removed for single source-of-truth across the matrix push loop. Tests: `Version::with_build` invariants (bare patch / variant pre-release / already present / no patch), publisher `apply_build_meta`, `BuildTimestampFormat` round-trip, and `test/tests/test_package_push_build_timestamp.py` covering CLI surface (datetime / date / none / bare / cascade / double-push / NoPatch). Docs: CHANGELOG `[Unreleased]` entries, ADR `adr_version_build_separator.md` → Accepted, `subsystem-package.md` adds `version/build_meta.rs` row, `subsystem-mirror.md` re-export note, dead ADR link → prose, `command-line.md` documents absent-flag clause.
1 parent 5fb23b8 commit a8a81fd

20 files changed

Lines changed: 1124 additions & 154 deletions

File tree

.claude/artifacts/adr_version_build_separator.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
## Metadata
44

5-
**Status:** Proposed
5+
**Status:** Accepted
66
**Date:** 2026-03-12
7+
**Date Accepted:** 2026-05-14
78
**Deciders:** mherwig
89
**Beads Issue:** N/A
910
**Related PRD:** N/A

.claude/rules/subsystem-cli-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ ADR: [`adr_cli_high_low_layering.md`](../../.claude/artifacts/adr_cli_high_low_l
6969
| `lock` | Resolve project tool tags to digests and write `ocx.lock` | No | `-g/--group` |
7070
| `package pull PKGS...` | Download to object store only | N/A (is pull) | `-p` |
7171
| `package create PATH` | Bundle directory into archive | No | `-o`, `-m`, `-l`, `-j`, `--force` |
72-
| `package push -i ID LAYERS...` | Publish archive to registry | No | `-i/--identifier` (required), `-c/--cascade`, `-n`, `-m`, `-p` |
72+
| `package push -i ID LAYERS...` | Publish archive to registry | No | `-i/--identifier` (required), `-c/--cascade`, `-n`, `-m`, `-p`, `--build-timestamp [datetime\|date\|none]` |
7373
| `package describe ID` | Push description metadata | No | `--readme`, `--logo`, `--title` |
7474
| `package test -i ID LAYERS... -- CMD` | Materialize + exec locally (no registry) | **Yes** (deps only) | `-i/--identifier` (required), `-p`, `-m`, `--keep`, `-o/--output`, `--self`, `--clean` |
7575
| `package info ID` | Display description metadata | No | `--save-readme`, `--save-logo` |

.claude/rules/subsystem-mirror.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Separate crate: mirror tool standalone binary, own CLI, not part of `ocx` packag
2020
| `command/check.rs` | Dry-run sync |
2121
| `command/validate.rs` | Spec validation only |
2222
| `command/options.rs` | Shared `SyncOptions` (--exact-version, --latest, --fail-fast) |
23-
| `spec/spec.rs` | `MirrorSpec` root, `load_spec()`, extends chain resolution |
23+
| `spec/spec.rs` | `MirrorSpec` root, `load_spec()`, extends chain resolution; `build_timestamp` field uses `BuildTimestampFormat` re-exported from `ocx_lib::package::version` |
2424
| `spec/source.rs` | `Source` enum (GithubRelease, UrlIndex) |
2525
| `spec/target.rs` | `Target` (registry + repository) |
2626
| `spec/assets.rs` | `AssetPatterns` (platform → regex[] mapping) |
@@ -40,7 +40,7 @@ Separate crate: mirror tool standalone binary, own CLI, not part of `ocx` packag
4040
| `pipeline/mirror_result.rs` | `MirrorResult`: Pushed/Skipped/Failed |
4141
| `resolver.rs` | `resolve_assets()`: apply regex patterns to asset names |
4242
| `filter.rs` | `filter_versions()`: apply bounds, prerelease skip, backfill cap |
43-
| `normalizer.rs` | `normalize_version()`: add build timestamp |
43+
| `normalizer.rs` | `normalize_version()`: attach build timestamp to a version string; re-exports `build_timestamp` from `ocx_lib::package::version` (defined in `version/build_meta.rs`) |
4444
| `error.rs` | `MirrorError`: SpecInvalid, SpecNotFound, ExecutionFailed, SourceError |
4545

4646
## Pipeline Architecture

.claude/rules/subsystem-package.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Tagged enum metadata (`Metadata::Bundle`) supports future format versions, no br
3535
| `bundle.rs` | `BundleBuilder`: tar archive creation with configurable compression |
3636
| `cascade.rs` | Cascade algebra: `decompose()`, `cascade()`, `resolve_cascade_tags()`, `push_with_cascade()` |
3737
| `tag.rs` | `Tag` enum: Latest, Internal(InternalTag), Version, Canonical, Other |
38-
| `version.rs` | `Version` struct: semver-inspired with build + prerelease, rolling tag support |
38+
| `version.rs` | `Version` struct: semver-inspired with build + prerelease, rolling tag support; `with_build(seg)` attaches a build-metadata segment (errors if no `X.Y.Z` core or build already present); re-exports `BuildTimestampFormat` + `build_timestamp` from `version/build_meta.rs` |
39+
| `version/build_meta.rs` | `BuildTimestampFormat` enum (`Datetime` / `Date` / `None`); `build_timestamp(format)` returns the UTC stamp string or `None`; `BuildMetaError` enum (`NoPatch` / `AlreadyPresent`) |
3940
| `install_info.rs` | `InstallInfo`: identifier + metadata + content path |
4041
| `description.rs` | Package description metadata (title, description, keywords, README, logo) |
4142

.github/actions/build-rust/action.yml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
name: Build Rust Binary
2-
description: Build an ocx binary for a given target (native builds only)
2+
description: Build an ocx binary for a given target (native builds or zig cross-compile)
33

44
inputs:
55
target:
66
description: Rust target triple
77
required: true
8+
use_zigbuild:
9+
description: Use cargo-zigbuild instead of cargo build (required for cross-compiling aarch64-linux and musl from x86 runners)
10+
required: false
11+
default: "false"
812

913
runs:
1014
using: composite
@@ -14,15 +18,38 @@ runs:
1418
with:
1519
toolchain: stable
1620
targets: ${{ inputs.target }}
21+
- name: Install target on rust-toolchain.toml channel
22+
# rust-toolchain.toml pins an explicit channel; dtolnay's `targets:` only
23+
# adds the target to the `stable` alias toolchain. Re-add against the
24+
# pinned channel resolved from the workspace root so cargo finds std/core.
25+
shell: bash
26+
env:
27+
TARGET: ${{ inputs.target }}
28+
run: rustup target add "$TARGET"
1729
- name: Rust Cache
1830
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
1931
with:
2032
save-if: ${{ github.ref == 'refs/heads/main' }}
21-
- name: Build
33+
- name: Install Zig
34+
if: ${{ inputs.use_zigbuild == 'true' }}
35+
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
36+
- name: Install cargo-zigbuild
37+
if: ${{ inputs.use_zigbuild == 'true' }}
38+
uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17
39+
with:
40+
tool: cargo-zigbuild
41+
- name: Build (cargo)
42+
if: ${{ inputs.use_zigbuild != 'true' }}
2243
shell: bash
2344
env:
2445
TARGET: ${{ inputs.target }}
2546
run: cargo build --release --target="$TARGET" --locked
47+
- name: Build (zigbuild)
48+
if: ${{ inputs.use_zigbuild == 'true' }}
49+
shell: bash
50+
env:
51+
TARGET: ${{ inputs.target }}
52+
run: cargo zigbuild --release --target="$TARGET" --locked
2653
- name: Prepare Binaries
2754
shell: bash
2855
env:

.github/workflows/deploy-dev.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
name: Deploy Dev
2+
3+
# Rolling continuous-deploy pipeline. Runs after `Basic Verification` succeeds
4+
# on `main` (smoke + acceptance tests must pass first). Builds linux x86_64 and
5+
# aarch64 musl for both `ocx` and `ocx-mirror` via cargo-zigbuild, then
6+
# publishes them to `dev.ocx.sh/ocx:<core>-dev_<timestamp>` (and
7+
# `mirror-<core>-dev_<timestamp>`).
8+
#
9+
# Musl-only matches the prod target set in `post-release-oci-publish.yml`.
10+
# Cross-compile uses cargo-zigbuild on ubuntu-22.04 (same toolchain choices as
11+
# cargo-dist's prod build path; full `dist build` alignment is deferred until
12+
# we can mirror the exact `dist plan` matrix). Published artifacts are tagged
13+
# as pre-release (`-dev_<TS>`) so they cannot collide with the stable graph.
14+
15+
on:
16+
workflow_run:
17+
workflows: [Basic Verification]
18+
types: [completed]
19+
branches: [main]
20+
workflow_dispatch:
21+
22+
permissions:
23+
contents: read
24+
25+
# Serialize runs so the timestamp suffix stays monotonic; never cancel a
26+
# publish in flight.
27+
concurrency:
28+
group: deploy-dev-${{ github.ref }}
29+
cancel-in-progress: false
30+
31+
jobs:
32+
compute-version:
33+
name: Compute version
34+
# workflow_run fires on every completion; gate on success. workflow_dispatch
35+
# bypasses the gate (manual trigger always proceeds).
36+
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
37+
runs-on: ubuntu-latest
38+
outputs:
39+
version: ${{ steps.cliff.outputs.version }}
40+
version_tag: ${{ steps.cliff.outputs.version_tag }}
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
44+
with:
45+
fetch-depth: 0
46+
persist-credentials: false
47+
- name: Install git-cliff
48+
uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17
49+
with:
50+
tool: git-cliff
51+
- name: Compute next version and timestamp
52+
id: cliff
53+
shell: bash
54+
run: |
55+
set -euo pipefail
56+
raw="$(git-cliff --bumped-version)"
57+
version="${raw#v}"
58+
if [[ -z "$version" ]]; then
59+
echo "::error::git-cliff --bumped-version returned empty output; no commits since last tag — refusing to publish a dev build"
60+
exit 1
61+
fi
62+
ts="$(date -u +%Y%m%d%H%M%S)"
63+
echo "version=${version}" | tee -a "$GITHUB_OUTPUT"
64+
echo "version_tag=${version}-dev_${ts}" | tee -a "$GITHUB_OUTPUT"
65+
66+
build-linux:
67+
name: Build (${{ matrix.target }})
68+
needs: [compute-version]
69+
# ubuntu-22.04 matches the runner cargo-dist plans for linux targets —
70+
# older glibc baseline for portability of any future gnu targets.
71+
runs-on: ubuntu-22.04
72+
strategy:
73+
fail-fast: false
74+
matrix:
75+
target:
76+
- x86_64-unknown-linux-musl
77+
- aarch64-unknown-linux-musl
78+
steps:
79+
- name: Checkout
80+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
81+
with:
82+
submodules: true
83+
persist-credentials: false
84+
- name: Build
85+
uses: ./.github/actions/build-rust
86+
with:
87+
target: ${{ matrix.target }}
88+
# cargo-zigbuild covers aarch64 and musl targets uniformly from
89+
# x86_64 ubuntu runners; matches what cargo-dist uses for prod.
90+
use_zigbuild: true
91+
92+
publish:
93+
name: Publish to dev.ocx.sh
94+
needs: [compute-version, build-linux]
95+
uses: ./.github/workflows/oci-publish.yml
96+
with:
97+
registry: dev.ocx.sh
98+
repo: ocx
99+
version_tag: ${{ needs.compute-version.outputs.version_tag }}
100+
variants: '["","mirror"]'
101+
targets: '["x86_64-unknown-linux-musl","aarch64-unknown-linux-musl"]'
102+
environment: dev.ocx.sh
103+
secrets:
104+
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
105+
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}

.github/workflows/notify-discord.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
types: [published]
99

1010
workflow_run:
11-
workflows: ["Basic Verification", "License Compliance", "Deploy Canary", "Deploy Website"]
11+
workflows: ["Basic Verification", "License Compliance", "Deploy Canary", "Deploy Website", "Deploy Dev"]
1212
types: [completed]
1313

1414
deployment_status:

0 commit comments

Comments
 (0)