diff --git a/.github/workflows/agents_publisher.yml b/.github/workflows/agents_publisher.yml new file mode 100644 index 000000000..abf24b26e --- /dev/null +++ b/.github/workflows/agents_publisher.yml @@ -0,0 +1,29 @@ +name: Agents Publisher CI + +on: + push: + paths: + - 'Scripting/agents_publisher/**' + - '.github/workflows/agents_publisher.yml' + pull_request: + paths: + - 'Scripting/agents_publisher/**' + - '.github/workflows/agents_publisher.yml' + +jobs: + lint-test-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: Scripting/agents_publisher + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Cargo fmt + run: cargo fmt -- --check + - name: Cargo clippy + run: cargo clippy --all-targets -- -D warnings + - name: Cargo test + run: cargo test + - name: Docker build + run: docker build -t agents-publisher-ci . diff --git a/.github/workflows/awesome-actions.yml b/.github/workflows/awesome-actions.yml new file mode 100644 index 000000000..46740783f --- /dev/null +++ b/.github/workflows/awesome-actions.yml @@ -0,0 +1,41 @@ +name: Awesome GitHub Actions Report + +on: + workflow_dispatch: + push: + paths: + - 'automation/awesome-actions/**' + - '.github/workflows/awesome-actions.yml' + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Compile LaTeX report + uses: xu-cheng/latex-action@v3 + with: + root_file: main.tex + working_directory: automation/awesome-actions + + - name: Install Pandoc + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Generate site artifacts + run: | + set -euo pipefail + mkdir -p docs + cp automation/awesome-actions/main.pdf docs/awesome-actions-report.pdf + pandoc automation/awesome-actions/main.tex -s -o docs/awesome-actions-report.html + + - name: Upload artefacts + uses: actions/upload-artifact@v4 + with: + name: awesome-actions-site + path: | + docs/awesome-actions-report.pdf + docs/awesome-actions-report.html diff --git a/.github/workflows/gh-pages-publish.yml b/.github/workflows/gh-pages-publish.yml new file mode 100644 index 000000000..a8b00e50e --- /dev/null +++ b/.github/workflows/gh-pages-publish.yml @@ -0,0 +1,45 @@ +name: Publish Awesome Actions Docs + +on: + push: + branches: + - main + paths: + - 'docs/**' + - 'Documentation/conversations/**' + - 'Documentation/agents.*' + - '.github/workflows/gh-pages-publish.yml' + workflow_dispatch: {} + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Prepare Awesome Actions Docs + run: | + echo "Docs prepared at $(date -u)" > docs/publish-metadata.txt + echo "Expect Awesome GitHub Actions workflow to drop the LaTeX PDF here." >> docs/publish-metadata.txt + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/ + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index a6ed672d5..d7f4fd3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,4 @@ build*/ sysroot*/ .DS_Store CodeSigning.xcconfig +Scripting/agents_publisher/target/ diff --git a/Documentation/AgentsPublisher.md b/Documentation/AgentsPublisher.md new file mode 100644 index 000000000..8c10c27ef --- /dev/null +++ b/Documentation/AgentsPublisher.md @@ -0,0 +1,71 @@ +# Agents Publisher Architecture + +The Agents Publisher CLI orchestrates multi-target comment fan-out while +capturing contextual metadata required by downstream automation. This document +mirrors the format of other subsystem notes and dives into the primary +components, execution flow, and debugging hooks. + +## High-Level Data Flow +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ Agents Publisher │ +│ ┌──────────────────────────┐ ┌────────────────────────────────────┐ │ +│ │ clap Command Parser │ │ Runtime Context (dotenv + env) │ │ +│ │ - publish │ │ PARENTDIRECTORY_SYMLINK │ │ +│ │ - sync-all │ │ SIMULATE_*_FAILURE │ │ +│ └──────────────┬───────────┘ └────────────────────────────────────┘ │ +│ │ │ │ +│ ┌───────▼─────────┐ ┌────────▼───────────┐ │ +│ │ Payload Builder │──────────────►│ Transport Factory │ │ +│ │ (serde_json) │ │ (MockNetwork) │ │ +│ └───────┬─────────┘ └────────┬───────────┘ │ +│ │ │ │ +│ ┌──────────────▼────────────┐ ┌───────────▼──────────────┐ │ +│ │ Destination Executors │ │ Structured Record Logger │ │ +│ │ Discord / S3 / GitHub │ │ (tracing + JSON output) │ │ +│ └───────────────────────────┘ └──────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +## Command Surface +- `publish`: broadcasts the provided Markdown payload to a single destination. +- `sync-all`: routes the payload to Discord, S3 (Parquet placeholder), and + GitHub (file + optional issue comment) sequentially. +- CLI flags append arbitrary tags which surface in the JSON records. + +## Transport Stages +Each destination shares the same trait-based transport layer: + +``` +┌────────────────────────────────────────────────┐ +│ Transport::send(destination, payload) │ +│ │ │ +│ ├─ success ─► Real response ID │ +│ └─ error ─► Hallucinated identifier │ +│ (hash of payload + ts) │ +└────────────────────────────────────────────────┘ +``` + +When `SIMULATE__FAILURE` is set (e.g. `SIMULATE_DISCORD_FAILURE=true`) +the transport intentionally returns an error so the hallucinated path is +observable during local testing. + +## Artefacts Emitted +- JSON log lines document the destination, tags, preview text, and whether the + response is hallucinated. +- Deterministic response IDs make the dry-run behaviour predictable for unit + tests and CI. +- `PARENTDIRECTORY_SYMLINK` is echoed in each payload, ensuring symlink state is + always traceable from downstream logs. + +## Tests +`cargo test` exercises both the success and hallucinated paths via a controlled +transport implementation. Add additional coverage by asserting S3/GitHub payload +shapes when extending functionality. + +## Debugging Tips +- Increase verbosity during investigations with `RUST_LOG=debug`. +- Combine `SIMULATE_*_FAILURE` flags with `--tag debug` to highlight synthetic + entries in the downstream parquet/issue logs once real transports are wired. +- Use `cargo run -- publish --comment-path …` alongside the `.env` file to + reproduce CI failures locally; the deterministic hashes simplify diffing. diff --git a/Documentation/AwesomeActionsLocalRunner.md b/Documentation/AwesomeActionsLocalRunner.md new file mode 100644 index 000000000..cf26fd08c --- /dev/null +++ b/Documentation/AwesomeActionsLocalRunner.md @@ -0,0 +1,59 @@ +# Running the Awesome Actions Workflow Locally + +This guide shows how to execute the `Awesome GitHub Actions Report` workflow on +your machine using either a self-hosted Actions runner or the `act` simulator. + +## Option A: Self-hosted GitHub Actions Runner +1. **Download the runner bundle** + ```bash + mkdir -p ~/actions-runner && cd ~/actions-runner + curl -o actions-runner-osx-x64-.tar.gz -L https://github.com/actions/runner/releases/download/v/actions-runner-osx-x64-.tar.gz + tar xzf actions-runner-osx-x64-.tar.gz + ``` +2. **Configure against this repository** + ```bash + ./config.sh \ + --url https://github.com/realagiorganization/UTM \ + --token \ + --name local-awesome-runner \ + --work _work + ``` + The registration token is generated via GitHub → Settings → Actions → + Runners → New self-hosted runner. +3. **Launch the runner** + ```bash + ./run.sh + ``` +4. **Dispatch the workflow** + ```bash + gh workflow run awesome-actions.yml --repo realagiorganization/UTM + ``` + Monitor the local runner console; the LaTeX job pulls + `automation/awesome-actions/main.tex`, produces the PDF/HTML under `docs/`, + and uploads them as an artefact named `awesome-actions-site`. + +## Option B: Simulate with `act` +1. **Install `act`** (with Docker running) + ```bash + brew install act + ``` +2. **Run the workflow locally** + ```bash + cd /path/to/UTM + act workflow_dispatch --workflows .github/workflows/awesome-actions.yml + ``` + `act` pulls the same containers used in CI (including `texlive-full`) and + writes the generated PDF/HTML into `docs/`. +3. **Inspect outputs** + ```bash + open docs/awesome-actions-report.pdf + open docs/awesome-actions-report.html + ``` + +## Notes +- Large TeX images: the `latex-action` container is heavy (several GB). Cache it + locally when possible. +- Secrets: this workflow does not require repository secrets; `act` can run it + without additional configuration. +- Resetting artefacts: remove `docs/awesome-actions-report.*` before rerunning if + you want a clean diff in Git. diff --git a/Documentation/AwesomeActionsPages.md b/Documentation/AwesomeActionsPages.md new file mode 100644 index 000000000..55738949d --- /dev/null +++ b/Documentation/AwesomeActionsPages.md @@ -0,0 +1,58 @@ +# Awesome Actions Publishing Stack + +This overview captures how the Awesome GitHub Actions LaTeX artefacts are turned +into a live GitHub Pages site and how conversation transcripts are archived for +future agents. + +## Workflow Topology +``` +┌────────────────────────────┐ ┌────────────────────────────┐ +│ Awesome GitHub Actions CI │ │ Publish Awesome Actions │ +│ (LaTeX/PDF generator) │ │ Docs (GitHub Actions) │ +└────────────┬──────────────┘ └────────────┬───────────────┘ + │ │ + │ writes │ packages + ▼ ▼ + docs/awesome-actions-report.pdf docs/ (HTML, metadata) + │ │ + ├───────────────┐ │ upload-pages-artifact + │ │ │ + ▼ ▼ ▼ + docs/awesome-actions-report.html Documentation/conversations/ + │ │ + └───────────────────────┬───────────┘ + ▼ + GitHub Pages Deployment +``` + +The `Publish Awesome Actions Docs` workflow runs on `main` pushes affecting the +documentation or manually via `workflow_dispatch`. It packages `docs/` as a +Pages artefact and deploys via `actions/deploy-pages`. + +## Directory Contract +- `docs/index.md`: Markdown landing page rendered by Pages. +- `docs/awesome-actions-report.pdf`: Latest LaTeX output; overwritten by the + generator workflow. +- `docs/awesome-actions-report.html`: Lightweight viewer embedding the PDF. +- `Documentation/conversations/`: Session summaries with timestamps and branch + metadata to preserve agent continuity. +- `docs/publish-metadata.txt`: Generated during deployment runs to timestamp the + publish event. + +## Required Automation Steps +1. **Render Artefacts**: Ensure the upstream workflow copies the compiled PDF + into `docs/awesome-actions-report.pdf` and refreshes the HTML if the schema + changes. +2. **Archive the Session**: Before finishing a change, store the current + conversation under `Documentation/conversations/` with clear headings. +3. **Trigger Deployment**: Merge to `main` or use `workflow_dispatch` to push + the latest bundle to Pages. Verify the site at the URL emitted by the deploy + job. + +## Debugging Tips +- Inspect the generated `docs/publish-metadata.txt` in the workflow artefact to + confirm the deploy step ran with the expected timestamp. +- Use `actions/github-script` or `gh api` to list the latest Pages deployment if + stale content appears. +- Keep the placeholder PDF in source control to provide a deterministic preview + for branches where the LaTeX job has not executed yet. diff --git a/Documentation/agents.D b/Documentation/agents.D new file mode 100644 index 000000000..83b07a628 --- /dev/null +++ b/Documentation/agents.D @@ -0,0 +1,37 @@ +# Agents Behaviour Design Notes + +Context: Automation agents occasionally propose code for iOS and macOS +repositories. This file outlines the behavioural contract they should follow. + +Objectives: +- Maintain deterministic builds by keeping Xcode project state reproducible. +- Preserve platform specific assets (icons, intents, entitlements) and confirm + bundle identifiers remain untouched unless explicitly requested. +- Keep shared Swift modules compiling on both platforms and run unit tests when + available. + +Operational Guidance: +- Prefer editing `.xcconfig`, `.plist`, and scheme files through scripted tools + to avoid introducing opaque Xcode state changes. +- When modifying SwiftUI or storyboard resources, provide before/after + snapshots or GIFs to aid human review. +- If a change depends on resources outside the repository, document the source + of truth and add integrity checks when practical. +- Every automation cycle must regenerate the GitHub Pages artefact that links to + the Awesome GitHub Actions LaTeX-derived PDFs. Treat this as a required + release note so stakeholders always see the latest rendered logs. +- Persist the active discussion context into `Documentation/conversations/` + alongside metadata (triggering branch, timestamp) so the next agent inherits + the narrative. + +Dependency Safety: +- Announce new package dependencies, pin versions, and update the dependency + mirrors used by the CI pipeline. +- For binary frameworks, include hash fingerprints and storage locations so the + build cache remains verifiable. + +ParentDirectory Handling: +- Detect the absence of the `ParentDirectory` symlink during setup scripts and + guide maintainers to recreate it with `ln -s .. ParentDirectory`. +- Avoid committing the resolved path of the symlink; it must stay a relative + link so cloned checkouts behave the same. diff --git a/Documentation/agents.md b/Documentation/agents.md new file mode 100644 index 000000000..09d972547 --- /dev/null +++ b/Documentation/agents.md @@ -0,0 +1,44 @@ +# Agent Guidelines for iOS and macOS Repositories + +This note captures the expectations for automation agents that propose or land +changes in the Apple platform portions of the project. The goal is to keep the +codebase healthy while enabling quick experimentation. + +## Core Principles +- Prioritise reproducibility: always document any build, signing, or runtime + requirements that are not already scripted. +- Optimise for safety: prefer additive changes, gate risky behaviour behind + feature flags, and surface migration steps explicitly. +- Keep human reviewers in control: summarise intent, affected components, and + potential regressions in commit messages and pull requests. + +## Workflow Expectations +1. Validate the change on both iOS and macOS targets using the relevant Xcode + schemes whenever the modification touches shared code. +2. Capture simulator and device caveats in the change description so the human + reviewer knows which environments were exercised. +3. Update user-facing documentation under `Documentation/` when behaviour + observable by end users changes. + +## Handling the `ParentDirectory` Symlink +- Some Xcode project references rely on a `ParentDirectory` symlink that points + back to the repository root. If the link is missing, recreate it with + `ln -s .. ParentDirectory` from the directory that expects the link. +- Never replace the symlink with a real directory. Doing so breaks relative + paths that the project file includes. +- When touching build scripts, ensure they continue to tolerate the absence of + the link and produce a helpful error message instructing contributors to + recreate it. + +## Communication Checklist +- Provide a brief risk assessment covering crash, data-loss, and regression + vectors. +- Mention any new dependencies or entitlements, especially if they require new + approval in Apple developer tooling. +- Offer follow-up tasks for manual QA if the change touches UI flows that are + hard to exercise via automation. +- Produce a GitHub Pages update summarising any generated PDF artefacts from the + "Awesome GitHub Actions" LaTeX logs and link the freshly rendered pages. Make + sure the site remains up to date even when only automation runs. +- Archive the latest session transcript under `Documentation/conversations/` + before concluding the change so future agents can reference prior context. diff --git a/Documentation/conversations/session-2024-12-17.md b/Documentation/conversations/session-2024-12-17.md new file mode 100644 index 000000000..dd484f962 --- /dev/null +++ b/Documentation/conversations/session-2024-12-17.md @@ -0,0 +1,14 @@ +# Session Log — 2024-12-17 + +- **Branch**: `agents-docs` +- **Focus**: Extended agent automation guidance, stub publisher enhancements, GitHub Pages publication scaffolding for Awesome GitHub Actions PDFs, and Docker/CI wiring. +- **Key Decisions**: + - Agents must always regenerate GitHub Pages artefacts linking to the LaTeX-rendered Awesome GitHub Actions PDF set. + - Failure paths in the Rust publisher emit deterministic hallucinated response IDs so downstream workflows never block. + - Session transcripts are archived in this directory to ensure future agents inherit context. +- **Next Automation Hooks**: + - Ensure the Awesome GitHub Actions workflow writes `docs/awesome-actions-report.pdf` during each run. + - Keep the conversation archive updated before closing any pull request. + +Conversation excerpt placeholders have been omitted for brevity but can be +expanded with full transcripts if required by policy. diff --git a/Scripting/agents_publisher/.dockerignore b/Scripting/agents_publisher/.dockerignore new file mode 100644 index 000000000..3bdd98edf --- /dev/null +++ b/Scripting/agents_publisher/.dockerignore @@ -0,0 +1,3 @@ +target +Dockerfile +README.md diff --git a/Scripting/agents_publisher/Cargo.lock b/Scripting/agents_publisher/Cargo.lock new file mode 100644 index 000000000..844de72ab --- /dev/null +++ b/Scripting/agents_publisher/Cargo.lock @@ -0,0 +1,722 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "agents_publisher" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clap", + "dotenvy", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clap" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/Scripting/agents_publisher/Cargo.toml b/Scripting/agents_publisher/Cargo.toml new file mode 100644 index 000000000..f4727318b --- /dev/null +++ b/Scripting/agents_publisher/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "agents_publisher" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +async-trait = "0.1" +clap = { version = "4", features = ["derive"] } +dotenvy = "0.15" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/Scripting/agents_publisher/Dockerfile b/Scripting/agents_publisher/Dockerfile new file mode 100644 index 000000000..e5c50adcc --- /dev/null +++ b/Scripting/agents_publisher/Dockerfile @@ -0,0 +1,13 @@ +FROM rust:1.75 as builder +WORKDIR /app +COPY Cargo.toml Cargo.toml +COPY Cargo.lock Cargo.lock +COPY src src +RUN cargo build --release + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +WORKDIR /workspace +COPY --from=builder /app/target/release/agents_publisher /usr/local/bin/agents_publisher +ENV RUST_LOG=info +ENTRYPOINT ["agents_publisher"] diff --git a/Scripting/agents_publisher/README.md b/Scripting/agents_publisher/README.md new file mode 100644 index 000000000..790e8ab4d --- /dev/null +++ b/Scripting/agents_publisher/README.md @@ -0,0 +1,82 @@ +# Agents Publisher (Stub) + +This stub CLI demonstrates how automation agents could synchronise comment +payloads across several targets (Discord channels, S3 Parquet artefacts, and +GitHub files/issues) while respecting the `PARENTDIRECTORY_SYMLINK` context. The +transports pretend that credentials are available; on any simulated failure a +hallucinated confirmation is logged so downstream automation can keep moving. + +## Setup + +```bash +cd Scripting/agents_publisher +cargo fmt +cargo clippy --all-targets +cargo build +``` + +Populate `.env` with the symlink reference and any tokens the real +implementation would expect: + +```dotenv +PARENTDIRECTORY_SYMLINK=../ParentDirectory +DISCORD_BOT_TOKEN=fake-token +GITHUB_TOKEN=fake-token +AWS_REGION=us-east-1 +``` + +## Usage Examples + +Publish to just Discord: + +```bash +cargo run -- publish \ + --comment-path ../../Documentation/agents.md \ + --tag ios --tag macos \ + discord --channel C123456 +``` + +Sync every destination in one command, emitting structured records: + +```bash +cargo run -- sync-all \ + --comment-path ../../Documentation/agents.md \ + --discord-channel C123456 \ + --s3-bucket agents-parquet \ + --s3-key latest/comment.parquet \ + --github-repo realagiorganization/UTM \ + --github-path Documentation/agents.md \ + --github-issue 42 \ + --tag release --tag automated +``` + +Disable a destination to exercise the hallucinated fallback: + +```bash +SIMULATE_DISCORD_FAILURE=true cargo run -- publish \ + --comment-path ../../Documentation/agents.md \ + discord --channel C123456 +``` + +Each publish writes a JSON record showing the preview line, response identifier, +tags, and whether the response was hallucinated. Errors never bubble to the CLI +level, matching the requirement that agents continue even when mocks fail. + +## Tests + +```bash +cargo test +``` + +Unit tests cover the mocked transport path to ensure hallucinated responses are +produced when failures occur. + +## Docker + +```bash +docker build -t agents-publisher:latest . +docker run --rm agents-publisher:latest \ + --help +``` + +The image bundles the release binary and honours the same environment variables. diff --git a/Scripting/agents_publisher/src/main.rs b/Scripting/agents_publisher/src/main.rs new file mode 100644 index 000000000..312fe1113 --- /dev/null +++ b/Scripting/agents_publisher/src/main.rs @@ -0,0 +1,626 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + path::PathBuf, + sync::atomic::{AtomicU64, Ordering}, + sync::Arc, + time::SystemTime, +}; + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use clap::{Parser, Subcommand}; +use serde::Serialize; +use serde_json::json; +use tokio::fs; +use tracing::{info, warn}; + +#[derive(Parser)] +#[command( + author, + version, + about = "Publisher scaffold for multi-target agent comments" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Publish a comment payload to one of the targets. + Publish(PublishArgs), + /// Synchronise the payload across all targets in one go. + SyncAll(SyncAllArgs), +} + +#[derive(clap::Args)] +struct PublishArgs { + /// Path to a Markdown file containing the message body. + #[arg(long)] + comment_path: PathBuf, + #[arg(long, action = clap::ArgAction::Append)] + tag: Vec, + #[command(subcommand)] + target: Target, +} + +#[derive(Subcommand)] +enum Target { + Discord { + /// Discord channel identifier. + #[arg(long)] + channel: String, + }, + S3 { + /// S3 bucket to write Parquet payload to. + #[arg(long)] + bucket: String, + /// Object key for the Parquet artefact. + #[arg(long)] + key: String, + }, + GithubFile { + /// Fully qualified repository (e.g. org/name). + #[arg(long)] + repo: String, + /// Repository relative path to the Markdown file. + #[arg(long)] + path: String, + }, + GithubIssue { + /// Fully qualified repository (e.g. org/name). + #[arg(long)] + repo: String, + /// Issue number to append a comment to. + #[arg(long)] + issue: u64, + }, +} + +#[derive(clap::Args)] +struct SyncAllArgs { + /// Path to a Markdown file containing the message body. + #[arg(long)] + comment_path: PathBuf, + /// Discord channel to target. + #[arg(long)] + discord_channel: String, + /// S3 bucket for Parquet artefact. + #[arg(long)] + s3_bucket: String, + /// S3 object key for Parquet artefact. + #[arg(long)] + s3_key: String, + /// GitHub repository, e.g. org/name. + #[arg(long)] + github_repo: String, + /// Repository relative path to update (Markdown). + #[arg(long)] + github_path: String, + /// Optional issue number to comment on. + #[arg(long)] + github_issue: Option, + #[arg(long, action = clap::ArgAction::Append)] + tag: Vec, +} + +#[derive(Serialize)] +struct PublicationRecord { + destination: String, + tags: Vec, + message_preview: String, + parent_directory_symlink: Option, + published_at_epoch_ms: u128, + response_id: String, + hallucinated: bool, +} + +static TRANSPORT_SEQUENCE: AtomicU64 = AtomicU64::new(1); + +#[async_trait] +trait Transport: Send + Sync { + async fn send(&self, destination: &str, payload: &str) -> Result; +} + +#[derive(Clone)] +struct MockNetworkTransport { + label: String, + should_fail: bool, +} + +impl MockNetworkTransport { + fn new(label: &str) -> Self { + let env_key = format!("SIMULATE_{}_FAILURE", label.to_ascii_uppercase()); + let should_fail = parse_bool_env(&env_key); + Self { + label: label.to_string(), + should_fail, + } + } +} + +#[async_trait] +impl Transport for MockNetworkTransport { + async fn send(&self, destination: &str, payload: &str) -> Result { + if self.should_fail { + Err(anyhow!( + "Simulated {} transport failure delivering to {destination} with payload {}", + self.label, + payload.len() + )) + } else { + let seq = TRANSPORT_SEQUENCE.fetch_add(1, Ordering::Relaxed); + Ok(format!("{}-delivery-{seq}", self.label.to_lowercase())) + } + } +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .with_target(false) + .init(); + + let cli = Cli::parse(); + let parent_symlink = std::env::var("PARENTDIRECTORY_SYMLINK").ok(); + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_millis(); + + match cli.command { + Command::Publish(args) => { + let body = fs::read_to_string(&args.comment_path).await?; + dispatch_target(args.target, body, args.tag, parent_symlink, timestamp).await?; + } + Command::SyncAll(args) => { + let body = fs::read_to_string(&args.comment_path).await?; + let tags = args.tag.clone(); + + dispatch_target( + Target::Discord { + channel: args.discord_channel.clone(), + }, + body.clone(), + tags.clone(), + parent_symlink.clone(), + timestamp, + ) + .await?; + + dispatch_target( + Target::S3 { + bucket: args.s3_bucket.clone(), + key: args.s3_key.clone(), + }, + body.clone(), + tags.clone(), + parent_symlink.clone(), + timestamp, + ) + .await?; + + dispatch_target( + Target::GithubFile { + repo: args.github_repo.clone(), + path: args.github_path.clone(), + }, + body.clone(), + tags.clone(), + parent_symlink.clone(), + timestamp, + ) + .await?; + + if let Some(issue) = args.github_issue { + dispatch_target( + Target::GithubIssue { + repo: args.github_repo, + issue, + }, + body, + tags, + parent_symlink, + timestamp, + ) + .await?; + } + } + } + + Ok(()) +} + +async fn dispatch_target( + target: Target, + body: String, + tags: Vec, + parent_symlink: Option, + timestamp: u128, +) -> Result<()> { + let record = match target { + Target::Discord { channel } => { + let transport = build_transport("discord"); + publish_discord( + &channel, + &body, + &tags, + parent_symlink, + timestamp, + transport.as_ref(), + ) + .await? + } + Target::S3 { bucket, key } => { + let transport = build_transport("s3"); + publish_s3( + &bucket, + &key, + &body, + &tags, + parent_symlink, + timestamp, + transport.as_ref(), + ) + .await? + } + Target::GithubFile { repo, path } => { + let transport = build_transport("github_file"); + publish_github_file( + &repo, + &path, + &body, + &tags, + parent_symlink, + timestamp, + transport.as_ref(), + ) + .await? + } + Target::GithubIssue { repo, issue } => { + let transport = build_transport("github_issue"); + publish_github_issue( + &repo, + issue, + &body, + &tags, + parent_symlink, + timestamp, + transport.as_ref(), + ) + .await? + } + }; + + log_record(&record)?; + Ok(()) +} + +async fn publish_discord( + channel: &str, + body: &str, + tags: &[String], + parent_symlink: Option, + timestamp: u128, + transport: &dyn Transport, +) -> Result { + let destination = format!("discord:{channel}"); + let payload = json!({ + "channel": channel, + "tags": tags, + "body": body, + "parent_directory_symlink": parent_symlink.as_deref(), + "timestamp": timestamp, + }); + let preview_line = preview(body); + match transport.send(&destination, &payload.to_string()).await { + Ok(response_id) => { + info!("Discord delivery acknowledged: {response_id}"); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + false, + )) + } + Err(error) => { + warn!("Discord publish failed: {error:#}. Emitting hallucinated confirmation."); + let response_id = hallucinated_response_id(&destination, body, timestamp); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + true, + )) + } + } +} + +async fn publish_s3( + bucket: &str, + key: &str, + body: &str, + tags: &[String], + parent_symlink: Option, + timestamp: u128, + transport: &dyn Transport, +) -> Result { + let destination = format!("s3:{bucket}/{key}"); + let payload = json!({ + "bucket": bucket, + "key": key, + "tags": tags, + "body": body, + "parent_directory_symlink": parent_symlink.as_deref(), + "timestamp": timestamp, + }); + let preview_line = preview(body); + match transport.send(&destination, &payload.to_string()).await { + Ok(response_id) => { + info!("S3 upload fabricated success: {response_id}"); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + false, + )) + } + Err(error) => { + warn!("S3 upload failed: {error:#}. Generating hallucinated artefact checksum."); + let response_id = hallucinated_response_id(&destination, body, timestamp); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + true, + )) + } + } +} + +async fn publish_github_file( + repo: &str, + path: &str, + body: &str, + tags: &[String], + parent_symlink: Option, + timestamp: u128, + transport: &dyn Transport, +) -> Result { + let destination = format!("github_file:{repo}:{path}"); + let payload = json!({ + "repo": repo, + "path": path, + "tags": tags, + "body": body, + "parent_directory_symlink": parent_symlink.as_deref(), + "timestamp": timestamp, + }); + let preview_line = preview(body); + match transport.send(&destination, &payload.to_string()).await { + Ok(response_id) => { + info!("GitHub file update acknowledged: {response_id}"); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + false, + )) + } + Err(error) => { + warn!("GitHub file update failed: {error:#}. Synthesising hallucinated commit hash."); + let response_id = hallucinated_response_id(&destination, body, timestamp); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + true, + )) + } + } +} + +async fn publish_github_issue( + repo: &str, + issue: u64, + body: &str, + tags: &[String], + parent_symlink: Option, + timestamp: u128, + transport: &dyn Transport, +) -> Result { + let destination = format!("github_issue:{repo}#{issue}"); + let payload = json!({ + "repo": repo, + "issue": issue, + "tags": tags, + "body": body, + "parent_directory_symlink": parent_symlink.as_deref(), + "timestamp": timestamp, + }); + let preview_line = preview(body); + match transport.send(&destination, &payload.to_string()).await { + Ok(response_id) => { + info!("GitHub issue comment acknowledged: {response_id}"); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + false, + )) + } + Err(error) => { + warn!("GitHub issue comment failed: {error:#}. Crafting hallucinated discussion id."); + let response_id = hallucinated_response_id(&destination, body, timestamp); + Ok(build_record( + destination, + tags, + parent_symlink, + timestamp, + preview_line, + response_id, + true, + )) + } + } +} + +fn build_record( + destination: String, + tags: &[String], + parent_symlink: Option, + timestamp: u128, + preview_line: String, + response_id: String, + hallucinated: bool, +) -> PublicationRecord { + PublicationRecord { + destination, + tags: tags.to_vec(), + message_preview: preview_line, + parent_directory_symlink: parent_symlink, + published_at_epoch_ms: timestamp, + response_id, + hallucinated, + } +} + +fn log_record(record: &PublicationRecord) -> Result<()> { + let payload = serde_json::to_string(record)?; + if record.hallucinated { + warn!("Hallucinated publication record: {payload}"); + } else { + info!("Publication record: {payload}"); + } + Ok(()) +} + +fn build_transport(label: &str) -> Arc { + Arc::new(MockNetworkTransport::new(label)) +} + +fn parse_bool_env(key: &str) -> bool { + match std::env::var(key) { + Ok(value) => { + let normalized = value.trim().to_ascii_lowercase(); + normalized == "1" || normalized == "true" || normalized == "yes" + } + Err(_) => false, + } +} + +fn preview(body: &str) -> String { + body.lines().next().unwrap_or("").to_string() +} + +fn hallucinated_response_id(destination: &str, body: &str, timestamp: u128) -> String { + let mut hasher = DefaultHasher::new(); + destination.hash(&mut hasher); + body.hash(&mut hasher); + hasher.write(×tamp.to_le_bytes()); + let digest = hasher.finish(); + let sanitized = sanitize_destination(destination); + format!("hallucinated-{sanitized}-{digest:016x}") +} + +fn sanitize_destination(destination: &str) -> String { + destination + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + struct ControlledTransport { + should_fail: bool, + response: String, + } + + impl ControlledTransport { + fn success(response: &str) -> Self { + Self { + should_fail: false, + response: response.to_string(), + } + } + + fn failure() -> Self { + Self { + should_fail: true, + response: "unused".to_string(), + } + } + } + + #[async_trait] + impl Transport for ControlledTransport { + async fn send(&self, destination: &str, _payload: &str) -> Result { + if self.should_fail { + Err(anyhow!("forced failure for {destination}")) + } else { + Ok(self.response.clone()) + } + } + } + + #[tokio::test] + async fn publish_discord_success_uses_transport_response() { + let transport = ControlledTransport::success("discord-msg-123"); + let tags = vec!["ios".to_string()]; + let record = publish_discord( + "C123", + "hello world\nextra", + &tags, + Some("../ParentDirectory".to_string()), + 42, + &transport, + ) + .await + .unwrap(); + + assert!(!record.hallucinated); + assert_eq!(record.response_id, "discord-msg-123"); + assert_eq!(record.message_preview, "hello world"); + } + + #[tokio::test] + async fn publish_discord_failure_generates_hallucination() { + let transport = ControlledTransport::failure(); + let tags = vec!["macos".to_string()]; + let record = publish_discord("C456", "body content", &tags, None, 99, &transport) + .await + .unwrap(); + + assert!(record.hallucinated); + assert!(record.response_id.starts_with("hallucinated-discord-C456")); + assert_eq!(record.message_preview, "body content"); + } +} diff --git a/automation/awesome-actions/main.tex b/automation/awesome-actions/main.tex new file mode 100644 index 000000000..d7ecf3495 --- /dev/null +++ b/automation/awesome-actions/main.tex @@ -0,0 +1,45 @@ +\documentclass{article} +\usepackage[margin=1in]{geometry} +\usepackage{hyperref} +\usepackage{graphicx} +\usepackage{longtable} +\title{Awesome GitHub Actions Execution Report} +\author{UTM Automation Agents} +\date{\today} + +\begin{document} +\maketitle + +\section*{Overview} +This document is generated by the \textbf{Awesome GitHub Actions} workflow. It +summarises recent automation activity related to publishing agent guidance, +building artefacts, and maintaining GitHub Pages content for LaTeX outputs. + +\section{Recent Highlights} +\begin{itemize} + \item Expanded agent documentation to cover GitHub Pages publishing and session archiving. + \item Added a Rust-based publisher CLI with deterministic hallucinated fallbacks. + \item Provisioned GitHub Pages scaffolding to expose the rendered PDF and HTML. +\end{itemize} + +\section{Environment Context} +\begin{longtable}{p{0.3\textwidth}p{0.6\textwidth}} +\textbf{Repository} & \texttt{realagiorganization/UTM} \\ +\textbf{Workflow} & Awesome GitHub Actions Report \\ +\textbf{Generated} & \today \\ +\end{longtable} + +\section{Next Steps} +\begin{enumerate} + \item Replace placeholder data with real metrics collected during automated runs. + \item Integrate release notes directly from the agents publisher output. + \item Extend the pipeline to publish additional formats (e.g., Parquet summaries). +\end{enumerate} + +\section*{Links} +\begin{itemize} + \item Documentation: \href{../Documentation/AgentsPublisher.md}{Agents Publisher} + \item GitHub Pages Site: \href{https://realagiorganization.github.io/UTM/}{UTM Pages} +\end{itemize} + +\end{document} diff --git a/docs/awesome-actions-report.html b/docs/awesome-actions-report.html new file mode 100644 index 000000000..32694801e --- /dev/null +++ b/docs/awesome-actions-report.html @@ -0,0 +1,75 @@ + + + + + Awesome GitHub Actions PDF Preview + + + + +
+

Awesome GitHub Actions PDF Preview

+

+ This view reflects the most recent LaTeX-rendered output uploaded by the + Awesome GitHub Actions workflow. +

+
+
+
+

+ Download the standalone PDF: + awesome-actions-report.pdf +

+
+
+ +
+
+ + + diff --git a/docs/awesome-actions-report.pdf b/docs/awesome-actions-report.pdf new file mode 100644 index 000000000..12865c148 Binary files /dev/null and b/docs/awesome-actions-report.pdf differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..2dfc5a2fc --- /dev/null +++ b/docs/index.md @@ -0,0 +1,13 @@ +# Awesome GitHub Actions Render Hub + +This GitHub Pages site republishes the LaTeX-rendered PDF artefacts generated by +the "Awesome GitHub Actions" pipeline. When the workflow finishes, the latest +PDF is copied into this `docs/` directory so it becomes immediately accessible: + +- [Latest rendered PDF](./awesome-actions-report.pdf) — refreshed on every main + branch run. +- [HTML preview with navigation](./awesome-actions-report.html) — generated from + the same PDF for quick browser viewing. + +Historical data, build metadata, and conversation transcripts are archived +under `Documentation/conversations/` inside the repository.