Viya emulation: a Viya-shaped surface for development and testing
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 20/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- typescript
- Área
- backend, testing-qa
Línea de trabajo
Start by reading the adapter call surface in section 3 and the proposed route table, overlay, service, token, translation, and version modules in section 4. The first deliverable is the contract capture harness and a captured pass for each listed family; inspect the generated table and corpus against the pinned version. Done means the emulator answers the adapter’s in-scope calls through existing services, with captured response shapes, explicit unsupported routes, and the emulation marker.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Viya emulation on sasjs/server: implementation plan
Status: proposal for approval. No implementation code exists yet.
This issue is the durable home for the plan. Decisions land here as they are taken, and the text below is amended in place rather than re-posted.
Date: 2026-11-11
Owner: [email protected] (sasjs profile agent)
Tracking issue to open: sasjs/server, "Viya emulation: a Viya-shaped surface for development and testing"
1. Purpose
This workstream exists to make development and testing of apps that deploy to a real SAS Viya estate possible without an estate: the app keeps serverType=SASVIYA, points serverUrl at an emulator, and its normal calls are answered in Viya's shape with generated data. The emulator runs anywhere the app runs, including entirely inside a browser, so a Viya-targeted app can be built, reviewed, demonstrated and regression-tested with no VPN, no credentials and no server.
It also gives adapter, CLI and app defects a deterministic reproduction that survives the fix, instead of a hand-built stub that is deleted afterwards.
2. What this is not
This is a development and testing aid for apps that ship to real Viya. It is not a Viya replacement, and it cannot become one.
- It executes no SAS Viya engine. Compute sessions, jobs and contexts are emulated, so results are indicative of shape and contract, never of platform behaviour.
- It implements only what the adapter calls. Anything outside that surface answers a 404 from the emulator, which is not evidence about how Viya behaves.
- It carries no platform guarantees: no multi-tenancy, no scheduling, no security model beyond the adapter's own auth flow, no performance characteristics.
- It must never serve production traffic, and no answer it gives is authoritative for a real deployment.
- Every response says so, in an identifying header and in the server's own status output (section 6), precisely so an emulated answer cannot be mistaken for a real one.
Corollaries that follow from that: do not use it as a staging environment for a real deployment, do not treat emulated output as platform truth in a support case, and do not extend it to a route the adapter does not call on the grounds that Viya has one.
3. Scope: the adapter's call surface, and nothing else
The scope of the emulator is defined by the client. @sasjs/adapter builds these calls, and the emulator answers exactly these, in this shape:
auth /SASLogon/oauth/token getAccessTokenForViya.ts:28, refreshTokensForViya.ts:26
/SASLogon/oauth/authorize SASViyaApiClient.ts:761
/SASLogon/login (GET, POST form) AuthManager.ts:175, 189
/SASLogon/logout(.do) AuthManager.ts:29-32
identity /identities/users/@currentUser AuthManager.ts:263
/identities SASViyaApiClient.ts
folders /folders/folders SASViyaApiClient.ts
/folders/folders/@item SASViyaApiClient.ts
/folders/folders/{id} SASViyaApiClient.ts
/folders/folders/{id}/members SASViyaApiClient.ts
/folders/folders/@myRecycleBin SASViyaApiClient.ts
files /files/files SASViyaApiClient.ts
/files/files/{id} SASViyaApiClient.ts
/files/files/{id}/content SASViyaApiClient.ts
compute /compute/contexts SASViyaApiClient.ts
/compute/contexts/{id}/sessions SASViyaApiClient.ts
/compute/sessions/{id} SASViyaApiClient.ts
/compute/sessions/{id}/state SASViyaApiClient.ts
/compute/sessions/{id}/jobs SASViyaApiClient.ts
/compute/sessions/{id}/jobs/{id}/state
/compute/sessions/{id}/filerefs/_webout/content
jobs /jobDefinitions, /jobDefinitions/definitions/{id}
/jobExecution/jobs, /jobExecution/jobs/{id}, /jobExecution/jobs/{id}/state
/SASJobExecution
A scope guard holds this list to the source rather than to memory: a check extracts the path literals the adapter builds and fails when the table and the adapter disagree in either direction. An adapter route the table lacks is a gap; a table entry the adapter never calls is scope the emulator should not carry.
Explicitly out, because no adapter call reaches it: CAS and dataTables, reports and visual analytics, microanalytic score, model repositories, jobDefinitions beyond the read the adapter performs, and the platform's administrative endpoints.
4. Architecture: a second route table, not a second server
The emulator is a second table inside @sasjs/api, over the same services, dispatched by the same dispatcher. Each host then owns only transport.
src/viya/table.ts route descriptors: path, verb, params, status set, target version
src/viya/overlay.ts implemented | declined-with-reason | hostResponses
src/viya/service.ts handlers per family, calling the existing ports
src/viya/tokens.ts token minting and validation for the emulated auth flow
src/viya/translate.ts Viya shape to port shape, the only module that knows both
src/viya/version.ts the pinned target version, and the header it stamps
src/corpus/viya/*.ts cases, replayed by the existing runner
src/host/dispatch.ts one more entry: dispatchViya(table, request, ports)
Family mapping, so no decision is implemented twice:
Viya /folders, /files -> DriveService
Viya /identities, /SASLogon/oauth/* -> IdentityService plus the token port
Viya /compute/contexts|sessions|jobs -> SessionService plus RuntimeService
Viya /jobExecution, /SASJobExecution -> the runtime job path plus AppStreamRegistry
Viya /files/files/{id}/content (_webout) -> the runtime output store
Hosts:
sasjs/server mounted on the same origin behind EMULATE_VIYA, so a Viya app runs unchanged
sasjs/cli mock the same table inside the service worker, so the whole emulator runs in a browser
sasjs/mocks retired; any fixture in it that still describes a current answer moves into the corpus
Rules the implementation follows:
- The table is the contract. A status the table does not state is a build failure at import, not a runtime surprise.
- Every operation is annotated: implemented, or declined with a reason that travels into the 501 body.
- Corpus cases pin shapes and status sets, never minted identifiers or timestamps, because hosts mint differently.
- Handlers answer Viya's surface by calling DriveService, IdentityService and RuntimeService. No rule is implemented twice.
5. Contract capture and version pinning
Viya publishes no machine-readable document for these routes, so the contract is captured from a real estate, run by the operator against an account with the permissions the emulation needs. No estate name, host or credential belongs in this repository or in the package.
Capture harness (Phase 0 deliverable):
- A recorder in front of the adapter, as a proxy or as an interceptor in a scratch build, capturing path, verb, request headers and shape, status, response headers, body and timing per call.
- One scripted pass per family: token, current user, folder item, folder members, file create/read/replace/delete, context list, session create, job submit/poll/log/output, job execution submit/state, app stream publish/serve.
- A generator that turns captures into
src/viya/table.tsandsrc/corpus/viya/*.tswith stable ordering and a reviewable diff.
Version pinning is what turns drift into a report rather than a surprise:
src/viya/version.tsdeclares oneVIYA_TARGET_VERSION. The emulator emulates that version, and only that version, and names it in every response.- Every operation and every corpus case records the version it was captured against, so a case that describes an older shape is visible as such.
- A scheduled drift job, not part of PR CI, re-captures against an estate of the pinned version and reports differences as an issue with the operations, statuses and fields that moved. A drift report never reddens an unrelated pull request.
- The first pinned version is whatever the first estate made available for capture runs. It is recorded with its capture date, so a reader can see how old the contract is. Pinning ahead of what an estate can capture would produce a table nobody can verify.
- Raising
VIYA_TARGET_VERSIONis a deliberate change: table, corpus, drift baseline and header move in one commit, with a fresh capture to justify it.
Where an operation cannot be captured, for permission or sequencing reasons, it is marked recorded: false with the reason, and its confidence is stated as lower. A table without captures is a guess wearing a schema, and the marker keeps that visible.
6. Making emulation unmistakable
Emulated answers must be identifiable by a machine and by a person, because a demo that looks like production is a support risk.
- Every response carries
x-sasjs-emulated: viya@<VIYA_TARGET_VERSION>, including refusals and 404s. A 404 from the emulator is not evidence about Viya. - Where the browser reads the response across an origin, the header is exposed through
Access-Control-Expose-Headers. - The server's status output and the settings documentation state plainly, at startup, that Viya emulation is on, which version it emulates, and that it is not a Viya deployment.
- The CLI's mock build stamps the same header and the same version into the site it generates.
- The CLI and the server both print a one-line warning on start, once, so a captured console log says which mode produced it.
7. Authentication design
The adapter fixes the vocabulary. These paths are string literals in @sasjs/adapter:
getAccessTokenForViya.ts:28 /SASLogon/oauth/token
refreshTokensForViya.ts:26 /SASLogon/oauth/token
SASViyaApiClient.ts:761 /SASLogon/oauth/authorize
AuthManager.ts:18,26,29-32 /SASLogon, /SASLogon/login, /SASLogon/logout(.do)
AuthManager.ts:175,260,370 GET /SASLogon/login?service=..., login.do to login
verifySasViyaLogin.ts:31 location.href.includes('SASLogon')
verifySas9Login.ts:16 location.href.includes('SASLogon')
RequestClient.ts:855 url.includes('/SASLogon/login')
Renaming the emulator's paths is therefore not available: it would require patching the adapter (the app stops being stock) or a configurable auth base path (every app must set it). Both defeat the purpose. The emulator answers /SASLogon/*.
The server's own auth paths stay where they are:
POST /SASLogon/login JSON body, CSRF cookie, 429 lockout, JSON answer (owned)
GET /SASLogon/logout clears the session, re-issues the CSRF cookie (owned)
/SASLogon/authorize server SPA and OIDC flow (owned)
/SASLogon/openid, /openid/callback, /openid/logout (owned, registered externally)
docs/pages/auth.md:60-65 instructs operators to register https://<host>/SASLogon/openid/callback with their identity provider, and loginRedirectUri at :130 is the same path, including in the Cloudron deployment notes. Renaming those paths moves a migration onto every customer. They stay.
The overlap is one method on one path, with a clean split:
/SASLogon/oauth/token uncontested the server serves no such path
/SASLogon/oauth/authorize uncontested
GET /SASLogon/login uncontested the server owns POST only
POST /SASLogon/login.do uncontested the server never serves it
POST /SASLogon/login SHARED split by request content type
GET /SASLogon/logout SHARED split by request shape
For the shared POST the discriminator is what the adapter already implements: AuthManager.sendLoginRequest posts application/json for ServerType.Sasjs and application/x-www-form-urlencoded for Viya and SAS9 to the same path. The server's route keeps its JSON contract; the emulated branch answers form-encoded logins. Each side pins its case, and the route description states the rule.
If a future major wants a namespaced home for SASjs's own auth, the destination is /SASjsApi/auth/login, beside /SASjsApi/auth/token, /refresh and /logout, with /SASLogon/login kept as a documented alias for a release. /SASjsLogon and /logon are rejected: the first duplicates an existing namespace, the second invents a third name for a thing that already has two.
8. Data: generated, seeded, reproducible
Data is generated, never captured. Captured values belong to the estate they came from and would put estate data in a repository.
- A seed comes from the target configuration. The same seed produces the same rows, the same pages and the same screenshots, so a ticket's evidence stays true after a rebuild.
- Structure is always emulated: folders, files, sessions, jobs, identities, tokens, launch pages, app stream entries, backed by the same ports the server uses.
- Results are generated from a program's declared
@inputand@outputtables: column names come from the program, row counts and values from the seed and the table's declared types. - Execution is optional. Where the server runs with
RUN_TIMES=jsorRUN_TIMES=sas, the emulated session runs the program for real against the generated tables and returns the real webout. That widens the spectrum without changing the contract, and the status output states which mode is in force.
9. Failure fixtures: the ticket payoff
Every failure shape that matters becomes a corpus case the adapter must survive, so the case that reproduces a defect also guards its fix:
adapter#895 target unreachable transport timeout at /SASLogon/oauth/token
adapter#837 invalid JSON body is HTML, or truncated JSON, with a log present
adapter#803 debug on log is an object, an array, and a string
cli#1350 failed job, no log job state 'failed' with no log href, parent session has one
Each case names its status set and body shape, and each runs through the real transport of every host that mounts the table.
10. Phases
Phase 0 - capture and pin.
Deliverables: recorder, generator, src/viya/table.ts, src/corpus/viya/*.ts, src/viya/version.ts with the pinned target, and a measurement note giving the operation count per family and the list marked recorded: false.
Exit criteria: every operation carries a captured case or a stated reason; the scope guard compares the table against the adapter's path literals and agrees; the pinned version appears in the table and in the header.
Phase 1 - the package table.
Deliverables: handlers for the families in section 3, the parity spec (table against corpus, both directions, every operation annotated), the 501 path with reasons, and the emulated header on every answer.
Exit criteria: parity spec green and biting when a status is added on one side only; the full corpus replays with no host attached.
Phase 2 - mount in sasjs/server behind EMULATE_VIYA=true.
Deliverables: same-origin mounting, the /SASLogon split from section 7, conformance replay over the server's real HTTP surface (the pattern in api/src/routes/api/spec/driveCorpus.spec.ts), a documentation page, a settings entry, and the startup notice.
Exit criteria: a seed app reconfigured to serverType=SASVIYA authenticates, runs a service, renders webout and serves its app against the server with no Viya present; the corpus replays green over HTTP; the server's own login tests still pass.
Phase 3 - the same table in the CLI, and in a browser.
Deliverables: the Viya table inside the service worker, replacing the hand-written Viya-adjacent pieces, retiring sasjs/mocks routes, a mock-web build of a Viya-targeted app, and the emulated header stamped into the generated site.
Exit criteria: the app runs in a browser with no backend; the same corpus cases pass in the worker; a mock-compiled Viya app is indistinguishable from the server-hosted one apart from the header and the data.
Phase 4 - tooling.
Deliverables: CLI support so an emulated Viya is one command, the seed in the target configuration, and the drift job wired on a schedule.
Exit criteria: a documented seed reproduces a dataset and a rendering across hosts; a drift report names operations rather than dumping a diff.
11. Decisions
Decided:
- Implementation lives in
@sasjs/apias a second route table. One dispatcher, one corpus runner, three hosts. - The server is the first host; the CLI's browser host follows with the same table, and full mock compatibility for the CLI is a completion criterion, not a later idea.
- Scope is the adapter's call surface, enforced by a guard against the adapter's path literals. Routes Viya has but the adapter never calls are out.
- Data is generated from a seed, never captured from an estate.
- Every response carries
x-sasjs-emulated: viya@<VIYA_TARGET_VERSION>, and the runtime states the same in its status output and startup notice. - The contract is pinned to one
VIYA_TARGET_VERSION; captures record the version they came from; drift is reported by a scheduled job, not by PR CI. - Same-origin activation behind
EMULATE_VIYA, because the adapter'sserverUrlmust be the app's own origin for cookies and same-origin behaviour to match. - One interface, two implementations:
RUN_TIMES=sasviyaproxies to a real Viya,EMULATE_VIYAanswers an emulated one, and a target chooses between them. - No path renames.
/SASLogon/*belongs to the emulator because the adapter requires it; the server's own paths stay where customers have registered them; the shared POST splits by content type. MOCK_SERVERTYPEstays until the new mode covers its use;sasjs/mocksretires once nothing in it describes a current answer.- The first pinned version is the version the first estate made available for capture runs. Pinning is a contract, so it is recorded with its capture date and raised deliberately.
Open, to settle during Phase 0:
- The exact header spelling and whether a second header separates version from mode. Default:
x-sasjs-emulated: viya@<version>, one header, one fact. - Whether the seed lives in the target configuration or in a project file. Default: the target, beside
serverUrlandappLoc.
12. Risks
- Fidelity is the cost driver. Viya's shapes are undocumented in places and move between versions. Capture plus the version pin is what keeps the table honest.
- Some operations start without captures, so confidence is not uniform. The
recordedflag states that per operation rather than leaving it implied. - The emulator can drift into a second implementation of the server's own logic. Handlers call the services; translation sits in one module; the parity spec is the ratchet.
- The auth split is one path carrying two protocols, so it needs a case on each side and a sentence in the route description, or the next reader will not know which answer they are looking at.
- Emulated data can be mistaken for real output. The header, the startup notice and the status line are the defence, and they are not optional.
- Scope creep. The guard against the adapter's path literals is the mechanism that keeps the surface at what the client uses.
13. First actions
- Open the tracking issue with sections 1 to 3 as its statement of purpose and scope.
- Build the recorder and run one scripted pass per family against an operator-supplied estate (Phase 0).
- Record
VIYA_TARGET_VERSIONas the version of the first estate made available for capture, with its capture date; generate the table and publish the per-family operation count. - Land sasjs/adapter#895 in parallel: it is one file, it stops a credential from reaching a log, and its failure shape becomes the emulator's first case.
Prior art in this estate
- sasjs/adapter#968 - the transport-failure half of adapter#895, which supplies the first failure case for section 9.
- sasjs/cli#1550 - the same "one description, several hosts" argument for the SASjs API, and the pattern this plan copies.
- sasjs/cli#1585 - the mock app stream mounted through
@sasjs/api's dispatcher, which is the shape Phase 3 follows. - sasjs/server#506 - Viya-shaped session state, the closest existing piece of the answer.
docs/viya-runtime.md- theRUN_TIMES=sasviyaproxy, the other implementation of the same interface.
Status
Updated 2026-11-12. Measured against a reachable Viya 4 estate on that date; no estate name, host or credential is recorded here or in the repository.
The recorder landed in sasjs/api #71, and section 8's blocker has moved: an estate answered from this environment on 2026-11-12, so the capture pass ran. #79 records the first real captures and the defect the run found - the write step passed the access token into createFolder's parentFolderPath, so the request left unauthenticated, the estate answered 401, and the step still logged ok. #80 fixes a CI step on main that has been red since #78 by comparing the three builds with each other rather than with a route-count literal.
Captured so far, with every request authorized and every capture passing its own leak verifier:
identities GET /identities/users/@currentUser
compute GET /compute/contexts (list, and filter= by name)
compute POST /compute/contexts/{id}/sessions, GET .../state, DELETE the session
folders GET /folders/folders/@item?path=..., GET .../{id}/members
folders POST /folders/folders, PATCH /folders/folders/{id} (a move to the recycle bin)
files GET /types/types, POST /files/files, PATCH /files/files/{id}
files GET /files/files/{id}/content, GET /files/files/{id}, PUT .../{id}/content
Three contract facts the captures established that the family list in section 5 did not anticipate:
- File creation is three requests, not one:
/types/typesfor the extension's type definition, thenPOST, thenPATCHwith the properties. deleteFolderis aPATCHthat moves the folder into the recycle bin, and the client reads@myRecycleBinon the way. An emulator answeringDELETEwould be answering a call nobody makes.- Every file read and replace lists the parent folder's members first, so a member list sits on the path of the file family.
Not captured yet, and each is a separate pass: job execution (/jobExecution/jobs, submit, poll, log, output), the app stream (publish and serve), and uploadFile's multipart path. They need a deployed service on the estate, which the first pass did not assume.
Correction to section 3's scope, on the evidence rather than on Viya's route list: deleting a file is not part of @sasjs/adapter's surface - the client creates, reads and replaces, and never removes - so /files/files/{id} with DELETE is out of scope by decision 3, and the recorder's own cleanup of the file it created is marked cleanup and kept out of the table.
The execute family is captured too: executeScript with the log, which is also where the pinned version comes from. %put &=sysvlong;, run through the adapter in Compute Reusable, returns:
V.04.00M0P100526
captured 2026-11-12. That is the value VIYA_TARGET_VERSION pins to, and the value decision 12's header names.
Decision 12 is settled: one header, one fact - x-sasjs-emulated: [email protected]. Decision 13 (where the seed lives) is the remaining open one, and it is visible in every table the generator emits.
One behaviour of the recorder's own client that a host will meet: @sasjs/adapter 6.0.2 treats a brand-new one-hour token as expiring - its refresh margin equals the estate's TTL - so executeScript refreshes before submitting, and that refresh rotates the token pair. A host answering the emulated token endpoint has to answer the refresh grant, not only the password grant.
The scope guard is the other half of Phase 0's exit criterion, and it is open as sasjs/api#81. It measures the client rather than asserting the surface: tools/viya-capture/surface.mjs reads the installed adapter and lists every path literal it builds (56 from 6.0.2), spec/viya/adapter-paths.json is that list committed, and src/viya/surface.ts states in or declined with a reason for each. npm run check:scope holds the inventory against the adapter, and spec/viyaScope.spec.ts holds the declaration against the inventory, in both directions.
Two shapes the measurement produced that a hand-written list would not have: /identities/users/@currentUser is built as a template that starts with the server URL, so an extractor demanding a leading slash misses the call every session begins with; and /compute/sessions/{}/variables/{} and /jobDefinitions/definitions are calls no six-family summary mentions.
Phase 0 is complete as data: sasjs/api#82 generates src/viya/table.ts (21 operations from 8 capture runs), src/corpus/viya/operations.ts (the same capture as replayable cases) and src/viya/unrecorded.ts (the 14 in-scope operations with no capture, each with a reason that names what is missing), and pins VIYA_TARGET_VERSION with its capture date and the header the answers carry. A capture never enters the repository: paths keep their shape, queries keep their names, bodies become type sketches, and a spec fails if an identifier, host or filesystem path reaches the generated files.
The next pass is the one unrecorded family with real work behind it: a deployed job definition, so /jobExecution/jobs and /jobDefinitions/definitions can be captured. After that, Phase 1 - handlers, the overlay and dispatchViya over the same services.
What the token endpoint has to answer
Section 7 states the paths; the grants are decided by the client software, so they are stated here. Measured in @sasjs/cli and @sasjs/adapter rather than assumed:
- password -
sasjs auth login(Viya only) postsgrant_type=passwordto/SASLogon/oauth/tokenfromcli/src/utils/auth.ts, against the secret-lesssas.clipublic client. The CLI's own code, not the adapter's. - authorization_code -
sasjs auth(aliassasjs add cred) sends the user to/SASLogon/oauth/authorize?client_id=...&response_type=codewith a registered client and secret, then exchanges the pasted code. That exchange is the adapter'sgetAccessTokenForViya. - refresh_token - the adapter's
getTokensrefreshes with the configured client, falling back tosas.cliwith an empty secret, so a token minted byauth logincan be refreshed.refreshTokensForViyapostsgrant_type=refresh_token. - client_credentials - nothing in either package asks for it. It is out of scope by decision 3, which is worth stating because a family list written from Viya's surface would have included it.
The recorder now captures the refresh grant (against sas.cli) and the table carries POST /SASLogon/oauth/token; the password and authorization-code grants are the two the emulated endpoint has to implement without a capture, because obtaining either needs a browser (a code) or a password the recorder is not given.
A SASJS target needs none of this: the CLI reproduces the server's own sign-in (GET / for the XSRF cookie, POST /SASLogon/login, POST /SASLogon/authorize, POST /SASjsApi/auth/token), which is section 7's shared-POST split seen from the client side.
The seed is the target's serverUrl
Decision 13 asked whether the seed lives in the target configuration or in a project file. It needs neither: the seed is derived from the target's serverUrl, because the URL already identifies which estate a target talks to, and two targets pointing at one emulator are pointing at one estate.
seedOf(target) = target.seed ?? normalise(target.serverUrl) ?? EMULATED_DEFAULT
normalise: lowercase host, drop a default port (80/443), drop a trailing slash, keep a non-default port
seed therefore becomes an optional override rather than a required field, for the two cases where an address is the wrong identity: the same emulator reached at another host (localhost versus a container name), and a port that moves between runs (an ephemeral CI port would otherwise change the data and invalidate every screenshot the seed exists to keep true).
The cost is deliberate: one emulator serves one dataset, so two suites that want different fixtures need two emulators. An address that gave different callers different rows would be the surprising behaviour, and appLoc is not mixed in either - moving an app to another folder must not change its data.
Snapshot
A one-week snapshot of this text is at https://paste.4gl.io/?c3eab5bf1d7e703c#DMDFvw9yv3WD1unNRCjZdyL1f2wcQCdDkJQ1fXqzqf5n - this issue is the authority.
- Lenguaje dominante
- TypeScript
- Estrellas
- 22
- Forks
- 3
- Merge medio
- 1 h 47 min
- PR fusionados (30 d)
- 200
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Issues similares
-
idea-scout idea:453
Dificultad 2/5 1-3 horas Aptitud para principiantes 83/100
jeanibarz/agent-notebook#453 ·
Los mantenedores suelen responder en 1 día
-
level/task
Dificultad 1/5 Menos de una hora Aptitud para principiantes 95/100
wazuh/wazuh-dashboard-plugins#9642 ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 91/100
unicef/adt-studio#1112 ·
Los mantenedores suelen responder en 2 días
-
feature request good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
TabularisDB/tabularis#1513 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 75/100