From c46bf679fbc93c82889058bfef3a8ab9930d474a Mon Sep 17 00:00:00 2001 From: w2 Date: Thu, 22 Oct 2026 15:59:34 +0000 Subject: [PATCH 1/2] feat: the shared app release workflow, and the org community files in Spanish app-release.yml builds and packs on pull requests. On a v* tag it checks the tag against info.xml and the vendored aps-common copies, then signs SHA256SUMS in the caller's release environment, proves it against the suite's apps.pub and publishes four fixed-name assets. scripts/pack-app.sh does the packing, signing and checks; its self-test and a fixture caller run in ci.yml with actionlint and zizmor. The community files follow the docs-SSOT T06 ruling: a short Spanish profile, CONTRIBUTING, SECURITY, Error/Tarea forms and a four-check PR template. Refs APS-Conecta/instalador#31 Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .github/ISSUE_TEMPLATE/config.yml | 7 - .github/ISSUE_TEMPLATE/dev-task.yml | 40 ---- .github/ISSUE_TEMPLATE/error.yml | 16 ++ .github/ISSUE_TEMPLATE/owner-task.yml | 28 --- .github/ISSUE_TEMPLATE/tarea.yml | 14 ++ .github/PULL_REQUEST_TEMPLATE.md | 25 +-- .github/actionlint.yaml | 5 + .github/workflows/app-release.yml | 162 +++++++++++++++ .github/workflows/ci.yml | 56 ++++++ CONTRIBUTING.md | 70 +------ SECURITY.md | 45 +---- docs/agents/triage-labels.md | 2 - profile/README.md | 70 +------ scripts/pack-app.sh | 279 ++++++++++++++++++++++++++ tests/fixture/.gitignore | 2 + tests/fixture/appinfo/info.xml | 17 ++ tests/fixture/package-lock.json | 10 + tests/fixture/package.json | 7 + tests/fixture/src/main.js | 1 + tests/fixture/tests/main.test.js | 1 + 20 files changed, 580 insertions(+), 277 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/dev-task.yml create mode 100644 .github/ISSUE_TEMPLATE/error.yml delete mode 100644 .github/ISSUE_TEMPLATE/owner-task.yml create mode 100644 .github/ISSUE_TEMPLATE/tarea.yml create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/app-release.yml create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/pack-app.sh create mode 100644 tests/fixture/.gitignore create mode 100644 tests/fixture/appinfo/info.xml create mode 100644 tests/fixture/package-lock.json create mode 100644 tests/fixture/package.json create mode 100644 tests/fixture/src/main.js create mode 100644 tests/fixture/tests/main.test.js diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index de1fae7..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: Cómo trabajamos / How we work - # The ORG copy, not .github's. Most repositories deliberately have no local CONTRIBUTING.md — they - # inherit this one (ADR-0012 in gestion) — so .github pointed at a file that will never exist. - url: https://github.com/APS-Conecta/.github/blob/main/CONTRIBUTING.md - about: Workflow, PR gate, Conventional Commits, and documentation rules. diff --git a/.github/ISSUE_TEMPLATE/dev-task.yml b/.github/ISSUE_TEMPLATE/dev-task.yml deleted file mode 100644 index 3c60331..0000000 --- a/.github/ISSUE_TEMPLATE/dev-task.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Dev task / bug -description: Technical task or bug for the developers (references epics/stories/ADRs; assumes fluency with this repository's own gate). -title: "[dev]: " -body: - - type: dropdown - id: type - attributes: - label: Type - options: - - feature - - bug - - refactor - - chore - - test - validations: - required: true - - type: textarea - id: summary - attributes: - label: Summary - description: What needs to happen, technically. - validations: - required: true - - type: input - id: refs - attributes: - label: References - description: Related epic / story / ADR. Name the repository if the ADR is not in this one. - - type: textarea - id: repro - attributes: - label: Steps to reproduce (bugs only) - description: Exact commands + expected vs actual. Say where each runs (host / which container / which user). - - type: textarea - id: acceptance - attributes: - label: Acceptance criteria - description: How we'll know it's done (Given/When/Then welcome). Name the gate that proves it, green. - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/error.yml b/.github/ISSUE_TEMPLATE/error.yml new file mode 100644 index 0000000..b037e5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/error.yml @@ -0,0 +1,16 @@ +name: Error +description: Algo no funciona como debería. +type: Bug +body: + - type: textarea + id: que-ocurrio + attributes: + label: ¿Qué ocurrió? + description: Qué hizo, qué esperaba y qué pasó. + validations: + required: true + - type: textarea + id: pasos + attributes: + label: Pasos para reproducirlo + description: Los comandos exactos y dónde se ejecutan (servidor, contenedor o navegador). diff --git a/.github/ISSUE_TEMPLATE/owner-task.yml b/.github/ISSUE_TEMPLATE/owner-task.yml deleted file mode 100644 index 3ea0029..0000000 --- a/.github/ISSUE_TEMPLATE/owner-task.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Solicitud (lenguaje simple) / Plain-language request -description: Para pedir un cambio sin lenguaje técnico. Request a change without code jargon. -title: "[solicitud]: " -body: - - type: textarea - id: what - attributes: - label: ¿Qué debe cambiar? / What should change? - description: Descríbelo en palabras simples. Describe it in plain words. - validations: - required: true - - type: textarea - id: why - attributes: - label: ¿Por qué? ¿Para qué sirve? / Why? What is it for? - description: Qué problema resuelve. What problem it solves. - - type: textarea - id: done - attributes: - label: ¿Cómo sabremos que está listo? / How do we know it's done? - description: Cómo se ve cuando funciona (aceptación / UAT). What "done" looks like (acceptance / UAT). - validations: - required: true - - type: input - id: who - attributes: - label: ¿Quién debería revisarlo? / Who should look at this? - description: Opcional. Optional. diff --git a/.github/ISSUE_TEMPLATE/tarea.yml b/.github/ISSUE_TEMPLATE/tarea.yml new file mode 100644 index 0000000..f6ec63f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tarea.yml @@ -0,0 +1,14 @@ +name: Tarea +description: Un trabajo por hacer. +type: Task +body: + - type: textarea + id: que-hacer + attributes: + label: ¿Qué hay que hacer? + validations: + required: true + - type: textarea + id: listo + attributes: + label: ¿Cómo sabremos que está listo? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3281099..3ce7af2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,21 +1,4 @@ - - -## What & why - - - -Closes # - -## Checklist - -- [ ] Title follows **Conventional Commits** (`feat:`, `fix:`, `docs:`, `chore:`, `test:`…). -- [ ] The repository's own gate passes locally — the same one CI runs. -- [ ] Docs updated where a fact changed (one owner per fact — no duplication). -- [ ] Small and reviewable; **1 approval** before merge. - -## AI involvement - - - -- [ ] This PR was written **with AI assistance** → labeled `ai-assisted`. -- [ ] Human-reviewed. +- [ ] El título está en inglés y sigue Conventional Commits. +- [ ] La compuerta del repositorio está en verde, o el PR indica qué no se ejecutó. +- [ ] El sitio de documentación está al día si cambió un hecho. +- [ ] Lleva la etiqueta `ai-assisted` si una IA ayudó. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..2bd2c10 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +# actionlint 1.7.12 predates the documented job.workflow_* context, which app-release.yml reads. +paths: + .github/workflows/app-release.yml: + ignore: + - 'property "workflow_(repository|sha|ref)" is not defined' diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml new file mode 100644 index 0000000..683f3a3 --- /dev/null +++ b/.github/workflows/app-release.yml @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: 2026 APS Conecta +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# The shared app release. An app repo calls it, pinned by commit SHA, on pull requests and on v* tags: +# +# on: +# pull_request: +# push: +# tags: ["v*"] +# jobs: +# release: +# uses: APS-Conecta/.github/.github/workflows/app-release.yml@ # +# permissions: +# contents: write +# with: +# app-id: +# secrets: +# APP_SIGNING_KEY: ${{ secrets.APP_SIGNING_KEY }} +# APP_SIGNING_KEY_PASSWORD: ${{ secrets.APP_SIGNING_KEY_PASSWORD }} +# +# A pull request builds and packs. A v* tag also checks the tag against info.xml and the vendored +# aps-common copies, then, inside the caller's protected `release` environment (the app key's only +# home), signs SHA256SUMS, proves it against the suite's apps.pub and publishes +# .tar.gz, info.xml, SHA256SUMS and SHA256SUMS.minisig. The build job never sees the key. +name: App release + +on: + workflow_call: + inputs: + app-id: + description: The app id, as in appinfo/info.xml. + required: true + type: string + app-dir: + description: The app's directory in the repo. + default: "." + type: string + node-version: + description: The Node.js version the build runs on. + default: "24" + type: string + build: + description: The build command, run in app-dir. + default: npm ci && npm run build + type: string + publish: + description: Publish on a v* tag. False only builds and packs. + default: true + type: boolean + secrets: + APP_SIGNING_KEY: + description: The app key (the .sec file's contents), from the caller's release environment. + required: false + APP_SIGNING_KEY_PASSWORD: + description: The app key's passphrase. + required: false + +permissions: {} + +jobs: + build: + name: Build and pack + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + env: + APP_ID: ${{ inputs.app-id }} + APP_DIR: app/${{ inputs.app-dir }} + TAG: ${{ github.ref_name }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: app + persist-credentials: false + - name: Check out the packer at this workflow's own commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: tools + persist-credentials: false + - name: The tag is v + info.xml's version + if: github.ref_type == 'tag' + run: bash tools/scripts/pack-app.sh tag "$APP_DIR" "$TAG" + - name: The vendored aps-common copies equal aps-common + if: github.ref_type == 'tag' + run: bash tools/scripts/pack-app.sh parity "$APP_DIR" + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: ${{ inputs.node-version }} + package-manager-cache: false + - name: Build + working-directory: app/${{ inputs.app-dir }} + env: + BUILD: ${{ inputs.build }} + run: bash -euo pipefail -c "$BUILD" + - name: Pack + run: bash tools/scripts/pack-app.sh pack "$APP_ID" "$APP_DIR" dist + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: app-release + path: dist/ + if-no-files-found: error + + publish: + name: Sign and publish + needs: build + if: inputs.publish && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + runs-on: ubuntu-24.04 + timeout-minutes: 15 + environment: release + permissions: + contents: write + env: + APP_ID: ${{ inputs.app-id }} + TAG: ${{ github.ref_name }} + steps: + - name: The caller pins this workflow by commit SHA + env: + WORKFLOW_REF: ${{ job.workflow_ref }} + run: | + [[ "$WORKFLOW_REF" =~ @[0-9a-f]{40}$ ]] \ + || { echo "::error::call app-release.yml by commit SHA, not @${WORKFLOW_REF##*@}"; exit 1; } + - name: Check out the packer at this workflow's own commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: tools + persist-credentials: false + - name: Check out apps.pub, the app key the installer trusts + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: APS-Conecta/instalador + ref: main + path: suite + sparse-checkout: apps.pub + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Install signify + run: | + sudo apt-get update -qq && sudo apt-get install -y signify-openbsd + command -v signify >/dev/null || sudo ln -sf "$(command -v signify-openbsd)" /usr/local/bin/signify + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: app-release + path: dist + - name: Sign and prove against apps.pub + env: + APP_SIGNING_KEY: ${{ secrets.APP_SIGNING_KEY }} + APP_SIGNING_KEY_PASSWORD: ${{ secrets.APP_SIGNING_KEY_PASSWORD }} + run: bash tools/scripts/pack-app.sh sign dist suite/apps.pub + - name: Publish the release, latest only for the highest version + env: + GH_TOKEN: ${{ github.token }} + run: | + top="$(gh api --paginate "repos/$GITHUB_REPOSITORY/tags" --jq '.[].name' \ + | grep -E '^v[0-9]+(\.[0-9]+)+$' | sort -V | tail -n 1)" + latest=false; [ "$top" = "$TAG" ] && latest=true + gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --latest="$latest" \ + "dist/$APP_ID.tar.gz" dist/info.xml dist/SHA256SUMS dist/SHA256SUMS.minisig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46a60c7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2026 APS Conecta +# SPDX-License-Identifier: AGPL-3.0-or-later +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + self-test: + name: Packer self-test + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install signify + run: | + sudo apt-get update -qq && sudo apt-get install -y signify-openbsd + command -v signify >/dev/null || sudo ln -sf "$(command -v signify-openbsd)" /usr/local/bin/signify + - run: shellcheck scripts/pack-app.sh + - run: bash scripts/pack-app.sh --self-test + + lint: + name: actionlint and zizmor + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: actionlint 1.7.12 + run: | + curl -fsSLo "$RUNNER_TEMP/actionlint.tar.gz" \ + https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz + echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $RUNNER_TEMP/actionlint.tar.gz" | sha256sum -c - + tar -xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$RUNNER_TEMP" actionlint + "$RUNNER_TEMP/actionlint" + - name: zizmor 1.31.1 + run: pipx run zizmor==1.31.1 .github/workflows + + fixture: + name: Fixture caller + # zizmor prefers $/ over ./ here; actionlint 1.7.12 cannot parse $/ yet. + uses: ./.github/workflows/app-release.yml # zizmor: ignore[self-repository] + permissions: + contents: write # GitHub checks the nested publish job's grant before its if:; publish: false skips it + with: + app-id: fixture + app-dir: tests/fixture + publish: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2c70c9..7585941 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,69 +1 @@ -# Contributing — APS-Conecta - -Organisation-wide contribution contract. It applies to every `APS-Conecta` repository unless that -repository ships its own `CONTRIBUTING.md`, which takes precedence. - -The repositories themselves are private. This file is public because GitHub only serves -organisation defaults from a public `.github` repository. - -## Workflow (GitHub Flow) - -1. Branch off the default branch, short-lived: `feat/…`, `fix/…`, `docs/…`, `chore/…`. -2. Commit with [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, - `docs:`, `chore:`, `test:`, `refactor:`. -3. Open a PR. **One human approval before merge**, and treat that as binding whether or not any - tooling stops you. -4. AI-assisted PRs are labelled `ai-assisted` and disclose the involvement in the description. -5. Run the repository's own gate before opening the PR. CI runs the same checks. - -`CODEOWNERS` auto-requests reviewers. Prefer small, reviewable PRs. - -## Issues - -File new work with the templates in `.github/ISSUE_TEMPLATE/` — a technical *Dev task/bug* and a -plain-language *Solicitud*. Use the plain-language form when you are describing a need rather than -a diagnosis; translating it into technical terms is the developer's job, not yours. - -## Documentation rules - -Documentation must let someone **rebuild** the system, not just read about it. - -1. Numbered steps, one action each — the exact copyable command. -2. Every step states its expected output and what to do if it fails. A step you cannot verify is - not a step. -3. Every command says where it runs — the host, or which container and as which user. -4. Never assert what you have not run. If it is untested, the document says so *there*, not in a - preface. -5. One owner per fact. Others link; they do not repeat. A second copy desyncs the day it is written. -6. Prefer generated over hand-written. -7. Do not copy a gate's count into prose — say what the gate proves and let it print the number. - -If a document mentions something retired, it must acknowledge that it is. Dated history — ADRs, -changelogs — is exempt; its date is the label. - -## Decisions - -Architectural decisions are recorded as ADRs under `docs/adr/`, following -[MADR](https://adr.github.io/madr/). `Status:` is mandatory. Superseded ADRs stay in place and link -forward — a deleted ADR is a deleted reason. - -A number is unique inside its own repository and means nothing outside one, so numbers are never -reused and never renumbered, and a series may carry permanent gaps. Cite an ADR from another -repository by naming that repository, with an absolute URL — never a relative path, which depends on -where someone happened to clone things. A decision affecting the whole organisation belongs in the -trunk repository rather than in the first product that needed it. - -## Licensing - -Everything `APS-Conecta` produces is **AGPL-3.0-or-later**. The dependencies it builds on are open source -and self-hosted, and because the Nextcloud apps link `@nextcloud/vue` the obligation is inherited -rather than chosen. Contributions are accepted on that basis: by contributing, you license your work -under the same terms. - -Every repository carries one identical `LICENSE`, and the same identifier must appear in -`appinfo/info.xml`, `composer.json` and `package.json` wherever those exist. The brand marks — logo, -mono logo, lockup and favicon — are reserved under AGPL §7(e); the licence does not hand them over. - -## Security - -Do not open a public issue for a security problem. See [`SECURITY.md`](SECURITY.md). +Las reglas para contribuir están en [Calidad](https://aps-conecta.github.io/documentation/desarrollo/calidad.html) y [Documentación](https://aps-conecta.github.io/documentation/desarrollo/documentation.html). diff --git a/SECURITY.md b/SECURITY.md index 8f6a1c4..e83e1d9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,42 +1,5 @@ -# Security policy — APS-Conecta +# Política de seguridad -Organisation-wide policy. It applies to every `APS-Conecta` repository unless that repository ships its -own `SECURITY.md`, which takes precedence. - -## Scope - -`APS-Conecta` builds internal-operations software for Chilean CESFAMs (primary-healthcare centres): -documents, coordination and team processes. Each install serves one establishment, named in that -install's configuration; the software itself names none. - -**These systems hold no patient or clinical data.** Development uses synthetic fixtures only. They -are not clinical or patient-record systems, and are not intended to become one. - -The code repositories are private. Only this organisation profile is public. - -## Reporting a vulnerability - -**Do not open a public issue for a security problem.** - -Report privately through GitHub's **Report a vulnerability** button on the affected repository -(Security → Advisories). If you cannot reach the repository — it is private and you are not a -member — report it through the contact on the [organisation profile](https://github.com/APS-Conecta). - -Please include what you did, what happened, and what you expected. A proof of concept helps. -We acknowledge reports as fast as a very small team can, and we will tell you honestly if a fix -will take a while. - -## Out of scope - -Reports about the following will be closed as intentional: - -- Findings against a **local development stack** — it is bound to loopback, runs synthetic data, - and is deliberately convenient rather than hardened. It is not a deployment. -- Vulnerabilities in third-party open-source components, unless our configuration is what makes - them exploitable. Report those upstream; tell us too, and we will track the update. -- Missing hardening on repositories that are private and have no external surface. - -## Disclosure - -We ask for coordinated disclosure: give us a chance to ship a fix before publishing. We will credit -you unless you ask us not to. +No abra un issue público para informar un problema de seguridad. Use «Report a vulnerability» (Security → Advisories) en el repositorio público afectado, o en APS-Conecta/instalador si el código no es público. +Informe los problemas de componentes de terceros a su proyecto de origen. +La divulgación es coordinada. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md index 0855345..53b50c9 100644 --- a/docs/agents/triage-labels.md +++ b/docs/agents/triage-labels.md @@ -11,5 +11,3 @@ The skills speak in terms of five canonical triage roles. This file maps those r | `wontfix` | `wontfix` | Will not be actioned | When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. - -Edit the "Label in our tracker" column to match whatever vocabulary you actually use. diff --git a/profile/README.md b/profile/README.md index ac46737..9d28ea3 100644 --- a/profile/README.md +++ b/profile/README.md @@ -1,71 +1,3 @@ # APS Conecta -**Herramientas de software para la gestión interna de centros de salud familiar (CESFAM) en Chile.** -_Software tools for the internal management of Chilean primary-healthcare centres (CESFAM)._ - ---- - -## 🇨🇱 Español - -**APS Conecta** desarrolla software para ordenar la **gestión interna** de un centro de salud familiar -(CESFAM): documentos, coordinación y procesos del equipo. Cada instalación atiende a **un -establecimiento**, definido en la configuración de esa instalación; el software no nombra a ninguno. - -**El problema que resuelve.** La información suele vivir dispersa —en WhatsApp, correos, pendrives y -carpetas sueltas— y cada dato en su propio archivo. Eso hace que no exista una versión única y confiable, -y que la información no se pueda buscar ni analizar. APS Conecta la convierte en un espacio de trabajo -**único, ordenado, buscable y editable en conjunto**. - -**Cómo lo hace.** - -- **Construido sobre software libre y de código abierto**, **auto-alojado** — sin licencias pagadas. -- Centrado en la **privacidad**: pensado para operación interna. **No** es una ficha clínica y no - almacena datos de pacientes. -- Construido sobre plataformas abiertas y consolidadas, no desde cero. - -**Licencia.** El código propio de APS Conecta es **software libre**, bajo -**[AGPL-3.0-or-later](https://www.gnu.org/licenses/agpl-3.0.html)**. Los repositorios son privados, -pero la licencia es *copyleft*: quien recibe el software, o lo usa a través de la red, tiene derecho a -pedir el código fuente y a modificarlo y redistribuirlo, y ese derecho se respeta. La **identidad -visual** —logo, logo monocromo, lockup y favicon— queda reservada como marca según el artículo 7(e) -de la AGPL: la licencia del código no la cede. - -**Cómo trabaja esta organización.** APS Conecta lo desarrolla una sola persona. El perfil de la -organización es público; los repositorios se mantienen **privados**. Las reglas de colaboración están -en [`CONTRIBUTING.md`](https://github.com/APS-Conecta/.github/blob/main/CONTRIBUTING.md) y la -política de seguridad en [`SECURITY.md`](https://github.com/APS-Conecta/.github/blob/main/SECURITY.md). - -🔗 **[apsconecta.cl](https://apsconecta.cl)** — *sitio público de la organización, en línea.* - ---- - -## 🇬🇧 English - -**APS Conecta** builds software to organize the **internal operations** of a Chilean primary-healthcare -centre (CESFAM): the team's documents, coordination, and processes. Each install serves **one -establishment**, named in that install's configuration; the software itself names none. - -**The problem it solves.** Information tends to live scattered — across WhatsApp, email, USB drives, and -loose folders — each fact in its own file. There's no single trusted version, and nothing can be searched -or analyzed. APS Conecta turns it into **one organized, searchable, collaboratively-editable** workspace. - -**How it works.** - -- **Built on free and open-source**, **self-hosted** software — no paid licenses. -- **Privacy-first**: built for internal operations. It is **not** a patient-record system and holds no - clinical data. -- Built on established open platforms rather than from scratch. - -**Licensing.** APS Conecta's own code is **free software**, under -**[AGPL-3.0-or-later](https://www.gnu.org/licenses/agpl-3.0.html)**. The repositories are private, but -the licence is *copyleft*: anyone who receives the software, or uses it over a network, is entitled to -request the source and to modify and redistribute it — and that entitlement is honoured. The **visual -identity** — logo, mono logo, lockup and favicon — is reserved as a mark under AGPL section 7(e): the -code licence does not hand it over. - -**How this organization works.** APS Conecta is built by one person. The organization profile is -public; the repositories stay **private**. Collaboration rules live in -[`CONTRIBUTING.md`](https://github.com/APS-Conecta/.github/blob/main/CONTRIBUTING.md), and the -security policy in [`SECURITY.md`](https://github.com/APS-Conecta/.github/blob/main/SECURITY.md). - -🔗 **[apsconecta.cl](https://apsconecta.cl)** — *the organization's public site, online.* +[Documentación](https://aps-conecta.github.io/documentation/) · [Licencias y marcas](https://aps-conecta.github.io/documentation/aviso.html) · [Seguridad](https://github.com/APS-Conecta/.github/blob/main/SECURITY.md) diff --git a/scripts/pack-app.sh b/scripts/pack-app.sh new file mode 100644 index 0000000..782ded0 --- /dev/null +++ b/scripts/pack-app.sh @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 APS Conecta +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Packs, signs and checks one app release for .github/workflows/app-release.yml. +# +# pack-app.sh tag DIR TAG refuse unless TAG is v + DIR/appinfo/info.xml's version +# pack-app.sh parity DIR DIR's vendored aps-common copies equal aps-common main +# pack-app.sh pack ID DIR OUT OUT/ID.tar.gz, OUT/info.xml and OUT/SHA256SUMS from DIR, after the build +# pack-app.sh sign OUT PUB OUT/SHA256SUMS.minisig, proven against PUB +# pack-app.sh --self-test hermetic: fixture repos and throwaway keys, no network +# +# sign reads the app key from APP_SIGNING_KEY (the .sec file's contents) and its passphrase from +# APP_SIGNING_KEY_PASSWORD ("" for a key made with signify -G -n). APS_COMMON_URL overrides the +# aps-common clone URL. +set -euo pipefail + +SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" + +die() { echo "pack-app: $*" >&2; exit 1; } + +# info_field FILE NAME — the text of a top-level element of an info.xml +info_field() { + python3 -c 'import sys, xml.etree.ElementTree as E; print((E.parse(sys.argv[1]).getroot().findtext(sys.argv[2]) or "").strip())' "$1" "$2" +} + +# scratch — a temp dir in $work, removed on exit +scratch() { work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT; } + +tag() { + local dir="$1" ref="$2" version + [ -f "$dir/appinfo/info.xml" ] || die "$dir/appinfo/info.xml not found" + version="$(info_field "$dir/appinfo/info.xml" version)" + [[ "$version" =~ ^[0-9]+(\.[0-9]+)+$ ]] || die "info.xml version '$version' is not dotted numbers" + [ "$ref" = "v$version" ] || die "tag $ref is not v$version, info.xml's version" +} + +# The runtime files: everything but these, unless the app ships a .nextcloudignore, which replaces +# them. Both use rsync rules, anchored at the app root ("/src" never drops vendor/aps/common/src). +DEFAULT_EXCLUDES='+ /.htaccess +- /.* +- /src +- /tests +- /e2e +- /docs +- /screenshots +- /compose*.yaml +- /compose*.yml +- /Dockerfile* +- /AGENTS.md +- /CLAUDE.md +- /CONTEXT.md +- /Makefile +- /package.json +- /package-lock.json +- /composer.json +- /composer.lock +- /*.config.js +- /*.config.mjs +- /*.config.ts +- /phpunit*.xml +- /phpunit*.xml.dist +- /phpstan* +- /psalm.xml +- /appinfo/signature.json' + +# pack ID DIR OUT — stage DIR's runtime files as ID/, tar them deterministically, write SHA256SUMS +pack() { + local id="$1" dir="$2" out="$3" ts rules tracked list + [ -f "$dir/appinfo/info.xml" ] || die "$dir/appinfo/info.xml not found" + [ "$(info_field "$dir/appinfo/info.xml" id)" = "$id" ] || die "info.xml's is not $id" + tracked="$(git -C "$dir" ls-files -- ':(glob)js/*.js' ':(glob)js/*.mjs')" + [ -z "$tracked" ] || die "the bundle is committed (${tracked//$'\n'/ }); the release builds it — remove it from git" + mkdir -p "$out" + [ -z "$(ls -A "$out")" ] || die "$out is not empty" + scratch + rules="$work/rules" + if [ -f "$dir/.nextcloudignore" ]; then + ! grep -q '^!' "$dir/.nextcloudignore" \ + || die ".nextcloudignore: '!' lines are not supported (rsync rules; rsync would drop the path silently)" + cp "$dir/.nextcloudignore" "$rules" + else + printf '%s\n' "$DEFAULT_EXCLUDES" > "$rules" + fi + rsync -a --filter='- .git' --filter='- node_modules' --exclude-from="$rules" "$dir/" "$work/$id/" + ts="$(git -C "$dir" log -1 --format=%ct)" + tar --sort=name --mtime="@$ts" --owner=0 --group=0 --numeric-owner --format=gnu \ + --mode='u+w,go-w,a+rX' -C "$work" -c "$id" | gzip -n > "$work/$id.tar.gz" + + list="$(tar -tzf "$work/$id.tar.gz")" + ! grep -qv "^$id/" <<<"$list" || die "a member sits outside $id/" + ! grep -qx "$id/appinfo/signature.json" <<<"$list" \ + || die "appinfo/signature.json is in the tarball; Nextcloud's integrity check would flag the app" + grep -qE "^$id/js/[^/]+\.m?js$" <<<"$list" || die "no bundle: $id/js/ has no .js or .mjs file after the build" + tar -xOzf "$work/$id.tar.gz" "$id/appinfo/info.xml" | cmp -s - "$dir/appinfo/info.xml" \ + || die "the tarball's info.xml is not the app's" + + mv "$work/$id.tar.gz" "$out/$id.tar.gz" + cp "$dir/appinfo/info.xml" "$out/info.xml" + ( cd "$out" && sha256sum "$id.tar.gz" info.xml > SHA256SUMS ) +} + +# parity DIR — the aps-common copies DIR vendors (the layout of every app's `make aps-sync`) equal +# aps-common's main; an app that vendors none passes without cloning it +parity() { + local dir="$1" common bad=0 + if [ ! -e "$dir/vendor/aps/common/src" ] && [ ! -e "$dir/src/aps" ] && [ ! -e "$dir/tests/stubs/OC/Hooks/Emitter.php" ]; then + echo "pack-app: no vendored aps-common copy"; return 0 + fi + scratch + common="$work/aps-common" + git clone -q --depth 1 "${APS_COMMON_URL:-https://github.com/APS-Conecta/aps-common.git}" "$common" \ + || die "cannot clone aps-common" + if [ -e "$dir/vendor/aps/common/src" ]; then diff -r "$common/src" "$dir/vendor/aps/common/src" || bad=1; fi + if [ -e "$dir/src/aps" ]; then diff -r --exclude='*.test.js' "$common/js" "$dir/src/aps" || bad=1; fi + if [ -e "$dir/tests/stubs/OC/Hooks/Emitter.php" ]; then + diff "$common/stub/OC/Hooks/Emitter.php" "$dir/tests/stubs/OC/Hooks/Emitter.php" || bad=1 + fi + [ "$bad" -eq 0 ] || die "vendored aps-common copies differ from aps-common main; run 'make aps-sync' and commit" +} + +# sign OUT PUB — sign OUT/SHA256SUMS with the app key, then prove it against PUB (apps.pub) +sign() { + local out="$1" pub="$2" + [ -n "${APP_SIGNING_KEY:-}" ] || die "APP_SIGNING_KEY is empty; the caller passes it from its release environment" + scratch + ( umask 077 && printf '%s\n' "$APP_SIGNING_KEY" > "$work/apps.sec" ) + printf '%s\n' "${APP_SIGNING_KEY_PASSWORD:-}" \ + | signify -S -s "$work/apps.sec" -m "$out/SHA256SUMS" -x "$out/SHA256SUMS.minisig" \ + || { rm -f "$out/SHA256SUMS.minisig"; die "signing failed: wrong APP_SIGNING_KEY or APP_SIGNING_KEY_PASSWORD"; } + signify -V -q -p "$pub" -m "$out/SHA256SUMS" -x "$out/SHA256SUMS.minisig" \ + || die "the signature does not verify against $pub; the key is not the app key the installer trusts" + ( cd "$out" && sha256sum -c --quiet SHA256SUMS ) || die "SHA256SUMS does not match the files beside it" +} + +# shellcheck disable=SC2016,SC2034,SC2317 # check's payloads are eval'd single-quoted strings +if [ "${1:-}" = "--self-test" ]; then + for c in git rsync signify python3; do command -v "$c" >/dev/null || die "self-test: $c missing"; done + tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT + n=0; failed=0 + check() { n=$((n + 1)); if eval "$2"; then :; else echo " FAIL: $1" >&2; failed=$((failed + 1)); fi; } + run() { rc=0; bash "$SELF" "$@" >/dev/null 2>&1 || rc=$?; } + + # mk_app DIR VERSION — a committed fixture app, id demo, laid out like the org's apps + mk_app() { + mkdir -p "$1/appinfo" "$1/lib" "$1/src" "$1/tests" "$1/docs" "$1/.github" \ + "$1/vendor/aps/common/src" "$1/templates" + printf '\n\n demo\n %s\n\n' "$2" > "$1/appinfo/info.xml" + echo ' "$1/lib/Application.php" + echo ' "$1/vendor/aps/common/src/Text.php" + echo ' "$1/templates/main.php" + echo 'main' > "$1/src/main.js" + echo ' "$1/tests/AppTest.php" + echo 'docs' > "$1/docs/index.md" + echo 'ci' > "$1/.github/ci.yml" + echo 'Options -Indexes' > "$1/.htaccess" + echo 'agents' > "$1/AGENTS.md" + echo 'services:' > "$1/compose.yaml" + echo '{}' > "$1/package.json" + printf 'js/\nnode_modules/\n' > "$1/.gitignore" + ( cd "$1" && git init -q && git add -A \ + && git -c user.email=t@test.invalid -c user.name=t commit -qm fixture ) + } + # build DIR — what the caller's build step leaves behind: the bundle and node_modules + build() { mkdir -p "$1/js" "$1/node_modules/dep"; echo 'bundle' > "$1/js/demo-main.mjs"; echo 'dep' > "$1/node_modules/dep/index.js"; } + members() { tar -tzf "$1/demo.tar.gz" 2>/dev/null; } + # has OUT PATH... — every PATH is a member under demo/; lacks OUT ERE — no member under demo/ matches + has() { local list f; list="$(members "$1")"; shift + for f in "$@"; do grep -qxF "demo/$f" <<<"$list" || return 1; done; } + lacks() { ! grep -qE "^demo/($2)(/|$)" <<<"$(members "$1")"; } + + mk_app "$tmp/app" 1.2.3 + run tag "$tmp/app" v1.2.3 + check "tag: v + info.xml's version passes" '[ "$rc" -eq 0 ]' + run tag "$tmp/app" v1.2.4 + check "tag: a tag that differs from info.xml's version is refused" '[ "$rc" -ne 0 ]' + mk_app "$tmp/fork" 3.1.7-aps1 + run tag "$tmp/fork" v3.1.7-aps1 + check "tag: a non-numeric version is refused (fork versions stay monotonic)" '[ "$rc" -ne 0 ]' + + build "$tmp/app" + run pack demo "$tmp/app" "$tmp/o1" + check "pack: the tarball, info.xml and SHA256SUMS" \ + '[ "$rc" -eq 0 ] && [ -f "$tmp/o1/demo.tar.gz" ] && [ -f "$tmp/o1/info.xml" ] && [ -f "$tmp/o1/SHA256SUMS" ]' + check "pack: every member sits under demo/" \ + '[ -n "$(members "$tmp/o1")" ] && ! grep -qv "^demo/" <<<"$(members "$tmp/o1")"' + check "pack: info.xml is the app's, byte for byte" 'cmp -s "$tmp/o1/info.xml" "$tmp/app/appinfo/info.xml"' + check "pack: SHA256SUMS names the tarball and info.xml, and verifies" \ + '[ "$(cut -d" " -f3 "$tmp/o1/SHA256SUMS" | tr "\n" " ")" = "demo.tar.gz info.xml " ] \ + && ( cd "$tmp/o1" && sha256sum -c --quiet SHA256SUMS )' + check "defaults: runtime files stay — lib, templates, the bundle, .htaccess, vendored aps-common" \ + 'has "$tmp/o1" lib/Application.php templates/main.php js/demo-main.mjs .htaccess vendor/aps/common/src/Text.php' + check "defaults: dev files go — src, tests, docs, dotfiles, agent files, compose, package.json, node_modules" \ + 'lacks "$tmp/o1" "src|tests|docs|\.github|\.gitignore|AGENTS\.md|compose\.yaml|package\.json|node_modules"' + + # a second clone of the same commit: other mtimes, another umask — the bytes must not move + ( umask 077 && git clone -q "$tmp/app" "$tmp/clone" && build "$tmp/clone" \ + && find "$tmp/clone" -exec touch -d '2001-02-03 04:05:06' {} + ) + run pack demo "$tmp/clone" "$tmp/o2" + check "determinism: another clone of the commit packs the same sha256" \ + '[ "$rc" -eq 0 ] && [ "$(sha256sum < "$tmp/o2/demo.tar.gz")" = "$(sha256sum < "$tmp/o1/demo.tar.gz")" ]' + + printf '/.*\n/docs\n' > "$tmp/clone/.nextcloudignore" + run pack demo "$tmp/clone" "$tmp/o3" + check ".nextcloudignore: it replaces the defaults — docs goes, src and tests stay" \ + '[ "$rc" -eq 0 ] && lacks "$tmp/o3" "docs|\.nextcloudignore" && has "$tmp/o3" src/main.js tests/AppTest.php' + printf '/vendor/*\n!/vendor/aps\n' > "$tmp/clone/.nextcloudignore" + run pack demo "$tmp/clone" "$tmp/o4" + check ".nextcloudignore: a '!' line is refused (rsync would drop vendor/aps silently)" \ + '[ "$rc" -ne 0 ] && [ ! -e "$tmp/o4/demo.tar.gz" ]' + rm "$tmp/clone/.nextcloudignore" + + echo '{}' > "$tmp/clone/appinfo/signature.json"; echo '/.*' > "$tmp/clone/.nextcloudignore" + run pack demo "$tmp/clone" "$tmp/o5" + check "refused: appinfo/signature.json in the tarball (Nextcloud's integrity check would flag the app)" '[ "$rc" -ne 0 ]' + rm "$tmp/clone/appinfo/signature.json" "$tmp/clone/.nextcloudignore" + run pack other "$tmp/clone" "$tmp/o6" + check "refused: an app id that is not info.xml's " '[ "$rc" -ne 0 ]' + git clone -q "$tmp/app" "$tmp/unbuilt" + run pack demo "$tmp/unbuilt" "$tmp/o7" + check "refused: no bundle (js/*.js or js/*.mjs) — the build made nothing" '[ "$rc" -ne 0 ]' + build "$tmp/unbuilt" + ( cd "$tmp/unbuilt" && git add -f js/demo-main.mjs && git -c user.email=t@test.invalid -c user.name=t commit -qm bundle ) + run pack demo "$tmp/unbuilt" "$tmp/o8" + check "refused: a committed bundle — the release builds it" '[ "$rc" -ne 0 ]' + + # throwaway keys: a passphrase-less one, an encrypted one, and an unrelated public key + ( umask 077 && signify -G -n -p "$tmp/plain.pub" -s "$tmp/plain.sec" -c test \ + && printf 'pw\npw\n' | signify -G -p "$tmp/enc.pub" -s "$tmp/enc.sec" -c test >/dev/null \ + && signify -G -n -p "$tmp/other.pub" -s "$tmp/other.sec" -c test ) + # sign_as N SECFILE PASSWORD PUB — sign a fresh copy of o1 as s + sign_as() { cp -r "$tmp/o1" "$tmp/s$1" + rc=0; APP_SIGNING_KEY="$(cat "$2")" APP_SIGNING_KEY_PASSWORD="$3" bash "$SELF" sign "$tmp/s$1" "$4" >/dev/null 2>&1 || rc=$?; } + proven() { ( cd "$tmp/s$1" && signify -V -q -p "$2" -m SHA256SUMS -x SHA256SUMS.minisig ); } + sign_as 1 "$tmp/plain.sec" "" "$tmp/plain.pub" + check "sign: a passphrase-less key signs SHA256SUMS and the signature verifies" '[ "$rc" -eq 0 ] && proven 1 "$tmp/plain.pub"' + sign_as 2 "$tmp/enc.sec" pw "$tmp/enc.pub" + check "sign: an encrypted key signs with its passphrase on stdin" '[ "$rc" -eq 0 ] && proven 2 "$tmp/enc.pub"' + sign_as 3 "$tmp/enc.sec" wrong "$tmp/enc.pub" + check "sign: a wrong passphrase fails and leaves no signature" '[ "$rc" -ne 0 ] && [ ! -e "$tmp/s3/SHA256SUMS.minisig" ]' + sign_as 4 /dev/null "" "$tmp/plain.pub" + check "sign: an empty key fails (the caller did not pass the secret)" '[ "$rc" -ne 0 ]' + sign_as 5 "$tmp/plain.sec" "" "$tmp/other.pub" + check "sign: the proof fails against a public key that is not the signer's" '[ "$rc" -ne 0 ]' + + # an aps-common fixture repo, and an app vendoring all three copies the way `make aps-sync` lays them out + mkdir -p "$tmp/common/src" "$tmp/common/js" "$tmp/common/stub/OC/Hooks" + echo ' "$tmp/common/src/Text.php" + echo 'api' > "$tmp/common/js/api.js" + echo 'test' > "$tmp/common/js/api.test.js" + echo ' "$tmp/common/stub/OC/Hooks/Emitter.php" + ( cd "$tmp/common" && git init -q && git add -A && git -c user.email=t@test.invalid -c user.name=t commit -qm common ) + mkdir -p "$tmp/clone/src/aps" "$tmp/clone/tests/stubs/OC/Hooks" + echo 'api' > "$tmp/clone/src/aps/api.js" + echo ' "$tmp/clone/tests/stubs/OC/Hooks/Emitter.php" + parity_of() { rc=0; APS_COMMON_URL="$2" bash "$SELF" parity "$1" >/dev/null 2>&1 || rc=$?; } + parity_of "$tmp/clone" "$tmp/common" + check "parity: vendored copies equal to aps-common pass" '[ "$rc" -eq 0 ]' + echo ' "$tmp/clone/vendor/aps/common/src/Text.php" + parity_of "$tmp/clone" "$tmp/common" + check "parity: a vendored PHP copy that differs from aps-common fails" '[ "$rc" -ne 0 ]' + echo ' "$tmp/clone/vendor/aps/common/src/Text.php"; echo 'drift' > "$tmp/clone/src/aps/api.js" + parity_of "$tmp/clone" "$tmp/common" + check "parity: a vendored JS copy that differs from aps-common fails" '[ "$rc" -ne 0 ]' + parity_of "$tmp/unbuilt/lib" "$tmp/no-such-repo" + check "parity: an app with no vendored copy passes without cloning aps-common" '[ "$rc" -eq 0 ]' + + echo "self-test: $n checks, $failed failed" + [ "$failed" -eq 0 ] + exit +fi + +case "${1:-}" in + tag) [ $# -eq 3 ] || die "usage: pack-app.sh tag DIR TAG"; tag "$2" "$3" ;; + pack) [ $# -eq 4 ] || die "usage: pack-app.sh pack ID DIR OUT"; pack "$2" "$3" "$4" ;; + sign) [ $# -eq 3 ] || die "usage: pack-app.sh sign OUT PUB"; sign "$2" "$3" ;; + parity) [ $# -eq 2 ] || die "usage: pack-app.sh parity DIR"; parity "$2" ;; + *) die "usage: pack-app.sh tag DIR TAG | pack ID DIR OUT | sign OUT PUB | parity DIR | --self-test" ;; +esac diff --git a/tests/fixture/.gitignore b/tests/fixture/.gitignore new file mode 100644 index 0000000..2c70dce --- /dev/null +++ b/tests/fixture/.gitignore @@ -0,0 +1,2 @@ +js/ +node_modules/ diff --git a/tests/fixture/appinfo/info.xml b/tests/fixture/appinfo/info.xml new file mode 100644 index 0000000..2d7a2dd --- /dev/null +++ b/tests/fixture/appinfo/info.xml @@ -0,0 +1,17 @@ + + + fixture + Fixture + The shared release workflow's fixture app. + The shared release workflow's fixture app. + 1.0.0 + agpl + APS Conecta + Fixture + tools + https://github.com/APS-Conecta/.github/issues + + + + diff --git a/tests/fixture/package-lock.json b/tests/fixture/package-lock.json new file mode 100644 index 0000000..9cdda55 --- /dev/null +++ b/tests/fixture/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "fixture", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fixture" + } + } +} diff --git a/tests/fixture/package.json b/tests/fixture/package.json new file mode 100644 index 0000000..c9216c7 --- /dev/null +++ b/tests/fixture/package.json @@ -0,0 +1,7 @@ +{ + "name": "fixture", + "private": true, + "scripts": { + "build": "mkdir -p js && cp src/main.js js/fixture-main.js" + } +} diff --git a/tests/fixture/src/main.js b/tests/fixture/src/main.js new file mode 100644 index 0000000..8e28519 --- /dev/null +++ b/tests/fixture/src/main.js @@ -0,0 +1 @@ +console.log('fixture') diff --git a/tests/fixture/tests/main.test.js b/tests/fixture/tests/main.test.js new file mode 100644 index 0000000..a5215c2 --- /dev/null +++ b/tests/fixture/tests/main.test.js @@ -0,0 +1 @@ +test('fixture', () => {}) From f4951d7f7fddf2d4b50abbca91a7b397249aa9b1 Mon Sep 17 00:00:00 2001 From: w2 Date: Thu, 22 Oct 2026 16:33:58 +0000 Subject: [PATCH 2/2] docs: the org profile carries the product sentence while the org header keeps the old website --- profile/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profile/README.md b/profile/README.md index 9d28ea3..24d7122 100644 --- a/profile/README.md +++ b/profile/README.md @@ -1,3 +1,3 @@ # APS Conecta -[Documentación](https://aps-conecta.github.io/documentation/) · [Licencias y marcas](https://aps-conecta.github.io/documentation/aviso.html) · [Seguridad](https://github.com/APS-Conecta/.github/blob/main/SECURITY.md) +Plataforma de colaboración para centros de atención primaria de Chile. No guarda datos de pacientes. [Documentación](https://aps-conecta.github.io/documentation/) · [Licencias y marcas](https://aps-conecta.github.io/documentation/aviso.html) · [Seguridad](https://github.com/APS-Conecta/.github/blob/main/SECURITY.md)