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
15 changes: 11 additions & 4 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,17 @@ file content, not substring `LIKE`).
### `repository` nested fields (all resolved on the `RepositoryObject` type)

`ownerLogin`, `issues`, `pullRequests`, `workflowRuns`, `packages`,
`secretNames`, `branches`, `tree(ref, path)`, `wikiPages`, `wikiPage(page)`,
`commits(ref, limit)`, `activity(limit)`, `collaborators`, `labels`,
`milestones`, `projects` (with nested `columns` → `cards` → `issue`),
`branchProtectionRules`, `webhooks`.
`secretNames`, `branches`, `tree(ref, path)`, `fileContent(ref, path)` (a
single file's content — `content` as UTF-8 text plus `contentBase64` always,
`isBinary`/`size`; returns `null` for a missing path/ref or a file over
10MiB), `wikiPages`, `wikiPage(page)`, `commits(ref, limit)`,
`activity(limit)`, `collaborators`, `labels`, `milestones`, `projects` (with
nested `columns` → `cards` → `issue`), `branchProtectionRules`, `webhooks`.

### `issue` nested fields (all resolved on the `IssueObject` type)

`labels`, `milestone`, `comments` (posted via `commentOnIssue`, oldest
first).

### Mutations, grouped

Expand Down
4 changes: 4 additions & 0 deletions JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ Alternative Rust à GitHub/Forgejo/GitLab/Coder. Objectif 22h00.
5. **Dev-workspace hosting par le runner standalone**: le `kind = 'dev_workspace_action'` était réservé mais mort — maintenant câblé bout en bout: `createDevWorkspace(..., onRunner: true)` pousse un `runner_jobs` (nouvelle colonne `result` pour faire remonter `container_id`/output, migration `10_dev_workspace_runner.sql`), le runner le récupère et l'exécute contre **son propre** Docker (nouveau `crates/runner/src/dev_workspace_poll.rs`, plus plausible stub), rapporte le résultat, la mutation poll la DB (pas de pub/sub, juste polling 300ms/20s timeout) et renvoie l'objet à jour. `delete`/`exec` suivent le même chemin pour un workspace déjà hébergé par un runner. Honnêteté: `start`/`stop` et surtout le **reverse-proxy HTTP live** vers un workspace hébergé sur un runner restent non supportés (le conteneur est sur un Docker daemon que `server` n'a aucun moyen réseau d'atteindre — ça demanderait un vrai tunnel inverse runner→server, hors scope ici) — documenté comme lacune connue plutôt que de faire semblant que ça marche.
6. **Alternative safe au DinD**: en creusant, `server`/`runner` ne faisaient déjà **pas** de vrai Docker-in-Docker (pas de `dockerd` imbriqué) — juste un montage du socket Docker de l'**hôte** ("Docker outside of Docker"), ce qui est en pratique un risque équivalent (accès root-like à l'hôte via `-v /:/host`). `DOCKER_SOCKET_PATH` existait déjà en config mais n'était jamais branché nulle part (juste loggé) — maintenant réellement câblé (`Executor::new_with_socket`, `WorkspaceManager::connect_socket`, via `bollard::Docker::connect_with_socket`). Recherche web: **Podman rootless** (`systemctl --user enable --now podman.socket`, API compatible Docker, isolation par user-namespace, zéro changement de code requis côté Genome) recommandé en premier, **Sysbox** (`nestybox/sysbox`) documenté pour le cas où un workflow a réellement besoin de nested Docker (ex: un step qui fait `docker build`). Tout documenté dans `DOC.md` §2a, rien d'actif par défaut (il faut un socket disponible pour un `docker compose up` zéro-config) mais le knob marche vraiment maintenant.
- Tests ajoutés: 6 nouveaux tests unitaires `marketplace` (parsing `ActionRef`, résolution d'inputs, substitution), 2 nouveaux tests `git-core` (rename repo+wiki avec rollback/collision, idempotence du backfill hook). `cargo test --workspace`: seuls les 6 tests d'intégration qui construisent un `actions::Executor`/`dev_env::WorkspaceManager` échouent, et uniquement à cause de l'absence de `/var/run/docker.sock` **dans ce bac à sable de session** (pas de démon Docker démarrable, `systemd` absent) — confirmé non lié à mes changements (ces tests appellent le même `Executor::new()`/`connect_local()` qu'avant, code non modifié sur ce chemin). Pushé (ec8ec2e).
- **(reprise de session, 21 juillet)** — Audit ciblé de l'API GraphQL demandé par l'utilisateur ("flag les vrais gaps, n'en invente pas"): confirmé par lecture de code (pas de supposition) que deux capacités manquaient réellement, malgré le socle bas-niveau déjà présent:
1. **Lecture de contenu de fichier**: `RepoManager::read_file_at_ref` existait dans `git-core` depuis la vague FTS5/marketplace mais n'était jamais appelé que côté serveur (lecture interne du YAML de workflow pour `triggerWorkflowDispatch`) — aucune query GraphQL ne l'exposait. `Repository.tree(ref, path)` ne renvoie que des métadonnées (nom/chemin/taille/oid), jamais les octets. Ajouté `Repository.fileContent(ref, path)`: nouveau type `FileContentObject` (`content` UTF-8 si le fichier n'est pas binaire, `contentBase64` toujours présent, `isBinary`, `size`), `null` si le path/ref ne résout à rien ou si le fichier dépasse 10MiB (garde-fou coût, pas de troncature silencieuse).
2. **Lecture des commentaires d'issue**: `commentOnIssue` insérait dans `issue_comments` et renvoyait le commentaire tout juste créé, mais rien ne relisait cette table ensuite — `IssueObject` n'exposait que `labels`/`milestone`. Ajouté `IssueObject.comments` (tri `created_at ASC`), même pattern que `labels` (query_as sur `entity::issue_comment::Model`, déjà juste un dérivé `Deserialize`, pas besoin d'un nouvel impl `From<&mut Row>`).
- `rustc` remis à jour 1.94→1.97 (hiqlite 0.14 l'exige, déjà rencontré lors du chantier précédent). `cargo check/test --workspace` et `DOC.md` (§4, nouvelles sections nested-fields `repository`/`issue`) mis à jour en conséquence.
60 changes: 60 additions & 0 deletions crates/graphql-api/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use async_graphql::{ComplexObject, Context, SimpleObject};
use base64::Engine;
use chrono::{DateTime, Utc};
use hiqlite::params;
use uuid::Uuid;
Expand Down Expand Up @@ -191,6 +192,40 @@ impl RepositoryObject {
Ok(entries.into_iter().map(TreeEntryObject::from).collect())
}

/// Reads a single file's content at a given ref. Returns `null` if the
/// ref/path doesn't resolve to a regular file. Caps at 10MiB to avoid an
/// API client pulling an oversized blob into a single GraphQL response;
/// larger files are reported as `null` rather than truncated.
async fn file_content(
&self,
ctx: &Context<'_>,
#[graphql(name = "ref", default = "\"HEAD\".to_string()")] r#ref: String,
path: String,
) -> async_graphql::Result<Option<FileContentObject>> {
const MAX_FILE_BYTES: usize = 10 * 1024 * 1024;
let app = ctx.data::<AppContext>()?;
let bytes = match app
.repo_manager
.read_file_at_ref(&self.owner_login, &self.name, &r#ref, &path)
{
Ok(bytes) if bytes.len() <= MAX_FILE_BYTES => bytes,
_ => return Ok(None),
};
let is_binary = bytes.contains(&0u8);
let content = if is_binary {
None
} else {
String::from_utf8(bytes.clone()).ok()
};
Ok(Some(FileContentObject {
path,
size: bytes.len() as i64,
is_binary,
content,
content_base64: base64::engine::general_purpose::STANDARD.encode(&bytes),
}))
}

async fn wiki_pages(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<String>> {
let app = ctx.data::<AppContext>()?;
let pages = app
Expand Down Expand Up @@ -407,6 +442,19 @@ impl IssueObject {
.next();
Ok(milestone.map(MilestoneObject::from))
}

/// Comments posted on this issue via `commentOnIssue`, oldest first.
async fn comments(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<IssueCommentObject>> {
let app = ctx.data::<AppContext>()?;
let comments = app
.db
.query_as::<entity::issue_comment::Model, _>(
"SELECT * FROM issue_comments WHERE issue_id = ?1 ORDER BY created_at ASC",
params!(self.id.to_string()),
)
.await?;
Ok(comments.into_iter().map(IssueCommentObject::from).collect())
}
}

#[derive(SimpleObject, Clone)]
Expand Down Expand Up @@ -633,6 +681,18 @@ impl From<git_core::TreeEntry> for TreeEntryObject {
}
}

/// A single file's content read at a ref, returned by `Repository.fileContent`.
#[derive(SimpleObject, Clone)]
pub struct FileContentObject {
pub path: String,
pub size: i64,
pub is_binary: bool,
/// UTF-8 text content, or `null` if the file is binary / not valid UTF-8.
pub content: Option<String>,
/// Base64-encoded raw bytes, always present regardless of `isBinary`.
pub content_base64: String,
}

#[derive(SimpleObject, Clone)]
pub struct AuthPayload {
pub token: String,
Expand Down
Loading