Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions .github/workflows/testnet-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ name: Testnet Nightly
# frozen push, no-token push, third-party verify, depth+filter
#
# They split by what they need:
# * READ-ONLY specs need only a network path to testnet DAPI and the seeded fixture repo,
# so they run unconditionally and their failure fails this workflow.
# * READ-ONLY specs need only a network path to testnet DAPI and the read fixture repo
# (seeded by `seed-read-fixture` when the secrets exist; e2e/README.md reserves it), so
# they run unconditionally and their failure fails this workflow.
# * The CLI suite and the write specs need FUNDED fixture identities, which cannot live in
# a fork's PR context. A separate `fixtures` probe job decides whether they run, so when
# the secrets are absent the suite job's conclusion is literally `skipped` rather than a
# green job with everything gated out — a distinction any badge or alert rule can see.
#
# Fixture identity secrets (e2e/cli/config.sh names the roles):
# FORGE_TEST_IDENTITY_DEPLOYER - owns the reused m1 repo, grants tokens
# FORGE_TEST_IDENTITY_DEPLOYER - owns the CLI suite's repo and the read fixture, grants
# tokens (which repo each suite may write: e2e/README.md)
# FORGE_TEST_IDENTITY_COLLAB - holds an unfrozen WRITE token
# FORGE_TEST_IDENTITY_CONTRIB - holds no token (negative push case)
# FORGE_TEST_IDENTITY_FROZEN - holds a frozen WRITE token (negative push case)
Expand All @@ -35,6 +37,11 @@ concurrency:
jobs:
web-read-paths:
name: playwright (read-only, live testnet)
# Reads the dedicated read fixture (e2e/README.md), so it waits for the seed job — but
# still runs when that job is skipped (no secrets: the fixture is read as it stands) or
# failed (the specs then say what is wrong with it).
needs: seed-read-fixture
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
Expand Down Expand Up @@ -76,6 +83,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
present: ${{ steps.probe.outputs.present }}
# The read-fixture seeder signs with DEPLOYER alone.
deployer: ${{ steps.probe.outputs.deployer }}
steps:
- name: Probe
id: probe
Expand All @@ -85,6 +94,11 @@ jobs:
FORGE_TEST_IDENTITY_CONTRIB: ${{ secrets.FORGE_TEST_IDENTITY_CONTRIB }}
FORGE_TEST_IDENTITY_FROZEN: ${{ secrets.FORGE_TEST_IDENTITY_FROZEN }}
run: |
if [ -n "$FORGE_TEST_IDENTITY_DEPLOYER" ]; then
echo "deployer=true" >> "$GITHUB_OUTPUT"
else
echo "deployer=false" >> "$GITHUB_OUTPUT"
fi
missing=()
# Report the SECRET names an operator has to create, not internal aliases.
for name in FORGE_TEST_IDENTITY_DEPLOYER FORGE_TEST_IDENTITY_COLLAB \
Expand All @@ -108,6 +122,46 @@ jobs:
echo "present=true" >> "$GITHUB_OUTPUT"
fi

