diff --git a/elixir/Makefile b/elixir/Makefile index 61c40270a..967bf3770 100644 --- a/elixir/Makefile +++ b/elixir/Makefile @@ -1,9 +1,9 @@ -.PHONY: help all setup deps build fmt fmt-check lint test coverage ci dialyzer e2e +.PHONY: help all setup deps build fmt fmt-check lint test coverage ci dialyzer e2e launch-app github-pr-media MIX ?= mix help: - @echo "Targets: setup, deps, fmt, fmt-check, lint, test, coverage, dialyzer, e2e, ci" + @echo "Targets: setup, deps, fmt, fmt-check, lint, test, coverage, dialyzer, e2e, launch-app, github-pr-media, ci" setup: $(MIX) setup @@ -36,6 +36,12 @@ dialyzer: e2e: SYMPHONY_RUN_LIVE_E2E=1 $(MIX) test test/symphony_elixir/live_e2e_test.exs +launch-app: + ./scripts/launch-app + +github-pr-media: + ./scripts/github-pr-media + ci: $(MAKE) setup $(MAKE) build diff --git a/elixir/README.md b/elixir/README.md index 603b4bb00..052b0fbb1 100644 --- a/elixir/README.md +++ b/elixir/README.md @@ -65,6 +65,19 @@ mise exec -- mix build mise exec -- ./bin/symphony ./WORKFLOW.md ``` +### Runtime validation helpers (for unattended app tickets) + +```bash +# Boot app and verify it responds on localhost +make launch-app + +# Bundle runtime artifacts + manifest for PR handoff +make github-pr-media +``` + +Equivalent script entrypoints are available at `./scripts/launch-app` and +`./scripts/github-pr-media`. + ## Configuration Pass a custom workflow file path to `./bin/symphony` when starting the service: diff --git a/elixir/WORKFLOW.md b/elixir/WORKFLOW.md index d102b62fe..082c3f765 100644 --- a/elixir/WORKFLOW.md +++ b/elixir/WORKFLOW.md @@ -212,7 +212,7 @@ Use this only when completion is blocked by missing required tools or missing au - You may make temporary local proof edits to validate assumptions (for example: tweak a local build input for `make`, or hardcode a UI account / response path) when this increases confidence. - Revert every temporary proof edit before commit/push. - Document these temporary proof steps and outcomes in the workpad `Validation`/`Notes` sections so reviewers can follow the evidence. - - If app-touching, run `launch-app` validation and capture/upload media via `github-pr-media` before handoff. + - If app-touching, run runtime validation via `make -C elixir launch-app` (or `elixir/scripts/launch-app`) and collect reviewer media via `make -C elixir github-pr-media` (or `elixir/scripts/github-pr-media`) before handoff. 6. Re-check all acceptance criteria and close any gaps. 7. Before every `git push` attempt, run the required validation for your scope and confirm it passes; if it fails, address issues and rerun until green, then commit and push changes. 8. Attach PR URL to the issue (prefer attachment; use the workpad comment only if attachment is unavailable). @@ -267,7 +267,7 @@ Use this only when completion is blocked by missing required tools or missing au - PR feedback sweep is complete and no actionable comments remain. - PR checks are green, branch is pushed, and PR is linked on the issue. - Required PR metadata is present (`symphony` label). -- If app-touching, runtime validation/media requirements from `App runtime validation (required)` are complete. +- If app-touching, runtime validation/media requirements from `App runtime validation (required)` are complete (`make -C elixir launch-app` + `make -C elixir github-pr-media`). ## Guardrails @@ -289,6 +289,20 @@ Use this only when completion is blocked by missing required tools or missing au - Keep issue text concise, specific, and reviewer-oriented. - If blocked and no workpad exists yet, add one blocker comment describing blocker, impact, and next unblock action. +## App runtime validation (required) + +Use these repo-local commands whenever the change touches app runtime behavior/UI: + +1. Run runtime boot validation: + - `make -C elixir launch-app` + - Equivalent script: `elixir/scripts/launch-app` +2. Package runtime evidence for reviewer handoff: + - `make -C elixir github-pr-media` + - Equivalent script: `elixir/scripts/github-pr-media [artifact-dir]` +3. Attach or reference the generated manifest/archive paths in the workpad + PR handoff. + +These commands are designed to work in unattended Codex sessions without depending on globally installed helper binaries. + ## Workpad template Use this exact structure for the persistent workpad comment and keep it updated in place throughout execution: diff --git a/elixir/scripts/github-pr-media b/elixir/scripts/github-pr-media new file mode 100755 index 000000000..de059c766 --- /dev/null +++ b/elixir/scripts/github-pr-media @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +SOURCE_DIR="${1:-artifacts/runtime}" +OUTPUT_DIR="${OUTPUT_DIR:-artifacts/review}" +mkdir -p "$OUTPUT_DIR" + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +MANIFEST="$OUTPUT_DIR/github-pr-media-${TIMESTAMP}.md" +ARCHIVE="$OUTPUT_DIR/github-pr-media-${TIMESTAMP}.tar.gz" + +if [[ ! -d "$SOURCE_DIR" ]]; then + echo "Source directory '$SOURCE_DIR' does not exist." >&2 + exit 1 +fi + +shopt -s nullglob +FILES=("$SOURCE_DIR"/*) +shopt -u nullglob + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "No media/artifact files found under '$SOURCE_DIR'." >&2 + exit 1 +fi + +tar -czf "$ARCHIVE" -C "$SOURCE_DIR" . + +{ + echo "## Runtime validation artifacts" + echo + echo "Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + printf 'Source dir: `%s`\n' "$SOURCE_DIR" + printf 'Archive: `%s`\n' "$ARCHIVE" + echo + echo "### Files" + for file in "${FILES[@]}"; do + if [[ -f "$file" ]]; then + size=$(wc -c <"$file" | tr -d ' ') + echo "- $(basename "$file") (${size} bytes)" + else + echo "- $(basename "$file") (non-file entry)" + fi + done + echo + echo "### PR handoff" + echo "1. Upload the archive (or selected files) to the PR/comment workflow." + echo "2. Paste this manifest in the PR comment body for reviewer context." +} >"$MANIFEST" + +echo "Created archive: $ARCHIVE" +echo "Created manifest: $MANIFEST" diff --git a/elixir/scripts/launch-app b/elixir/scripts/launch-app new file mode 100755 index 000000000..d3fcd881b --- /dev/null +++ b/elixir/scripts/launch-app @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +PORT="${PORT:-4010}" +STARTUP_TIMEOUT="${STARTUP_TIMEOUT:-45}" +ARTIFACT_DIR="${ARTIFACT_DIR:-artifacts/runtime}" +MIX_CMD="${MIX_CMD:-mix phx.server}" + +mkdir -p "$ARTIFACT_DIR" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +LOG_FILE="$ARTIFACT_DIR/launch-app-${TIMESTAMP}.log" +STATUS_FILE="$ARTIFACT_DIR/launch-app-${TIMESTAMP}.txt" + +cleanup() { + if [[ -n "${SERVER_PID:-}" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then + kill "$SERVER_PID" >/dev/null 2>&1 || true + wait "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "Starting app with: $MIX_CMD (PORT=$PORT)" +PORT="$PORT" $MIX_CMD >"$LOG_FILE" 2>&1 & +SERVER_PID=$! + +READY=0 +for ((i=1; i<=STARTUP_TIMEOUT; i++)); do + if curl -fsS "http://127.0.0.1:${PORT}" >/dev/null 2>&1; then + READY=1 + break + fi + sleep 1 +done + +if [[ "$READY" -eq 1 ]]; then + { + echo "launch-app: PASS" + echo "url: http://127.0.0.1:${PORT}" + echo "server_pid: ${SERVER_PID}" + echo "log: ${LOG_FILE}" + } >"$STATUS_FILE" + echo "App responded successfully." + echo "Status: $STATUS_FILE" +else + { + echo "launch-app: FAIL" + echo "url: http://127.0.0.1:${PORT}" + echo "timeout_seconds: ${STARTUP_TIMEOUT}" + echo "log: ${LOG_FILE}" + } >"$STATUS_FILE" + echo "App did not become ready in ${STARTUP_TIMEOUT}s." >&2 + echo "Status: $STATUS_FILE" >&2 + exit 1 +fi