# Make the browser specs' read fixture exist and hold exactly the expected commit. It is
# its own repo, written only by this job, so nothing the CLI suite or an ad-hoc storage run
# stores can reach what the browser reads (e2e/README.md). Idempotent: a no-op once seeded.
seed-read-fixture:
name: seed the read fixture
needs: fixtures
if: needs.fixtures.outputs.deployer == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
ID_DEPLOYER_JSON: ${{ secrets.FORGE_TEST_IDENTITY_DEPLOYER }}
steps:
- uses: actions/checkout@v5
- name: Install protoc
env:
PROTOC_VERSION: '28.3'
run: |
curl -sSLo /tmp/protoc.zip \
"https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip"
sudo unzip -q -o /tmp/protoc.zip -d /usr/local bin/protoc 'include/*'
sudo chmod +x /usr/local/bin/protoc
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: e2e-cli
- name: Materialize the DEPLOYER identity
run: |
dir="${HOME}/.config/dash-forge/test-identities"
mkdir -p "$dir" && chmod 700 "$dir"
printf '%s' "$ID_DEPLOYER_JSON" > "$dir/DEPLOYER.identity.json"
# harness_init checks for the whole role set; only DEPLOYER signs here.
for r in COLLAB CONTRIB; do cp "$dir/DEPLOYER.identity.json" "$dir/$r.identity.json"; done
chmod 600 "$dir"/*.identity.json
- name: Build CLI binaries
timeout-minutes: 30
run: cargo build --locked -p dg -p git-remote-dash
- name: Seed
timeout-minutes: 12
run: bash e2e/cli/seed-read-fixture.sh

cli-suite:
name: cli e2e (live testnet)
needs: fixtures
Expand Down Expand Up @@ -140,6 +194,8 @@ jobs:
- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
shared-key: e2e-cli

- name: Materialize fixture identities
run: |
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ SHELL := /bin/bash

COMPOSE_FILE := infra/docker-compose.yml

.PHONY: check check-rust check-web build build-rust build-web infra-up infra-down e2e devnet-identities devnet-identities-verify storage-it storage-e2e
.PHONY: check check-rust check-web build build-rust build-web infra-up infra-down e2e e2e-fixture devnet-identities devnet-identities-verify storage-it storage-e2e

## check: run rust + web lint/test suites; tolerant of dirs that don't exist yet
check: check-rust check-web
Expand Down Expand Up @@ -54,13 +54,17 @@ infra-up:
infra-down:
docker compose -f $(COMPOSE_FILE) down -v

## e2e: run the CLI end-to-end suite (LIVE testnet) against the reused m1 repo.
## e2e: run the CLI end-to-end suite (LIVE testnet) against its reserved repo (e2e/README.md).
## Builds the binaries if needed, then drives real git push/clone through the
## dash:// helper. See e2e/cli/README-less run.sh header for env knobs
## (RUN_ID, E2E_TIMEOUT, E2E_NO_CLEANUP, subset args). Exits non-zero on any FAIL.
e2e: build-rust
@bash e2e/cli/run.sh

## e2e-fixture: seed the browser specs' read fixture (idempotent; see e2e/README.md).
e2e-fixture: build-rust
@bash e2e/cli/seed-read-fixture.sh

## devnet-identities: mint (or resume) the 9-role identity pool on a devnet,
## funded from the devnet's faucet wallet key, then verify every identity on
## Platform. The key is read from dash-network-configs at runtime (process
Expand Down
50 changes: 16 additions & 34 deletions crates/forge-core/src/collab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ const DOC_EVENT: &str = "event";
const DOC_REVIEW: &str = "review";
const DOC_LABEL: &str = "label";
const DOC_RELEASE: &str = "release";
const DOC_REF_UPDATE: &str = "refUpdate";
const DOC_PROTECTED_REF_UPDATE: &str = "protectedRefUpdate";
// Registry-contract document types.
const DOC_STAR: &str = "star";
const DOC_FOLLOW: &str = "follow";
Expand Down Expand Up @@ -926,46 +924,30 @@ impl<'a> PullRequestService<'a> {
}

/// Collect every oid that was ever a tip of `base_ref_name` (the monotonic merge-
/// reachability set) plus the newest such tip. Walks the full `refUpdate` +
/// `protectedRefUpdate` history for the ref (paginated), taking every non-null `newOid`.
/// reachability set) plus the newest such tip. Walks the ref's full `refUpdate` +
/// `protectedRefUpdate` history — [`crate::refs::read_ref_history`], an equality read on
/// one `refNameHash`, so its cost is this ref's pushes, not the repo's — taking every
/// non-null `newOid`.
async fn base_ref_tips(
&self,
contract: &LoadedContract,
base_ref_name: &str,
) -> Result<(std::collections::BTreeSet<String>, Option<String>)> {
let ref_name_hash = sha256(base_ref_name.as_bytes());
let updates = crate::refs::read_ref_history(self.client, contract, ref_name_hash).await?;
let mut tips: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut newest: Option<(u64, String, String)> = None; // (created_at, id, oid)
for doc_type in [DOC_REF_UPDATE, DOC_PROTECTED_REF_UPDATE] {
let docs = self
.client
.query_all_documents(
contract,
doc_type,
&[QueryFilter::eq(
"refNameHash",
FieldValue::bytes32(ref_name_hash),
)],
&[QueryOrder::asc("$createdAt")],
)
.await?;
for d in &docs {
let Some(oid) = d.field_hex("newOid") else {
continue;
};
if oid.is_empty() || oid.bytes().all(|b| b == b'0') {
continue; // null oid = ref deletion, never a reachable tip
}
tips.insert(oid.clone());
let created_at = d.created_at.unwrap_or(0);
let candidate = (created_at, d.id.clone(), oid);
let better = match &newest {
None => true,
Some(n) => (n.0, &n.1) < (candidate.0, &candidate.1),
};
if better {
newest = Some(candidate);
}
for u in updates {
if u.new_oid.is_empty() || u.new_oid.bytes().all(|b| b == b'0') {
continue; // null oid = ref deletion, never a reachable tip
}
tips.insert(u.new_oid.clone());
let better = match &newest {
None => true,
Some(n) => (n.0, &n.1) < (u.created_at, &u.id),
};
if better {
newest = Some((u.created_at, u.id, u.new_oid));
}
}
Ok((tips, newest.map(|(_, _, oid)| oid)))
Expand Down
3 changes: 3 additions & 0 deletions crates/forge-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
//! `WriteEngine` document create/delete lifecycle + idempotent-retry journal types.
//! - [`repo`] — `RepoService`: the repo-lifecycle API (`create_repo` / `resolve_repo` /
//! ref + pack-manifest + chunk read/write) `git-remote-dash` calls.
//! - [`refs`] — complete ref-update reads (keyset scan + completeness fallback) shared by
//! ref listing and PR base-tip resolution.
//! - [`tokens`] — `TokenService`: the collaborator ACL (grant/suspend/revoke = token
//! mint/freeze/destroy; balances = the on-chain collaborator list).
//! - [`collab`] — issue / PR / review / release / label services + the registry social
Expand All @@ -32,6 +34,7 @@ pub mod keystore;
pub mod network;
pub mod pack;
pub mod platform;
pub mod refs;
pub mod repo;
pub mod rules;
pub mod storage;
Expand Down
Loading
Loading